Skip to main content

stryke/
parser.rs

1use crate::ast::*;
2use crate::error::{ErrorKind, PerlError, PerlResult};
3use crate::interpreter::Interpreter;
4use crate::lexer::{Lexer, LITERAL_DOLLAR_IN_DQUOTE};
5use crate::token::Token;
6
7/// True when `[` after `expr` is chained array access (`$r->{k}[0]`, `$a[1][2]`, `$$r[0]`).
8/// False for `(sort ...)[0]` / `@{ ... }[i]` — those slice a list value, not an array ref container.
9fn postfix_lbracket_is_arrow_container(expr: &Expr) -> bool {
10    matches!(
11        expr.kind,
12        ExprKind::ArrayElement { .. }
13            | ExprKind::HashElement { .. }
14            | ExprKind::ArrowDeref { .. }
15            | ExprKind::Deref {
16                kind: Sigil::Scalar,
17                ..
18            }
19    )
20}
21
22fn destructure_stmt_from_var_decls(keyword: &str, decls: Vec<VarDecl>, line: usize) -> Statement {
23    let kind = match keyword {
24        "my" => StmtKind::My(decls),
25        "mysync" => StmtKind::MySync(decls),
26        "our" => StmtKind::Our(decls),
27        "local" => StmtKind::Local(decls),
28        "state" => StmtKind::State(decls),
29        _ => unreachable!("parse_my_our_local keyword"),
30    };
31    Statement {
32        label: None,
33        kind,
34        line,
35    }
36}
37
38fn destructure_stmt_die_string(line: usize, msg: &str) -> Statement {
39    Statement {
40        label: None,
41        kind: StmtKind::Expression(Expr {
42            kind: ExprKind::Die(vec![Expr {
43                kind: ExprKind::String(msg.to_string()),
44                line,
45            }]),
46            line,
47        }),
48        line,
49    }
50}
51
52fn destructure_stmt_unless_die(line: usize, cond: Expr, msg: &str) -> Statement {
53    Statement {
54        label: None,
55        kind: StmtKind::Unless {
56            condition: cond,
57            body: vec![destructure_stmt_die_string(line, msg)],
58            else_block: None,
59        },
60        line,
61    }
62}
63
64fn destructure_expr_scalar_tmp(name: &str, line: usize) -> Expr {
65    Expr {
66        kind: ExprKind::ScalarVar(name.to_string()),
67        line,
68    }
69}
70
71fn destructure_expr_array_len(tmp: &str, line: usize) -> Expr {
72    Expr {
73        kind: ExprKind::Deref {
74            expr: Box::new(destructure_expr_scalar_tmp(tmp, line)),
75            kind: Sigil::Array,
76        },
77        line,
78    }
79}
80
81pub struct Parser {
82    tokens: Vec<(Token, usize)>,
83    pos: usize,
84    /// Monotonic slot id for `rate_limit(...)` sliding-window state in the interpreter.
85    next_rate_limit_slot: u32,
86    /// When > 0, `expr` `(` is not parsed as [`ExprKind::IndirectCall`] — e.g. `sort $k (1)` must
87    /// treat `(1)` as the sort list, not `$k(1)`.
88    suppress_indirect_paren_call: u32,
89    /// When > 0, the current expression is being parsed as the RHS of `|>`
90    /// (pipe-forward). Builtins that normally require a list/string/second arg
91    /// (`map`, `grep`, `sort`, `join`, `reverse` / `reversed`, `split`, …) may accept a
92    /// placeholder when this flag is set, because [`Self::pipe_forward_apply`]
93    /// will substitute the piped value in afterwards.
94    pipe_rhs_depth: u32,
95    /// When > 0, [`Self::parse_pipe_forward`] will **not** consume a trailing `|>`
96    /// and leaves it for an outer parser instead. Bumped while parsing paren-less
97    /// arg lists (`parse_list_until_terminator`, paren-less method args, `map`/`grep`
98    /// LIST, …) so `@a |> head 2 |> join "-"` chains left-associatively as
99    /// `(@a |> head 2) |> join "-"` instead of `head` swallowing the outer `|>`
100    /// as part of its first arg. Reset to 0 on entry to any parenthesized
101    /// arg list (`parse_arg_list`) so `head(2 |> foo, 3)` still works.
102    no_pipe_forward_depth: u32,
103    /// When > 0, `{` after a scalar / scalar deref is not `%hash{key}` / `->{}`, so
104    /// `if let` / `while let` scrutinees can be followed by `{ ... }`.
105    suppress_scalar_hash_brace: u32,
106    /// Counter for `while let` / similar desugar temps (`$__while_let_0`, …).
107    next_desugar_tmp: u32,
108    /// Source path for [`PerlError`] (matches lexer / `parse_with_file`).
109    error_file: String,
110    /// User-declared sub names (for allowing UDF to shadow stryke extensions in compat mode).
111    declared_subs: std::collections::HashSet<String>,
112    /// When > 0, `parse_named_expr` will not consume following barewords as paren-less
113    /// function arguments. Used by thread macro to prevent `t Color::Red p` from
114    /// interpreting `p` as an argument to the enum constructor instead of a stage.
115    suppress_parenless_call: u32,
116    /// When > 0, `parse_multiplication` will not consume `Token::Slash` as division.
117    /// Used by thread macro so `/pattern/` is left for the stage parser to handle.
118    suppress_slash_as_div: u32,
119    /// When > 0, the lexer should not interpret `m/`, `s/`, etc. as regex-starters.
120    /// Used by thread macro to prevent `/m/` from being misparsed.
121    pub suppress_m_regex: u32,
122    /// When > 0, `parse_range` will not consume `:` as the short-form range operator.
123    /// Bumped while parsing the then-branch of a ternary `? :` so `a ? b : c` doesn't
124    /// misparse `b : c` as a range.
125    suppress_colon_range: u32,
126    /// When true, `pipe_forward_apply` uses thread-last semantics (append to args)
127    /// instead of thread-first (prepend). Set by `->>` thread macro.
128    thread_last_mode: bool,
129    /// When true, we're parsing a module (via `use`/`require`), not user code.
130    /// Modules are allowed to shadow builtins; user code is not (unless `--compat`).
131    pub parsing_module: bool,
132}
133
134impl Parser {
135    pub fn new(tokens: Vec<(Token, usize)>) -> Self {
136        Self::new_with_file(tokens, "-e")
137    }
138
139    pub fn new_with_file(tokens: Vec<(Token, usize)>, file: impl Into<String>) -> Self {
140        Self {
141            tokens,
142            pos: 0,
143            next_rate_limit_slot: 0,
144            suppress_indirect_paren_call: 0,
145            pipe_rhs_depth: 0,
146            no_pipe_forward_depth: 0,
147            suppress_scalar_hash_brace: 0,
148            next_desugar_tmp: 0,
149            error_file: file.into(),
150            declared_subs: std::collections::HashSet::new(),
151            suppress_parenless_call: 0,
152            suppress_slash_as_div: 0,
153            suppress_m_regex: 0,
154            suppress_colon_range: 0,
155            thread_last_mode: false,
156            parsing_module: false,
157        }
158    }
159
160    fn alloc_desugar_tmp(&mut self) -> u32 {
161        let n = self.next_desugar_tmp;
162        self.next_desugar_tmp = self.next_desugar_tmp.saturating_add(1);
163        n
164    }
165
166    /// True when we are currently parsing the RHS of a `|>` pipe-forward.
167    /// Used by builtins (`map`, `grep`, `sort`, `join`, …) to supply a
168    /// placeholder list instead of erroring on a missing operand.
169    #[inline]
170    fn in_pipe_rhs(&self) -> bool {
171        self.pipe_rhs_depth > 0
172    }
173
174    /// List-slurping builtin: the operand is entirely the LHS of `|>` (no following list tokens).
175    /// A newline after the builtin name also terminates the pipe stage (implicit semicolon).
176    fn pipe_supplies_slurped_list_operand(&self) -> bool {
177        self.in_pipe_rhs()
178            && (matches!(
179                self.peek(),
180                Token::Semicolon
181                    | Token::RBrace
182                    | Token::RParen
183                    | Token::Eof
184                    | Token::Comma
185                    | Token::PipeForward
186            ) || self.peek_line() > self.prev_line())
187    }
188
189    /// Empty placeholder list used as a stand-in for the list operand of
190    /// list-taking builtins when they appear on the RHS of `|>`.
191    /// [`Self::pipe_forward_apply`] rewrites this slot with the actual piped
192    /// value at desugar time, so the placeholder is never evaluated.
193    #[inline]
194    fn pipe_placeholder_list(&self, line: usize) -> Expr {
195        Expr {
196            kind: ExprKind::List(vec![]),
197            line,
198        }
199    }
200
201    /// List builtins that take `{ BLOCK }, LIST` and accept the threaded list at
202    /// `args[1]` via [`Self::pipe_forward_apply`]. Used by both the pipe-forward
203    /// dispatcher and `parse_thread_stage_with_block` so `~> @a NAME { ... }` and
204    /// `@a |> NAME { ... }` route through the same substitution.
205    fn is_block_then_list_pipe_builtin(name: &str) -> bool {
206        matches!(
207            name,
208            "pfirst"
209                | "pany"
210                | "any"
211                | "all"
212                | "none"
213                | "first"
214                | "take_while"
215                | "drop_while"
216                | "skip_while"
217                | "reject"
218                | "tap"
219                | "peek"
220                | "group_by"
221                | "chunk_by"
222                | "partition"
223                | "min_by"
224                | "max_by"
225                | "zip_with"
226                | "count_by"
227        )
228    }
229
230    /// Lift a `Bareword("f")` to `FuncCall { f, [$_] }`.
231    ///
232    /// stryke extension contexts (map/grep/fore expression forms, pipe-forward)
233    /// call this so that `map sha512, @list` invokes `sha512($_)` for each
234    /// element instead of stringifying the bareword.  Non-bareword expressions
235    /// pass through unchanged.
236    ///
237    /// Also injects `$_` into known builtins that were parsed with zero
238    /// arguments (e.g. `fore unlink`, `map stat`) so they operate on the
239    /// topic variable instead of being no-ops.
240    fn lift_bareword_to_topic_call(expr: Expr) -> Expr {
241        let line = expr.line;
242        let topic = || Expr {
243            kind: ExprKind::ScalarVar("_".into()),
244            line,
245        };
246        match expr.kind {
247            ExprKind::Bareword(ref name) => Expr {
248                kind: ExprKind::FuncCall {
249                    name: name.clone(),
250                    args: vec![topic()],
251                },
252                line,
253            },
254            // Builtins that take Vec<Expr> args — inject $_ when empty.
255            ExprKind::Unlink(ref args) if args.is_empty() => Expr {
256                kind: ExprKind::Unlink(vec![topic()]),
257                line,
258            },
259            ExprKind::Chmod(ref args) if args.is_empty() => Expr {
260                kind: ExprKind::Chmod(vec![topic()]),
261                line,
262            },
263            // Builtins that take Box<Expr> — inject $_ when arg is implicit.
264            ExprKind::Stat(_) => expr,
265            ExprKind::Lstat(_) => expr,
266            ExprKind::Readlink(_) => expr,
267            // rev with empty list should use $_
268            ExprKind::Rev(ref inner) => {
269                if matches!(inner.kind, ExprKind::List(ref v) if v.is_empty()) {
270                    Expr {
271                        kind: ExprKind::Rev(Box::new(topic())),
272                        line,
273                    }
274                } else {
275                    expr
276                }
277            }
278            _ => expr,
279        }
280    }
281
282    /// `parse_assign_expr` with `no_pipe_forward_depth` bumped for the
283    /// duration, so any trailing `|>` is left to the enclosing parser instead
284    /// of being absorbed into this sub-expression. Used by paren-less arg
285    /// parsers (`parse_list_until_terminator`, `chunked`/`windowed` paren-less,
286    /// paren-less method args, …) so `@a |> head 2 |> join "-"` chains
287    /// left-associatively instead of letting `head`'s first arg swallow the
288    /// outer `|>`. The counter is restored on both success and error paths.
289    fn parse_assign_expr_stop_at_pipe(&mut self) -> PerlResult<Expr> {
290        self.no_pipe_forward_depth = self.no_pipe_forward_depth.saturating_add(1);
291        let r = self.parse_assign_expr();
292        self.no_pipe_forward_depth = self.no_pipe_forward_depth.saturating_sub(1);
293        r
294    }
295
296    fn syntax_err(&self, message: impl Into<String>, line: usize) -> PerlError {
297        PerlError::new(ErrorKind::Syntax, message, line, self.error_file.clone())
298    }
299
300    fn alloc_rate_limit_slot(&mut self) -> u32 {
301        let s = self.next_rate_limit_slot;
302        self.next_rate_limit_slot = self.next_rate_limit_slot.saturating_add(1);
303        s
304    }
305
306    fn peek(&self) -> &Token {
307        self.tokens
308            .get(self.pos)
309            .map(|(t, _)| t)
310            .unwrap_or(&Token::Eof)
311    }
312
313    fn peek_line(&self) -> usize {
314        self.tokens.get(self.pos).map(|(_, l)| *l).unwrap_or(0)
315    }
316
317    fn peek_at(&self, offset: usize) -> &Token {
318        self.tokens
319            .get(self.pos + offset)
320            .map(|(t, _)| t)
321            .unwrap_or(&Token::Eof)
322    }
323
324    fn advance(&mut self) -> (Token, usize) {
325        let tok = self
326            .tokens
327            .get(self.pos)
328            .cloned()
329            .unwrap_or((Token::Eof, 0));
330        self.pos += 1;
331        tok
332    }
333
334    /// Line number of the most recently consumed token (the token at `pos - 1`).
335    fn prev_line(&self) -> usize {
336        if self.pos > 0 {
337            self.tokens.get(self.pos - 1).map(|(_, l)| *l).unwrap_or(0)
338        } else {
339            0
340        }
341    }
342
343    /// Check if `{ ... }` starting at current position looks like a hashref rather than a block.
344    /// Heuristics (assuming current token is `{`):
345    /// - `{ bareword =>` → hashref
346    /// - `{ "string" =>` → hashref
347    /// - `{ $var =>` → hashref
348    /// - `{ 0 =>` → hashref (numeric key)
349    /// - `{ %hash }` or `{ %hash, ...}` → hashref (spread)
350    /// - `{ }` (empty) → hashref
351    fn looks_like_hashref(&self) -> bool {
352        debug_assert!(matches!(self.peek(), Token::LBrace));
353        let tok1 = self.peek_at(1);
354        let tok2 = self.peek_at(2);
355        match tok1 {
356            Token::RBrace => true,
357            Token::Ident(_)
358            | Token::SingleString(_)
359            | Token::DoubleString(_)
360            | Token::ScalarVar(_)
361            | Token::Integer(_) => matches!(tok2, Token::FatArrow),
362            Token::HashVar(_) => matches!(tok2, Token::RBrace | Token::Comma),
363            _ => false,
364        }
365    }
366
367    fn expect(&mut self, expected: &Token) -> PerlResult<usize> {
368        let (tok, line) = self.advance();
369        if std::mem::discriminant(&tok) == std::mem::discriminant(expected) {
370            Ok(line)
371        } else {
372            Err(self.syntax_err(format!("Expected {:?}, got {:?}", expected, tok), line))
373        }
374    }
375
376    fn eat(&mut self, expected: &Token) -> bool {
377        if std::mem::discriminant(self.peek()) == std::mem::discriminant(expected) {
378            self.advance();
379            true
380        } else {
381            false
382        }
383    }
384
385    fn at_eof(&self) -> bool {
386        matches!(self.peek(), Token::Eof)
387    }
388
389    /// True when a file test (`-d`, `-f`, …) may omit its operand and use `$_` (Perl filetest default).
390    fn filetest_allows_implicit_topic(tok: &Token) -> bool {
391        matches!(
392            tok,
393            Token::RParen
394                | Token::Semicolon
395                | Token::Comma
396                | Token::RBrace
397                | Token::Eof
398                | Token::LogAnd
399                | Token::LogOr
400                | Token::LogAndWord
401                | Token::LogOrWord
402                | Token::PipeForward
403        )
404    }
405
406    /// True when the next token is a statement-starting keyword on a *different*
407    /// line from `stmt_line`.  Used by `parse_use` / `parse_no` to stop parsing
408    /// import lists when semicolons are omitted (stryke extension).
409    fn next_is_new_stmt_keyword(&self, stmt_line: usize) -> bool {
410        // Semicolons-optional is a stryke extension; in compat mode, require them.
411        if crate::compat_mode() {
412            return false;
413        }
414        if self.peek_line() == stmt_line {
415            return false;
416        }
417        matches!(
418            self.peek(),
419            Token::Ident(ref kw) if matches!(kw.as_str(),
420                "use" | "no" | "my" | "our" | "local" | "sub" | "struct" | "enum"
421                | "if" | "unless" | "while" | "until" | "for" | "foreach"
422                | "return" | "last" | "next" | "redo" | "package" | "require"
423                | "BEGIN" | "END" | "UNITCHECK" | "frozen" | "const" | "typed"
424            )
425        )
426    }
427
428    /// True when the next token is on a different line from `stmt_line` and could
429    /// start a new statement. More permissive than `next_is_new_stmt_keyword` —
430    /// includes sigil-prefixed variables like `$var`, `@arr`, `%hash`.
431    fn next_is_new_statement_start(&self, stmt_line: usize) -> bool {
432        if crate::compat_mode() {
433            return false;
434        }
435        if self.peek_line() == stmt_line {
436            return false;
437        }
438        matches!(
439            self.peek(),
440            Token::ScalarVar(_)
441                | Token::DerefScalarVar(_)
442                | Token::ArrayVar(_)
443                | Token::HashVar(_)
444                | Token::LBrace
445        ) || self.next_is_new_stmt_keyword(stmt_line)
446    }
447
448    // ── Top level ──
449
450    pub fn parse_program(&mut self) -> PerlResult<Program> {
451        let statements = self.parse_statements()?;
452        Ok(Program { statements })
453    }
454
455    /// Parse statements until EOF. Used by parse_program and parse_block_from_str.
456    pub fn parse_statements(&mut self) -> PerlResult<Vec<Statement>> {
457        let mut statements = Vec::new();
458        while !self.at_eof() {
459            if matches!(self.peek(), Token::Semicolon) {
460                let line = self.peek_line();
461                self.advance();
462                statements.push(Statement {
463                    label: None,
464                    kind: StmtKind::Empty,
465                    line,
466                });
467                continue;
468            }
469            statements.push(self.parse_statement()?);
470        }
471        Ok(statements)
472    }
473
474    // ── Statements ──
475
476    fn parse_statement(&mut self) -> PerlResult<Statement> {
477        let line = self.peek_line();
478
479        // Statement label `FOO:` / `boot:` / `BAR_BAZ:` (not `Foo::` — that is `Ident` + `::`).
480        // Uppercase-only was too strict: XSLoader.pm uses `boot:` before `my $xs = ...`.
481        let label = match self.peek().clone() {
482            Token::Ident(_) => {
483                if matches!(self.peek_at(1), Token::Colon)
484                    && !matches!(self.peek_at(2), Token::Colon)
485                {
486                    let (tok, _) = self.advance();
487                    let l = match tok {
488                        Token::Ident(l) => l,
489                        _ => unreachable!(),
490                    };
491                    self.advance(); // ':'
492                    Some(l)
493                } else {
494                    None
495                }
496            }
497            _ => None,
498        };
499
500        let mut stmt = match self.peek().clone() {
501            Token::FormatDecl { .. } => {
502                let tok_line = self.peek_line();
503                let (tok, _) = self.advance();
504                match tok {
505                    Token::FormatDecl { name, lines } => Statement {
506                        label: label.clone(),
507                        kind: StmtKind::FormatDecl { name, lines },
508                        line: tok_line,
509                    },
510                    _ => unreachable!(),
511                }
512            }
513            Token::Ident(ref kw) => match kw.as_str() {
514                "if" => self.parse_if()?,
515                "unless" => self.parse_unless()?,
516                "while" => {
517                    let mut s = self.parse_while()?;
518                    if let StmtKind::While {
519                        label: ref mut lbl, ..
520                    } = s.kind
521                    {
522                        *lbl = label.clone();
523                    }
524                    s
525                }
526                "until" => {
527                    let mut s = self.parse_until()?;
528                    if let StmtKind::Until {
529                        label: ref mut lbl, ..
530                    } = s.kind
531                    {
532                        *lbl = label.clone();
533                    }
534                    s
535                }
536                "for" => {
537                    let mut s = self.parse_for_or_foreach()?;
538                    match s.kind {
539                        StmtKind::For {
540                            label: ref mut lbl, ..
541                        }
542                        | StmtKind::Foreach {
543                            label: ref mut lbl, ..
544                        } => *lbl = label.clone(),
545                        _ => {}
546                    }
547                    s
548                }
549                "foreach" => {
550                    let mut s = self.parse_foreach()?;
551                    if let StmtKind::Foreach {
552                        label: ref mut lbl, ..
553                    } = s.kind
554                    {
555                        *lbl = label.clone();
556                    }
557                    s
558                }
559                "sub" => {
560                    if crate::no_interop_mode() {
561                        return Err(self.syntax_err(
562                            "stryke uses `fn` instead of `sub` (--no-interop is active)",
563                            self.peek_line(),
564                        ));
565                    }
566                    self.parse_sub_decl(true)?
567                }
568                "fn" => self.parse_sub_decl(false)?,
569                "struct" => {
570                    if crate::compat_mode() {
571                        return Err(self.syntax_err(
572                            "`struct` is a stryke extension (disabled by --compat)",
573                            self.peek_line(),
574                        ));
575                    }
576                    self.parse_struct_decl()?
577                }
578                "enum" => {
579                    if crate::compat_mode() {
580                        return Err(self.syntax_err(
581                            "`enum` is a stryke extension (disabled by --compat)",
582                            self.peek_line(),
583                        ));
584                    }
585                    self.parse_enum_decl()?
586                }
587                "class" => {
588                    if crate::compat_mode() {
589                        // TODO: parse Perl 5.38 class syntax with :isa()
590                        return Err(self.syntax_err(
591                            "Perl 5.38 `class` syntax not yet implemented in --compat mode",
592                            self.peek_line(),
593                        ));
594                    }
595                    self.parse_class_decl(false, false)?
596                }
597                "abstract" => {
598                    self.advance(); // abstract
599                    if !matches!(self.peek(), Token::Ident(ref s) if s == "class") {
600                        return Err(self.syntax_err(
601                            "`abstract` must be followed by `class`",
602                            self.peek_line(),
603                        ));
604                    }
605                    self.parse_class_decl(true, false)?
606                }
607                "final" => {
608                    self.advance(); // final
609                    if !matches!(self.peek(), Token::Ident(ref s) if s == "class") {
610                        return Err(self
611                            .syntax_err("`final` must be followed by `class`", self.peek_line()));
612                    }
613                    self.parse_class_decl(false, true)?
614                }
615                "trait" => {
616                    if crate::compat_mode() {
617                        return Err(self.syntax_err(
618                            "`trait` is a stryke extension (disabled by --compat)",
619                            self.peek_line(),
620                        ));
621                    }
622                    self.parse_trait_decl()?
623                }
624                "my" => self.parse_my_our_local("my", false)?,
625                "state" => self.parse_my_our_local("state", false)?,
626                "mysync" => {
627                    if crate::compat_mode() {
628                        return Err(self.syntax_err(
629                            "`mysync` is a stryke extension (disabled by --compat)",
630                            self.peek_line(),
631                        ));
632                    }
633                    self.parse_my_our_local("mysync", false)?
634                }
635                "frozen" | "const" => {
636                    let leading = kw.as_str().to_string();
637                    if crate::compat_mode() {
638                        return Err(self.syntax_err(
639                            format!("`{leading}` is a stryke extension (disabled by --compat)"),
640                            self.peek_line(),
641                        ));
642                    }
643                    // `frozen my $x = val;` / `const my $x = val;` — the
644                    // two spellings are interchangeable (`const` is the
645                    // more-familiar name for new users). Expects `my`
646                    // to follow.
647                    self.advance(); // consume "frozen"/"const"
648                    if let Token::Ident(ref kw) = self.peek().clone() {
649                        if kw == "my" {
650                            let mut stmt = self.parse_my_our_local("my", false)?;
651                            if let StmtKind::My(ref mut decls) = stmt.kind {
652                                for decl in decls.iter_mut() {
653                                    decl.frozen = true;
654                                }
655                            }
656                            stmt
657                        } else {
658                            return Err(self.syntax_err(
659                                format!("Expected 'my' after '{leading}'"),
660                                self.peek_line(),
661                            ));
662                        }
663                    } else {
664                        return Err(self.syntax_err(
665                            format!("Expected 'my' after '{leading}'"),
666                            self.peek_line(),
667                        ));
668                    }
669                }
670                "typed" => {
671                    if crate::compat_mode() {
672                        return Err(self.syntax_err(
673                            "`typed` is a stryke extension (disabled by --compat)",
674                            self.peek_line(),
675                        ));
676                    }
677                    self.advance();
678                    if let Token::Ident(ref kw) = self.peek().clone() {
679                        if kw == "my" {
680                            self.parse_my_our_local("my", true)?
681                        } else {
682                            return Err(
683                                self.syntax_err("Expected 'my' after 'typed'", self.peek_line())
684                            );
685                        }
686                    } else {
687                        return Err(
688                            self.syntax_err("Expected 'my' after 'typed'", self.peek_line())
689                        );
690                    }
691                }
692                "our" => self.parse_my_our_local("our", false)?,
693                "local" => self.parse_my_our_local("local", false)?,
694                "package" => self.parse_package()?,
695                "use" => self.parse_use()?,
696                "no" => self.parse_no()?,
697                "return" => self.parse_return()?,
698                "last" => {
699                    self.advance();
700                    let lbl = if let Token::Ident(ref s) = self.peek() {
701                        if s.chars().all(|c| c.is_uppercase() || c == '_') {
702                            let (Token::Ident(l), _) = self.advance() else {
703                                unreachable!()
704                            };
705                            Some(l)
706                        } else {
707                            None
708                        }
709                    } else {
710                        None
711                    };
712                    let stmt = Statement {
713                        label: None,
714                        kind: StmtKind::Last(lbl.or(label.clone())),
715                        line,
716                    };
717                    self.parse_stmt_postfix_modifier(stmt)?
718                }
719                "next" => {
720                    self.advance();
721                    let lbl = if let Token::Ident(ref s) = self.peek() {
722                        if s.chars().all(|c| c.is_uppercase() || c == '_') {
723                            let (Token::Ident(l), _) = self.advance() else {
724                                unreachable!()
725                            };
726                            Some(l)
727                        } else {
728                            None
729                        }
730                    } else {
731                        None
732                    };
733                    let stmt = Statement {
734                        label: None,
735                        kind: StmtKind::Next(lbl.or(label.clone())),
736                        line,
737                    };
738                    self.parse_stmt_postfix_modifier(stmt)?
739                }
740                "redo" => {
741                    self.advance();
742                    self.eat(&Token::Semicolon);
743                    Statement {
744                        label: None,
745                        kind: StmtKind::Redo(label.clone()),
746                        line,
747                    }
748                }
749                "BEGIN" => {
750                    self.advance();
751                    let block = self.parse_block()?;
752                    Statement {
753                        label: None,
754                        kind: StmtKind::Begin(block),
755                        line,
756                    }
757                }
758                "END" => {
759                    self.advance();
760                    let block = self.parse_block()?;
761                    Statement {
762                        label: None,
763                        kind: StmtKind::End(block),
764                        line,
765                    }
766                }
767                "UNITCHECK" => {
768                    self.advance();
769                    let block = self.parse_block()?;
770                    Statement {
771                        label: None,
772                        kind: StmtKind::UnitCheck(block),
773                        line,
774                    }
775                }
776                "CHECK" => {
777                    self.advance();
778                    let block = self.parse_block()?;
779                    Statement {
780                        label: None,
781                        kind: StmtKind::Check(block),
782                        line,
783                    }
784                }
785                "INIT" => {
786                    self.advance();
787                    let block = self.parse_block()?;
788                    Statement {
789                        label: None,
790                        kind: StmtKind::Init(block),
791                        line,
792                    }
793                }
794                "goto" => {
795                    self.advance();
796                    let target = self.parse_expression()?;
797                    let stmt = Statement {
798                        label: None,
799                        kind: StmtKind::Goto {
800                            target: Box::new(target),
801                        },
802                        line,
803                    };
804                    // `goto $l if COND;` / `goto &$cr if defined &$cr;` (XSLoader.pm)
805                    self.parse_stmt_postfix_modifier(stmt)?
806                }
807                "continue" => {
808                    self.advance();
809                    let block = self.parse_block()?;
810                    Statement {
811                        label: None,
812                        kind: StmtKind::Continue(block),
813                        line,
814                    }
815                }
816                "try" => self.parse_try_catch()?,
817                "defer" => self.parse_defer_stmt()?,
818                "tie" => self.parse_tie_stmt()?,
819                "given" => self.parse_given()?,
820                "when" => self.parse_when_stmt()?,
821                "default" => self.parse_default_stmt()?,
822                "eval_timeout" => self.parse_eval_timeout()?,
823                "do" => {
824                    if matches!(self.peek_at(1), Token::LBrace) {
825                        self.advance();
826                        let body = self.parse_block()?;
827                        if let Token::Ident(ref w) = self.peek().clone() {
828                            if w == "while" {
829                                self.advance();
830                                self.expect(&Token::LParen)?;
831                                let mut condition = self.parse_expression()?;
832                                Self::mark_match_scalar_g_for_boolean_condition(&mut condition);
833                                self.expect(&Token::RParen)?;
834                                self.eat(&Token::Semicolon);
835                                Statement {
836                                    label: label.clone(),
837                                    kind: StmtKind::DoWhile { body, condition },
838                                    line,
839                                }
840                            } else {
841                                let inner_line = body.first().map(|s| s.line).unwrap_or(line);
842                                let inner = Expr {
843                                    kind: ExprKind::CodeRef {
844                                        params: vec![],
845                                        body,
846                                    },
847                                    line: inner_line,
848                                };
849                                let expr = Expr {
850                                    kind: ExprKind::Do(Box::new(inner)),
851                                    line,
852                                };
853                                let stmt = Statement {
854                                    label: label.clone(),
855                                    kind: StmtKind::Expression(expr),
856                                    line,
857                                };
858                                // `do { } if EXPR` / `do { } unless EXPR` — postfix modifier, not a new `if (` statement.
859                                self.parse_stmt_postfix_modifier(stmt)?
860                            }
861                        } else {
862                            let inner_line = body.first().map(|s| s.line).unwrap_or(line);
863                            let inner = Expr {
864                                kind: ExprKind::CodeRef {
865                                    params: vec![],
866                                    body,
867                                },
868                                line: inner_line,
869                            };
870                            let expr = Expr {
871                                kind: ExprKind::Do(Box::new(inner)),
872                                line,
873                            };
874                            let stmt = Statement {
875                                label: label.clone(),
876                                kind: StmtKind::Expression(expr),
877                                line,
878                            };
879                            self.parse_stmt_postfix_modifier(stmt)?
880                        }
881                    } else {
882                        if let Some(expr) = self.try_parse_bareword_stmt_call() {
883                            let stmt = self.maybe_postfix_modifier(expr)?;
884                            self.parse_stmt_postfix_modifier(stmt)?
885                        } else {
886                            let expr = self.parse_expression()?;
887                            let stmt = self.maybe_postfix_modifier(expr)?;
888                            self.parse_stmt_postfix_modifier(stmt)?
889                        }
890                    }
891                }
892                _ => {
893                    // `foo;` or `{ foo }` — bareword statement is a zero-arg call (topic `$_` at runtime).
894                    if let Some(expr) = self.try_parse_bareword_stmt_call() {
895                        let stmt = self.maybe_postfix_modifier(expr)?;
896                        self.parse_stmt_postfix_modifier(stmt)?
897                    } else {
898                        let expr = self.parse_expression()?;
899                        let stmt = self.maybe_postfix_modifier(expr)?;
900                        self.parse_stmt_postfix_modifier(stmt)?
901                    }
902                }
903            },
904            Token::LBrace => {
905                // Disambiguate hashref `{ k => v }` from block `{ stmt; stmt }`.
906                // If it looks like a hashref, parse as expression; otherwise parse as block.
907                if self.looks_like_hashref() {
908                    let expr = self.parse_expression()?;
909                    let stmt = self.maybe_postfix_modifier(expr)?;
910                    self.parse_stmt_postfix_modifier(stmt)?
911                } else {
912                    let block = self.parse_block()?;
913                    let stmt = Statement {
914                        label: None,
915                        kind: StmtKind::Block(block),
916                        line,
917                    };
918                    // `{ … } if EXPR` / `{ … } unless EXPR` — same postfix rule as `do { } if …` (not `if (`).
919                    self.parse_stmt_postfix_modifier(stmt)?
920                }
921            }
922            _ => {
923                let expr = self.parse_expression()?;
924                let stmt = self.maybe_postfix_modifier(expr)?;
925                self.parse_stmt_postfix_modifier(stmt)?
926            }
927        };
928
929        stmt.label = label;
930        Ok(stmt)
931    }
932
933    /// Handle postfix if/unless on statement-level keywords like last/next.
934    fn parse_stmt_postfix_modifier(&mut self, stmt: Statement) -> PerlResult<Statement> {
935        let line = stmt.line;
936        // Implicit semicolon: a modifier keyword on a new line is a new
937        // statement, not a postfix modifier.  This prevents semicolon-less
938        // code like `my $x = "val"\nif ($x) { ... }` from being mis-parsed
939        // as `my $x = "val" if ($x) { ... }`.
940        if self.peek_line() > self.prev_line() {
941            self.eat(&Token::Semicolon);
942            return Ok(stmt);
943        }
944        if let Token::Ident(ref kw) = self.peek().clone() {
945            match kw.as_str() {
946                "if" => {
947                    self.advance();
948                    let mut cond = self.parse_expression()?;
949                    Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
950                    self.eat(&Token::Semicolon);
951                    return Ok(Statement {
952                        label: None,
953                        kind: StmtKind::If {
954                            condition: cond,
955                            body: vec![stmt],
956                            elsifs: vec![],
957                            else_block: None,
958                        },
959                        line,
960                    });
961                }
962                "unless" => {
963                    self.advance();
964                    let mut cond = self.parse_expression()?;
965                    Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
966                    self.eat(&Token::Semicolon);
967                    return Ok(Statement {
968                        label: None,
969                        kind: StmtKind::Unless {
970                            condition: cond,
971                            body: vec![stmt],
972                            else_block: None,
973                        },
974                        line,
975                    });
976                }
977                "while" | "until" | "for" | "foreach" => {
978                    // `do { } for @a` / `{ } while COND` — same postfix forms as [`maybe_postfix_modifier`],
979                    // not a new `for (` / `while (` statement (which would require `(` after `for`).
980                    if let Some(expr) = Self::stmt_into_postfix_body_expr(stmt) {
981                        let out = self.maybe_postfix_modifier(expr)?;
982                        self.eat(&Token::Semicolon);
983                        return Ok(out);
984                    }
985                    return Err(self.syntax_err(
986                        format!("postfix `{}` is not supported on this statement form", kw),
987                        self.peek_line(),
988                    ));
989                }
990                // `{ } pmap @a` / `{ } pflat_map @a` / `{ } pfor @a` / `do { } …` — same shapes as prefix forms.
991                "pmap" | "pflat_map" | "pgrep" | "pfor" | "preduce" | "pcache" => {
992                    let line = stmt.line;
993                    let block = self.stmt_into_parallel_block(stmt)?;
994                    let which = kw.as_str();
995                    self.advance();
996                    self.eat(&Token::Comma);
997                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
998                    self.eat(&Token::Semicolon);
999                    let list = Box::new(list);
1000                    let progress = progress.map(Box::new);
1001                    let kind = match which {
1002                        "pmap" => ExprKind::PMapExpr {
1003                            block,
1004                            list,
1005                            progress,
1006                            flat_outputs: false,
1007                            on_cluster: None,
1008                            stream: false,
1009                        },
1010                        "pflat_map" => ExprKind::PMapExpr {
1011                            block,
1012                            list,
1013                            progress,
1014                            flat_outputs: true,
1015                            on_cluster: None,
1016                            stream: false,
1017                        },
1018                        "pgrep" => ExprKind::PGrepExpr {
1019                            block,
1020                            list,
1021                            progress,
1022                            stream: false,
1023                        },
1024                        "pfor" => ExprKind::PForExpr {
1025                            block,
1026                            list,
1027                            progress,
1028                        },
1029                        "preduce" => ExprKind::PReduceExpr {
1030                            block,
1031                            list,
1032                            progress,
1033                        },
1034                        "pcache" => ExprKind::PcacheExpr {
1035                            block,
1036                            list,
1037                            progress,
1038                        },
1039                        _ => unreachable!(),
1040                    };
1041                    return Ok(Statement {
1042                        label: None,
1043                        kind: StmtKind::Expression(Expr { kind, line }),
1044                        line,
1045                    });
1046                }
1047                _ => {}
1048            }
1049        }
1050        self.eat(&Token::Semicolon);
1051        Ok(stmt)
1052    }
1053
1054    /// Block body for postfix `pmap` / `pfor` / … — bare `{ }`, `do { }`, or any expression
1055    /// statement (wrapped as a one-line block, e.g. `` `cmd` pfor @a ``).
1056    fn stmt_into_parallel_block(&self, stmt: Statement) -> PerlResult<Block> {
1057        let line = stmt.line;
1058        match stmt.kind {
1059            StmtKind::Block(block) => Ok(block),
1060            StmtKind::Expression(expr) => {
1061                if let ExprKind::Do(ref inner) = expr.kind {
1062                    if let ExprKind::CodeRef { ref body, .. } = inner.kind {
1063                        return Ok(body.clone());
1064                    }
1065                }
1066                Ok(vec![Statement {
1067                    label: None,
1068                    kind: StmtKind::Expression(expr),
1069                    line,
1070                }])
1071            }
1072            _ => Err(self.syntax_err(
1073                "postfix parallel op expects `do { }`, a bare `{ }` block, or an expression statement",
1074                line,
1075            )),
1076        }
1077    }
1078
1079    /// `StmtKind::Expression` or a bare block (`StmtKind::Block`) as an [`Expr`] for postfix
1080    /// `while` / `until` / `for` / `foreach` (mirrors `do { }` → [`ExprKind::Do`](ExprKind::Do)([`CodeRef`](ExprKind::CodeRef))).
1081    fn stmt_into_postfix_body_expr(stmt: Statement) -> Option<Expr> {
1082        match stmt.kind {
1083            StmtKind::Expression(expr) => Some(expr),
1084            StmtKind::Block(block) => {
1085                let line = stmt.line;
1086                let inner = Expr {
1087                    kind: ExprKind::CodeRef {
1088                        params: vec![],
1089                        body: block,
1090                    },
1091                    line,
1092                };
1093                Some(Expr {
1094                    kind: ExprKind::Do(Box::new(inner)),
1095                    line,
1096                })
1097            }
1098            _ => None,
1099        }
1100    }
1101
1102    /// Statement-modifier keywords that must not be consumed as part of a comma-separated list
1103    /// (same set as [`parse_list_until_terminator`]).
1104    fn peek_is_postfix_stmt_modifier_keyword(&self) -> bool {
1105        matches!(
1106            self.peek(),
1107            Token::Ident(ref kw)
1108                if matches!(
1109                    kw.as_str(),
1110                    "if" | "unless" | "while" | "until" | "for" | "foreach"
1111                )
1112        )
1113    }
1114
1115    fn maybe_postfix_modifier(&mut self, expr: Expr) -> PerlResult<Statement> {
1116        let line = expr.line;
1117        // Implicit semicolon: modifier keyword on a new line starts a new statement.
1118        if self.peek_line() > self.prev_line() {
1119            return Ok(Statement {
1120                label: None,
1121                kind: StmtKind::Expression(expr),
1122                line,
1123            });
1124        }
1125        match self.peek() {
1126            Token::Ident(ref kw) => match kw.as_str() {
1127                "if" => {
1128                    self.advance();
1129                    let cond = self.parse_expression()?;
1130                    Ok(Statement {
1131                        label: None,
1132                        kind: StmtKind::Expression(Expr {
1133                            kind: ExprKind::PostfixIf {
1134                                expr: Box::new(expr),
1135                                condition: Box::new(cond),
1136                            },
1137                            line,
1138                        }),
1139                        line,
1140                    })
1141                }
1142                "unless" => {
1143                    self.advance();
1144                    let cond = self.parse_expression()?;
1145                    Ok(Statement {
1146                        label: None,
1147                        kind: StmtKind::Expression(Expr {
1148                            kind: ExprKind::PostfixUnless {
1149                                expr: Box::new(expr),
1150                                condition: Box::new(cond),
1151                            },
1152                            line,
1153                        }),
1154                        line,
1155                    })
1156                }
1157                "while" => {
1158                    self.advance();
1159                    let cond = self.parse_expression()?;
1160                    Ok(Statement {
1161                        label: None,
1162                        kind: StmtKind::Expression(Expr {
1163                            kind: ExprKind::PostfixWhile {
1164                                expr: Box::new(expr),
1165                                condition: Box::new(cond),
1166                            },
1167                            line,
1168                        }),
1169                        line,
1170                    })
1171                }
1172                "until" => {
1173                    self.advance();
1174                    let cond = self.parse_expression()?;
1175                    Ok(Statement {
1176                        label: None,
1177                        kind: StmtKind::Expression(Expr {
1178                            kind: ExprKind::PostfixUntil {
1179                                expr: Box::new(expr),
1180                                condition: Box::new(cond),
1181                            },
1182                            line,
1183                        }),
1184                        line,
1185                    })
1186                }
1187                "for" | "foreach" => {
1188                    self.advance();
1189                    let list = self.parse_expression()?;
1190                    Ok(Statement {
1191                        label: None,
1192                        kind: StmtKind::Expression(Expr {
1193                            kind: ExprKind::PostfixForeach {
1194                                expr: Box::new(expr),
1195                                list: Box::new(list),
1196                            },
1197                            line,
1198                        }),
1199                        line,
1200                    })
1201                }
1202                _ => Ok(Statement {
1203                    label: None,
1204                    kind: StmtKind::Expression(expr),
1205                    line,
1206                }),
1207            },
1208            _ => Ok(Statement {
1209                label: None,
1210                kind: StmtKind::Expression(expr),
1211                line,
1212            }),
1213        }
1214    }
1215
1216    /// `name;` or `name}` — a bare identifier statement is a sub call with no explicit args (`$_` implied).
1217    fn try_parse_bareword_stmt_call(&mut self) -> Option<Expr> {
1218        let saved = self.pos;
1219        let line = self.peek_line();
1220        let mut name = match self.peek() {
1221            Token::Ident(n) => n.clone(),
1222            _ => return None,
1223        };
1224        // Names that begin `parse_named_expr` (builtins / `undef` / …) must use that path, not a sub call.
1225        if name.starts_with('\x00') || !Self::bareword_stmt_may_be_sub(&name) {
1226            return None;
1227        }
1228        self.advance();
1229        while self.eat(&Token::PackageSep) {
1230            match self.advance() {
1231                (Token::Ident(part), _) => {
1232                    name = format!("{}::{}", name, part);
1233                }
1234                _ => {
1235                    self.pos = saved;
1236                    return None;
1237                }
1238            }
1239        }
1240        match self.peek() {
1241            Token::Semicolon | Token::RBrace => Some(Expr {
1242                kind: ExprKind::FuncCall { name, args: vec![] },
1243                line,
1244            }),
1245            _ => {
1246                self.pos = saved;
1247                None
1248            }
1249        }
1250    }
1251
1252    /// Identifiers that start a [`parse_named_expr`] arm (builtins / special forms), not a bare sub call.
1253    fn bareword_stmt_may_be_sub(name: &str) -> bool {
1254        !matches!(
1255            name,
1256            "__FILE__"
1257                | "__LINE__"
1258                | "abs"
1259                | "async"
1260                | "spawn"
1261                | "atan2"
1262                | "await"
1263                | "barrier"
1264                | "bless"
1265                | "caller"
1266                | "capture"
1267                | "cat"
1268                | "chdir"
1269                | "chmod"
1270                | "chomp"
1271                | "chop"
1272                | "chr"
1273                | "chown"
1274                | "closedir"
1275                | "close"
1276                | "collect"
1277                | "cos"
1278                | "crypt"
1279                | "defined"
1280                | "dec"
1281                | "delete"
1282                | "die"
1283                | "deque"
1284                | "do"
1285                | "each"
1286                | "eof"
1287                | "fore"
1288                | "eval"
1289                | "exec"
1290                | "exists"
1291                | "exit"
1292                | "exp"
1293                | "fan"
1294                | "fan_cap"
1295                | "fc"
1296                | "fetch_url"
1297                | "d"
1298                | "dirs"
1299                | "dr"
1300                | "f"
1301                | "fi"
1302                | "files"
1303                | "filesf"
1304                | "filter"
1305                | "fr"
1306                | "getcwd"
1307                | "glob_par"
1308                | "par_sed"
1309                | "glob"
1310                | "grep"
1311                | "greps"
1312                | "heap"
1313                | "hex"
1314                | "inc"
1315                | "index"
1316                | "int"
1317                | "join"
1318                | "keys"
1319                | "lcfirst"
1320                | "lc"
1321                | "length"
1322                | "link"
1323                | "log"
1324                | "lstat"
1325                | "map"
1326                | "flat_map"
1327                | "maps"
1328                | "flat_maps"
1329                | "flatten"
1330                | "frequencies"
1331                | "freq"
1332                | "interleave"
1333                | "ddump"
1334                | "stringify"
1335                | "str"
1336                | "s"
1337                | "input"
1338                | "lines"
1339                | "words"
1340                | "chars"
1341                | "digits"
1342                | "letters"
1343                | "letters_uc"
1344                | "letters_lc"
1345                | "punctuation"
1346                | "sentences"
1347                | "paragraphs"
1348                | "sections"
1349                | "numbers"
1350                | "graphemes"
1351                | "columns"
1352                | "trim"
1353                | "avg"
1354                | "top"
1355                | "pager"
1356                | "pg"
1357                | "less"
1358                | "count_by"
1359                | "to_file"
1360                | "to_json"
1361                | "to_csv"
1362                | "grep_v"
1363                | "select_keys"
1364                | "pluck"
1365                | "clamp"
1366                | "normalize"
1367                | "stddev"
1368                | "squared"
1369                | "square"
1370                | "cubed"
1371                | "cube"
1372                | "expt"
1373                | "pow"
1374                | "pw"
1375                | "snake_case"
1376                | "camel_case"
1377                | "kebab_case"
1378                | "to_toml"
1379                | "to_yaml"
1380                | "to_xml"
1381                | "to_html"
1382                | "to_markdown"
1383                | "xopen"
1384                | "clip"
1385                | "paste"
1386                | "to_table"
1387                | "sparkline"
1388                | "bar_chart"
1389                | "flame"
1390                | "set"
1391                | "list_count"
1392                | "list_size"
1393                | "count"
1394                | "size"
1395                | "cnt"
1396                | "len"
1397                | "all"
1398                | "any"
1399                | "none"
1400                | "take_while"
1401                | "drop_while"
1402                | "skip_while"
1403                | "skip"
1404                | "first_or"
1405                | "tap"
1406                | "peek"
1407                | "partition"
1408                | "min_by"
1409                | "max_by"
1410                | "zip_with"
1411                | "group_by"
1412                | "chunk_by"
1413                | "with_index"
1414                | "puniq"
1415                | "pfirst"
1416                | "pany"
1417                | "uniq"
1418                | "distinct"
1419                | "shuffle"
1420                | "shuffled"
1421                | "chunked"
1422                | "windowed"
1423                | "match"
1424                | "mkdir"
1425                | "every"
1426                | "gen"
1427                | "oct"
1428                | "open"
1429                | "p"
1430                | "opendir"
1431                | "ord"
1432                | "par_lines"
1433                | "par_walk"
1434                | "pipe"
1435                | "pipes"
1436                | "block_devices"
1437                | "char_devices"
1438                | "exe"
1439                | "executables"
1440                | "rate_limit"
1441                | "retry"
1442                | "pcache"
1443                | "pchannel"
1444                | "pfor"
1445                | "pgrep"
1446                | "pgreps"
1447                | "pipeline"
1448                | "pmap_chunked"
1449                | "pmap_reduce"
1450                | "pmap_on"
1451                | "pflat_map_on"
1452                | "pmap"
1453                | "pmaps"
1454                | "pflat_map"
1455                | "pflat_maps"
1456                | "pop"
1457                | "pos"
1458                | "ppool"
1459                | "preduce_init"
1460                | "preduce"
1461                | "pselect"
1462                | "printf"
1463                | "print"
1464                | "pr"
1465                | "psort"
1466                | "push"
1467                | "pwatch"
1468                | "rand"
1469                | "readdir"
1470                | "readlink"
1471                | "reduce"
1472                | "fold"
1473                | "inject"
1474                | "first"
1475                | "detect"
1476                | "find"
1477                | "find_all"
1478                | "ref"
1479                | "rename"
1480                | "require"
1481                | "rev"
1482                | "reverse"
1483                | "reversed"
1484                | "rewinddir"
1485                | "rindex"
1486                | "rmdir"
1487                | "rm"
1488                | "say"
1489                | "scalar"
1490                | "seekdir"
1491                | "shift"
1492                | "sin"
1493                | "slurp"
1494                | "sockets"
1495                | "sort"
1496                | "splice"
1497                | "split"
1498                | "sprintf"
1499                | "sqrt"
1500                | "srand"
1501                | "stat"
1502                | "study"
1503                | "substr"
1504                | "symlink"
1505                | "sym_links"
1506                | "system"
1507                | "telldir"
1508                | "timer"
1509                | "trace"
1510                | "ucfirst"
1511                | "uc"
1512                | "undef"
1513                | "umask"
1514                | "unlink"
1515                | "unshift"
1516                | "utime"
1517                | "values"
1518                | "wantarray"
1519                | "warn"
1520                | "watch"
1521                | "yield"
1522                | "sub"
1523        )
1524    }
1525
1526    fn parse_block(&mut self) -> PerlResult<Block> {
1527        self.expect(&Token::LBrace)?;
1528        // Statements inside a block are NOT pipe RHS - reset depth so nested `~>`
1529        // parses its own input instead of using `$_[0]` placeholder.
1530        let saved_pipe_rhs_depth = self.pipe_rhs_depth;
1531        self.pipe_rhs_depth = 0;
1532        let mut stmts = Vec::new();
1533        // `{ |$a, $b| body }` — Ruby-style block params.
1534        // Desugars to `my $a = $_` (1 param), `my $a = $a; my $b = $b` (2 — sort/reduce),
1535        // or `my $p = $_N` for positional N≥3.
1536        if let Some(param_stmts) = self.try_parse_block_params()? {
1537            stmts.extend(param_stmts);
1538        }
1539        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
1540            if self.eat(&Token::Semicolon) {
1541                continue;
1542            }
1543            stmts.push(self.parse_statement()?);
1544        }
1545        self.expect(&Token::RBrace)?;
1546        self.pipe_rhs_depth = saved_pipe_rhs_depth;
1547        Self::default_topic_for_sole_bareword(&mut stmts);
1548        Ok(stmts)
1549    }
1550
1551    /// Try to parse `|$var1, $var2, ...|` at the start of a block.
1552    /// Returns `None` if the leading `|` is not block-param syntax.
1553    /// When successful, returns `my $var = <implicit>` assignment statements
1554    /// that alias the block's positional arguments.
1555    fn try_parse_block_params(&mut self) -> PerlResult<Option<Vec<Statement>>> {
1556        if !matches!(self.peek(), Token::BitOr) {
1557            return Ok(None);
1558        }
1559        // Lookahead: `| $scalar [, $scalar]* |` — verify before consuming.
1560        let mut i = 1; // skip the opening `|`
1561        loop {
1562            match self.peek_at(i) {
1563                Token::ScalarVar(_) => i += 1,
1564                _ => return Ok(None), // not `|$var...|`
1565            }
1566            match self.peek_at(i) {
1567                Token::BitOr => break,  // closing `|`
1568                Token::Comma => i += 1, // more params
1569                _ => return Ok(None),   // not block params
1570            }
1571        }
1572        // Confirmed — consume and build assignments.
1573        let line = self.peek_line();
1574        self.advance(); // eat opening `|`
1575        let mut names = Vec::new();
1576        loop {
1577            if let Token::ScalarVar(ref name) = self.peek().clone() {
1578                names.push(name.clone());
1579                self.advance();
1580            }
1581            if self.eat(&Token::BitOr) {
1582                break;
1583            }
1584            self.expect(&Token::Comma)?;
1585        }
1586        // Generate `my $name = <source>` for each param.
1587        // 1 param  → source is `$_` (map/grep/each/for topic)
1588        // 2 params → sources are `$a`, `$b` (sort/reduce)
1589        // N params → sources are `$_`, `$_1`, `$_2`, … (positional)
1590        let sources: Vec<&str> = match names.len() {
1591            1 => vec!["_"],
1592            2 => vec!["a", "b"],
1593            n => {
1594                // Can't return borrowed from a generated vec, handle below.
1595                let _ = n;
1596                vec![] // sentinel — handled in the else branch
1597            }
1598        };
1599        let mut stmts = Vec::with_capacity(names.len());
1600        if !sources.is_empty() {
1601            for (name, src) in names.iter().zip(sources.iter()) {
1602                stmts.push(Statement {
1603                    label: None,
1604                    kind: StmtKind::My(vec![VarDecl {
1605                        sigil: Sigil::Scalar,
1606                        name: name.clone(),
1607                        initializer: Some(Expr {
1608                            kind: ExprKind::ScalarVar(src.to_string()),
1609                            line,
1610                        }),
1611                        frozen: false,
1612                        type_annotation: None,
1613                    }]),
1614                    line,
1615                });
1616            }
1617        } else {
1618            // N≥3: positional `$_`, `$_1`, `$_2`, …
1619            for (idx, name) in names.iter().enumerate() {
1620                let src = if idx == 0 {
1621                    "_".to_string()
1622                } else {
1623                    format!("_{idx}")
1624                };
1625                stmts.push(Statement {
1626                    label: None,
1627                    kind: StmtKind::My(vec![VarDecl {
1628                        sigil: Sigil::Scalar,
1629                        name: name.clone(),
1630                        initializer: Some(Expr {
1631                            kind: ExprKind::ScalarVar(src),
1632                            line,
1633                        }),
1634                        frozen: false,
1635                        type_annotation: None,
1636                    }]),
1637                    line,
1638                });
1639            }
1640        }
1641        Ok(Some(stmts))
1642    }
1643
1644    /// Block shorthand: when the body is literally one bare builtin call
1645    /// (`{ uc }`, `{ basename }`, `{ to_json }`), inject `$_` as its first
1646    /// argument so `map { basename }` == `map { basename($_) }` uniformly.
1647    ///
1648    /// Without this, the ExprKind-modeled core names (`uc`/`lc`/`length`/…)
1649    /// default to `$_` via their own parse arms, but generic `FuncCall`-
1650    /// dispatched builtins (`basename`/`to_json`/`tj`/`bn`) are called with
1651    /// empty args and return the wrong value. This rewrite levels the
1652    /// playing field at parse time — no per-builtin handling needed.
1653    ///
1654    /// Narrow by design: fires only when the block has *exactly one*
1655    /// expression statement whose sole content is a known-bareword call
1656    /// with zero args. Multi-statement blocks and blocks with any other
1657    /// content are untouched.
1658    fn default_topic_for_sole_bareword(stmts: &mut [Statement]) {
1659        let [only] = stmts else { return };
1660        let StmtKind::Expression(ref mut expr) = only.kind else {
1661            return;
1662        };
1663        let topic_line = expr.line;
1664        let topic_arg = || Expr {
1665            kind: ExprKind::ScalarVar("_".to_string()),
1666            line: topic_line,
1667        };
1668        match expr.kind {
1669            // Zero-arg FuncCall whose name is a known builtin → inject `$_`.
1670            ExprKind::FuncCall {
1671                ref name,
1672                ref mut args,
1673            } if args.is_empty()
1674                && (Self::is_known_bareword(name) || Self::is_try_builtin_name(name)) =>
1675            {
1676                args.push(topic_arg());
1677            }
1678            // Lone bareword (the parser sometimes keeps a bareword as a
1679            // `Bareword` node instead of a zero-arg `FuncCall` —
1680            // e.g. `{ to_json }`, `{ ddump }`). Promote to a call.
1681            ExprKind::Bareword(ref name)
1682                if (Self::is_known_bareword(name) || Self::is_try_builtin_name(name)) =>
1683            {
1684                let n = name.clone();
1685                expr.kind = ExprKind::FuncCall {
1686                    name: n,
1687                    args: vec![topic_arg()],
1688                };
1689            }
1690            _ => {}
1691        }
1692    }
1693
1694    /// `defer { BLOCK }` — register a block to run when the current scope exits.
1695    /// Desugars to a `defer__internal(fn { BLOCK })` function call that the compiler
1696    /// handles specially by emitting Op::DeferBlock.
1697    fn parse_defer_stmt(&mut self) -> PerlResult<Statement> {
1698        let line = self.peek_line();
1699        self.advance(); // defer
1700        let body = self.parse_block()?;
1701        self.eat(&Token::Semicolon);
1702        // Desugar: defer { BLOCK } → defer__internal(fn { BLOCK })
1703        let coderef = Expr {
1704            kind: ExprKind::CodeRef {
1705                params: vec![],
1706                body,
1707            },
1708            line,
1709        };
1710        Ok(Statement {
1711            label: None,
1712            kind: StmtKind::Expression(Expr {
1713                kind: ExprKind::FuncCall {
1714                    name: "defer__internal".to_string(),
1715                    args: vec![coderef],
1716                },
1717                line,
1718            }),
1719            line,
1720        })
1721    }
1722
1723    /// `try { } catch ($err) { }` with optional `finally { }`
1724    fn parse_try_catch(&mut self) -> PerlResult<Statement> {
1725        let line = self.peek_line();
1726        self.advance(); // try
1727        let try_block = self.parse_block()?;
1728        match self.peek() {
1729            Token::Ident(ref k) if k == "catch" => {
1730                self.advance();
1731            }
1732            _ => {
1733                return Err(self.syntax_err("expected 'catch' after try block", self.peek_line()));
1734            }
1735        }
1736        self.expect(&Token::LParen)?;
1737        let catch_var = self.parse_scalar_var_name()?;
1738        self.expect(&Token::RParen)?;
1739        let catch_block = self.parse_block()?;
1740        let finally_block = match self.peek() {
1741            Token::Ident(ref k) if k == "finally" => {
1742                self.advance();
1743                Some(self.parse_block()?)
1744            }
1745            _ => None,
1746        };
1747        self.eat(&Token::Semicolon);
1748        Ok(Statement {
1749            label: None,
1750            kind: StmtKind::TryCatch {
1751                try_block,
1752                catch_var,
1753                catch_block,
1754                finally_block,
1755            },
1756            line,
1757        })
1758    }
1759
1760    /// `thread EXPR stage1 stage2 ...` — Clojure-style threading macro.
1761    /// Desugars to `EXPR |> stage1 |> stage2 |> ...`
1762    ///
1763    /// When `thread_last` is true (`->>` syntax), injects as last arg instead of first.
1764    ///
1765    /// When invoked as the RHS of `|>` (e.g. `LHS |> t s1 s2 ...`), the init
1766    /// is not parsed from tokens — using `parse_unary()` there lets the first
1767    /// bareword greedily consume the next token as its arg, which misparses
1768    /// `t inc pow($_, 2) p` as init=`inc(pow(…))` + stage=`p` instead of three
1769    /// separate stages. Instead, seed init with `$_[0]`, run every remaining
1770    /// token through the stage loop, and wrap the resulting chain in a
1771    /// `CodeRef`. The outer `pipe_forward_apply` then calls it with `lhs` as
1772    /// `$_[0]`, giving `LHS |> t s1 s2 s3` == `LHS |> s1 |> s2 |> s3`.
1773    fn parse_thread_macro(&mut self, _line: usize, thread_last: bool) -> PerlResult<Expr> {
1774        // Set thread-last mode for pipe_forward_apply calls within this macro
1775        let saved_thread_last = self.thread_last_mode;
1776        self.thread_last_mode = thread_last;
1777
1778        let pipe_rhs_wrap = self.in_pipe_rhs();
1779        let mut result = if pipe_rhs_wrap {
1780            Expr {
1781                kind: ExprKind::ArrayElement {
1782                    array: "_".to_string(),
1783                    index: Box::new(Expr {
1784                        kind: ExprKind::Integer(0),
1785                        line: _line,
1786                    }),
1787                },
1788                line: _line,
1789            }
1790        } else {
1791            // Suppress paren-less function calls so `t Color::Red p` parses
1792            // the enum variant without consuming `p` as an argument.
1793            self.suppress_parenless_call = self.suppress_parenless_call.saturating_add(1);
1794            let expr = self.parse_thread_input();
1795            self.suppress_parenless_call = self.suppress_parenless_call.saturating_sub(1);
1796            expr?
1797        };
1798
1799        // Track line where the last stage ended (initially the input expression's line).
1800        let mut last_stage_end_line = self.prev_line();
1801
1802        // Parse stages until we hit a statement terminator
1803        loop {
1804            // Newline termination: if the next token is on a different line than where
1805            // the previous stage ended, the thread macro terminates. This allows
1806            // `~> @arr map { $_ * 2 }` on one line followed by `my @b = ...` on the next
1807            // without requiring a semicolon.
1808            if self.peek_line() > last_stage_end_line {
1809                break;
1810            }
1811
1812            // Check for terminators - |> ends thread and allows piping the result.
1813            // Variables ($x, @x, %x) and declaration keywords (my, our, local, state)
1814            // cannot be stages, so they implicitly terminate the thread macro.
1815            match self.peek() {
1816                Token::Semicolon
1817                | Token::RBrace
1818                | Token::RParen
1819                | Token::RBracket
1820                | Token::PipeForward
1821                | Token::Eof
1822                | Token::ScalarVar(_)
1823                | Token::ArrayVar(_)
1824                | Token::HashVar(_)
1825                | Token::Comma => break,
1826                Token::Ident(ref kw)
1827                    if matches!(
1828                        kw.as_str(),
1829                        "my" | "our"
1830                            | "local"
1831                            | "state"
1832                            | "if"
1833                            | "unless"
1834                            | "while"
1835                            | "until"
1836                            | "for"
1837                            | "foreach"
1838                            | "return"
1839                            | "last"
1840                            | "next"
1841                            | "redo"
1842                    ) =>
1843                {
1844                    break
1845                }
1846                _ => {}
1847            }
1848
1849            let stage_line = self.peek_line();
1850
1851            // Parse a stage and apply it to result via pipe
1852            match self.peek().clone() {
1853                // `>{ block }` — standalone anonymous block (sugar for fn { })
1854                Token::ArrowBrace => {
1855                    self.advance(); // consume `>{`
1856                    let mut stmts = Vec::new();
1857                    while !matches!(self.peek(), Token::RBrace | Token::Eof) {
1858                        if self.eat(&Token::Semicolon) {
1859                            continue;
1860                        }
1861                        stmts.push(self.parse_statement()?);
1862                    }
1863                    self.expect(&Token::RBrace)?;
1864                    let code_ref = Expr {
1865                        kind: ExprKind::CodeRef {
1866                            params: vec![],
1867                            body: stmts,
1868                        },
1869                        line: stage_line,
1870                    };
1871                    result = self.pipe_forward_apply(result, code_ref, stage_line)?;
1872                }
1873                // `sub { block }` — blocked in no-interop mode
1874                Token::Ident(ref name) if name == "sub" => {
1875                    if crate::no_interop_mode() {
1876                        return Err(self.syntax_err(
1877                            "stryke uses `fn {}` instead of `sub {}` (--no-interop)",
1878                            stage_line,
1879                        ));
1880                    }
1881                    self.advance(); // consume `sub`
1882                    let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
1883                    let body = self.parse_block()?;
1884                    let code_ref = Expr {
1885                        kind: ExprKind::CodeRef { params, body },
1886                        line: stage_line,
1887                    };
1888                    result = self.pipe_forward_apply(result, code_ref, stage_line)?;
1889                }
1890                // `fn { block }` — stryke anonymous function
1891                Token::Ident(ref name) if name == "fn" => {
1892                    self.advance(); // consume `fn`
1893                    let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
1894                    let body = self.parse_block()?;
1895                    let code_ref = Expr {
1896                        kind: ExprKind::CodeRef { params, body },
1897                        line: stage_line,
1898                    };
1899                    result = self.pipe_forward_apply(result, code_ref, stage_line)?;
1900                }
1901                // `ident` possibly followed by block (or namespaced like `Foo::Bar::func`)
1902                Token::Ident(ref name) => {
1903                    let mut func_name = name.clone();
1904                    self.advance();
1905
1906                    // Collect namespaced function name (e.g., Rosetta::Stack::push)
1907                    while matches!(self.peek(), Token::PackageSep) {
1908                        self.advance(); // consume `::`
1909                        if let Token::Ident(ref part) = self.peek().clone() {
1910                            func_name.push_str("::");
1911                            func_name.push_str(part);
1912                            self.advance();
1913                        } else {
1914                            return Err(self.syntax_err(
1915                                format!(
1916                                    "Expected identifier after `::` in thread stage, got {:?}",
1917                                    self.peek()
1918                                ),
1919                                stage_line,
1920                            ));
1921                        }
1922                    }
1923
1924                    // Handle s/// and tr/// encoded tokens
1925                    if func_name.starts_with('\x00') {
1926                        let parts: Vec<&str> = func_name.split('\x00').collect();
1927                        if parts.len() >= 4 && parts[1] == "s" {
1928                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
1929                            let stage = Expr {
1930                                kind: ExprKind::Substitution {
1931                                    expr: Box::new(result.clone()),
1932                                    pattern: parts[2].to_string(),
1933                                    replacement: parts[3].to_string(),
1934                                    flags: format!("{}r", parts.get(4).unwrap_or(&"")),
1935                                    delim,
1936                                },
1937                                line: stage_line,
1938                            };
1939                            result = stage;
1940                            last_stage_end_line = self.prev_line();
1941                            continue;
1942                        }
1943                        if parts.len() >= 4 && parts[1] == "tr" {
1944                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
1945                            let stage = Expr {
1946                                kind: ExprKind::Transliterate {
1947                                    expr: Box::new(result.clone()),
1948                                    from: parts[2].to_string(),
1949                                    to: parts[3].to_string(),
1950                                    flags: format!("{}r", parts.get(4).unwrap_or(&"")),
1951                                    delim,
1952                                },
1953                                line: stage_line,
1954                            };
1955                            result = stage;
1956                            last_stage_end_line = self.prev_line();
1957                            continue;
1958                        }
1959                        return Err(
1960                            self.syntax_err("Unexpected encoded token in thread", stage_line)
1961                        );
1962                    }
1963
1964                    // `map +{ ... }` — hashref expression form (not a code block).
1965                    // The `+` disambiguates: `+{` is always a hashref constructor.
1966                    // Desugars to `MapExprComma` so pipe_forward_apply threads the
1967                    // list correctly: `t LIST map +{k => $_}` → `map +{k => $_}, LIST`.
1968                    if matches!(self.peek(), Token::Plus)
1969                        && matches!(self.peek_at(1), Token::LBrace)
1970                    {
1971                        self.advance(); // consume `+`
1972                        self.expect(&Token::LBrace)?;
1973                        // try_parse_hash_ref consumes the closing `}`
1974                        let pairs = self.try_parse_hash_ref()?;
1975                        let hashref_expr = Expr {
1976                            kind: ExprKind::HashRef(pairs),
1977                            line: stage_line,
1978                        };
1979                        let flatten_array_refs =
1980                            matches!(func_name.as_str(), "flat_map" | "flat_maps");
1981                        let stream = matches!(func_name.as_str(), "maps" | "flat_maps");
1982                        // Placeholder list — pipe_forward_apply replaces it with `result`.
1983                        let placeholder = Expr {
1984                            kind: ExprKind::Undef,
1985                            line: stage_line,
1986                        };
1987                        let map_node = Expr {
1988                            kind: ExprKind::MapExprComma {
1989                                expr: Box::new(hashref_expr),
1990                                list: Box::new(placeholder),
1991                                flatten_array_refs,
1992                                stream,
1993                            },
1994                            line: stage_line,
1995                        };
1996                        result = self.pipe_forward_apply(result, map_node, stage_line)?;
1997                    // `pmap_chunked CHUNK_SIZE { BLOCK }` — parallel chunked map
1998                    } else if func_name == "pmap_chunked" {
1999                        let chunk_size = self.parse_assign_expr()?;
2000                        let block = self.parse_block_or_bareword_block()?;
2001                        let placeholder = self.pipe_placeholder_list(stage_line);
2002                        let stage = Expr {
2003                            kind: ExprKind::PMapChunkedExpr {
2004                                chunk_size: Box::new(chunk_size),
2005                                block,
2006                                list: Box::new(placeholder),
2007                                progress: None,
2008                            },
2009                            line: stage_line,
2010                        };
2011                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2012                    // `preduce_init INIT { BLOCK }` — parallel reduce with init value
2013                    } else if func_name == "preduce_init" {
2014                        let init = self.parse_assign_expr()?;
2015                        let block = self.parse_block_or_bareword_block()?;
2016                        let placeholder = self.pipe_placeholder_list(stage_line);
2017                        let stage = Expr {
2018                            kind: ExprKind::PReduceInitExpr {
2019                                init: Box::new(init),
2020                                block,
2021                                list: Box::new(placeholder),
2022                                progress: None,
2023                            },
2024                            line: stage_line,
2025                        };
2026                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2027                    // `pmap_reduce { MAP } { REDUCE }` — parallel map-reduce
2028                    } else if func_name == "pmap_reduce" {
2029                        let map_block = self.parse_block_or_bareword_block()?;
2030                        let reduce_block = if matches!(self.peek(), Token::LBrace) {
2031                            self.parse_block()?
2032                        } else {
2033                            self.expect(&Token::Comma)?;
2034                            self.parse_block_or_bareword_cmp_block()?
2035                        };
2036                        let placeholder = self.pipe_placeholder_list(stage_line);
2037                        let stage = Expr {
2038                            kind: ExprKind::PMapReduceExpr {
2039                                map_block,
2040                                reduce_block,
2041                                list: Box::new(placeholder),
2042                                progress: None,
2043                            },
2044                            line: stage_line,
2045                        };
2046                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2047                    // `pmap_on $cluster { BLOCK }` — parallel map dispatched to a remote
2048                    // cluster. Mirrors the `pmap_chunked` thread-stage shape; the cluster
2049                    // expression is parsed before the block, the threaded list slots in
2050                    // as the placeholder.
2051                    } else if func_name == "pmap_on" || func_name == "pflat_map_on" {
2052                        // Suppress `$cluster { ... }` auto-arrow (`$h->{...}`) so the
2053                        // brace opens the block, not a hash subscript.
2054                        self.suppress_scalar_hash_brace =
2055                            self.suppress_scalar_hash_brace.saturating_add(1);
2056                        let cluster = self.parse_assign_expr();
2057                        self.suppress_scalar_hash_brace =
2058                            self.suppress_scalar_hash_brace.saturating_sub(1);
2059                        let cluster = cluster?;
2060                        // Optional comma between cluster and block (matches the
2061                        // canonical `pmap_on $c, { BLOCK } @list` form in the LSP docs).
2062                        self.eat(&Token::Comma);
2063                        let block = self.parse_block_or_bareword_block()?;
2064                        let placeholder = self.pipe_placeholder_list(stage_line);
2065                        let stage = Expr {
2066                            kind: ExprKind::PMapExpr {
2067                                block,
2068                                list: Box::new(placeholder),
2069                                progress: None,
2070                                flat_outputs: func_name == "pflat_map_on",
2071                                on_cluster: Some(Box::new(cluster)),
2072                                stream: false,
2073                            },
2074                            line: stage_line,
2075                        };
2076                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2077                    // Check if followed by a block (like `filter { }`, `sort { }`, `map { }`)
2078                    } else if matches!(self.peek(), Token::LBrace) {
2079                        // Parse as a block-taking builtin
2080                        self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_add(1);
2081                        let stage = self.parse_thread_stage_with_block(&func_name, stage_line)?;
2082                        self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_sub(1);
2083                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2084                    } else if matches!(self.peek(), Token::LParen) {
2085                        // Special handling for join(sep) and split(pattern) in thread context.
2086                        // These take the threaded list/string as their data argument, not as $_.
2087                        if func_name == "join" {
2088                            self.advance(); // consume `(`
2089                            let separator = self.parse_assign_expr()?;
2090                            self.expect(&Token::RParen)?;
2091                            let placeholder = self.pipe_placeholder_list(stage_line);
2092                            let stage = Expr {
2093                                kind: ExprKind::JoinExpr {
2094                                    separator: Box::new(separator),
2095                                    list: Box::new(placeholder),
2096                                },
2097                                line: stage_line,
2098                            };
2099                            result = self.pipe_forward_apply(result, stage, stage_line)?;
2100                        } else if func_name == "split" {
2101                            self.advance(); // consume `(`
2102                            let pattern = self.parse_assign_expr()?;
2103                            let limit = if self.eat(&Token::Comma) {
2104                                Some(Box::new(self.parse_assign_expr()?))
2105                            } else {
2106                                None
2107                            };
2108                            self.expect(&Token::RParen)?;
2109                            let placeholder = Expr {
2110                                kind: ExprKind::ScalarVar("_".to_string()),
2111                                line: stage_line,
2112                            };
2113                            let stage = Expr {
2114                                kind: ExprKind::SplitExpr {
2115                                    pattern: Box::new(pattern),
2116                                    string: Box::new(placeholder),
2117                                    limit,
2118                                },
2119                                line: stage_line,
2120                            };
2121                            result = self.pipe_forward_apply(result, stage, stage_line)?;
2122                        } else {
2123                            // `name($_-bearing-args)` — parse explicit args, require at
2124                            // least one `$_` placeholder, then wrap as a `>{...}` block
2125                            // so the threaded value binds to `$_` at any position.
2126                            // Examples:
2127                            //   t 10 add2($_, 5) p      → add2(10, 5)
2128                            //   t 10 sub2(20, $_) p     → sub2(20, 10)
2129                            //   t 10 add3($_, 5, 10) p  → add3(10, 5, 10)
2130                            // To pass the threaded value as a sole arg, use bare form:
2131                            //   t 10 add2 p   (not `add2()`)
2132                            self.advance(); // consume `(`
2133                            let mut call_args = Vec::new();
2134                            while !matches!(self.peek(), Token::RParen | Token::Eof) {
2135                                call_args.push(self.parse_assign_expr()?);
2136                                if !self.eat(&Token::Comma) {
2137                                    break;
2138                                }
2139                            }
2140                            self.expect(&Token::RParen)?;
2141                            // If no `$_` placeholder, auto-inject threaded value.
2142                            // Thread-first: `t data to_file("/tmp/o.html")` → `to_file($_, "/tmp/o.html")`
2143                            // Thread-last: `->> data to_file("/tmp/o.html")` → `to_file("/tmp/o.html", $_)`
2144                            if !call_args.iter().any(Self::expr_contains_topic_var) {
2145                                let topic = Expr {
2146                                    kind: ExprKind::ScalarVar("_".to_string()),
2147                                    line: stage_line,
2148                                };
2149                                if self.thread_last_mode {
2150                                    call_args.push(topic);
2151                                } else {
2152                                    call_args.insert(0, topic);
2153                                }
2154                            }
2155                            let call_expr = Expr {
2156                                kind: ExprKind::FuncCall {
2157                                    name: func_name.clone(),
2158                                    args: call_args,
2159                                },
2160                                line: stage_line,
2161                            };
2162                            let code_ref = Expr {
2163                                kind: ExprKind::CodeRef {
2164                                    params: vec![],
2165                                    body: vec![Statement {
2166                                        label: None,
2167                                        kind: StmtKind::Expression(call_expr),
2168                                        line: stage_line,
2169                                    }],
2170                                },
2171                                line: stage_line,
2172                            };
2173                            result = self.pipe_forward_apply(result, code_ref, stage_line)?;
2174                        }
2175                    } else {
2176                        // Bare function name — handle unary builtins specially
2177                        result = self.thread_apply_bare_func(&func_name, result, stage_line)?;
2178                    }
2179                }
2180                // `/pattern/flags` — grep filter (desugar to `grep { /pattern/flags }`)
2181                Token::Regex(ref pattern, ref flags, delim) => {
2182                    let pattern = pattern.clone();
2183                    let flags = flags.clone();
2184                    self.advance();
2185                    result =
2186                        self.thread_regex_grep_stage(result, pattern, flags, delim, stage_line);
2187                }
2188                // Handle `/` that was lexed as Slash (division) because it followed a term.
2189                // In thread stage context, `/pattern/` should be a regex filter.
2190                Token::Slash => {
2191                    self.advance(); // consume opening /
2192
2193                    // Special case: if next token is Ident("m") or similar followed by Regex,
2194                    // the lexer interpreted `/m/` as `/ m/pattern/` where `m/` started a new regex.
2195                    // We need to handle this: the pattern is just "m" (or whatever the ident is).
2196                    if let Token::Ident(ref ident_s) = self.peek().clone() {
2197                        if matches!(ident_s.as_str(), "m" | "s" | "tr" | "y" | "qr")
2198                            && matches!(self.peek_at(1), Token::Regex(..))
2199                        {
2200                            // The `m` (or s/tr/y/qr) is our pattern, the Regex token was misparsed
2201                            self.advance(); // consume the ident
2202                                            // The Token::Regex after it was a misparsed `m/...` - we need to
2203                                            // extract what would have been the closing `/` situation.
2204                                            // Actually, the lexer consumed everything. Let's just use the ident
2205                                            // as the pattern and expect a closing slash.
2206                            if let Token::Regex(ref misparsed_pattern, ref misparsed_flags, _) =
2207                                self.peek().clone()
2208                            {
2209                                // The misparsed regex ate our closing `/`.
2210                                // For `/m/`, lexer saw `m/` and parsed until next `/`, finding nothing or wrong content.
2211                                // Actually for `/m/ less`, after Slash, lexer sees `m`, then `/`,
2212                                // interprets as m// regex start, reads until next `/` (none) -> error.
2213                                // So we shouldn't reach here if there was an error.
2214                                // But if lexer succeeded parsing `m/ less/` as regex, we'd have wrong pattern.
2215                                // This is getting complicated. Let me try a different approach.
2216                                // Just consume the Regex token and issue a warning? No, let's reconstruct.
2217                                // Skip for now and fall through to manual parsing.
2218                                let _ = (misparsed_pattern, misparsed_flags);
2219                            }
2220                        }
2221                    }
2222
2223                    // Manually parse the regex pattern from tokens until we hit another Slash
2224                    let mut pattern = String::new();
2225                    loop {
2226                        match self.peek().clone() {
2227                            Token::Slash => {
2228                                self.advance(); // consume closing /
2229                                break;
2230                            }
2231                            Token::Eof | Token::Semicolon | Token::Newline => {
2232                                return Err(self
2233                                    .syntax_err("Unterminated regex in thread stage", stage_line));
2234                            }
2235                            // Handle case where lexer misparsed m/pattern/ as Ident("m") + Regex
2236                            Token::Regex(ref inner_pattern, ref inner_flags, delim) => {
2237                                // This means `/m/` was lexed as Slash, then `m/` started a regex.
2238                                // The Regex token contains whatever was between the inner `m/` and closing `/`.
2239                                // For `/m/ less`, lexer would fail earlier. For `/m/i`, it might work weirdly.
2240                                // The safest: if we see a Regex token here and pattern is empty or just "m"/"s"/etc,
2241                                // treat the previous ident as the whole pattern and this Regex as misparsed.
2242                                // Actually, let's just prepend the ident we may have seen and use empty pattern.
2243                                // This is a lexer bug workaround.
2244                                if pattern.is_empty()
2245                                    || matches!(pattern.as_str(), "m" | "s" | "tr" | "y" | "qr")
2246                                {
2247                                    // The whole thing was probably `/X/` where X is m/s/tr/y/qr
2248                                    // and lexer misparsed. The Regex token is garbage.
2249                                    // Just use the ident as pattern and ignore this Regex.
2250                                    // But we already advanced past the ident...
2251                                    // This is messy. Let me try a cleaner approach.
2252                                    let _ = (inner_pattern, inner_flags, delim);
2253                                }
2254                                // For now, error out - this case is too complex
2255                                return Err(self.syntax_err(
2256                                    "Complex regex in thread stage - use m/pattern/ syntax instead",
2257                                    stage_line,
2258                                ));
2259                            }
2260                            Token::Ident(ref s) => {
2261                                pattern.push_str(s);
2262                                self.advance();
2263                            }
2264                            Token::Integer(n) => {
2265                                pattern.push_str(&n.to_string());
2266                                self.advance();
2267                            }
2268                            Token::ScalarVar(ref v) => {
2269                                pattern.push('$');
2270                                pattern.push_str(v);
2271                                self.advance();
2272                            }
2273                            Token::Dot => {
2274                                pattern.push('.');
2275                                self.advance();
2276                            }
2277                            Token::Star => {
2278                                pattern.push('*');
2279                                self.advance();
2280                            }
2281                            Token::Plus => {
2282                                pattern.push('+');
2283                                self.advance();
2284                            }
2285                            Token::Question => {
2286                                pattern.push('?');
2287                                self.advance();
2288                            }
2289                            Token::LParen => {
2290                                pattern.push('(');
2291                                self.advance();
2292                            }
2293                            Token::RParen => {
2294                                pattern.push(')');
2295                                self.advance();
2296                            }
2297                            Token::LBracket => {
2298                                pattern.push('[');
2299                                self.advance();
2300                            }
2301                            Token::RBracket => {
2302                                pattern.push(']');
2303                                self.advance();
2304                            }
2305                            Token::Backslash => {
2306                                pattern.push('\\');
2307                                self.advance();
2308                            }
2309                            Token::BitOr => {
2310                                pattern.push('|');
2311                                self.advance();
2312                            }
2313                            Token::Power => {
2314                                pattern.push_str("**");
2315                                self.advance();
2316                            }
2317                            Token::BitXor => {
2318                                pattern.push('^');
2319                                self.advance();
2320                            }
2321                            Token::Minus => {
2322                                pattern.push('-');
2323                                self.advance();
2324                            }
2325                            _ => {
2326                                return Err(self.syntax_err(
2327                                    format!("Unexpected token in regex pattern: {:?}", self.peek()),
2328                                    stage_line,
2329                                ));
2330                            }
2331                        }
2332                    }
2333                    // Parse optional flags (sequence of letters after closing /)
2334                    // Be careful: single letters like 'e' could be regex flags OR thread
2335                    // stages like `fore`/`e`. If followed by `{`, it's a stage, not a flag.
2336                    let mut flags = String::new();
2337                    if let Token::Ident(ref s) = self.peek().clone() {
2338                        let is_flag_only =
2339                            s.chars().all(|c| "gimsxecor".contains(c)) && s.len() <= 6;
2340                        let followed_by_brace = matches!(self.peek_at(1), Token::LBrace);
2341                        if is_flag_only && !followed_by_brace {
2342                            flags.push_str(s);
2343                            self.advance();
2344                        }
2345                    }
2346                    result = self.thread_regex_grep_stage(result, pattern, flags, '/', stage_line);
2347                }
2348                tok => {
2349                    return Err(self.syntax_err(
2350                        format!(
2351                            "thread: expected stage (ident, fn {{}}, s///, tr///, or /re/), got {:?}",
2352                            tok
2353                        ),
2354                        stage_line,
2355                    ));
2356                }
2357            };
2358            last_stage_end_line = self.prev_line();
2359        }
2360
2361        // Restore thread-last mode
2362        self.thread_last_mode = saved_thread_last;
2363
2364        if pipe_rhs_wrap {
2365            // Wrap as `fn { …stages threaded from $_[0]… }` so the outer
2366            // `pipe_forward_apply` can invoke it with `lhs` as the arg.
2367            let body_line = result.line;
2368            return Ok(Expr {
2369                kind: ExprKind::CodeRef {
2370                    params: vec![],
2371                    body: vec![Statement {
2372                        label: None,
2373                        kind: StmtKind::Expression(result),
2374                        line: body_line,
2375                    }],
2376                },
2377                line: _line,
2378            });
2379        }
2380        Ok(result)
2381    }
2382
2383    /// Build a grep filter stage from a regex pattern for the thread macro.
2384    fn thread_regex_grep_stage(
2385        &self,
2386        list: Expr,
2387        pattern: String,
2388        flags: String,
2389        delim: char,
2390        line: usize,
2391    ) -> Expr {
2392        let topic = Expr {
2393            kind: ExprKind::ScalarVar("_".to_string()),
2394            line,
2395        };
2396        let match_expr = Expr {
2397            kind: ExprKind::Match {
2398                expr: Box::new(topic),
2399                pattern,
2400                flags,
2401                scalar_g: false,
2402                delim,
2403            },
2404            line,
2405        };
2406        let block = vec![Statement {
2407            label: None,
2408            kind: StmtKind::Expression(match_expr),
2409            line,
2410        }];
2411        Expr {
2412            kind: ExprKind::GrepExpr {
2413                block,
2414                list: Box::new(list),
2415                keyword: crate::ast::GrepBuiltinKeyword::Grep,
2416            },
2417            line,
2418        }
2419    }
2420
2421    /// Check whether an expression contains a `$_` reference anywhere in its sub-tree.
2422    /// Used by the thread macro to validate `name(args)` call-stages: the threaded
2423    /// value is bound to `$_` via a wrapping CodeRef, so at least one `$_` placeholder
2424    /// must appear in the args, otherwise the threaded value is silently dropped.
2425    ///
2426    /// Implementation uses Rust's `Debug` to serialize the entire sub-tree once and
2427    /// scan for the canonical `ScalarVar("_")` representation. This avoids a
2428    /// per-variant walker that would need to be updated whenever new `ExprKind`
2429    /// variants are added (and would silently miss any it forgot to handle).
2430    /// Parse-time perf is non-critical and the AST is small at this scope.
2431    fn expr_contains_topic_var(e: &Expr) -> bool {
2432        format!("{:?}", e).contains("ScalarVar(\"_\")")
2433    }
2434
2435    /// Apply a bare function name in thread context, handling unary builtins specially.
2436    fn thread_apply_bare_func(&self, name: &str, arg: Expr, line: usize) -> PerlResult<Expr> {
2437        let kind = match name {
2438            // String functions
2439            "uc" => ExprKind::Uc(Box::new(arg)),
2440            "lc" => ExprKind::Lc(Box::new(arg)),
2441            "ucfirst" | "ufc" => ExprKind::Ucfirst(Box::new(arg)),
2442            "lcfirst" | "lfc" => ExprKind::Lcfirst(Box::new(arg)),
2443            "fc" => ExprKind::Fc(Box::new(arg)),
2444            "chomp" => ExprKind::Chomp(Box::new(arg)),
2445            "chop" => ExprKind::Chop(Box::new(arg)),
2446            "length" => ExprKind::Length(Box::new(arg)),
2447            "len" | "cnt" => ExprKind::FuncCall {
2448                name: "count".to_string(),
2449                args: vec![arg],
2450            },
2451            "quotemeta" | "qm" => ExprKind::FuncCall {
2452                name: "quotemeta".to_string(),
2453                args: vec![arg],
2454            },
2455            // Numeric functions
2456            "abs" => ExprKind::Abs(Box::new(arg)),
2457            "int" => ExprKind::Int(Box::new(arg)),
2458            "sqrt" | "sq" => ExprKind::Sqrt(Box::new(arg)),
2459            "sin" => ExprKind::Sin(Box::new(arg)),
2460            "cos" => ExprKind::Cos(Box::new(arg)),
2461            "exp" => ExprKind::Exp(Box::new(arg)),
2462            "log" => ExprKind::Log(Box::new(arg)),
2463            "hex" => ExprKind::Hex(Box::new(arg)),
2464            "oct" => ExprKind::Oct(Box::new(arg)),
2465            "chr" => ExprKind::Chr(Box::new(arg)),
2466            "ord" => ExprKind::Ord(Box::new(arg)),
2467            // Type/ref functions
2468            "defined" | "def" => ExprKind::Defined(Box::new(arg)),
2469            "ref" => ExprKind::Ref(Box::new(arg)),
2470            "scalar" => ExprKind::ScalarContext(Box::new(arg)),
2471            // Array/hash functions
2472            "keys" => ExprKind::Keys(Box::new(arg)),
2473            "values" => ExprKind::Values(Box::new(arg)),
2474            "each" => ExprKind::Each(Box::new(arg)),
2475            "pop" => ExprKind::Pop(Box::new(arg)),
2476            "shift" => ExprKind::Shift(Box::new(arg)),
2477            "reverse" => {
2478                if crate::no_interop_mode() {
2479                    return Err(self.syntax_err(
2480                        "stryke uses `rev` instead of `reverse` (--no-interop)",
2481                        line,
2482                    ));
2483                }
2484                ExprKind::ReverseExpr(Box::new(arg))
2485            }
2486            "reversed" | "rv" | "rev" => ExprKind::Rev(Box::new(arg)),
2487            "sort" | "so" => ExprKind::SortExpr {
2488                cmp: None,
2489                list: Box::new(arg),
2490            },
2491            "psort" => ExprKind::PSortExpr {
2492                cmp: None,
2493                list: Box::new(arg),
2494                progress: None,
2495            },
2496            "uniq" | "distinct" | "uq" => ExprKind::FuncCall {
2497                name: "uniq".to_string(),
2498                args: vec![arg],
2499            },
2500            "trim" | "tm" => ExprKind::FuncCall {
2501                name: "trim".to_string(),
2502                args: vec![arg],
2503            },
2504            "flatten" | "fl" => ExprKind::FuncCall {
2505                name: "flatten".to_string(),
2506                args: vec![arg],
2507            },
2508            "compact" | "cpt" => ExprKind::FuncCall {
2509                name: "compact".to_string(),
2510                args: vec![arg],
2511            },
2512            "shuffle" | "shuf" => ExprKind::FuncCall {
2513                name: "shuffle".to_string(),
2514                args: vec![arg],
2515            },
2516            "frequencies" | "freq" | "frq" => ExprKind::FuncCall {
2517                name: "frequencies".to_string(),
2518                args: vec![arg],
2519            },
2520            "dedup" | "dup" => ExprKind::FuncCall {
2521                name: "dedup".to_string(),
2522                args: vec![arg],
2523            },
2524            "enumerate" | "en" => ExprKind::FuncCall {
2525                name: "enumerate".to_string(),
2526                args: vec![arg],
2527            },
2528            "lines" | "ln" => ExprKind::FuncCall {
2529                name: "lines".to_string(),
2530                args: vec![arg],
2531            },
2532            "words" | "wd" => ExprKind::FuncCall {
2533                name: "words".to_string(),
2534                args: vec![arg],
2535            },
2536            "chars" | "ch" => ExprKind::FuncCall {
2537                name: "chars".to_string(),
2538                args: vec![arg],
2539            },
2540            "digits" | "dg" => ExprKind::FuncCall {
2541                name: "digits".to_string(),
2542                args: vec![arg],
2543            },
2544            "letters" | "lts" => ExprKind::FuncCall {
2545                name: "letters".to_string(),
2546                args: vec![arg],
2547            },
2548            "letters_uc" => ExprKind::FuncCall {
2549                name: "letters_uc".to_string(),
2550                args: vec![arg],
2551            },
2552            "letters_lc" => ExprKind::FuncCall {
2553                name: "letters_lc".to_string(),
2554                args: vec![arg],
2555            },
2556            "punctuation" | "punct" => ExprKind::FuncCall {
2557                name: "punctuation".to_string(),
2558                args: vec![arg],
2559            },
2560            "sentences" | "sents" => ExprKind::FuncCall {
2561                name: "sentences".to_string(),
2562                args: vec![arg],
2563            },
2564            "paragraphs" | "paras" => ExprKind::FuncCall {
2565                name: "paragraphs".to_string(),
2566                args: vec![arg],
2567            },
2568            "sections" | "sects" => ExprKind::FuncCall {
2569                name: "sections".to_string(),
2570                args: vec![arg],
2571            },
2572            "numbers" | "nums" => ExprKind::FuncCall {
2573                name: "numbers".to_string(),
2574                args: vec![arg],
2575            },
2576            "graphemes" | "grs" => ExprKind::FuncCall {
2577                name: "graphemes".to_string(),
2578                args: vec![arg],
2579            },
2580            "columns" | "cols" => ExprKind::FuncCall {
2581                name: "columns".to_string(),
2582                args: vec![arg],
2583            },
2584            // File functions
2585            "slurp" | "sl" => ExprKind::Slurp(Box::new(arg)),
2586            "chdir" => ExprKind::Chdir(Box::new(arg)),
2587            "stat" => ExprKind::Stat(Box::new(arg)),
2588            "lstat" => ExprKind::Lstat(Box::new(arg)),
2589            "readlink" => ExprKind::Readlink(Box::new(arg)),
2590            "readdir" => ExprKind::Readdir(Box::new(arg)),
2591            "close" => ExprKind::Close(Box::new(arg)),
2592            "basename" | "bn" => ExprKind::FuncCall {
2593                name: "basename".to_string(),
2594                args: vec![arg],
2595            },
2596            "dirname" | "dn" => ExprKind::FuncCall {
2597                name: "dirname".to_string(),
2598                args: vec![arg],
2599            },
2600            "realpath" | "rp" => ExprKind::FuncCall {
2601                name: "realpath".to_string(),
2602                args: vec![arg],
2603            },
2604            "which" | "wh" => ExprKind::FuncCall {
2605                name: "which".to_string(),
2606                args: vec![arg],
2607            },
2608            // Other
2609            "eval" => ExprKind::Eval(Box::new(arg)),
2610            "require" => ExprKind::Require(Box::new(arg)),
2611            "study" => ExprKind::Study(Box::new(arg)),
2612            // Case conversion
2613            "snake_case" | "sc" => ExprKind::FuncCall {
2614                name: "snake_case".to_string(),
2615                args: vec![arg],
2616            },
2617            "camel_case" | "cc" => ExprKind::FuncCall {
2618                name: "camel_case".to_string(),
2619                args: vec![arg],
2620            },
2621            "kebab_case" | "kc" => ExprKind::FuncCall {
2622                name: "kebab_case".to_string(),
2623                args: vec![arg],
2624            },
2625            // Serialization
2626            "to_json" | "tj" => ExprKind::FuncCall {
2627                name: "to_json".to_string(),
2628                args: vec![arg],
2629            },
2630            "to_yaml" | "ty" => ExprKind::FuncCall {
2631                name: "to_yaml".to_string(),
2632                args: vec![arg],
2633            },
2634            "to_toml" | "tt" => ExprKind::FuncCall {
2635                name: "to_toml".to_string(),
2636                args: vec![arg],
2637            },
2638            "to_csv" | "tc" => ExprKind::FuncCall {
2639                name: "to_csv".to_string(),
2640                args: vec![arg],
2641            },
2642            "to_xml" | "tx" => ExprKind::FuncCall {
2643                name: "to_xml".to_string(),
2644                args: vec![arg],
2645            },
2646            "to_html" | "th" => ExprKind::FuncCall {
2647                name: "to_html".to_string(),
2648                args: vec![arg],
2649            },
2650            "to_markdown" | "to_md" | "tmd" => ExprKind::FuncCall {
2651                name: "to_markdown".to_string(),
2652                args: vec![arg],
2653            },
2654            "xopen" | "xo" => ExprKind::FuncCall {
2655                name: "xopen".to_string(),
2656                args: vec![arg],
2657            },
2658            "clip" | "clipboard" | "pbcopy" => ExprKind::FuncCall {
2659                name: "clip".to_string(),
2660                args: vec![arg],
2661            },
2662            "to_table" | "table" | "tbl" => ExprKind::FuncCall {
2663                name: "to_table".to_string(),
2664                args: vec![arg],
2665            },
2666            "sparkline" | "spark" => ExprKind::FuncCall {
2667                name: "sparkline".to_string(),
2668                args: vec![arg],
2669            },
2670            "bar_chart" | "bars" => ExprKind::FuncCall {
2671                name: "bar_chart".to_string(),
2672                args: vec![arg],
2673            },
2674            "flame" | "flamechart" => ExprKind::FuncCall {
2675                name: "flame".to_string(),
2676                args: vec![arg],
2677            },
2678            "ddump" | "dd" => ExprKind::FuncCall {
2679                name: "ddump".to_string(),
2680                args: vec![arg],
2681            },
2682            "say" => {
2683                if crate::no_interop_mode() {
2684                    return Err(
2685                        self.syntax_err("stryke uses `p` instead of `say` (--no-interop)", line)
2686                    );
2687                }
2688                ExprKind::Say {
2689                    handle: None,
2690                    args: vec![arg],
2691                }
2692            }
2693            "p" => ExprKind::Say {
2694                handle: None,
2695                args: vec![arg],
2696            },
2697            "print" => ExprKind::Print {
2698                handle: None,
2699                args: vec![arg],
2700            },
2701            "warn" => ExprKind::Warn(vec![arg]),
2702            "die" => ExprKind::Die(vec![arg]),
2703            "stringify" | "str" => ExprKind::FuncCall {
2704                name: "stringify".to_string(),
2705                args: vec![arg],
2706            },
2707            "json_decode" | "jd" => ExprKind::FuncCall {
2708                name: "json_decode".to_string(),
2709                args: vec![arg],
2710            },
2711            "yaml_decode" | "yd" => ExprKind::FuncCall {
2712                name: "yaml_decode".to_string(),
2713                args: vec![arg],
2714            },
2715            "toml_decode" | "td" => ExprKind::FuncCall {
2716                name: "toml_decode".to_string(),
2717                args: vec![arg],
2718            },
2719            "xml_decode" | "xd" => ExprKind::FuncCall {
2720                name: "xml_decode".to_string(),
2721                args: vec![arg],
2722            },
2723            "json_encode" | "je" => ExprKind::FuncCall {
2724                name: "json_encode".to_string(),
2725                args: vec![arg],
2726            },
2727            "yaml_encode" | "ye" => ExprKind::FuncCall {
2728                name: "yaml_encode".to_string(),
2729                args: vec![arg],
2730            },
2731            "toml_encode" | "te" => ExprKind::FuncCall {
2732                name: "toml_encode".to_string(),
2733                args: vec![arg],
2734            },
2735            "xml_encode" | "xe" => ExprKind::FuncCall {
2736                name: "xml_encode".to_string(),
2737                args: vec![arg],
2738            },
2739            // Encoding
2740            "base64_encode" | "b64e" => ExprKind::FuncCall {
2741                name: "base64_encode".to_string(),
2742                args: vec![arg],
2743            },
2744            "base64_decode" | "b64d" => ExprKind::FuncCall {
2745                name: "base64_decode".to_string(),
2746                args: vec![arg],
2747            },
2748            "hex_encode" | "hxe" => ExprKind::FuncCall {
2749                name: "hex_encode".to_string(),
2750                args: vec![arg],
2751            },
2752            "hex_decode" | "hxd" => ExprKind::FuncCall {
2753                name: "hex_decode".to_string(),
2754                args: vec![arg],
2755            },
2756            "url_encode" | "uri_escape" | "ue" => ExprKind::FuncCall {
2757                name: "url_encode".to_string(),
2758                args: vec![arg],
2759            },
2760            "url_decode" | "uri_unescape" | "ud" => ExprKind::FuncCall {
2761                name: "url_decode".to_string(),
2762                args: vec![arg],
2763            },
2764            "gzip" | "gz" => ExprKind::FuncCall {
2765                name: "gzip".to_string(),
2766                args: vec![arg],
2767            },
2768            "gunzip" | "ugz" => ExprKind::FuncCall {
2769                name: "gunzip".to_string(),
2770                args: vec![arg],
2771            },
2772            "zstd" | "zst" => ExprKind::FuncCall {
2773                name: "zstd".to_string(),
2774                args: vec![arg],
2775            },
2776            "zstd_decode" | "uzst" => ExprKind::FuncCall {
2777                name: "zstd_decode".to_string(),
2778                args: vec![arg],
2779            },
2780            // Crypto
2781            "sha256" | "s256" => ExprKind::FuncCall {
2782                name: "sha256".to_string(),
2783                args: vec![arg],
2784            },
2785            "sha1" | "s1" => ExprKind::FuncCall {
2786                name: "sha1".to_string(),
2787                args: vec![arg],
2788            },
2789            "md5" | "m5" => ExprKind::FuncCall {
2790                name: "md5".to_string(),
2791                args: vec![arg],
2792            },
2793            "uuid" | "uid" => ExprKind::FuncCall {
2794                name: "uuid".to_string(),
2795                args: vec![arg],
2796            },
2797            // Datetime
2798            "datetime_utc" | "utc" => ExprKind::FuncCall {
2799                name: "datetime_utc".to_string(),
2800                args: vec![arg],
2801            },
2802            // Bare `e` / `fore` / `ep` in thread context: foreach element, say it.
2803            // `t @list e` == `@list |> e p` == `@list |> ep` == foreach (@list) { say }
2804            "e" | "fore" | "ep" => ExprKind::ForEachExpr {
2805                block: vec![Statement {
2806                    label: None,
2807                    kind: StmtKind::Expression(Expr {
2808                        kind: ExprKind::Say {
2809                            handle: None,
2810                            args: vec![Expr {
2811                                kind: ExprKind::ScalarVar("_".into()),
2812                                line,
2813                            }],
2814                        },
2815                        line,
2816                    }),
2817                    line,
2818                }],
2819                list: Box::new(arg),
2820            },
2821            // Default: generic function call
2822            _ => ExprKind::FuncCall {
2823                name: name.to_string(),
2824                args: vec![arg],
2825            },
2826        };
2827        Ok(Expr { kind, line })
2828    }
2829
2830    /// Parse a thread stage that has a block: `map { }`, `filter { }`, `sort { }`, etc.
2831    /// In thread context, we only parse the block - the list comes from the piped result.
2832    fn parse_thread_stage_with_block(&mut self, name: &str, line: usize) -> PerlResult<Expr> {
2833        let block = self.parse_block()?;
2834        // Use a placeholder for the list - pipe_forward_apply will replace it
2835        let placeholder = self.pipe_placeholder_list(line);
2836
2837        match name {
2838            "map" | "flat_map" | "maps" | "flat_maps" => {
2839                let flatten_array_refs = matches!(name, "flat_map" | "flat_maps");
2840                let stream = matches!(name, "maps" | "flat_maps");
2841                Ok(Expr {
2842                    kind: ExprKind::MapExpr {
2843                        block,
2844                        list: Box::new(placeholder),
2845                        flatten_array_refs,
2846                        stream,
2847                    },
2848                    line,
2849                })
2850            }
2851            "grep" | "greps" | "filter" | "fi" | "find_all" | "gr" => {
2852                let keyword = match name {
2853                    "grep" | "gr" => crate::ast::GrepBuiltinKeyword::Grep,
2854                    "greps" => crate::ast::GrepBuiltinKeyword::Greps,
2855                    "filter" | "fi" => crate::ast::GrepBuiltinKeyword::Filter,
2856                    "find_all" => crate::ast::GrepBuiltinKeyword::FindAll,
2857                    _ => unreachable!(),
2858                };
2859                Ok(Expr {
2860                    kind: ExprKind::GrepExpr {
2861                        block,
2862                        list: Box::new(placeholder),
2863                        keyword,
2864                    },
2865                    line,
2866                })
2867            }
2868            "sort" | "so" => Ok(Expr {
2869                kind: ExprKind::SortExpr {
2870                    cmp: Some(SortComparator::Block(block)),
2871                    list: Box::new(placeholder),
2872                },
2873                line,
2874            }),
2875            "reduce" | "rd" => Ok(Expr {
2876                kind: ExprKind::ReduceExpr {
2877                    block,
2878                    list: Box::new(placeholder),
2879                },
2880                line,
2881            }),
2882            "fore" | "e" | "ep" => Ok(Expr {
2883                kind: ExprKind::ForEachExpr {
2884                    block,
2885                    list: Box::new(placeholder),
2886                },
2887                line,
2888            }),
2889            "pmap" | "pflat_map" | "pmaps" | "pflat_maps" => Ok(Expr {
2890                kind: ExprKind::PMapExpr {
2891                    block,
2892                    list: Box::new(placeholder),
2893                    progress: None,
2894                    flat_outputs: name == "pflat_map" || name == "pflat_maps",
2895                    on_cluster: None,
2896                    stream: name == "pmaps" || name == "pflat_maps",
2897                },
2898                line,
2899            }),
2900            "pgrep" | "pgreps" => Ok(Expr {
2901                kind: ExprKind::PGrepExpr {
2902                    block,
2903                    list: Box::new(placeholder),
2904                    progress: None,
2905                    stream: name == "pgreps",
2906                },
2907                line,
2908            }),
2909            "pfor" => Ok(Expr {
2910                kind: ExprKind::PForExpr {
2911                    block,
2912                    list: Box::new(placeholder),
2913                    progress: None,
2914                },
2915                line,
2916            }),
2917            "preduce" => Ok(Expr {
2918                kind: ExprKind::PReduceExpr {
2919                    block,
2920                    list: Box::new(placeholder),
2921                    progress: None,
2922                },
2923                line,
2924            }),
2925            "pcache" => Ok(Expr {
2926                kind: ExprKind::PcacheExpr {
2927                    block,
2928                    list: Box::new(placeholder),
2929                    progress: None,
2930                },
2931                line,
2932            }),
2933            "psort" => Ok(Expr {
2934                kind: ExprKind::PSortExpr {
2935                    cmp: Some(block),
2936                    list: Box::new(placeholder),
2937                    progress: None,
2938                },
2939                line,
2940            }),
2941            _ => {
2942                // Generic: parse block and treat as FuncCall with code ref arg.
2943                // Block-then-list pipe builtins (`pfirst`, `any`, `take_while`, etc.)
2944                // need the threaded list slot pre-allocated at args[1] so
2945                // `pipe_forward_apply` can substitute the lhs there (parser.rs:5823).
2946                // For everything else, the generic pipe-forward arm prepends or
2947                // appends the lhs based on `thread_last_mode`.
2948                let code_ref = Expr {
2949                    kind: ExprKind::CodeRef {
2950                        params: vec![],
2951                        body: block,
2952                    },
2953                    line,
2954                };
2955                let args = if Self::is_block_then_list_pipe_builtin(name) {
2956                    vec![code_ref, placeholder]
2957                } else {
2958                    vec![code_ref]
2959                };
2960                Ok(Expr {
2961                    kind: ExprKind::FuncCall {
2962                        name: name.to_string(),
2963                        args,
2964                    },
2965                    line,
2966                })
2967            }
2968        }
2969    }
2970
2971    /// `tie %hash | tie @arr | tie $x , 'Class', ...args`
2972    fn parse_tie_stmt(&mut self) -> PerlResult<Statement> {
2973        let line = self.peek_line();
2974        self.advance(); // tie
2975        let target = match self.peek().clone() {
2976            Token::HashVar(h) => {
2977                self.advance();
2978                TieTarget::Hash(h)
2979            }
2980            Token::ArrayVar(a) => {
2981                self.advance();
2982                TieTarget::Array(a)
2983            }
2984            Token::ScalarVar(s) => {
2985                self.advance();
2986                TieTarget::Scalar(s)
2987            }
2988            tok => {
2989                return Err(self.syntax_err(
2990                    format!("tie expects $scalar, @array, or %hash, got {:?}", tok),
2991                    self.peek_line(),
2992                ));
2993            }
2994        };
2995        self.expect(&Token::Comma)?;
2996        let class = self.parse_assign_expr()?;
2997        let mut args = Vec::new();
2998        while self.eat(&Token::Comma) {
2999            if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof) {
3000                break;
3001            }
3002            args.push(self.parse_assign_expr()?);
3003        }
3004        self.eat(&Token::Semicolon);
3005        Ok(Statement {
3006            label: None,
3007            kind: StmtKind::Tie {
3008                target,
3009                class,
3010                args,
3011            },
3012            line,
3013        })
3014    }
3015
3016    /// `given (EXPR) { ... }`
3017    fn parse_given(&mut self) -> PerlResult<Statement> {
3018        let line = self.peek_line();
3019        self.advance();
3020        self.expect(&Token::LParen)?;
3021        let topic = self.parse_expression()?;
3022        self.expect(&Token::RParen)?;
3023        let body = self.parse_block()?;
3024        self.eat(&Token::Semicolon);
3025        Ok(Statement {
3026            label: None,
3027            kind: StmtKind::Given { topic, body },
3028            line,
3029        })
3030    }
3031
3032    /// `when (COND) { ... }` — only meaningful inside `given`
3033    fn parse_when_stmt(&mut self) -> PerlResult<Statement> {
3034        let line = self.peek_line();
3035        self.advance();
3036        self.expect(&Token::LParen)?;
3037        let cond = self.parse_expression()?;
3038        self.expect(&Token::RParen)?;
3039        let body = self.parse_block()?;
3040        self.eat(&Token::Semicolon);
3041        Ok(Statement {
3042            label: None,
3043            kind: StmtKind::When { cond, body },
3044            line,
3045        })
3046    }
3047
3048    /// `default { ... }` — only meaningful inside `given`
3049    fn parse_default_stmt(&mut self) -> PerlResult<Statement> {
3050        let line = self.peek_line();
3051        self.advance();
3052        let body = self.parse_block()?;
3053        self.eat(&Token::Semicolon);
3054        Ok(Statement {
3055            label: None,
3056            kind: StmtKind::DefaultCase { body },
3057            line,
3058        })
3059    }
3060
3061    /// `cond { EXPR => RESULT, ..., default => RESULT }`
3062    ///
3063    /// Desugars to an if/elsif/else chain at parse time.
3064    /// Each arm is `condition => { body }` or `condition => expr`.
3065    /// `default => ...` becomes the else branch.
3066    fn parse_cond_expr(&mut self, line: usize) -> PerlResult<Expr> {
3067        self.expect(&Token::LBrace)?;
3068
3069        let mut arms: Vec<(Expr, Block)> = Vec::new();
3070        let mut else_block: Option<Block> = None;
3071
3072        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
3073            let arm_line = self.peek_line();
3074
3075            // Check for `default =>`
3076            let is_default = matches!(self.peek(), Token::Ident(ref s) if s == "default")
3077                && matches!(self.peek_at(1), Token::FatArrow);
3078
3079            if is_default {
3080                self.advance(); // consume `default`
3081                self.advance(); // consume `=>`
3082                let body = if matches!(self.peek(), Token::LBrace) {
3083                    self.parse_block()?
3084                } else {
3085                    let expr = self.parse_assign_expr()?;
3086                    vec![Statement {
3087                        label: None,
3088                        kind: StmtKind::Expression(expr),
3089                        line: arm_line,
3090                    }]
3091                };
3092                else_block = Some(body);
3093                self.eat(&Token::Comma);
3094                break; // default must be last
3095            }
3096
3097            // Parse condition expression (stop before `=>`)
3098            let condition = self.parse_assign_expr()?;
3099            self.expect(&Token::FatArrow)?;
3100
3101            let body = if matches!(self.peek(), Token::LBrace) {
3102                self.parse_block()?
3103            } else {
3104                let expr = self.parse_assign_expr()?;
3105                vec![Statement {
3106                    label: None,
3107                    kind: StmtKind::Expression(expr),
3108                    line: arm_line,
3109                }]
3110            };
3111
3112            arms.push((condition, body));
3113            self.eat(&Token::Comma);
3114        }
3115
3116        self.expect(&Token::RBrace)?;
3117
3118        if arms.is_empty() {
3119            return Err(self.syntax_err("cond requires at least one condition arm", line));
3120        }
3121
3122        // Build if/elsif/else chain from the arms.
3123        let (first_cond, first_body) = arms.remove(0);
3124        let elsifs: Vec<(Expr, Block)> = arms;
3125
3126        // Wrap in a do-block so `cond { ... }` is an expression.
3127        let if_stmt = Statement {
3128            label: None,
3129            kind: StmtKind::If {
3130                condition: first_cond,
3131                body: first_body,
3132                elsifs,
3133                else_block,
3134            },
3135            line,
3136        };
3137        let inner = Expr {
3138            kind: ExprKind::CodeRef {
3139                params: vec![],
3140                body: vec![if_stmt],
3141            },
3142            line,
3143        };
3144        Ok(Expr {
3145            kind: ExprKind::Do(Box::new(inner)),
3146            line,
3147        })
3148    }
3149
3150    /// `match (EXPR) { PATTERN => EXPR, ... }`
3151    fn parse_algebraic_match_expr(&mut self, line: usize) -> PerlResult<Expr> {
3152        self.expect(&Token::LParen)?;
3153        let subject = self.parse_expression()?;
3154        self.expect(&Token::RParen)?;
3155        self.expect(&Token::LBrace)?;
3156        let mut arms = Vec::new();
3157        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
3158            if self.eat(&Token::Semicolon) {
3159                continue;
3160            }
3161            let pattern = self.parse_match_pattern()?;
3162            let guard = if matches!(self.peek(), Token::Ident(ref s) if s == "if") {
3163                self.advance();
3164                // Use assign-level parsing so `=>` after the guard is not consumed as a comma/fat-comma
3165                // separator (see [`Self::parse_comma_expr`]).
3166                Some(Box::new(self.parse_assign_expr()?))
3167            } else {
3168                None
3169            };
3170            self.expect(&Token::FatArrow)?;
3171            // Use assign-level parsing so commas separate arms, not `List` elements.
3172            let body = self.parse_assign_expr()?;
3173            arms.push(MatchArm {
3174                pattern,
3175                guard,
3176                body,
3177            });
3178            self.eat(&Token::Comma);
3179        }
3180        self.expect(&Token::RBrace)?;
3181        Ok(Expr {
3182            kind: ExprKind::AlgebraicMatch {
3183                subject: Box::new(subject),
3184                arms,
3185            },
3186            line,
3187        })
3188    }
3189
3190    fn parse_match_pattern(&mut self) -> PerlResult<MatchPattern> {
3191        match self.peek().clone() {
3192            Token::Regex(pattern, flags, _delim) => {
3193                self.advance();
3194                Ok(MatchPattern::Regex { pattern, flags })
3195            }
3196            Token::Ident(ref s) if s == "_" => {
3197                self.advance();
3198                Ok(MatchPattern::Any)
3199            }
3200            Token::Ident(ref s) if s == "Some" => {
3201                self.advance();
3202                self.expect(&Token::LParen)?;
3203                let name = self.parse_scalar_var_name()?;
3204                self.expect(&Token::RParen)?;
3205                Ok(MatchPattern::OptionSome(name))
3206            }
3207            Token::LBracket => self.parse_match_array_pattern(),
3208            Token::LBrace => self.parse_match_hash_pattern(),
3209            Token::LParen => {
3210                self.advance();
3211                let e = self.parse_expression()?;
3212                self.expect(&Token::RParen)?;
3213                Ok(MatchPattern::Value(Box::new(e)))
3214            }
3215            _ => {
3216                let e = self.parse_assign_expr()?;
3217                Ok(MatchPattern::Value(Box::new(e)))
3218            }
3219        }
3220    }
3221
3222    /// Contents of `[ ... ]` for algebraic array patterns and `sub ($a, [ ... ])` signatures.
3223    fn parse_match_array_elems_until_rbracket(&mut self) -> PerlResult<Vec<MatchArrayElem>> {
3224        let mut elems = Vec::new();
3225        if self.eat(&Token::RBracket) {
3226            return Ok(vec![]);
3227        }
3228        loop {
3229            if matches!(self.peek(), Token::Star) {
3230                self.advance();
3231                elems.push(MatchArrayElem::Rest);
3232                self.eat(&Token::Comma);
3233                if !matches!(self.peek(), Token::RBracket) {
3234                    return Err(self.syntax_err(
3235                        "`*` must be the last element in an array match pattern",
3236                        self.peek_line(),
3237                    ));
3238                }
3239                self.expect(&Token::RBracket)?;
3240                return Ok(elems);
3241            }
3242            if let Token::ArrayVar(name) = self.peek().clone() {
3243                self.advance();
3244                elems.push(MatchArrayElem::RestBind(name));
3245                self.eat(&Token::Comma);
3246                if !matches!(self.peek(), Token::RBracket) {
3247                    return Err(self.syntax_err(
3248                        "`@name` rest bind must be the last element in an array match pattern",
3249                        self.peek_line(),
3250                    ));
3251                }
3252                self.expect(&Token::RBracket)?;
3253                return Ok(elems);
3254            }
3255            if let Token::ScalarVar(name) = self.peek().clone() {
3256                self.advance();
3257                elems.push(MatchArrayElem::CaptureScalar(name));
3258                if self.eat(&Token::Comma) {
3259                    if matches!(self.peek(), Token::RBracket) {
3260                        break;
3261                    }
3262                    continue;
3263                }
3264                break;
3265            }
3266            let e = self.parse_assign_expr()?;
3267            elems.push(MatchArrayElem::Expr(e));
3268            if self.eat(&Token::Comma) {
3269                if matches!(self.peek(), Token::RBracket) {
3270                    break;
3271                }
3272                continue;
3273            }
3274            break;
3275        }
3276        self.expect(&Token::RBracket)?;
3277        Ok(elems)
3278    }
3279
3280    fn parse_match_array_pattern(&mut self) -> PerlResult<MatchPattern> {
3281        self.expect(&Token::LBracket)?;
3282        let elems = self.parse_match_array_elems_until_rbracket()?;
3283        Ok(MatchPattern::Array(elems))
3284    }
3285
3286    fn parse_match_hash_pattern(&mut self) -> PerlResult<MatchPattern> {
3287        self.expect(&Token::LBrace)?;
3288        let mut pairs = Vec::new();
3289        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
3290            if self.eat(&Token::Semicolon) {
3291                continue;
3292            }
3293            let key = self.parse_assign_expr()?;
3294            self.expect(&Token::FatArrow)?;
3295            match self.advance().0 {
3296                Token::Ident(ref s) if s == "_" => {
3297                    pairs.push(MatchHashPair::KeyOnly { key });
3298                }
3299                Token::ScalarVar(name) => {
3300                    pairs.push(MatchHashPair::Capture { key, name });
3301                }
3302                tok => {
3303                    return Err(self.syntax_err(
3304                        format!(
3305                            "hash match pattern must bind with `=> $name` or `=> _`, got {:?}",
3306                            tok
3307                        ),
3308                        self.peek_line(),
3309                    ));
3310                }
3311            }
3312            self.eat(&Token::Comma);
3313        }
3314        self.expect(&Token::RBrace)?;
3315        Ok(MatchPattern::Hash(pairs))
3316    }
3317
3318    /// `eval_timeout SECS { ... }`
3319    fn parse_eval_timeout(&mut self) -> PerlResult<Statement> {
3320        let line = self.peek_line();
3321        self.advance();
3322        let timeout = self.parse_postfix()?;
3323        let body = self.parse_block_or_bareword_block_no_args()?;
3324        self.eat(&Token::Semicolon);
3325        Ok(Statement {
3326            label: None,
3327            kind: StmtKind::EvalTimeout { timeout, body },
3328            line,
3329        })
3330    }
3331
3332    fn mark_match_scalar_g_for_boolean_condition(cond: &mut Expr) {
3333        match &mut cond.kind {
3334            ExprKind::Match {
3335                flags, scalar_g, ..
3336            } if flags.contains('g') => {
3337                *scalar_g = true;
3338            }
3339            ExprKind::UnaryOp {
3340                op: UnaryOp::LogNot,
3341                expr,
3342            } => {
3343                if let ExprKind::Match {
3344                    flags, scalar_g, ..
3345                } = &mut expr.kind
3346                {
3347                    if flags.contains('g') {
3348                        *scalar_g = true;
3349                    }
3350                }
3351            }
3352            _ => {}
3353        }
3354    }
3355
3356    fn parse_if(&mut self) -> PerlResult<Statement> {
3357        let line = self.peek_line();
3358        self.advance(); // 'if'
3359        if matches!(self.peek(), Token::Ident(ref s) if s == "let") {
3360            if crate::compat_mode() {
3361                return Err(self.syntax_err(
3362                    "`if let` is a stryke extension (disabled by --compat)",
3363                    line,
3364                ));
3365            }
3366            return self.parse_if_let(line);
3367        }
3368        self.expect(&Token::LParen)?;
3369        let mut cond = self.parse_expression()?;
3370        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
3371        self.expect(&Token::RParen)?;
3372        let body = self.parse_block()?;
3373
3374        let mut elsifs = Vec::new();
3375        let mut else_block = None;
3376
3377        loop {
3378            if let Token::Ident(ref kw) = self.peek().clone() {
3379                if kw == "elsif" {
3380                    self.advance();
3381                    self.expect(&Token::LParen)?;
3382                    let mut c = self.parse_expression()?;
3383                    Self::mark_match_scalar_g_for_boolean_condition(&mut c);
3384                    self.expect(&Token::RParen)?;
3385                    let b = self.parse_block()?;
3386                    elsifs.push((c, b));
3387                    continue;
3388                }
3389                if kw == "else" {
3390                    self.advance();
3391                    else_block = Some(self.parse_block()?);
3392                }
3393            }
3394            break;
3395        }
3396
3397        Ok(Statement {
3398            label: None,
3399            kind: StmtKind::If {
3400                condition: cond,
3401                body,
3402                elsifs,
3403                else_block,
3404            },
3405            line,
3406        })
3407    }
3408
3409    /// `if let PAT = EXPR { ... } [ else { ... } ]` — desugars to [`ExprKind::AlgebraicMatch`].
3410    fn parse_if_let(&mut self, line: usize) -> PerlResult<Statement> {
3411        self.advance(); // `let`
3412        let pattern = self.parse_match_pattern()?;
3413        self.expect(&Token::Assign)?;
3414        // Use assign-level parsing so a following `{ ... }` is the `if let` body, not an anon hash.
3415        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_add(1);
3416        let rhs = self.parse_assign_expr();
3417        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_sub(1);
3418        let rhs = rhs?;
3419        let then_block = self.parse_block()?;
3420        let else_block_opt = match self.peek().clone() {
3421            Token::Ident(ref kw) if kw == "else" => {
3422                self.advance();
3423                Some(self.parse_block()?)
3424            }
3425            Token::Ident(ref kw) if kw == "elsif" => {
3426                return Err(self.syntax_err(
3427                    "`if let` does not support `elsif`; use `else { }` or a full `match`",
3428                    self.peek_line(),
3429                ));
3430            }
3431            _ => None,
3432        };
3433        let then_expr = Self::expr_do_anon_block(then_block, line);
3434        let else_expr = if let Some(eb) = else_block_opt {
3435            Self::expr_do_anon_block(eb, line)
3436        } else {
3437            Expr {
3438                kind: ExprKind::Undef,
3439                line,
3440            }
3441        };
3442        let arms = vec![
3443            MatchArm {
3444                pattern,
3445                guard: None,
3446                body: then_expr,
3447            },
3448            MatchArm {
3449                pattern: MatchPattern::Any,
3450                guard: None,
3451                body: else_expr,
3452            },
3453        ];
3454        Ok(Statement {
3455            label: None,
3456            kind: StmtKind::Expression(Expr {
3457                kind: ExprKind::AlgebraicMatch {
3458                    subject: Box::new(rhs),
3459                    arms,
3460                },
3461                line,
3462            }),
3463            line,
3464        })
3465    }
3466
3467    fn expr_do_anon_block(block: Block, outer_line: usize) -> Expr {
3468        let inner_line = block.first().map(|s| s.line).unwrap_or(outer_line);
3469        Expr {
3470            kind: ExprKind::Do(Box::new(Expr {
3471                kind: ExprKind::CodeRef {
3472                    params: vec![],
3473                    body: block,
3474                },
3475                line: inner_line,
3476            })),
3477            line: outer_line,
3478        }
3479    }
3480
3481    fn parse_unless(&mut self) -> PerlResult<Statement> {
3482        let line = self.peek_line();
3483        self.advance(); // 'unless'
3484        self.expect(&Token::LParen)?;
3485        let mut cond = self.parse_expression()?;
3486        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
3487        self.expect(&Token::RParen)?;
3488        let body = self.parse_block()?;
3489        let else_block = if let Token::Ident(ref kw) = self.peek().clone() {
3490            if kw == "else" {
3491                self.advance();
3492                Some(self.parse_block()?)
3493            } else {
3494                None
3495            }
3496        } else {
3497            None
3498        };
3499        Ok(Statement {
3500            label: None,
3501            kind: StmtKind::Unless {
3502                condition: cond,
3503                body,
3504                else_block,
3505            },
3506            line,
3507        })
3508    }
3509
3510    fn parse_while(&mut self) -> PerlResult<Statement> {
3511        let line = self.peek_line();
3512        self.advance(); // 'while'
3513        if matches!(self.peek(), Token::Ident(ref s) if s == "let") {
3514            if crate::compat_mode() {
3515                return Err(self.syntax_err(
3516                    "`while let` is a stryke extension (disabled by --compat)",
3517                    line,
3518                ));
3519            }
3520            return self.parse_while_let(line);
3521        }
3522        self.expect(&Token::LParen)?;
3523        let mut cond = self.parse_expression()?;
3524        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
3525        self.expect(&Token::RParen)?;
3526        let body = self.parse_block()?;
3527        let continue_block = self.parse_optional_continue_block()?;
3528        Ok(Statement {
3529            label: None,
3530            kind: StmtKind::While {
3531                condition: cond,
3532                body,
3533                label: None,
3534                continue_block,
3535            },
3536            line,
3537        })
3538    }
3539
3540    /// `while let PAT = EXPR { ... }` — desugars to a `match` that returns 0/1 plus `unless ($tmp) { last }`
3541    /// so bytecode does not run `last` inside a tree-assisted [`Op::AlgebraicMatch`] arm.
3542    fn parse_while_let(&mut self, line: usize) -> PerlResult<Statement> {
3543        self.advance(); // `let`
3544        let pattern = self.parse_match_pattern()?;
3545        self.expect(&Token::Assign)?;
3546        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_add(1);
3547        let rhs = self.parse_assign_expr();
3548        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_sub(1);
3549        let rhs = rhs?;
3550        let mut user_body = self.parse_block()?;
3551        let continue_block = self.parse_optional_continue_block()?;
3552        user_body.push(Statement::new(
3553            StmtKind::Expression(Expr {
3554                kind: ExprKind::Integer(1),
3555                line,
3556            }),
3557            line,
3558        ));
3559        let tmp = format!("__while_let_{}", self.alloc_desugar_tmp());
3560        let match_expr = Expr {
3561            kind: ExprKind::AlgebraicMatch {
3562                subject: Box::new(rhs),
3563                arms: vec![
3564                    MatchArm {
3565                        pattern,
3566                        guard: None,
3567                        body: Self::expr_do_anon_block(user_body, line),
3568                    },
3569                    MatchArm {
3570                        pattern: MatchPattern::Any,
3571                        guard: None,
3572                        body: Expr {
3573                            kind: ExprKind::Integer(0),
3574                            line,
3575                        },
3576                    },
3577                ],
3578            },
3579            line,
3580        };
3581        let my_stmt = Statement::new(
3582            StmtKind::My(vec![VarDecl {
3583                sigil: Sigil::Scalar,
3584                name: tmp.clone(),
3585                initializer: Some(match_expr),
3586                frozen: false,
3587                type_annotation: None,
3588            }]),
3589            line,
3590        );
3591        let unless_last = Statement::new(
3592            StmtKind::Unless {
3593                condition: Expr {
3594                    kind: ExprKind::ScalarVar(tmp),
3595                    line,
3596                },
3597                body: vec![Statement::new(StmtKind::Last(None), line)],
3598                else_block: None,
3599            },
3600            line,
3601        );
3602        Ok(Statement::new(
3603            StmtKind::While {
3604                condition: Expr {
3605                    kind: ExprKind::Integer(1),
3606                    line,
3607                },
3608                body: vec![my_stmt, unless_last],
3609                label: None,
3610                continue_block,
3611            },
3612            line,
3613        ))
3614    }
3615
3616    fn parse_until(&mut self) -> PerlResult<Statement> {
3617        let line = self.peek_line();
3618        self.advance(); // 'until'
3619        self.expect(&Token::LParen)?;
3620        let mut cond = self.parse_expression()?;
3621        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
3622        self.expect(&Token::RParen)?;
3623        let body = self.parse_block()?;
3624        let continue_block = self.parse_optional_continue_block()?;
3625        Ok(Statement {
3626            label: None,
3627            kind: StmtKind::Until {
3628                condition: cond,
3629                body,
3630                label: None,
3631                continue_block,
3632            },
3633            line,
3634        })
3635    }
3636
3637    /// `continue { ... }` after a loop body (optional).
3638    fn parse_optional_continue_block(&mut self) -> PerlResult<Option<Block>> {
3639        if let Token::Ident(ref kw) = self.peek().clone() {
3640            if kw == "continue" {
3641                self.advance();
3642                return Ok(Some(self.parse_block()?));
3643            }
3644        }
3645        Ok(None)
3646    }
3647
3648    fn parse_for_or_foreach(&mut self) -> PerlResult<Statement> {
3649        let line = self.peek_line();
3650        self.advance(); // 'for'
3651
3652        // Peek to determine if C-style for or foreach
3653        // C-style: for (init; cond; step)
3654        // foreach-style: for $var (list) or for (list)
3655        match self.peek() {
3656            Token::LParen => {
3657                // Check if next after ( is a semicolon or an assignment — C-style
3658                // Or if it's a list — foreach-style
3659                // Heuristic: if the token after ( is 'my' or '$' followed by
3660                // content that contains ';', it's C-style.
3661                let saved = self.pos;
3662                self.advance(); // consume (
3663                                // Look for semicolon at paren depth 0
3664                let mut depth = 1;
3665                let mut has_semi = false;
3666                let mut scan = self.pos;
3667                while scan < self.tokens.len() {
3668                    match &self.tokens[scan].0 {
3669                        Token::LParen => depth += 1,
3670                        Token::RParen => {
3671                            depth -= 1;
3672                            if depth == 0 {
3673                                break;
3674                            }
3675                        }
3676                        Token::Semicolon if depth == 1 => {
3677                            has_semi = true;
3678                            break;
3679                        }
3680                        _ => {}
3681                    }
3682                    scan += 1;
3683                }
3684                self.pos = saved;
3685
3686                if has_semi {
3687                    self.parse_c_style_for(line)
3688                } else {
3689                    // foreach without explicit var — uses $_
3690                    self.expect(&Token::LParen)?;
3691                    let list = self.parse_expression()?;
3692                    self.expect(&Token::RParen)?;
3693                    let body = self.parse_block()?;
3694                    let continue_block = self.parse_optional_continue_block()?;
3695                    Ok(Statement {
3696                        label: None,
3697                        kind: StmtKind::Foreach {
3698                            var: "_".to_string(),
3699                            list,
3700                            body,
3701                            label: None,
3702                            continue_block,
3703                        },
3704                        line,
3705                    })
3706                }
3707            }
3708            Token::Ident(ref kw) if kw == "my" => {
3709                self.advance(); // 'my'
3710                let var = self.parse_scalar_var_name()?;
3711                self.expect(&Token::LParen)?;
3712                let list = self.parse_expression()?;
3713                self.expect(&Token::RParen)?;
3714                let body = self.parse_block()?;
3715                let continue_block = self.parse_optional_continue_block()?;
3716                Ok(Statement {
3717                    label: None,
3718                    kind: StmtKind::Foreach {
3719                        var,
3720                        list,
3721                        body,
3722                        label: None,
3723                        continue_block,
3724                    },
3725                    line,
3726                })
3727            }
3728            Token::ScalarVar(_) => {
3729                let var = self.parse_scalar_var_name()?;
3730                self.expect(&Token::LParen)?;
3731                let list = self.parse_expression()?;
3732                self.expect(&Token::RParen)?;
3733                let body = self.parse_block()?;
3734                let continue_block = self.parse_optional_continue_block()?;
3735                Ok(Statement {
3736                    label: None,
3737                    kind: StmtKind::Foreach {
3738                        var,
3739                        list,
3740                        body,
3741                        label: None,
3742                        continue_block,
3743                    },
3744                    line,
3745                })
3746            }
3747            _ => self.parse_c_style_for(line),
3748        }
3749    }
3750
3751    fn parse_c_style_for(&mut self, line: usize) -> PerlResult<Statement> {
3752        self.expect(&Token::LParen)?;
3753        let init = if self.eat(&Token::Semicolon) {
3754            None
3755        } else {
3756            let s = self.parse_statement()?;
3757            self.eat(&Token::Semicolon);
3758            Some(Box::new(s))
3759        };
3760        let mut condition = if matches!(self.peek(), Token::Semicolon) {
3761            None
3762        } else {
3763            Some(self.parse_expression()?)
3764        };
3765        if let Some(ref mut c) = condition {
3766            Self::mark_match_scalar_g_for_boolean_condition(c);
3767        }
3768        self.expect(&Token::Semicolon)?;
3769        let step = if matches!(self.peek(), Token::RParen) {
3770            None
3771        } else {
3772            Some(self.parse_expression()?)
3773        };
3774        self.expect(&Token::RParen)?;
3775        let body = self.parse_block()?;
3776        let continue_block = self.parse_optional_continue_block()?;
3777        Ok(Statement {
3778            label: None,
3779            kind: StmtKind::For {
3780                init,
3781                condition,
3782                step,
3783                body,
3784                label: None,
3785                continue_block,
3786            },
3787            line,
3788        })
3789    }
3790
3791    fn parse_foreach(&mut self) -> PerlResult<Statement> {
3792        let line = self.peek_line();
3793        self.advance(); // 'foreach'
3794        let var = match self.peek() {
3795            Token::Ident(ref kw) if kw == "my" => {
3796                self.advance();
3797                self.parse_scalar_var_name()?
3798            }
3799            Token::ScalarVar(_) => self.parse_scalar_var_name()?,
3800            _ => "_".to_string(),
3801        };
3802        self.expect(&Token::LParen)?;
3803        let list = self.parse_expression()?;
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::Foreach {
3810                var,
3811                list,
3812                body,
3813                label: None,
3814                continue_block,
3815            },
3816            line,
3817        })
3818    }
3819
3820    fn parse_scalar_var_name(&mut self) -> PerlResult<String> {
3821        match self.advance() {
3822            (Token::ScalarVar(name), _) => Ok(name),
3823            (tok, line) => {
3824                Err(self.syntax_err(format!("Expected scalar variable, got {:?}", tok), line))
3825            }
3826        }
3827    }
3828
3829    /// After `(` was consumed: Perl5 prototype characters until `)` (or `$)` + `{`).
3830    fn parse_legacy_sub_prototype_tail(&mut self) -> PerlResult<String> {
3831        let mut s = String::new();
3832        loop {
3833            match self.peek().clone() {
3834                Token::RParen => {
3835                    self.advance();
3836                    break;
3837                }
3838                Token::Eof => {
3839                    return Err(self.syntax_err(
3840                        "Unterminated sub prototype (expected ')' before end of input)",
3841                        self.peek_line(),
3842                    ));
3843                }
3844                Token::ScalarVar(v) if v == ")" => {
3845                    // Lexer merges `$` + `)` into one token (`$)`). In `sub name ($) {`, the
3846                    // closing `)` of the prototype is not a separate `RParen` — next is `{`.
3847                    self.advance();
3848                    s.push('$');
3849                    if matches!(self.peek(), Token::LBrace) {
3850                        break;
3851                    }
3852                }
3853                Token::Ident(i) => {
3854                    let i = i.clone();
3855                    self.advance();
3856                    s.push_str(&i);
3857                }
3858                Token::Semicolon => {
3859                    self.advance();
3860                    s.push(';');
3861                }
3862                Token::LParen => {
3863                    self.advance();
3864                    s.push('(');
3865                }
3866                Token::LBracket => {
3867                    self.advance();
3868                    s.push('[');
3869                }
3870                Token::RBracket => {
3871                    self.advance();
3872                    s.push(']');
3873                }
3874                Token::Backslash => {
3875                    self.advance();
3876                    s.push('\\');
3877                }
3878                Token::Comma => {
3879                    self.advance();
3880                    s.push(',');
3881                }
3882                Token::ScalarVar(v) => {
3883                    let v = v.clone();
3884                    self.advance();
3885                    s.push('$');
3886                    s.push_str(&v);
3887                }
3888                Token::ArrayVar(v) => {
3889                    let v = v.clone();
3890                    self.advance();
3891                    s.push('@');
3892                    s.push_str(&v);
3893                }
3894                // Bare `@` / `%` in prototypes (e.g. Try::Tiny's `sub try (&;@)`).
3895                Token::ArrayAt => {
3896                    self.advance();
3897                    s.push('@');
3898                }
3899                Token::HashVar(v) => {
3900                    let v = v.clone();
3901                    self.advance();
3902                    s.push('%');
3903                    s.push_str(&v);
3904                }
3905                Token::HashPercent => {
3906                    self.advance();
3907                    s.push('%');
3908                }
3909                Token::Plus => {
3910                    self.advance();
3911                    s.push('+');
3912                }
3913                Token::Minus => {
3914                    self.advance();
3915                    s.push('-');
3916                }
3917                Token::BitAnd => {
3918                    self.advance();
3919                    s.push('&');
3920                }
3921                tok => {
3922                    return Err(self.syntax_err(
3923                        format!("Unexpected token in sub prototype: {:?}", tok),
3924                        self.peek_line(),
3925                    ));
3926                }
3927            }
3928        }
3929        Ok(s)
3930    }
3931
3932    fn sub_signature_list_starts_here(&self) -> bool {
3933        match self.peek() {
3934            Token::LBrace | Token::LBracket => true,
3935            Token::ScalarVar(name) if name != "$$" && name != ")" => true,
3936            Token::ArrayVar(_) | Token::HashVar(_) => true,
3937            _ => false,
3938        }
3939    }
3940
3941    fn parse_sub_signature_hash_key(&mut self) -> PerlResult<String> {
3942        let (tok, line) = self.advance();
3943        match tok {
3944            Token::Ident(i) => Ok(i),
3945            Token::SingleString(s) | Token::DoubleString(s) => Ok(s),
3946            tok => Err(self.syntax_err(
3947                format!(
3948                    "sub signature: expected hash key (identifier or string), got {:?}",
3949                    tok
3950                ),
3951                line,
3952            )),
3953        }
3954    }
3955
3956    fn parse_sub_signature_param_list(&mut self) -> PerlResult<Vec<SubSigParam>> {
3957        let mut params = Vec::new();
3958        loop {
3959            if matches!(self.peek(), Token::RParen) {
3960                break;
3961            }
3962            match self.peek().clone() {
3963                Token::ScalarVar(name) => {
3964                    if name == "$$" || name == ")" {
3965                        return Err(self.syntax_err(
3966                            format!(
3967                                "`{name}` cannot start a stryke sub signature (use legacy prototype `($$)` etc.)"
3968                            ),
3969                            self.peek_line(),
3970                        ));
3971                    }
3972                    self.advance();
3973                    let ty = if self.eat(&Token::Colon) {
3974                        match self.peek() {
3975                            Token::Ident(ref tname) => {
3976                                let tname = tname.clone();
3977                                self.advance();
3978                                Some(match tname.as_str() {
3979                                    "Int" => PerlTypeName::Int,
3980                                    "Str" => PerlTypeName::Str,
3981                                    "Float" => PerlTypeName::Float,
3982                                    "Bool" => PerlTypeName::Bool,
3983                                    "Array" => PerlTypeName::Array,
3984                                    "Hash" => PerlTypeName::Hash,
3985                                    "Ref" => PerlTypeName::Ref,
3986                                    "Any" => PerlTypeName::Any,
3987                                    _ => PerlTypeName::Struct(tname),
3988                                })
3989                            }
3990                            _ => {
3991                                return Err(self.syntax_err(
3992                                    "expected type name after `:` in sub signature",
3993                                    self.peek_line(),
3994                                ));
3995                            }
3996                        }
3997                    } else {
3998                        None
3999                    };
4000                    // Check for default value: `$x = expr`
4001                    let default = if self.eat(&Token::Assign) {
4002                        Some(Box::new(self.parse_ternary()?))
4003                    } else {
4004                        None
4005                    };
4006                    params.push(SubSigParam::Scalar(name, ty, default));
4007                }
4008                Token::ArrayVar(name) => {
4009                    self.advance();
4010                    let default = if self.eat(&Token::Assign) {
4011                        Some(Box::new(self.parse_ternary()?))
4012                    } else {
4013                        None
4014                    };
4015                    params.push(SubSigParam::Array(name, default));
4016                }
4017                Token::HashVar(name) => {
4018                    self.advance();
4019                    let default = if self.eat(&Token::Assign) {
4020                        Some(Box::new(self.parse_ternary()?))
4021                    } else {
4022                        None
4023                    };
4024                    params.push(SubSigParam::Hash(name, default));
4025                }
4026                Token::LBracket => {
4027                    self.advance();
4028                    let elems = self.parse_match_array_elems_until_rbracket()?;
4029                    params.push(SubSigParam::ArrayDestruct(elems));
4030                }
4031                Token::LBrace => {
4032                    self.advance();
4033                    let mut pairs = Vec::new();
4034                    loop {
4035                        if matches!(self.peek(), Token::RBrace | Token::Eof) {
4036                            break;
4037                        }
4038                        if self.eat(&Token::Comma) {
4039                            continue;
4040                        }
4041                        let key = self.parse_sub_signature_hash_key()?;
4042                        self.expect(&Token::FatArrow)?;
4043                        let bind = self.parse_scalar_var_name()?;
4044                        pairs.push((key, bind));
4045                        self.eat(&Token::Comma);
4046                    }
4047                    self.expect(&Token::RBrace)?;
4048                    params.push(SubSigParam::HashDestruct(pairs));
4049                }
4050                tok => {
4051                    return Err(self.syntax_err(
4052                        format!(
4053                            "expected `$name`, `[ ... ]`, or `{{ ... }}` in sub signature, got {:?}",
4054                            tok
4055                        ),
4056                        self.peek_line(),
4057                    ));
4058                }
4059            }
4060            match self.peek() {
4061                Token::Comma => {
4062                    self.advance();
4063                    if matches!(self.peek(), Token::RParen) {
4064                        return Err(self.syntax_err(
4065                            "trailing `,` before `)` in sub signature",
4066                            self.peek_line(),
4067                        ));
4068                    }
4069                }
4070                Token::RParen => break,
4071                _ => {
4072                    return Err(self.syntax_err(
4073                        format!(
4074                            "expected `,` or `)` after sub signature parameter, got {:?}",
4075                            self.peek()
4076                        ),
4077                        self.peek_line(),
4078                    ));
4079                }
4080            }
4081        }
4082        Ok(params)
4083    }
4084
4085    /// Optional `sub` parens: either a Perl 5 prototype string or a stryke **`$name` / `{ k => $v }`** signature.
4086    fn parse_sub_sig_or_prototype_opt(&mut self) -> PerlResult<(Vec<SubSigParam>, Option<String>)> {
4087        if !matches!(self.peek(), Token::LParen) {
4088            return Ok((vec![], None));
4089        }
4090        self.advance();
4091        if matches!(self.peek(), Token::RParen) {
4092            self.advance();
4093            return Ok((vec![], Some(String::new())));
4094        }
4095        if self.sub_signature_list_starts_here() {
4096            let params = self.parse_sub_signature_param_list()?;
4097            self.expect(&Token::RParen)?;
4098            return Ok((params, None));
4099        }
4100        let proto = self.parse_legacy_sub_prototype_tail()?;
4101        Ok((vec![], Some(proto)))
4102    }
4103
4104    /// Optional subroutine attributes after name/prototype: `sub foo : lvalue { }`, `sub : ATTR(ARGS) { }`.
4105    fn parse_sub_attributes(&mut self) -> PerlResult<()> {
4106        while self.eat(&Token::Colon) {
4107            match self.advance() {
4108                (Token::Ident(_), _) => {}
4109                (tok, line) => {
4110                    return Err(self.syntax_err(
4111                        format!("Expected attribute name after `:`, got {:?}", tok),
4112                        line,
4113                    ));
4114                }
4115            }
4116            if self.eat(&Token::LParen) {
4117                let mut depth = 1usize;
4118                while depth > 0 {
4119                    match self.advance().0 {
4120                        Token::LParen => depth += 1,
4121                        Token::RParen => {
4122                            depth -= 1;
4123                        }
4124                        Token::Eof => {
4125                            return Err(self.syntax_err(
4126                                "Unterminated sub attribute argument list",
4127                                self.peek_line(),
4128                            ));
4129                        }
4130                        _ => {}
4131                    }
4132                }
4133            }
4134        }
4135        Ok(())
4136    }
4137
4138    fn parse_sub_decl(&mut self, is_sub_keyword: bool) -> PerlResult<Statement> {
4139        let line = self.peek_line();
4140        self.advance(); // 'sub' or 'fn'
4141        match self.peek().clone() {
4142            Token::Ident(_) => {
4143                let name = self.parse_package_qualified_identifier()?;
4144                // Allow shadowing builtins:
4145                // - In compat mode (full Perl 5)
4146                // - When parsing a module (imports should work)
4147                // Block shadowing:
4148                // - In user code (default mode, not parsing module)
4149                // - Always in no-interop mode
4150                let allow_shadow =
4151                    crate::compat_mode() || (self.parsing_module && !crate::no_interop_mode());
4152                if !allow_shadow {
4153                    self.check_udf_shadows_builtin(&name, line)?;
4154                }
4155                self.declared_subs.insert(name.clone());
4156                let (params, prototype) = self.parse_sub_sig_or_prototype_opt()?;
4157                self.parse_sub_attributes()?;
4158                let body = self.parse_block()?;
4159                Ok(Statement {
4160                    label: None,
4161                    kind: StmtKind::SubDecl {
4162                        name,
4163                        params,
4164                        body,
4165                        prototype,
4166                    },
4167                    line,
4168                })
4169            }
4170            Token::LParen | Token::LBrace | Token::Colon => {
4171                // In no-interop mode, `sub {}` anonymous is not allowed — must use `fn {}`
4172                if is_sub_keyword && crate::no_interop_mode() {
4173                    return Err(self.syntax_err(
4174                        "stryke uses `fn {}` instead of `sub {}` (--no-interop)",
4175                        line,
4176                    ));
4177                }
4178                // Statement-level anonymous sub: `fn { }`, `sub () { }`, `sub :lvalue { }`
4179                let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
4180                self.parse_sub_attributes()?;
4181                let body = self.parse_block()?;
4182                Ok(Statement {
4183                    label: None,
4184                    kind: StmtKind::Expression(Expr {
4185                        kind: ExprKind::CodeRef { params, body },
4186                        line,
4187                    }),
4188                    line,
4189                })
4190            }
4191            tok => Err(self.syntax_err(
4192                format!("Expected sub name, `(`, `{{`, or `:`, got {:?}", tok),
4193                self.peek_line(),
4194            )),
4195        }
4196    }
4197
4198    /// `struct Name { field => Type, ... ; fn method { } }`
4199    fn parse_struct_decl(&mut self) -> PerlResult<Statement> {
4200        let line = self.peek_line();
4201        self.advance(); // struct
4202        let name = match self.advance() {
4203            (Token::Ident(n), _) => n,
4204            (tok, err_line) => {
4205                return Err(
4206                    self.syntax_err(format!("Expected struct name, got {:?}", tok), err_line)
4207                )
4208            }
4209        };
4210        self.expect(&Token::LBrace)?;
4211        let mut fields = Vec::new();
4212        let mut methods = Vec::new();
4213        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
4214            // Check for method definition: `fn name { }` or `fn name { }`
4215            let is_method = match self.peek() {
4216                Token::Ident(s) => s == "fn" || s == "sub",
4217                _ => false,
4218            };
4219            if is_method {
4220                self.advance(); // fn/sub
4221                let method_name = match self.advance() {
4222                    (Token::Ident(n), _) => n,
4223                    (tok, err_line) => {
4224                        return Err(self
4225                            .syntax_err(format!("Expected method name, got {:?}", tok), err_line))
4226                    }
4227                };
4228                // Parse optional signature: `($self, $arg: Type, ...)`
4229                let params = if self.eat(&Token::LParen) {
4230                    let p = self.parse_sub_signature_param_list()?;
4231                    self.expect(&Token::RParen)?;
4232                    p
4233                } else {
4234                    Vec::new()
4235                };
4236                // parse_block handles its own { } delimiters
4237                let body = self.parse_block()?;
4238                methods.push(crate::ast::StructMethod {
4239                    name: method_name,
4240                    params,
4241                    body,
4242                });
4243                // Optional trailing comma/semicolon after method
4244                self.eat(&Token::Comma);
4245                self.eat(&Token::Semicolon);
4246                continue;
4247            }
4248
4249            let field_name = match self.advance() {
4250                (Token::Ident(n), _) => n,
4251                (tok, err_line) => {
4252                    return Err(
4253                        self.syntax_err(format!("Expected field name, got {:?}", tok), err_line)
4254                    )
4255                }
4256            };
4257            // Support both `field => Type` and bare `field` (implies Any type)
4258            let ty = if self.eat(&Token::FatArrow) {
4259                self.parse_type_name()?
4260            } else {
4261                crate::ast::PerlTypeName::Any
4262            };
4263            let default = if self.eat(&Token::Assign) {
4264                // Use parse_ternary to avoid consuming commas (next field separator)
4265                Some(self.parse_ternary()?)
4266            } else {
4267                None
4268            };
4269            fields.push(StructField {
4270                name: field_name,
4271                ty,
4272                default,
4273            });
4274            if !self.eat(&Token::Comma) {
4275                // Also allow semicolons as field separators
4276                self.eat(&Token::Semicolon);
4277            }
4278        }
4279        self.expect(&Token::RBrace)?;
4280        self.eat(&Token::Semicolon);
4281        Ok(Statement {
4282            label: None,
4283            kind: StmtKind::StructDecl {
4284                def: StructDef {
4285                    name,
4286                    fields,
4287                    methods,
4288                },
4289            },
4290            line,
4291        })
4292    }
4293
4294    /// `enum Name { Variant1, Variant2 => Type, ... }`
4295    fn parse_enum_decl(&mut self) -> PerlResult<Statement> {
4296        let line = self.peek_line();
4297        self.advance(); // enum
4298        let name = match self.advance() {
4299            (Token::Ident(n), _) => n,
4300            (tok, err_line) => {
4301                return Err(self.syntax_err(format!("Expected enum name, got {:?}", tok), err_line))
4302            }
4303        };
4304        self.expect(&Token::LBrace)?;
4305        let mut variants = Vec::new();
4306        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
4307            let variant_name = match self.advance() {
4308                (Token::Ident(n), _) => n,
4309                (tok, err_line) => {
4310                    return Err(
4311                        self.syntax_err(format!("Expected variant name, got {:?}", tok), err_line)
4312                    )
4313                }
4314            };
4315            let ty = if self.eat(&Token::FatArrow) {
4316                Some(self.parse_type_name()?)
4317            } else {
4318                None
4319            };
4320            variants.push(EnumVariant {
4321                name: variant_name,
4322                ty,
4323            });
4324            if !self.eat(&Token::Comma) {
4325                self.eat(&Token::Semicolon);
4326            }
4327        }
4328        self.expect(&Token::RBrace)?;
4329        self.eat(&Token::Semicolon);
4330        Ok(Statement {
4331            label: None,
4332            kind: StmtKind::EnumDecl {
4333                def: EnumDef { name, variants },
4334            },
4335            line,
4336        })
4337    }
4338
4339    /// `[abstract|final] class Name extends Parent impl Trait { fields; methods }`
4340    fn parse_class_decl(&mut self, is_abstract: bool, is_final: bool) -> PerlResult<Statement> {
4341        use crate::ast::{ClassDef, ClassField, ClassMethod, ClassStaticField, Visibility};
4342        let line = self.peek_line();
4343        self.advance(); // class
4344        let name = match self.advance() {
4345            (Token::Ident(n), _) => n,
4346            (tok, err_line) => {
4347                return Err(self.syntax_err(format!("Expected class name, got {:?}", tok), err_line))
4348            }
4349        };
4350
4351        // Parse `extends Parent1, Parent2`
4352        let mut extends = Vec::new();
4353        if matches!(self.peek(), Token::Ident(ref s) if s == "extends") {
4354            self.advance(); // extends
4355            loop {
4356                match self.advance() {
4357                    (Token::Ident(parent), _) => extends.push(parent),
4358                    (tok, err_line) => {
4359                        return Err(self.syntax_err(
4360                            format!("Expected parent class name after `extends`, got {:?}", tok),
4361                            err_line,
4362                        ))
4363                    }
4364                }
4365                if !self.eat(&Token::Comma) {
4366                    break;
4367                }
4368            }
4369        }
4370
4371        // Parse `impl Trait1, Trait2`
4372        let mut implements = Vec::new();
4373        if matches!(self.peek(), Token::Ident(ref s) if s == "impl") {
4374            self.advance(); // impl
4375            loop {
4376                match self.advance() {
4377                    (Token::Ident(trait_name), _) => implements.push(trait_name),
4378                    (tok, err_line) => {
4379                        return Err(self.syntax_err(
4380                            format!("Expected trait name after `impl`, got {:?}", tok),
4381                            err_line,
4382                        ))
4383                    }
4384                }
4385                if !self.eat(&Token::Comma) {
4386                    break;
4387                }
4388            }
4389        }
4390
4391        self.expect(&Token::LBrace)?;
4392        let mut fields = Vec::new();
4393        let mut methods = Vec::new();
4394        let mut static_fields = Vec::new();
4395
4396        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
4397            // Check for visibility modifier
4398            let visibility = match self.peek() {
4399                Token::Ident(ref s) if s == "pub" => {
4400                    self.advance();
4401                    Visibility::Public
4402                }
4403                Token::Ident(ref s) if s == "priv" => {
4404                    self.advance();
4405                    Visibility::Private
4406                }
4407                Token::Ident(ref s) if s == "prot" => {
4408                    self.advance();
4409                    Visibility::Protected
4410                }
4411                _ => Visibility::Public, // default public
4412            };
4413
4414            // Check for static field: `static name: Type = default`
4415            if matches!(self.peek(), Token::Ident(ref s) if s == "static") {
4416                self.advance(); // static
4417
4418                // Could be a static method (`static fn`) or static field
4419                if matches!(self.peek(), Token::Ident(ref s) if s == "fn" || s == "sub") {
4420                    // static fn is same as fn Self.name — handled below but not here
4421                    return Err(self.syntax_err(
4422                        "use `fn Self.name` for static methods, not `static fn`",
4423                        self.peek_line(),
4424                    ));
4425                }
4426
4427                let field_name = match self.advance() {
4428                    (Token::Ident(n), _) => n,
4429                    (tok, err_line) => {
4430                        return Err(self.syntax_err(
4431                            format!("Expected static field name, got {:?}", tok),
4432                            err_line,
4433                        ))
4434                    }
4435                };
4436
4437                let ty = if self.eat(&Token::Colon) {
4438                    self.parse_type_name()?
4439                } else {
4440                    crate::ast::PerlTypeName::Any
4441                };
4442
4443                let default = if self.eat(&Token::Assign) {
4444                    Some(self.parse_ternary()?)
4445                } else {
4446                    None
4447                };
4448
4449                static_fields.push(ClassStaticField {
4450                    name: field_name,
4451                    ty,
4452                    visibility,
4453                    default,
4454                });
4455
4456                if !self.eat(&Token::Comma) {
4457                    self.eat(&Token::Semicolon);
4458                }
4459                continue;
4460            }
4461
4462            // Check for `final` modifier before fn
4463            let method_is_final = matches!(self.peek(), Token::Ident(ref s) if s == "final");
4464            if method_is_final {
4465                self.advance(); // final
4466            }
4467
4468            // Check for method: `fn name` or `fn Self.name` (static)
4469            let is_method = matches!(self.peek(), Token::Ident(ref s) if s == "fn" || s == "sub");
4470            if is_method {
4471                self.advance(); // fn/sub
4472
4473                // Check for static method: `fn Self.name`
4474                let is_static = matches!(self.peek(), Token::Ident(ref s) if s == "Self");
4475                if is_static {
4476                    self.advance(); // Self
4477                    self.expect(&Token::Dot)?;
4478                }
4479
4480                let method_name = match self.advance() {
4481                    (Token::Ident(n), _) => n,
4482                    (tok, err_line) => {
4483                        return Err(self
4484                            .syntax_err(format!("Expected method name, got {:?}", tok), err_line))
4485                    }
4486                };
4487
4488                // Parse optional signature
4489                let params = if self.eat(&Token::LParen) {
4490                    let p = self.parse_sub_signature_param_list()?;
4491                    self.expect(&Token::RParen)?;
4492                    p
4493                } else {
4494                    Vec::new()
4495                };
4496
4497                // Body is optional (abstract method in trait has no body)
4498                let body = if matches!(self.peek(), Token::LBrace) {
4499                    Some(self.parse_block()?)
4500                } else {
4501                    None
4502                };
4503
4504                methods.push(ClassMethod {
4505                    name: method_name,
4506                    params,
4507                    body,
4508                    visibility,
4509                    is_static,
4510                    is_final: method_is_final,
4511                });
4512                self.eat(&Token::Comma);
4513                self.eat(&Token::Semicolon);
4514                continue;
4515            } else if method_is_final {
4516                return Err(self.syntax_err("`final` must be followed by `fn`", self.peek_line()));
4517            }
4518
4519            // Parse field: `name: Type = default`
4520            let field_name = match self.advance() {
4521                (Token::Ident(n), _) => n,
4522                (tok, err_line) => {
4523                    return Err(
4524                        self.syntax_err(format!("Expected field name, got {:?}", tok), err_line)
4525                    )
4526                }
4527            };
4528
4529            // Type after colon: `name: Type`
4530            let ty = if self.eat(&Token::Colon) {
4531                self.parse_type_name()?
4532            } else {
4533                crate::ast::PerlTypeName::Any
4534            };
4535
4536            // Default value after `=`
4537            let default = if self.eat(&Token::Assign) {
4538                Some(self.parse_ternary()?)
4539            } else {
4540                None
4541            };
4542
4543            fields.push(ClassField {
4544                name: field_name,
4545                ty,
4546                visibility,
4547                default,
4548            });
4549
4550            if !self.eat(&Token::Comma) {
4551                self.eat(&Token::Semicolon);
4552            }
4553        }
4554
4555        self.expect(&Token::RBrace)?;
4556        self.eat(&Token::Semicolon);
4557
4558        Ok(Statement {
4559            label: None,
4560            kind: StmtKind::ClassDecl {
4561                def: ClassDef {
4562                    name,
4563                    is_abstract,
4564                    is_final,
4565                    extends,
4566                    implements,
4567                    fields,
4568                    methods,
4569                    static_fields,
4570                },
4571            },
4572            line,
4573        })
4574    }
4575
4576    /// `trait Name { fn required; fn with_default { } }`
4577    fn parse_trait_decl(&mut self) -> PerlResult<Statement> {
4578        use crate::ast::{ClassMethod, TraitDef, Visibility};
4579        let line = self.peek_line();
4580        self.advance(); // trait
4581        let name = match self.advance() {
4582            (Token::Ident(n), _) => n,
4583            (tok, err_line) => {
4584                return Err(self.syntax_err(format!("Expected trait name, got {:?}", tok), err_line))
4585            }
4586        };
4587
4588        self.expect(&Token::LBrace)?;
4589        let mut methods = Vec::new();
4590
4591        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
4592            // Optional visibility
4593            let visibility = match self.peek() {
4594                Token::Ident(ref s) if s == "pub" => {
4595                    self.advance();
4596                    Visibility::Public
4597                }
4598                Token::Ident(ref s) if s == "priv" => {
4599                    self.advance();
4600                    Visibility::Private
4601                }
4602                Token::Ident(ref s) if s == "prot" => {
4603                    self.advance();
4604                    Visibility::Protected
4605                }
4606                _ => Visibility::Public,
4607            };
4608
4609            // Expect `fn` or `sub`
4610            if !matches!(self.peek(), Token::Ident(ref s) if s == "fn" || s == "sub") {
4611                return Err(self.syntax_err("Expected `fn` in trait definition", self.peek_line()));
4612            }
4613            self.advance(); // fn/sub
4614
4615            let method_name = match self.advance() {
4616                (Token::Ident(n), _) => n,
4617                (tok, err_line) => {
4618                    return Err(
4619                        self.syntax_err(format!("Expected method name, got {:?}", tok), err_line)
4620                    )
4621                }
4622            };
4623
4624            // Optional signature
4625            let params = if self.eat(&Token::LParen) {
4626                let p = self.parse_sub_signature_param_list()?;
4627                self.expect(&Token::RParen)?;
4628                p
4629            } else {
4630                Vec::new()
4631            };
4632
4633            // Body is optional (no body = abstract/required method)
4634            let body = if matches!(self.peek(), Token::LBrace) {
4635                Some(self.parse_block()?)
4636            } else {
4637                None
4638            };
4639
4640            methods.push(ClassMethod {
4641                name: method_name,
4642                params,
4643                body,
4644                visibility,
4645                is_static: false,
4646                is_final: false,
4647            });
4648
4649            self.eat(&Token::Comma);
4650            self.eat(&Token::Semicolon);
4651        }
4652
4653        self.expect(&Token::RBrace)?;
4654        self.eat(&Token::Semicolon);
4655
4656        Ok(Statement {
4657            label: None,
4658            kind: StmtKind::TraitDecl {
4659                def: TraitDef { name, methods },
4660            },
4661            line,
4662        })
4663    }
4664
4665    fn local_simple_target_to_var_decl(target: &Expr) -> Option<VarDecl> {
4666        match &target.kind {
4667            ExprKind::ScalarVar(name) => Some(VarDecl {
4668                sigil: Sigil::Scalar,
4669                name: name.clone(),
4670                initializer: None,
4671                frozen: false,
4672                type_annotation: None,
4673            }),
4674            ExprKind::ArrayVar(name) => Some(VarDecl {
4675                sigil: Sigil::Array,
4676                name: name.clone(),
4677                initializer: None,
4678                frozen: false,
4679                type_annotation: None,
4680            }),
4681            ExprKind::HashVar(name) => Some(VarDecl {
4682                sigil: Sigil::Hash,
4683                name: name.clone(),
4684                initializer: None,
4685                frozen: false,
4686                type_annotation: None,
4687            }),
4688            ExprKind::Typeglob(name) => Some(VarDecl {
4689                sigil: Sigil::Typeglob,
4690                name: name.clone(),
4691                initializer: None,
4692                frozen: false,
4693                type_annotation: None,
4694            }),
4695            _ => None,
4696        }
4697    }
4698
4699    fn parse_decl_array_destructure(
4700        &mut self,
4701        keyword: &str,
4702        line: usize,
4703    ) -> PerlResult<Statement> {
4704        self.expect(&Token::LBracket)?;
4705        let elems = self.parse_match_array_elems_until_rbracket()?;
4706        self.expect(&Token::Assign)?;
4707        self.suppress_scalar_hash_brace += 1;
4708        let rhs = self.parse_expression()?;
4709        self.suppress_scalar_hash_brace -= 1;
4710        let stmt = self.desugar_array_destructure(keyword, line, elems, rhs)?;
4711        self.parse_stmt_postfix_modifier(stmt)
4712    }
4713
4714    fn parse_decl_hash_destructure(&mut self, keyword: &str, line: usize) -> PerlResult<Statement> {
4715        let MatchPattern::Hash(pairs) = self.parse_match_hash_pattern()? else {
4716            unreachable!("parse_match_hash_pattern returns Hash");
4717        };
4718        self.expect(&Token::Assign)?;
4719        self.suppress_scalar_hash_brace += 1;
4720        let rhs = self.parse_expression()?;
4721        self.suppress_scalar_hash_brace -= 1;
4722        let stmt = self.desugar_hash_destructure(keyword, line, pairs, rhs)?;
4723        self.parse_stmt_postfix_modifier(stmt)
4724    }
4725
4726    fn desugar_array_destructure(
4727        &mut self,
4728        keyword: &str,
4729        line: usize,
4730        elems: Vec<MatchArrayElem>,
4731        rhs: Expr,
4732    ) -> PerlResult<Statement> {
4733        let tmp = format!("__stryke_ds_{}", self.alloc_desugar_tmp());
4734        let mut stmts: Vec<Statement> = Vec::new();
4735        stmts.push(destructure_stmt_from_var_decls(
4736            keyword,
4737            vec![VarDecl {
4738                sigil: Sigil::Scalar,
4739                name: tmp.clone(),
4740                initializer: Some(rhs),
4741                frozen: false,
4742                type_annotation: None,
4743            }],
4744            line,
4745        ));
4746
4747        let has_rest = elems
4748            .iter()
4749            .any(|e| matches!(e, MatchArrayElem::Rest | MatchArrayElem::RestBind(_)));
4750        let fixed_slots = elems
4751            .iter()
4752            .filter(|e| {
4753                matches!(
4754                    e,
4755                    MatchArrayElem::CaptureScalar(_) | MatchArrayElem::Expr(_)
4756                )
4757            })
4758            .count();
4759        if !has_rest {
4760            let cond = Expr {
4761                kind: ExprKind::BinOp {
4762                    left: Box::new(destructure_expr_array_len(&tmp, line)),
4763                    op: BinOp::NumEq,
4764                    right: Box::new(Expr {
4765                        kind: ExprKind::Integer(fixed_slots as i64),
4766                        line,
4767                    }),
4768                },
4769                line,
4770            };
4771            stmts.push(destructure_stmt_unless_die(
4772                line,
4773                cond,
4774                "array destructure: length mismatch",
4775            ));
4776        }
4777
4778        let mut idx: i64 = 0;
4779        for elem in elems {
4780            match elem {
4781                MatchArrayElem::Rest => break,
4782                MatchArrayElem::RestBind(name) => {
4783                    let list_source = Expr {
4784                        kind: ExprKind::Deref {
4785                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
4786                            kind: Sigil::Array,
4787                        },
4788                        line,
4789                    };
4790                    let last_ix = Expr {
4791                        kind: ExprKind::BinOp {
4792                            left: Box::new(destructure_expr_array_len(&tmp, line)),
4793                            op: BinOp::Sub,
4794                            right: Box::new(Expr {
4795                                kind: ExprKind::Integer(1),
4796                                line,
4797                            }),
4798                        },
4799                        line,
4800                    };
4801                    let range = Expr {
4802                        kind: ExprKind::Range {
4803                            from: Box::new(Expr {
4804                                kind: ExprKind::Integer(idx),
4805                                line,
4806                            }),
4807                            to: Box::new(last_ix),
4808                            exclusive: false,
4809                            step: None,
4810                        },
4811                        line,
4812                    };
4813                    let slice = Expr {
4814                        kind: ExprKind::AnonymousListSlice {
4815                            source: Box::new(list_source),
4816                            indices: vec![range],
4817                        },
4818                        line,
4819                    };
4820                    stmts.push(destructure_stmt_from_var_decls(
4821                        keyword,
4822                        vec![VarDecl {
4823                            sigil: Sigil::Array,
4824                            name,
4825                            initializer: Some(slice),
4826                            frozen: false,
4827                            type_annotation: None,
4828                        }],
4829                        line,
4830                    ));
4831                    break;
4832                }
4833                MatchArrayElem::CaptureScalar(name) => {
4834                    let arrow = Expr {
4835                        kind: ExprKind::ArrowDeref {
4836                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
4837                            index: Box::new(Expr {
4838                                kind: ExprKind::Integer(idx),
4839                                line,
4840                            }),
4841                            kind: DerefKind::Array,
4842                        },
4843                        line,
4844                    };
4845                    stmts.push(destructure_stmt_from_var_decls(
4846                        keyword,
4847                        vec![VarDecl {
4848                            sigil: Sigil::Scalar,
4849                            name,
4850                            initializer: Some(arrow),
4851                            frozen: false,
4852                            type_annotation: None,
4853                        }],
4854                        line,
4855                    ));
4856                    idx += 1;
4857                }
4858                MatchArrayElem::Expr(e) => {
4859                    let elem_subj = Expr {
4860                        kind: ExprKind::ArrowDeref {
4861                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
4862                            index: Box::new(Expr {
4863                                kind: ExprKind::Integer(idx),
4864                                line,
4865                            }),
4866                            kind: DerefKind::Array,
4867                        },
4868                        line,
4869                    };
4870                    let match_expr = Expr {
4871                        kind: ExprKind::AlgebraicMatch {
4872                            subject: Box::new(elem_subj),
4873                            arms: vec![
4874                                MatchArm {
4875                                    pattern: MatchPattern::Value(Box::new(e.clone())),
4876                                    guard: None,
4877                                    body: Expr {
4878                                        kind: ExprKind::Integer(0),
4879                                        line,
4880                                    },
4881                                },
4882                                MatchArm {
4883                                    pattern: MatchPattern::Any,
4884                                    guard: None,
4885                                    body: Expr {
4886                                        kind: ExprKind::Die(vec![Expr {
4887                                            kind: ExprKind::String(
4888                                                "array destructure: element pattern mismatch"
4889                                                    .to_string(),
4890                                            ),
4891                                            line,
4892                                        }]),
4893                                        line,
4894                                    },
4895                                },
4896                            ],
4897                        },
4898                        line,
4899                    };
4900                    stmts.push(Statement {
4901                        label: None,
4902                        kind: StmtKind::Expression(match_expr),
4903                        line,
4904                    });
4905                    idx += 1;
4906                }
4907            }
4908        }
4909
4910        Ok(Statement {
4911            label: None,
4912            kind: StmtKind::StmtGroup(stmts),
4913            line,
4914        })
4915    }
4916
4917    fn desugar_hash_destructure(
4918        &mut self,
4919        keyword: &str,
4920        line: usize,
4921        pairs: Vec<MatchHashPair>,
4922        rhs: Expr,
4923    ) -> PerlResult<Statement> {
4924        let tmp = format!("__stryke_ds_{}", self.alloc_desugar_tmp());
4925        let mut stmts: Vec<Statement> = Vec::new();
4926        stmts.push(destructure_stmt_from_var_decls(
4927            keyword,
4928            vec![VarDecl {
4929                sigil: Sigil::Scalar,
4930                name: tmp.clone(),
4931                initializer: Some(rhs),
4932                frozen: false,
4933                type_annotation: None,
4934            }],
4935            line,
4936        ));
4937
4938        for pair in pairs {
4939            match pair {
4940                MatchHashPair::KeyOnly { key } => {
4941                    let exists_op = Expr {
4942                        kind: ExprKind::Exists(Box::new(Expr {
4943                            kind: ExprKind::ArrowDeref {
4944                                expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
4945                                index: Box::new(key),
4946                                kind: DerefKind::Hash,
4947                            },
4948                            line,
4949                        })),
4950                        line,
4951                    };
4952                    stmts.push(destructure_stmt_unless_die(
4953                        line,
4954                        exists_op,
4955                        "hash destructure: missing required key",
4956                    ));
4957                }
4958                MatchHashPair::Capture { key, name } => {
4959                    let init = Expr {
4960                        kind: ExprKind::ArrowDeref {
4961                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
4962                            index: Box::new(key),
4963                            kind: DerefKind::Hash,
4964                        },
4965                        line,
4966                    };
4967                    stmts.push(destructure_stmt_from_var_decls(
4968                        keyword,
4969                        vec![VarDecl {
4970                            sigil: Sigil::Scalar,
4971                            name,
4972                            initializer: Some(init),
4973                            frozen: false,
4974                            type_annotation: None,
4975                        }],
4976                        line,
4977                    ));
4978                }
4979            }
4980        }
4981
4982        Ok(Statement {
4983            label: None,
4984            kind: StmtKind::StmtGroup(stmts),
4985            line,
4986        })
4987    }
4988
4989    fn parse_my_our_local(
4990        &mut self,
4991        keyword: &str,
4992        allow_type_annotation: bool,
4993    ) -> PerlResult<Statement> {
4994        let line = self.peek_line();
4995        self.advance(); // 'my'/'our'/'local'
4996
4997        if keyword == "local"
4998            && !matches!(self.peek(), Token::LParen | Token::LBracket | Token::LBrace)
4999        {
5000            let target = self.parse_postfix()?;
5001            let mut initializer: Option<Expr> = None;
5002            if self.eat(&Token::Assign) {
5003                initializer = Some(self.parse_expression()?);
5004            } else if matches!(
5005                self.peek(),
5006                Token::OrAssign | Token::DefinedOrAssign | Token::AndAssign
5007            ) {
5008                if matches!(&target.kind, ExprKind::Typeglob(_)) {
5009                    return Err(self.syntax_err(
5010                        "compound assignment on typeglob declaration is not supported",
5011                        self.peek_line(),
5012                    ));
5013                }
5014                let op = match self.peek().clone() {
5015                    Token::OrAssign => BinOp::LogOr,
5016                    Token::DefinedOrAssign => BinOp::DefinedOr,
5017                    Token::AndAssign => BinOp::LogAnd,
5018                    _ => unreachable!(),
5019                };
5020                self.advance();
5021                let rhs = self.parse_assign_expr()?;
5022                let tgt_line = target.line;
5023                initializer = Some(Expr {
5024                    kind: ExprKind::CompoundAssign {
5025                        target: Box::new(target.clone()),
5026                        op,
5027                        value: Box::new(rhs),
5028                    },
5029                    line: tgt_line,
5030                });
5031            }
5032
5033            let kind = if let Some(mut decl) = Self::local_simple_target_to_var_decl(&target) {
5034                decl.initializer = initializer;
5035                StmtKind::Local(vec![decl])
5036            } else {
5037                StmtKind::LocalExpr {
5038                    target,
5039                    initializer,
5040                }
5041            };
5042            let stmt = Statement {
5043                label: None,
5044                kind,
5045                line,
5046            };
5047            return self.parse_stmt_postfix_modifier(stmt);
5048        }
5049
5050        if matches!(self.peek(), Token::LBracket) {
5051            return self.parse_decl_array_destructure(keyword, line);
5052        }
5053        if matches!(self.peek(), Token::LBrace) {
5054            return self.parse_decl_hash_destructure(keyword, line);
5055        }
5056
5057        let mut decls = Vec::new();
5058
5059        if self.eat(&Token::LParen) {
5060            // my ($a, @b, %c)
5061            while !matches!(self.peek(), Token::RParen | Token::Eof) {
5062                let decl = self.parse_var_decl(allow_type_annotation)?;
5063                decls.push(decl);
5064                if !self.eat(&Token::Comma) {
5065                    break;
5066                }
5067            }
5068            self.expect(&Token::RParen)?;
5069        } else {
5070            decls.push(self.parse_var_decl(allow_type_annotation)?);
5071        }
5072
5073        // Optional initializer: my $x = expr — plus `our @EXPORT = our @EXPORT_OK = qw(...)` (Try::Tiny).
5074        if self.eat(&Token::Assign) {
5075            if keyword == "our" && decls.len() == 1 {
5076                while matches!(self.peek(), Token::Ident(ref i) if i == "our") {
5077                    self.advance();
5078                    decls.push(self.parse_var_decl(allow_type_annotation)?);
5079                    if !self.eat(&Token::Assign) {
5080                        return Err(self.syntax_err(
5081                            "expected `=` after `our` in chained our-declaration",
5082                            self.peek_line(),
5083                        ));
5084                    }
5085                }
5086            }
5087            let val = self.parse_expression()?;
5088            // Validate assignment for single variable declarations (not destructuring)
5089            // `my ($a, $b) = (1, 2)` is destructuring, not scalar-from-list
5090            if !crate::compat_mode() && decls.len() == 1 {
5091                let decl = &decls[0];
5092                let target_kind = match decl.sigil {
5093                    Sigil::Scalar => ExprKind::ScalarVar(decl.name.clone()),
5094                    Sigil::Array => ExprKind::ArrayVar(decl.name.clone()),
5095                    Sigil::Hash => ExprKind::HashVar(decl.name.clone()),
5096                    Sigil::Typeglob => {
5097                        // Skip validation for typeglob
5098                        if decls.len() == 1 {
5099                            decls[0].initializer = Some(val);
5100                        } else {
5101                            for d in &mut decls {
5102                                d.initializer = Some(val.clone());
5103                            }
5104                        }
5105                        return Ok(Statement {
5106                            label: None,
5107                            kind: match keyword {
5108                                "my" => StmtKind::My(decls),
5109                                "mysync" => StmtKind::MySync(decls),
5110                                "our" => StmtKind::Our(decls),
5111                                "local" => StmtKind::Local(decls),
5112                                "state" => StmtKind::State(decls),
5113                                _ => unreachable!(),
5114                            },
5115                            line,
5116                        });
5117                    }
5118                };
5119                let target = Expr {
5120                    kind: target_kind,
5121                    line,
5122                };
5123                self.validate_assignment(&target, &val, line)?;
5124            }
5125            if decls.len() == 1 {
5126                decls[0].initializer = Some(val);
5127            } else {
5128                for decl in &mut decls {
5129                    decl.initializer = Some(val.clone());
5130                }
5131            }
5132        } else if decls.len() == 1 {
5133            // `our $Verbose ||= 0` (Exporter.pm) — compound assign on a single decl
5134            let op = match self.peek().clone() {
5135                Token::OrAssign => Some(BinOp::LogOr),
5136                Token::DefinedOrAssign => Some(BinOp::DefinedOr),
5137                Token::AndAssign => Some(BinOp::LogAnd),
5138                _ => None,
5139            };
5140            if let Some(op) = op {
5141                let d = &decls[0];
5142                if matches!(d.sigil, Sigil::Typeglob) {
5143                    return Err(self.syntax_err(
5144                        "compound assignment on typeglob declaration is not supported",
5145                        self.peek_line(),
5146                    ));
5147                }
5148                self.advance();
5149                let rhs = self.parse_assign_expr()?;
5150                let target = Expr {
5151                    kind: match d.sigil {
5152                        Sigil::Scalar => ExprKind::ScalarVar(d.name.clone()),
5153                        Sigil::Array => ExprKind::ArrayVar(d.name.clone()),
5154                        Sigil::Hash => ExprKind::HashVar(d.name.clone()),
5155                        Sigil::Typeglob => unreachable!(),
5156                    },
5157                    line,
5158                };
5159                decls[0].initializer = Some(Expr {
5160                    kind: ExprKind::CompoundAssign {
5161                        target: Box::new(target),
5162                        op,
5163                        value: Box::new(rhs),
5164                    },
5165                    line,
5166                });
5167            }
5168        }
5169
5170        let kind = match keyword {
5171            "my" => StmtKind::My(decls),
5172            "mysync" => StmtKind::MySync(decls),
5173            "our" => StmtKind::Our(decls),
5174            "local" => StmtKind::Local(decls),
5175            "state" => StmtKind::State(decls),
5176            _ => unreachable!(),
5177        };
5178        let stmt = Statement {
5179            label: None,
5180            kind,
5181            line,
5182        };
5183        // `my $x = 1 if $y;` — statement modifier applies to the whole declaration (Perl).
5184        self.parse_stmt_postfix_modifier(stmt)
5185    }
5186
5187    fn parse_var_decl(&mut self, allow_type_annotation: bool) -> PerlResult<VarDecl> {
5188        let mut decl = match self.advance() {
5189            (Token::ScalarVar(name), _) => VarDecl {
5190                sigil: Sigil::Scalar,
5191                name,
5192                initializer: None,
5193                frozen: false,
5194                type_annotation: None,
5195            },
5196            (Token::ArrayVar(name), _) => VarDecl {
5197                sigil: Sigil::Array,
5198                name,
5199                initializer: None,
5200                frozen: false,
5201                type_annotation: None,
5202            },
5203            (Token::HashVar(name), line) => {
5204                if !crate::compat_mode() {
5205                    self.check_hash_shadows_reserved(&name, line)?;
5206                }
5207                VarDecl {
5208                    sigil: Sigil::Hash,
5209                    name,
5210                    initializer: None,
5211                    frozen: false,
5212                    type_annotation: None,
5213                }
5214            }
5215            (Token::Star, _line) => {
5216                let name = match self.advance() {
5217                    (Token::Ident(n), _) => n,
5218                    (tok, l) => {
5219                        return Err(self
5220                            .syntax_err(format!("Expected identifier after *, got {:?}", tok), l));
5221                    }
5222                };
5223                VarDecl {
5224                    sigil: Sigil::Typeglob,
5225                    name,
5226                    initializer: None,
5227                    frozen: false,
5228                    type_annotation: None,
5229                }
5230            }
5231            // `my ($a, undef, $c) = (1, 2, 3)` — Perl idiom for discarding a
5232            // slot in a list assignment. The interpreter treats `undef`-named
5233            // scalar decls as throwaway: declared into a unique sink so the
5234            // distribute-to-decls loop advances past the slot.
5235            (Token::Ident(ref kw), _) if kw == "undef" => VarDecl {
5236                sigil: Sigil::Scalar,
5237                // Synthesize a name that user code cannot reference. Each
5238                // sink slot in a list-assign gets its own unique name so the
5239                // declarations don't collide.
5240                name: format!("__undef_sink_{}", self.pos),
5241                initializer: None,
5242                frozen: false,
5243                type_annotation: None,
5244            },
5245            (tok, line) => {
5246                return Err(self.syntax_err(
5247                    format!("Expected variable in declaration, got {:?}", tok),
5248                    line,
5249                ));
5250            }
5251        };
5252        if allow_type_annotation && self.eat(&Token::Colon) {
5253            let ty = self.parse_type_name()?;
5254            if decl.sigil != Sigil::Scalar {
5255                return Err(self.syntax_err(
5256                    "`: Type` is only valid for scalar declarations (typed my $name : Int)",
5257                    self.peek_line(),
5258                ));
5259            }
5260            decl.type_annotation = Some(ty);
5261        }
5262        Ok(decl)
5263    }
5264
5265    fn parse_type_name(&mut self) -> PerlResult<PerlTypeName> {
5266        match self.advance() {
5267            (Token::Ident(name), _) => match name.as_str() {
5268                "Int" => Ok(PerlTypeName::Int),
5269                "Str" => Ok(PerlTypeName::Str),
5270                "Float" => Ok(PerlTypeName::Float),
5271                "Bool" => Ok(PerlTypeName::Bool),
5272                "Array" => Ok(PerlTypeName::Array),
5273                "Hash" => Ok(PerlTypeName::Hash),
5274                "Ref" => Ok(PerlTypeName::Ref),
5275                "Any" => Ok(PerlTypeName::Any),
5276                _ => Ok(PerlTypeName::Struct(name)),
5277            },
5278            (tok, err_line) => Err(self.syntax_err(
5279                format!("Expected type name after `:`, got {:?}", tok),
5280                err_line,
5281            )),
5282        }
5283    }
5284
5285    fn parse_package(&mut self) -> PerlResult<Statement> {
5286        let line = self.peek_line();
5287        self.advance(); // 'package'
5288        let name = match self.advance() {
5289            (Token::Ident(n), _) => n,
5290            (tok, line) => {
5291                return Err(self.syntax_err(format!("Expected package name, got {:?}", tok), line))
5292            }
5293        };
5294        // Handle Foo::Bar
5295        let mut full_name = name;
5296        while self.eat(&Token::PackageSep) {
5297            if let (Token::Ident(part), _) = self.advance() {
5298                full_name = format!("{}::{}", full_name, part);
5299            }
5300        }
5301        self.eat(&Token::Semicolon);
5302        Ok(Statement {
5303            label: None,
5304            kind: StmtKind::Package { name: full_name },
5305            line,
5306        })
5307    }
5308
5309    fn parse_use(&mut self) -> PerlResult<Statement> {
5310        let line = self.peek_line();
5311        self.advance(); // 'use'
5312        let (tok, tok_line) = self.advance();
5313        match tok {
5314            Token::Float(v) => {
5315                self.eat(&Token::Semicolon);
5316                Ok(Statement {
5317                    label: None,
5318                    kind: StmtKind::UsePerlVersion { version: v },
5319                    line,
5320                })
5321            }
5322            Token::Integer(n) => {
5323                if matches!(self.peek(), Token::Semicolon | Token::Eof) {
5324                    self.eat(&Token::Semicolon);
5325                    Ok(Statement {
5326                        label: None,
5327                        kind: StmtKind::UsePerlVersion { version: n as f64 },
5328                        line,
5329                    })
5330                } else {
5331                    Err(self.syntax_err(
5332                        format!("Expected ';' after use VERSION (got {:?})", self.peek()),
5333                        line,
5334                    ))
5335                }
5336            }
5337            Token::Ident(n) => {
5338                let mut full_name = n;
5339                while self.eat(&Token::PackageSep) {
5340                    if let (Token::Ident(part), _) = self.advance() {
5341                        full_name = format!("{}::{}", full_name, part);
5342                    }
5343                }
5344                if full_name == "overload" {
5345                    let mut pairs = Vec::new();
5346                    let mut parse_overload_pairs = |this: &mut Self| -> PerlResult<()> {
5347                        loop {
5348                            if matches!(this.peek(), Token::RParen | Token::Semicolon | Token::Eof)
5349                            {
5350                                break;
5351                            }
5352                            let key_e = this.parse_assign_expr()?;
5353                            this.expect(&Token::FatArrow)?;
5354                            let val_e = this.parse_assign_expr()?;
5355                            let key = this.expr_to_overload_key(&key_e)?;
5356                            let val = this.expr_to_overload_sub(&val_e)?;
5357                            pairs.push((key, val));
5358                            if !this.eat(&Token::Comma) {
5359                                break;
5360                            }
5361                        }
5362                        Ok(())
5363                    };
5364                    if self.eat(&Token::LParen) {
5365                        // `use overload ();` — common in JSON::PP and other modules.
5366                        parse_overload_pairs(self)?;
5367                        self.expect(&Token::RParen)?;
5368                    } else if !matches!(self.peek(), Token::Semicolon | Token::Eof) {
5369                        parse_overload_pairs(self)?;
5370                    }
5371                    self.eat(&Token::Semicolon);
5372                    return Ok(Statement {
5373                        label: None,
5374                        kind: StmtKind::UseOverload { pairs },
5375                        line,
5376                    });
5377                }
5378                let mut imports = Vec::new();
5379                if !matches!(self.peek(), Token::Semicolon | Token::Eof)
5380                    && !self.next_is_new_statement_start(tok_line)
5381                {
5382                    loop {
5383                        if matches!(self.peek(), Token::Semicolon | Token::Eof) {
5384                            break;
5385                        }
5386                        imports.push(self.parse_expression()?);
5387                        if !self.eat(&Token::Comma) {
5388                            break;
5389                        }
5390                    }
5391                }
5392                self.eat(&Token::Semicolon);
5393                Ok(Statement {
5394                    label: None,
5395                    kind: StmtKind::Use {
5396                        module: full_name,
5397                        imports,
5398                    },
5399                    line,
5400                })
5401            }
5402            other => Err(self.syntax_err(
5403                format!("Expected module name or version after use, got {:?}", other),
5404                tok_line,
5405            )),
5406        }
5407    }
5408
5409    fn parse_no(&mut self) -> PerlResult<Statement> {
5410        let line = self.peek_line();
5411        self.advance(); // 'no'
5412        let module = match self.advance() {
5413            (Token::Ident(n), tok_line) => (n, tok_line),
5414            (tok, line) => {
5415                return Err(self.syntax_err(
5416                    format!("Expected module name after no, got {:?}", tok),
5417                    line,
5418                ))
5419            }
5420        };
5421        let (module_name, tok_line) = module;
5422        let mut full_name = module_name;
5423        while self.eat(&Token::PackageSep) {
5424            if let (Token::Ident(part), _) = self.advance() {
5425                full_name = format!("{}::{}", full_name, part);
5426            }
5427        }
5428        let mut imports = Vec::new();
5429        if !matches!(self.peek(), Token::Semicolon | Token::Eof)
5430            && !self.next_is_new_statement_start(tok_line)
5431        {
5432            loop {
5433                if matches!(self.peek(), Token::Semicolon | Token::Eof) {
5434                    break;
5435                }
5436                imports.push(self.parse_expression()?);
5437                if !self.eat(&Token::Comma) {
5438                    break;
5439                }
5440            }
5441        }
5442        self.eat(&Token::Semicolon);
5443        Ok(Statement {
5444            label: None,
5445            kind: StmtKind::No {
5446                module: full_name,
5447                imports,
5448            },
5449            line,
5450        })
5451    }
5452
5453    fn parse_return(&mut self) -> PerlResult<Statement> {
5454        let line = self.peek_line();
5455        self.advance(); // 'return'
5456        let val = if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof) {
5457            None
5458        } else {
5459            // Only parse up to the assign level to avoid consuming postfix if/unless
5460            Some(self.parse_assign_expr()?)
5461        };
5462        // Check for postfix modifiers on return
5463        let stmt = Statement {
5464            label: None,
5465            kind: StmtKind::Return(val),
5466            line,
5467        };
5468        if let Token::Ident(ref kw) = self.peek().clone() {
5469            match kw.as_str() {
5470                "if" => {
5471                    self.advance();
5472                    let cond = self.parse_expression()?;
5473                    self.eat(&Token::Semicolon);
5474                    return Ok(Statement {
5475                        label: None,
5476                        kind: StmtKind::If {
5477                            condition: cond,
5478                            body: vec![stmt],
5479                            elsifs: vec![],
5480                            else_block: None,
5481                        },
5482                        line,
5483                    });
5484                }
5485                "unless" => {
5486                    self.advance();
5487                    let cond = self.parse_expression()?;
5488                    self.eat(&Token::Semicolon);
5489                    return Ok(Statement {
5490                        label: None,
5491                        kind: StmtKind::Unless {
5492                            condition: cond,
5493                            body: vec![stmt],
5494                            else_block: None,
5495                        },
5496                        line,
5497                    });
5498                }
5499                _ => {}
5500            }
5501        }
5502        self.eat(&Token::Semicolon);
5503        Ok(stmt)
5504    }
5505
5506    // ── Expressions (Pratt / precedence climbing) ──
5507
5508    fn parse_expression(&mut self) -> PerlResult<Expr> {
5509        self.parse_comma_expr()
5510    }
5511
5512    fn parse_comma_expr(&mut self) -> PerlResult<Expr> {
5513        let expr = self.parse_assign_expr()?;
5514        let mut exprs = vec![expr];
5515        while self.eat(&Token::Comma) || self.eat(&Token::FatArrow) {
5516            if matches!(
5517                self.peek(),
5518                Token::RParen | Token::RBracket | Token::RBrace | Token::Semicolon | Token::Eof
5519            ) {
5520                break; // trailing comma
5521            }
5522            exprs.push(self.parse_assign_expr()?);
5523        }
5524        if exprs.len() == 1 {
5525            return Ok(exprs.pop().unwrap());
5526        }
5527        let line = exprs[0].line;
5528        Ok(Expr {
5529            kind: ExprKind::List(exprs),
5530            line,
5531        })
5532    }
5533
5534    fn parse_assign_expr(&mut self) -> PerlResult<Expr> {
5535        let expr = self.parse_ternary()?;
5536        let line = expr.line;
5537
5538        match self.peek().clone() {
5539            Token::Assign => {
5540                self.advance();
5541                let right = self.parse_assign_expr()?;
5542                // Desugar `$obj->field = value` into `$obj->field(value)` (setter call)
5543                if let ExprKind::MethodCall { ref args, .. } = expr.kind {
5544                    if args.is_empty() {
5545                        // Destructure again to take ownership
5546                        let ExprKind::MethodCall {
5547                            object,
5548                            method,
5549                            super_call,
5550                            ..
5551                        } = expr.kind
5552                        else {
5553                            unreachable!()
5554                        };
5555                        return Ok(Expr {
5556                            kind: ExprKind::MethodCall {
5557                                object,
5558                                method,
5559                                args: vec![right],
5560                                super_call,
5561                            },
5562                            line,
5563                        });
5564                    }
5565                }
5566                self.validate_assignment(&expr, &right, line)?;
5567                Ok(Expr {
5568                    kind: ExprKind::Assign {
5569                        target: Box::new(expr),
5570                        value: Box::new(right),
5571                    },
5572                    line,
5573                })
5574            }
5575            Token::PlusAssign => {
5576                self.advance();
5577                let r = self.parse_assign_expr()?;
5578                Ok(Expr {
5579                    kind: ExprKind::CompoundAssign {
5580                        target: Box::new(expr),
5581                        op: BinOp::Add,
5582                        value: Box::new(r),
5583                    },
5584                    line,
5585                })
5586            }
5587            Token::MinusAssign => {
5588                self.advance();
5589                let r = self.parse_assign_expr()?;
5590                Ok(Expr {
5591                    kind: ExprKind::CompoundAssign {
5592                        target: Box::new(expr),
5593                        op: BinOp::Sub,
5594                        value: Box::new(r),
5595                    },
5596                    line,
5597                })
5598            }
5599            Token::MulAssign => {
5600                self.advance();
5601                let r = self.parse_assign_expr()?;
5602                Ok(Expr {
5603                    kind: ExprKind::CompoundAssign {
5604                        target: Box::new(expr),
5605                        op: BinOp::Mul,
5606                        value: Box::new(r),
5607                    },
5608                    line,
5609                })
5610            }
5611            Token::DivAssign => {
5612                self.advance();
5613                let r = self.parse_assign_expr()?;
5614                Ok(Expr {
5615                    kind: ExprKind::CompoundAssign {
5616                        target: Box::new(expr),
5617                        op: BinOp::Div,
5618                        value: Box::new(r),
5619                    },
5620                    line,
5621                })
5622            }
5623            Token::ModAssign => {
5624                self.advance();
5625                let r = self.parse_assign_expr()?;
5626                Ok(Expr {
5627                    kind: ExprKind::CompoundAssign {
5628                        target: Box::new(expr),
5629                        op: BinOp::Mod,
5630                        value: Box::new(r),
5631                    },
5632                    line,
5633                })
5634            }
5635            Token::PowAssign => {
5636                self.advance();
5637                let r = self.parse_assign_expr()?;
5638                Ok(Expr {
5639                    kind: ExprKind::CompoundAssign {
5640                        target: Box::new(expr),
5641                        op: BinOp::Pow,
5642                        value: Box::new(r),
5643                    },
5644                    line,
5645                })
5646            }
5647            Token::DotAssign => {
5648                self.advance();
5649                let r = self.parse_assign_expr()?;
5650                Ok(Expr {
5651                    kind: ExprKind::CompoundAssign {
5652                        target: Box::new(expr),
5653                        op: BinOp::Concat,
5654                        value: Box::new(r),
5655                    },
5656                    line,
5657                })
5658            }
5659            Token::BitAndAssign => {
5660                self.advance();
5661                let r = self.parse_assign_expr()?;
5662                Ok(Expr {
5663                    kind: ExprKind::CompoundAssign {
5664                        target: Box::new(expr),
5665                        op: BinOp::BitAnd,
5666                        value: Box::new(r),
5667                    },
5668                    line,
5669                })
5670            }
5671            Token::BitOrAssign => {
5672                self.advance();
5673                let r = self.parse_assign_expr()?;
5674                Ok(Expr {
5675                    kind: ExprKind::CompoundAssign {
5676                        target: Box::new(expr),
5677                        op: BinOp::BitOr,
5678                        value: Box::new(r),
5679                    },
5680                    line,
5681                })
5682            }
5683            Token::XorAssign => {
5684                self.advance();
5685                let r = self.parse_assign_expr()?;
5686                Ok(Expr {
5687                    kind: ExprKind::CompoundAssign {
5688                        target: Box::new(expr),
5689                        op: BinOp::BitXor,
5690                        value: Box::new(r),
5691                    },
5692                    line,
5693                })
5694            }
5695            Token::ShiftLeftAssign => {
5696                self.advance();
5697                let r = self.parse_assign_expr()?;
5698                Ok(Expr {
5699                    kind: ExprKind::CompoundAssign {
5700                        target: Box::new(expr),
5701                        op: BinOp::ShiftLeft,
5702                        value: Box::new(r),
5703                    },
5704                    line,
5705                })
5706            }
5707            Token::ShiftRightAssign => {
5708                self.advance();
5709                let r = self.parse_assign_expr()?;
5710                Ok(Expr {
5711                    kind: ExprKind::CompoundAssign {
5712                        target: Box::new(expr),
5713                        op: BinOp::ShiftRight,
5714                        value: Box::new(r),
5715                    },
5716                    line,
5717                })
5718            }
5719            Token::OrAssign => {
5720                self.advance();
5721                let r = self.parse_assign_expr()?;
5722                Ok(Expr {
5723                    kind: ExprKind::CompoundAssign {
5724                        target: Box::new(expr),
5725                        op: BinOp::LogOr,
5726                        value: Box::new(r),
5727                    },
5728                    line,
5729                })
5730            }
5731            Token::DefinedOrAssign => {
5732                self.advance();
5733                let r = self.parse_assign_expr()?;
5734                Ok(Expr {
5735                    kind: ExprKind::CompoundAssign {
5736                        target: Box::new(expr),
5737                        op: BinOp::DefinedOr,
5738                        value: Box::new(r),
5739                    },
5740                    line,
5741                })
5742            }
5743            Token::AndAssign => {
5744                self.advance();
5745                let r = self.parse_assign_expr()?;
5746                Ok(Expr {
5747                    kind: ExprKind::CompoundAssign {
5748                        target: Box::new(expr),
5749                        op: BinOp::LogAnd,
5750                        value: Box::new(r),
5751                    },
5752                    line,
5753                })
5754            }
5755            _ => Ok(expr),
5756        }
5757    }
5758
5759    fn parse_ternary(&mut self) -> PerlResult<Expr> {
5760        let expr = self.parse_pipe_forward()?;
5761        if self.eat(&Token::Question) {
5762            let line = expr.line;
5763            self.suppress_colon_range = self.suppress_colon_range.saturating_add(1);
5764            let then_expr = self.parse_assign_expr();
5765            self.suppress_colon_range = self.suppress_colon_range.saturating_sub(1);
5766            let then_expr = then_expr?;
5767            self.expect(&Token::Colon)?;
5768            let else_expr = self.parse_assign_expr()?;
5769            return Ok(Expr {
5770                kind: ExprKind::Ternary {
5771                    condition: Box::new(expr),
5772                    then_expr: Box::new(then_expr),
5773                    else_expr: Box::new(else_expr),
5774                },
5775                line,
5776            });
5777        }
5778        Ok(expr)
5779    }
5780
5781    /// `EXPR |> CALL` — pipe-forward (F#/Elixir). Left-associative; the LHS is threaded
5782    /// in as the **first argument** of the RHS call at parse time (pure AST rewrite,
5783    /// no runtime cost). `x |> f(a, b)` → `f(x, a, b)`; `x |> f` → `f(x)`; chain
5784    /// `x |> f |> g(2)` → `g(f(x), 2)`. Precedence sits between `?:` and `||`, so
5785    /// `x + 1 |> f || y` parses as `f(x + 1) || y`.
5786    fn parse_pipe_forward(&mut self) -> PerlResult<Expr> {
5787        let mut left = self.parse_or_word()?;
5788        // Inside a paren-less arg list, `|>` is a hard terminator for the
5789        // enclosing call — leave it for the outer `parse_pipe_forward` loop
5790        // so `qw(…) |> head 2 |> join "-"` chains left-to-right as
5791        // `(qw(…) |> head 2) |> join "-"` instead of `head` swallowing the
5792        // outer `|>` via its first-arg `parse_assign_expr`.
5793        if self.no_pipe_forward_depth > 0 {
5794            return Ok(left);
5795        }
5796        while matches!(self.peek(), Token::PipeForward) {
5797            if crate::compat_mode() {
5798                return Err(self.syntax_err(
5799                    "pipe-forward operator `|>` is a stryke extension (disabled by --compat)",
5800                    left.line,
5801                ));
5802            }
5803            let line = left.line;
5804            self.advance();
5805            // Set pipe-RHS context so list-taking builtins (`map`, `grep`,
5806            // `join`, …) accept a placeholder in place of their list operand.
5807            self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_add(1);
5808            let right_result = self.parse_or_word();
5809            self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_sub(1);
5810            let right = right_result?;
5811            left = self.pipe_forward_apply(left, right, line)?;
5812        }
5813        Ok(left)
5814    }
5815
5816    /// Desugar `lhs |> rhs`: thread `lhs` into the call that `rhs` represents as
5817    /// its **first** argument (Elixir / R / proposed-JS convention).
5818    ///
5819    /// The strategy depends on the shape of `rhs`:
5820    /// - Generic calls (`FuncCall`, `MethodCall`, `IndirectCall`) and variadic
5821    ///   builtins (`Print`, `Say`, `Printf`, `Die`, `Warn`, `Sprintf`, `System`,
5822    ///   `Exec`, `Unlink`, `Chmod`, `Chown`, `Glob`, …) — **prepend** `lhs` to
5823    ///   the args list. So `URL |> json_jq ".[]"` → `json_jq(URL, ".[]")`,
5824    ///   matching the `(data, filter)` signature the builtin expects.
5825    /// - Unary-style builtins (`Length`, `Abs`, `Lc`, `Uc`, `Defined`, `Ref`,
5826    ///   `Keys`, `Values`, `Pop`, `Shift`, …) — **replace** the sole operand with
5827    ///   `lhs` (these parse a single default `$_` when called without an arg, so
5828    ///   piping overrides that default; first-arg and last-arg are identical).
5829    /// - List-taking higher-order forms (`map`, `flat_map`, `grep`, `sort`, `join`, `reduce`, `fold`,
5830    ///   `pmap`, `pflat_map`, `pgrep`, `pfor`, …) — **replace** the `list` field with `lhs`, so
5831    ///   `@arr |> map { $_ * 2 }` becomes `map { $_ * 2 } @arr`.
5832    /// - `Bareword("f")` — lift to `FuncCall { f, [lhs] }`.
5833    /// - Scalar / deref / coderef expressions — wrap in `IndirectCall` with `lhs`
5834    ///   as the sole argument.
5835    /// - Ambiguous forms (binary ops, ternaries, literals, lists) — parse error,
5836    ///   since silently calling a non-callable at runtime would be worse.
5837    fn pipe_forward_apply(&self, lhs: Expr, rhs: Expr, line: usize) -> PerlResult<Expr> {
5838        let Expr { kind, line: rline } = rhs;
5839        let new_kind = match kind {
5840            // ── Generic / user-defined calls ───────────────────────────────────
5841            ExprKind::FuncCall { name, mut args } => {
5842                // Stryke builtins are unprefixed; `CORE::` callers route back to the
5843                // bare-name pipe-forward dispatch below.
5844                let dispatch_name: &str = name.strip_prefix("CORE::").unwrap_or(name.as_str());
5845                match dispatch_name {
5846                    "puniq" | "uniq" | "distinct" | "flatten" | "set" | "list_count"
5847                    | "list_size" | "count" | "size" | "cnt" | "len" | "with_index" | "shuffle"
5848                    | "shuffled" | "frequencies" | "freq" | "interleave" | "ddump"
5849                    | "stringify" | "str" | "lines" | "words" | "chars" | "digits" | "letters"
5850                    | "letters_uc" | "letters_lc" | "punctuation" | "numbers" | "graphemes"
5851                    | "columns" | "sentences" | "paragraphs" | "sections" | "trim" | "avg"
5852                    | "to_json" | "to_csv" | "to_toml" | "to_yaml" | "to_xml" | "to_html"
5853                    | "from_json" | "from_csv" | "from_toml" | "from_yaml" | "from_xml"
5854                    | "to_markdown" | "to_table" | "xopen" | "clip" | "sparkline" | "bar_chart"
5855                    | "flame" | "stddev" | "squared" | "sq" | "square" | "cubed" | "cb"
5856                    | "cube" | "normalize" | "snake_case" | "camel_case" | "kebab_case" => {
5857                        if args.is_empty() {
5858                            args.push(lhs);
5859                        } else {
5860                            args[0] = lhs;
5861                        }
5862                    }
5863                    "chunked" | "windowed" => {
5864                        if args.is_empty() {
5865                            return Err(self.syntax_err(
5866                                "|>: chunked(N) / windowed(N) needs size — e.g. `@a |> windowed(2)`",
5867                                line,
5868                            ));
5869                        }
5870                        args.insert(0, lhs);
5871                    }
5872                    "reduce" | "fold" => {
5873                        args.push(lhs);
5874                    }
5875                    "grep_v" | "pluck" | "tee" | "nth" | "chunk" => {
5876                        // data |> grep_v "pattern" → grep_v("pattern", data...)
5877                        // data |> pluck "key" → pluck("key", data...)
5878                        // data |> tee "file" → tee("file", data...)
5879                        // data |> nth N → nth(N, data...)
5880                        // data |> chunk N → chunk(N, data...)
5881                        args.push(lhs);
5882                    }
5883                    "enumerate" | "dedup" => {
5884                        // data |> enumerate → enumerate(data)
5885                        // data |> dedup → dedup(data)
5886                        args.insert(0, lhs);
5887                    }
5888                    "clamp" => {
5889                        // data |> clamp MIN, MAX → clamp(MIN, MAX, data...)
5890                        args.push(lhs);
5891                    }
5892                    n if Self::is_block_then_list_pipe_builtin(n) => {
5893                        if args.len() < 2 {
5894                            return Err(self.syntax_err(
5895                                format!(
5896                                    "|>: `{name}` needs {{ BLOCK }}, LIST so the list can receive the pipe"
5897                                ),
5898                                line,
5899                            ));
5900                        }
5901                        args[1] = lhs;
5902                    }
5903                    "take" | "head" | "tail" | "drop" => {
5904                        if args.is_empty() {
5905                            return Err(self.syntax_err(
5906                                "|>: `{name}` needs N last — e.g. `@a |> take(3)` for `take(@a, 3)`",
5907                                line,
5908                            ));
5909                        }
5910                        // `LIST |> take N` → `take(LIST, N)` (prepend piped list before trailing count)
5911                        args.insert(0, lhs);
5912                    }
5913                    _ => {
5914                        if self.thread_last_mode {
5915                            args.push(lhs);
5916                        } else {
5917                            args.insert(0, lhs);
5918                        }
5919                    }
5920                }
5921                ExprKind::FuncCall { name, args }
5922            }
5923            ExprKind::MethodCall {
5924                object,
5925                method,
5926                mut args,
5927                super_call,
5928            } => {
5929                if self.thread_last_mode {
5930                    args.push(lhs);
5931                } else {
5932                    args.insert(0, lhs);
5933                }
5934                ExprKind::MethodCall {
5935                    object,
5936                    method,
5937                    args,
5938                    super_call,
5939                }
5940            }
5941            ExprKind::IndirectCall {
5942                target,
5943                mut args,
5944                ampersand,
5945                pass_caller_arglist: _,
5946            } => {
5947                if self.thread_last_mode {
5948                    args.push(lhs);
5949                } else {
5950                    args.insert(0, lhs);
5951                }
5952                ExprKind::IndirectCall {
5953                    target,
5954                    args,
5955                    ampersand,
5956                    // Prepending an explicit first arg means this is no longer
5957                    // "pass the caller's @_" — that form is only bare `&$cr`.
5958                    pass_caller_arglist: false,
5959                }
5960            }
5961
5962            // ── Print-like / diagnostic ops (variadic) ─────────────────────────
5963            ExprKind::Print { handle, mut args } => {
5964                if self.thread_last_mode {
5965                    args.push(lhs);
5966                } else {
5967                    args.insert(0, lhs);
5968                }
5969                ExprKind::Print { handle, args }
5970            }
5971            ExprKind::Say { handle, mut args } => {
5972                if self.thread_last_mode {
5973                    args.push(lhs);
5974                } else {
5975                    args.insert(0, lhs);
5976                }
5977                ExprKind::Say { handle, args }
5978            }
5979            ExprKind::Printf { handle, mut args } => {
5980                if self.thread_last_mode {
5981                    args.push(lhs);
5982                } else {
5983                    args.insert(0, lhs);
5984                }
5985                ExprKind::Printf { handle, args }
5986            }
5987            ExprKind::Die(mut args) => {
5988                if self.thread_last_mode {
5989                    args.push(lhs);
5990                } else {
5991                    args.insert(0, lhs);
5992                }
5993                ExprKind::Die(args)
5994            }
5995            ExprKind::Warn(mut args) => {
5996                if self.thread_last_mode {
5997                    args.push(lhs);
5998                } else {
5999                    args.insert(0, lhs);
6000                }
6001                ExprKind::Warn(args)
6002            }
6003
6004            // ── Sprintf: first-arg pipe threads lhs into the `format` slot ─────
6005            //   `"n=%d" |> sprintf(42)` → `sprintf("n=%d", 42)` is awkward,
6006            //   but piping the format string is the rarer case. Prepending
6007            //   to the values list gives `sprintf(format, lhs, ...args)` for
6008            //   the common `$n |> sprintf "count=%d"` case.
6009            ExprKind::Sprintf { format, mut args } => {
6010                if self.thread_last_mode {
6011                    args.push(lhs);
6012                } else {
6013                    args.insert(0, lhs);
6014                }
6015                ExprKind::Sprintf { format, args }
6016            }
6017
6018            // ── System / exec / globbing / filesystem variadics ────────────────
6019            ExprKind::System(mut args) => {
6020                if self.thread_last_mode {
6021                    args.push(lhs);
6022                } else {
6023                    args.insert(0, lhs);
6024                }
6025                ExprKind::System(args)
6026            }
6027            ExprKind::Exec(mut args) => {
6028                if self.thread_last_mode {
6029                    args.push(lhs);
6030                } else {
6031                    args.insert(0, lhs);
6032                }
6033                ExprKind::Exec(args)
6034            }
6035            ExprKind::Unlink(mut args) => {
6036                if self.thread_last_mode {
6037                    args.push(lhs);
6038                } else {
6039                    args.insert(0, lhs);
6040                }
6041                ExprKind::Unlink(args)
6042            }
6043            ExprKind::Chmod(mut args) => {
6044                if self.thread_last_mode {
6045                    args.push(lhs);
6046                } else {
6047                    args.insert(0, lhs);
6048                }
6049                ExprKind::Chmod(args)
6050            }
6051            ExprKind::Chown(mut args) => {
6052                if self.thread_last_mode {
6053                    args.push(lhs);
6054                } else {
6055                    args.insert(0, lhs);
6056                }
6057                ExprKind::Chown(args)
6058            }
6059            ExprKind::Glob(mut args) => {
6060                if self.thread_last_mode {
6061                    args.push(lhs);
6062                } else {
6063                    args.insert(0, lhs);
6064                }
6065                ExprKind::Glob(args)
6066            }
6067            ExprKind::Files(mut args) => {
6068                if self.thread_last_mode {
6069                    args.push(lhs);
6070                } else {
6071                    args.insert(0, lhs);
6072                }
6073                ExprKind::Files(args)
6074            }
6075            ExprKind::Filesf(mut args) => {
6076                if self.thread_last_mode {
6077                    args.push(lhs);
6078                } else {
6079                    args.insert(0, lhs);
6080                }
6081                ExprKind::Filesf(args)
6082            }
6083            ExprKind::FilesfRecursive(mut args) => {
6084                if self.thread_last_mode {
6085                    args.push(lhs);
6086                } else {
6087                    args.insert(0, lhs);
6088                }
6089                ExprKind::FilesfRecursive(args)
6090            }
6091            ExprKind::Dirs(mut args) => {
6092                if self.thread_last_mode {
6093                    args.push(lhs);
6094                } else {
6095                    args.insert(0, lhs);
6096                }
6097                ExprKind::Dirs(args)
6098            }
6099            ExprKind::DirsRecursive(mut args) => {
6100                if self.thread_last_mode {
6101                    args.push(lhs);
6102                } else {
6103                    args.insert(0, lhs);
6104                }
6105                ExprKind::DirsRecursive(args)
6106            }
6107            ExprKind::SymLinks(mut args) => {
6108                if self.thread_last_mode {
6109                    args.push(lhs);
6110                } else {
6111                    args.insert(0, lhs);
6112                }
6113                ExprKind::SymLinks(args)
6114            }
6115            ExprKind::Sockets(mut args) => {
6116                if self.thread_last_mode {
6117                    args.push(lhs);
6118                } else {
6119                    args.insert(0, lhs);
6120                }
6121                ExprKind::Sockets(args)
6122            }
6123            ExprKind::Pipes(mut args) => {
6124                if self.thread_last_mode {
6125                    args.push(lhs);
6126                } else {
6127                    args.insert(0, lhs);
6128                }
6129                ExprKind::Pipes(args)
6130            }
6131            ExprKind::BlockDevices(mut args) => {
6132                if self.thread_last_mode {
6133                    args.push(lhs);
6134                } else {
6135                    args.insert(0, lhs);
6136                }
6137                ExprKind::BlockDevices(args)
6138            }
6139            ExprKind::CharDevices(mut args) => {
6140                if self.thread_last_mode {
6141                    args.push(lhs);
6142                } else {
6143                    args.insert(0, lhs);
6144                }
6145                ExprKind::CharDevices(args)
6146            }
6147            ExprKind::GlobPar { mut args, progress } => {
6148                if self.thread_last_mode {
6149                    args.push(lhs);
6150                } else {
6151                    args.insert(0, lhs);
6152                }
6153                ExprKind::GlobPar { args, progress }
6154            }
6155            ExprKind::ParSed { mut args, progress } => {
6156                if self.thread_last_mode {
6157                    args.push(lhs);
6158                } else {
6159                    args.insert(0, lhs);
6160                }
6161                ExprKind::ParSed { args, progress }
6162            }
6163
6164            // ── Unary-style builtins: replace the lone operand with `lhs` ──────
6165            ExprKind::Length(_) => ExprKind::Length(Box::new(lhs)),
6166            ExprKind::Abs(_) => ExprKind::Abs(Box::new(lhs)),
6167            ExprKind::Int(_) => ExprKind::Int(Box::new(lhs)),
6168            ExprKind::Sqrt(_) => ExprKind::Sqrt(Box::new(lhs)),
6169            ExprKind::Sin(_) => ExprKind::Sin(Box::new(lhs)),
6170            ExprKind::Cos(_) => ExprKind::Cos(Box::new(lhs)),
6171            ExprKind::Exp(_) => ExprKind::Exp(Box::new(lhs)),
6172            ExprKind::Log(_) => ExprKind::Log(Box::new(lhs)),
6173            ExprKind::Hex(_) => ExprKind::Hex(Box::new(lhs)),
6174            ExprKind::Oct(_) => ExprKind::Oct(Box::new(lhs)),
6175            ExprKind::Lc(_) => ExprKind::Lc(Box::new(lhs)),
6176            ExprKind::Uc(_) => ExprKind::Uc(Box::new(lhs)),
6177            ExprKind::Lcfirst(_) => ExprKind::Lcfirst(Box::new(lhs)),
6178            ExprKind::Ucfirst(_) => ExprKind::Ucfirst(Box::new(lhs)),
6179            ExprKind::Fc(_) => ExprKind::Fc(Box::new(lhs)),
6180            ExprKind::Chr(_) => ExprKind::Chr(Box::new(lhs)),
6181            ExprKind::Ord(_) => ExprKind::Ord(Box::new(lhs)),
6182            ExprKind::Chomp(_) => ExprKind::Chomp(Box::new(lhs)),
6183            ExprKind::Chop(_) => ExprKind::Chop(Box::new(lhs)),
6184            ExprKind::Defined(_) => ExprKind::Defined(Box::new(lhs)),
6185            ExprKind::Ref(_) => ExprKind::Ref(Box::new(lhs)),
6186            ExprKind::ScalarContext(_) => ExprKind::ScalarContext(Box::new(lhs)),
6187            ExprKind::Keys(_) => ExprKind::Keys(Box::new(lhs)),
6188            ExprKind::Values(_) => ExprKind::Values(Box::new(lhs)),
6189            ExprKind::Each(_) => ExprKind::Each(Box::new(lhs)),
6190            ExprKind::Pop(_) => ExprKind::Pop(Box::new(lhs)),
6191            ExprKind::Shift(_) => ExprKind::Shift(Box::new(lhs)),
6192            ExprKind::Delete(_) => ExprKind::Delete(Box::new(lhs)),
6193            ExprKind::Exists(_) => ExprKind::Exists(Box::new(lhs)),
6194            ExprKind::ReverseExpr(_) => ExprKind::ReverseExpr(Box::new(lhs)),
6195            ExprKind::Rev(_) => ExprKind::Rev(Box::new(lhs)),
6196            ExprKind::Slurp(_) => ExprKind::Slurp(Box::new(lhs)),
6197            ExprKind::Capture(_) => ExprKind::Capture(Box::new(lhs)),
6198            ExprKind::Qx(_) => ExprKind::Qx(Box::new(lhs)),
6199            ExprKind::FetchUrl(_) => ExprKind::FetchUrl(Box::new(lhs)),
6200            ExprKind::Close(_) => ExprKind::Close(Box::new(lhs)),
6201            ExprKind::Chdir(_) => ExprKind::Chdir(Box::new(lhs)),
6202            ExprKind::Readdir(_) => ExprKind::Readdir(Box::new(lhs)),
6203            ExprKind::Closedir(_) => ExprKind::Closedir(Box::new(lhs)),
6204            ExprKind::Rewinddir(_) => ExprKind::Rewinddir(Box::new(lhs)),
6205            ExprKind::Telldir(_) => ExprKind::Telldir(Box::new(lhs)),
6206            ExprKind::Stat(_) => ExprKind::Stat(Box::new(lhs)),
6207            ExprKind::Lstat(_) => ExprKind::Lstat(Box::new(lhs)),
6208            ExprKind::Readlink(_) => ExprKind::Readlink(Box::new(lhs)),
6209            ExprKind::Study(_) => ExprKind::Study(Box::new(lhs)),
6210            ExprKind::Await(_) => ExprKind::Await(Box::new(lhs)),
6211            ExprKind::Eval(_) => ExprKind::Eval(Box::new(lhs)),
6212            ExprKind::Rand(_) => ExprKind::Rand(Some(Box::new(lhs))),
6213            ExprKind::Srand(_) => ExprKind::Srand(Some(Box::new(lhs))),
6214            ExprKind::Pos(_) => ExprKind::Pos(Some(Box::new(lhs))),
6215            ExprKind::Exit(_) => ExprKind::Exit(Some(Box::new(lhs))),
6216
6217            // ── Higher-order / list-taking forms: replace the `list` slot ──────
6218            ExprKind::MapExpr {
6219                block,
6220                list: _,
6221                flatten_array_refs,
6222                stream,
6223            } => ExprKind::MapExpr {
6224                block,
6225                list: Box::new(lhs),
6226                flatten_array_refs,
6227                stream,
6228            },
6229            ExprKind::MapExprComma {
6230                expr,
6231                list: _,
6232                flatten_array_refs,
6233                stream,
6234            } => ExprKind::MapExprComma {
6235                expr,
6236                list: Box::new(lhs),
6237                flatten_array_refs,
6238                stream,
6239            },
6240            ExprKind::GrepExpr {
6241                block,
6242                list: _,
6243                keyword,
6244            } => ExprKind::GrepExpr {
6245                block,
6246                list: Box::new(lhs),
6247                keyword,
6248            },
6249            ExprKind::GrepExprComma {
6250                expr,
6251                list: _,
6252                keyword,
6253            } => ExprKind::GrepExprComma {
6254                expr,
6255                list: Box::new(lhs),
6256                keyword,
6257            },
6258            ExprKind::ForEachExpr { block, list: _ } => ExprKind::ForEachExpr {
6259                block,
6260                list: Box::new(lhs),
6261            },
6262            ExprKind::SortExpr { cmp, list: _ } => ExprKind::SortExpr {
6263                cmp,
6264                list: Box::new(lhs),
6265            },
6266            ExprKind::JoinExpr { separator, list: _ } => ExprKind::JoinExpr {
6267                separator,
6268                list: Box::new(lhs),
6269            },
6270            ExprKind::ReduceExpr { block, list: _ } => ExprKind::ReduceExpr {
6271                block,
6272                list: Box::new(lhs),
6273            },
6274            ExprKind::PMapExpr {
6275                block,
6276                list: _,
6277                progress,
6278                flat_outputs,
6279                on_cluster,
6280                stream,
6281            } => ExprKind::PMapExpr {
6282                block,
6283                list: Box::new(lhs),
6284                progress,
6285                flat_outputs,
6286                on_cluster,
6287                stream,
6288            },
6289            ExprKind::PMapChunkedExpr {
6290                chunk_size,
6291                block,
6292                list: _,
6293                progress,
6294            } => ExprKind::PMapChunkedExpr {
6295                chunk_size,
6296                block,
6297                list: Box::new(lhs),
6298                progress,
6299            },
6300            ExprKind::PGrepExpr {
6301                block,
6302                list: _,
6303                progress,
6304                stream,
6305            } => ExprKind::PGrepExpr {
6306                block,
6307                list: Box::new(lhs),
6308                progress,
6309                stream,
6310            },
6311            ExprKind::PForExpr {
6312                block,
6313                list: _,
6314                progress,
6315            } => ExprKind::PForExpr {
6316                block,
6317                list: Box::new(lhs),
6318                progress,
6319            },
6320            ExprKind::PSortExpr {
6321                cmp,
6322                list: _,
6323                progress,
6324            } => ExprKind::PSortExpr {
6325                cmp,
6326                list: Box::new(lhs),
6327                progress,
6328            },
6329            ExprKind::PReduceExpr {
6330                block,
6331                list: _,
6332                progress,
6333            } => ExprKind::PReduceExpr {
6334                block,
6335                list: Box::new(lhs),
6336                progress,
6337            },
6338            ExprKind::PcacheExpr {
6339                block,
6340                list: _,
6341                progress,
6342            } => ExprKind::PcacheExpr {
6343                block,
6344                list: Box::new(lhs),
6345                progress,
6346            },
6347            ExprKind::PReduceInitExpr {
6348                init,
6349                block,
6350                list: _,
6351                progress,
6352            } => ExprKind::PReduceInitExpr {
6353                init,
6354                block,
6355                list: Box::new(lhs),
6356                progress,
6357            },
6358            ExprKind::PMapReduceExpr {
6359                map_block,
6360                reduce_block,
6361                list: _,
6362                progress,
6363            } => ExprKind::PMapReduceExpr {
6364                map_block,
6365                reduce_block,
6366                list: Box::new(lhs),
6367                progress,
6368            },
6369
6370            // ── Push / unshift: first arg is the array, so pipe the LHS
6371            //     into the **values** list — `"x" |> push(@arr)` → `push @arr, "x"`
6372            //     is unchanged, but `@arr |> push "x"` is unnatural; use push
6373            //     directly for that.
6374            ExprKind::Push { array, mut values } => {
6375                values.insert(0, lhs);
6376                ExprKind::Push { array, values }
6377            }
6378            ExprKind::Unshift { array, mut values } => {
6379                values.insert(0, lhs);
6380                ExprKind::Unshift { array, values }
6381            }
6382
6383            // ── Split: pipe the subject string — `$line |> split /,/` ─────────
6384            ExprKind::SplitExpr {
6385                pattern,
6386                string: _,
6387                limit,
6388            } => ExprKind::SplitExpr {
6389                pattern,
6390                string: Box::new(lhs),
6391                limit,
6392            },
6393
6394            // ── Regex ops: pipe the subject — `$str |> s/\n//g` ────────────────
6395            //    Auto-inject `r` flag so the substitution returns the modified
6396            //    string instead of the match count (non-destructive / Perl /r).
6397            ExprKind::Substitution {
6398                pattern,
6399                replacement,
6400                mut flags,
6401                expr: _,
6402                delim,
6403            } => {
6404                if !flags.contains('r') {
6405                    flags.push('r');
6406                }
6407                ExprKind::Substitution {
6408                    expr: Box::new(lhs),
6409                    pattern,
6410                    replacement,
6411                    flags,
6412                    delim,
6413                }
6414            }
6415            ExprKind::Transliterate {
6416                from,
6417                to,
6418                mut flags,
6419                expr: _,
6420                delim,
6421            } => {
6422                if !flags.contains('r') {
6423                    flags.push('r');
6424                }
6425                ExprKind::Transliterate {
6426                    expr: Box::new(lhs),
6427                    from,
6428                    to,
6429                    flags,
6430                    delim,
6431                }
6432            }
6433            ExprKind::Match {
6434                pattern,
6435                flags,
6436                scalar_g,
6437                expr: _,
6438                delim,
6439            } => ExprKind::Match {
6440                expr: Box::new(lhs),
6441                pattern,
6442                flags,
6443                scalar_g,
6444                delim,
6445            },
6446            // Bare `/regex/` (no explicit `m`): promote to Match on piped LHS
6447            ExprKind::Regex(pattern, flags) => ExprKind::Match {
6448                expr: Box::new(lhs),
6449                pattern,
6450                flags,
6451                scalar_g: false,
6452                delim: '/',
6453            },
6454
6455            // ── Bareword function name → plain unary call ──────────────────────
6456            ExprKind::Bareword(name) => match name.as_str() {
6457                "reverse" => {
6458                    if crate::no_interop_mode() {
6459                        return Err(self.syntax_err(
6460                            "stryke uses `rev` instead of `reverse` (--no-interop)",
6461                            line,
6462                        ));
6463                    }
6464                    ExprKind::ReverseExpr(Box::new(lhs))
6465                }
6466                "rv" | "reversed" | "rev" => ExprKind::Rev(Box::new(lhs)),
6467                "uq" | "uniq" | "distinct" => ExprKind::FuncCall {
6468                    name: "uniq".to_string(),
6469                    args: vec![lhs],
6470                },
6471                "fl" | "flatten" => ExprKind::FuncCall {
6472                    name: "flatten".to_string(),
6473                    args: vec![lhs],
6474                },
6475                _ => ExprKind::FuncCall {
6476                    name,
6477                    args: vec![lhs],
6478                },
6479            },
6480
6481            // ── Callable scalars / coderefs / derefs → IndirectCall ────────────
6482            kind @ (ExprKind::ScalarVar(_)
6483            | ExprKind::ArrayElement { .. }
6484            | ExprKind::HashElement { .. }
6485            | ExprKind::Deref { .. }
6486            | ExprKind::ArrowDeref { .. }
6487            | ExprKind::CodeRef { .. }
6488            | ExprKind::SubroutineRef(_)
6489            | ExprKind::SubroutineCodeRef(_)
6490            | ExprKind::DynamicSubCodeRef(_)) => ExprKind::IndirectCall {
6491                target: Box::new(Expr { kind, line: rline }),
6492                args: vec![lhs],
6493                ampersand: false,
6494                pass_caller_arglist: false,
6495            },
6496
6497            // `LHS |> >{ BLOCK }` — the `>{}` form is parsed everywhere as `Do(CodeRef)` (IIFE).
6498            // On the RHS of `|>` we want pipe-apply semantics instead: unwrap the Do and invoke
6499            // the inner coderef with `lhs` as `$_[0]`, matching `LHS |> fn { ... }`.
6500            ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. }) => {
6501                ExprKind::IndirectCall {
6502                    target: inner,
6503                    args: vec![lhs],
6504                    ampersand: false,
6505                    pass_caller_arglist: false,
6506                }
6507            }
6508
6509            other => {
6510                return Err(self.syntax_err(
6511                    format!(
6512                        "right-hand side of `|>` must be a call, builtin, or coderef \
6513                         expression (got {})",
6514                        Self::expr_kind_name(&other)
6515                    ),
6516                    line,
6517                ));
6518            }
6519        };
6520        Ok(Expr {
6521            kind: new_kind,
6522            line,
6523        })
6524    }
6525
6526    /// Short label for an `ExprKind` (used in `|>` error messages).
6527    fn expr_kind_name(kind: &ExprKind) -> &'static str {
6528        match kind {
6529            ExprKind::Integer(_) | ExprKind::Float(_) => "numeric literal",
6530            ExprKind::String(_) | ExprKind::InterpolatedString(_) => "string literal",
6531            ExprKind::BinOp { .. } => "binary expression",
6532            ExprKind::UnaryOp { .. } => "unary expression",
6533            ExprKind::Ternary { .. } => "ternary expression",
6534            ExprKind::Assign { .. } | ExprKind::CompoundAssign { .. } => "assignment",
6535            ExprKind::List(_) => "list expression",
6536            ExprKind::Range { .. } => "range expression",
6537            _ => "expression",
6538        }
6539    }
6540
6541    // or / not (lowest precedence word operators)
6542    fn parse_or_word(&mut self) -> PerlResult<Expr> {
6543        let mut left = self.parse_and_word()?;
6544        while matches!(self.peek(), Token::LogOrWord) {
6545            let line = left.line;
6546            self.advance();
6547            let right = self.parse_and_word()?;
6548            left = Expr {
6549                kind: ExprKind::BinOp {
6550                    left: Box::new(left),
6551                    op: BinOp::LogOrWord,
6552                    right: Box::new(right),
6553                },
6554                line,
6555            };
6556        }
6557        Ok(left)
6558    }
6559
6560    fn parse_and_word(&mut self) -> PerlResult<Expr> {
6561        let mut left = self.parse_not_word()?;
6562        while matches!(self.peek(), Token::LogAndWord) {
6563            let line = left.line;
6564            self.advance();
6565            let right = self.parse_not_word()?;
6566            left = Expr {
6567                kind: ExprKind::BinOp {
6568                    left: Box::new(left),
6569                    op: BinOp::LogAndWord,
6570                    right: Box::new(right),
6571                },
6572                line,
6573            };
6574        }
6575        Ok(left)
6576    }
6577
6578    fn parse_not_word(&mut self) -> PerlResult<Expr> {
6579        if matches!(self.peek(), Token::LogNotWord) {
6580            let line = self.peek_line();
6581            self.advance();
6582            let expr = self.parse_not_word()?;
6583            return Ok(Expr {
6584                kind: ExprKind::UnaryOp {
6585                    op: UnaryOp::LogNotWord,
6586                    expr: Box::new(expr),
6587                },
6588                line,
6589            });
6590        }
6591        self.parse_range()
6592    }
6593
6594    fn parse_log_or(&mut self) -> PerlResult<Expr> {
6595        let mut left = self.parse_log_and()?;
6596        loop {
6597            let op = match self.peek() {
6598                Token::LogOr => BinOp::LogOr,
6599                Token::DefinedOr => BinOp::DefinedOr,
6600                _ => break,
6601            };
6602            let line = left.line;
6603            self.advance();
6604            let right = self.parse_log_and()?;
6605            left = Expr {
6606                kind: ExprKind::BinOp {
6607                    left: Box::new(left),
6608                    op,
6609                    right: Box::new(right),
6610                },
6611                line,
6612            };
6613        }
6614        Ok(left)
6615    }
6616
6617    fn parse_log_and(&mut self) -> PerlResult<Expr> {
6618        let mut left = self.parse_bit_or()?;
6619        while matches!(self.peek(), Token::LogAnd) {
6620            let line = left.line;
6621            self.advance();
6622            let right = self.parse_bit_or()?;
6623            left = Expr {
6624                kind: ExprKind::BinOp {
6625                    left: Box::new(left),
6626                    op: BinOp::LogAnd,
6627                    right: Box::new(right),
6628                },
6629                line,
6630            };
6631        }
6632        Ok(left)
6633    }
6634
6635    fn parse_bit_or(&mut self) -> PerlResult<Expr> {
6636        let mut left = self.parse_bit_xor()?;
6637        while matches!(self.peek(), Token::BitOr) {
6638            let line = left.line;
6639            self.advance();
6640            let right = self.parse_bit_xor()?;
6641            left = Expr {
6642                kind: ExprKind::BinOp {
6643                    left: Box::new(left),
6644                    op: BinOp::BitOr,
6645                    right: Box::new(right),
6646                },
6647                line,
6648            };
6649        }
6650        Ok(left)
6651    }
6652
6653    fn parse_bit_xor(&mut self) -> PerlResult<Expr> {
6654        let mut left = self.parse_bit_and()?;
6655        while matches!(self.peek(), Token::BitXor) {
6656            let line = left.line;
6657            self.advance();
6658            let right = self.parse_bit_and()?;
6659            left = Expr {
6660                kind: ExprKind::BinOp {
6661                    left: Box::new(left),
6662                    op: BinOp::BitXor,
6663                    right: Box::new(right),
6664                },
6665                line,
6666            };
6667        }
6668        Ok(left)
6669    }
6670
6671    fn parse_bit_and(&mut self) -> PerlResult<Expr> {
6672        let mut left = self.parse_equality()?;
6673        while matches!(self.peek(), Token::BitAnd) {
6674            let line = left.line;
6675            self.advance();
6676            let right = self.parse_equality()?;
6677            left = Expr {
6678                kind: ExprKind::BinOp {
6679                    left: Box::new(left),
6680                    op: BinOp::BitAnd,
6681                    right: Box::new(right),
6682                },
6683                line,
6684            };
6685        }
6686        Ok(left)
6687    }
6688
6689    fn parse_equality(&mut self) -> PerlResult<Expr> {
6690        let mut left = self.parse_comparison()?;
6691        loop {
6692            let op = match self.peek() {
6693                Token::NumEq => BinOp::NumEq,
6694                Token::NumNe => BinOp::NumNe,
6695                Token::StrEq => BinOp::StrEq,
6696                Token::StrNe => BinOp::StrNe,
6697                Token::Spaceship => BinOp::Spaceship,
6698                Token::StrCmp => BinOp::StrCmp,
6699                _ => break,
6700            };
6701            let line = left.line;
6702            self.advance();
6703            let right = self.parse_comparison()?;
6704            left = Expr {
6705                kind: ExprKind::BinOp {
6706                    left: Box::new(left),
6707                    op,
6708                    right: Box::new(right),
6709                },
6710                line,
6711            };
6712        }
6713        Ok(left)
6714    }
6715
6716    fn parse_comparison(&mut self) -> PerlResult<Expr> {
6717        let left = self.parse_shift()?;
6718        let first_op = match self.peek() {
6719            Token::NumLt => BinOp::NumLt,
6720            Token::NumGt => BinOp::NumGt,
6721            Token::NumLe => BinOp::NumLe,
6722            Token::NumGe => BinOp::NumGe,
6723            Token::StrLt => BinOp::StrLt,
6724            Token::StrGt => BinOp::StrGt,
6725            Token::StrLe => BinOp::StrLe,
6726            Token::StrGe => BinOp::StrGe,
6727            _ => return Ok(left),
6728        };
6729        let line = left.line;
6730        self.advance();
6731        let middle = self.parse_shift()?;
6732
6733        let second_op = match self.peek() {
6734            Token::NumLt => Some(BinOp::NumLt),
6735            Token::NumGt => Some(BinOp::NumGt),
6736            Token::NumLe => Some(BinOp::NumLe),
6737            Token::NumGe => Some(BinOp::NumGe),
6738            Token::StrLt => Some(BinOp::StrLt),
6739            Token::StrGt => Some(BinOp::StrGt),
6740            Token::StrLe => Some(BinOp::StrLe),
6741            Token::StrGe => Some(BinOp::StrGe),
6742            _ => None,
6743        };
6744
6745        if second_op.is_none() {
6746            return Ok(Expr {
6747                kind: ExprKind::BinOp {
6748                    left: Box::new(left),
6749                    op: first_op,
6750                    right: Box::new(middle),
6751                },
6752                line,
6753            });
6754        }
6755
6756        // Chained comparison: `a < b < c` → `(a < b) && (b < c)`
6757        // Collect all operands and operators for chains like `1 < x < 10 < y`
6758        let mut operands = vec![left, middle];
6759        let mut ops = vec![first_op];
6760
6761        loop {
6762            let op = match self.peek() {
6763                Token::NumLt => BinOp::NumLt,
6764                Token::NumGt => BinOp::NumGt,
6765                Token::NumLe => BinOp::NumLe,
6766                Token::NumGe => BinOp::NumGe,
6767                Token::StrLt => BinOp::StrLt,
6768                Token::StrGt => BinOp::StrGt,
6769                Token::StrLe => BinOp::StrLe,
6770                Token::StrGe => BinOp::StrGe,
6771                _ => break,
6772            };
6773            self.advance();
6774            ops.push(op);
6775            operands.push(self.parse_shift()?);
6776        }
6777
6778        // Build `(a op0 b) && (b op1 c) && (c op2 d) && ...`
6779        let mut result = Expr {
6780            kind: ExprKind::BinOp {
6781                left: Box::new(operands[0].clone()),
6782                op: ops[0],
6783                right: Box::new(operands[1].clone()),
6784            },
6785            line,
6786        };
6787
6788        for i in 1..ops.len() {
6789            let cmp = Expr {
6790                kind: ExprKind::BinOp {
6791                    left: Box::new(operands[i].clone()),
6792                    op: ops[i],
6793                    right: Box::new(operands[i + 1].clone()),
6794                },
6795                line,
6796            };
6797            result = Expr {
6798                kind: ExprKind::BinOp {
6799                    left: Box::new(result),
6800                    op: BinOp::LogAnd,
6801                    right: Box::new(cmp),
6802                },
6803                line,
6804            };
6805        }
6806
6807        Ok(result)
6808    }
6809
6810    fn parse_shift(&mut self) -> PerlResult<Expr> {
6811        let mut left = self.parse_addition()?;
6812        loop {
6813            let op = match self.peek() {
6814                Token::ShiftLeft => BinOp::ShiftLeft,
6815                Token::ShiftRight => BinOp::ShiftRight,
6816                _ => break,
6817            };
6818            let line = left.line;
6819            self.advance();
6820            let right = self.parse_addition()?;
6821            left = Expr {
6822                kind: ExprKind::BinOp {
6823                    left: Box::new(left),
6824                    op,
6825                    right: Box::new(right),
6826                },
6827                line,
6828            };
6829        }
6830        Ok(left)
6831    }
6832
6833    fn parse_addition(&mut self) -> PerlResult<Expr> {
6834        let mut left = self.parse_multiplication()?;
6835        loop {
6836            // Implicit semicolon: `-` or `+` on a new line is a unary operator on
6837            // the next statement, not a binary operator continuing this expression.
6838            let op = match self.peek() {
6839                Token::Plus if self.peek_line() == self.prev_line() => BinOp::Add,
6840                Token::Minus if self.peek_line() == self.prev_line() => BinOp::Sub,
6841                Token::Dot => BinOp::Concat,
6842                _ => break,
6843            };
6844            let line = left.line;
6845            self.advance();
6846            let right = self.parse_multiplication()?;
6847            left = Expr {
6848                kind: ExprKind::BinOp {
6849                    left: Box::new(left),
6850                    op,
6851                    right: Box::new(right),
6852                },
6853                line,
6854            };
6855        }
6856        Ok(left)
6857    }
6858
6859    fn parse_multiplication(&mut self) -> PerlResult<Expr> {
6860        let mut left = self.parse_regex_bind()?;
6861        loop {
6862            let op = match self.peek() {
6863                Token::Star => BinOp::Mul,
6864                Token::Slash if self.suppress_slash_as_div == 0 => BinOp::Div,
6865                // Implicit semicolon: `%` on a new line is a hash dereference or hash
6866                // sigil for the next statement, not modulo operator on this expression.
6867                Token::Percent if self.peek_line() == self.prev_line() => BinOp::Mod,
6868                Token::X => {
6869                    let line = left.line;
6870                    self.advance();
6871                    let right = self.parse_regex_bind()?;
6872                    left = Expr {
6873                        kind: ExprKind::Repeat {
6874                            expr: Box::new(left),
6875                            count: Box::new(right),
6876                        },
6877                        line,
6878                    };
6879                    continue;
6880                }
6881                _ => break,
6882            };
6883            let line = left.line;
6884            self.advance();
6885            let right = self.parse_regex_bind()?;
6886            left = Expr {
6887                kind: ExprKind::BinOp {
6888                    left: Box::new(left),
6889                    op,
6890                    right: Box::new(right),
6891                },
6892                line,
6893            };
6894        }
6895        Ok(left)
6896    }
6897
6898    fn parse_regex_bind(&mut self) -> PerlResult<Expr> {
6899        let left = self.parse_unary()?;
6900        match self.peek() {
6901            Token::BindMatch => {
6902                let line = left.line;
6903                self.advance();
6904                match self.peek().clone() {
6905                    Token::Regex(pattern, flags, delim) => {
6906                        self.advance();
6907                        Ok(Expr {
6908                            kind: ExprKind::Match {
6909                                expr: Box::new(left),
6910                                pattern,
6911                                flags,
6912                                scalar_g: false,
6913                                delim,
6914                            },
6915                            line,
6916                        })
6917                    }
6918                    Token::Ident(ref s) if s.starts_with('\x00') => {
6919                        let (Token::Ident(encoded), _) = self.advance() else {
6920                            unreachable!()
6921                        };
6922                        let parts: Vec<&str> = encoded.split('\x00').collect();
6923                        if parts.len() >= 4 && parts[1] == "s" {
6924                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
6925                            Ok(Expr {
6926                                kind: ExprKind::Substitution {
6927                                    expr: Box::new(left),
6928                                    pattern: parts[2].to_string(),
6929                                    replacement: parts[3].to_string(),
6930                                    flags: parts.get(4).unwrap_or(&"").to_string(),
6931                                    delim,
6932                                },
6933                                line,
6934                            })
6935                        } else if parts.len() >= 4 && parts[1] == "tr" {
6936                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
6937                            Ok(Expr {
6938                                kind: ExprKind::Transliterate {
6939                                    expr: Box::new(left),
6940                                    from: parts[2].to_string(),
6941                                    to: parts[3].to_string(),
6942                                    flags: parts.get(4).unwrap_or(&"").to_string(),
6943                                    delim,
6944                                },
6945                                line,
6946                            })
6947                        } else {
6948                            Err(self.syntax_err("Invalid regex binding", line))
6949                        }
6950                    }
6951                    _ => {
6952                        let rhs = self.parse_unary()?;
6953                        Ok(Expr {
6954                            kind: ExprKind::BinOp {
6955                                left: Box::new(left),
6956                                op: BinOp::BindMatch,
6957                                right: Box::new(rhs),
6958                            },
6959                            line,
6960                        })
6961                    }
6962                }
6963            }
6964            Token::BindNotMatch => {
6965                let line = left.line;
6966                self.advance();
6967                match self.peek().clone() {
6968                    Token::Regex(pattern, flags, delim) => {
6969                        self.advance();
6970                        Ok(Expr {
6971                            kind: ExprKind::UnaryOp {
6972                                op: UnaryOp::LogNot,
6973                                expr: Box::new(Expr {
6974                                    kind: ExprKind::Match {
6975                                        expr: Box::new(left),
6976                                        pattern,
6977                                        flags,
6978                                        scalar_g: false,
6979                                        delim,
6980                                    },
6981                                    line,
6982                                }),
6983                            },
6984                            line,
6985                        })
6986                    }
6987                    Token::Ident(ref s) if s.starts_with('\x00') => {
6988                        let (Token::Ident(encoded), _) = self.advance() else {
6989                            unreachable!()
6990                        };
6991                        let parts: Vec<&str> = encoded.split('\x00').collect();
6992                        if parts.len() >= 4 && parts[1] == "s" {
6993                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
6994                            Ok(Expr {
6995                                kind: ExprKind::UnaryOp {
6996                                    op: UnaryOp::LogNot,
6997                                    expr: Box::new(Expr {
6998                                        kind: ExprKind::Substitution {
6999                                            expr: Box::new(left),
7000                                            pattern: parts[2].to_string(),
7001                                            replacement: parts[3].to_string(),
7002                                            flags: parts.get(4).unwrap_or(&"").to_string(),
7003                                            delim,
7004                                        },
7005                                        line,
7006                                    }),
7007                                },
7008                                line,
7009                            })
7010                        } else if parts.len() >= 4 && parts[1] == "tr" {
7011                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
7012                            Ok(Expr {
7013                                kind: ExprKind::UnaryOp {
7014                                    op: UnaryOp::LogNot,
7015                                    expr: Box::new(Expr {
7016                                        kind: ExprKind::Transliterate {
7017                                            expr: Box::new(left),
7018                                            from: parts[2].to_string(),
7019                                            to: parts[3].to_string(),
7020                                            flags: parts.get(4).unwrap_or(&"").to_string(),
7021                                            delim,
7022                                        },
7023                                        line,
7024                                    }),
7025                                },
7026                                line,
7027                            })
7028                        } else {
7029                            Err(self.syntax_err("Invalid regex binding after !~", line))
7030                        }
7031                    }
7032                    _ => {
7033                        let rhs = self.parse_unary()?;
7034                        Ok(Expr {
7035                            kind: ExprKind::BinOp {
7036                                left: Box::new(left),
7037                                op: BinOp::BindNotMatch,
7038                                right: Box::new(rhs),
7039                            },
7040                            line,
7041                        })
7042                    }
7043                }
7044            }
7045            _ => Ok(left),
7046        }
7047    }
7048
7049    /// Parse thread macro input. Like `parse_range` but suppresses `/` as division
7050    /// so that `/pattern/` is left for the thread stage parser to handle as regex filter.
7051    fn parse_thread_input(&mut self) -> PerlResult<Expr> {
7052        self.suppress_slash_as_div = self.suppress_slash_as_div.saturating_add(1);
7053        let result = self.parse_range();
7054        self.suppress_slash_as_div = self.suppress_slash_as_div.saturating_sub(1);
7055        result
7056    }
7057
7058    /// Perl `..` / `...` operator — precedence sits between `?:` and `||` (`perlop`), so
7059    /// `$x .. $x + 3` parses as `$x .. ($x + 3)` and `1..$n||5` parses as `1..($n||5)`. Both
7060    /// operands recurse through `parse_log_or`, which in turn walks down through all tighter
7061    /// operators (additive, multiplicative, regex bind, unary). Non-associative: the right
7062    /// operand is a single `parse_log_or` so `1..5..10` is a parse error in Perl, but we accept
7063    /// it greedily (left-associated) because the lexer already forbids `..` after a range RHS.
7064    fn parse_range(&mut self) -> PerlResult<Expr> {
7065        let left = self.parse_log_or()?;
7066        let line = left.line;
7067        // `1..10` or `1...10` (traditional) or `1:10` (short form)
7068        let (exclusive, _colon_style) = if self.eat(&Token::RangeExclusive) {
7069            (true, false)
7070        } else if self.eat(&Token::Range) {
7071            (false, false)
7072        } else if self.suppress_colon_range == 0 && self.eat(&Token::Colon) {
7073            // `1:10` short form — only valid for numeric ranges, not ternary
7074            // Lookahead: must be followed by something that looks like a range endpoint
7075            (false, true)
7076        } else {
7077            return Ok(left);
7078        };
7079        let right = self.parse_log_or()?;
7080        // Optional step: `1..100:2` or `1:100:2`
7081        let step = if self.eat(&Token::Colon) {
7082            Some(Box::new(self.parse_unary()?))
7083        } else {
7084            None
7085        };
7086        Ok(Expr {
7087            kind: ExprKind::Range {
7088                from: Box::new(left),
7089                to: Box::new(right),
7090                exclusive,
7091                step,
7092            },
7093            line,
7094        })
7095    }
7096
7097    /// `name` or `Foo::Bar::baz` — used after `sub`, unary `&`, etc.
7098    fn parse_package_qualified_identifier(&mut self) -> PerlResult<String> {
7099        let mut name = match self.advance() {
7100            (Token::Ident(n), _) => n,
7101            (tok, l) => {
7102                return Err(self.syntax_err(format!("Expected identifier, got {:?}", tok), l));
7103            }
7104        };
7105        while self.eat(&Token::PackageSep) {
7106            match self.advance() {
7107                (Token::Ident(part), _) => {
7108                    name.push_str("::");
7109                    name.push_str(&part);
7110                }
7111                (tok, l) => {
7112                    return Err(self
7113                        .syntax_err(format!("Expected identifier after `::`, got {:?}", tok), l));
7114                }
7115            }
7116        }
7117        Ok(name)
7118    }
7119
7120    /// After consuming unary `&`: `name` or `Foo::Bar::baz` (Perl `&foo` / `&Foo::bar`).
7121    fn parse_qualified_subroutine_name(&mut self) -> PerlResult<String> {
7122        self.parse_package_qualified_identifier()
7123    }
7124
7125    fn parse_unary(&mut self) -> PerlResult<Expr> {
7126        let line = self.peek_line();
7127        match self.peek().clone() {
7128            Token::Minus => {
7129                self.advance();
7130                let expr = self.parse_power()?;
7131                Ok(Expr {
7132                    kind: ExprKind::UnaryOp {
7133                        op: UnaryOp::Negate,
7134                        expr: Box::new(expr),
7135                    },
7136                    line,
7137                })
7138            }
7139            // Unary `+EXPR` — Perl uses this to disambiguate barewords in hash subscripts (`$h{+Foo}`)
7140            // and for scalar context; treat as a no-op on the parsed operand.
7141            Token::Plus => {
7142                self.advance();
7143                self.parse_unary()
7144            }
7145            Token::LogNot => {
7146                self.advance();
7147                let expr = self.parse_unary()?;
7148                Ok(Expr {
7149                    kind: ExprKind::UnaryOp {
7150                        op: UnaryOp::LogNot,
7151                        expr: Box::new(expr),
7152                    },
7153                    line,
7154                })
7155            }
7156            Token::BitNot => {
7157                self.advance();
7158                let expr = self.parse_unary()?;
7159                Ok(Expr {
7160                    kind: ExprKind::UnaryOp {
7161                        op: UnaryOp::BitNot,
7162                        expr: Box::new(expr),
7163                    },
7164                    line,
7165                })
7166            }
7167            Token::Increment => {
7168                self.advance();
7169                let expr = self.parse_postfix()?;
7170                Ok(Expr {
7171                    kind: ExprKind::UnaryOp {
7172                        op: UnaryOp::PreIncrement,
7173                        expr: Box::new(expr),
7174                    },
7175                    line,
7176                })
7177            }
7178            Token::Decrement => {
7179                self.advance();
7180                let expr = self.parse_postfix()?;
7181                Ok(Expr {
7182                    kind: ExprKind::UnaryOp {
7183                        op: UnaryOp::PreDecrement,
7184                        expr: Box::new(expr),
7185                    },
7186                    line,
7187                })
7188            }
7189            Token::BitAnd => {
7190                // Unary `&name` / `&Pkg::name` (call / coderef); binary `&` is in `parse_bit_and`.
7191                // `&$coderef(...)` — call sub whose ref is in a scalar (core `B.pm` / `&$recurse($sym)`).
7192                self.advance();
7193                if matches!(self.peek(), Token::LBrace) {
7194                    self.advance();
7195                    let inner = self.parse_expression()?;
7196                    self.expect(&Token::RBrace)?;
7197                    return Ok(Expr {
7198                        kind: ExprKind::DynamicSubCodeRef(Box::new(inner)),
7199                        line,
7200                    });
7201                }
7202                if matches!(self.peek(), Token::Ident(_)) {
7203                    let name = self.parse_qualified_subroutine_name()?;
7204                    return Ok(Expr {
7205                        kind: ExprKind::SubroutineRef(name),
7206                        line,
7207                    });
7208                }
7209                let target = self.parse_primary()?;
7210                if matches!(self.peek(), Token::LParen) {
7211                    self.advance();
7212                    let args = self.parse_arg_list()?;
7213                    self.expect(&Token::RParen)?;
7214                    return Ok(Expr {
7215                        kind: ExprKind::IndirectCall {
7216                            target: Box::new(target),
7217                            args,
7218                            ampersand: true,
7219                            pass_caller_arglist: false,
7220                        },
7221                        line,
7222                    });
7223                }
7224                // `&$coderef` / `&{expr}` with no `(...)` — call with caller's @_ (Perl `&$sub`).
7225                Ok(Expr {
7226                    kind: ExprKind::IndirectCall {
7227                        target: Box::new(target),
7228                        args: vec![],
7229                        ampersand: true,
7230                        pass_caller_arglist: true,
7231                    },
7232                    line,
7233                })
7234            }
7235            Token::Backslash => {
7236                self.advance();
7237                let expr = self.parse_unary()?;
7238                if let ExprKind::SubroutineRef(name) = expr.kind {
7239                    return Ok(Expr {
7240                        kind: ExprKind::SubroutineCodeRef(name),
7241                        line,
7242                    });
7243                }
7244                if matches!(expr.kind, ExprKind::DynamicSubCodeRef(_)) {
7245                    return Ok(expr);
7246                }
7247                // `\` uses `ScalarRef`; array/hash vars and `\@{...}` lower to binding or alias refs.
7248                Ok(Expr {
7249                    kind: ExprKind::ScalarRef(Box::new(expr)),
7250                    line,
7251                })
7252            }
7253            Token::FileTest(op) => {
7254                self.advance();
7255                // Perl: `-d` with no operand uses `$_` (e.g. `if (-d)` inside `for` / `while read`).
7256                let expr = if Self::filetest_allows_implicit_topic(self.peek()) {
7257                    Expr {
7258                        kind: ExprKind::ScalarVar("_".into()),
7259                        line: self.peek_line(),
7260                    }
7261                } else {
7262                    self.parse_unary()?
7263                };
7264                Ok(Expr {
7265                    kind: ExprKind::FileTest {
7266                        op,
7267                        expr: Box::new(expr),
7268                    },
7269                    line,
7270                })
7271            }
7272            _ => self.parse_power(),
7273        }
7274    }
7275
7276    fn parse_power(&mut self) -> PerlResult<Expr> {
7277        let left = self.parse_postfix()?;
7278        if matches!(self.peek(), Token::Power) {
7279            let line = left.line;
7280            self.advance();
7281            let right = self.parse_unary()?; // right-associative
7282            return Ok(Expr {
7283                kind: ExprKind::BinOp {
7284                    left: Box::new(left),
7285                    op: BinOp::Pow,
7286                    right: Box::new(right),
7287                },
7288                line,
7289            });
7290        }
7291        Ok(left)
7292    }
7293
7294    fn parse_postfix(&mut self) -> PerlResult<Expr> {
7295        let mut expr = self.parse_primary()?;
7296        loop {
7297            match self.peek().clone() {
7298                Token::Increment => {
7299                    // Implicit semicolon: `++` on a new line is a prefix operator
7300                    // on the next statement, not postfix on the previous expression.
7301                    if self.peek_line() > self.prev_line() {
7302                        break;
7303                    }
7304                    let line = expr.line;
7305                    self.advance();
7306                    expr = Expr {
7307                        kind: ExprKind::PostfixOp {
7308                            expr: Box::new(expr),
7309                            op: PostfixOp::Increment,
7310                        },
7311                        line,
7312                    };
7313                }
7314                Token::Decrement => {
7315                    // Implicit semicolon: `--` on a new line is a prefix operator
7316                    // on the next statement, not postfix on the previous expression.
7317                    if self.peek_line() > self.prev_line() {
7318                        break;
7319                    }
7320                    let line = expr.line;
7321                    self.advance();
7322                    expr = Expr {
7323                        kind: ExprKind::PostfixOp {
7324                            expr: Box::new(expr),
7325                            op: PostfixOp::Decrement,
7326                        },
7327                        line,
7328                    };
7329                }
7330                Token::LParen => {
7331                    if self.suppress_indirect_paren_call > 0 {
7332                        break;
7333                    }
7334                    // Implicit semicolon: `(` on a new line after an expression
7335                    // is a new statement, not a postfix code-ref call.
7336                    // e.g.  `my $x = $ENV{"KEY"}\n($y =~ s/.../.../)`
7337                    if self.peek_line() > self.prev_line() {
7338                        break;
7339                    }
7340                    let line = expr.line;
7341                    self.advance();
7342                    let args = self.parse_arg_list()?;
7343                    self.expect(&Token::RParen)?;
7344                    expr = Expr {
7345                        kind: ExprKind::IndirectCall {
7346                            target: Box::new(expr),
7347                            args,
7348                            ampersand: false,
7349                            pass_caller_arglist: false,
7350                        },
7351                        line,
7352                    };
7353                }
7354                Token::Arrow => {
7355                    let line = expr.line;
7356                    self.advance();
7357                    match self.peek().clone() {
7358                        Token::LBracket => {
7359                            self.advance();
7360                            let index = self.parse_expression()?;
7361                            self.expect(&Token::RBracket)?;
7362                            expr = Expr {
7363                                kind: ExprKind::ArrowDeref {
7364                                    expr: Box::new(expr),
7365                                    index: Box::new(index),
7366                                    kind: DerefKind::Array,
7367                                },
7368                                line,
7369                            };
7370                        }
7371                        Token::LBrace => {
7372                            self.advance();
7373                            let key = self.parse_hash_subscript_key()?;
7374                            self.expect(&Token::RBrace)?;
7375                            expr = Expr {
7376                                kind: ExprKind::ArrowDeref {
7377                                    expr: Box::new(expr),
7378                                    index: Box::new(key),
7379                                    kind: DerefKind::Hash,
7380                                },
7381                                line,
7382                            };
7383                        }
7384                        Token::LParen => {
7385                            self.advance();
7386                            let args = self.parse_arg_list()?;
7387                            self.expect(&Token::RParen)?;
7388                            expr = Expr {
7389                                kind: ExprKind::ArrowDeref {
7390                                    expr: Box::new(expr),
7391                                    index: Box::new(Expr {
7392                                        kind: ExprKind::List(args),
7393                                        line,
7394                                    }),
7395                                    kind: DerefKind::Call,
7396                                },
7397                                line,
7398                            };
7399                        }
7400                        Token::Ident(method) => {
7401                            self.advance();
7402                            if method == "SUPER" {
7403                                self.expect(&Token::PackageSep)?;
7404                                let real_method = match self.advance() {
7405                                    (Token::Ident(n), _) => n,
7406                                    (tok, l) => {
7407                                        return Err(self.syntax_err(
7408                                            format!(
7409                                                "Expected method name after SUPER::, got {:?}",
7410                                                tok
7411                                            ),
7412                                            l,
7413                                        ));
7414                                    }
7415                                };
7416                                let args = if self.eat(&Token::LParen) {
7417                                    let a = self.parse_arg_list()?;
7418                                    self.expect(&Token::RParen)?;
7419                                    a
7420                                } else {
7421                                    self.parse_method_arg_list_no_paren()?
7422                                };
7423                                expr = Expr {
7424                                    kind: ExprKind::MethodCall {
7425                                        object: Box::new(expr),
7426                                        method: real_method,
7427                                        args,
7428                                        super_call: true,
7429                                    },
7430                                    line,
7431                                };
7432                            } else {
7433                                let mut method_name = method;
7434                                while self.eat(&Token::PackageSep) {
7435                                    match self.advance() {
7436                                        (Token::Ident(part), _) => {
7437                                            method_name.push_str("::");
7438                                            method_name.push_str(&part);
7439                                        }
7440                                        (tok, l) => {
7441                                            return Err(self.syntax_err(
7442                                                format!(
7443                                                    "Expected identifier after :: in method name, got {:?}",
7444                                                    tok
7445                                                ),
7446                                                l,
7447                                            ));
7448                                        }
7449                                    }
7450                                }
7451                                let args = if self.eat(&Token::LParen) {
7452                                    let a = self.parse_arg_list()?;
7453                                    self.expect(&Token::RParen)?;
7454                                    a
7455                                } else {
7456                                    self.parse_method_arg_list_no_paren()?
7457                                };
7458                                expr = Expr {
7459                                    kind: ExprKind::MethodCall {
7460                                        object: Box::new(expr),
7461                                        method: method_name,
7462                                        args,
7463                                        super_call: false,
7464                                    },
7465                                    line,
7466                                };
7467                            }
7468                        }
7469                        // Postfix dereference (Perl 5.20+, default 5.24+):
7470                        //   `$ref->@*`         — full array      ≡ `@{$ref}`
7471                        //   `$ref->@[i,j]`     — array slice     ≡ `@{$ref}[i,j]`
7472                        //   `$ref->@{k,l}`     — hash slice (vals) ≡ `@{$ref}{k,l}`
7473                        //   `$ref->%*`         — full hash       ≡ `%{$ref}`
7474                        Token::ArrayAt => {
7475                            self.advance(); // consume `@`
7476                            match self.peek().clone() {
7477                                Token::Star => {
7478                                    self.advance();
7479                                    expr = Expr {
7480                                        kind: ExprKind::Deref {
7481                                            expr: Box::new(expr),
7482                                            kind: Sigil::Array,
7483                                        },
7484                                        line,
7485                                    };
7486                                }
7487                                Token::LBracket => {
7488                                    self.advance();
7489                                    let indices = self.parse_slice_arg_list(false)?;
7490                                    self.expect(&Token::RBracket)?;
7491                                    let source = Expr {
7492                                        kind: ExprKind::Deref {
7493                                            expr: Box::new(expr),
7494                                            kind: Sigil::Array,
7495                                        },
7496                                        line,
7497                                    };
7498                                    expr = Expr {
7499                                        kind: ExprKind::AnonymousListSlice {
7500                                            source: Box::new(source),
7501                                            indices,
7502                                        },
7503                                        line,
7504                                    };
7505                                }
7506                                Token::LBrace => {
7507                                    self.advance();
7508                                    let keys = self.parse_slice_arg_list(true)?;
7509                                    self.expect(&Token::RBrace)?;
7510                                    expr = Expr {
7511                                        kind: ExprKind::HashSliceDeref {
7512                                            container: Box::new(expr),
7513                                            keys,
7514                                        },
7515                                        line,
7516                                    };
7517                                }
7518                                tok => {
7519                                    return Err(self.syntax_err(
7520                                        format!(
7521                                            "Expected `*`, `[…]`, or `{{…}}` after `->@`, got {:?}",
7522                                            tok
7523                                        ),
7524                                        line,
7525                                    ));
7526                                }
7527                            }
7528                        }
7529                        Token::HashPercent => {
7530                            self.advance(); // consume `%`
7531                            match self.peek().clone() {
7532                                Token::Star => {
7533                                    self.advance();
7534                                    expr = Expr {
7535                                        kind: ExprKind::Deref {
7536                                            expr: Box::new(expr),
7537                                            kind: Sigil::Hash,
7538                                        },
7539                                        line,
7540                                    };
7541                                }
7542                                tok => {
7543                                    return Err(self.syntax_err(
7544                                        format!("Expected `*` after `->%`, got {:?}", tok),
7545                                        line,
7546                                    ));
7547                                }
7548                            }
7549                        }
7550                        // `x` is lexed as `Token::X` (repeat op); after `->` it is a method name.
7551                        Token::X => {
7552                            self.advance();
7553                            let args = if self.eat(&Token::LParen) {
7554                                let a = self.parse_arg_list()?;
7555                                self.expect(&Token::RParen)?;
7556                                a
7557                            } else {
7558                                self.parse_method_arg_list_no_paren()?
7559                            };
7560                            expr = Expr {
7561                                kind: ExprKind::MethodCall {
7562                                    object: Box::new(expr),
7563                                    method: "x".to_string(),
7564                                    args,
7565                                    super_call: false,
7566                                },
7567                                line,
7568                            };
7569                        }
7570                        _ => break,
7571                    }
7572                }
7573                Token::LBracket => {
7574                    // Implicit semicolon: `[` on a new line is a new statement (array literal),
7575                    // not an array subscript on the preceding expression.
7576                    if self.peek_line() > self.prev_line() {
7577                        break;
7578                    }
7579                    // `$a[i]` — or chained `$r->{k}[i]` / `$a[1][2]` — or list slice `(sort ...)[0]`.
7580                    let line = expr.line;
7581                    if matches!(expr.kind, ExprKind::ScalarVar(_)) {
7582                        if let ExprKind::ScalarVar(ref name) = expr.kind {
7583                            let name = name.clone();
7584                            self.advance();
7585                            let index = self.parse_expression()?;
7586                            self.expect(&Token::RBracket)?;
7587                            expr = Expr {
7588                                kind: ExprKind::ArrayElement {
7589                                    array: name,
7590                                    index: Box::new(index),
7591                                },
7592                                line,
7593                            };
7594                        }
7595                    } else if postfix_lbracket_is_arrow_container(&expr) {
7596                        self.advance();
7597                        let indices = self.parse_arg_list()?;
7598                        self.expect(&Token::RBracket)?;
7599                        expr = Expr {
7600                            kind: ExprKind::ArrowDeref {
7601                                expr: Box::new(expr),
7602                                index: Box::new(Expr {
7603                                    kind: ExprKind::List(indices),
7604                                    line,
7605                                }),
7606                                kind: DerefKind::Array,
7607                            },
7608                            line,
7609                        };
7610                    } else {
7611                        self.advance();
7612                        let indices = self.parse_arg_list()?;
7613                        self.expect(&Token::RBracket)?;
7614                        expr = Expr {
7615                            kind: ExprKind::AnonymousListSlice {
7616                                source: Box::new(expr),
7617                                indices,
7618                            },
7619                            line,
7620                        };
7621                    }
7622                }
7623                Token::LBrace => {
7624                    if self.suppress_scalar_hash_brace > 0 {
7625                        break;
7626                    }
7627                    // Implicit semicolon: `{` on a new line is a new statement (block/hashref),
7628                    // not a hash subscript on the preceding expression.
7629                    if self.peek_line() > self.prev_line() {
7630                        break;
7631                    }
7632                    // `$h{k}`, or chained `$h{k2}{k3}` / `$r->{a}{b}` / `$a[0]{k}` — second+ `{…}` is
7633                    // hash subscript on the scalar value (same as `-> { … }` without extra `->`).
7634                    let line = expr.line;
7635                    let is_scalar_named_hash = matches!(expr.kind, ExprKind::ScalarVar(_));
7636                    let is_chainable_hash_subscript = is_scalar_named_hash
7637                        || matches!(
7638                            expr.kind,
7639                            ExprKind::HashElement { .. }
7640                                | ExprKind::ArrayElement { .. }
7641                                | ExprKind::ArrowDeref { .. }
7642                                | ExprKind::Deref {
7643                                    kind: Sigil::Scalar,
7644                                    ..
7645                                }
7646                        );
7647                    if !is_chainable_hash_subscript {
7648                        break;
7649                    }
7650                    self.advance();
7651                    let key = self.parse_hash_subscript_key()?;
7652                    self.expect(&Token::RBrace)?;
7653                    expr = if is_scalar_named_hash {
7654                        if let ExprKind::ScalarVar(ref name) = expr.kind {
7655                            let name = name.clone();
7656                            // Perl: `$_ { k }` means `$_->{k}` (implicit arrow), not the `%_` stash hash.
7657                            if name == "_" {
7658                                Expr {
7659                                    kind: ExprKind::ArrowDeref {
7660                                        expr: Box::new(Expr {
7661                                            kind: ExprKind::ScalarVar("_".into()),
7662                                            line,
7663                                        }),
7664                                        index: Box::new(key),
7665                                        kind: DerefKind::Hash,
7666                                    },
7667                                    line,
7668                                }
7669                            } else {
7670                                Expr {
7671                                    kind: ExprKind::HashElement {
7672                                        hash: name,
7673                                        key: Box::new(key),
7674                                    },
7675                                    line,
7676                                }
7677                            }
7678                        } else {
7679                            unreachable!("is_scalar_named_hash implies ScalarVar");
7680                        }
7681                    } else {
7682                        Expr {
7683                            kind: ExprKind::ArrowDeref {
7684                                expr: Box::new(expr),
7685                                index: Box::new(key),
7686                                kind: DerefKind::Hash,
7687                            },
7688                            line,
7689                        }
7690                    };
7691                }
7692                _ => break,
7693            }
7694        }
7695        Ok(expr)
7696    }
7697
7698    fn parse_primary(&mut self) -> PerlResult<Expr> {
7699        let line = self.peek_line();
7700        // `my $x = …` (or `our` / `state` / `local`) used inside an expression —
7701        // typically `if (my $x = …)` / `while (my $line = <FH>)`.  Returns the
7702        // assigned value(s); has the side effect of declaring the variable in
7703        // the current scope.  See `ExprKind::MyExpr`.
7704        if let Token::Ident(ref kw) = self.peek().clone() {
7705            if matches!(kw.as_str(), "my" | "our" | "state" | "local") {
7706                let kw_owned = kw.clone();
7707                // Parse exactly like the statement form via `parse_my_our_local`,
7708                // then unwrap the resulting `StmtKind::*` back into a list of
7709                // `VarDecl`s for the expression node.  This re-uses the full
7710                // syntax (typed sigs, list destructuring, type annotations).
7711                let saved_pos = self.pos;
7712                let stmt = self.parse_my_our_local(&kw_owned, false)?;
7713                let decls = match stmt.kind {
7714                    StmtKind::My(d)
7715                    | StmtKind::Our(d)
7716                    | StmtKind::State(d)
7717                    | StmtKind::Local(d) => d,
7718                    _ => {
7719                        // `local *FOO = …` / non-decl forms — fall back to the
7720                        // statement parser (already advanced); restore position
7721                        // and let the surrounding code handle it as a statement
7722                        // by erroring loudly here.
7723                        self.pos = saved_pos;
7724                        return Err(self.syntax_err(
7725                            "`my`/`our`/`local` in expression must declare variables",
7726                            line,
7727                        ));
7728                    }
7729                };
7730                return Ok(Expr {
7731                    kind: ExprKind::MyExpr {
7732                        keyword: kw_owned,
7733                        decls,
7734                    },
7735                    line,
7736                });
7737            }
7738        }
7739        match self.peek().clone() {
7740            Token::Integer(n) => {
7741                self.advance();
7742                Ok(Expr {
7743                    kind: ExprKind::Integer(n),
7744                    line,
7745                })
7746            }
7747            Token::Float(f) => {
7748                self.advance();
7749                Ok(Expr {
7750                    kind: ExprKind::Float(f),
7751                    line,
7752                })
7753            }
7754            // `>{ BLOCK }` — IIFE block expression (immediately-invoked anonymous sub).
7755            // Valid in any expression position; evaluates the block and yields its last value.
7756            // In thread-macro stage position (`EXPR |>` already consumed by the stage loop in
7757            // `parse_thread_macro`), the explicit branch at ~1417 wins and the block is
7758            // instead pipe-applied as a coderef — that path is never reached from here.
7759            Token::ArrowBrace => {
7760                self.advance();
7761                let mut stmts = Vec::new();
7762                while !matches!(self.peek(), Token::RBrace | Token::Eof) {
7763                    if self.eat(&Token::Semicolon) {
7764                        continue;
7765                    }
7766                    stmts.push(self.parse_statement()?);
7767                }
7768                self.expect(&Token::RBrace)?;
7769                let inner_line = stmts.first().map(|s| s.line).unwrap_or(line);
7770                let inner = Expr {
7771                    kind: ExprKind::CodeRef {
7772                        params: vec![],
7773                        body: stmts,
7774                    },
7775                    line: inner_line,
7776                };
7777                Ok(Expr {
7778                    kind: ExprKind::Do(Box::new(inner)),
7779                    line,
7780                })
7781            }
7782            Token::Star => {
7783                self.advance();
7784                if matches!(self.peek(), Token::LBrace) {
7785                    self.advance();
7786                    let inner = self.parse_expression()?;
7787                    self.expect(&Token::RBrace)?;
7788                    return Ok(Expr {
7789                        kind: ExprKind::Deref {
7790                            expr: Box::new(inner),
7791                            kind: Sigil::Typeglob,
7792                        },
7793                        line,
7794                    });
7795                }
7796                // `*$_{$k}`, `*${expr}`, `*$foo` — typeglob from a sigil expression (Perl 5 `*$globref`).
7797                if matches!(
7798                    self.peek(),
7799                    Token::ScalarVar(_)
7800                        | Token::ArrayVar(_)
7801                        | Token::HashVar(_)
7802                        | Token::DerefScalarVar(_)
7803                        | Token::HashPercent
7804                ) {
7805                    let inner = self.parse_postfix()?;
7806                    return Ok(Expr {
7807                        kind: ExprKind::TypeglobExpr(Box::new(inner)),
7808                        line,
7809                    });
7810                }
7811                // `x` tokenizes as `Token::X` (repeat op) — still a valid package/typeglob name.
7812                let mut full_name = match self.advance() {
7813                    (Token::Ident(n), _) => n,
7814                    (Token::X, _) => "x".to_string(),
7815                    (tok, l) => {
7816                        return Err(self
7817                            .syntax_err(format!("Expected identifier after *, got {:?}", tok), l));
7818                    }
7819                };
7820                while self.eat(&Token::PackageSep) {
7821                    match self.advance() {
7822                        (Token::Ident(part), _) => {
7823                            full_name = format!("{}::{}", full_name, part);
7824                        }
7825                        (Token::X, _) => {
7826                            full_name = format!("{}::x", full_name);
7827                        }
7828                        (tok, l) => {
7829                            return Err(self.syntax_err(
7830                                format!("Expected identifier after :: in typeglob, got {:?}", tok),
7831                                l,
7832                            ));
7833                        }
7834                    }
7835                }
7836                Ok(Expr {
7837                    kind: ExprKind::Typeglob(full_name),
7838                    line,
7839                })
7840            }
7841            Token::SingleString(s) => {
7842                self.advance();
7843                Ok(Expr {
7844                    kind: ExprKind::String(s),
7845                    line,
7846                })
7847            }
7848            Token::DoubleString(s) => {
7849                self.advance();
7850                self.parse_interpolated_string(&s, line)
7851            }
7852            Token::BacktickString(s) => {
7853                self.advance();
7854                let inner = self.parse_interpolated_string(&s, line)?;
7855                Ok(Expr {
7856                    kind: ExprKind::Qx(Box::new(inner)),
7857                    line,
7858                })
7859            }
7860            Token::HereDoc(_, body, interpolate) => {
7861                self.advance();
7862                if interpolate {
7863                    self.parse_interpolated_string(&body, line)
7864                } else {
7865                    Ok(Expr {
7866                        kind: ExprKind::String(body),
7867                        line,
7868                    })
7869                }
7870            }
7871            Token::Regex(pattern, flags, _delim) => {
7872                self.advance();
7873                Ok(Expr {
7874                    kind: ExprKind::Regex(pattern, flags),
7875                    line,
7876                })
7877            }
7878            Token::QW(words) => {
7879                self.advance();
7880                Ok(Expr {
7881                    kind: ExprKind::QW(words),
7882                    line,
7883                })
7884            }
7885            Token::DerefScalarVar(name) => {
7886                self.advance();
7887                Ok(Expr {
7888                    kind: ExprKind::Deref {
7889                        expr: Box::new(Expr {
7890                            kind: ExprKind::ScalarVar(name),
7891                            line,
7892                        }),
7893                        kind: Sigil::Scalar,
7894                    },
7895                    line,
7896                })
7897            }
7898            Token::ScalarVar(name) => {
7899                self.advance();
7900                Ok(Expr {
7901                    kind: ExprKind::ScalarVar(name),
7902                    line,
7903                })
7904            }
7905            Token::ArrayVar(name) => {
7906                self.advance();
7907                // Check for slice: @arr[...] (array slice) or @hash{...} (hash slice)
7908                match self.peek() {
7909                    Token::LBracket => {
7910                        self.advance();
7911                        let indices = self.parse_slice_arg_list(false)?;
7912                        self.expect(&Token::RBracket)?;
7913                        Ok(Expr {
7914                            kind: ExprKind::ArraySlice {
7915                                array: name,
7916                                indices,
7917                            },
7918                            line,
7919                        })
7920                    }
7921                    Token::LBrace if self.suppress_scalar_hash_brace == 0 => {
7922                        self.advance();
7923                        let keys = self.parse_slice_arg_list(true)?;
7924                        self.expect(&Token::RBrace)?;
7925                        Ok(Expr {
7926                            kind: ExprKind::HashSlice { hash: name, keys },
7927                            line,
7928                        })
7929                    }
7930                    _ => Ok(Expr {
7931                        kind: ExprKind::ArrayVar(name),
7932                        line,
7933                    }),
7934                }
7935            }
7936            Token::HashVar(name) => {
7937                self.advance();
7938                Ok(Expr {
7939                    kind: ExprKind::HashVar(name),
7940                    line,
7941                })
7942            }
7943            Token::HashPercent => {
7944                // `%$href` — hash ref deref; `%{ $expr }` — symbolic / braced form
7945                self.advance();
7946                if matches!(self.peek(), Token::ScalarVar(_)) {
7947                    let n = match self.advance() {
7948                        (Token::ScalarVar(n), _) => n,
7949                        (tok, l) => {
7950                            return Err(self.syntax_err(
7951                                format!("Expected scalar variable after %%, got {:?}", tok),
7952                                l,
7953                            ));
7954                        }
7955                    };
7956                    return Ok(Expr {
7957                        kind: ExprKind::Deref {
7958                            expr: Box::new(Expr {
7959                                kind: ExprKind::ScalarVar(n),
7960                                line,
7961                            }),
7962                            kind: Sigil::Hash,
7963                        },
7964                        line,
7965                    });
7966                }
7967                // `%[a => 1, b => 2]` — sugar for `%{+{a=>1,b=>2}}`: dereference an
7968                // anonymous hashref inline, using `[...]` as the delimiter to avoid
7969                // the block-vs-hashref ambiguity that `%{a=>1}` has in real Perl.
7970                // Real Perl errors on `%[...]` syntactically, so no compat risk.
7971                if matches!(self.peek(), Token::LBracket) {
7972                    self.advance();
7973                    let pairs = self.parse_hashref_pairs_until(&Token::RBracket)?;
7974                    self.expect(&Token::RBracket)?;
7975                    let href = Expr {
7976                        kind: ExprKind::HashRef(pairs),
7977                        line,
7978                    };
7979                    return Ok(Expr {
7980                        kind: ExprKind::Deref {
7981                            expr: Box::new(href),
7982                            kind: Sigil::Hash,
7983                        },
7984                        line,
7985                    });
7986                }
7987                self.expect(&Token::LBrace)?;
7988                // Peek to disambiguate `%{ $ref }` (deref a hashref expression) from
7989                // `%{ k => v }` (inline hash literal). Real Perl's block-vs-hashref
7990                // heuristic is famously unreliable — when the first non-whitespace
7991                // token is an ident/string followed by `=>`, treat the whole thing
7992                // as a hashref literal to make `%{a=>1,b=>2}` work reliably.
7993                let looks_like_pair = matches!(
7994                    self.peek(),
7995                    Token::Ident(_) | Token::SingleString(_) | Token::DoubleString(_)
7996                ) && matches!(self.peek_at(1), Token::FatArrow);
7997                let inner = if looks_like_pair {
7998                    let pairs = self.parse_hashref_pairs_until(&Token::RBrace)?;
7999                    Expr {
8000                        kind: ExprKind::HashRef(pairs),
8001                        line,
8002                    }
8003                } else {
8004                    self.parse_expression()?
8005                };
8006                self.expect(&Token::RBrace)?;
8007                Ok(Expr {
8008                    kind: ExprKind::Deref {
8009                        expr: Box::new(inner),
8010                        kind: Sigil::Hash,
8011                    },
8012                    line,
8013                })
8014            }
8015            Token::ArrayAt => {
8016                self.advance();
8017                // `@{ $expr }` / `@{ "Pkg::NAME" }` — symbolic array (e.g. `@{"$pkg\::EXPORT"}` in Exporter.pm)
8018                if matches!(self.peek(), Token::LBrace) {
8019                    self.advance();
8020                    let inner = self.parse_expression()?;
8021                    self.expect(&Token::RBrace)?;
8022                    return Ok(Expr {
8023                        kind: ExprKind::Deref {
8024                            expr: Box::new(inner),
8025                            kind: Sigil::Array,
8026                        },
8027                        line,
8028                    });
8029                }
8030                // `@[a, b, c]` — sugar for `@{[a, b, c]}`: dereference an
8031                // anonymous arrayref inline. Real Perl rejects `@[...]` at
8032                // the parser level, so this extension has no compat risk.
8033                if matches!(self.peek(), Token::LBracket) {
8034                    self.advance();
8035                    let mut elems = Vec::new();
8036                    if !matches!(self.peek(), Token::RBracket) {
8037                        elems.push(self.parse_assign_expr()?);
8038                        while self.eat(&Token::Comma) {
8039                            if matches!(self.peek(), Token::RBracket) {
8040                                break;
8041                            }
8042                            elems.push(self.parse_assign_expr()?);
8043                        }
8044                    }
8045                    self.expect(&Token::RBracket)?;
8046                    let aref = Expr {
8047                        kind: ExprKind::ArrayRef(elems),
8048                        line,
8049                    };
8050                    return Ok(Expr {
8051                        kind: ExprKind::Deref {
8052                            expr: Box::new(aref),
8053                            kind: Sigil::Array,
8054                        },
8055                        line,
8056                    });
8057                }
8058                // `@$arr` — array dereference; `@$h{k1,k2}` — hash slice via hashref
8059                let container = match self.peek().clone() {
8060                    Token::ScalarVar(n) => {
8061                        self.advance();
8062                        Expr {
8063                            kind: ExprKind::ScalarVar(n),
8064                            line,
8065                        }
8066                    }
8067                    _ => {
8068                        return Err(self.syntax_err(
8069                            "Expected `$name`, `{`, or `[` after `@` (e.g. `@$aref`, `@{expr}`, `@[1,2,3]`, or `@$href{keys}`)",
8070                            line,
8071                        ));
8072                    }
8073                };
8074                if matches!(self.peek(), Token::LBrace) {
8075                    self.advance();
8076                    let keys = self.parse_slice_arg_list(true)?;
8077                    self.expect(&Token::RBrace)?;
8078                    return Ok(Expr {
8079                        kind: ExprKind::HashSliceDeref {
8080                            container: Box::new(container),
8081                            keys,
8082                        },
8083                        line,
8084                    });
8085                }
8086                Ok(Expr {
8087                    kind: ExprKind::Deref {
8088                        expr: Box::new(container),
8089                        kind: Sigil::Array,
8090                    },
8091                    line,
8092                })
8093            }
8094            Token::LParen => {
8095                self.advance();
8096                if matches!(self.peek(), Token::RParen) {
8097                    self.advance();
8098                    return Ok(Expr {
8099                        kind: ExprKind::List(vec![]),
8100                        line,
8101                    });
8102                }
8103                // Inside parens, pipe-forward is allowed even if we're in a
8104                // paren-less arg context. Save and restore no_pipe_forward_depth.
8105                let saved_no_pipe = self.no_pipe_forward_depth;
8106                self.no_pipe_forward_depth = 0;
8107                let expr = self.parse_expression();
8108                self.no_pipe_forward_depth = saved_no_pipe;
8109                let expr = expr?;
8110                self.expect(&Token::RParen)?;
8111                Ok(expr)
8112            }
8113            Token::LBracket => {
8114                self.advance();
8115                let elems = self.parse_arg_list()?;
8116                self.expect(&Token::RBracket)?;
8117                Ok(Expr {
8118                    kind: ExprKind::ArrayRef(elems),
8119                    line,
8120                })
8121            }
8122            Token::LBrace => {
8123                // Could be hash ref or block — disambiguate
8124                self.advance();
8125                // Try to parse as hash ref: { key => val, ... }
8126                let saved = self.pos;
8127                match self.try_parse_hash_ref() {
8128                    Ok(pairs) => Ok(Expr {
8129                        kind: ExprKind::HashRef(pairs),
8130                        line,
8131                    }),
8132                    Err(_) => {
8133                        self.pos = saved;
8134                        // Parse as block, wrap in code ref
8135                        let mut stmts = Vec::new();
8136                        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
8137                            if self.eat(&Token::Semicolon) {
8138                                continue;
8139                            }
8140                            stmts.push(self.parse_statement()?);
8141                        }
8142                        self.expect(&Token::RBrace)?;
8143                        Ok(Expr {
8144                            kind: ExprKind::CodeRef {
8145                                params: vec![],
8146                                body: stmts,
8147                            },
8148                            line,
8149                        })
8150                    }
8151                }
8152            }
8153            Token::Diamond => {
8154                self.advance();
8155                Ok(Expr {
8156                    kind: ExprKind::ReadLine(None),
8157                    line,
8158                })
8159            }
8160            Token::ReadLine(handle) => {
8161                self.advance();
8162                Ok(Expr {
8163                    kind: ExprKind::ReadLine(Some(handle)),
8164                    line,
8165                })
8166            }
8167
8168            // Named functions / builtins
8169            Token::ThreadArrow => {
8170                self.advance();
8171                self.parse_thread_macro(line, false)
8172            }
8173            Token::ThreadArrowLast => {
8174                self.advance();
8175                self.parse_thread_macro(line, true)
8176            }
8177            Token::Ident(ref name) => {
8178                let name = name.clone();
8179                // Handle s///
8180                if name.starts_with('\x00') {
8181                    self.advance();
8182                    let parts: Vec<&str> = name.split('\x00').collect();
8183                    if parts.len() >= 4 && parts[1] == "s" {
8184                        let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
8185                        return Ok(Expr {
8186                            kind: ExprKind::Substitution {
8187                                expr: Box::new(Expr {
8188                                    kind: ExprKind::ScalarVar("_".into()),
8189                                    line,
8190                                }),
8191                                pattern: parts[2].to_string(),
8192                                replacement: parts[3].to_string(),
8193                                flags: parts.get(4).unwrap_or(&"").to_string(),
8194                                delim,
8195                            },
8196                            line,
8197                        });
8198                    }
8199                    if parts.len() >= 4 && parts[1] == "tr" {
8200                        let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
8201                        return Ok(Expr {
8202                            kind: ExprKind::Transliterate {
8203                                expr: Box::new(Expr {
8204                                    kind: ExprKind::ScalarVar("_".into()),
8205                                    line,
8206                                }),
8207                                from: parts[2].to_string(),
8208                                to: parts[3].to_string(),
8209                                flags: parts.get(4).unwrap_or(&"").to_string(),
8210                                delim,
8211                            },
8212                            line,
8213                        });
8214                    }
8215                    return Err(self.syntax_err("Unexpected encoded token", line));
8216                }
8217                self.parse_named_expr(name)
8218            }
8219
8220            // `%name` when lexer emitted `Token::Percent` (due to preceding term context)
8221            // instead of `Token::HashVar`. This happens after `t` (thread macro) etc.
8222            Token::Percent => {
8223                self.advance();
8224                match self.peek().clone() {
8225                    Token::Ident(name) => {
8226                        self.advance();
8227                        Ok(Expr {
8228                            kind: ExprKind::HashVar(name),
8229                            line,
8230                        })
8231                    }
8232                    Token::ScalarVar(n) => {
8233                        self.advance();
8234                        Ok(Expr {
8235                            kind: ExprKind::Deref {
8236                                expr: Box::new(Expr {
8237                                    kind: ExprKind::ScalarVar(n),
8238                                    line,
8239                                }),
8240                                kind: Sigil::Hash,
8241                            },
8242                            line,
8243                        })
8244                    }
8245                    Token::LBrace => {
8246                        self.advance();
8247                        let looks_like_pair = matches!(
8248                            self.peek(),
8249                            Token::Ident(_) | Token::SingleString(_) | Token::DoubleString(_)
8250                        ) && matches!(self.peek_at(1), Token::FatArrow);
8251                        let inner = if looks_like_pair {
8252                            let pairs = self.parse_hashref_pairs_until(&Token::RBrace)?;
8253                            Expr {
8254                                kind: ExprKind::HashRef(pairs),
8255                                line,
8256                            }
8257                        } else {
8258                            self.parse_expression()?
8259                        };
8260                        self.expect(&Token::RBrace)?;
8261                        Ok(Expr {
8262                            kind: ExprKind::Deref {
8263                                expr: Box::new(inner),
8264                                kind: Sigil::Hash,
8265                            },
8266                            line,
8267                        })
8268                    }
8269                    Token::LBracket => {
8270                        self.advance();
8271                        let pairs = self.parse_hashref_pairs_until(&Token::RBracket)?;
8272                        self.expect(&Token::RBracket)?;
8273                        let href = Expr {
8274                            kind: ExprKind::HashRef(pairs),
8275                            line,
8276                        };
8277                        Ok(Expr {
8278                            kind: ExprKind::Deref {
8279                                expr: Box::new(href),
8280                                kind: Sigil::Hash,
8281                            },
8282                            line,
8283                        })
8284                    }
8285                    tok => Err(self.syntax_err(
8286                        format!(
8287                            "Expected identifier, `$`, `{{`, or `[` after `%`, got {:?}",
8288                            tok
8289                        ),
8290                        line,
8291                    )),
8292                }
8293            }
8294
8295            tok => Err(self.syntax_err(format!("Unexpected token {:?}", tok), line)),
8296        }
8297    }
8298
8299    fn parse_named_expr(&mut self, mut name: String) -> PerlResult<Expr> {
8300        let line = self.peek_line();
8301        self.advance(); // consume the ident
8302        while self.eat(&Token::PackageSep) {
8303            match self.advance() {
8304                (Token::Ident(part), _) => {
8305                    name = format!("{}::{}", name, part);
8306                }
8307                (tok, err_line) => {
8308                    return Err(self.syntax_err(
8309                        format!("Expected identifier after `::`, got {:?}", tok),
8310                        err_line,
8311                    ));
8312                }
8313            }
8314        }
8315
8316        // Fat-arrow auto-quoting: ANY bareword (including keywords/builtins)
8317        // before `=>` is treated as a string key, matching Perl 5 semantics.
8318        // e.g. `(print => 1, pr => "x", sort => 3)` are all valid hash pairs.
8319        if matches!(self.peek(), Token::FatArrow) {
8320            return Ok(Expr {
8321                kind: ExprKind::String(name),
8322                line,
8323            });
8324        }
8325
8326        if crate::compat_mode() {
8327            if let Some(ext) = Self::stryke_extension_name(&name) {
8328                if !self.declared_subs.contains(&name) {
8329                    return Err(self.syntax_err(
8330                        format!("`{ext}` is a stryke extension (disabled by --compat)"),
8331                        line,
8332                    ));
8333                }
8334            }
8335        }
8336
8337        match name.as_str() {
8338            "__FILE__" => Ok(Expr {
8339                kind: ExprKind::MagicConst(MagicConstKind::File),
8340                line,
8341            }),
8342            "__LINE__" => Ok(Expr {
8343                kind: ExprKind::MagicConst(MagicConstKind::Line),
8344                line,
8345            }),
8346            "__SUB__" => Ok(Expr {
8347                kind: ExprKind::MagicConst(MagicConstKind::Sub),
8348                line,
8349            }),
8350            "stdin" => Ok(Expr {
8351                kind: ExprKind::FuncCall {
8352                    name: "stdin".into(),
8353                    args: vec![],
8354                },
8355                line,
8356            }),
8357            "range" => {
8358                let args = self.parse_builtin_args()?;
8359                Ok(Expr {
8360                    kind: ExprKind::FuncCall {
8361                        name: "range".into(),
8362                        args,
8363                    },
8364                    line,
8365                })
8366            }
8367            "print" | "pr" => self.parse_print_like(|h, a| ExprKind::Print { handle: h, args: a }),
8368            "say" => {
8369                if crate::no_interop_mode() {
8370                    return Err(
8371                        self.syntax_err("stryke uses `p` instead of `say` (--no-interop)", line)
8372                    );
8373                }
8374                self.parse_print_like(|h, a| ExprKind::Say { handle: h, args: a })
8375            }
8376            "p" => self.parse_print_like(|h, a| ExprKind::Say { handle: h, args: a }),
8377            "printf" => self.parse_print_like(|h, a| ExprKind::Printf { handle: h, args: a }),
8378            "die" => {
8379                let args = self.parse_list_until_terminator()?;
8380                Ok(Expr {
8381                    kind: ExprKind::Die(args),
8382                    line,
8383                })
8384            }
8385            "warn" => {
8386                let args = self.parse_list_until_terminator()?;
8387                Ok(Expr {
8388                    kind: ExprKind::Warn(args),
8389                    line,
8390                })
8391            }
8392            // `croak` / `confess` — `Carp` builtins available without `use Carp`
8393            // (matches the doc claim in `lsp.rs:1243`). For now both desugar to
8394            // `die` — TODO: croak should report caller's file/line, confess
8395            // should append a full stack trace.
8396            "croak" | "confess" => {
8397                let args = self.parse_list_until_terminator()?;
8398                Ok(Expr {
8399                    kind: ExprKind::Die(args),
8400                    line,
8401                })
8402            }
8403            // `carp` / `cluck` — `Carp` warning siblings of `croak`/`confess`.
8404            "carp" | "cluck" => {
8405                let args = self.parse_list_until_terminator()?;
8406                Ok(Expr {
8407                    kind: ExprKind::Warn(args),
8408                    line,
8409                })
8410            }
8411            "chomp" => {
8412                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8413                    return Ok(e);
8414                }
8415                let a = self.parse_one_arg_or_default()?;
8416                Ok(Expr {
8417                    kind: ExprKind::Chomp(Box::new(a)),
8418                    line,
8419                })
8420            }
8421            "chop" => {
8422                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8423                    return Ok(e);
8424                }
8425                let a = self.parse_one_arg_or_default()?;
8426                Ok(Expr {
8427                    kind: ExprKind::Chop(Box::new(a)),
8428                    line,
8429                })
8430            }
8431            "length" => {
8432                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8433                    return Ok(e);
8434                }
8435                let a = self.parse_one_arg_or_default()?;
8436                Ok(Expr {
8437                    kind: ExprKind::Length(Box::new(a)),
8438                    line,
8439                })
8440            }
8441            "defined" => {
8442                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8443                    return Ok(e);
8444                }
8445                let a = self.parse_one_arg_or_default()?;
8446                Ok(Expr {
8447                    kind: ExprKind::Defined(Box::new(a)),
8448                    line,
8449                })
8450            }
8451            "ref" => {
8452                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8453                    return Ok(e);
8454                }
8455                let a = self.parse_one_arg_or_default()?;
8456                Ok(Expr {
8457                    kind: ExprKind::Ref(Box::new(a)),
8458                    line,
8459                })
8460            }
8461            "undef" => {
8462                // `undef $var` sets `$var` to undef — but a variable on a new line
8463                // is a separate statement (implicit semicolon), not an argument.
8464                if self.peek_line() == self.prev_line()
8465                    && matches!(
8466                        self.peek(),
8467                        Token::ScalarVar(_) | Token::ArrayVar(_) | Token::HashVar(_)
8468                    )
8469                {
8470                    let target = self.parse_primary()?;
8471                    return Ok(Expr {
8472                        kind: ExprKind::Assign {
8473                            target: Box::new(target),
8474                            value: Box::new(Expr {
8475                                kind: ExprKind::Undef,
8476                                line,
8477                            }),
8478                        },
8479                        line,
8480                    });
8481                }
8482                Ok(Expr {
8483                    kind: ExprKind::Undef,
8484                    line,
8485                })
8486            }
8487            "scalar" => {
8488                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8489                    return Ok(e);
8490                }
8491                let a = self.parse_one_arg_or_default()?;
8492                Ok(Expr {
8493                    kind: ExprKind::ScalarContext(Box::new(a)),
8494                    line,
8495                })
8496            }
8497            "abs" => {
8498                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8499                    return Ok(e);
8500                }
8501                let a = self.parse_one_arg_or_default()?;
8502                Ok(Expr {
8503                    kind: ExprKind::Abs(Box::new(a)),
8504                    line,
8505                })
8506            }
8507            // stryke unary numeric extensions — treat like `abs` so a bare
8508            // identifier in `map { inc }` / `for (…) { p inc }` becomes a
8509            // call with implicit `$_` rather than falling through to the
8510            // generic `Bareword` arm (which stringifies to `"inc"`).
8511            "inc" | "dec" => {
8512                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8513                    return Ok(e);
8514                }
8515                let a = self.parse_one_arg_or_default()?;
8516                Ok(Expr {
8517                    kind: ExprKind::FuncCall {
8518                        name,
8519                        args: vec![a],
8520                    },
8521                    line,
8522                })
8523            }
8524            "int" => {
8525                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8526                    return Ok(e);
8527                }
8528                let a = self.parse_one_arg_or_default()?;
8529                Ok(Expr {
8530                    kind: ExprKind::Int(Box::new(a)),
8531                    line,
8532                })
8533            }
8534            "sqrt" => {
8535                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8536                    return Ok(e);
8537                }
8538                let a = self.parse_one_arg_or_default()?;
8539                Ok(Expr {
8540                    kind: ExprKind::Sqrt(Box::new(a)),
8541                    line,
8542                })
8543            }
8544            "sin" => {
8545                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8546                    return Ok(e);
8547                }
8548                let a = self.parse_one_arg_or_default()?;
8549                Ok(Expr {
8550                    kind: ExprKind::Sin(Box::new(a)),
8551                    line,
8552                })
8553            }
8554            "cos" => {
8555                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8556                    return Ok(e);
8557                }
8558                let a = self.parse_one_arg_or_default()?;
8559                Ok(Expr {
8560                    kind: ExprKind::Cos(Box::new(a)),
8561                    line,
8562                })
8563            }
8564            "atan2" => {
8565                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8566                    return Ok(e);
8567                }
8568                let args = self.parse_builtin_args()?;
8569                if args.len() != 2 {
8570                    return Err(self.syntax_err("atan2 requires two arguments", line));
8571                }
8572                Ok(Expr {
8573                    kind: ExprKind::Atan2 {
8574                        y: Box::new(args[0].clone()),
8575                        x: Box::new(args[1].clone()),
8576                    },
8577                    line,
8578                })
8579            }
8580            "exp" => {
8581                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8582                    return Ok(e);
8583                }
8584                let a = self.parse_one_arg_or_default()?;
8585                Ok(Expr {
8586                    kind: ExprKind::Exp(Box::new(a)),
8587                    line,
8588                })
8589            }
8590            "log" => {
8591                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8592                    return Ok(e);
8593                }
8594                let a = self.parse_one_arg_or_default()?;
8595                Ok(Expr {
8596                    kind: ExprKind::Log(Box::new(a)),
8597                    line,
8598                })
8599            }
8600            "input" => {
8601                let args = if matches!(
8602                    self.peek(),
8603                    Token::Semicolon
8604                        | Token::RBrace
8605                        | Token::RParen
8606                        | Token::Eof
8607                        | Token::Comma
8608                        | Token::PipeForward
8609                ) {
8610                    vec![]
8611                } else if matches!(self.peek(), Token::LParen) {
8612                    self.advance();
8613                    if matches!(self.peek(), Token::RParen) {
8614                        self.advance();
8615                        vec![]
8616                    } else {
8617                        let a = self.parse_expression()?;
8618                        self.expect(&Token::RParen)?;
8619                        vec![a]
8620                    }
8621                } else {
8622                    let a = self.parse_one_arg()?;
8623                    vec![a]
8624                };
8625                Ok(Expr {
8626                    kind: ExprKind::FuncCall {
8627                        name: "input".to_string(),
8628                        args,
8629                    },
8630                    line,
8631                })
8632            }
8633            "rand" => {
8634                if matches!(
8635                    self.peek(),
8636                    Token::Semicolon
8637                        | Token::RBrace
8638                        | Token::RParen
8639                        | Token::Eof
8640                        | Token::Comma
8641                        | Token::PipeForward
8642                ) {
8643                    Ok(Expr {
8644                        kind: ExprKind::Rand(None),
8645                        line,
8646                    })
8647                } else if matches!(self.peek(), Token::LParen) {
8648                    self.advance();
8649                    if matches!(self.peek(), Token::RParen) {
8650                        self.advance();
8651                        Ok(Expr {
8652                            kind: ExprKind::Rand(None),
8653                            line,
8654                        })
8655                    } else {
8656                        let a = self.parse_expression()?;
8657                        self.expect(&Token::RParen)?;
8658                        Ok(Expr {
8659                            kind: ExprKind::Rand(Some(Box::new(a))),
8660                            line,
8661                        })
8662                    }
8663                } else {
8664                    let a = self.parse_one_arg()?;
8665                    Ok(Expr {
8666                        kind: ExprKind::Rand(Some(Box::new(a))),
8667                        line,
8668                    })
8669                }
8670            }
8671            "srand" => {
8672                if matches!(
8673                    self.peek(),
8674                    Token::Semicolon
8675                        | Token::RBrace
8676                        | Token::RParen
8677                        | Token::Eof
8678                        | Token::Comma
8679                        | Token::PipeForward
8680                ) {
8681                    Ok(Expr {
8682                        kind: ExprKind::Srand(None),
8683                        line,
8684                    })
8685                } else if matches!(self.peek(), Token::LParen) {
8686                    self.advance();
8687                    if matches!(self.peek(), Token::RParen) {
8688                        self.advance();
8689                        Ok(Expr {
8690                            kind: ExprKind::Srand(None),
8691                            line,
8692                        })
8693                    } else {
8694                        let a = self.parse_expression()?;
8695                        self.expect(&Token::RParen)?;
8696                        Ok(Expr {
8697                            kind: ExprKind::Srand(Some(Box::new(a))),
8698                            line,
8699                        })
8700                    }
8701                } else {
8702                    let a = self.parse_one_arg()?;
8703                    Ok(Expr {
8704                        kind: ExprKind::Srand(Some(Box::new(a))),
8705                        line,
8706                    })
8707                }
8708            }
8709            "hex" => {
8710                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8711                    return Ok(e);
8712                }
8713                let a = self.parse_one_arg_or_default()?;
8714                Ok(Expr {
8715                    kind: ExprKind::Hex(Box::new(a)),
8716                    line,
8717                })
8718            }
8719            "oct" => {
8720                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8721                    return Ok(e);
8722                }
8723                let a = self.parse_one_arg_or_default()?;
8724                Ok(Expr {
8725                    kind: ExprKind::Oct(Box::new(a)),
8726                    line,
8727                })
8728            }
8729            "chr" => {
8730                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8731                    return Ok(e);
8732                }
8733                let a = self.parse_one_arg_or_default()?;
8734                Ok(Expr {
8735                    kind: ExprKind::Chr(Box::new(a)),
8736                    line,
8737                })
8738            }
8739            "ord" => {
8740                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8741                    return Ok(e);
8742                }
8743                let a = self.parse_one_arg_or_default()?;
8744                Ok(Expr {
8745                    kind: ExprKind::Ord(Box::new(a)),
8746                    line,
8747                })
8748            }
8749            "lc" => {
8750                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8751                    return Ok(e);
8752                }
8753                let a = self.parse_one_arg_or_default()?;
8754                Ok(Expr {
8755                    kind: ExprKind::Lc(Box::new(a)),
8756                    line,
8757                })
8758            }
8759            "uc" => {
8760                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8761                    return Ok(e);
8762                }
8763                let a = self.parse_one_arg_or_default()?;
8764                Ok(Expr {
8765                    kind: ExprKind::Uc(Box::new(a)),
8766                    line,
8767                })
8768            }
8769            "lcfirst" => {
8770                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8771                    return Ok(e);
8772                }
8773                let a = self.parse_one_arg_or_default()?;
8774                Ok(Expr {
8775                    kind: ExprKind::Lcfirst(Box::new(a)),
8776                    line,
8777                })
8778            }
8779            "ucfirst" => {
8780                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8781                    return Ok(e);
8782                }
8783                let a = self.parse_one_arg_or_default()?;
8784                Ok(Expr {
8785                    kind: ExprKind::Ucfirst(Box::new(a)),
8786                    line,
8787                })
8788            }
8789            "fc" => {
8790                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8791                    return Ok(e);
8792                }
8793                let a = self.parse_one_arg_or_default()?;
8794                Ok(Expr {
8795                    kind: ExprKind::Fc(Box::new(a)),
8796                    line,
8797                })
8798            }
8799            "crypt" => {
8800                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8801                    return Ok(e);
8802                }
8803                let args = self.parse_builtin_args()?;
8804                if args.len() != 2 {
8805                    return Err(self.syntax_err("crypt requires two arguments", line));
8806                }
8807                Ok(Expr {
8808                    kind: ExprKind::Crypt {
8809                        plaintext: Box::new(args[0].clone()),
8810                        salt: Box::new(args[1].clone()),
8811                    },
8812                    line,
8813                })
8814            }
8815            "pos" => {
8816                if matches!(
8817                    self.peek(),
8818                    Token::Semicolon
8819                        | Token::RBrace
8820                        | Token::RParen
8821                        | Token::Eof
8822                        | Token::Comma
8823                        | Token::PipeForward
8824                ) {
8825                    Ok(Expr {
8826                        kind: ExprKind::Pos(None),
8827                        line,
8828                    })
8829                } else if matches!(self.peek(), Token::Assign) {
8830                    // Perl: `pos = EXPR` is `pos($_) = EXPR` (Text::Balanced `_eb_delims`).
8831                    self.advance();
8832                    let rhs = self.parse_assign_expr()?;
8833                    Ok(Expr {
8834                        kind: ExprKind::Assign {
8835                            target: Box::new(Expr {
8836                                kind: ExprKind::Pos(Some(Box::new(Expr {
8837                                    kind: ExprKind::ScalarVar("_".into()),
8838                                    line,
8839                                }))),
8840                                line,
8841                            }),
8842                            value: Box::new(rhs),
8843                        },
8844                        line,
8845                    })
8846                } else if matches!(self.peek(), Token::LParen) {
8847                    self.advance();
8848                    if matches!(self.peek(), Token::RParen) {
8849                        self.advance();
8850                        Ok(Expr {
8851                            kind: ExprKind::Pos(None),
8852                            line,
8853                        })
8854                    } else {
8855                        let a = self.parse_expression()?;
8856                        self.expect(&Token::RParen)?;
8857                        Ok(Expr {
8858                            kind: ExprKind::Pos(Some(Box::new(a))),
8859                            line,
8860                        })
8861                    }
8862                } else {
8863                    let saved = self.pos;
8864                    let subj = self.parse_unary()?;
8865                    if matches!(self.peek(), Token::Assign) {
8866                        self.advance();
8867                        let rhs = self.parse_assign_expr()?;
8868                        Ok(Expr {
8869                            kind: ExprKind::Assign {
8870                                target: Box::new(Expr {
8871                                    kind: ExprKind::Pos(Some(Box::new(subj))),
8872                                    line,
8873                                }),
8874                                value: Box::new(rhs),
8875                            },
8876                            line,
8877                        })
8878                    } else {
8879                        self.pos = saved;
8880                        let a = self.parse_one_arg()?;
8881                        Ok(Expr {
8882                            kind: ExprKind::Pos(Some(Box::new(a))),
8883                            line,
8884                        })
8885                    }
8886                }
8887            }
8888            "study" => {
8889                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8890                    return Ok(e);
8891                }
8892                let a = self.parse_one_arg_or_default()?;
8893                Ok(Expr {
8894                    kind: ExprKind::Study(Box::new(a)),
8895                    line,
8896                })
8897            }
8898            "push" => {
8899                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8900                    return Ok(e);
8901                }
8902                let args = self.parse_builtin_args()?;
8903                let (first, rest) = args
8904                    .split_first()
8905                    .ok_or_else(|| self.syntax_err("push requires arguments", line))?;
8906                Ok(Expr {
8907                    kind: ExprKind::Push {
8908                        array: Box::new(first.clone()),
8909                        values: rest.to_vec(),
8910                    },
8911                    line,
8912                })
8913            }
8914            "pop" => {
8915                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8916                    return Ok(e);
8917                }
8918                let a = self.parse_one_arg_or_argv()?;
8919                Ok(Expr {
8920                    kind: ExprKind::Pop(Box::new(a)),
8921                    line,
8922                })
8923            }
8924            "shift" => {
8925                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8926                    return Ok(e);
8927                }
8928                let a = self.parse_one_arg_or_argv()?;
8929                Ok(Expr {
8930                    kind: ExprKind::Shift(Box::new(a)),
8931                    line,
8932                })
8933            }
8934            "unshift" => {
8935                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8936                    return Ok(e);
8937                }
8938                let args = self.parse_builtin_args()?;
8939                let (first, rest) = args
8940                    .split_first()
8941                    .ok_or_else(|| self.syntax_err("unshift requires arguments", line))?;
8942                Ok(Expr {
8943                    kind: ExprKind::Unshift {
8944                        array: Box::new(first.clone()),
8945                        values: rest.to_vec(),
8946                    },
8947                    line,
8948                })
8949            }
8950            "splice" => {
8951                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8952                    return Ok(e);
8953                }
8954                let args = self.parse_builtin_args()?;
8955                let mut iter = args.into_iter();
8956                let array = Box::new(
8957                    iter.next()
8958                        .ok_or_else(|| self.syntax_err("splice requires arguments", line))?,
8959                );
8960                let offset = iter.next().map(Box::new);
8961                let length = iter.next().map(Box::new);
8962                let replacement: Vec<Expr> = iter.collect();
8963                Ok(Expr {
8964                    kind: ExprKind::Splice {
8965                        array,
8966                        offset,
8967                        length,
8968                        replacement,
8969                    },
8970                    line,
8971                })
8972            }
8973            "delete" => {
8974                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8975                    return Ok(e);
8976                }
8977                let a = self.parse_postfix()?;
8978                Ok(Expr {
8979                    kind: ExprKind::Delete(Box::new(a)),
8980                    line,
8981                })
8982            }
8983            "exists" => {
8984                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8985                    return Ok(e);
8986                }
8987                let a = self.parse_postfix()?;
8988                Ok(Expr {
8989                    kind: ExprKind::Exists(Box::new(a)),
8990                    line,
8991                })
8992            }
8993            "keys" => {
8994                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
8995                    return Ok(e);
8996                }
8997                let a = self.parse_one_arg_or_default()?;
8998                Ok(Expr {
8999                    kind: ExprKind::Keys(Box::new(a)),
9000                    line,
9001                })
9002            }
9003            "values" => {
9004                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9005                    return Ok(e);
9006                }
9007                let a = self.parse_one_arg_or_default()?;
9008                Ok(Expr {
9009                    kind: ExprKind::Values(Box::new(a)),
9010                    line,
9011                })
9012            }
9013            "each" => {
9014                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9015                    return Ok(e);
9016                }
9017                let a = self.parse_one_arg_or_default()?;
9018                Ok(Expr {
9019                    kind: ExprKind::Each(Box::new(a)),
9020                    line,
9021                })
9022            }
9023            "fore" | "e" | "ep" => {
9024                // `fore { BLOCK } LIST` / `ep` — forEach expression (pipe-forward friendly)
9025                if matches!(self.peek(), Token::LBrace) {
9026                    let (block, list) = self.parse_block_list()?;
9027                    Ok(Expr {
9028                        kind: ExprKind::ForEachExpr {
9029                            block,
9030                            list: Box::new(list),
9031                        },
9032                        line,
9033                    })
9034                } else if self.in_pipe_rhs() {
9035                    // `|> ep` — bare ep at end of pipe: default to `say $_`
9036                    // `|> fore say` / `|> e say` — blockless pipe form: wrap EXPR into a synthetic block
9037                    let is_terminal = matches!(
9038                        self.peek(),
9039                        Token::Semicolon
9040                            | Token::RParen
9041                            | Token::Eof
9042                            | Token::PipeForward
9043                            | Token::RBrace
9044                    );
9045                    let block = if name == "ep" && is_terminal {
9046                        vec![Statement {
9047                            label: None,
9048                            kind: StmtKind::Expression(Expr {
9049                                kind: ExprKind::Say {
9050                                    handle: None,
9051                                    args: vec![Expr {
9052                                        kind: ExprKind::ScalarVar("_".into()),
9053                                        line,
9054                                    }],
9055                                },
9056                                line,
9057                            }),
9058                            line,
9059                        }]
9060                    } else {
9061                        let expr = self.parse_assign_expr_stop_at_pipe()?;
9062                        let expr = Self::lift_bareword_to_topic_call(expr);
9063                        vec![Statement {
9064                            label: None,
9065                            kind: StmtKind::Expression(expr),
9066                            line,
9067                        }]
9068                    };
9069                    let list = self.pipe_placeholder_list(line);
9070                    Ok(Expr {
9071                        kind: ExprKind::ForEachExpr {
9072                            block,
9073                            list: Box::new(list),
9074                        },
9075                        line,
9076                    })
9077                } else {
9078                    // `fore EXPR, LIST` — comma form
9079                    let expr = self.parse_assign_expr()?;
9080                    let expr = Self::lift_bareword_to_topic_call(expr);
9081                    self.expect(&Token::Comma)?;
9082                    let list_parts = self.parse_list_until_terminator()?;
9083                    let list_expr = if list_parts.len() == 1 {
9084                        list_parts.into_iter().next().unwrap()
9085                    } else {
9086                        Expr {
9087                            kind: ExprKind::List(list_parts),
9088                            line,
9089                        }
9090                    };
9091                    let block = vec![Statement {
9092                        label: None,
9093                        kind: StmtKind::Expression(expr),
9094                        line,
9095                    }];
9096                    Ok(Expr {
9097                        kind: ExprKind::ForEachExpr {
9098                            block,
9099                            list: Box::new(list_expr),
9100                        },
9101                        line,
9102                    })
9103                }
9104            }
9105            "rev" => {
9106                // `rev` — context-aware reverse: string in scalar, list in list context.
9107                // Defaults to $_ when no argument given.
9108                // Only use pipe placeholder when directly in pipe RHS (not inside a block).
9109                // RBrace means we're inside a block like `map { rev }` - use $_ default.
9110                let a = if self.in_pipe_rhs()
9111                    && matches!(
9112                        self.peek(),
9113                        Token::Semicolon | Token::RParen | Token::Eof | Token::PipeForward
9114                    ) {
9115                    self.pipe_placeholder_list(line)
9116                } else {
9117                    self.parse_one_arg_or_default()?
9118                };
9119                Ok(Expr {
9120                    kind: ExprKind::Rev(Box::new(a)),
9121                    line,
9122                })
9123            }
9124            "reverse" => {
9125                if crate::no_interop_mode() {
9126                    return Err(self.syntax_err(
9127                        "stryke uses `rev` instead of `reverse` (--no-interop)",
9128                        line,
9129                    ));
9130                }
9131                // On the RHS of `|>`, the operand is supplied by the piped LHS.
9132                let a = if self.in_pipe_rhs()
9133                    && matches!(
9134                        self.peek(),
9135                        Token::Semicolon
9136                            | Token::RBrace
9137                            | Token::RParen
9138                            | Token::Eof
9139                            | Token::PipeForward
9140                    ) {
9141                    self.pipe_placeholder_list(line)
9142                } else {
9143                    self.parse_one_arg()?
9144                };
9145                Ok(Expr {
9146                    kind: ExprKind::ReverseExpr(Box::new(a)),
9147                    line,
9148                })
9149            }
9150            "reversed" | "rv" => {
9151                // On the RHS of `|>`, the operand is supplied by the piped LHS.
9152                let a = if self.in_pipe_rhs()
9153                    && matches!(
9154                        self.peek(),
9155                        Token::Semicolon
9156                            | Token::RBrace
9157                            | Token::RParen
9158                            | Token::Eof
9159                            | Token::PipeForward
9160                    ) {
9161                    self.pipe_placeholder_list(line)
9162                } else {
9163                    self.parse_one_arg()?
9164                };
9165                Ok(Expr {
9166                    kind: ExprKind::Rev(Box::new(a)),
9167                    line,
9168                })
9169            }
9170            "join" => {
9171                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9172                    return Ok(e);
9173                }
9174                let args = self.parse_builtin_args()?;
9175                if args.is_empty() {
9176                    return Err(self.syntax_err("join requires separator and list", line));
9177                }
9178                // `@list |> join(",")` — list slot is filled by the piped LHS.
9179                if args.len() < 2 && !self.in_pipe_rhs() {
9180                    return Err(self.syntax_err("join requires separator and list", line));
9181                }
9182                Ok(Expr {
9183                    kind: ExprKind::JoinExpr {
9184                        separator: Box::new(args[0].clone()),
9185                        list: Box::new(Expr {
9186                            kind: ExprKind::List(args[1..].to_vec()),
9187                            line,
9188                        }),
9189                    },
9190                    line,
9191                })
9192            }
9193            "split" => {
9194                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9195                    return Ok(e);
9196                }
9197                let args = self.parse_builtin_args()?;
9198                let pattern = args.first().cloned().unwrap_or(Expr {
9199                    kind: ExprKind::String(" ".into()),
9200                    line,
9201                });
9202                let string = args.get(1).cloned().unwrap_or(Expr {
9203                    kind: ExprKind::ScalarVar("_".into()),
9204                    line,
9205                });
9206                let limit = args.get(2).cloned().map(Box::new);
9207                Ok(Expr {
9208                    kind: ExprKind::SplitExpr {
9209                        pattern: Box::new(pattern),
9210                        string: Box::new(string),
9211                        limit,
9212                    },
9213                    line,
9214                })
9215            }
9216            "substr" => {
9217                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9218                    return Ok(e);
9219                }
9220                let args = self.parse_builtin_args()?;
9221                Ok(Expr {
9222                    kind: ExprKind::Substr {
9223                        string: Box::new(args[0].clone()),
9224                        offset: Box::new(args[1].clone()),
9225                        length: args.get(2).cloned().map(Box::new),
9226                        replacement: args.get(3).cloned().map(Box::new),
9227                    },
9228                    line,
9229                })
9230            }
9231            "index" => {
9232                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9233                    return Ok(e);
9234                }
9235                let args = self.parse_builtin_args()?;
9236                Ok(Expr {
9237                    kind: ExprKind::Index {
9238                        string: Box::new(args[0].clone()),
9239                        substr: Box::new(args[1].clone()),
9240                        position: args.get(2).cloned().map(Box::new),
9241                    },
9242                    line,
9243                })
9244            }
9245            "rindex" => {
9246                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9247                    return Ok(e);
9248                }
9249                let args = self.parse_builtin_args()?;
9250                Ok(Expr {
9251                    kind: ExprKind::Rindex {
9252                        string: Box::new(args[0].clone()),
9253                        substr: Box::new(args[1].clone()),
9254                        position: args.get(2).cloned().map(Box::new),
9255                    },
9256                    line,
9257                })
9258            }
9259            "sprintf" => {
9260                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9261                    return Ok(e);
9262                }
9263                let args = self.parse_builtin_args()?;
9264                let (first, rest) = args
9265                    .split_first()
9266                    .ok_or_else(|| self.syntax_err("sprintf requires format", line))?;
9267                Ok(Expr {
9268                    kind: ExprKind::Sprintf {
9269                        format: Box::new(first.clone()),
9270                        args: rest.to_vec(),
9271                    },
9272                    line,
9273                })
9274            }
9275            "map" | "flat_map" | "maps" | "flat_maps" => {
9276                let flatten_array_refs = matches!(name.as_str(), "flat_map" | "flat_maps");
9277                let stream = matches!(name.as_str(), "maps" | "flat_maps");
9278                if matches!(self.peek(), Token::LBrace) {
9279                    let (block, list) = self.parse_block_list()?;
9280                    Ok(Expr {
9281                        kind: ExprKind::MapExpr {
9282                            block,
9283                            list: Box::new(list),
9284                            flatten_array_refs,
9285                            stream,
9286                        },
9287                        line,
9288                    })
9289                } else {
9290                    let expr = self.parse_assign_expr_stop_at_pipe()?;
9291                    // Lift bareword to FuncCall($_) so `map sha512, @list`
9292                    // calls sha512($_) for each element instead of stringifying.
9293                    let expr = Self::lift_bareword_to_topic_call(expr);
9294                    let list_expr = if self.in_pipe_rhs()
9295                        && matches!(
9296                            self.peek(),
9297                            Token::Semicolon
9298                                | Token::RBrace
9299                                | Token::RParen
9300                                | Token::Eof
9301                                | Token::PipeForward
9302                        ) {
9303                        self.pipe_placeholder_list(line)
9304                    } else {
9305                        self.expect(&Token::Comma)?;
9306                        let list_parts = self.parse_list_until_terminator()?;
9307                        if list_parts.len() == 1 {
9308                            list_parts.into_iter().next().unwrap()
9309                        } else {
9310                            Expr {
9311                                kind: ExprKind::List(list_parts),
9312                                line,
9313                            }
9314                        }
9315                    };
9316                    Ok(Expr {
9317                        kind: ExprKind::MapExprComma {
9318                            expr: Box::new(expr),
9319                            list: Box::new(list_expr),
9320                            flatten_array_refs,
9321                            stream,
9322                        },
9323                        line,
9324                    })
9325                }
9326            }
9327            "cond" => {
9328                if crate::compat_mode() {
9329                    return Err(self
9330                        .syntax_err("`cond` is a stryke extension (disabled by --compat)", line));
9331                }
9332                self.parse_cond_expr(line)
9333            }
9334            "match" => {
9335                if crate::compat_mode() {
9336                    return Err(self.syntax_err(
9337                        "algebraic `match` is a stryke extension (disabled by --compat)",
9338                        line,
9339                    ));
9340                }
9341                self.parse_algebraic_match_expr(line)
9342            }
9343            "grep" | "greps" | "filter" | "fi" | "find_all" => {
9344                let keyword = match name.as_str() {
9345                    "grep" => crate::ast::GrepBuiltinKeyword::Grep,
9346                    "greps" => crate::ast::GrepBuiltinKeyword::Greps,
9347                    "filter" | "fi" => crate::ast::GrepBuiltinKeyword::Filter,
9348                    "find_all" => crate::ast::GrepBuiltinKeyword::FindAll,
9349                    _ => unreachable!(),
9350                };
9351                if matches!(self.peek(), Token::LBrace) {
9352                    let (block, list) = self.parse_block_list()?;
9353                    Ok(Expr {
9354                        kind: ExprKind::GrepExpr {
9355                            block,
9356                            list: Box::new(list),
9357                            keyword,
9358                        },
9359                        line,
9360                    })
9361                } else {
9362                    let expr = self.parse_assign_expr_stop_at_pipe()?;
9363                    if self.in_pipe_rhs()
9364                        && matches!(
9365                            self.peek(),
9366                            Token::Semicolon
9367                                | Token::RBrace
9368                                | Token::RParen
9369                                | Token::Eof
9370                                | Token::PipeForward
9371                        )
9372                    {
9373                        // Pipe-RHS blockless form: `|> grep EXPR`
9374                        // For literals, desugar to `$_ eq/== EXPR` so
9375                        // `|> filter 't'` keeps only elements equal to 't'.
9376                        // For regexes, desugar to `$_ =~ EXPR`.
9377                        let list = self.pipe_placeholder_list(line);
9378                        let topic = Expr {
9379                            kind: ExprKind::ScalarVar("_".into()),
9380                            line,
9381                        };
9382                        let test = match &expr.kind {
9383                            ExprKind::Integer(_) | ExprKind::Float(_) => Expr {
9384                                kind: ExprKind::BinOp {
9385                                    op: BinOp::NumEq,
9386                                    left: Box::new(topic),
9387                                    right: Box::new(expr),
9388                                },
9389                                line,
9390                            },
9391                            ExprKind::String(_) | ExprKind::InterpolatedString(_) => Expr {
9392                                kind: ExprKind::BinOp {
9393                                    op: BinOp::StrEq,
9394                                    left: Box::new(topic),
9395                                    right: Box::new(expr),
9396                                },
9397                                line,
9398                            },
9399                            ExprKind::Regex { .. } => Expr {
9400                                kind: ExprKind::BinOp {
9401                                    op: BinOp::BindMatch,
9402                                    left: Box::new(topic),
9403                                    right: Box::new(expr),
9404                                },
9405                                line,
9406                            },
9407                            _ => {
9408                                // Non-literal (e.g. `defined`): lift bareword to call
9409                                Self::lift_bareword_to_topic_call(expr)
9410                            }
9411                        };
9412                        let block = vec![Statement {
9413                            label: None,
9414                            kind: StmtKind::Expression(test),
9415                            line,
9416                        }];
9417                        Ok(Expr {
9418                            kind: ExprKind::GrepExpr {
9419                                block,
9420                                list: Box::new(list),
9421                                keyword,
9422                            },
9423                            line,
9424                        })
9425                    } else {
9426                        let expr = Self::lift_bareword_to_topic_call(expr);
9427                        self.expect(&Token::Comma)?;
9428                        let list_parts = self.parse_list_until_terminator()?;
9429                        let list_expr = if list_parts.len() == 1 {
9430                            list_parts.into_iter().next().unwrap()
9431                        } else {
9432                            Expr {
9433                                kind: ExprKind::List(list_parts),
9434                                line,
9435                            }
9436                        };
9437                        Ok(Expr {
9438                            kind: ExprKind::GrepExprComma {
9439                                expr: Box::new(expr),
9440                                list: Box::new(list_expr),
9441                                keyword,
9442                            },
9443                            line,
9444                        })
9445                    }
9446                }
9447            }
9448            "sort" => {
9449                use crate::ast::SortComparator;
9450                if matches!(self.peek(), Token::LBrace) {
9451                    let block = self.parse_block()?;
9452                    let _ = self.eat(&Token::Comma);
9453                    let list = if self.in_pipe_rhs()
9454                        && matches!(
9455                            self.peek(),
9456                            Token::Semicolon
9457                                | Token::RBrace
9458                                | Token::RParen
9459                                | Token::Eof
9460                                | Token::PipeForward
9461                        ) {
9462                        self.pipe_placeholder_list(line)
9463                    } else {
9464                        self.parse_expression()?
9465                    };
9466                    Ok(Expr {
9467                        kind: ExprKind::SortExpr {
9468                            cmp: Some(SortComparator::Block(block)),
9469                            list: Box::new(list),
9470                        },
9471                        line,
9472                    })
9473                } else if matches!(self.peek(), Token::ScalarVar(ref v) if v == "a" || v == "b") {
9474                    // Blockless comparator: `sort $a <=> $b, @list`
9475                    let block = self.parse_block_or_bareword_cmp_block()?;
9476                    let _ = self.eat(&Token::Comma);
9477                    let list = if self.in_pipe_rhs()
9478                        && matches!(
9479                            self.peek(),
9480                            Token::Semicolon
9481                                | Token::RBrace
9482                                | Token::RParen
9483                                | Token::Eof
9484                                | Token::PipeForward
9485                        ) {
9486                        self.pipe_placeholder_list(line)
9487                    } else {
9488                        self.parse_expression()?
9489                    };
9490                    Ok(Expr {
9491                        kind: ExprKind::SortExpr {
9492                            cmp: Some(SortComparator::Block(block)),
9493                            list: Box::new(list),
9494                        },
9495                        line,
9496                    })
9497                } else if matches!(self.peek(), Token::ScalarVar(_)) {
9498                    // `sort $coderef (LIST)` — comparator is first; list often parenthesized
9499                    self.suppress_indirect_paren_call =
9500                        self.suppress_indirect_paren_call.saturating_add(1);
9501                    let code = self.parse_assign_expr()?;
9502                    self.suppress_indirect_paren_call =
9503                        self.suppress_indirect_paren_call.saturating_sub(1);
9504                    let list = if matches!(self.peek(), Token::LParen) {
9505                        self.advance();
9506                        let e = self.parse_expression()?;
9507                        self.expect(&Token::RParen)?;
9508                        e
9509                    } else {
9510                        self.parse_expression()?
9511                    };
9512                    Ok(Expr {
9513                        kind: ExprKind::SortExpr {
9514                            cmp: Some(SortComparator::Code(Box::new(code))),
9515                            list: Box::new(list),
9516                        },
9517                        line,
9518                    })
9519                } else if matches!(self.peek(), Token::Ident(ref name) if !Self::is_known_bareword(name))
9520                {
9521                    // Blockless comparator via bare sub name: `sort my_cmp @list`
9522                    let block = self.parse_block_or_bareword_cmp_block()?;
9523                    let _ = self.eat(&Token::Comma);
9524                    let list = if self.in_pipe_rhs()
9525                        && matches!(
9526                            self.peek(),
9527                            Token::Semicolon
9528                                | Token::RBrace
9529                                | Token::RParen
9530                                | Token::Eof
9531                                | Token::PipeForward
9532                        ) {
9533                        self.pipe_placeholder_list(line)
9534                    } else {
9535                        self.parse_expression()?
9536                    };
9537                    Ok(Expr {
9538                        kind: ExprKind::SortExpr {
9539                            cmp: Some(SortComparator::Block(block)),
9540                            list: Box::new(list),
9541                        },
9542                        line,
9543                    })
9544                } else {
9545                    // Bare `sort` with no comparator and no list: only allowed
9546                    // as the RHS of `|>`, where the list comes from the LHS.
9547                    let list = if self.in_pipe_rhs()
9548                        && matches!(
9549                            self.peek(),
9550                            Token::Semicolon
9551                                | Token::RBrace
9552                                | Token::RParen
9553                                | Token::Eof
9554                                | Token::PipeForward
9555                        ) {
9556                        self.pipe_placeholder_list(line)
9557                    } else {
9558                        self.parse_expression()?
9559                    };
9560                    Ok(Expr {
9561                        kind: ExprKind::SortExpr {
9562                            cmp: None,
9563                            list: Box::new(list),
9564                        },
9565                        line,
9566                    })
9567                }
9568            }
9569            "reduce" | "fold" | "inject" => {
9570                let (block, list) = self.parse_block_list()?;
9571                Ok(Expr {
9572                    kind: ExprKind::ReduceExpr {
9573                        block,
9574                        list: Box::new(list),
9575                    },
9576                    line,
9577                })
9578            }
9579            // Parallel extensions
9580            "pmap" => {
9581                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9582                Ok(Expr {
9583                    kind: ExprKind::PMapExpr {
9584                        block,
9585                        list: Box::new(list),
9586                        progress: progress.map(Box::new),
9587                        flat_outputs: false,
9588                        on_cluster: None,
9589                        stream: false,
9590                    },
9591                    line,
9592                })
9593            }
9594            "pmap_on" => {
9595                let (cluster, block, list, progress) =
9596                    self.parse_cluster_block_then_list_optional_progress()?;
9597                Ok(Expr {
9598                    kind: ExprKind::PMapExpr {
9599                        block,
9600                        list: Box::new(list),
9601                        progress: progress.map(Box::new),
9602                        flat_outputs: false,
9603                        on_cluster: Some(Box::new(cluster)),
9604                        stream: false,
9605                    },
9606                    line,
9607                })
9608            }
9609            "pflat_map" => {
9610                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9611                Ok(Expr {
9612                    kind: ExprKind::PMapExpr {
9613                        block,
9614                        list: Box::new(list),
9615                        progress: progress.map(Box::new),
9616                        flat_outputs: true,
9617                        on_cluster: None,
9618                        stream: false,
9619                    },
9620                    line,
9621                })
9622            }
9623            "pflat_map_on" => {
9624                let (cluster, block, list, progress) =
9625                    self.parse_cluster_block_then_list_optional_progress()?;
9626                Ok(Expr {
9627                    kind: ExprKind::PMapExpr {
9628                        block,
9629                        list: Box::new(list),
9630                        progress: progress.map(Box::new),
9631                        flat_outputs: true,
9632                        on_cluster: Some(Box::new(cluster)),
9633                        stream: false,
9634                    },
9635                    line,
9636                })
9637            }
9638            "pmaps" => {
9639                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9640                Ok(Expr {
9641                    kind: ExprKind::PMapExpr {
9642                        block,
9643                        list: Box::new(list),
9644                        progress: progress.map(Box::new),
9645                        flat_outputs: false,
9646                        on_cluster: None,
9647                        stream: true,
9648                    },
9649                    line,
9650                })
9651            }
9652            "pflat_maps" => {
9653                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9654                Ok(Expr {
9655                    kind: ExprKind::PMapExpr {
9656                        block,
9657                        list: Box::new(list),
9658                        progress: progress.map(Box::new),
9659                        flat_outputs: true,
9660                        on_cluster: None,
9661                        stream: true,
9662                    },
9663                    line,
9664                })
9665            }
9666            "pgreps" => {
9667                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9668                Ok(Expr {
9669                    kind: ExprKind::PGrepExpr {
9670                        block,
9671                        list: Box::new(list),
9672                        progress: progress.map(Box::new),
9673                        stream: true,
9674                    },
9675                    line,
9676                })
9677            }
9678            "pmap_chunked" => {
9679                let chunk_size = self.parse_assign_expr()?;
9680                let block = self.parse_block_or_bareword_block()?;
9681                self.eat(&Token::Comma);
9682                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
9683                Ok(Expr {
9684                    kind: ExprKind::PMapChunkedExpr {
9685                        chunk_size: Box::new(chunk_size),
9686                        block,
9687                        list: Box::new(list),
9688                        progress: progress.map(Box::new),
9689                    },
9690                    line,
9691                })
9692            }
9693            "pgrep" => {
9694                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9695                Ok(Expr {
9696                    kind: ExprKind::PGrepExpr {
9697                        block,
9698                        list: Box::new(list),
9699                        progress: progress.map(Box::new),
9700                        stream: false,
9701                    },
9702                    line,
9703                })
9704            }
9705            "pfor" => {
9706                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
9707                Ok(Expr {
9708                    kind: ExprKind::PForExpr {
9709                        block,
9710                        list: Box::new(list),
9711                        progress: progress.map(Box::new),
9712                    },
9713                    line,
9714                })
9715            }
9716            "par_lines" | "par_walk" => {
9717                let args = self.parse_builtin_args()?;
9718                if args.len() < 2 {
9719                    return Err(
9720                        self.syntax_err(format!("{} requires at least two arguments", name), line)
9721                    );
9722                }
9723
9724                if name == "par_lines" {
9725                    Ok(Expr {
9726                        kind: ExprKind::ParLinesExpr {
9727                            path: Box::new(args[0].clone()),
9728                            callback: Box::new(args[1].clone()),
9729                            progress: None,
9730                        },
9731                        line,
9732                    })
9733                } else {
9734                    Ok(Expr {
9735                        kind: ExprKind::ParWalkExpr {
9736                            path: Box::new(args[0].clone()),
9737                            callback: Box::new(args[1].clone()),
9738                            progress: None,
9739                        },
9740                        line,
9741                    })
9742                }
9743            }
9744            "pwatch" | "watch" => {
9745                let args = self.parse_builtin_args()?;
9746                if args.len() < 2 {
9747                    return Err(
9748                        self.syntax_err(format!("{} requires at least two arguments", name), line)
9749                    );
9750                }
9751                Ok(Expr {
9752                    kind: ExprKind::PwatchExpr {
9753                        path: Box::new(args[0].clone()),
9754                        callback: Box::new(args[1].clone()),
9755                    },
9756                    line,
9757                })
9758            }
9759            "fan" => {
9760                // fan { BLOCK }            — no count, block body
9761                // fan COUNT { BLOCK }      — count + block body
9762                // fan EXPR;                — no count, blockless body (wrap EXPR as block)
9763                // fan COUNT EXPR;          — count + blockless body
9764                // Optional: `, progress => EXPR` or `progress => EXPR` (no comma before progress)
9765                let (count, block) = self.parse_fan_count_and_block(line)?;
9766                let progress = self.parse_fan_optional_progress("fan")?;
9767                Ok(Expr {
9768                    kind: ExprKind::FanExpr {
9769                        count,
9770                        block,
9771                        progress,
9772                        capture: false,
9773                    },
9774                    line,
9775                })
9776            }
9777            "fan_cap" => {
9778                let (count, block) = self.parse_fan_count_and_block(line)?;
9779                let progress = self.parse_fan_optional_progress("fan_cap")?;
9780                Ok(Expr {
9781                    kind: ExprKind::FanExpr {
9782                        count,
9783                        block,
9784                        progress,
9785                        capture: true,
9786                    },
9787                    line,
9788                })
9789            }
9790            "async" => {
9791                if !matches!(self.peek(), Token::LBrace) {
9792                    return Err(self.syntax_err("async must be followed by { BLOCK }", line));
9793                }
9794                let block = self.parse_block()?;
9795                Ok(Expr {
9796                    kind: ExprKind::AsyncBlock { body: block },
9797                    line,
9798                })
9799            }
9800            "spawn" => {
9801                if !matches!(self.peek(), Token::LBrace) {
9802                    return Err(self.syntax_err("spawn must be followed by { BLOCK }", line));
9803                }
9804                let block = self.parse_block()?;
9805                Ok(Expr {
9806                    kind: ExprKind::SpawnBlock { body: block },
9807                    line,
9808                })
9809            }
9810            "trace" => {
9811                if !matches!(self.peek(), Token::LBrace) {
9812                    return Err(self.syntax_err("trace must be followed by { BLOCK }", line));
9813                }
9814                let block = self.parse_block()?;
9815                Ok(Expr {
9816                    kind: ExprKind::Trace { body: block },
9817                    line,
9818                })
9819            }
9820            "timer" => {
9821                let block = self.parse_block_or_bareword_block_no_args()?;
9822                Ok(Expr {
9823                    kind: ExprKind::Timer { body: block },
9824                    line,
9825                })
9826            }
9827            "bench" => {
9828                let block = self.parse_block_or_bareword_block_no_args()?;
9829                let times = Box::new(self.parse_expression()?);
9830                Ok(Expr {
9831                    kind: ExprKind::Bench { body: block, times },
9832                    line,
9833                })
9834            }
9835            "spinner" => {
9836                // `spinner "msg" { BLOCK }` or `spinner { BLOCK }`
9837                let (message, body) = if matches!(self.peek(), Token::LBrace) {
9838                    let body = self.parse_block()?;
9839                    (
9840                        Box::new(Expr {
9841                            kind: ExprKind::String("working".to_string()),
9842                            line,
9843                        }),
9844                        body,
9845                    )
9846                } else {
9847                    let msg = self.parse_assign_expr()?;
9848                    let body = self.parse_block()?;
9849                    (Box::new(msg), body)
9850                };
9851                Ok(Expr {
9852                    kind: ExprKind::Spinner { message, body },
9853                    line,
9854                })
9855            }
9856            "thread" | "t" => {
9857                // `thread EXPR stage1 stage2 ...` — threading macro (thread-first)
9858                // `t` is a short alias for `thread`
9859                // Each stage is either:
9860                //   - `ident` — bare function call
9861                //   - `ident { block }` — function with block arg
9862                //   - `ident arg1 arg2 { block }` — function with args and optional block
9863                //   - `fn { block }` — standalone anonymous block
9864                //   - `>{ block }` — shorthand for standalone anonymous block
9865                // Desugars to: EXPR |> stage1 |> stage2 |> ...
9866                self.parse_thread_macro(line, false)
9867            }
9868            "retry" => {
9869                // `retry { BLOCK }` or `retry BAREWORD` — bareword becomes zero-arg call.
9870                // An optional comma before `times` is allowed in both forms.
9871                let body = if matches!(self.peek(), Token::LBrace) {
9872                    self.parse_block()?
9873                } else {
9874                    let bw_line = self.peek_line();
9875                    let Token::Ident(ref name) = self.peek().clone() else {
9876                        return Err(self
9877                            .syntax_err("retry: expected block or bareword function name", line));
9878                    };
9879                    let name = name.clone();
9880                    self.advance();
9881                    vec![Statement::new(
9882                        StmtKind::Expression(Expr {
9883                            kind: ExprKind::FuncCall { name, args: vec![] },
9884                            line: bw_line,
9885                        }),
9886                        bw_line,
9887                    )]
9888                };
9889                self.eat(&Token::Comma);
9890                match self.peek() {
9891                    Token::Ident(ref s) if s == "times" => {
9892                        self.advance();
9893                    }
9894                    _ => {
9895                        return Err(self.syntax_err("retry: expected `times =>` after block", line));
9896                    }
9897                }
9898                self.expect(&Token::FatArrow)?;
9899                let times = Box::new(self.parse_assign_expr()?);
9900                let mut backoff = RetryBackoff::None;
9901                if self.eat(&Token::Comma) {
9902                    match self.peek() {
9903                        Token::Ident(ref s) if s == "backoff" => {
9904                            self.advance();
9905                        }
9906                        _ => {
9907                            return Err(
9908                                self.syntax_err("retry: expected `backoff =>` after comma", line)
9909                            );
9910                        }
9911                    }
9912                    self.expect(&Token::FatArrow)?;
9913                    let Token::Ident(mode) = self.peek().clone() else {
9914                        return Err(self.syntax_err(
9915                            "retry: expected backoff mode (none, linear, exponential)",
9916                            line,
9917                        ));
9918                    };
9919                    backoff = match mode.as_str() {
9920                        "none" => RetryBackoff::None,
9921                        "linear" => RetryBackoff::Linear,
9922                        "exponential" => RetryBackoff::Exponential,
9923                        _ => {
9924                            return Err(
9925                                self.syntax_err(format!("retry: invalid backoff `{mode}`"), line)
9926                            );
9927                        }
9928                    };
9929                    self.advance();
9930                }
9931                Ok(Expr {
9932                    kind: ExprKind::RetryBlock {
9933                        body,
9934                        times,
9935                        backoff,
9936                    },
9937                    line,
9938                })
9939            }
9940            "rate_limit" => {
9941                self.expect(&Token::LParen)?;
9942                let max = Box::new(self.parse_assign_expr()?);
9943                self.expect(&Token::Comma)?;
9944                let window = Box::new(self.parse_assign_expr()?);
9945                self.expect(&Token::RParen)?;
9946                let body = self.parse_block_or_bareword_block_no_args()?;
9947                let slot = self.alloc_rate_limit_slot();
9948                Ok(Expr {
9949                    kind: ExprKind::RateLimitBlock {
9950                        slot,
9951                        max,
9952                        window,
9953                        body,
9954                    },
9955                    line,
9956                })
9957            }
9958            "every" => {
9959                // `every("500ms") { BLOCK }` or `every "500ms" BODY` — parens optional.
9960                // Body consumes `|>` (every is an infinite loop, not a pipeable source).
9961                let has_paren = self.eat(&Token::LParen);
9962                let interval = Box::new(self.parse_assign_expr()?);
9963                if has_paren {
9964                    self.expect(&Token::RParen)?;
9965                }
9966                let body = if matches!(self.peek(), Token::LBrace) {
9967                    self.parse_block()?
9968                } else {
9969                    let bline = self.peek_line();
9970                    let expr = self.parse_assign_expr()?;
9971                    vec![Statement::new(StmtKind::Expression(expr), bline)]
9972                };
9973                Ok(Expr {
9974                    kind: ExprKind::EveryBlock { interval, body },
9975                    line,
9976                })
9977            }
9978            "gen" => {
9979                if !matches!(self.peek(), Token::LBrace) {
9980                    return Err(self.syntax_err("gen must be followed by { BLOCK }", line));
9981                }
9982                let body = self.parse_block()?;
9983                Ok(Expr {
9984                    kind: ExprKind::GenBlock { body },
9985                    line,
9986                })
9987            }
9988            "yield" => {
9989                let e = self.parse_assign_expr()?;
9990                Ok(Expr {
9991                    kind: ExprKind::Yield(Box::new(e)),
9992                    line,
9993                })
9994            }
9995            "await" => {
9996                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9997                    return Ok(e);
9998                }
9999                // `await` defaults to `$_` so `map { await } @tasks` works
10000                // (Perl-style topic-defaulting unary).
10001                let a = self.parse_one_arg_or_default()?;
10002                Ok(Expr {
10003                    kind: ExprKind::Await(Box::new(a)),
10004                    line,
10005                })
10006            }
10007            "slurp" | "cat" | "c" => {
10008                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10009                    return Ok(e);
10010                }
10011                let a = self.parse_one_arg_or_default()?;
10012                Ok(Expr {
10013                    kind: ExprKind::Slurp(Box::new(a)),
10014                    line,
10015                })
10016            }
10017            "capture" => {
10018                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10019                    return Ok(e);
10020                }
10021                let a = self.parse_one_arg()?;
10022                Ok(Expr {
10023                    kind: ExprKind::Capture(Box::new(a)),
10024                    line,
10025                })
10026            }
10027            "fetch_url" => {
10028                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10029                    return Ok(e);
10030                }
10031                let a = self.parse_one_arg()?;
10032                Ok(Expr {
10033                    kind: ExprKind::FetchUrl(Box::new(a)),
10034                    line,
10035                })
10036            }
10037            "pchannel" => {
10038                let capacity = if self.eat(&Token::LParen) {
10039                    if matches!(self.peek(), Token::RParen) {
10040                        self.advance();
10041                        None
10042                    } else {
10043                        let e = self.parse_expression()?;
10044                        self.expect(&Token::RParen)?;
10045                        Some(Box::new(e))
10046                    }
10047                } else {
10048                    None
10049                };
10050                Ok(Expr {
10051                    kind: ExprKind::Pchannel { capacity },
10052                    line,
10053                })
10054            }
10055            "psort" => {
10056                if matches!(self.peek(), Token::LBrace)
10057                    || matches!(self.peek(), Token::ScalarVar(ref v) if v == "a" || v == "b")
10058                    || matches!(self.peek(), Token::Ident(ref name) if !Self::is_known_bareword(name))
10059                {
10060                    let block = self.parse_block_or_bareword_cmp_block()?;
10061                    self.eat(&Token::Comma);
10062                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10063                    Ok(Expr {
10064                        kind: ExprKind::PSortExpr {
10065                            cmp: Some(block),
10066                            list: Box::new(list),
10067                            progress: progress.map(Box::new),
10068                        },
10069                        line,
10070                    })
10071                } else {
10072                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10073                    Ok(Expr {
10074                        kind: ExprKind::PSortExpr {
10075                            cmp: None,
10076                            list: Box::new(list),
10077                            progress: progress.map(Box::new),
10078                        },
10079                        line,
10080                    })
10081                }
10082            }
10083            "preduce" => {
10084                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10085                Ok(Expr {
10086                    kind: ExprKind::PReduceExpr {
10087                        block,
10088                        list: Box::new(list),
10089                        progress: progress.map(Box::new),
10090                    },
10091                    line,
10092                })
10093            }
10094            "preduce_init" => {
10095                let (init, block, list, progress) =
10096                    self.parse_init_block_then_list_optional_progress()?;
10097                Ok(Expr {
10098                    kind: ExprKind::PReduceInitExpr {
10099                        init: Box::new(init),
10100                        block,
10101                        list: Box::new(list),
10102                        progress: progress.map(Box::new),
10103                    },
10104                    line,
10105                })
10106            }
10107            "pmap_reduce" => {
10108                let map_block = self.parse_block_or_bareword_block()?;
10109                // After the map block, expect either a `{ REDUCE }` block, or
10110                // after an eaten comma, a blockless reduce expr (`$a + $b`).
10111                let reduce_block = if matches!(self.peek(), Token::LBrace) {
10112                    self.parse_block()?
10113                } else {
10114                    // comma separates blockless map from blockless reduce
10115                    self.expect(&Token::Comma)?;
10116                    self.parse_block_or_bareword_cmp_block()?
10117                };
10118                self.eat(&Token::Comma);
10119                let line = self.peek_line();
10120                if let Token::Ident(ref kw) = self.peek().clone() {
10121                    if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
10122                        self.advance();
10123                        self.expect(&Token::FatArrow)?;
10124                        let prog = self.parse_assign_expr()?;
10125                        return Ok(Expr {
10126                            kind: ExprKind::PMapReduceExpr {
10127                                map_block,
10128                                reduce_block,
10129                                list: Box::new(Expr {
10130                                    kind: ExprKind::List(vec![]),
10131                                    line,
10132                                }),
10133                                progress: Some(Box::new(prog)),
10134                            },
10135                            line,
10136                        });
10137                    }
10138                }
10139                if matches!(
10140                    self.peek(),
10141                    Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
10142                ) {
10143                    return Ok(Expr {
10144                        kind: ExprKind::PMapReduceExpr {
10145                            map_block,
10146                            reduce_block,
10147                            list: Box::new(Expr {
10148                                kind: ExprKind::List(vec![]),
10149                                line,
10150                            }),
10151                            progress: None,
10152                        },
10153                        line,
10154                    });
10155                }
10156                let mut parts = vec![self.parse_assign_expr()?];
10157                loop {
10158                    if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
10159                        break;
10160                    }
10161                    if matches!(
10162                        self.peek(),
10163                        Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
10164                    ) {
10165                        break;
10166                    }
10167                    if let Token::Ident(ref kw) = self.peek().clone() {
10168                        if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
10169                            self.advance();
10170                            self.expect(&Token::FatArrow)?;
10171                            let prog = self.parse_assign_expr()?;
10172                            return Ok(Expr {
10173                                kind: ExprKind::PMapReduceExpr {
10174                                    map_block,
10175                                    reduce_block,
10176                                    list: Box::new(merge_expr_list(parts)),
10177                                    progress: Some(Box::new(prog)),
10178                                },
10179                                line,
10180                            });
10181                        }
10182                    }
10183                    parts.push(self.parse_assign_expr()?);
10184                }
10185                Ok(Expr {
10186                    kind: ExprKind::PMapReduceExpr {
10187                        map_block,
10188                        reduce_block,
10189                        list: Box::new(merge_expr_list(parts)),
10190                        progress: None,
10191                    },
10192                    line,
10193                })
10194            }
10195            "puniq" => {
10196                if self.pipe_supplies_slurped_list_operand() {
10197                    return Ok(Expr {
10198                        kind: ExprKind::FuncCall {
10199                            name: "puniq".to_string(),
10200                            args: vec![],
10201                        },
10202                        line,
10203                    });
10204                }
10205                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10206                let mut args = vec![list];
10207                if let Some(p) = progress {
10208                    args.push(p);
10209                }
10210                Ok(Expr {
10211                    kind: ExprKind::FuncCall {
10212                        name: "puniq".to_string(),
10213                        args,
10214                    },
10215                    line,
10216                })
10217            }
10218            "pfirst" => {
10219                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10220                let cr = Expr {
10221                    kind: ExprKind::CodeRef {
10222                        params: vec![],
10223                        body: block,
10224                    },
10225                    line,
10226                };
10227                let mut args = vec![cr, list];
10228                if let Some(p) = progress {
10229                    args.push(p);
10230                }
10231                Ok(Expr {
10232                    kind: ExprKind::FuncCall {
10233                        name: "pfirst".to_string(),
10234                        args,
10235                    },
10236                    line,
10237                })
10238            }
10239            "pany" => {
10240                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10241                let cr = Expr {
10242                    kind: ExprKind::CodeRef {
10243                        params: vec![],
10244                        body: block,
10245                    },
10246                    line,
10247                };
10248                let mut args = vec![cr, list];
10249                if let Some(p) = progress {
10250                    args.push(p);
10251                }
10252                Ok(Expr {
10253                    kind: ExprKind::FuncCall {
10254                        name: "pany".to_string(),
10255                        args,
10256                    },
10257                    line,
10258                })
10259            }
10260            "uniq" | "distinct" => {
10261                if self.pipe_supplies_slurped_list_operand() {
10262                    return Ok(Expr {
10263                        kind: ExprKind::FuncCall {
10264                            name: name.clone(),
10265                            args: vec![],
10266                        },
10267                        line,
10268                    });
10269                }
10270                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10271                if progress.is_some() {
10272                    return Err(self.syntax_err(
10273                        "`progress =>` is not supported for uniq (use puniq for parallel + progress)",
10274                        line,
10275                    ));
10276                }
10277                Ok(Expr {
10278                    kind: ExprKind::FuncCall {
10279                        name: name.clone(),
10280                        args: vec![list],
10281                    },
10282                    line,
10283                })
10284            }
10285            "flatten" => {
10286                if self.pipe_supplies_slurped_list_operand() {
10287                    return Ok(Expr {
10288                        kind: ExprKind::FuncCall {
10289                            name: "flatten".to_string(),
10290                            args: vec![],
10291                        },
10292                        line,
10293                    });
10294                }
10295                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10296                if progress.is_some() {
10297                    return Err(self.syntax_err("`progress =>` is not supported for flatten", line));
10298                }
10299                Ok(Expr {
10300                    kind: ExprKind::FuncCall {
10301                        name: "flatten".to_string(),
10302                        args: vec![list],
10303                    },
10304                    line,
10305                })
10306            }
10307            "set" => {
10308                if self.pipe_supplies_slurped_list_operand() {
10309                    return Ok(Expr {
10310                        kind: ExprKind::FuncCall {
10311                            name: "set".to_string(),
10312                            args: vec![],
10313                        },
10314                        line,
10315                    });
10316                }
10317                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10318                if progress.is_some() {
10319                    return Err(self.syntax_err("`progress =>` is not supported for set", line));
10320                }
10321                Ok(Expr {
10322                    kind: ExprKind::FuncCall {
10323                        name: "set".to_string(),
10324                        args: vec![list],
10325                    },
10326                    line,
10327                })
10328            }
10329            // `size` is the file-size builtin (Perl `-s`), not a list-count alias.
10330            // Defaults to `$_` when no arg is given, like `length`. See
10331            // `builtin_file_size` in builtins.rs for the runtime behavior.
10332            "size" => {
10333                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10334                    return Ok(e);
10335                }
10336                if self.pipe_supplies_slurped_list_operand() {
10337                    return Ok(Expr {
10338                        kind: ExprKind::FuncCall {
10339                            name: "size".to_string(),
10340                            args: vec![],
10341                        },
10342                        line,
10343                    });
10344                }
10345                let a = self.parse_one_arg_or_default()?;
10346                Ok(Expr {
10347                    kind: ExprKind::FuncCall {
10348                        name: "size".to_string(),
10349                        args: vec![a],
10350                    },
10351                    line,
10352                })
10353            }
10354            "list_count" | "list_size" | "count" | "len" | "cnt" => {
10355                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10356                    return Ok(e);
10357                }
10358                if self.pipe_supplies_slurped_list_operand() {
10359                    return Ok(Expr {
10360                        kind: ExprKind::FuncCall {
10361                            name: name.clone(),
10362                            args: vec![],
10363                        },
10364                        line,
10365                    });
10366                }
10367                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10368                if progress.is_some() {
10369                    return Err(self.syntax_err(
10370                        "`progress =>` is not supported for list_count / list_size / count / cnt",
10371                        line,
10372                    ));
10373                }
10374                Ok(Expr {
10375                    kind: ExprKind::FuncCall {
10376                        name: name.clone(),
10377                        args: vec![list],
10378                    },
10379                    line,
10380                })
10381            }
10382            "shuffle" | "shuffled" => {
10383                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10384                    return Ok(e);
10385                }
10386                if self.pipe_supplies_slurped_list_operand() {
10387                    return Ok(Expr {
10388                        kind: ExprKind::FuncCall {
10389                            name: "shuffle".to_string(),
10390                            args: vec![],
10391                        },
10392                        line,
10393                    });
10394                }
10395                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10396                if progress.is_some() {
10397                    return Err(self.syntax_err("`progress =>` is not supported for shuffle", line));
10398                }
10399                Ok(Expr {
10400                    kind: ExprKind::FuncCall {
10401                        name: "shuffle".to_string(),
10402                        args: vec![list],
10403                    },
10404                    line,
10405                })
10406            }
10407            "chunked" => {
10408                let mut parts = Vec::new();
10409                if self.eat(&Token::LParen) {
10410                    if !matches!(self.peek(), Token::RParen) {
10411                        parts.push(self.parse_assign_expr()?);
10412                        while self.eat(&Token::Comma) {
10413                            if matches!(self.peek(), Token::RParen) {
10414                                break;
10415                            }
10416                            parts.push(self.parse_assign_expr()?);
10417                        }
10418                    }
10419                    self.expect(&Token::RParen)?;
10420                } else {
10421                    // Paren-less `chunked N`: `|>` is a hard terminator, not
10422                    // an operator inside the arg (see
10423                    // `parse_assign_expr_stop_at_pipe`).
10424                    parts.push(self.parse_assign_expr_stop_at_pipe()?);
10425                    loop {
10426                        if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
10427                            break;
10428                        }
10429                        if matches!(
10430                            self.peek(),
10431                            Token::Semicolon
10432                                | Token::RBrace
10433                                | Token::RParen
10434                                | Token::Eof
10435                                | Token::PipeForward
10436                        ) {
10437                            break;
10438                        }
10439                        if self.peek_is_postfix_stmt_modifier_keyword() {
10440                            break;
10441                        }
10442                        parts.push(self.parse_assign_expr_stop_at_pipe()?);
10443                    }
10444                }
10445                if parts.len() == 1 {
10446                    let n = parts.pop().unwrap();
10447                    return Ok(Expr {
10448                        kind: ExprKind::FuncCall {
10449                            name: "chunked".to_string(),
10450                            args: vec![n],
10451                        },
10452                        line,
10453                    });
10454                }
10455                if parts.is_empty() {
10456                    return Ok(Expr {
10457                        kind: ExprKind::FuncCall {
10458                            name: "chunked".to_string(),
10459                            args: parts,
10460                        },
10461                        line,
10462                    });
10463                }
10464                if parts.len() == 2 {
10465                    let n = parts.pop().unwrap();
10466                    let list = parts.pop().unwrap();
10467                    return Ok(Expr {
10468                        kind: ExprKind::FuncCall {
10469                            name: "chunked".to_string(),
10470                            args: vec![list, n],
10471                        },
10472                        line,
10473                    });
10474                }
10475                Err(self.syntax_err(
10476                    "chunked: use LIST |> chunked(N) or chunked((1,2,3), 2)",
10477                    line,
10478                ))
10479            }
10480            "windowed" => {
10481                let mut parts = Vec::new();
10482                if self.eat(&Token::LParen) {
10483                    if !matches!(self.peek(), Token::RParen) {
10484                        parts.push(self.parse_assign_expr()?);
10485                        while self.eat(&Token::Comma) {
10486                            if matches!(self.peek(), Token::RParen) {
10487                                break;
10488                            }
10489                            parts.push(self.parse_assign_expr()?);
10490                        }
10491                    }
10492                    self.expect(&Token::RParen)?;
10493                } else {
10494                    // Paren-less `windowed N`: same `|>`-terminator rule as
10495                    // `chunked` above.
10496                    parts.push(self.parse_assign_expr_stop_at_pipe()?);
10497                    loop {
10498                        if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
10499                            break;
10500                        }
10501                        if matches!(
10502                            self.peek(),
10503                            Token::Semicolon
10504                                | Token::RBrace
10505                                | Token::RParen
10506                                | Token::Eof
10507                                | Token::PipeForward
10508                        ) {
10509                            break;
10510                        }
10511                        if self.peek_is_postfix_stmt_modifier_keyword() {
10512                            break;
10513                        }
10514                        parts.push(self.parse_assign_expr_stop_at_pipe()?);
10515                    }
10516                }
10517                if parts.len() == 1 {
10518                    let n = parts.pop().unwrap();
10519                    return Ok(Expr {
10520                        kind: ExprKind::FuncCall {
10521                            name: "windowed".to_string(),
10522                            args: vec![n],
10523                        },
10524                        line,
10525                    });
10526                }
10527                if parts.is_empty() {
10528                    return Ok(Expr {
10529                        kind: ExprKind::FuncCall {
10530                            name: "windowed".to_string(),
10531                            args: parts,
10532                        },
10533                        line,
10534                    });
10535                }
10536                if parts.len() == 2 {
10537                    let n = parts.pop().unwrap();
10538                    let list = parts.pop().unwrap();
10539                    return Ok(Expr {
10540                        kind: ExprKind::FuncCall {
10541                            name: "windowed".to_string(),
10542                            args: vec![list, n],
10543                        },
10544                        line,
10545                    });
10546                }
10547                Err(self.syntax_err(
10548                    "windowed: use LIST |> windowed(N) or windowed((1,2,3), 2)",
10549                    line,
10550                ))
10551            }
10552            "any" | "all" | "none" => {
10553                // `any(CODEREF, LIST)` with parens — parse as normal call.
10554                if matches!(self.peek(), Token::LParen) {
10555                    self.advance();
10556                    let args = self.parse_arg_list()?;
10557                    self.expect(&Token::RParen)?;
10558                    return Ok(Expr {
10559                        kind: ExprKind::FuncCall {
10560                            name: name.clone(),
10561                            args,
10562                        },
10563                        line,
10564                    });
10565                }
10566                // `any BLOCK LIST` without parens.
10567                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10568                if progress.is_some() {
10569                    return Err(self.syntax_err(
10570                        "`progress =>` is not supported for any/all/none (use pany for parallel + progress)",
10571                        line,
10572                    ));
10573                }
10574                let cr = Expr {
10575                    kind: ExprKind::CodeRef {
10576                        params: vec![],
10577                        body: block,
10578                    },
10579                    line,
10580                };
10581                Ok(Expr {
10582                    kind: ExprKind::FuncCall {
10583                        name: name.clone(),
10584                        args: vec![cr, list],
10585                    },
10586                    line,
10587                })
10588            }
10589            // Ruby `detect` / `find` — same as `first` (first element matching block).
10590            "first" | "detect" | "find" => {
10591                // `first(CODEREF, LIST)` with parens — parse as normal call.
10592                if matches!(self.peek(), Token::LParen) {
10593                    self.advance();
10594                    let args = self.parse_arg_list()?;
10595                    self.expect(&Token::RParen)?;
10596                    return Ok(Expr {
10597                        kind: ExprKind::FuncCall {
10598                            name: "first".to_string(),
10599                            args,
10600                        },
10601                        line,
10602                    });
10603                }
10604                // `first BLOCK LIST` without parens.
10605                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10606                if progress.is_some() {
10607                    return Err(self.syntax_err(
10608                        "`progress =>` is not supported for first/detect/find (use pfirst for parallel + progress)",
10609                        line,
10610                    ));
10611                }
10612                let cr = Expr {
10613                    kind: ExprKind::CodeRef {
10614                        params: vec![],
10615                        body: block,
10616                    },
10617                    line,
10618                };
10619                Ok(Expr {
10620                    kind: ExprKind::FuncCall {
10621                        name: "first".to_string(),
10622                        args: vec![cr, list],
10623                    },
10624                    line,
10625                })
10626            }
10627            "take_while" | "drop_while" | "skip_while" | "reject" | "tap" | "peek"
10628            | "partition" | "min_by" | "max_by" | "zip_with" | "count_by" => {
10629                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10630                if progress.is_some() {
10631                    return Err(
10632                        self.syntax_err(format!("`progress =>` is not supported for {name}"), line)
10633                    );
10634                }
10635                let cr = Expr {
10636                    kind: ExprKind::CodeRef {
10637                        params: vec![],
10638                        body: block,
10639                    },
10640                    line,
10641                };
10642                Ok(Expr {
10643                    kind: ExprKind::FuncCall {
10644                        name: name.to_string(),
10645                        args: vec![cr, list],
10646                    },
10647                    line,
10648                })
10649            }
10650            "group_by" | "chunk_by" => {
10651                if matches!(self.peek(), Token::LBrace) {
10652                    let (block, list) = self.parse_block_list()?;
10653                    let cr = Expr {
10654                        kind: ExprKind::CodeRef {
10655                            params: vec![],
10656                            body: block,
10657                        },
10658                        line,
10659                    };
10660                    Ok(Expr {
10661                        kind: ExprKind::FuncCall {
10662                            name: name.to_string(),
10663                            args: vec![cr, list],
10664                        },
10665                        line,
10666                    })
10667                } else {
10668                    let key_expr = self.parse_assign_expr()?;
10669                    self.expect(&Token::Comma)?;
10670                    let list_parts = self.parse_list_until_terminator()?;
10671                    let list_expr = if list_parts.len() == 1 {
10672                        list_parts.into_iter().next().unwrap()
10673                    } else {
10674                        Expr {
10675                            kind: ExprKind::List(list_parts),
10676                            line,
10677                        }
10678                    };
10679                    Ok(Expr {
10680                        kind: ExprKind::FuncCall {
10681                            name: name.to_string(),
10682                            args: vec![key_expr, list_expr],
10683                        },
10684                        line,
10685                    })
10686                }
10687            }
10688            "with_index" => {
10689                if self.pipe_supplies_slurped_list_operand() {
10690                    return Ok(Expr {
10691                        kind: ExprKind::FuncCall {
10692                            name: "with_index".to_string(),
10693                            args: vec![],
10694                        },
10695                        line,
10696                    });
10697                }
10698                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10699                if progress.is_some() {
10700                    return Err(
10701                        self.syntax_err("`progress =>` is not supported for with_index", line)
10702                    );
10703                }
10704                Ok(Expr {
10705                    kind: ExprKind::FuncCall {
10706                        name: "with_index".to_string(),
10707                        args: vec![list],
10708                    },
10709                    line,
10710                })
10711            }
10712            "pcache" => {
10713                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10714                Ok(Expr {
10715                    kind: ExprKind::PcacheExpr {
10716                        block,
10717                        list: Box::new(list),
10718                        progress: progress.map(Box::new),
10719                    },
10720                    line,
10721                })
10722            }
10723            "pselect" => {
10724                let paren = self.eat(&Token::LParen);
10725                let (receivers, timeout) = self.parse_comma_expr_list_with_timeout_tail(paren)?;
10726                if paren {
10727                    self.expect(&Token::RParen)?;
10728                }
10729                if receivers.is_empty() {
10730                    return Err(self.syntax_err("pselect needs at least one receiver", line));
10731                }
10732                Ok(Expr {
10733                    kind: ExprKind::PselectExpr {
10734                        receivers,
10735                        timeout: timeout.map(Box::new),
10736                    },
10737                    line,
10738                })
10739            }
10740            "open" => {
10741                let paren = matches!(self.peek(), Token::LParen);
10742                if paren {
10743                    self.advance();
10744                }
10745                if matches!(self.peek(), Token::Ident(ref s) if s == "my") {
10746                    self.advance();
10747                    let name = self.parse_scalar_var_name()?;
10748                    self.expect(&Token::Comma)?;
10749                    let mode = self.parse_assign_expr()?;
10750                    let file = if self.eat(&Token::Comma) {
10751                        Some(self.parse_assign_expr()?)
10752                    } else {
10753                        None
10754                    };
10755                    if paren {
10756                        self.expect(&Token::RParen)?;
10757                    }
10758                    Ok(Expr {
10759                        kind: ExprKind::Open {
10760                            handle: Box::new(Expr {
10761                                kind: ExprKind::OpenMyHandle { name },
10762                                line,
10763                            }),
10764                            mode: Box::new(mode),
10765                            file: file.map(Box::new),
10766                        },
10767                        line,
10768                    })
10769                } else {
10770                    let args = if paren {
10771                        self.parse_arg_list()?
10772                    } else {
10773                        self.parse_list_until_terminator()?
10774                    };
10775                    if paren {
10776                        self.expect(&Token::RParen)?;
10777                    }
10778                    if args.len() < 2 {
10779                        return Err(self.syntax_err("open requires at least 2 arguments", line));
10780                    }
10781                    Ok(Expr {
10782                        kind: ExprKind::Open {
10783                            handle: Box::new(args[0].clone()),
10784                            mode: Box::new(args[1].clone()),
10785                            file: args.get(2).cloned().map(Box::new),
10786                        },
10787                        line,
10788                    })
10789                }
10790            }
10791            "close" => {
10792                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10793                    return Ok(e);
10794                }
10795                let a = self.parse_one_arg_or_default()?;
10796                Ok(Expr {
10797                    kind: ExprKind::Close(Box::new(a)),
10798                    line,
10799                })
10800            }
10801            "opendir" => {
10802                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10803                    return Ok(e);
10804                }
10805                let args = self.parse_builtin_args()?;
10806                if args.len() != 2 {
10807                    return Err(self.syntax_err("opendir requires two arguments", line));
10808                }
10809                Ok(Expr {
10810                    kind: ExprKind::Opendir {
10811                        handle: Box::new(args[0].clone()),
10812                        path: Box::new(args[1].clone()),
10813                    },
10814                    line,
10815                })
10816            }
10817            "readdir" => {
10818                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10819                    return Ok(e);
10820                }
10821                let a = self.parse_one_arg()?;
10822                Ok(Expr {
10823                    kind: ExprKind::Readdir(Box::new(a)),
10824                    line,
10825                })
10826            }
10827            "closedir" => {
10828                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10829                    return Ok(e);
10830                }
10831                let a = self.parse_one_arg()?;
10832                Ok(Expr {
10833                    kind: ExprKind::Closedir(Box::new(a)),
10834                    line,
10835                })
10836            }
10837            "rewinddir" => {
10838                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10839                    return Ok(e);
10840                }
10841                let a = self.parse_one_arg()?;
10842                Ok(Expr {
10843                    kind: ExprKind::Rewinddir(Box::new(a)),
10844                    line,
10845                })
10846            }
10847            "telldir" => {
10848                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10849                    return Ok(e);
10850                }
10851                let a = self.parse_one_arg()?;
10852                Ok(Expr {
10853                    kind: ExprKind::Telldir(Box::new(a)),
10854                    line,
10855                })
10856            }
10857            "seekdir" => {
10858                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10859                    return Ok(e);
10860                }
10861                let args = self.parse_builtin_args()?;
10862                if args.len() != 2 {
10863                    return Err(self.syntax_err("seekdir requires two arguments", line));
10864                }
10865                Ok(Expr {
10866                    kind: ExprKind::Seekdir {
10867                        handle: Box::new(args[0].clone()),
10868                        position: Box::new(args[1].clone()),
10869                    },
10870                    line,
10871                })
10872            }
10873            "eof" => {
10874                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10875                    return Ok(e);
10876                }
10877                if matches!(self.peek(), Token::LParen) {
10878                    self.advance();
10879                    if matches!(self.peek(), Token::RParen) {
10880                        self.advance();
10881                        Ok(Expr {
10882                            kind: ExprKind::Eof(None),
10883                            line,
10884                        })
10885                    } else {
10886                        let a = self.parse_expression()?;
10887                        self.expect(&Token::RParen)?;
10888                        Ok(Expr {
10889                            kind: ExprKind::Eof(Some(Box::new(a))),
10890                            line,
10891                        })
10892                    }
10893                } else {
10894                    Ok(Expr {
10895                        kind: ExprKind::Eof(None),
10896                        line,
10897                    })
10898                }
10899            }
10900            "system" => {
10901                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10902                    return Ok(e);
10903                }
10904                let args = self.parse_builtin_args()?;
10905                Ok(Expr {
10906                    kind: ExprKind::System(args),
10907                    line,
10908                })
10909            }
10910            "exec" => {
10911                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10912                    return Ok(e);
10913                }
10914                let args = self.parse_builtin_args()?;
10915                Ok(Expr {
10916                    kind: ExprKind::Exec(args),
10917                    line,
10918                })
10919            }
10920            "eval" => {
10921                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10922                    return Ok(e);
10923                }
10924                let a = if matches!(self.peek(), Token::LBrace) {
10925                    let block = self.parse_block()?;
10926                    Expr {
10927                        kind: ExprKind::CodeRef {
10928                            params: vec![],
10929                            body: block,
10930                        },
10931                        line,
10932                    }
10933                } else {
10934                    self.parse_one_arg_or_default()?
10935                };
10936                Ok(Expr {
10937                    kind: ExprKind::Eval(Box::new(a)),
10938                    line,
10939                })
10940            }
10941            "do" => {
10942                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10943                    return Ok(e);
10944                }
10945                let a = self.parse_one_arg()?;
10946                Ok(Expr {
10947                    kind: ExprKind::Do(Box::new(a)),
10948                    line,
10949                })
10950            }
10951            "require" => {
10952                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10953                    return Ok(e);
10954                }
10955                let a = self.parse_one_arg()?;
10956                Ok(Expr {
10957                    kind: ExprKind::Require(Box::new(a)),
10958                    line,
10959                })
10960            }
10961            "exit" => {
10962                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10963                    return Ok(e);
10964                }
10965                if matches!(
10966                    self.peek(),
10967                    Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
10968                ) {
10969                    Ok(Expr {
10970                        kind: ExprKind::Exit(None),
10971                        line,
10972                    })
10973                } else {
10974                    let a = self.parse_one_arg()?;
10975                    Ok(Expr {
10976                        kind: ExprKind::Exit(Some(Box::new(a))),
10977                        line,
10978                    })
10979                }
10980            }
10981            "chdir" => {
10982                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10983                    return Ok(e);
10984                }
10985                let a = self.parse_one_arg_or_default()?;
10986                Ok(Expr {
10987                    kind: ExprKind::Chdir(Box::new(a)),
10988                    line,
10989                })
10990            }
10991            "mkdir" => {
10992                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10993                    return Ok(e);
10994                }
10995                let args = self.parse_builtin_args()?;
10996                Ok(Expr {
10997                    kind: ExprKind::Mkdir {
10998                        path: Box::new(args[0].clone()),
10999                        mode: args.get(1).cloned().map(Box::new),
11000                    },
11001                    line,
11002                })
11003            }
11004            "unlink" | "rm" => {
11005                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11006                    return Ok(e);
11007                }
11008                let args = self.parse_builtin_args()?;
11009                Ok(Expr {
11010                    kind: ExprKind::Unlink(args),
11011                    line,
11012                })
11013            }
11014            "rename" => {
11015                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11016                    return Ok(e);
11017                }
11018                let args = self.parse_builtin_args()?;
11019                if args.len() != 2 {
11020                    return Err(self.syntax_err("rename requires two arguments", line));
11021                }
11022                Ok(Expr {
11023                    kind: ExprKind::Rename {
11024                        old: Box::new(args[0].clone()),
11025                        new: Box::new(args[1].clone()),
11026                    },
11027                    line,
11028                })
11029            }
11030            "chmod" => {
11031                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11032                    return Ok(e);
11033                }
11034                let args = self.parse_builtin_args()?;
11035                if args.len() < 2 {
11036                    return Err(self.syntax_err("chmod requires mode and at least one file", line));
11037                }
11038                Ok(Expr {
11039                    kind: ExprKind::Chmod(args),
11040                    line,
11041                })
11042            }
11043            "chown" => {
11044                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11045                    return Ok(e);
11046                }
11047                let args = self.parse_builtin_args()?;
11048                if args.len() < 3 {
11049                    return Err(
11050                        self.syntax_err("chown requires uid, gid, and at least one file", line)
11051                    );
11052                }
11053                Ok(Expr {
11054                    kind: ExprKind::Chown(args),
11055                    line,
11056                })
11057            }
11058            "stat" => {
11059                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11060                    return Ok(e);
11061                }
11062                let args = self.parse_builtin_args()?;
11063                let arg = if args.len() == 1 {
11064                    args[0].clone()
11065                } else if args.is_empty() {
11066                    Expr {
11067                        kind: ExprKind::ScalarVar("_".into()),
11068                        line,
11069                    }
11070                } else {
11071                    return Err(self.syntax_err("stat requires zero or one argument", line));
11072                };
11073                Ok(Expr {
11074                    kind: ExprKind::Stat(Box::new(arg)),
11075                    line,
11076                })
11077            }
11078            "lstat" => {
11079                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11080                    return Ok(e);
11081                }
11082                let args = self.parse_builtin_args()?;
11083                let arg = if args.len() == 1 {
11084                    args[0].clone()
11085                } else if args.is_empty() {
11086                    Expr {
11087                        kind: ExprKind::ScalarVar("_".into()),
11088                        line,
11089                    }
11090                } else {
11091                    return Err(self.syntax_err("lstat requires zero or one argument", line));
11092                };
11093                Ok(Expr {
11094                    kind: ExprKind::Lstat(Box::new(arg)),
11095                    line,
11096                })
11097            }
11098            "link" => {
11099                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11100                    return Ok(e);
11101                }
11102                let args = self.parse_builtin_args()?;
11103                if args.len() != 2 {
11104                    return Err(self.syntax_err("link requires two arguments", line));
11105                }
11106                Ok(Expr {
11107                    kind: ExprKind::Link {
11108                        old: Box::new(args[0].clone()),
11109                        new: Box::new(args[1].clone()),
11110                    },
11111                    line,
11112                })
11113            }
11114            "symlink" => {
11115                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11116                    return Ok(e);
11117                }
11118                let args = self.parse_builtin_args()?;
11119                if args.len() != 2 {
11120                    return Err(self.syntax_err("symlink requires two arguments", line));
11121                }
11122                Ok(Expr {
11123                    kind: ExprKind::Symlink {
11124                        old: Box::new(args[0].clone()),
11125                        new: Box::new(args[1].clone()),
11126                    },
11127                    line,
11128                })
11129            }
11130            "readlink" => {
11131                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11132                    return Ok(e);
11133                }
11134                let args = self.parse_builtin_args()?;
11135                let arg = if args.len() == 1 {
11136                    args[0].clone()
11137                } else if args.is_empty() {
11138                    Expr {
11139                        kind: ExprKind::ScalarVar("_".into()),
11140                        line,
11141                    }
11142                } else {
11143                    return Err(self.syntax_err("readlink requires zero or one argument", line));
11144                };
11145                Ok(Expr {
11146                    kind: ExprKind::Readlink(Box::new(arg)),
11147                    line,
11148                })
11149            }
11150            "files" => {
11151                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11152                    return Ok(e);
11153                }
11154                let args = self.parse_builtin_args()?;
11155                Ok(Expr {
11156                    kind: ExprKind::Files(args),
11157                    line,
11158                })
11159            }
11160            "filesf" | "f" => {
11161                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11162                    return Ok(e);
11163                }
11164                let args = self.parse_builtin_args()?;
11165                Ok(Expr {
11166                    kind: ExprKind::Filesf(args),
11167                    line,
11168                })
11169            }
11170            "fr" => {
11171                let args = self.parse_builtin_args()?;
11172                Ok(Expr {
11173                    kind: ExprKind::FilesfRecursive(args),
11174                    line,
11175                })
11176            }
11177            "dirs" | "d" => {
11178                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11179                    return Ok(e);
11180                }
11181                let args = self.parse_builtin_args()?;
11182                Ok(Expr {
11183                    kind: ExprKind::Dirs(args),
11184                    line,
11185                })
11186            }
11187            "dr" => {
11188                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11189                    return Ok(e);
11190                }
11191                let args = self.parse_builtin_args()?;
11192                Ok(Expr {
11193                    kind: ExprKind::DirsRecursive(args),
11194                    line,
11195                })
11196            }
11197            "sym_links" => {
11198                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11199                    return Ok(e);
11200                }
11201                let args = self.parse_builtin_args()?;
11202                Ok(Expr {
11203                    kind: ExprKind::SymLinks(args),
11204                    line,
11205                })
11206            }
11207            "sockets" => {
11208                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11209                    return Ok(e);
11210                }
11211                let args = self.parse_builtin_args()?;
11212                Ok(Expr {
11213                    kind: ExprKind::Sockets(args),
11214                    line,
11215                })
11216            }
11217            "pipes" => {
11218                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11219                    return Ok(e);
11220                }
11221                let args = self.parse_builtin_args()?;
11222                Ok(Expr {
11223                    kind: ExprKind::Pipes(args),
11224                    line,
11225                })
11226            }
11227            "block_devices" => {
11228                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11229                    return Ok(e);
11230                }
11231                let args = self.parse_builtin_args()?;
11232                Ok(Expr {
11233                    kind: ExprKind::BlockDevices(args),
11234                    line,
11235                })
11236            }
11237            "char_devices" => {
11238                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11239                    return Ok(e);
11240                }
11241                let args = self.parse_builtin_args()?;
11242                Ok(Expr {
11243                    kind: ExprKind::CharDevices(args),
11244                    line,
11245                })
11246            }
11247            "exe" | "executables" => {
11248                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11249                    return Ok(e);
11250                }
11251                let args = self.parse_builtin_args()?;
11252                Ok(Expr {
11253                    kind: ExprKind::Executables(args),
11254                    line,
11255                })
11256            }
11257            "glob" => {
11258                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11259                    return Ok(e);
11260                }
11261                let args = self.parse_builtin_args()?;
11262                Ok(Expr {
11263                    kind: ExprKind::Glob(args),
11264                    line,
11265                })
11266            }
11267            "glob_par" => {
11268                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11269                    return Ok(e);
11270                }
11271                let (args, progress) = self.parse_glob_par_or_par_sed_args()?;
11272                Ok(Expr {
11273                    kind: ExprKind::GlobPar { args, progress },
11274                    line,
11275                })
11276            }
11277            "par_sed" => {
11278                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11279                    return Ok(e);
11280                }
11281                let (args, progress) = self.parse_glob_par_or_par_sed_args()?;
11282                Ok(Expr {
11283                    kind: ExprKind::ParSed { args, progress },
11284                    line,
11285                })
11286            }
11287            "bless" => {
11288                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11289                    return Ok(e);
11290                }
11291                let args = self.parse_builtin_args()?;
11292                Ok(Expr {
11293                    kind: ExprKind::Bless {
11294                        ref_expr: Box::new(args[0].clone()),
11295                        class: args.get(1).cloned().map(Box::new),
11296                    },
11297                    line,
11298                })
11299            }
11300            "caller" => {
11301                if matches!(self.peek(), Token::LParen) {
11302                    self.advance();
11303                    if matches!(self.peek(), Token::RParen) {
11304                        self.advance();
11305                        Ok(Expr {
11306                            kind: ExprKind::Caller(None),
11307                            line,
11308                        })
11309                    } else {
11310                        let a = self.parse_expression()?;
11311                        self.expect(&Token::RParen)?;
11312                        Ok(Expr {
11313                            kind: ExprKind::Caller(Some(Box::new(a))),
11314                            line,
11315                        })
11316                    }
11317                } else {
11318                    Ok(Expr {
11319                        kind: ExprKind::Caller(None),
11320                        line,
11321                    })
11322                }
11323            }
11324            "wantarray" => {
11325                if matches!(self.peek(), Token::LParen) {
11326                    self.advance();
11327                    self.expect(&Token::RParen)?;
11328                }
11329                Ok(Expr {
11330                    kind: ExprKind::Wantarray,
11331                    line,
11332                })
11333            }
11334            "sub" => {
11335                // In no-interop mode, `sub {}` is not valid — must use `fn {}`
11336                if crate::no_interop_mode() {
11337                    return Err(self.syntax_err(
11338                        "stryke uses `fn {}` instead of `sub {}` (--no-interop)",
11339                        line,
11340                    ));
11341                }
11342                // Anonymous sub — optional prototype `sub () { }` (e.g. Carp.pm `*X = sub () { 1 }`)
11343                let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
11344                let body = self.parse_block()?;
11345                Ok(Expr {
11346                    kind: ExprKind::CodeRef { params, body },
11347                    line,
11348                })
11349            }
11350            "fn" => {
11351                // Anonymous fn — stryke syntax for anonymous subroutines
11352                let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
11353                let body = self.parse_block()?;
11354                Ok(Expr {
11355                    kind: ExprKind::CodeRef { params, body },
11356                    line,
11357                })
11358            }
11359            _ => {
11360                // Generic function call
11361                // Check for fat arrow (bareword string in hash)
11362                if matches!(self.peek(), Token::FatArrow) {
11363                    return Ok(Expr {
11364                        kind: ExprKind::String(name),
11365                        line,
11366                    });
11367                }
11368                // Bare `_` in expression position → topic variable `$_`.
11369                // Allows concise blocks: `map { _ * 2 }`, `fi { _ > 5 }`.
11370                if name == "_" {
11371                    return Ok(Expr {
11372                        kind: ExprKind::ScalarVar("_".to_string()),
11373                        line,
11374                    });
11375                }
11376                // Function call with optional parens
11377                if matches!(self.peek(), Token::LParen) {
11378                    self.advance();
11379                    let args = self.parse_arg_list()?;
11380                    self.expect(&Token::RParen)?;
11381                    Ok(Expr {
11382                        kind: ExprKind::FuncCall { name, args },
11383                        line,
11384                    })
11385                } else if self.peek().is_term_start()
11386                    && !(matches!(self.peek(), Token::Ident(ref kw) if kw == "sub")
11387                        && matches!(self.peek_at(1), Token::Ident(_)))
11388                    && !(self.suppress_parenless_call > 0 && matches!(self.peek(), Token::Ident(_)))
11389                    && !(matches!(self.peek(), Token::LBrace)
11390                        && self.peek_line() > self.prev_line())
11391                {
11392                    // Perl allows func arg without parens
11393                    // Guard: `sub <name> { }` is a named sub declaration (new
11394                    // statement), not an argument to the preceding call.
11395                    // Guard: suppress_parenless_call > 0 with Ident prevents consuming
11396                    // barewords (used by thread macro so `t Color::Red p` treats
11397                    // `p` as a stage, not an argument to the enum variant), but
11398                    // still allows `{` for struct/hash literals like `t Foo { x => 1 } p`.
11399                    // Guard: `{` on a new line is a new statement (hashref/block),
11400                    // not an argument to the preceding bareword call.
11401                    let args = self.parse_list_until_terminator()?;
11402                    Ok(Expr {
11403                        kind: ExprKind::FuncCall { name, args },
11404                        line,
11405                    })
11406                } else {
11407                    // No parens, no visible arguments — emit a Bareword.
11408                    // At runtime, Bareword tries sub resolution first (zero-arg
11409                    // call) and falls back to a string value.  stryke extension
11410                    // contexts (pipe-forward, map/fore) lift Bareword → FuncCall
11411                    // with `$_` injection separately.
11412                    Ok(Expr {
11413                        kind: ExprKind::Bareword(name),
11414                        line,
11415                    })
11416                }
11417            }
11418        }
11419    }
11420
11421    fn parse_print_like(
11422        &mut self,
11423        make: impl FnOnce(Option<String>, Vec<Expr>) -> ExprKind,
11424    ) -> PerlResult<Expr> {
11425        let line = self.peek_line();
11426        // Check for filehandle: print STDERR "msg"  /  print $fh "msg"
11427        let handle = if let Token::Ident(ref h) = self.peek().clone() {
11428            if h.chars().all(|c| c.is_uppercase() || c == '_')
11429                && !matches!(self.peek(), Token::LParen)
11430            {
11431                let h = h.clone();
11432                let saved = self.pos;
11433                self.advance();
11434                // Verify next token is a term start (not operator)
11435                if self.peek().is_term_start()
11436                    || matches!(
11437                        self.peek(),
11438                        Token::DoubleString(_) | Token::BacktickString(_) | Token::SingleString(_)
11439                    )
11440                {
11441                    Some(h)
11442                } else {
11443                    self.pos = saved;
11444                    None
11445                }
11446            } else {
11447                None
11448            }
11449        } else if let Token::ScalarVar(ref v) = self.peek().clone() {
11450            // `print $fh "msg"` — scalar variable as indirect filehandle.
11451            // Treat as handle when the next token (after $var) is a term-start or
11452            // string literal *without* a preceding comma/operator, matching Perl's
11453            // indirect-object heuristic.
11454            // Exclude `$_` — it's virtually always the topic variable, not a handle.
11455            // Exclude `[` and `{` — those are array/hash subscripts on the variable
11456            // itself (`print $F[0]`, `print $h{k}`), not separate print arguments.
11457            // Exclude statement modifiers (`if`/`unless`/`while`/`until`/`for`/`foreach`)
11458            // — `print $_ if COND` prints `$_` to STDOUT, not to a handle named `$_`.
11459            let v = v.clone();
11460            if v == "_" {
11461                None
11462            } else {
11463                let saved = self.pos;
11464                self.advance();
11465                let next = self.peek().clone();
11466                let is_stmt_modifier = matches!(&next, Token::Ident(kw)
11467                    if matches!(kw.as_str(), "if" | "unless" | "while" | "until" | "for" | "foreach"));
11468                if !is_stmt_modifier
11469                    && !matches!(next, Token::LBracket | Token::LBrace)
11470                    && (next.is_term_start()
11471                        || matches!(
11472                            next,
11473                            Token::DoubleString(_)
11474                                | Token::BacktickString(_)
11475                                | Token::SingleString(_)
11476                        ))
11477                {
11478                    // Next token looks like a print argument — $var is the handle.
11479                    Some(format!("${v}"))
11480                } else {
11481                    self.pos = saved;
11482                    None
11483                }
11484            }
11485        } else {
11486            None
11487        };
11488        // `print()` / `say()` / `printf()` — empty parens default to `$_`,
11489        // matching Perl 5: `perldoc -f print` / `-f say` say "If no arguments
11490        // are given, prints $_." (Same convention as the topic-default unary
11491        // builtins handled in `parse_one_arg_or_default`.)
11492        let args =
11493            if matches!(self.peek(), Token::LParen) && matches!(self.peek_at(1), Token::RParen) {
11494                let line_topic = self.peek_line();
11495                self.advance(); // (
11496                self.advance(); // )
11497                vec![Expr {
11498                    kind: ExprKind::ScalarVar("_".into()),
11499                    line: line_topic,
11500                }]
11501            } else {
11502                self.parse_list_until_terminator()?
11503            };
11504        Ok(Expr {
11505            kind: make(handle, args),
11506            line,
11507        })
11508    }
11509
11510    fn parse_block_list(&mut self) -> PerlResult<(Block, Expr)> {
11511        let block = self.parse_block()?;
11512        let block_end_line = self.prev_line();
11513        self.eat(&Token::Comma);
11514        // On the RHS of `|>`, the list operand is supplied by the piped LHS
11515        // and will be substituted at desugar time — accept a placeholder when
11516        // we're at a terminator here or on a new line (implicit semicolon).
11517        if self.in_pipe_rhs()
11518            && (matches!(
11519                self.peek(),
11520                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
11521            ) || self.peek_line() > block_end_line)
11522        {
11523            let line = self.peek_line();
11524            return Ok((block, self.pipe_placeholder_list(line)));
11525        }
11526        let list = self.parse_expression()?;
11527        Ok((block, list))
11528    }
11529
11530    /// Comma-separated expressions with optional trailing `timeout => SECS` (for `pselect`).
11531    /// When `paren` is true, stops at `)` as well as normal terminators.
11532    fn parse_comma_expr_list_with_timeout_tail(
11533        &mut self,
11534        paren: bool,
11535    ) -> PerlResult<(Vec<Expr>, Option<Expr>)> {
11536        let mut parts = vec![self.parse_assign_expr()?];
11537        loop {
11538            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
11539                break;
11540            }
11541            if paren && matches!(self.peek(), Token::RParen) {
11542                break;
11543            }
11544            if matches!(
11545                self.peek(),
11546                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
11547            ) {
11548                break;
11549            }
11550            if self.peek_is_postfix_stmt_modifier_keyword() {
11551                break;
11552            }
11553            if let Token::Ident(ref kw) = self.peek().clone() {
11554                if kw == "timeout" && matches!(self.peek_at(1), Token::FatArrow) {
11555                    self.advance();
11556                    self.expect(&Token::FatArrow)?;
11557                    let t = self.parse_assign_expr()?;
11558                    return Ok((parts, Some(t)));
11559                }
11560            }
11561            parts.push(self.parse_assign_expr()?);
11562        }
11563        Ok((parts, None))
11564    }
11565
11566    /// `preduce_init EXPR, BLOCK, LIST` with optional `, progress => EXPR`.
11567    fn parse_init_block_then_list_optional_progress(
11568        &mut self,
11569    ) -> PerlResult<(Expr, Block, Expr, Option<Expr>)> {
11570        let init = self.parse_assign_expr()?;
11571        self.expect(&Token::Comma)?;
11572        let block = self.parse_block_or_bareword_block()?;
11573        self.eat(&Token::Comma);
11574        let line = self.peek_line();
11575        if let Token::Ident(ref kw) = self.peek().clone() {
11576            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
11577                self.advance();
11578                self.expect(&Token::FatArrow)?;
11579                let prog = self.parse_assign_expr()?;
11580                return Ok((
11581                    init,
11582                    block,
11583                    Expr {
11584                        kind: ExprKind::List(vec![]),
11585                        line,
11586                    },
11587                    Some(prog),
11588                ));
11589            }
11590        }
11591        if matches!(
11592            self.peek(),
11593            Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
11594        ) {
11595            return Ok((
11596                init,
11597                block,
11598                Expr {
11599                    kind: ExprKind::List(vec![]),
11600                    line,
11601                },
11602                None,
11603            ));
11604        }
11605        let mut parts = vec![self.parse_assign_expr()?];
11606        loop {
11607            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
11608                break;
11609            }
11610            if matches!(
11611                self.peek(),
11612                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
11613            ) {
11614                break;
11615            }
11616            if self.peek_is_postfix_stmt_modifier_keyword() {
11617                break;
11618            }
11619            if let Token::Ident(ref kw) = self.peek().clone() {
11620                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
11621                    self.advance();
11622                    self.expect(&Token::FatArrow)?;
11623                    let prog = self.parse_assign_expr()?;
11624                    return Ok((init, block, merge_expr_list(parts), Some(prog)));
11625                }
11626            }
11627            parts.push(self.parse_assign_expr()?);
11628        }
11629        Ok((init, block, merge_expr_list(parts), None))
11630    }
11631
11632    /// `pmap_on CLUSTER { BLOCK } LIST [, progress => EXPR]` — cluster expr, then same tail as [`Self::parse_block_then_list_optional_progress`].
11633    fn parse_cluster_block_then_list_optional_progress(
11634        &mut self,
11635    ) -> PerlResult<(Expr, Block, Expr, Option<Expr>)> {
11636        // `pmap_on $c { BLOCK } @list` — suppress `$c { ... }` hash-subscript
11637        // auto-arrow so the brace opens the BLOCK, not a `$c->{...}` deref.
11638        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_add(1);
11639        let cluster = self.parse_assign_expr();
11640        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_sub(1);
11641        let cluster = cluster?;
11642        // Accept the canonical `pmap_on $c, { BLOCK } @list` LSP-doc form too.
11643        self.eat(&Token::Comma);
11644        let block = self.parse_block_or_bareword_block()?;
11645        self.eat(&Token::Comma);
11646        let line = self.peek_line();
11647        if let Token::Ident(ref kw) = self.peek().clone() {
11648            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
11649                self.advance();
11650                self.expect(&Token::FatArrow)?;
11651                let prog = self.parse_assign_expr_stop_at_pipe()?;
11652                return Ok((
11653                    cluster,
11654                    block,
11655                    Expr {
11656                        kind: ExprKind::List(vec![]),
11657                        line,
11658                    },
11659                    Some(prog),
11660                ));
11661            }
11662        }
11663        let empty_list_ok = matches!(
11664            self.peek(),
11665            Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
11666        ) || (self.in_pipe_rhs() && matches!(self.peek(), Token::Comma));
11667        if empty_list_ok {
11668            return Ok((
11669                cluster,
11670                block,
11671                Expr {
11672                    kind: ExprKind::List(vec![]),
11673                    line,
11674                },
11675                None,
11676            ));
11677        }
11678        let mut parts = vec![self.parse_assign_expr_stop_at_pipe()?];
11679        loop {
11680            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
11681                break;
11682            }
11683            if matches!(
11684                self.peek(),
11685                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
11686            ) {
11687                break;
11688            }
11689            if self.peek_is_postfix_stmt_modifier_keyword() {
11690                break;
11691            }
11692            if let Token::Ident(ref kw) = self.peek().clone() {
11693                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
11694                    self.advance();
11695                    self.expect(&Token::FatArrow)?;
11696                    let prog = self.parse_assign_expr_stop_at_pipe()?;
11697                    return Ok((cluster, block, merge_expr_list(parts), Some(prog)));
11698                }
11699            }
11700            parts.push(self.parse_assign_expr_stop_at_pipe()?);
11701        }
11702        Ok((cluster, block, merge_expr_list(parts), None))
11703    }
11704
11705    /// Like [`parse_block_list`] but supports a trailing `, progress => EXPR`
11706    /// (`pmap`, `pgrep`, `preduce`, `pfor`, `pcache`, `psort`, …).
11707    ///
11708    /// Always invoked for paren-less trailing forms (`pmap { … } LIST`,
11709    /// `pmap { … } LIST, progress => EXPR`), so `|>` must terminate the whole
11710    /// stage — individual list parts and the progress value parse through
11711    /// [`Self::parse_assign_expr_stop_at_pipe`] to keep pipe-forward
11712    /// left-associative in `@a |> pmap { $_ * 2 }, progress => 0 |> join ','`.
11713    fn parse_block_then_list_optional_progress(
11714        &mut self,
11715    ) -> PerlResult<(Block, Expr, Option<Expr>)> {
11716        let block = self.parse_block_or_bareword_block()?;
11717        self.eat(&Token::Comma);
11718        let line = self.peek_line();
11719        if let Token::Ident(ref kw) = self.peek().clone() {
11720            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
11721                self.advance();
11722                self.expect(&Token::FatArrow)?;
11723                let prog = self.parse_assign_expr_stop_at_pipe()?;
11724                return Ok((
11725                    block,
11726                    Expr {
11727                        kind: ExprKind::List(vec![]),
11728                        line,
11729                    },
11730                    Some(prog),
11731                ));
11732            }
11733        }
11734        // An empty list operand is allowed when the next token terminates the
11735        // enclosing context. Inside a pipe-forward RHS, a trailing `,` also
11736        // counts — `foo(bar, @a |> pmap { $_ * 2 }, baz)`. `|>` is also a
11737        // terminator — left-associative chaining leaves the outer `|>` for
11738        // the enclosing pipe-forward loop.
11739        let empty_list_ok = matches!(
11740            self.peek(),
11741            Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
11742        ) || (self.in_pipe_rhs() && matches!(self.peek(), Token::Comma));
11743        if empty_list_ok {
11744            return Ok((
11745                block,
11746                Expr {
11747                    kind: ExprKind::List(vec![]),
11748                    line,
11749                },
11750                None,
11751            ));
11752        }
11753        let mut parts = vec![self.parse_assign_expr_stop_at_pipe()?];
11754        loop {
11755            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
11756                break;
11757            }
11758            if matches!(
11759                self.peek(),
11760                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
11761            ) {
11762                break;
11763            }
11764            if self.peek_is_postfix_stmt_modifier_keyword() {
11765                break;
11766            }
11767            if let Token::Ident(ref kw) = self.peek().clone() {
11768                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
11769                    self.advance();
11770                    self.expect(&Token::FatArrow)?;
11771                    let prog = self.parse_assign_expr_stop_at_pipe()?;
11772                    return Ok((block, merge_expr_list(parts), Some(prog)));
11773                }
11774            }
11775            parts.push(self.parse_assign_expr_stop_at_pipe()?);
11776        }
11777        Ok((block, merge_expr_list(parts), None))
11778    }
11779
11780    /// Parse fan/fan_cap arguments: optional count + block or blockless expression.
11781    fn parse_fan_count_and_block(&mut self, line: usize) -> PerlResult<(Option<Box<Expr>>, Block)> {
11782        // `fan { BLOCK }` — no count
11783        if matches!(self.peek(), Token::LBrace) {
11784            let block = self.parse_block()?;
11785            return Ok((None, block));
11786        }
11787        let saved = self.pos;
11788        // Not a brace — first expr could be count or body
11789        let first = self.parse_postfix()?;
11790        if matches!(self.peek(), Token::LBrace) {
11791            // `fan COUNT { BLOCK }`
11792            let block = self.parse_block()?;
11793            Ok((Some(Box::new(first)), block))
11794        } else if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof)
11795            || (matches!(self.peek(), Token::Comma)
11796                && matches!(self.peek_at(1), Token::Ident(ref kw) if kw == "progress"))
11797        {
11798            // `fan EXPR;` — no count, first is the body
11799            let block = self.bareword_to_no_arg_block(first);
11800            Ok((None, block))
11801        } else if matches!(first.kind, ExprKind::Integer(_)) {
11802            // `fan COUNT EXPR` or `fan COUNT, EXPR` — integer count + body
11803            self.eat(&Token::Comma);
11804            let body = self.parse_fan_blockless_body(line)?;
11805            Ok((Some(Box::new(first)), body))
11806        } else {
11807            // Non-integer first (e.g. `$_`) followed by binary op (e.g. `* $_`)
11808            // — backtrack and re-parse as a full body expression.
11809            self.pos = saved;
11810            let body = self.parse_fan_blockless_body(line)?;
11811            Ok((None, body))
11812        }
11813    }
11814
11815    /// Parse a blockless fan/fan_cap body as a full expression (not just postfix).
11816    fn parse_fan_blockless_body(&mut self, line: usize) -> PerlResult<Block> {
11817        if matches!(self.peek(), Token::LBrace) {
11818            return self.parse_block();
11819        }
11820        // Check for bareword (zero-arg sub call) terminated by ; } EOF , or pipe
11821        if let Token::Ident(ref name) = self.peek().clone() {
11822            if matches!(
11823                self.peek_at(1),
11824                Token::Comma | Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
11825            ) {
11826                let name = name.clone();
11827                self.advance();
11828                let body = Expr {
11829                    kind: ExprKind::FuncCall { name, args: vec![] },
11830                    line,
11831                };
11832                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
11833            }
11834        }
11835        // Full expression (handles `$_ * $_`, `$_ + 1`, etc.)
11836        let expr = self.parse_assign_expr_stop_at_pipe()?;
11837        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
11838    }
11839
11840    /// Wrap a parsed expression as a single-statement block, converting bare
11841    /// identifiers to zero-arg calls (`work` → `work()`).
11842    fn bareword_to_no_arg_block(&self, expr: Expr) -> Block {
11843        let line = expr.line;
11844        let body = match &expr.kind {
11845            ExprKind::Bareword(name) => Expr {
11846                kind: ExprKind::FuncCall {
11847                    name: name.clone(),
11848                    args: vec![],
11849                },
11850                line,
11851            },
11852            _ => expr,
11853        };
11854        vec![Statement::new(StmtKind::Expression(body), line)]
11855    }
11856
11857    /// Parse either a `{ BLOCK }` or a bare expression and wrap it as a synthetic block.
11858    ///
11859    /// When the next token is `{`, delegates to [`Self::parse_block`].
11860    /// Otherwise parses a single postfix expression and wraps it as a call
11861    /// with `$_` as argument (for barewords) or a plain expression statement:
11862    ///
11863    /// - Bareword `foo` → `{ foo($_) }`
11864    /// - Other expr     → `{ EXPR }`
11865    fn parse_block_or_bareword_block(&mut self) -> PerlResult<Block> {
11866        if matches!(self.peek(), Token::LBrace) {
11867            return self.parse_block();
11868        }
11869        let line = self.peek_line();
11870        // A lone identifier followed by a list-terminator is a bare sub name:
11871        // `pmap double, @list` → block is `{ double($_) }`, rest is list.
11872        if let Token::Ident(ref name) = self.peek().clone() {
11873            if matches!(
11874                self.peek_at(1),
11875                Token::Comma | Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
11876            ) {
11877                let name = name.clone();
11878                self.advance();
11879                let body = Expr {
11880                    kind: ExprKind::FuncCall {
11881                        name,
11882                        args: vec![Expr {
11883                            kind: ExprKind::ScalarVar("_".to_string()),
11884                            line,
11885                        }],
11886                    },
11887                    line,
11888                };
11889                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
11890            }
11891        }
11892        // Not a simple bareword — parse as expression (e.g. `$_ * 2`, `uc $_`)
11893        let expr = self.parse_assign_expr_stop_at_pipe()?;
11894        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
11895    }
11896
11897    /// Like [`parse_block_or_bareword_block`] but for fan/timer/bench where the
11898    /// bare function takes no args (body runs stand-alone, not per-element).
11899    /// Only consumes a single bareword identifier — does NOT let `parse_primary`
11900    /// greedily swallow subsequent tokens as function arguments.
11901    fn parse_block_or_bareword_block_no_args(&mut self) -> PerlResult<Block> {
11902        if matches!(self.peek(), Token::LBrace) {
11903            return self.parse_block();
11904        }
11905        let line = self.peek_line();
11906        if let Token::Ident(ref name) = self.peek().clone() {
11907            if matches!(
11908                self.peek_at(1),
11909                Token::Comma
11910                    | Token::Semicolon
11911                    | Token::RBrace
11912                    | Token::Eof
11913                    | Token::PipeForward
11914                    | Token::Integer(_)
11915            ) {
11916                let name = name.clone();
11917                self.advance();
11918                let body = Expr {
11919                    kind: ExprKind::FuncCall { name, args: vec![] },
11920                    line,
11921                };
11922                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
11923            }
11924        }
11925        let expr = self.parse_postfix()?;
11926        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
11927    }
11928
11929    /// Returns true if `name` is a Perl keyword/builtin that should NOT be
11930    /// treated as a bare sub name (e.g. inside `sort`).
11931    /// True for any bareword the parser treats as a known builtin / keyword —
11932    /// Perl 5 core *or* a stryke extension. Used to suppress "call as user
11933    /// sub" interpretations (e.g. `sort my_cmp @list` only treats `my_cmp`
11934    /// as a comparator name if it *isn't* a known bareword). Previously named
11935    /// `is_perl_keyword`, which was misleading.
11936    fn is_known_bareword(name: &str) -> bool {
11937        Self::is_perl5_core(name) || Self::stryke_extension_name(name).is_some()
11938    }
11939
11940    /// True iff `name` appears as any spelling (primary *or* alias) in a
11941    /// `try_builtin` match arm. Picks up the ~300 aliases that don't show
11942    /// up in the parser-level keyword lists but are still callable at
11943    /// runtime — so `map { tj }` can default to `tj($_)` the same way
11944    /// `map { to_json }` does.
11945    fn is_try_builtin_name(name: &str) -> bool {
11946        crate::builtins::BUILTIN_ARMS
11947            .iter()
11948            .any(|arm| arm.contains(&name))
11949    }
11950
11951    /// True iff `name` is a Perl 5 core keyword/builtin (as shipped in stock
11952    /// `perl`). Extensions (`pmap`, `fan`, `timer`, …) are *not* included
11953    /// here — those live in `stryke_extension_name`. `%stryke::perl_compats`
11954    /// is derived from this list by `build.rs`.
11955    fn is_perl5_core(name: &str) -> bool {
11956        matches!(
11957            name,
11958            // ── array / list ────────────────────────────────────────────
11959            "map" | "grep" | "sort" | "reverse" | "join" | "split"
11960            | "push" | "pop" | "shift" | "unshift" | "splice"
11961            | "pack" | "unpack"
11962            // ── hash ────────────────────────────────────────────────────
11963            | "keys" | "values" | "each"
11964            // ── string ──────────────────────────────────────────────────
11965            | "chomp" | "chop" | "chr" | "ord" | "hex" | "oct"
11966            | "lc" | "uc" | "lcfirst" | "ucfirst"
11967            | "length" | "substr" | "index" | "rindex"
11968            | "sprintf" | "printf" | "print" | "say"
11969            | "pos" | "quotemeta" | "study"
11970            // ── numeric ─────────────────────────────────────────────────
11971            | "abs" | "int" | "sqrt" | "sin" | "cos" | "atan2"
11972            | "exp" | "log" | "rand" | "srand"
11973            // ── time ────────────────────────────────────────────────────
11974            | "time" | "localtime" | "gmtime"
11975            // ── type / reflection ───────────────────────────────────────
11976            | "defined" | "undef" | "ref" | "scalar" | "wantarray"
11977            | "caller" | "delete" | "exists" | "bless" | "prototype"
11978            | "tie" | "untie" | "tied"
11979            // ── io ──────────────────────────────────────────────────────
11980            | "open" | "close" | "read" | "readline" | "write" | "seek" | "tell"
11981            | "eof" | "binmode" | "getc" | "fileno" | "truncate"
11982            | "format" | "formline" | "select" | "vec"
11983            | "sysopen" | "sysread" | "sysseek" | "syswrite"
11984            // ── filesystem ──────────────────────────────────────────────
11985            | "stat" | "lstat" | "rename" | "unlink" | "utime"
11986            | "mkdir" | "rmdir" | "chdir" | "chmod" | "chown"
11987            | "glob" | "opendir" | "readdir" | "closedir"
11988            | "link" | "readlink" | "symlink"
11989            // ── ipc ─────────────────────────────────────────────────────
11990            | "fcntl" | "flock" | "ioctl" | "pipe" | "dbmopen" | "dbmclose"
11991            // ── sysv ipc ────────────────────────────────────────────────
11992            | "msgctl" | "msgget" | "msgrcv" | "msgsnd"
11993            | "semctl" | "semget" | "semop"
11994            | "shmctl" | "shmget" | "shmread" | "shmwrite"
11995            // ── process / system ────────────────────────────────────────
11996            | "system" | "exec" | "exit" | "die" | "warn" | "dump"
11997            | "fork" | "wait" | "waitpid" | "kill" | "alarm" | "sleep"
11998            | "chroot" | "times" | "umask" | "reset"
11999            | "getpgrp" | "setpgrp" | "getppid"
12000            | "getpriority" | "setpriority"
12001            // ── socket ──────────────────────────────────────────────────
12002            | "socket" | "socketpair" | "connect" | "listen" | "accept" | "shutdown"
12003            | "send" | "recv" | "bind" | "setsockopt" | "getsockopt"
12004            | "getpeername" | "getsockname"
12005            // ── posix metadata ──────────────────────────────────────────
12006            | "getpwnam" | "getpwuid" | "getpwent" | "setpwent"
12007            | "getgrnam" | "getgrgid" | "getgrent" | "setgrent"
12008            | "getlogin"
12009            | "gethostbyname" | "gethostbyaddr" | "gethostent"
12010            | "getnetbyname" | "getnetent"
12011            | "getprotobyname" | "getprotoent"
12012            | "getservbyname" | "getservent"
12013            | "sethostent" | "setnetent" | "setprotoent" | "setservent"
12014            | "endpwent" | "endgrent"
12015            | "endhostent" | "endnetent" | "endprotoent" | "endservent"
12016            // ── control flow ────────────────────────────────────────────
12017            | "return" | "do" | "eval" | "require"
12018            | "my" | "our" | "local" | "use" | "no"
12019            | "sub" | "if" | "unless" | "while" | "until"
12020            | "for" | "foreach" | "last" | "next" | "redo" | "goto"
12021            | "not" | "and" | "or"
12022            // ── quoting ─────────────────────────────────────────────────
12023            | "qw" | "qq" | "q"
12024            // ── phase blocks ────────────────────────────────────────────
12025            | "BEGIN" | "END"
12026        )
12027    }
12028
12029    /// If `name` is a stryke-only extension keyword/builtin, return it; else `None`.
12030    /// Used by `--compat` to reject extensions at parse time.
12031    fn stryke_extension_name(name: &str) -> Option<&str> {
12032        match name {
12033            // ── parallel ────────────────────────────────────────────────────
12034            | "pmap" | "pmap_on" | "pflat_map" | "pflat_map_on" | "pmap_chunked"
12035            | "pgrep" | "pfor" | "psort" | "preduce" | "preduce_init" | "pmap_reduce"
12036            | "pcache" | "pchannel" | "pselect" | "puniq" | "pfirst" | "pany"
12037            | "fan" | "fan_cap" | "par_lines" | "par_walk" | "par_sed"
12038            | "par_find_files" | "par_line_count" | "pwatch" | "par_pipeline_stream"
12039            | "glob_par" | "ppool" | "barrier" | "pipeline" | "cluster"
12040            | "pmaps" | "pflat_maps" | "pgreps"
12041            // ── functional / iterator ───────────────────────────────────────
12042            | "fore" | "e" | "ep" | "flat_map" | "flat_maps" | "maps" | "filter" | "fi" | "find_all" | "reduce" | "fold"
12043            | "inject" | "collect" | "uniq" | "distinct" | "any" | "all" | "none"
12044            | "first" | "detect" | "find" | "compact" | "concat" | "chain" | "reject" | "flatten" | "set"
12045            | "min_by" | "max_by" | "sort_by" | "tally" | "find_index"
12046            | "each_with_index" | "count" | "cnt" |"len" | "group_by" | "chunk_by"
12047            | "zip" | "chunk" | "chunked" | "sliding_window" | "windowed"
12048            | "enumerate" | "with_index" | "shuffle" | "shuffled"| "heap"
12049            | "take_while" | "drop_while" | "skip_while" | "tap" | "peek" | "partition"
12050            | "zip_with" | "count_by" | "skip" | "first_or"
12051            // ── pipeline / string helpers ───────────────────────────────────
12052            | "input" | "lines" | "words" | "chars" | "digits" | "letters" | "letters_uc" | "letters_lc"
12053            | "punctuation" | "punct"
12054            | "sentences" | "sents"
12055            | "paragraphs" | "paras" | "sections" | "sects"
12056            | "numbers" | "nums" | "graphemes" | "grs" | "columns" | "cols"
12057            | "trim" | "avg" | "stddev"
12058            | "squared" | "sq" | "square" | "cubed" | "cb" | "cube" | "expt" | "pow" | "pw"
12059            | "normalize" | "snake_case" | "camel_case" | "kebab_case"
12060            | "frequencies" | "freq" | "interleave" | "ddump" | "stringify" | "str" | "top"
12061            | "to_json" | "to_csv" | "to_toml" | "to_yaml" | "to_xml"
12062            | "to_html" | "to_markdown" | "to_table" | "xopen"
12063            | "from_json" | "from_csv" | "from_toml" | "from_yaml" | "from_xml"
12064            | "clip" | "clipboard" | "paste" | "pbcopy" | "pbpaste" | "preview"
12065            | "sparkline" | "spark" | "bar_chart" | "bars" | "flame" | "flamechart"
12066            | "histo" | "gauge" | "spinner" | "spinner_start" | "spinner_stop"
12067            | "to_hash" | "to_set"
12068            | "to_file" | "read_lines" | "append_file" | "write_json" | "read_json"
12069            | "tempfile" | "tempdir" | "list_count" | "list_size" | "size"
12070            | "clamp" | "grep_v" | "select_keys" | "pluck" | "glob_match" | "which_all"
12071            | "dedup" | "nth" | "tail" | "take" | "drop" | "tee" | "range"
12072            | "inc" | "dec" | "elapsed"
12073            // ── filesystem extensions ───────────────────────────────────────
12074            | "files" | "filesf" | "f" | "fr" | "dirs" | "d" | "dr" | "sym_links"
12075            | "sockets" | "pipes" | "block_devices" | "char_devices" | "exe" | "executables"
12076            | "basename" | "dirname" | "fileparse" | "realpath" | "canonpath"
12077            | "copy" | "move" | "spurt" | "read_bytes" | "which"
12078            | "getcwd" | "touch" | "gethostname" | "uname"
12079            // ── data / network ──────────────────────────────────────────────
12080            | "csv_read" | "csv_write" | "dataframe" | "sqlite"
12081            | "fetch" | "fetch_json" | "fetch_async" | "fetch_async_json"
12082            | "par_fetch" | "par_csv_read" | "par_pipeline"
12083            | "json_encode" | "json_decode" | "json_jq"
12084            | "http_request" | "serve" | "ssh"
12085            | "html_parse" | "css_select" | "xml_parse" | "xpath"
12086            | "smtp_send"
12087            | "net_interfaces" | "net_ipv4" | "net_ipv6" | "net_mac"
12088            | "net_public_ip" | "net_dns" | "net_reverse_dns"
12089            | "net_ping" | "net_port_open" | "net_ports_scan"
12090            | "net_latency" | "net_download" | "net_headers"
12091            | "net_dns_servers" | "net_gateway" | "net_whois" | "net_hostname"
12092            // ── git ─────────────────────────────────────────────────────────
12093            | "git_log" | "git_status" | "git_diff" | "git_branches"
12094            | "git_tags" | "git_blame" | "git_authors" | "git_files"
12095            | "git_show" | "git_root"
12096            // ── audio / media ───────────────────────────────────────────────
12097            | "audio_convert" | "audio_info" | "id3_read" | "id3_write"
12098            // ── pdf ─────────────────────────────────────────────────────────
12099            | "to_pdf" | "pdf_text" | "pdf_pages"
12100            // ── serialization (stryke-only encoders) ────────────────────────
12101            | "toml_encode" | "toml_decode"
12102            | "yaml_encode" | "yaml_decode"
12103            | "xml_encode" | "xml_decode"
12104            // ── crypto / encoding ───────────────────────────────────────────
12105            | "md5" | "sha1" | "sha224" | "sha256" | "sha384" | "sha512"
12106            | "sha3_256" | "s3_256" | "sha3_512" | "s3_512"
12107            | "shake128" | "shake256"
12108            | "hmac_sha256" | "hmac_sha1" | "hmac_sha384" | "hmac_sha512" | "hmac_md5"
12109            | "uuid" | "crc32"
12110            | "blake2b" | "b2b" | "blake2s" | "b2s" | "blake3" | "b3"
12111            | "ripemd160" | "rmd160" | "md4"
12112            | "xxh32" | "xxhash32" | "xxh64" | "xxhash64" | "xxh3" | "xxhash3" | "xxh3_128" | "xxhash3_128"
12113            | "murmur3" | "murmur3_32" | "murmur3_128"
12114            | "siphash" | "siphash_keyed"
12115            | "hkdf_sha256" | "hkdf" | "hkdf_sha512"
12116            | "poly1305" | "poly1305_mac"
12117            | "base32_encode" | "b32e" | "base32_decode" | "b32d"
12118            | "base58_encode" | "b58e" | "base58_decode" | "b58d"
12119            | "totp" | "totp_generate" | "totp_verify" | "hotp" | "hotp_generate"
12120            | "aes_cbc_encrypt" | "aes_cbc_enc" | "aes_cbc_decrypt" | "aes_cbc_dec"
12121            | "blowfish_encrypt" | "bf_enc" | "blowfish_decrypt" | "bf_dec"
12122            | "des3_encrypt" | "3des_enc" | "tdes_enc" | "des3_decrypt" | "3des_dec" | "tdes_dec"
12123            | "twofish_encrypt" | "tf_enc" | "twofish_decrypt" | "tf_dec"
12124            | "camellia_encrypt" | "cam_enc" | "camellia_decrypt" | "cam_dec"
12125            | "cast5_encrypt" | "cast5_enc" | "cast5_decrypt" | "cast5_dec"
12126            | "salsa20" | "salsa20_encrypt" | "salsa20_decrypt"
12127            | "xsalsa20" | "xsalsa20_encrypt" | "xsalsa20_decrypt"
12128            | "secretbox" | "secretbox_seal" | "secretbox_open"
12129            | "nacl_box_keygen" | "box_keygen" | "nacl_box" | "nacl_box_seal" | "box_seal"
12130            | "nacl_box_open" | "box_open"
12131            | "qr_ascii" | "qr" | "qr_png" | "qr_svg"
12132            | "barcode_code128" | "code128" | "barcode_code39" | "code39"
12133            | "barcode_ean13" | "ean13" | "barcode_svg"
12134            | "argon2_hash" | "argon2" | "argon2_verify"
12135            | "bcrypt_hash" | "bcrypt" | "bcrypt_verify"
12136            | "scrypt_hash" | "scrypt" | "scrypt_verify"
12137            | "pbkdf2" | "pbkdf2_derive"
12138            | "random_bytes" | "randbytes" | "random_bytes_hex" | "randhex"
12139            | "aes_encrypt" | "aes_enc" | "aes_decrypt" | "aes_dec"
12140            | "chacha_encrypt" | "chacha_enc" | "chacha_decrypt" | "chacha_dec"
12141            | "rsa_keygen" | "rsa_encrypt" | "rsa_enc" | "rsa_decrypt" | "rsa_dec"
12142            | "rsa_encrypt_pkcs1" | "rsa_decrypt_pkcs1" | "rsa_sign" | "rsa_verify"
12143            | "ecdsa_p256_keygen" | "p256_keygen" | "ecdsa_p256_sign" | "p256_sign"
12144            | "ecdsa_p256_verify" | "p256_verify"
12145            | "ecdsa_p384_keygen" | "p384_keygen" | "ecdsa_p384_sign" | "p384_sign"
12146            | "ecdsa_p384_verify" | "p384_verify"
12147            | "ecdsa_secp256k1_keygen" | "secp256k1_keygen"
12148            | "ecdsa_secp256k1_sign" | "secp256k1_sign"
12149            | "ecdsa_secp256k1_verify" | "secp256k1_verify"
12150            | "ecdh_p256" | "p256_dh" | "ecdh_p384" | "p384_dh"
12151            | "ed25519_keygen" | "ed_keygen" | "ed25519_sign" | "ed_sign"
12152            | "ed25519_verify" | "ed_verify"
12153            | "x25519_keygen" | "x_keygen" | "x25519_dh" | "x_dh"
12154            | "base64_encode" | "base64_decode"
12155            | "hex_encode" | "hex_decode"
12156            | "url_encode" | "url_decode"
12157            | "gzip" | "gunzip" | "gz" | "ugz" | "zstd" | "zstd_decode" | "zst" | "uzst"
12158            | "brotli" | "br" | "brotli_decode" | "ubr"
12159            | "xz" | "lzma" | "xz_decode" | "unxz" | "unlzma"
12160            | "bzip2" | "bz2" | "bzip2_decode" | "bunzip2" | "ubz2"
12161            | "lz4" | "lz4_decode" | "unlz4"
12162            | "snappy" | "snp" | "snappy_decode" | "unsnappy"
12163            | "lzw" | "lzw_decode" | "unlzw"
12164            | "tar_create" | "tar" | "tar_extract" | "untar" | "tar_list"
12165            | "tar_gz_create" | "tgz" | "tar_gz_extract" | "untgz"
12166            | "zip_create" | "zip_archive" | "zip_extract" | "unzip_archive" | "zip_list"
12167            // ── special math functions ────────────────────────────────────────
12168            | "erf" | "erfc" | "gamma" | "tgamma" | "lgamma" | "ln_gamma"
12169            | "digamma" | "psi" | "beta_fn" | "lbeta" | "ln_beta"
12170            | "betainc" | "beta_reg" | "gammainc" | "gamma_li"
12171            | "gammaincc" | "gamma_ui" | "gammainc_reg" | "gamma_lr"
12172            | "gammaincc_reg" | "gamma_ur"
12173            // ── date / time ─────────────────────────────────────────────────
12174            | "datetime_utc" | "datetime_now_tz"
12175            | "datetime_format_tz" | "datetime_add_seconds"
12176            | "datetime_from_epoch"
12177            | "datetime_parse_rfc3339" | "datetime_parse_local"
12178            | "datetime_strftime"
12179            | "dateseq" | "dategrep" | "dateround" | "datesort"
12180            // ── jwt ─────────────────────────────────────────────────────────
12181            | "jwt_encode" | "jwt_decode" | "jwt_decode_unsafe"
12182            // ── logging ─────────────────────────────────────────────────────
12183            | "log_info" | "log_warn" | "log_error"
12184            | "log_debug" | "log_trace" | "log_json" | "log_level"
12185            // ── concurrency / timing ────────────────────────────────────────
12186            | "async" | "spawn" | "trace" | "timer" | "bench"
12187            | "eval_timeout" | "retry" | "rate_limit" | "every"
12188            | "gen" | "watch"
12189            // ── caching ────────────────────────────────────────────────────────
12190            | "cache_clear" | "cache_exists" | "cache_stats" | "cacheview"
12191            // ── testing framework ────────────────────────────────────────────
12192            | "assert_eq" | "assert_ne" | "assert_ok" | "assert_err"
12193            | "assert_true" | "assert_false"
12194            | "assert_gt" | "assert_lt" | "assert_ge" | "assert_le"
12195            | "assert_match" | "assert_contains" | "assert_near" | "assert_dies"
12196            | "test_run"
12197            // ── system info ─────────────────────────────────────────────────
12198            | "mounts" | "du" | "du_tree" | "process_list"
12199            | "thread_count" | "pool_info" | "par_bench"
12200            // ── stress testing ──────────────────────────────────────────────
12201            | "stress_cpu" | "scpu" | "stress_mem" | "smem"
12202            | "stress_io" | "sio" | "stress_test" | "st"
12203            | "heat" | "fire" | "fire_and_forget" | "pin"
12204            // ── I/O extensions ──────────────────────────────────────────────
12205            | "slurp" | "cat" | "c" | "capture" | "pager" | "pg" | "less"
12206            | "stdin"
12207            // ── internal ────────────────────────────────────────────────────
12208            | "__stryke_rust_compile"
12209            // ── short aliases ───────────────────────────────────────────────
12210            | "p" | "rev"
12211            // ── trivial numeric / predicate builtins ────────────────────────
12212            | "even" | "odd" | "zero" | "nonzero"
12213            | "positive" | "pos_n" | "negative" | "neg_n"
12214            | "sign" | "negate" | "double" | "triple" | "half"
12215            | "identity" | "id"
12216            | "round" | "floor" | "ceil" | "ceiling" | "trunc" | "truncn"
12217            | "gcd" | "lcm" | "min2" | "max2"
12218            | "log2" | "log10" | "hypot"
12219            | "rad_to_deg" | "r2d" | "deg_to_rad" | "d2r"
12220            | "pow2" | "abs_diff"
12221            | "factorial" | "fact" | "fibonacci" | "fib"
12222            | "is_prime" | "is_square" | "is_power_of_two" | "is_pow2"
12223            | "cbrt" | "exp2" | "percent" | "pct" | "inverse"
12224            | "median" | "mode_val" | "variance"
12225            // ── trivial string ops ──────────────────────────────────────────
12226            | "is_empty" | "is_blank" | "is_numeric"
12227            | "is_upper" | "is_lower" | "is_alpha" | "is_digit" | "is_alnum"
12228            | "is_space" | "is_whitespace"
12229            | "starts_with" | "sw" | "ends_with" | "ew" | "contains"
12230            | "capitalize" | "cap" | "swap_case" | "repeat"
12231            | "title_case" | "title" | "squish"
12232            | "pad_left" | "lpad" | "pad_right" | "rpad" | "center"
12233            | "truncate_at" | "shorten" | "reverse_str" | "rev_str"
12234            | "char_count" | "word_count" | "wc" | "line_count" | "lc_lines"
12235            // ── trivial type predicates ─────────────────────────────────────
12236            | "is_array" | "is_arrayref" | "is_hash" | "is_hashref"
12237            | "is_code" | "is_coderef" | "is_ref"
12238            | "is_undef" | "is_defined" | "is_def"
12239            | "is_string" | "is_str" | "is_int" | "is_integer" | "is_float"
12240            // ── hash helpers ────────────────────────────────────────────────
12241            | "invert" | "merge_hash"
12242            | "has_key" | "hk" | "has_any_key" | "has_all_keys"
12243            // ── boolean combinators ─────────────────────────────────────────
12244            | "both" | "either" | "neither" | "xor_bool" | "bool_to_int" | "b2i"
12245            // ── collection helpers (trivial) ────────────────────────────────
12246            | "riffle" | "intersperse" | "every_nth"
12247            | "drop_n" | "take_n" | "rotate" | "swap_pairs"
12248            // ── base conversion ─────────────────────────────────────────────
12249            | "to_bin" | "bin_of" | "to_hex" | "hex_of" | "to_oct" | "oct_of"
12250            | "from_bin" | "from_hex" | "from_oct" | "to_base" | "from_base"
12251            | "bits_count" | "popcount" | "leading_zeros" | "lz"
12252            | "trailing_zeros" | "tz" | "bit_length" | "bitlen"
12253            // ── bit ops ─────────────────────────────────────────────────────
12254            | "bit_and" | "bit_or" | "bit_xor" | "bit_not"
12255            | "shift_left" | "shl" | "shift_right" | "shr"
12256            | "bit_set" | "bit_clear" | "bit_toggle" | "bit_test"
12257            // ── unit conversions: temperature ───────────────────────────────
12258            | "c_to_f" | "f_to_c" | "c_to_k" | "k_to_c" | "f_to_k" | "k_to_f"
12259            // ── unit conversions: distance ──────────────────────────────────
12260            | "miles_to_km" | "km_to_miles" | "miles_to_m" | "m_to_miles"
12261            | "feet_to_m" | "m_to_feet" | "inches_to_cm" | "cm_to_inches"
12262            | "yards_to_m" | "m_to_yards"
12263            // ── unit conversions: mass ──────────────────────────────────────
12264            | "kg_to_lbs" | "lbs_to_kg" | "g_to_oz" | "oz_to_g"
12265            | "stone_to_kg" | "kg_to_stone"
12266            // ── unit conversions: digital ───────────────────────────────────
12267            | "bytes_to_kb" | "b_to_kb" | "kb_to_bytes" | "kb_to_b"
12268            | "bytes_to_mb" | "mb_to_bytes" | "bytes_to_gb" | "gb_to_bytes"
12269            | "kb_to_mb" | "mb_to_gb"
12270            | "bits_to_bytes" | "bytes_to_bits"
12271            // ── unit conversions: time ──────────────────────────────────────
12272            | "seconds_to_minutes" | "s_to_m" | "minutes_to_seconds" | "m_to_s"
12273            | "seconds_to_hours" | "hours_to_seconds"
12274            | "seconds_to_days" | "days_to_seconds"
12275            | "minutes_to_hours" | "hours_to_minutes"
12276            | "hours_to_days" | "days_to_hours"
12277            // ── date helpers ────────────────────────────────────────────────
12278            | "is_leap_year" | "is_leap" | "days_in_month"
12279            | "month_name" | "month_short"
12280            | "weekday_name" | "weekday_short" | "quarter_of"
12281            // ── now / timestamp ─────────────────────────────────────────────
12282            | "now_ms" | "now_us" | "now_ns"
12283            | "unix_epoch" | "epoch" | "unix_epoch_ms" | "epoch_ms"
12284            // ── color / ANSI ────────────────────────────────────────────────
12285            | "rgb_to_hex" | "hex_to_rgb"
12286            | "ansi_red" | "ansi_green" | "ansi_yellow" | "ansi_blue"
12287            | "ansi_magenta" | "ansi_cyan" | "ansi_white" | "ansi_black"
12288            | "ansi_bold" | "ansi_dim" | "ansi_underline" | "ansi_reverse"
12289            | "strip_ansi"
12290            | "red" | "green" | "yellow" | "blue" | "magenta" | "purple" | "cyan"
12291            | "white" | "black" | "bold" | "dim" | "italic" | "underline"
12292            | "strikethrough" | "ansi_off" | "off" | "gray" | "grey"
12293            | "bright_red" | "bright_green" | "bright_yellow" | "bright_blue"
12294            | "bright_magenta" | "bright_cyan" | "bright_white"
12295            | "bg_red" | "bg_green" | "bg_yellow" | "bg_blue"
12296            | "bg_magenta" | "bg_cyan" | "bg_white" | "bg_black"
12297            | "red_bold" | "bold_red" | "green_bold" | "bold_green"
12298            | "yellow_bold" | "bold_yellow" | "blue_bold" | "bold_blue"
12299            | "magenta_bold" | "bold_magenta" | "cyan_bold" | "bold_cyan"
12300            | "white_bold" | "bold_white"
12301            | "blink" | "rapid_blink" | "hidden" | "overline"
12302            | "bg_bright_red" | "bg_bright_green" | "bg_bright_yellow" | "bg_bright_blue"
12303            | "bg_bright_magenta" | "bg_bright_cyan" | "bg_bright_white"
12304            | "rgb" | "bg_rgb" | "color256" | "c256" | "bg_color256" | "bg_c256"
12305            // ── network / validation ────────────────────────────────────────
12306            | "ipv4_to_int" | "int_to_ipv4"
12307            | "is_valid_ipv4" | "is_valid_ipv6" | "is_valid_email" | "is_valid_url"
12308            // ── path helpers ────────────────────────────────────────────────
12309            | "path_ext" | "path_stem" | "path_parent" | "path_join" | "path_split"
12310            | "strip_prefix" | "strip_suffix" | "ensure_prefix" | "ensure_suffix"
12311            // ── functional primitives ───────────────────────────────────────
12312            | "const_fn" | "always_true" | "always_false"
12313            | "flip_args" | "first_arg" | "second_arg" | "last_arg"
12314            // ── more list helpers ───────────────────────────────────────────
12315            | "count_eq" | "count_ne" | "all_eq"
12316            | "all_distinct" | "all_unique" | "has_duplicates"
12317            | "sum_of" | "product_of" | "max_of" | "min_of" | "range_of"
12318            // ── string quote / escape ───────────────────────────────────────
12319            | "quote" | "single_quote" | "unquote"
12320            | "extract_between" | "ellipsis"
12321            // ── random ──────────────────────────────────────────────────────
12322            | "coin_flip" | "dice_roll"
12323            | "random_int" | "random_float" | "random_bool"
12324            | "random_choice" | "random_between"
12325            | "random_string" | "random_alpha" | "random_digit"
12326            // ── system introspection ────────────────────────────────────────
12327            | "os_name" | "os_arch" | "num_cpus"
12328            | "pid" | "ppid" | "uid" | "gid"
12329            | "username" | "home_dir" | "temp_dir"
12330            | "mem_total" | "mem_free" | "mem_used"
12331            | "swap_total" | "swap_free" | "swap_used"
12332            | "disk_total" | "disk_free" | "disk_avail" | "disk_used"
12333            | "load_avg" | "sys_uptime" | "page_size"
12334            | "os_version" | "os_family" | "endianness" | "pointer_width"
12335            | "proc_mem" | "rss"
12336            // ── collection more ─────────────────────────────────────────────
12337            | "transpose" | "unzip"
12338            | "run_length_encode" | "rle" | "run_length_decode" | "rld"
12339            | "sliding_pairs" | "consecutive_eq" | "flatten_deep"
12340            // ── trig / math (batch 2) ───────────────────────────────────────
12341            | "tan" | "asin" | "acos" | "atan"
12342            | "sinh" | "cosh" | "tanh" | "asinh" | "acosh" | "atanh"
12343            | "sqr" | "cube_fn"
12344            | "mod_op" | "ceil_div" | "floor_div"
12345            | "is_finite" | "is_infinite" | "is_inf" | "is_nan"
12346            | "degrees" | "radians"
12347            | "min_abs" | "max_abs"
12348            | "saturate" | "sat01" | "wrap_around"
12349            // ── string (batch 2) ────────────────────────────────────────────
12350            | "rot13" | "rot47" | "caesar_shift" | "reverse_words"
12351            | "count_vowels" | "count_consonants" | "is_vowel" | "is_consonant"
12352            | "first_word" | "last_word"
12353            | "left_str" | "head_str" | "right_str" | "tail_str" | "mid_str"
12354            | "lowercase" | "uppercase"
12355            | "pascal_case" | "pc_case"
12356            | "constant_case" | "upper_snake" | "dot_case" | "path_case"
12357            | "is_palindrome" | "hamming_distance"
12358            | "longest_common_prefix" | "lcp"
12359            | "ascii_ord" | "ascii_chr" | "count_char" | "indexes_of"
12360            | "replace_first" | "replace_all_str"
12361            | "contains_any" | "contains_all"
12362            | "starts_with_any" | "ends_with_any"
12363            // ── predicates (batch 2) ────────────────────────────────────────
12364            | "is_pair" | "is_triple"
12365            | "is_sorted" | "is_asc" | "is_sorted_desc" | "is_desc"
12366            | "is_empty_arr" | "is_empty_hash"
12367            | "is_subset" | "is_superset" | "is_permutation"
12368            // ── collection (batch 2) ────────────────────────────────────────
12369            | "first_eq" | "last_eq"
12370            | "index_of" | "last_index_of" | "positions_of"
12371            | "batch" | "binary_search" | "bsearch" | "linear_search" | "lsearch"
12372            | "distinct_count" | "longest" | "shortest"
12373            | "array_union" | "list_union"
12374            | "array_intersection" | "list_intersection"
12375            | "array_difference" | "list_difference"
12376            | "symmetric_diff" | "group_of_n" | "chunk_n"
12377            | "repeat_list" | "cycle_n" | "random_sample" | "sample_n"
12378            // ── hash ops (batch 2) ──────────────────────────────────────────
12379            | "pick_keys" | "pick" | "omit_keys" | "omit"
12380            | "map_keys_fn" | "map_values_fn"
12381            | "hash_size" | "hash_from_pairs" | "pairs_from_hash"
12382            | "hash_eq" | "keys_sorted" | "values_sorted" | "remove_keys"
12383            // ── date (batch 2) ──────────────────────────────────────────────
12384            | "today" | "yesterday" | "tomorrow" | "is_weekend" | "is_weekday"
12385            // ── json helpers ────────────────────────────────────────────────
12386            | "json_pretty" | "json_minify" | "escape_json" | "json_escape"
12387            // ── process / env ───────────────────────────────────────────────
12388            | "cmd_exists" | "env_get" | "env_has" | "env_keys"
12389            | "argc" | "script_name"
12390            | "has_stdin_tty" | "has_stdout_tty" | "has_stderr_tty"
12391            // ── id helpers ──────────────────────────────────────────────────
12392            | "uuid_v4" | "nanoid" | "short_id" | "is_uuid" | "token"
12393            // ── url / email parts ───────────────────────────────────────────
12394            | "email_domain" | "email_local"
12395            | "url_host" | "url_path" | "url_query" | "url_scheme"
12396            // ── file stat / path ────────────────────────────────────────────
12397            | "file_size" | "fsize" | "file_mtime" | "mtime"
12398            | "file_atime" | "atime" | "file_ctime" | "ctime"
12399            | "is_symlink" | "is_readable" | "is_writable" | "is_executable"
12400            | "path_is_abs" | "path_is_rel"
12401            // ── stats / sort / array / format / cmp / regex / time conv / volume / force ──
12402            | "min_max" | "percentile" | "harmonic_mean" | "geometric_mean" | "zscore"
12403            | "sorted" | "sorted_desc" | "sorted_nums" | "sorted_by_length"
12404            | "reverse_list" | "list_reverse"
12405            | "without" | "without_nth" | "take_last" | "drop_last"
12406            | "pairwise" | "zipmap"
12407            | "format_bytes" | "human_bytes"
12408            | "format_duration" | "human_duration"
12409            | "format_number" | "group_number"
12410            | "format_percent" | "pad_number"
12411            | "spaceship" | "cmp_num" | "cmp_str"
12412            | "compare_versions" | "version_cmp"
12413            | "hash_insert" | "hash_update" | "hash_delete"
12414            | "matches_regex" | "re_match"
12415            | "count_regex_matches" | "regex_extract"
12416            | "regex_split_str" | "regex_replace_str"
12417            | "shuffle_chars" | "random_char" | "nth_word"
12418            | "head_lines" | "tail_lines" | "count_substring"
12419            | "is_valid_hex" | "hex_upper" | "hex_lower"
12420            | "ms_to_s" | "s_to_ms" | "ms_to_ns" | "ns_to_ms"
12421            | "us_to_ns" | "ns_to_us"
12422            | "liters_to_gallons" | "gallons_to_liters"
12423            | "liters_to_ml" | "ml_to_liters"
12424            | "cups_to_ml" | "ml_to_cups"
12425            | "newtons_to_lbf" | "lbf_to_newtons"
12426            | "joules_to_cal" | "cal_to_joules"
12427            | "watts_to_hp" | "hp_to_watts"
12428            | "pascals_to_psi" | "psi_to_pascals"
12429            | "bar_to_pascals" | "pascals_to_bar"
12430            // ── algebraic match ─────────────────────────────────────────────
12431            | "match"
12432            // ── clojure stdlib (only names not matched above) ─────────────────
12433            | "fst" | "rest" | "rst" | "second" | "snd"
12434            | "last_clj" | "lastc" | "butlast" | "bl"
12435            | "ffirst" | "ffs" | "fnext" | "fne" | "nfirst" | "nfs" | "nnext" | "nne"
12436            | "cons" | "conj"
12437            | "peek_clj" | "pkc" | "pop_clj" | "popc"
12438            | "some" | "not_any" | "not_every"
12439            | "comp" | "compose" | "partial" | "constantly" | "complement" | "compl"
12440            | "fnil" | "juxt"
12441            | "memoize" | "memo" | "curry" | "once"
12442            | "deep_clone" | "dclone" | "deep_merge" | "dmerge" | "deep_equal" | "deq"
12443            | "iterate" | "iter" | "repeatedly" | "rptd" | "cycle" | "cyc"
12444            | "mapcat" | "mcat" | "keep" | "kp" | "remove_clj" | "remc"
12445            | "reductions" | "rdcs"
12446            | "partition_by" | "pby" | "partition_all" | "pall"
12447            | "split_at" | "spat" | "split_with" | "spw"
12448            | "assoc" | "dissoc" | "get_in" | "gin" | "assoc_in" | "ain" | "update_in" | "uin"
12449            | "into" | "empty_clj" | "empc" | "seq" | "vec_clj" | "vecc"
12450            | "apply" | "appl"
12451            // ── python/ruby stdlib ───────────────────────────────────────────
12452            | "divmod" | "dm" | "accumulate" | "accum" | "starmap" | "smap"
12453            | "zip_longest" | "zipl" | "zip_fill" | "zipf" | "combinations" | "comb" | "permutations" | "perm"
12454            | "cartesian_product" | "cprod" | "compress" | "cmpr" | "filterfalse" | "falf"
12455            | "islice" | "isl" | "chain_from" | "chfr" | "pairwise_iter" | "pwi"
12456            | "tee_iter" | "teei" | "groupby_iter" | "gbi"
12457            | "each_slice" | "eslice" | "each_cons" | "econs"
12458            | "one" | "none_match" | "nonem"
12459            | "find_index_fn" | "fidx" | "rindex_fn" | "ridx"
12460            | "minmax" | "mmx" | "minmax_by" | "mmxb"
12461            | "dig" | "values_at" | "vat" | "fetch_val" | "fv" | "slice_arr" | "sla"
12462            | "transform_keys" | "tkeys" | "transform_values" | "tvals"
12463            | "sum_by" | "sumb" | "uniq_by" | "uqb"
12464            | "flat_map_fn" | "fmf" | "then_fn" | "thfn" | "times_fn" | "timf"
12465            | "step" | "upto" | "downto"
12466            // ── javascript array/object methods ─────────────────────────────
12467            | "find_last" | "fndl" | "find_last_index" | "fndli"
12468            | "at_index" | "ati" | "replace_at" | "repa"
12469            | "to_sorted" | "tsrt" | "to_reversed" | "trev" | "to_spliced" | "tspl"
12470            | "flat_depth" | "fltd" | "fill_arr" | "filla" | "includes_val" | "incv"
12471            | "object_keys" | "okeys" | "object_values" | "ovals"
12472            | "object_entries" | "oents" | "object_from_entries" | "ofents"
12473            // ── haskell list functions ──────────────────────────────────────
12474            | "span_fn" | "spanf" | "break_fn" | "brkf" | "group_runs" | "gruns"
12475            | "nub" | "sort_on" | "srton"
12476            | "intersperse_val" | "isp" | "intercalate" | "ical"
12477            | "replicate_val" | "repv" | "elem_of" | "elof" | "not_elem" | "ntelm"
12478            | "lookup_assoc" | "lkpa" | "scanl" | "scanr" | "unfoldr" | "unfr"
12479            // ── rust iterator methods ───────────────────────────────────────
12480            | "find_map" | "fndm" | "filter_map" | "fltm" | "fold_right" | "fldr"
12481            | "partition_either" | "peith" | "try_fold" | "tfld"
12482            | "map_while" | "mapw" | "inspect" | "insp"
12483            // ── ruby enumerable extras ──────────────────────────────────────
12484            | "tally_by" | "talb" | "sole" | "chunk_while" | "chkw" | "count_while" | "cntw"
12485            // ── go/general functional utilities ─────────────────────────────
12486            | "insert_at" | "insa" | "delete_at" | "dela" | "update_at" | "upda"
12487            | "split_on" | "spon" | "words_from" | "wfrm" | "unwords" | "unwds"
12488            | "lines_from" | "lfrm" | "unlines" | "unlns"
12489            | "window_n" | "winn" | "adjacent_pairs" | "adjp"
12490            | "zip_all" | "zall" | "unzip_pairs" | "uzp"
12491            | "interpose" | "ipos" | "partition_n" | "partn"
12492            | "map_indexed" | "mapi" | "reduce_indexed" | "redi" | "filter_indexed" | "flti"
12493            | "group_by_fn" | "gbf" | "index_by" | "idxb" | "associate" | "assoc_fn"
12494            // ── additional missing stdlib functions ─────────────────────────
12495            | "combinations_rep" | "combrep" | "inits" | "tails" | "subsequences" | "subseqs"
12496            | "nub_by" | "nubb" | "slice_when" | "slcw" | "slice_before" | "slcb" | "slice_after" | "slca"
12497            | "each_with_object" | "ewo" | "reduce_right" | "redr"
12498            | "is_sorted_by" | "issrtb" | "intersperse_with" | "ispw"
12499            | "running_reduce" | "runred" | "windowed_circular" | "wincirc"
12500            | "distinct_by" | "distb" | "average" | "mean" | "copy_within" | "cpyw"
12501            | "and_list" | "andl" | "or_list" | "orl" | "concat_map" | "cmap"
12502            | "elem_index" | "elidx" | "elem_indices" | "elidxs" | "find_indices" | "fndidxs"
12503            | "delete_first" | "delfst" | "delete_by" | "delby" | "insert_sorted" | "inssrt"
12504            | "union_list" | "unionl" | "intersect_list" | "intl"
12505            | "maximum_by" | "maxby" | "minimum_by" | "minby" | "batched" | "btch"
12506            // ── Extended stdlib: Text Processing ─────────────────────────────
12507            | "match_all" | "mall" | "capture_groups" | "capg" | "is_match" | "ism"
12508            | "split_regex" | "splre" | "replace_regex" | "replre"
12509            | "is_ascii" | "isasc" | "to_ascii" | "toasc"
12510            | "char_at" | "chat" | "code_point_at" | "cpat" | "from_code_point" | "fcp"
12511            | "normalize_spaces" | "nrmsp" | "remove_whitespace" | "rmws"
12512            | "pluralize" | "plur" | "ordinalize" | "ordn"
12513            | "parse_int" | "pint" | "parse_float" | "pflt" | "parse_bool" | "pbool"
12514            | "levenshtein" | "lev" | "soundex" | "sdx" | "similarity" | "sim"
12515            | "common_prefix" | "cpfx" | "common_suffix" | "csfx"
12516            | "wrap_text" | "wrpt" | "dedent" | "ddt" | "indent" | "idt"
12517            // ── Extended stdlib: Advanced Numeric ────────────────────────────
12518            | "lerp" | "inv_lerp" | "ilerp" | "smoothstep" | "smst" | "remap"
12519            | "dot_product" | "dotp" | "cross_product" | "crossp"
12520            | "matrix_mul" | "matmul" | "mm"
12521            | "magnitude" | "mag" | "normalize_vec" | "nrmv"
12522            | "distance" | "dist" | "manhattan_distance" | "mdist"
12523            | "covariance" | "cov" | "correlation" | "corr"
12524            | "iqr" | "quantile" | "qntl" | "clamp_int" | "clpi"
12525            | "in_range" | "inrng" | "wrap_range" | "wrprng"
12526            | "sum_squares" | "sumsq" | "rms" | "cumsum" | "csum" | "cumprod" | "cprod_acc" | "diff"
12527            // ── Extended stdlib: Date/Time ───────────────────────────────────
12528            | "add_days" | "addd" | "add_hours" | "addh" | "add_minutes" | "addm"
12529            | "diff_days" | "diffd" | "diff_hours" | "diffh"
12530            | "start_of_day" | "sod" | "end_of_day" | "eod"
12531            | "start_of_hour" | "soh" | "start_of_minute" | "som"
12532            // ── Extended stdlib: Encoding/Hashing ────────────────────────────
12533            | "urle" | "urld"
12534            | "html_encode" | "htmle" | "html_decode" | "htmld"
12535            | "adler32" | "adl32" | "fnv1a" | "djb2"
12536            // ── Extended stdlib: Validation ──────────────────────────────────
12537            | "is_credit_card" | "iscc" | "is_isbn10" | "isbn10" | "is_isbn13" | "isbn13"
12538            | "is_iban" | "isiban" | "is_hex_str" | "ishex" | "is_binary_str" | "isbin"
12539            | "is_octal_str" | "isoct" | "is_json" | "isjson" | "is_base64" | "isb64"
12540            | "is_semver" | "issv" | "is_slug" | "isslug" | "slugify" | "slug"
12541            // ── Extended stdlib: Collection Advanced ─────────────────────────
12542            | "mode_stat" | "mstat" | "sampn" | "weighted_sample" | "wsamp"
12543            | "shuffle_arr" | "shuf" | "argmax" | "amax" | "argmin" | "amin"
12544            | "argsort" | "asrt" | "rank" | "rnk" | "dense_rank" | "drnk"
12545            | "partition_point" | "ppt" | "lower_bound" | "lbound"
12546            | "upper_bound" | "ubound" | "equal_range" | "eqrng"
12547            // ── Extended stdlib: Matrix Operations ───────────────────────────
12548            | "matrix_add" | "madd" | "matrix_sub" | "msub" | "matrix_mult" | "mmult"
12549            | "matrix_scalar" | "mscal" | "matrix_identity" | "mident"
12550            | "matrix_zeros" | "mzeros" | "matrix_ones" | "mones"
12551            | "matrix_diag" | "mdiag" | "matrix_trace" | "mtrace"
12552            | "matrix_row" | "mrow" | "matrix_col" | "mcol"
12553            | "matrix_shape" | "mshape" | "matrix_det" | "mdet"
12554            | "matrix_scale" | "mat_scale" | "diagonal" | "diag"
12555            // ── Extended stdlib: Graph Algorithms ────────────────────────────
12556            | "topological_sort" | "toposort" | "bfs_traverse" | "bfs"
12557            | "dfs_traverse" | "dfs" | "shortest_path_bfs" | "spbfs"
12558            | "connected_components_graph" | "ccgraph"
12559            | "has_cycle_graph" | "hascyc" | "is_bipartite_graph" | "isbip"
12560            // ── Extended stdlib: Data Validation ─────────────────────────────
12561            | "is_ipv4_addr" | "isip4" | "is_ipv6_addr" | "isip6"
12562            | "is_mac_addr" | "ismac" | "is_port_num" | "isport"
12563            | "is_hostname_valid" | "ishost"
12564            | "is_iso_date" | "isisodt" | "is_iso_time" | "isisotm"
12565            | "is_iso_datetime" | "isisodtm"
12566            | "is_phone_num" | "isphone" | "is_us_zip" | "iszip"
12567            // ── Extended stdlib: String Utilities Novel ──────────────────────
12568            | "word_wrap_text" | "wwrap" | "center_text" | "ctxt"
12569            | "ljust_text" | "ljt" | "rjust_text" | "rjt" | "zfill_num" | "zfill"
12570            | "remove_all_str" | "rmall" | "replace_n_times" | "repln"
12571            | "find_all_indices" | "fndalli"
12572            | "text_between" | "txbtwn" | "text_before" | "txbef" | "text_after" | "txaft"
12573            | "text_before_last" | "txbefl" | "text_after_last" | "txaftl"
12574            // ── Extended stdlib: Math Novel ──────────────────────────────────
12575            | "is_even_num" | "iseven" | "is_odd_num" | "isodd"
12576            | "is_positive_num" | "ispos" | "is_negative_num" | "isneg"
12577            | "is_zero_num" | "iszero" | "is_whole_num" | "iswhole"
12578            | "log_with_base" | "logb" | "nth_root_of" | "nroot"
12579            | "frac_part" | "fracp" | "reciprocal_of" | "recip"
12580            | "copy_sign" | "cpsgn" | "fused_mul_add" | "fmadd"
12581            | "floor_mod" | "fmod" | "floor_div_op" | "fdivop"
12582            | "signum_of" | "sgnum" | "midpoint_of" | "midpt"
12583            // ── Extended stdlib batch 3: Array Analysis ──────────────────────
12584            | "longest_run" | "lrun" | "longest_increasing" | "linc"
12585            | "longest_decreasing" | "ldec" | "max_sum_subarray" | "maxsub"
12586            | "majority_element" | "majority" | "kth_largest" | "kthl"
12587            | "kth_smallest" | "kths" | "count_inversions" | "cinv"
12588            | "is_monotonic" | "ismono" | "equilibrium_index" | "eqidx"
12589            // ── Extended stdlib batch 3: Set Operations ──────────────────────
12590            | "jaccard_index" | "jaccard" | "dice_coefficient" | "dicecoef"
12591            | "overlap_coefficient" | "overlapcoef"
12592            | "power_set" | "powerset" | "cartesian_power" | "cartpow"
12593            // ── Extended stdlib batch 3: Advanced String ─────────────────────
12594            | "is_isogram" | "isiso" | "is_heterogram" | "ishet"
12595            | "hamdist" | "jaro_similarity" | "jarosim"
12596            | "longest_common_substring" | "lcsub"
12597            | "longest_common_subsequence" | "lcseq"
12598            | "count_words" | "wcount" | "count_lines" | "lcount"
12599            | "count_chars" | "ccount" | "count_bytes" | "bcount"
12600            // ── Extended stdlib batch 3: More Math ───────────────────────────
12601            | "binomial" | "binom" | "catalan" | "catn" | "pascal_row" | "pascrow"
12602            | "is_coprime" | "iscopr" | "euler_totient" | "etot"
12603            | "mobius" | "mob" | "is_squarefree" | "issqfr"
12604            | "digital_root" | "digroot" | "is_narcissistic" | "isnarc"
12605            | "is_harshad" | "isharsh" | "is_kaprekar" | "iskap"
12606            // ── Extended stdlib batch 3: Date/Time Additional ────────────────
12607            | "day_of_year" | "doy" | "week_of_year" | "woy"
12608            | "days_in_month_fn" | "daysinmo" | "is_valid_date" | "isvdate"
12609            | "age_in_years" | "ageyrs"
12610            // ── functional combinators ──────────────────────────────────────
12611
12612            | "when_true" | "when_false" | "if_else" | "clamp_fn"
12613            | "attempt" | "try_fn" | "safe_div" | "safe_mod" | "safe_sqrt" | "safe_log"
12614            | "juxt2" | "juxt3" | "tap_val" | "debug_val" | "converge"
12615            | "iterate_n" | "unfold" | "arity_of" | "is_callable"
12616            | "coalesce" | "default_to" | "fallback"
12617            | "apply_list" | "zip_apply" | "scan"
12618            | "keep_if" | "reject_if" | "group_consecutive"
12619            | "after_n" | "before_n" | "clamp_list" | "normalize_list" | "softmax"
12620
12621            // ── matrix / linear algebra ─────────────────────────────────────
12622
12623
12624            | "matrix_multiply" | "mat_mul"
12625            | "identity_matrix" | "eye" | "zeros_matrix" | "zeros" | "ones_matrix" | "ones"
12626
12627
12628
12629            | "vec_normalize" | "unit_vec" | "vec_add" | "vec_sub" | "vec_scale"
12630            | "linspace" | "arange"
12631            // ── more regex ──────────────────────────────────────────────────
12632            | "re_test" | "re_find_all" | "re_groups" | "re_escape"
12633            | "re_split_limit" | "glob_to_regex" | "is_regex_valid"
12634            // ── more process / system ───────────────────────────────────────
12635            | "cwd" | "pwd_str" | "cpu_count" | "is_root" | "uptime_secs"
12636            | "env_pairs" | "env_set" | "env_remove" | "hostname_str" | "is_tty" | "signal_name"
12637            // ── data structure helpers ───────────────────────────────────────
12638            | "stack_new" | "queue_new" | "lru_new"
12639            | "counter" | "counter_most_common" | "defaultdict" | "ordered_set"
12640            | "bitset_new" | "bitset_set" | "bitset_test" | "bitset_clear"
12641            // ── trivial numeric helpers (batch 4) ─────────────────────────────
12642            | "abs_ceil" | "abs_each" | "abs_floor" | "ceil_each" | "dec_each"
12643            | "double_each" | "floor_each" | "half_each" | "inc_each" | "length_each"
12644            | "negate_each" | "not_each" | "offset_each" | "reverse_each" | "round_each"
12645            | "scale_each" | "sqrt_each" | "square_each" | "to_float_each" | "to_int_each"
12646            | "trim_each" | "type_each" | "upcase_each" | "downcase_each" | "bool_each"
12647            // ── math / physics constants ──────────────────────────────────────
12648            | "avogadro" | "boltzmann" | "golden_ratio" | "gravity" | "ln10" | "ln2"
12649            | "planck" | "speed_of_light" | "sqrt2"
12650            // ── physics formulas ──────────────────────────────────────────────
12651            | "bmi_calc" | "compound_interest" | "dew_point" | "discount_amount"
12652            | "force_mass_acc" | "freq_wavelength" | "future_value" | "haversine"
12653            | "heat_index" | "kinetic_energy" | "margin_price" | "markup_price"
12654            | "mortgage_payment" | "ohms_law_i" | "ohms_law_r" | "ohms_law_v"
12655            | "potential_energy" | "present_value" | "simple_interest" | "speed_distance_time"
12656            | "tax_amount" | "tip_amount" | "wavelength_freq" | "wind_chill"
12657            // ── math functions ────────────────────────────────────────────────
12658            | "angle_between_deg" | "approx_eq" | "chebyshev_distance" | "copysign"
12659            | "cosine_similarity" | "cube_root" | "entropy" | "float_bits" | "fma"
12660            | "int_bits" | "jaccard_similarity" | "log_base" | "mae" | "mse" | "nth_root"
12661            | "r_squared" | "reciprocal" | "relu" | "rmse" | "rotate_point" | "round_to"
12662            | "sigmoid" | "signum" | "square_root"
12663            // ── sequences ─────────────────────────────────────────────────────
12664            | "cubes_seq" | "fibonacci_seq" | "powers_of_seq" | "primes_seq"
12665            | "squares_seq" | "triangular_seq"
12666            // ── string helpers (batch 4) ──────────────────────────────────────
12667            | "alternate_case" | "angle_bracket" | "bracket" | "byte_length"
12668            | "bytes_to_hex_str" | "camel_words" | "char_length" | "chars_to_string"
12669            | "chomp_str" | "chop_str" | "filter_chars" | "from_csv_line" | "hex_to_bytes"
12670            | "insert_str" | "intersperse_char" | "ljust" | "map_chars" | "mirror_string"
12671            | "normalize_whitespace" | "only_alnum" | "only_alpha" | "only_ascii"
12672            | "only_digits" | "parenthesize" | "remove_str" | "repeat_string" | "rjust"
12673            | "sentence_case" | "string_count" | "string_sort" | "string_to_chars"
12674            | "string_unique_chars" | "substring" | "to_csv_line" | "trim_left" | "trim_right"
12675            | "xor_strings"
12676            // ── list helpers (batch 4) ─────────────────────────────────────────
12677            | "adjacent_difference" | "append_elem" | "consecutive_pairs" | "contains_elem"
12678            | "count_elem" | "drop_every" | "duplicate_count" | "elem_at" | "find_first"
12679            | "first_elem" | "flatten_once" | "fold_left" | "from_digits" | "from_pairs"
12680            | "group_by_size" | "hash_filter_keys" | "hash_from_list" | "hash_map_values"
12681            | "hash_merge_deep" | "hash_to_list" | "hash_zip" | "head_n" | "histogram_bins"
12682            | "index_of_elem" | "init_list" | "interleave_lists" | "last_elem" | "least_common"
12683            | "list_compact" | "list_eq" | "list_flatten_deep" | "max_list" | "mean_list"
12684            | "min_list" | "mode_list" | "most_common" | "partition_two" | "prefix_sums"
12685            | "prepend" | "product_list" | "remove_at" | "remove_elem" | "remove_first_elem"
12686            | "repeat_elem" | "running_max" | "running_min" | "sample_one" | "scan_left"
12687            | "second_elem" | "span" | "suffix_sums" | "sum_list" | "tail_n" | "take_every"
12688            | "third_elem" | "to_array" | "to_pairs" | "trimmed_mean" | "unique_count_of"
12689            | "wrap_index" | "digits_of"
12690            // ── predicates (batch 4) ──────────────────────────────────────────
12691            | "all_match" | "any_match" | "is_between" | "is_blank_or_nil" | "is_divisible_by"
12692            | "is_email" | "is_even" | "is_falsy" | "is_fibonacci" | "is_hex_color"
12693            | "is_in_range" | "is_ipv4" | "is_multiple_of" | "is_negative" | "is_nil"
12694            | "is_nonzero" | "is_odd" | "is_perfect_square" | "is_positive" | "is_power_of"
12695            | "is_prefix" | "is_present" | "is_strictly_decreasing" | "is_strictly_increasing"
12696            | "is_suffix" | "is_triangular" | "is_truthy" | "is_url" | "is_whole" | "is_zero"
12697            // ── counters (batch 4) ────────────────────────────────────────────
12698            | "count_digits" | "count_letters" | "count_lower" | "count_match"
12699            | "count_punctuation" | "count_spaces" | "count_upper" | "defined_count"
12700            | "empty_count" | "falsy_count" | "nonempty_count" | "numeric_count"
12701            | "truthy_count" | "undef_count"
12702            // ── conversion / utility (batch 4) ────────────────────────────────
12703            | "assert_type" | "between" | "clamp_each" | "die_if" | "die_unless"
12704            | "join_colons" | "join_commas" | "join_dashes" | "join_dots" | "join_lines"
12705            | "join_pipes" | "join_slashes" | "join_spaces" | "join_tabs" | "measure"
12706            | "max_float" | "min_float" | "noop_val" | "nop" | "pass" | "pred" | "succ"
12707            | "tap_debug" | "to_bool" | "to_float" | "to_int" | "to_string" | "void"
12708            | "range_exclusive" | "range_inclusive"
12709            // ── math / numeric (uncategorized batch) ────────────────────────────
12710            | "aliquot_sum" | "autocorrelation" | "bell_number" | "cagr" | "coeff_of_variation"
12711            | "collatz_length" | "collatz_sequence" | "convolution" | "cross_entropy"
12712            | "depreciation_double" | "depreciation_linear" | "discount" | "divisors"
12713            | "epsilon" | "euclidean_distance" | "euler_number" | "exponential_moving_average"
12714            | "f64_max" | "f64_min" | "fft_magnitude" | "goldbach" | "i64_max" | "i64_min"
12715            | "kurtosis" | "linear_regression" | "look_and_say" | "lucas" | "luhn_check"
12716            | "mean_absolute_error" | "mean_squared_error" | "median_absolute_deviation"
12717            | "minkowski_distance" | "moving_average" | "multinomial" | "neg_inf" | "npv"
12718            | "num_divisors" | "partition_number" | "pascals_triangle" | "skewness"
12719            | "standard_error" | "subfactorial" | "sum_divisors" | "totient_sum"
12720            | "tribonacci" | "weighted_mean" | "winsorize"
12721            // ── statistics (extended) ─────────────────────────────────────────
12722            | "chi_square_stat" | "describe" | "five_number_summary"
12723            | "gini" | "gini_coefficient" | "lorenz_curve" | "outliers_iqr"
12724            | "percentile_rank" | "quartiles" | "sample_stddev" | "sample_variance"
12725            | "spearman_correlation" | "t_test_one_sample" | "t_test_two_sample"
12726            | "z_score" | "z_scores"
12727            // ── number theory / primes ──────────────────────────────────────────
12728            | "abundant_numbers" | "deficient_numbers" | "is_abundant" | "is_deficient"
12729            | "is_pentagonal" | "is_perfect" | "is_smith" | "next_prime" | "nth_prime"
12730            | "pentagonal_number" | "perfect_numbers" | "prev_prime" | "prime_factors"
12731            | "prime_pi" | "primes_up_to" | "triangular_number" | "twin_primes"
12732            // ── geometry / physics ──────────────────────────────────────────────
12733            | "area_circle" | "area_ellipse" | "area_rectangle" | "area_trapezoid" | "area_triangle"
12734            | "bearing" | "circumference" | "cone_volume" | "cylinder_volume" | "heron_area"
12735            | "midpoint" | "perimeter_rectangle" | "perimeter_triangle" | "point_distance"
12736            | "polygon_area" | "slope" | "sphere_surface" | "sphere_volume" | "triangle_hypotenuse"
12737            // ── geometry (extended) ───────────────────────────────────────────
12738            | "angle_between" | "arc_length" | "bounding_box" | "centroid"
12739            | "circle_from_three_points" | "convex_hull" | "ellipse_perimeter"
12740            | "frustum_volume" | "haversine_distance" | "line_intersection"
12741            | "point_in_polygon" | "polygon_perimeter" | "pyramid_volume"
12742            | "reflect_point" | "scale_point" | "sector_area"
12743            | "torus_surface" | "torus_volume" | "translate_point"
12744            | "vector_angle" | "vector_cross" | "vector_dot" | "vector_magnitude" | "vector_normalize"
12745            // ── constants ───────────────────────────────────────────────────────
12746            | "avogadro_number" | "boltzmann_constant" | "electron_mass" | "elementary_charge"
12747            | "gravitational_constant" | "phi" | "pi" | "planck_constant" | "proton_mass"
12748            | "sol" | "tau"
12749            // ── finance ─────────────────────────────────────────────────────────
12750            | "bac_estimate" | "bmi" | "break_even" | "margin" | "markup" | "roi" | "tax" | "tip"
12751            // ── finance (extended) ────────────────────────────────────────────
12752            | "amortization_schedule" | "black_scholes_call" | "black_scholes_put"
12753            | "bond_price" | "bond_yield" | "capm" | "continuous_compound"
12754            | "discounted_payback" | "duration" | "irr"
12755            | "max_drawdown" | "modified_duration" | "nper" | "num_periods" | "payback_period"
12756            | "pmt" | "pv" | "rule_of_72" | "sharpe_ratio" | "sortino_ratio"
12757            | "wacc" | "xirr"
12758            // ── string processing (uncategorized batch) ─────────────────────────
12759            | "acronym" | "atbash" | "bigrams" | "camel_to_snake" | "char_frequencies"
12760            | "chunk_string" | "collapse_whitespace" | "dedent_text" | "indent_text"
12761            | "initials" | "leetspeak" | "mask_string" | "ngrams" | "pig_latin"
12762            | "remove_consonants" | "remove_vowels" | "reverse_each_word" | "snake_to_camel"
12763            | "sort_words" | "string_distance" | "string_multiply" | "strip_html"
12764            | "trigrams" | "unique_words" | "word_frequencies" | "zalgo"
12765            // ── encoding / phonetics ────────────────────────────────────────────
12766            | "braille_encode" | "double_metaphone" | "metaphone" | "morse_decode"
12767            | "morse_encode" | "nato_phonetic" | "phonetic_digit" | "subscript" | "superscript"
12768            | "to_emoji_num"
12769            // ── roman numerals ──────────────────────────────────────────────────
12770            | "int_to_roman" | "roman_add" | "roman_numeral_list" | "roman_to_int"
12771            // ── base / gray code ────────────────────────────────────────────────
12772            | "base_convert" | "binary_to_gray" | "gray_code_sequence" | "gray_to_binary"
12773            // ── color operations ────────────────────────────────────────────────
12774            | "ansi_256" | "ansi_truecolor" | "color_blend" | "color_complement"
12775            | "color_darken" | "color_distance" | "color_grayscale" | "color_invert"
12776            | "color_lighten" | "hsl_to_rgb" | "hsv_to_rgb" | "random_color"
12777            | "rgb_to_hsl" | "rgb_to_hsv"
12778            // ── matrix operations (uncategorized batch) ─────────────────────────
12779            | "matrix_flatten" | "matrix_from_rows" | "matrix_hadamard" | "matrix_inverse"
12780            | "matrix_map" | "matrix_max" | "matrix_min" | "matrix_power" | "matrix_sum"
12781            | "matrix_transpose"
12782            // ── array / list operations (uncategorized batch) ───────────────────
12783            | "binary_insert" | "bucket" | "clamp_array" | "group_consecutive_by"
12784            | "histogram" | "merge_sorted" | "next_permutation" | "normalize_array"
12785            | "normalize_range" | "peak_detect" | "range_compress" | "range_expand"
12786            | "reservoir_sample" | "run_length_decode_str" | "run_length_encode_str"
12787            | "zero_crossings"
12788            // ── DSP / signal (extended) ───────────────────────────────────────
12789            | "apply_window" | "bandpass_filter" | "cross_correlation" | "dft"
12790            | "downsample" | "energy" | "envelope" | "highpass_filter" | "idft"
12791            | "lowpass_filter" | "median_filter" | "normalize_signal" | "phase_spectrum"
12792            | "power_spectrum" | "resample" | "spectral_centroid" | "spectrogram" | "upsample"
12793            | "window_blackman" | "window_hamming" | "window_hann" | "window_kaiser"
12794            // ── validation predicates (uncategorized batch) ─────────────────────
12795            | "is_anagram" | "is_balanced_parens" | "is_control" | "is_numeric_string"
12796            | "is_pangram" | "is_printable" | "is_valid_cidr" | "is_valid_cron"
12797            | "is_valid_hex_color" | "is_valid_latitude" | "is_valid_longitude" | "is_valid_mime"
12798            // ── algorithms / puzzles ────────────────────────────────────────────
12799            | "eval_rpn" | "fizzbuzz" | "game_of_life_step" | "mandelbrot_char"
12800            | "sierpinski" | "tower_of_hanoi" | "truth_table"
12801            // ── misc / utility ──────────────────────────────────────────────────
12802            | "byte_size" | "degrees_to_compass" | "to_string_val" | "type_of"
12803            // ── math formulas ───────────────────────────────────────────────────
12804            | "quadratic_roots" | "quadratic_discriminant" | "arithmetic_series"
12805            | "geometric_series" | "stirling_approx"
12806            | "double_factorial" | "rising_factorial" | "falling_factorial"
12807            | "gamma_approx" | "erf_approx" | "normal_pdf" | "normal_cdf"
12808            | "poisson_pmf" | "exponential_pdf" | "inverse_lerp"
12809            | "map_range"
12810            // ── physics formulas ────────────────────────────────────────────────
12811            | "momentum" | "impulse" | "work" | "power_phys" | "torque" | "angular_velocity"
12812            | "centripetal_force" | "escape_velocity" | "orbital_velocity" | "orbital_period"
12813            | "gravitational_force" | "coulomb_force" | "electric_field" | "capacitance"
12814            | "capacitor_energy" | "inductor_energy" | "resonant_frequency"
12815            | "rc_time_constant" | "rl_time_constant" | "impedance_rlc"
12816            | "relativistic_mass" | "lorentz_factor" | "time_dilation" | "length_contraction"
12817            | "relativistic_energy" | "rest_energy" | "de_broglie_wavelength"
12818            | "photon_energy" | "photon_energy_wavelength" | "schwarzschild_radius"
12819            | "stefan_boltzmann" | "wien_displacement" | "ideal_gas_pressure" | "ideal_gas_volume"
12820            | "projectile_range" | "projectile_max_height" | "projectile_time"
12821            | "spring_force" | "spring_energy" | "pendulum_period" | "doppler_frequency"
12822            | "decibel_ratio" | "snells_law" | "brewster_angle" | "critical_angle"
12823            | "lens_power" | "thin_lens" | "magnification_lens"
12824            // ── math constants ──────────────────────────────────────────────────
12825            | "euler_mascheroni" | "apery_constant" | "feigenbaum_delta" | "feigenbaum_alpha"
12826            | "catalan_constant" | "khinchin_constant" | "glaisher_constant"
12827            | "plastic_number" | "silver_ratio" | "supergolden_ratio"
12828            // ── physics constants ───────────────────────────────────────────────
12829            | "vacuum_permittivity" | "vacuum_permeability" | "coulomb_constant"
12830            | "fine_structure_constant" | "rydberg_constant" | "bohr_radius"
12831            | "bohr_magneton" | "nuclear_magneton" | "stefan_boltzmann_constant"
12832            | "wien_constant" | "gas_constant" | "faraday_constant" | "neutron_mass"
12833            | "atomic_mass_unit" | "earth_mass" | "earth_radius" | "sun_mass" | "sun_radius"
12834            | "astronomical_unit" | "light_year" | "parsec" | "hubble_constant"
12835            | "planck_length" | "planck_time" | "planck_mass" | "planck_temperature"
12836            // ── linear algebra (extended) ──────────────────────────────────
12837            | "matrix_solve" | "msolve" | "solve"
12838            | "matrix_lu" | "mlu" | "matrix_qr" | "mqr"
12839            | "matrix_eigenvalues" | "meig" | "eigenvalues" | "eig"
12840            | "matrix_norm" | "mnorm" | "matrix_cond" | "mcond" | "cond"
12841            | "matrix_pinv" | "mpinv" | "pinv"
12842            | "matrix_cholesky" | "mchol" | "cholesky"
12843            | "matrix_det_general" | "mdetg" | "det"
12844            // ── statistics tests (extended) ────────────────────────────────
12845            | "welch_ttest" | "welcht" | "paired_ttest" | "pairedt"
12846            | "cohen_d" | "cohend" | "anova_oneway" | "anova" | "anova1"
12847            | "spearman_corr" | "rho" | "kendall_tau" | "kendall" | "ktau"
12848            | "confidence_interval" | "ci"
12849            // ── distributions (extended) ──────────────────────────────────
12850            | "beta_pdf" | "betapdf" | "gamma_pdf" | "gammapdf"
12851            | "chi2_pdf" | "chi2pdf" | "chi_squared_pdf"
12852            | "t_pdf" | "tpdf" | "student_pdf"
12853            | "f_pdf" | "fpdf" | "fisher_pdf"
12854            | "lognormal_pdf" | "lnormpdf" | "weibull_pdf" | "weibpdf"
12855            | "cauchy_pdf" | "cauchypdf" | "laplace_pdf" | "laplacepdf"
12856            | "pareto_pdf" | "paretopdf"
12857            // ── interpolation & curve fitting ─────────────────────────────
12858            | "lagrange_interp" | "lagrange" | "linterp"
12859            | "cubic_spline" | "cspline" | "spline"
12860            | "poly_eval" | "polyval" | "polynomial_fit" | "polyfit"
12861            // ── numerical integration & differentiation ───────────────────
12862            | "trapz" | "trapezoid" | "simpson" | "simps"
12863            | "numerical_diff" | "numdiff" | "diff_array"
12864            | "cumtrapz" | "cumulative_trapz"
12865            // ── optimization / root finding ────────────────────────────────
12866            | "bisection" | "bisect" | "newton_method" | "newton" | "newton_raphson"
12867            | "golden_section" | "golden" | "gss"
12868            // ── ODE solvers ───────────────────────────────────────────────
12869            | "rk4" | "runge_kutta" | "rk4_ode" | "euler_ode" | "euler_method"
12870            // ── graph algorithms (extended) ────────────────────────────────
12871            | "dijkstra" | "shortest_path" | "bellman_ford" | "bellmanford"
12872            | "floyd_warshall" | "floydwarshall" | "apsp"
12873            | "prim_mst" | "mst" | "prim"
12874            // ── trig extensions ───────────────────────────────────────────
12875            | "cot" | "sec" | "csc" | "acot" | "asec" | "acsc" | "sinc" | "versin" | "versine"
12876            // ── ML activation functions ───────────────────────────────────
12877            | "leaky_relu" | "lrelu" | "elu" | "selu" | "gelu"
12878            | "silu" | "swish" | "mish" | "softplus"
12879            | "hard_sigmoid" | "hardsigmoid" | "hard_swish" | "hardswish"
12880            // ── special functions ─────────────────────────────────────────
12881            | "bessel_j0" | "j0" | "bessel_j1" | "j1"
12882            | "lambert_w" | "lambertw" | "productlog"
12883            // ── number theory (extended) ──────────────────────────────────
12884            | "mod_exp" | "modexp" | "powmod"
12885            | "mod_inv" | "modinv" | "chinese_remainder" | "crt"
12886            | "miller_rabin" | "millerrabin" | "is_probable_prime"
12887            // ── combinatorics (extended) ──────────────────────────────────
12888            | "derangements" | "stirling2" | "stirling_second"
12889            | "bernoulli_number" | "bernoulli" | "harmonic_number" | "harmonic"
12890            // ── physics (new) ─────────────────────────────────────────────
12891            | "drag_force" | "fdrag" | "ideal_gas" | "pv_nrt"
12892            // ── financial greeks & risk ───────────────────────────────────
12893            | "bs_delta" | "bsdelta" | "option_delta"
12894            | "bs_gamma" | "bsgamma" | "option_gamma"
12895            | "bs_vega" | "bsvega" | "option_vega"
12896            | "bs_theta" | "bstheta" | "option_theta"
12897            | "bs_rho" | "bsrho" | "option_rho"
12898            | "bond_duration" | "mac_duration"
12899            // ── DSP extensions ────────────────────────────────────────────
12900            | "dct" | "idct" | "goertzel" | "chirp" | "chirp_signal"
12901            // ── encoding extensions ───────────────────────────────────────
12902            | "base85_encode" | "b85e" | "ascii85_encode" | "a85e"
12903            | "base85_decode" | "b85d" | "ascii85_decode" | "a85d"
12904            // ── R base: distributions ─────────────────────────────────────
12905            | "pnorm" | "qnorm" | "pbinom" | "dbinom" | "ppois"
12906            | "punif" | "pexp" | "pweibull" | "plnorm" | "pcauchy"
12907            // ── R base: matrix ops ────────────────────────────────────────
12908            | "rbind" | "cbind"
12909            | "row_sums" | "rowSums" | "col_sums" | "colSums"
12910            | "row_means" | "rowMeans" | "col_means" | "colMeans"
12911            | "outer_product" | "outer" | "crossprod" | "tcrossprod"
12912            | "nrow" | "ncol" | "prop_table" | "proptable"
12913            // ── R base: vector ops ────────────────────────────────────────
12914            | "cummax" | "cummin" | "scale_vec" | "scale"
12915            | "which_fn" | "tabulate"
12916            | "duplicated" | "duped" | "rev_vec"
12917            | "seq_fn" | "rep_fn" | "rep"
12918            | "cut_bins" | "cut" | "find_interval" | "findInterval"
12919            | "ecdf_fn" | "ecdf" | "density_est" | "density"
12920            | "embed_ts" | "embed"
12921            // ── R base: stats tests ───────────────────────────────────────
12922            | "shapiro_test" | "shapiro" | "ks_test" | "ks"
12923            | "wilcox_test" | "wilcox" | "mann_whitney"
12924            | "prop_test" | "proptest" | "binom_test" | "binomtest"
12925            // ── R base: apply / functional ────────────────────────────────
12926            | "sapply" | "tapply" | "do_call" | "docall"
12927            // ── R base: ML / clustering ───────────────────────────────────
12928            | "kmeans" | "prcomp" | "pca"
12929            // ── R base: random generators ─────────────────────────────────
12930            | "rnorm" | "runif" | "rexp" | "rbinom" | "rpois" | "rgeom"
12931            | "rgamma" | "rbeta" | "rchisq" | "rt" | "rf"
12932            | "rweibull" | "rlnorm" | "rcauchy"
12933            // ── R base: quantile functions ────────────────────────────────
12934            | "qunif" | "qexp" | "qweibull" | "qlnorm" | "qcauchy"
12935            // ── R base: additional CDFs ───────────────────────────────────
12936            | "pgamma" | "pbeta" | "pchisq" | "pt_cdf" | "pt" | "pf_cdf" | "pf"
12937            // ── R base: additional PMFs ───────────────────────────────────
12938            | "dgeom" | "dunif" | "dnbinom" | "dhyper"
12939            // ── R base: smoothing / interpolation ─────────────────────────
12940            | "lowess" | "loess" | "approx_fn" | "approx"
12941            // ── R base: linear models ─────────────────────────────────────
12942            | "lm_fit" | "lm"
12943            // ── R base: remaining quantiles ───────────────────────────────
12944            | "qgamma" | "qbeta" | "qchisq" | "qt_fn" | "qt" | "qf_fn" | "qf"
12945            | "qbinom" | "qpois"
12946            // ── R base: time series ───────────────────────────────────────
12947            | "acf_fn" | "acf" | "pacf_fn" | "pacf"
12948            | "diff_lag" | "diff_ts" | "ts_filter" | "filter_ts"
12949            // ── R base: regression diagnostics ────────────────────────────
12950            | "predict_lm" | "predict" | "confint_lm" | "confint"
12951            // ── R base: multivariate stats ────────────────────────────────
12952            | "cor_matrix" | "cor_mat" | "cov_matrix" | "cov_mat"
12953            | "mahalanobis" | "mahal" | "dist_matrix" | "dist_mat"
12954            | "hclust" | "cutree" | "weighted_var" | "wvar" | "cov2cor"
12955            // ── SVG plotting ──────────────────────────────────────────────
12956            | "scatter_svg" | "scatter_plot" | "line_svg" | "line_plot"
12957            | "plot_svg" | "hist_svg" | "histogram_svg"
12958            | "boxplot_svg" | "box_plot" | "bar_svg" | "barchart_svg"
12959            | "pie_svg" | "pie_chart" | "heatmap_svg" | "heatmap"
12960            | "donut_svg" | "donut" | "area_svg" | "area_chart"
12961            | "hbar_svg" | "hbar" | "radar_svg" | "radar" | "spider"
12962            | "candlestick_svg" | "candlestick" | "ohlc"
12963            | "violin_svg" | "violin" | "cor_heatmap" | "cor_matrix_svg"
12964            | "stacked_bar_svg" | "stacked_bar"
12965            | "wordcloud_svg" | "wordcloud" | "wcloud"
12966            | "treemap_svg" | "treemap"
12967            | "pvw"
12968            // ── Cyberpunk terminal art ────────────────────────────────
12969            | "cyber_city" | "cyber_grid" | "cyber_rain" | "matrix_rain"
12970            | "cyber_glitch" | "glitch_text" | "cyber_banner" | "neon_banner"
12971            | "cyber_circuit" | "cyber_skull" | "cyber_eye"
12972            => Some(name),
12973            _ => None,
12974        }
12975    }
12976
12977    /// Reserved hash names that cannot be shadowed by user declarations.
12978    /// These are stryke's reflection hashes populated from builtins metadata.
12979    fn is_reserved_hash_name(name: &str) -> bool {
12980        matches!(
12981            name,
12982            "b" | "pc"
12983                | "e"
12984                | "a"
12985                | "d"
12986                | "c"
12987                | "p"
12988                | "all"
12989                | "stryke::builtins"
12990                | "stryke::perl_compats"
12991                | "stryke::extensions"
12992                | "stryke::aliases"
12993                | "stryke::descriptions"
12994                | "stryke::categories"
12995                | "stryke::primaries"
12996                | "stryke::all"
12997        )
12998    }
12999
13000    /// Check if a UDF name shadows a stryke builtin and error if so.
13001    /// Called only in non-compat mode — compat mode allows shadowing for Perl 5 parity.
13002    /// Reserved words that cannot be used as function names because they are
13003    /// lexer-level operators or language keywords that would be mis-tokenized.
13004    const RESERVED_FUNCTION_NAMES: &'static [&'static str] = &[
13005        "y",
13006        "tr",
13007        "s",
13008        "m",
13009        "q",
13010        "qq",
13011        "qw",
13012        "qx",
13013        "qr",
13014        "if",
13015        "unless",
13016        "while",
13017        "until",
13018        "for",
13019        "foreach",
13020        "given",
13021        "when",
13022        "else",
13023        "elsif",
13024        "do",
13025        "eval",
13026        "return",
13027        "last",
13028        "next",
13029        "redo",
13030        "goto",
13031        "my",
13032        "our",
13033        "local",
13034        "state",
13035        "sub",
13036        "fn",
13037        "class",
13038        "struct",
13039        "enum",
13040        "trait",
13041        "use",
13042        "no",
13043        "require",
13044        "package",
13045        "BEGIN",
13046        "END",
13047        "CHECK",
13048        "INIT",
13049        "UNITCHECK",
13050        "and",
13051        "or",
13052        "not",
13053        "x",
13054        "eq",
13055        "ne",
13056        "lt",
13057        "gt",
13058        "le",
13059        "ge",
13060        "cmp",
13061    ];
13062
13063    fn check_udf_shadows_builtin(&self, name: &str, line: usize) -> PerlResult<()> {
13064        // Only check bare names, not namespaced ones (Foo::y is allowed)
13065        if !name.contains("::") {
13066            if Self::RESERVED_FUNCTION_NAMES.contains(&name) {
13067                return Err(self.syntax_err(
13068                    format!("`{name}` is a reserved word and cannot be used as a function name"),
13069                    line,
13070                ));
13071            }
13072            if Self::is_known_bareword(name)
13073                || Self::is_try_builtin_name(name)
13074                || crate::list_builtins::is_list_builtin_name(name)
13075            {
13076                return Err(self.syntax_err(
13077                    format!(
13078"`{name}` is a stryke builtin and cannot be redefined (this is not Perl 5; use `fn` not `sub`, or pass --compat)"
13079                    ),
13080                    line,
13081                ));
13082            }
13083        }
13084        Ok(())
13085    }
13086
13087    /// Check if a hash name shadows a reserved stryke hash and error if so.
13088    /// Called only in non-compat mode.
13089    fn check_hash_shadows_reserved(&self, name: &str, line: usize) -> PerlResult<()> {
13090        if Self::is_reserved_hash_name(name) {
13091            return Err(self.syntax_err(
13092                format!(
13093"`%{name}` is a stryke reserved hash and cannot be redefined (this is not Perl 5; pass --compat for Perl 5 mode)"
13094                ),
13095                line,
13096            ));
13097        }
13098        Ok(())
13099    }
13100
13101    /// Validate assignment to %hash in non-compat mode.
13102    /// Rejects: scalar, string, arrayref, hashref, coderef, undef, odd-length list.
13103    fn validate_hash_assignment(&self, value: &Expr, line: usize) -> PerlResult<()> {
13104        match &value.kind {
13105            ExprKind::Integer(_) | ExprKind::Float(_) => {
13106                return Err(self.syntax_err(
13107                    "cannot assign scalar to hash — use %h = (key => value) or %h = %{$hashref}",
13108                    line,
13109                ));
13110            }
13111            ExprKind::String(_) | ExprKind::InterpolatedString(_) | ExprKind::Bareword(_) => {
13112                return Err(self.syntax_err(
13113                    "cannot assign string to hash — use %h = (key => value) or %h = %{$hashref}",
13114                    line,
13115                ));
13116            }
13117            ExprKind::ArrayRef(_) => {
13118                return Err(self.syntax_err(
13119                    "cannot assign arrayref to hash — use %h = @{$arrayref} for even-length list",
13120                    line,
13121                ));
13122            }
13123            ExprKind::ScalarRef(inner) => {
13124                if matches!(inner.kind, ExprKind::ArrayVar(_)) {
13125                    return Err(self.syntax_err(
13126                        "cannot assign \\@array to hash — use %h = @array for even-length list",
13127                        line,
13128                    ));
13129                }
13130                if matches!(inner.kind, ExprKind::HashVar(_)) {
13131                    return Err(self.syntax_err(
13132                        "cannot assign \\%hash to hash — use %h = %other directly",
13133                        line,
13134                    ));
13135                }
13136            }
13137            ExprKind::HashRef(_) => {
13138                return Err(self.syntax_err(
13139                    "cannot assign hashref to hash — use %h = %{$hashref} to dereference",
13140                    line,
13141                ));
13142            }
13143            ExprKind::CodeRef { .. } => {
13144                return Err(self.syntax_err("cannot assign coderef to hash", line));
13145            }
13146            ExprKind::Undef => {
13147                return Err(
13148                    self.syntax_err("cannot assign undef to hash — use %h = () to empty", line)
13149                );
13150            }
13151            ExprKind::List(items)
13152                if items.len() % 2 != 0
13153                    && !items.iter().any(|e| {
13154                        matches!(
13155                            e.kind,
13156                            ExprKind::ArrayVar(_)
13157                                | ExprKind::HashVar(_)
13158                                | ExprKind::FuncCall { .. }
13159                                | ExprKind::Deref { .. }
13160                                | ExprKind::ScalarVar(_)
13161                        )
13162                    }) =>
13163            {
13164                return Err(self.syntax_err(
13165                        format!(
13166                            "odd-length list ({} elements) in hash assignment — missing value for last key",
13167                            items.len()
13168                        ),
13169                        line,
13170                    ));
13171            }
13172            _ => {}
13173        }
13174        Ok(())
13175    }
13176
13177    /// Validate assignment to @array in non-compat mode.
13178    /// Rejects: undef (likely a mistake — use `@a = ()` to empty).
13179    /// Note: bare scalars like `@a = 2` are allowed since Perl coerces them to single-element lists.
13180    /// Note: `@a = {hashref}` is allowed as a common pattern for single-element arrays.
13181    fn validate_array_assignment(&self, value: &Expr, line: usize) -> PerlResult<()> {
13182        if let ExprKind::Undef = &value.kind {
13183            return Err(
13184                self.syntax_err("cannot assign undef to array — use @a = () to empty", line)
13185            );
13186        }
13187        Ok(())
13188    }
13189
13190    /// Validate assignment to $scalar in non-compat mode.
13191    /// Rejects: list literals (Perl 5 silently returns last element — footgun).
13192    fn validate_scalar_assignment(&self, value: &Expr, line: usize) -> PerlResult<()> {
13193        if let ExprKind::List(items) = &value.kind {
13194            if items.len() > 1 {
13195                return Err(self.syntax_err(
13196                    format!(
13197                        "cannot assign {}-element list to scalar — Perl 5 silently takes last element; use ($x) = (list) or $x = $list[-1]",
13198                        items.len()
13199                    ),
13200                    line,
13201                ));
13202            }
13203        }
13204        Ok(())
13205    }
13206
13207    /// Validate an assignment based on target type (in non-compat mode only).
13208    fn validate_assignment(&self, target: &Expr, value: &Expr, line: usize) -> PerlResult<()> {
13209        if crate::compat_mode() {
13210            return Ok(());
13211        }
13212        match &target.kind {
13213            ExprKind::HashVar(_) => self.validate_hash_assignment(value, line),
13214            ExprKind::ArrayVar(_) => self.validate_array_assignment(value, line),
13215            ExprKind::ScalarVar(_) => self.validate_scalar_assignment(value, line),
13216            _ => Ok(()),
13217        }
13218    }
13219
13220    /// Parse a block OR a blockless comparison expression for sort/psort/heap.
13221    /// Blockless: `$a <=> $b` or `$a cmp $b` or any expression → wrapped as a Block.
13222    /// Also accepts a bare function name: `psort my_cmp, @list`.
13223    fn parse_block_or_bareword_cmp_block(&mut self) -> PerlResult<Block> {
13224        if matches!(self.peek(), Token::LBrace) {
13225            return self.parse_block();
13226        }
13227        let line = self.peek_line();
13228        // Bare sub name: `psort my_cmp, @list`
13229        if let Token::Ident(ref name) = self.peek().clone() {
13230            if matches!(
13231                self.peek_at(1),
13232                Token::Comma | Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
13233            ) {
13234                let name = name.clone();
13235                self.advance();
13236                let body = Expr {
13237                    kind: ExprKind::FuncCall {
13238                        name,
13239                        args: vec![
13240                            Expr {
13241                                kind: ExprKind::ScalarVar("a".to_string()),
13242                                line,
13243                            },
13244                            Expr {
13245                                kind: ExprKind::ScalarVar("b".to_string()),
13246                                line,
13247                            },
13248                        ],
13249                    },
13250                    line,
13251                };
13252                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
13253            }
13254        }
13255        // Blockless expression: `$a <=> $b`, `$b cmp $a`, etc.
13256        let expr = self.parse_assign_expr_stop_at_pipe()?;
13257        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
13258    }
13259
13260    /// After `fan` / `fan_cap` `{ BLOCK }`, optional `, progress => EXPR` or `progress => EXPR` (no comma).
13261    fn parse_fan_optional_progress(
13262        &mut self,
13263        which: &'static str,
13264    ) -> PerlResult<Option<Box<Expr>>> {
13265        let line = self.peek_line();
13266        if self.eat(&Token::Comma) {
13267            match self.peek() {
13268                Token::Ident(ref kw)
13269                    if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) =>
13270                {
13271                    self.advance();
13272                    self.expect(&Token::FatArrow)?;
13273                    return Ok(Some(Box::new(self.parse_assign_expr()?)));
13274                }
13275                _ => {
13276                    return Err(self.syntax_err(
13277                        format!("{which}: expected `progress => EXPR` after comma"),
13278                        line,
13279                    ));
13280                }
13281            }
13282        }
13283        if let Token::Ident(ref kw) = self.peek().clone() {
13284            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
13285                self.advance();
13286                self.expect(&Token::FatArrow)?;
13287                return Ok(Some(Box::new(self.parse_assign_expr()?)));
13288            }
13289        }
13290        Ok(None)
13291    }
13292
13293    /// Comma-separated assign expressions with optional trailing `, progress => EXPR`
13294    /// (for `pmap_chunked`, `psort`, etc.).
13295    ///
13296    /// Paren-less — individual parts parse through
13297    /// [`Self::parse_assign_expr_stop_at_pipe`] so a trailing `|>` is left for
13298    /// the enclosing pipe-forward loop (left-associative chaining).
13299    fn parse_assign_expr_list_optional_progress(&mut self) -> PerlResult<(Expr, Option<Expr>)> {
13300        // On the RHS of `|>`, list-taking builtins may be written bare with no
13301        // operand — `@a |> uniq`, `@a |> flatten`, `foo(bar, @a |> psort)`, etc.
13302        // When the next token is a list-terminator, yield an empty placeholder
13303        // list; [`Self::pipe_forward_apply`] substitutes the piped LHS at
13304        // desugar time, so the placeholder is never evaluated.
13305        if self.in_pipe_rhs()
13306            && matches!(
13307                self.peek(),
13308                Token::Semicolon
13309                    | Token::RBrace
13310                    | Token::RParen
13311                    | Token::Eof
13312                    | Token::PipeForward
13313                    | Token::Comma
13314            )
13315        {
13316            return Ok((self.pipe_placeholder_list(self.peek_line()), None));
13317        }
13318        let mut parts = vec![self.parse_assign_expr_stop_at_pipe()?];
13319        loop {
13320            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
13321                break;
13322            }
13323            if matches!(
13324                self.peek(),
13325                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
13326            ) {
13327                break;
13328            }
13329            if self.peek_is_postfix_stmt_modifier_keyword() {
13330                break;
13331            }
13332            if let Token::Ident(ref kw) = self.peek().clone() {
13333                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
13334                    self.advance();
13335                    self.expect(&Token::FatArrow)?;
13336                    let prog = self.parse_assign_expr_stop_at_pipe()?;
13337                    return Ok((merge_expr_list(parts), Some(prog)));
13338                }
13339            }
13340            parts.push(self.parse_assign_expr_stop_at_pipe()?);
13341        }
13342        Ok((merge_expr_list(parts), None))
13343    }
13344
13345    fn parse_one_arg(&mut self) -> PerlResult<Expr> {
13346        if matches!(self.peek(), Token::LParen) {
13347            self.advance();
13348            let expr = self.parse_expression()?;
13349            self.expect(&Token::RParen)?;
13350            Ok(expr)
13351        } else {
13352            self.parse_assign_expr_stop_at_pipe()
13353        }
13354    }
13355
13356    fn parse_one_arg_or_default(&mut self) -> PerlResult<Expr> {
13357        // Default to `$_` when the next token cannot start an argument expression
13358        // because it has lower precedence than a named unary operator. Perl 5
13359        // named unary precedence sits above ternary / comparison / logical / bitwise
13360        // / assignment / list ops; everything below should terminate the implicit
13361        // argument and let the surrounding expression continue.
13362        // See `perldoc perlop` ("Named Unary Operators").
13363        if matches!(
13364            self.peek(),
13365            // Statement / list / call boundaries
13366            Token::Semicolon
13367                | Token::RBrace
13368                | Token::RParen
13369                | Token::RBracket
13370                | Token::Eof
13371                | Token::Comma
13372                | Token::FatArrow
13373                | Token::PipeForward
13374            // Ternary `? :`
13375                | Token::Question
13376                | Token::Colon
13377            // Comparison / equality (numeric + string)
13378                | Token::NumEq | Token::NumNe | Token::NumLt | Token::NumGt
13379                | Token::NumLe | Token::NumGe | Token::Spaceship
13380                | Token::StrEq | Token::StrNe | Token::StrLt | Token::StrGt
13381                | Token::StrLe | Token::StrGe | Token::StrCmp
13382            // Logical (symbolic and word forms) + defined-or
13383                | Token::LogAnd | Token::LogOr | Token::LogNot
13384                | Token::LogAndWord | Token::LogOrWord | Token::LogNotWord
13385                | Token::DefinedOr
13386            // Range (lower precedence than named unary)
13387                | Token::Range | Token::RangeExclusive
13388            // Assignment (any compound form)
13389                | Token::Assign | Token::PlusAssign | Token::MinusAssign
13390                | Token::MulAssign | Token::DivAssign | Token::ModAssign
13391                | Token::PowAssign | Token::DotAssign | Token::AndAssign
13392                | Token::OrAssign | Token::XorAssign | Token::DefinedOrAssign
13393                | Token::ShiftLeftAssign | Token::ShiftRightAssign
13394                | Token::BitAndAssign | Token::BitOrAssign
13395        ) {
13396            return Ok(Expr {
13397                kind: ExprKind::ScalarVar("_".into()),
13398                line: self.peek_line(),
13399            });
13400        }
13401        // `f()` — empty parens default to `$_`, matching Perl 5 semantics.
13402        // `perldoc -f length`: "If EXPR is omitted, returns the length of $_."
13403        // Perl accepts both `length` and `length()` as `length($_)`.
13404        if matches!(self.peek(), Token::LParen) && matches!(self.peek_at(1), Token::RParen) {
13405            let line = self.peek_line();
13406            self.advance(); // (
13407            self.advance(); // )
13408            return Ok(Expr {
13409                kind: ExprKind::ScalarVar("_".into()),
13410                line,
13411            });
13412        }
13413        self.parse_one_arg()
13414    }
13415
13416    /// Array operand for `shift` / `pop`: default `@_`, or `shift(@a)` / `shift()` (empty parens = `@_`).
13417    fn parse_one_arg_or_argv(&mut self) -> PerlResult<Expr> {
13418        let line = self.prev_line(); // line where shift/pop keyword was
13419        if matches!(self.peek(), Token::LParen) {
13420            self.advance();
13421            if matches!(self.peek(), Token::RParen) {
13422                self.advance();
13423                return Ok(Expr {
13424                    kind: ExprKind::ArrayVar("_".into()),
13425                    line: self.peek_line(),
13426                });
13427            }
13428            let expr = self.parse_expression()?;
13429            self.expect(&Token::RParen)?;
13430            return Ok(expr);
13431        }
13432        // Implicit semicolon: if next token is on a different line, don't consume it
13433        if matches!(
13434            self.peek(),
13435            Token::Semicolon
13436                | Token::RBrace
13437                | Token::RParen
13438                | Token::Eof
13439                | Token::Comma
13440                | Token::PipeForward
13441        ) || self.peek_line() > line
13442        {
13443            Ok(Expr {
13444                kind: ExprKind::ArrayVar("_".into()),
13445                line,
13446            })
13447        } else {
13448            self.parse_assign_expr()
13449        }
13450    }
13451
13452    fn parse_builtin_args(&mut self) -> PerlResult<Vec<Expr>> {
13453        if matches!(self.peek(), Token::LParen) {
13454            self.advance();
13455            let args = self.parse_arg_list()?;
13456            self.expect(&Token::RParen)?;
13457            Ok(args)
13458        } else if self.suppress_parenless_call > 0 && matches!(self.peek(), Token::Ident(_)) {
13459            // In thread context, don't consume barewords as arguments
13460            // so `t filesf sorted ep` parses `sorted` as a stage, not an arg to filesf
13461            Ok(vec![])
13462        } else {
13463            self.parse_list_until_terminator()
13464        }
13465    }
13466
13467    /// Check if the next token is `=>` (fat arrow). If so, the preceding bareword
13468    /// should be treated as an auto-quoted string (hash key), not a function call.
13469    /// Returns `Some(Expr::String(name))` if fat arrow follows, `None` otherwise.
13470    #[inline]
13471    fn fat_arrow_autoquote(&self, name: &str, line: usize) -> Option<Expr> {
13472        if matches!(self.peek(), Token::FatArrow) {
13473            Some(Expr {
13474                kind: ExprKind::String(name.to_string()),
13475                line,
13476            })
13477        } else {
13478            None
13479        }
13480    }
13481
13482    /// Parse a hash subscript key inside `{…}`.
13483    ///
13484    /// Perl auto-quotes a single bareword before `}`, even for keywords:
13485    /// `$h{print}`, `$r->{f}` etc. all yield the string key.
13486    fn parse_hash_subscript_key(&mut self) -> PerlResult<Expr> {
13487        let line = self.peek_line();
13488        if let Token::Ident(ref k) = self.peek().clone() {
13489            if matches!(self.peek_at(1), Token::RBrace) {
13490                let s = k.clone();
13491                self.advance();
13492                return Ok(Expr {
13493                    kind: ExprKind::String(s),
13494                    line,
13495                });
13496            }
13497        }
13498        self.parse_expression()
13499    }
13500
13501    /// `progress` introducing the optional `progress => EXPR` suffix for `glob_par` / `par_sed`.
13502    #[inline]
13503    fn peek_is_glob_par_progress_kw(&self) -> bool {
13504        matches!(self.peek(), Token::Ident(ref kw) if kw == "progress")
13505            && matches!(self.peek_at(1), Token::FatArrow)
13506    }
13507
13508    /// Pattern list for `glob_par` / `par_sed` inside `(...)`, stopping before `)` or `progress =>`.
13509    fn parse_pattern_list_until_rparen_or_progress(&mut self) -> PerlResult<Vec<Expr>> {
13510        let mut args = Vec::new();
13511        loop {
13512            if matches!(self.peek(), Token::RParen | Token::Eof) {
13513                break;
13514            }
13515            if self.peek_is_glob_par_progress_kw() {
13516                break;
13517            }
13518            args.push(self.parse_assign_expr()?);
13519            match self.peek() {
13520                Token::RParen => break,
13521                Token::Comma => {
13522                    self.advance();
13523                    if matches!(self.peek(), Token::RParen) {
13524                        break;
13525                    }
13526                    if self.peek_is_glob_par_progress_kw() {
13527                        break;
13528                    }
13529                }
13530                _ => {
13531                    return Err(self.syntax_err(
13532                        "expected `,`, `)`, or `progress =>` after argument in `glob_par` / `par_sed`",
13533                        self.peek_line(),
13534                    ));
13535                }
13536            }
13537        }
13538        Ok(args)
13539    }
13540
13541    /// Paren-less pattern list for `glob_par` / `par_sed`, stopping before stmt end or `progress =>`.
13542    fn parse_pattern_list_glob_par_bare(&mut self) -> PerlResult<Vec<Expr>> {
13543        let mut args = Vec::new();
13544        loop {
13545            if matches!(
13546                self.peek(),
13547                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
13548            ) {
13549                break;
13550            }
13551            if self.peek_is_postfix_stmt_modifier_keyword() {
13552                break;
13553            }
13554            if self.peek_is_glob_par_progress_kw() {
13555                break;
13556            }
13557            args.push(self.parse_assign_expr()?);
13558            if !self.eat(&Token::Comma) {
13559                break;
13560            }
13561            if self.peek_is_glob_par_progress_kw() {
13562                break;
13563            }
13564        }
13565        Ok(args)
13566    }
13567
13568    /// `glob_pat EXPR, ...` or `glob_pat(...)` plus optional `, progress => EXPR` / inner `progress =>`.
13569    fn parse_glob_par_or_par_sed_args(&mut self) -> PerlResult<(Vec<Expr>, Option<Box<Expr>>)> {
13570        if matches!(self.peek(), Token::LParen) {
13571            self.advance();
13572            let args = self.parse_pattern_list_until_rparen_or_progress()?;
13573            let progress = if self.peek_is_glob_par_progress_kw() {
13574                self.advance();
13575                self.expect(&Token::FatArrow)?;
13576                Some(Box::new(self.parse_assign_expr()?))
13577            } else {
13578                None
13579            };
13580            self.expect(&Token::RParen)?;
13581            Ok((args, progress))
13582        } else {
13583            let args = self.parse_pattern_list_glob_par_bare()?;
13584            // Comma after the last pattern was consumed inside `parse_pattern_list_glob_par_bare`.
13585            let progress = if self.peek_is_glob_par_progress_kw() {
13586                self.advance();
13587                self.expect(&Token::FatArrow)?;
13588                Some(Box::new(self.parse_assign_expr()?))
13589            } else {
13590                None
13591            };
13592            Ok((args, progress))
13593        }
13594    }
13595
13596    pub(crate) fn parse_arg_list(&mut self) -> PerlResult<Vec<Expr>> {
13597        let mut args = Vec::new();
13598        // Inside `(...)`, `|>` is a normal operator again (e.g. `f(2 |> g, 3)`),
13599        // so shadow any outer paren-less-arg suppression from
13600        // `no_pipe_forward_depth`. Saturating so nested mixes are safe.
13601        let saved_no_pf = self.no_pipe_forward_depth;
13602        self.no_pipe_forward_depth = 0;
13603        while !matches!(
13604            self.peek(),
13605            Token::RParen | Token::RBracket | Token::RBrace | Token::Eof
13606        ) {
13607            let arg = match self.parse_assign_expr() {
13608                Ok(e) => e,
13609                Err(err) => {
13610                    self.no_pipe_forward_depth = saved_no_pf;
13611                    return Err(err);
13612                }
13613            };
13614            args.push(arg);
13615            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
13616                break;
13617            }
13618        }
13619        self.no_pipe_forward_depth = saved_no_pf;
13620        Ok(args)
13621    }
13622
13623    /// Parse a comma-separated list of slice subscript args. Each arg may be a regular
13624    /// expression, a closed range (`1:3`, `1..3:2`), or an open-ended Python-style colon
13625    /// range (`:`, `::`, `:N`, `N:`, `::-1`, `:N:M`, `N::M`, `::M`). Open-ended forms
13626    /// produce `ExprKind::SliceRange`; closed `1:3` produces `ExprKind::Range` (legacy).
13627    ///
13628    /// `is_hash` enables fat-comma-style bareword auto-quoting for endpoints — `{a:c:1}`
13629    /// treats `a` and `c` as string keys without quoting (cannot be a function call;
13630    /// use `func():other` if you actually want to invoke).
13631    pub(crate) fn parse_slice_arg_list(&mut self, is_hash: bool) -> PerlResult<Vec<Expr>> {
13632        let mut args = Vec::new();
13633        let saved_no_pf = self.no_pipe_forward_depth;
13634        self.no_pipe_forward_depth = 0;
13635        while !matches!(
13636            self.peek(),
13637            Token::RParen | Token::RBracket | Token::RBrace | Token::Eof
13638        ) {
13639            let arg = match self.parse_slice_arg(is_hash) {
13640                Ok(e) => e,
13641                Err(err) => {
13642                    self.no_pipe_forward_depth = saved_no_pf;
13643                    return Err(err);
13644                }
13645            };
13646            args.push(arg);
13647            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
13648                break;
13649            }
13650        }
13651        self.no_pipe_forward_depth = saved_no_pf;
13652        Ok(args)
13653    }
13654
13655    /// Parse one slice subscript argument (see [`Self::parse_slice_arg_list`]).
13656    fn parse_slice_arg(&mut self, is_hash: bool) -> PerlResult<Expr> {
13657        let line = self.peek_line();
13658
13659        // Open-start: `:` or `::` immediately
13660        if matches!(self.peek(), Token::Colon) {
13661            self.advance();
13662            return self.finish_slice_range(None, false, is_hash, line);
13663        }
13664        if matches!(self.peek(), Token::PackageSep) {
13665            self.advance();
13666            return self.finish_slice_range(None, true, is_hash, line);
13667        }
13668
13669        // Parse FROM with `:` suppressed inside `parse_range` so it doesn't get
13670        // consumed as a colon-range there — we want to handle the colon ourselves.
13671        self.suppress_colon_range = self.suppress_colon_range.saturating_add(1);
13672        let result = self.parse_slice_endpoint(is_hash);
13673        self.suppress_colon_range = self.suppress_colon_range.saturating_sub(1);
13674        let from_expr = result?;
13675
13676        // Trailing `:` or `::` after the FROM endpoint?
13677        if matches!(self.peek(), Token::Colon) {
13678            self.advance();
13679            return self.finish_slice_range(Some(Box::new(from_expr)), false, is_hash, line);
13680        }
13681        if matches!(self.peek(), Token::PackageSep) {
13682            self.advance();
13683            return self.finish_slice_range(Some(Box::new(from_expr)), true, is_hash, line);
13684        }
13685
13686        Ok(from_expr)
13687    }
13688
13689    /// After consuming the first colon (or `::` pair), parse the rest of the slice range.
13690    /// `double` is true if we just consumed `::` — TO is implicit `None`, the next
13691    /// expression (if any) is STEP.
13692    ///
13693    /// Returns `ExprKind::Range` for fully-closed forms (legacy compatibility) and
13694    /// `ExprKind::SliceRange` whenever any endpoint is omitted (open-ended).
13695    fn finish_slice_range(
13696        &mut self,
13697        from: Option<Box<Expr>>,
13698        double: bool,
13699        is_hash: bool,
13700        line: usize,
13701    ) -> PerlResult<Expr> {
13702        let (to, step) = if double {
13703            // `::` so TO is implicit; STEP is whatever (if anything) follows.
13704            let step_v = self.parse_slice_optional_endpoint(is_hash)?;
13705            (None, step_v)
13706        } else {
13707            // single `:` — parse TO, then optional `:STEP`.
13708            let to_v = self.parse_slice_optional_endpoint(is_hash)?;
13709            let step_v = if matches!(self.peek(), Token::Colon) {
13710                self.advance();
13711                self.parse_slice_optional_endpoint(is_hash)?
13712            } else if matches!(self.peek(), Token::PackageSep) {
13713                return Err(
13714                    self.syntax_err("Unexpected `::` after slice TO endpoint".to_string(), line)
13715                );
13716            } else {
13717                None
13718            };
13719            (to_v, step_v)
13720        };
13721
13722        // Closed form (both endpoints present) — produce a regular `Range` so the
13723        // rest of the compiler/VM keeps reusing existing range-expansion paths.
13724        if let (Some(f), Some(t)) = (from.as_ref(), to.as_ref()) {
13725            return Ok(Expr {
13726                kind: ExprKind::Range {
13727                    from: f.clone(),
13728                    to: t.clone(),
13729                    exclusive: false,
13730                    step,
13731                },
13732                line,
13733            });
13734        }
13735
13736        Ok(Expr {
13737            kind: ExprKind::SliceRange { from, to, step },
13738            line,
13739        })
13740    }
13741
13742    /// Parse an optional slice endpoint: returns `None` if the next token closes the slice
13743    /// arg (`,`, `]`, `}`, or another `:`). Otherwise parses an endpoint expression.
13744    fn parse_slice_optional_endpoint(&mut self, is_hash: bool) -> PerlResult<Option<Box<Expr>>> {
13745        if matches!(
13746            self.peek(),
13747            Token::Colon
13748                | Token::PackageSep
13749                | Token::Comma
13750                | Token::RBracket
13751                | Token::RBrace
13752                | Token::Eof
13753        ) {
13754            return Ok(None);
13755        }
13756        self.suppress_colon_range = self.suppress_colon_range.saturating_add(1);
13757        let r = self.parse_slice_endpoint(is_hash);
13758        self.suppress_colon_range = self.suppress_colon_range.saturating_sub(1);
13759        Ok(Some(Box::new(r?)))
13760    }
13761
13762    /// Parse a single slice endpoint expression. For hash slices, a bareword `Ident`
13763    /// followed by `:`, `::`, `,`, `]`, or `}` auto-quotes (fat-comma style); otherwise
13764    /// fall through to standard expression parsing. For array slices, no auto-quote.
13765    fn parse_slice_endpoint(&mut self, is_hash: bool) -> PerlResult<Expr> {
13766        if is_hash {
13767            if let Token::Ident(name) = self.peek().clone() {
13768                if matches!(
13769                    self.peek_at(1),
13770                    Token::Colon
13771                        | Token::PackageSep
13772                        | Token::Comma
13773                        | Token::RBracket
13774                        | Token::RBrace
13775                ) {
13776                    let line = self.peek_line();
13777                    self.advance();
13778                    return Ok(Expr {
13779                        kind: ExprKind::String(name),
13780                        line,
13781                    });
13782                }
13783            }
13784        }
13785        self.parse_assign_expr()
13786    }
13787
13788    /// Arguments for `->name` / `->SUPER::name` **without** `(...)`. Unlike `die foo + 1`
13789    /// (unary `+` on `1` passed to `foo`), Perl treats `$o->meth + 5` as infix `+` after a
13790    /// no-arg method call; we must not consume that `+` as the start of a first argument.
13791    fn parse_method_arg_list_no_paren(&mut self) -> PerlResult<Vec<Expr>> {
13792        let mut args = Vec::new();
13793        let call_line = self.prev_line();
13794        loop {
13795            // `$g->next { ... }` — `{` starts the enclosing statement's block, not an anonymous
13796            // hash argument to `next` (paren-less method call has no args here).
13797            if args.is_empty() && matches!(self.peek(), Token::LBrace) {
13798                break;
13799            }
13800            if matches!(
13801                self.peek(),
13802                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
13803            ) {
13804                break;
13805            }
13806            if let Token::Ident(ref kw) = self.peek().clone() {
13807                if matches!(
13808                    kw.as_str(),
13809                    "if" | "unless" | "while" | "until" | "for" | "foreach"
13810                ) {
13811                    break;
13812                }
13813            }
13814            // `foo($obj->meth, $x)` — comma separates *outer* args; it is not the start of a
13815            // paren-less method argument (those use spaces: `$obj->meth $a, $b`).
13816            if args.is_empty()
13817                && (self.peek_method_arg_infix_terminator() || matches!(self.peek(), Token::Comma))
13818            {
13819                break;
13820            }
13821            // Implicit semicolon: if no args collected yet and next token is on a different
13822            // line, treat newline as statement boundary. Allows `$p->method\nnext_stmt`.
13823            if args.is_empty() && self.peek_line() > call_line {
13824                break;
13825            }
13826            args.push(self.parse_assign_expr()?);
13827            if !self.eat(&Token::Comma) {
13828                break;
13829            }
13830        }
13831        Ok(args)
13832    }
13833
13834    /// Tokens that end a paren-less method arg list when no comma-separated args yet (infix on
13835    /// the whole `->meth` expression).
13836    fn peek_method_arg_infix_terminator(&self) -> bool {
13837        matches!(
13838            self.peek(),
13839            Token::Plus
13840                | Token::Minus
13841                | Token::Star
13842                | Token::Slash
13843                | Token::Percent
13844                | Token::Power
13845                | Token::Dot
13846                | Token::X
13847                | Token::NumEq
13848                | Token::NumNe
13849                | Token::NumLt
13850                | Token::NumGt
13851                | Token::NumLe
13852                | Token::NumGe
13853                | Token::Spaceship
13854                | Token::StrEq
13855                | Token::StrNe
13856                | Token::StrLt
13857                | Token::StrGt
13858                | Token::StrLe
13859                | Token::StrGe
13860                | Token::StrCmp
13861                | Token::LogAnd
13862                | Token::LogOr
13863                | Token::LogAndWord
13864                | Token::LogOrWord
13865                | Token::DefinedOr
13866                | Token::BitAnd
13867                | Token::BitOr
13868                | Token::BitXor
13869                | Token::ShiftLeft
13870                | Token::ShiftRight
13871                | Token::Range
13872                | Token::RangeExclusive
13873                | Token::BindMatch
13874                | Token::BindNotMatch
13875                | Token::Arrow
13876                // `($a->b) ? $a->c : $a->d` — `->c` must not slurp the ternary `:` / `?`.
13877                | Token::Question
13878                | Token::Colon
13879                // Assignment operators: `$obj->field = val` is setter sugar, not method arg.
13880                | Token::Assign
13881                | Token::PlusAssign
13882                | Token::MinusAssign
13883                | Token::MulAssign
13884                | Token::DivAssign
13885                | Token::ModAssign
13886                | Token::PowAssign
13887                | Token::DotAssign
13888                | Token::AndAssign
13889                | Token::OrAssign
13890                | Token::XorAssign
13891                | Token::DefinedOrAssign
13892                | Token::ShiftLeftAssign
13893                | Token::ShiftRightAssign
13894                | Token::BitAndAssign
13895                | Token::BitOrAssign
13896        )
13897    }
13898
13899    fn parse_list_until_terminator(&mut self) -> PerlResult<Vec<Expr>> {
13900        let mut args = Vec::new();
13901        // Line of the last consumed token (the keyword / function name that
13902        // triggered this arg parse).  Used for implicit-semicolon: if no args
13903        // have been parsed yet and the next token is on a *different* line,
13904        // treat the newline as a statement boundary and stop.
13905        let call_line = self.prev_line();
13906        loop {
13907            if matches!(
13908                self.peek(),
13909                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
13910            ) {
13911                break;
13912            }
13913            // Check for postfix modifiers — stop before `expr for LIST` / `expr if COND` etc.
13914            if let Token::Ident(ref kw) = self.peek().clone() {
13915                if matches!(
13916                    kw.as_str(),
13917                    "if" | "unless" | "while" | "until" | "for" | "foreach"
13918                ) {
13919                    break;
13920                }
13921            }
13922            // Implicit semicolons: if no args have been collected yet and the
13923            // next token is on a different line from the call keyword, treat
13924            // the newline as a statement boundary.  This prevents paren-less
13925            // calls (`say`, `print`, user subs) from greedily swallowing the
13926            // *next* statement when the author omitted a semicolon.
13927            // After a comma continuation, multi-line arg lists still work.
13928            if args.is_empty() && self.peek_line() > call_line {
13929                break;
13930            }
13931            // Paren-less builtin args: `|>` terminates the whole call list, so
13932            // individual args must not absorb a following `|>`.
13933            args.push(self.parse_assign_expr_stop_at_pipe()?);
13934            if !self.eat(&Token::Comma) {
13935                break;
13936            }
13937        }
13938        Ok(args)
13939    }
13940
13941    fn try_parse_hash_ref(&mut self) -> PerlResult<Vec<(Expr, Expr)>> {
13942        let mut pairs = Vec::new();
13943        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
13944            // Perl autoquotes a bareword immediately before `=>` (hash key), even for keywords like
13945            // `pos`, `bless`, `return` — see Text::Balanced `_failmsg` (`pos => $pos`).
13946            let line = self.peek_line();
13947            let key = if let Token::Ident(ref name) = self.peek().clone() {
13948                if matches!(self.peek_at(1), Token::FatArrow) {
13949                    self.advance();
13950                    Expr {
13951                        kind: ExprKind::String(name.clone()),
13952                        line,
13953                    }
13954                } else {
13955                    self.parse_assign_expr()?
13956                }
13957            } else {
13958                self.parse_assign_expr()?
13959            };
13960            // If the key expression is a hash/array variable and is followed by `}` or `,`
13961            // with no `=>`, treat the whole thing as a hash-from-expression construction.
13962            // This handles `{ %a }`, `{ %a, key => val }`, etc.
13963            if matches!(self.peek(), Token::RBrace | Token::Comma)
13964                && matches!(
13965                    key.kind,
13966                    ExprKind::HashVar(_)
13967                        | ExprKind::Deref {
13968                            kind: Sigil::Hash,
13969                            ..
13970                        }
13971                )
13972            {
13973                // Synthesize a pair whose key/value is spread from the hash expression.
13974                // Use a sentinel "spread" pair: key=the hash expr, value=undef.
13975                // The evaluator will flatten this.
13976                let sentinel_key = Expr {
13977                    kind: ExprKind::String("__HASH_SPREAD__".into()),
13978                    line,
13979                };
13980                pairs.push((sentinel_key, key));
13981                self.eat(&Token::Comma);
13982                continue;
13983            }
13984            // Expect => or , after key
13985            if self.eat(&Token::FatArrow) || self.eat(&Token::Comma) {
13986                let val = self.parse_assign_expr()?;
13987                pairs.push((key, val));
13988                self.eat(&Token::Comma);
13989            } else {
13990                return Err(self.syntax_err("Expected => or , in hash ref", key.line));
13991            }
13992        }
13993        self.expect(&Token::RBrace)?;
13994        Ok(pairs)
13995    }
13996
13997    /// Parse `key => val, key => val, ...` up to (but not consuming) `term`.
13998    /// Used by the `%[…]` and `%{k=>v,…}` sugar to build an inline hashref
13999    /// AST node, sidestepping the block/hashref ambiguity that `try_parse_hash_ref`
14000    /// navigates. Caller expects and consumes `term` itself.
14001    fn parse_hashref_pairs_until(&mut self, term: &Token) -> PerlResult<Vec<(Expr, Expr)>> {
14002        let mut pairs = Vec::new();
14003        while !matches!(&self.peek(), t if std::mem::discriminant(*t) == std::mem::discriminant(term))
14004            && !matches!(self.peek(), Token::Eof)
14005        {
14006            let line = self.peek_line();
14007            let key = if let Token::Ident(ref name) = self.peek().clone() {
14008                if matches!(self.peek_at(1), Token::FatArrow) {
14009                    self.advance();
14010                    Expr {
14011                        kind: ExprKind::String(name.clone()),
14012                        line,
14013                    }
14014                } else {
14015                    self.parse_assign_expr()?
14016                }
14017            } else {
14018                self.parse_assign_expr()?
14019            };
14020            if self.eat(&Token::FatArrow) || self.eat(&Token::Comma) {
14021                let val = self.parse_assign_expr()?;
14022                pairs.push((key, val));
14023                self.eat(&Token::Comma);
14024            } else {
14025                return Err(self.syntax_err("Expected => or , in hash ref", key.line));
14026            }
14027        }
14028        Ok(pairs)
14029    }
14030
14031    /// Inside an interpolated string, after a `$name`/`${EXPR}`/`$name[i]`/`$name{k}` base
14032    /// expression, consume any chain of `->[…]`, `->{…}`, **adjacent** `[…]`, or `{…}`
14033    /// subscripts. Perl auto-implies `->` between consecutive subscripts, so
14034    /// `$matrix[1][1]` is `$matrix[1]->[1]` and `$h{a}{b}` is `$h{a}->{b}`.
14035    /// Each step wraps the current expression in an `ArrowDeref`.
14036    fn interp_chain_subscripts(
14037        &self,
14038        chars: &[char],
14039        i: &mut usize,
14040        mut base: Expr,
14041        line: usize,
14042    ) -> Expr {
14043        loop {
14044            // Optional `->` connector
14045            let (after, requires_subscript) =
14046                if *i + 1 < chars.len() && chars[*i] == '-' && chars[*i + 1] == '>' {
14047                    (*i + 2, true)
14048                } else {
14049                    (*i, false)
14050                };
14051            if after >= chars.len() {
14052                break;
14053            }
14054            match chars[after] {
14055                '[' => {
14056                    *i = after + 1;
14057                    let mut idx_str = String::new();
14058                    while *i < chars.len() && chars[*i] != ']' {
14059                        idx_str.push(chars[*i]);
14060                        *i += 1;
14061                    }
14062                    if *i < chars.len() {
14063                        *i += 1;
14064                    }
14065                    let idx_expr = if let Some(rest) = idx_str.strip_prefix('$') {
14066                        Expr {
14067                            kind: ExprKind::ScalarVar(rest.to_string()),
14068                            line,
14069                        }
14070                    } else if let Ok(n) = idx_str.parse::<i64>() {
14071                        Expr {
14072                            kind: ExprKind::Integer(n),
14073                            line,
14074                        }
14075                    } else {
14076                        Expr {
14077                            kind: ExprKind::String(idx_str),
14078                            line,
14079                        }
14080                    };
14081                    base = Expr {
14082                        kind: ExprKind::ArrowDeref {
14083                            expr: Box::new(base),
14084                            index: Box::new(idx_expr),
14085                            kind: DerefKind::Array,
14086                        },
14087                        line,
14088                    };
14089                }
14090                '{' => {
14091                    *i = after + 1;
14092                    let mut key = String::new();
14093                    let mut depth = 1usize;
14094                    while *i < chars.len() && depth > 0 {
14095                        if chars[*i] == '{' {
14096                            depth += 1;
14097                        } else if chars[*i] == '}' {
14098                            depth -= 1;
14099                            if depth == 0 {
14100                                break;
14101                            }
14102                        }
14103                        key.push(chars[*i]);
14104                        *i += 1;
14105                    }
14106                    if *i < chars.len() {
14107                        *i += 1;
14108                    }
14109                    let key_expr = if let Some(rest) = key.strip_prefix('$') {
14110                        Expr {
14111                            kind: ExprKind::ScalarVar(rest.to_string()),
14112                            line,
14113                        }
14114                    } else {
14115                        Expr {
14116                            kind: ExprKind::String(key),
14117                            line,
14118                        }
14119                    };
14120                    base = Expr {
14121                        kind: ExprKind::ArrowDeref {
14122                            expr: Box::new(base),
14123                            index: Box::new(key_expr),
14124                            kind: DerefKind::Hash,
14125                        },
14126                        line,
14127                    };
14128                }
14129                _ => {
14130                    if requires_subscript {
14131                        // `->method()` etc — not interpolated, leave for literal output.
14132                    }
14133                    break;
14134                }
14135            }
14136        }
14137        base
14138    }
14139
14140    /// Reject `$a` / `$b` references in `--no-interop` mode (lexer catches them
14141    /// outside double-quoted strings; this catches the in-string interpolation
14142    /// path which has its own parser bypassing `Token::ScalarVar`).
14143    fn no_interop_check_scalar_var_name(&self, name: &str, line: usize) -> PerlResult<()> {
14144        if crate::no_interop_mode() && (name == "a" || name == "b") {
14145            return Err(self.syntax_err(
14146                format!(
14147                    "stryke uses `$_0` / `$_1` instead of `${}` (--no-interop is active)",
14148                    name
14149                ),
14150                line,
14151            ));
14152        }
14153        Ok(())
14154    }
14155
14156    fn parse_interpolated_string(&self, s: &str, line: usize) -> PerlResult<Expr> {
14157        // Parse $var and @var inside double-quoted strings
14158        let mut parts = Vec::new();
14159        let mut literal = String::new();
14160        let chars: Vec<char> = s.chars().collect();
14161        let mut i = 0;
14162
14163        'istr: while i < chars.len() {
14164            if chars[i] == LITERAL_DOLLAR_IN_DQUOTE {
14165                literal.push('$');
14166                i += 1;
14167                continue;
14168            }
14169            // "\\$x" in source: one backslash in the string, then interpolate $x (Perl double-quoted string).
14170            if chars[i] == '\\' && i + 1 < chars.len() && chars[i + 1] == '$' {
14171                literal.push('\\');
14172                i += 1;
14173                // i now points at '$' — fall through to $ handling below
14174            }
14175            if chars[i] == '$' && i + 1 < chars.len() {
14176                if !literal.is_empty() {
14177                    parts.push(StringPart::Literal(std::mem::take(&mut literal)));
14178                }
14179                i += 1; // past `$`
14180                        // Perl allows whitespace between `$` and the variable name (`$ foo` → `$foo`).
14181                while i < chars.len() && chars[i].is_whitespace() {
14182                    i += 1;
14183                }
14184                if i >= chars.len() {
14185                    return Err(self.syntax_err("Final $ should be \\$ or $name", line));
14186                }
14187                // `$#name` — last index of `@name` (Perl `$#array`).
14188                if chars[i] == '#' {
14189                    i += 1;
14190                    let mut sname = String::from("#");
14191                    while i < chars.len()
14192                        && (chars[i].is_alphanumeric() || chars[i] == '_' || chars[i] == ':')
14193                    {
14194                        sname.push(chars[i]);
14195                        i += 1;
14196                    }
14197                    while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
14198                        sname.push_str("::");
14199                        i += 2;
14200                        while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
14201                            sname.push(chars[i]);
14202                            i += 1;
14203                        }
14204                    }
14205                    self.no_interop_check_scalar_var_name(&sname, line)?;
14206                    parts.push(StringPart::ScalarVar(sname));
14207                    continue;
14208                }
14209                // `$$` — process id (Perl `$$`), only when the two `$` are adjacent (no whitespace
14210                // between) and the second `$` is not followed by a word character or digit (`$$x`
14211                // / `$$_` / `$$0` are `$` + `$x` / `$_` / `$0`).
14212                if chars[i] == '$' {
14213                    let next_c = chars.get(i + 1).copied();
14214                    let is_pid = match next_c {
14215                        None => true,
14216                        Some(c)
14217                            if !c.is_ascii_digit() && !matches!(c, 'A'..='Z' | 'a'..='z' | '_') =>
14218                        {
14219                            true
14220                        }
14221                        _ => false,
14222                    };
14223                    if is_pid {
14224                        parts.push(StringPart::ScalarVar("$$".to_string()));
14225                        i += 1; // consume second `$`
14226                        continue;
14227                    }
14228                    i += 1; // skip second `$` — same as a single `$` before the identifier
14229                }
14230                if chars[i] == '{' {
14231                    // `${…}` — braced variable OR expression interpolation.
14232                    //   `${name}`              → ScalarVar(name)        (Perl standard)
14233                    //   `${$ref}` / `${\EXPR}` → deref the expression   (Perl standard)
14234                    //   `${name}[idx]` / `${name}{k}` / `${$r}[i]` …    chain after `}`
14235                    // stryke's prior `#{expr}` form remains supported elsewhere.
14236                    i += 1;
14237                    let mut inner = String::new();
14238                    let mut depth = 1usize;
14239                    while i < chars.len() && depth > 0 {
14240                        match chars[i] {
14241                            '{' => depth += 1,
14242                            '}' => {
14243                                depth -= 1;
14244                                if depth == 0 {
14245                                    break;
14246                                }
14247                            }
14248                            _ => {}
14249                        }
14250                        inner.push(chars[i]);
14251                        i += 1;
14252                    }
14253                    if i < chars.len() {
14254                        i += 1; // skip closing }
14255                    }
14256
14257                    // Distinguish "name" from "expression". If trimmed inner starts with
14258                    // `$`, `\`, or contains operator/punctuation chars, treat as Perl
14259                    // expression and emit a scalar deref. Otherwise, plain variable name.
14260                    let trimmed = inner.trim();
14261                    let is_expr = trimmed.starts_with('$')
14262                        || trimmed.starts_with('\\')
14263                        || trimmed.starts_with('@')   // `${@arr}` rare but valid
14264                        || trimmed.starts_with('%')   // `${%h}`   rare but valid
14265                        || trimmed.contains(['(', '+', '-', '*', '/', '.', '?', '&', '|']);
14266                    let mut base: Expr = if is_expr {
14267                        // Re-parse the inner content as a Perl expression. Wrap in
14268                        // `Deref { kind: Sigil::Scalar }` to dereference the resulting
14269                        // scalar reference (Perl: `${$r}` ≡ `$$r`).
14270                        match parse_expression_from_str(trimmed, "<interp>") {
14271                            Ok(e) => Expr {
14272                                kind: ExprKind::Deref {
14273                                    expr: Box::new(e),
14274                                    kind: Sigil::Scalar,
14275                                },
14276                                line,
14277                            },
14278                            Err(_) => Expr {
14279                                kind: ExprKind::ScalarVar(inner.clone()),
14280                                line,
14281                            },
14282                        }
14283                    } else {
14284                        // Treat as a plain (possibly qualified) variable name.
14285                        self.no_interop_check_scalar_var_name(&inner, line)?;
14286                        Expr {
14287                            kind: ExprKind::ScalarVar(inner),
14288                            line,
14289                        }
14290                    };
14291
14292                    // After `${…}` we may see `[idx]` / `{key}` for indexing into the
14293                    // dereferenced array/hash (`${$ar}[1]`, `${$hr}{k}`), and arrow
14294                    // chains thereafter.
14295                    base = self.interp_chain_subscripts(&chars, &mut i, base, line);
14296                    parts.push(StringPart::Expr(base));
14297                } else if chars[i] == '^' {
14298                    // `$^V`, `$^O`, … — name stored as `^V`, `^O`, … (see [`Interpreter::get_special_var`]).
14299                    let mut name = String::from("^");
14300                    i += 1;
14301                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
14302                        name.push(chars[i]);
14303                        i += 1;
14304                    }
14305                    if i < chars.len() && chars[i] == '{' {
14306                        i += 1; // skip {
14307                        let mut key = String::new();
14308                        let mut depth = 1;
14309                        while i < chars.len() && depth > 0 {
14310                            if chars[i] == '{' {
14311                                depth += 1;
14312                            } else if chars[i] == '}' {
14313                                depth -= 1;
14314                                if depth == 0 {
14315                                    break;
14316                                }
14317                            }
14318                            key.push(chars[i]);
14319                            i += 1;
14320                        }
14321                        if i < chars.len() {
14322                            i += 1;
14323                        }
14324                        let key_expr = if let Some(rest) = key.strip_prefix('$') {
14325                            Expr {
14326                                kind: ExprKind::ScalarVar(rest.to_string()),
14327                                line,
14328                            }
14329                        } else {
14330                            Expr {
14331                                kind: ExprKind::String(key),
14332                                line,
14333                            }
14334                        };
14335                        parts.push(StringPart::Expr(Expr {
14336                            kind: ExprKind::HashElement {
14337                                hash: name,
14338                                key: Box::new(key_expr),
14339                            },
14340                            line,
14341                        }));
14342                    } else if i < chars.len() && chars[i] == '[' {
14343                        i += 1;
14344                        let mut idx_str = String::new();
14345                        while i < chars.len() && chars[i] != ']' {
14346                            idx_str.push(chars[i]);
14347                            i += 1;
14348                        }
14349                        if i < chars.len() {
14350                            i += 1;
14351                        }
14352                        let idx_expr = if let Some(rest) = idx_str.strip_prefix('$') {
14353                            Expr {
14354                                kind: ExprKind::ScalarVar(rest.to_string()),
14355                                line,
14356                            }
14357                        } else if let Ok(n) = idx_str.parse::<i64>() {
14358                            Expr {
14359                                kind: ExprKind::Integer(n),
14360                                line,
14361                            }
14362                        } else {
14363                            Expr {
14364                                kind: ExprKind::String(idx_str),
14365                                line,
14366                            }
14367                        };
14368                        parts.push(StringPart::Expr(Expr {
14369                            kind: ExprKind::ArrayElement {
14370                                array: name,
14371                                index: Box::new(idx_expr),
14372                            },
14373                            line,
14374                        }));
14375                    } else {
14376                        self.no_interop_check_scalar_var_name(&name, line)?;
14377                        parts.push(StringPart::ScalarVar(name));
14378                    }
14379                } else if chars[i].is_alphabetic() || chars[i] == '_' {
14380                    let mut name = String::new();
14381                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
14382                        name.push(chars[i]);
14383                        i += 1;
14384                    }
14385                    // `$_<`, `$_<<`, … — outer topic (stryke extension); only for bare `_`.
14386                    if name == "_" {
14387                        while i < chars.len() && chars[i] == '<' {
14388                            name.push('<');
14389                            i += 1;
14390                        }
14391                    }
14392                    // `--no-interop`: `$a` / `$b` are Perl-isms; reject inside
14393                    // string interpolation too. Catches both `"$a"` and `"$a[0]"`
14394                    // / `"$a{k}"` / `"$a->[0]"` because every branch below uses
14395                    // `name` to build the expression.
14396                    self.no_interop_check_scalar_var_name(&name, line)?;
14397                    // Build the base expression, then thread arrow-deref chains
14398                    // (`->[…]` / `->{…}`) onto it so things like `$ar->[2]`,
14399                    // `$href->{k}`, and chained `$x->{a}[1]->{b}` interpolate
14400                    // correctly inside double-quoted strings (Perl convention).
14401                    let mut base = if i < chars.len() && chars[i] == '{' {
14402                        // $hash{key}
14403                        i += 1; // skip {
14404                        let mut key = String::new();
14405                        let mut depth = 1;
14406                        while i < chars.len() && depth > 0 {
14407                            if chars[i] == '{' {
14408                                depth += 1;
14409                            } else if chars[i] == '}' {
14410                                depth -= 1;
14411                                if depth == 0 {
14412                                    break;
14413                                }
14414                            }
14415                            key.push(chars[i]);
14416                            i += 1;
14417                        }
14418                        if i < chars.len() {
14419                            i += 1;
14420                        } // skip }
14421                        let key_expr = if let Some(rest) = key.strip_prefix('$') {
14422                            Expr {
14423                                kind: ExprKind::ScalarVar(rest.to_string()),
14424                                line,
14425                            }
14426                        } else {
14427                            Expr {
14428                                kind: ExprKind::String(key),
14429                                line,
14430                            }
14431                        };
14432                        Expr {
14433                            kind: ExprKind::HashElement {
14434                                hash: name,
14435                                key: Box::new(key_expr),
14436                            },
14437                            line,
14438                        }
14439                    } else if i < chars.len() && chars[i] == '[' {
14440                        // $array[idx]
14441                        i += 1;
14442                        let mut idx_str = String::new();
14443                        while i < chars.len() && chars[i] != ']' {
14444                            idx_str.push(chars[i]);
14445                            i += 1;
14446                        }
14447                        if i < chars.len() {
14448                            i += 1;
14449                        }
14450                        let idx_expr = if let Some(rest) = idx_str.strip_prefix('$') {
14451                            Expr {
14452                                kind: ExprKind::ScalarVar(rest.to_string()),
14453                                line,
14454                            }
14455                        } else if let Ok(n) = idx_str.parse::<i64>() {
14456                            Expr {
14457                                kind: ExprKind::Integer(n),
14458                                line,
14459                            }
14460                        } else {
14461                            Expr {
14462                                kind: ExprKind::String(idx_str),
14463                                line,
14464                            }
14465                        };
14466                        Expr {
14467                            kind: ExprKind::ArrayElement {
14468                                array: name,
14469                                index: Box::new(idx_expr),
14470                            },
14471                            line,
14472                        }
14473                    } else {
14474                        // Bare $name — defer to the chain-extension loop below.
14475                        Expr {
14476                            kind: ExprKind::ScalarVar(name),
14477                            line,
14478                        }
14479                    };
14480
14481                    // Chain `->[…]` / `->{…}` AND adjacent `[…]` / `{…}` — Perl
14482                    // implies `->` between consecutive subscripts (`$m[1][2]`
14483                    // ≡ `$m[1]->[2]`).  See `interp_chain_subscripts`.
14484                    base = self.interp_chain_subscripts(&chars, &mut i, base, line);
14485                    parts.push(StringPart::Expr(base));
14486                } else if chars[i].is_ascii_digit() {
14487                    // $0 (program name), $1…$n (regexp captures). Perl disallows $01, $02, …
14488                    if chars[i] == '0' {
14489                        i += 1;
14490                        if i < chars.len() && chars[i].is_ascii_digit() {
14491                            return Err(self.syntax_err(
14492                                "Numeric variables with more than one digit may not start with '0'",
14493                                line,
14494                            ));
14495                        }
14496                        parts.push(StringPart::ScalarVar("0".into()));
14497                    } else {
14498                        let start = i;
14499                        while i < chars.len() && chars[i].is_ascii_digit() {
14500                            i += 1;
14501                        }
14502                        parts.push(StringPart::ScalarVar(chars[start..i].iter().collect()));
14503                    }
14504                } else {
14505                    let c = chars[i];
14506                    let probe = c.to_string();
14507                    if Interpreter::is_special_scalar_name_for_get(&probe)
14508                        || matches!(c, '\'' | '`')
14509                    {
14510                        i += 1;
14511                        // Check for hash element access: `$+{key}`, `$-{key}`, etc.
14512                        if i < chars.len() && chars[i] == '{' {
14513                            i += 1; // skip {
14514                            let mut key = String::new();
14515                            let mut depth = 1;
14516                            while i < chars.len() && depth > 0 {
14517                                if chars[i] == '{' {
14518                                    depth += 1;
14519                                } else if chars[i] == '}' {
14520                                    depth -= 1;
14521                                    if depth == 0 {
14522                                        break;
14523                                    }
14524                                }
14525                                key.push(chars[i]);
14526                                i += 1;
14527                            }
14528                            if i < chars.len() {
14529                                i += 1;
14530                            } // skip }
14531                            let key_expr = if let Some(rest) = key.strip_prefix('$') {
14532                                Expr {
14533                                    kind: ExprKind::ScalarVar(rest.to_string()),
14534                                    line,
14535                                }
14536                            } else {
14537                                Expr {
14538                                    kind: ExprKind::String(key),
14539                                    line,
14540                                }
14541                            };
14542                            let mut base = Expr {
14543                                kind: ExprKind::HashElement {
14544                                    hash: probe,
14545                                    key: Box::new(key_expr),
14546                                },
14547                                line,
14548                            };
14549                            base = self.interp_chain_subscripts(&chars, &mut i, base, line);
14550                            parts.push(StringPart::Expr(base));
14551                        } else {
14552                            // Check for arrow deref chain: `$@->{key}`, etc.
14553                            let mut base = Expr {
14554                                kind: ExprKind::ScalarVar(probe),
14555                                line,
14556                            };
14557                            base = self.interp_chain_subscripts(&chars, &mut i, base, line);
14558                            if matches!(base.kind, ExprKind::ScalarVar(_)) {
14559                                // No chain extension — use the simpler ScalarVar part
14560                                if let ExprKind::ScalarVar(name) = base.kind {
14561                                    self.no_interop_check_scalar_var_name(&name, line)?;
14562                                    parts.push(StringPart::ScalarVar(name));
14563                                }
14564                            } else {
14565                                parts.push(StringPart::Expr(base));
14566                            }
14567                        }
14568                    } else {
14569                        literal.push('$');
14570                        literal.push(c);
14571                        i += 1;
14572                    }
14573                }
14574            } else if chars[i] == '@' && i + 1 < chars.len() {
14575                let next = chars[i + 1];
14576                // `@$aref` / `@${expr}` — array dereference in interpolation (Perl `"@$r"` → elements of @$r).
14577                if next == '$' {
14578                    if !literal.is_empty() {
14579                        parts.push(StringPart::Literal(std::mem::take(&mut literal)));
14580                    }
14581                    i += 1; // past `@`
14582                    debug_assert_eq!(chars[i], '$');
14583                    i += 1; // past `$`
14584                    while i < chars.len() && chars[i].is_whitespace() {
14585                        i += 1;
14586                    }
14587                    if i >= chars.len() {
14588                        return Err(self.syntax_err(
14589                            "Expected variable or block after `@$` in double-quoted string",
14590                            line,
14591                        ));
14592                    }
14593                    let inner_expr = if chars[i] == '{' {
14594                        i += 1;
14595                        let start = i;
14596                        let mut depth = 1usize;
14597                        while i < chars.len() && depth > 0 {
14598                            match chars[i] {
14599                                '{' => depth += 1,
14600                                '}' => {
14601                                    depth -= 1;
14602                                    if depth == 0 {
14603                                        break;
14604                                    }
14605                                }
14606                                _ => {}
14607                            }
14608                            i += 1;
14609                        }
14610                        if depth != 0 {
14611                            return Err(self.syntax_err(
14612                                "Unterminated `${ ... }` after `@` in double-quoted string",
14613                                line,
14614                            ));
14615                        }
14616                        let inner: String = chars[start..i].iter().collect();
14617                        i += 1; // closing `}`
14618                        parse_expression_from_str(inner.trim(), "-e")?
14619                    } else {
14620                        let mut name = String::new();
14621                        if chars[i] == '^' {
14622                            name.push('^');
14623                            i += 1;
14624                            while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_')
14625                            {
14626                                name.push(chars[i]);
14627                                i += 1;
14628                            }
14629                        } else {
14630                            while i < chars.len()
14631                                && (chars[i].is_alphanumeric()
14632                                    || chars[i] == '_'
14633                                    || chars[i] == ':')
14634                            {
14635                                name.push(chars[i]);
14636                                i += 1;
14637                            }
14638                            while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
14639                                name.push_str("::");
14640                                i += 2;
14641                                while i < chars.len()
14642                                    && (chars[i].is_alphanumeric() || chars[i] == '_')
14643                                {
14644                                    name.push(chars[i]);
14645                                    i += 1;
14646                                }
14647                            }
14648                        }
14649                        if name.is_empty() {
14650                            return Err(self.syntax_err(
14651                                "Expected identifier after `@$` in double-quoted string",
14652                                line,
14653                            ));
14654                        }
14655                        Expr {
14656                            kind: ExprKind::ScalarVar(name),
14657                            line,
14658                        }
14659                    };
14660                    parts.push(StringPart::Expr(Expr {
14661                        kind: ExprKind::Deref {
14662                            expr: Box::new(inner_expr),
14663                            kind: Sigil::Array,
14664                        },
14665                        line,
14666                    }));
14667                    continue 'istr;
14668                }
14669                if next == '{' {
14670                    if !literal.is_empty() {
14671                        parts.push(StringPart::Literal(std::mem::take(&mut literal)));
14672                    }
14673                    i += 2; // `@{`
14674                    let start = i;
14675                    let mut depth = 1usize;
14676                    while i < chars.len() && depth > 0 {
14677                        match chars[i] {
14678                            '{' => depth += 1,
14679                            '}' => {
14680                                depth -= 1;
14681                                if depth == 0 {
14682                                    break;
14683                                }
14684                            }
14685                            _ => {}
14686                        }
14687                        i += 1;
14688                    }
14689                    if depth != 0 {
14690                        return Err(
14691                            self.syntax_err("Unterminated @{ ... } in double-quoted string", line)
14692                        );
14693                    }
14694                    let inner: String = chars[start..i].iter().collect();
14695                    i += 1; // closing `}`
14696                    let inner_expr = parse_expression_from_str(inner.trim(), "-e")?;
14697                    parts.push(StringPart::Expr(Expr {
14698                        kind: ExprKind::Deref {
14699                            expr: Box::new(inner_expr),
14700                            kind: Sigil::Array,
14701                        },
14702                        line,
14703                    }));
14704                    continue 'istr;
14705                }
14706                if !(next.is_alphabetic() || next == '_' || next == '+' || next == '-') {
14707                    literal.push(chars[i]);
14708                    i += 1;
14709                } else {
14710                    if !literal.is_empty() {
14711                        parts.push(StringPart::Literal(std::mem::take(&mut literal)));
14712                    }
14713                    i += 1;
14714                    let mut name = String::new();
14715                    if i < chars.len() && (chars[i] == '+' || chars[i] == '-') {
14716                        name.push(chars[i]);
14717                        i += 1;
14718                    } else {
14719                        while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
14720                            name.push(chars[i]);
14721                            i += 1;
14722                        }
14723                        while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
14724                            name.push_str("::");
14725                            i += 2;
14726                            while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_')
14727                            {
14728                                name.push(chars[i]);
14729                                i += 1;
14730                            }
14731                        }
14732                    }
14733                    if i < chars.len() && chars[i] == '[' {
14734                        i += 1;
14735                        let start_inner = i;
14736                        let mut depth = 1usize;
14737                        while i < chars.len() && depth > 0 {
14738                            match chars[i] {
14739                                '[' => depth += 1,
14740                                ']' => depth -= 1,
14741                                _ => {}
14742                            }
14743                            if depth == 0 {
14744                                let inner: String = chars[start_inner..i].iter().collect();
14745                                i += 1; // closing ]
14746                                let indices = parse_slice_indices_from_str(inner.trim(), "-e")?;
14747                                parts.push(StringPart::Expr(Expr {
14748                                    kind: ExprKind::ArraySlice {
14749                                        array: name.clone(),
14750                                        indices,
14751                                    },
14752                                    line,
14753                                }));
14754                                continue 'istr;
14755                            }
14756                            i += 1;
14757                        }
14758                        return Err(self.syntax_err(
14759                            "Unterminated [ in array slice inside quoted string",
14760                            line,
14761                        ));
14762                    }
14763                    parts.push(StringPart::ArrayVar(name));
14764                }
14765            } else if chars[i] == '#'
14766                && i + 1 < chars.len()
14767                && chars[i + 1] == '{'
14768                && !crate::compat_mode()
14769            {
14770                // #{expr} — Ruby-style expression interpolation (stryke extension).
14771                if !literal.is_empty() {
14772                    parts.push(StringPart::Literal(std::mem::take(&mut literal)));
14773                }
14774                i += 2; // skip `#{`
14775                let mut inner = String::new();
14776                let mut depth = 1usize;
14777                while i < chars.len() && depth > 0 {
14778                    match chars[i] {
14779                        '{' => depth += 1,
14780                        '}' => {
14781                            depth -= 1;
14782                            if depth == 0 {
14783                                break;
14784                            }
14785                        }
14786                        _ => {}
14787                    }
14788                    inner.push(chars[i]);
14789                    i += 1;
14790                }
14791                if i < chars.len() {
14792                    i += 1; // skip closing `}`
14793                }
14794                let expr = parse_block_from_str(inner.trim(), "-e", line)?;
14795                parts.push(StringPart::Expr(expr));
14796            } else {
14797                literal.push(chars[i]);
14798                i += 1;
14799            }
14800        }
14801        if !literal.is_empty() {
14802            parts.push(StringPart::Literal(literal));
14803        }
14804
14805        if parts.len() == 1 {
14806            if let StringPart::Literal(s) = &parts[0] {
14807                return Ok(Expr {
14808                    kind: ExprKind::String(s.clone()),
14809                    line,
14810                });
14811            }
14812        }
14813        if parts.is_empty() {
14814            return Ok(Expr {
14815                kind: ExprKind::String(String::new()),
14816                line,
14817            });
14818        }
14819
14820        Ok(Expr {
14821            kind: ExprKind::InterpolatedString(parts),
14822            line,
14823        })
14824    }
14825
14826    fn expr_to_overload_key(&self, e: &Expr) -> PerlResult<String> {
14827        match &e.kind {
14828            ExprKind::String(s) => Ok(s.clone()),
14829            _ => Err(self.syntax_err(
14830                "overload key must be a string literal (e.g. '\"\"' or '+')",
14831                e.line,
14832            )),
14833        }
14834    }
14835
14836    fn expr_to_overload_sub(&self, e: &Expr) -> PerlResult<String> {
14837        match &e.kind {
14838            ExprKind::String(s) => Ok(s.clone()),
14839            ExprKind::Integer(n) => Ok(n.to_string()),
14840            ExprKind::SubroutineRef(s) | ExprKind::SubroutineCodeRef(s) => Ok(s.clone()),
14841            _ => Err(self.syntax_err(
14842                "overload handler must be a string literal, number (e.g. fallback => 1), or \\&subname (method in current package)",
14843                e.line,
14844            )),
14845        }
14846    }
14847}
14848
14849fn merge_expr_list(parts: Vec<Expr>) -> Expr {
14850    if parts.len() == 1 {
14851        parts.into_iter().next().unwrap()
14852    } else {
14853        let line = parts.first().map(|e| e.line).unwrap_or(0);
14854        Expr {
14855            kind: ExprKind::List(parts),
14856            line,
14857        }
14858    }
14859}
14860
14861/// Parse a single expression from `s` (e.g. contents of `@{ ... }` inside a double-quoted string).
14862pub fn parse_expression_from_str(s: &str, file: &str) -> PerlResult<Expr> {
14863    let mut lexer = Lexer::new_with_file(s, file);
14864    let tokens = lexer.tokenize()?;
14865    let mut parser = Parser::new_with_file(tokens, file);
14866    let e = parser.parse_expression()?;
14867    if !parser.at_eof() {
14868        return Err(parser.syntax_err(
14869            "Extra tokens in embedded string expression",
14870            parser.peek_line(),
14871        ));
14872    }
14873    Ok(e)
14874}
14875
14876/// Parse a statement list from `s` and wrap as `do { ... }` (for `#{...}` interpolation).
14877pub fn parse_block_from_str(s: &str, file: &str, line: usize) -> PerlResult<Expr> {
14878    let mut lexer = Lexer::new_with_file(s, file);
14879    let tokens = lexer.tokenize()?;
14880    let mut parser = Parser::new_with_file(tokens, file);
14881    let stmts = parser.parse_statements()?;
14882    let inner_line = stmts.first().map(|st| st.line).unwrap_or(line);
14883    let inner = Expr {
14884        kind: ExprKind::CodeRef {
14885            params: vec![],
14886            body: stmts,
14887        },
14888        line: inner_line,
14889    };
14890    Ok(Expr {
14891        kind: ExprKind::Do(Box::new(inner)),
14892        line,
14893    })
14894}
14895
14896/// Comma-separated expressions on a `format` value line (below a picture line).
14897/// Parse `[ ... ]` contents for `@a[...]` (same rules as `parse_arg_list` / comma-separated indices).
14898pub fn parse_slice_indices_from_str(s: &str, file: &str) -> PerlResult<Vec<Expr>> {
14899    let mut lexer = Lexer::new_with_file(s, file);
14900    let tokens = lexer.tokenize()?;
14901    let mut parser = Parser::new_with_file(tokens, file);
14902    parser.parse_arg_list()
14903}
14904
14905pub fn parse_format_value_line(line: &str) -> PerlResult<Vec<Expr>> {
14906    let trimmed = line.trim();
14907    if trimmed.is_empty() {
14908        return Ok(vec![]);
14909    }
14910    let mut lexer = Lexer::new(trimmed);
14911    let tokens = lexer.tokenize()?;
14912    let mut parser = Parser::new(tokens);
14913    let mut exprs = Vec::new();
14914    loop {
14915        if parser.at_eof() {
14916            break;
14917        }
14918        // Assignment-level expressions so `a, b` yields two fields (not one comma list).
14919        exprs.push(parser.parse_assign_expr()?);
14920        if parser.eat(&Token::Comma) {
14921            continue;
14922        }
14923        if !parser.at_eof() {
14924            return Err(parser.syntax_err("Extra tokens in format value line", parser.peek_line()));
14925        }
14926        break;
14927    }
14928    Ok(exprs)
14929}
14930
14931#[cfg(test)]
14932mod tests {
14933    use super::*;
14934
14935    fn parse_ok(code: &str) -> Program {
14936        let mut lexer = Lexer::new(code);
14937        let tokens = lexer.tokenize().expect("tokenize");
14938        let mut parser = Parser::new(tokens);
14939        parser.parse_program().expect("parse")
14940    }
14941
14942    fn parse_err(code: &str) -> String {
14943        let mut lexer = Lexer::new(code);
14944        let tokens = match lexer.tokenize() {
14945            Ok(t) => t,
14946            Err(e) => return e.message,
14947        };
14948        let mut parser = Parser::new(tokens);
14949        parser.parse_program().unwrap_err().message
14950    }
14951
14952    #[test]
14953    fn parse_empty_program() {
14954        let p = parse_ok("");
14955        assert!(p.statements.is_empty());
14956    }
14957
14958    #[test]
14959    fn parse_semicolons_only() {
14960        let p = parse_ok(";;");
14961        assert!(p.statements.len() <= 3);
14962    }
14963
14964    #[test]
14965    fn parse_simple_scalar_assignment() {
14966        let p = parse_ok("$x = 1");
14967        assert_eq!(p.statements.len(), 1);
14968    }
14969
14970    #[test]
14971    fn parse_simple_array_assignment() {
14972        let p = parse_ok("@arr = (1, 2, 3)");
14973        assert_eq!(p.statements.len(), 1);
14974    }
14975
14976    #[test]
14977    fn parse_simple_hash_assignment() {
14978        let p = parse_ok("%h = (a => 1, b => 2)");
14979        assert_eq!(p.statements.len(), 1);
14980    }
14981
14982    #[test]
14983    fn parse_subroutine_decl() {
14984        let p = parse_ok("fn foo { 1 }");
14985        assert_eq!(p.statements.len(), 1);
14986        match &p.statements[0].kind {
14987            StmtKind::SubDecl { name, .. } => assert_eq!(name, "foo"),
14988            _ => panic!("expected SubDecl"),
14989        }
14990    }
14991
14992    #[test]
14993    fn parse_subroutine_with_prototype() {
14994        let p = parse_ok("fn foo ($$) { 1 }");
14995        assert_eq!(p.statements.len(), 1);
14996        match &p.statements[0].kind {
14997            StmtKind::SubDecl { prototype, .. } => {
14998                assert!(prototype.is_some());
14999            }
15000            _ => panic!("expected SubDecl"),
15001        }
15002    }
15003
15004    #[test]
15005    fn parse_anonymous_fn() {
15006        let p = parse_ok("my $f = fn { 1 }");
15007        assert_eq!(p.statements.len(), 1);
15008    }
15009
15010    #[test]
15011    fn parse_if_statement() {
15012        let p = parse_ok("if (1) { 2 }");
15013        assert_eq!(p.statements.len(), 1);
15014        matches!(&p.statements[0].kind, StmtKind::If { .. });
15015    }
15016
15017    #[test]
15018    fn parse_if_elsif_else() {
15019        let p = parse_ok("if (0) { 1 } elsif (1) { 2 } else { 3 }");
15020        assert_eq!(p.statements.len(), 1);
15021    }
15022
15023    #[test]
15024    fn parse_unless_statement() {
15025        let p = parse_ok("unless (0) { 1 }");
15026        assert_eq!(p.statements.len(), 1);
15027    }
15028
15029    #[test]
15030    fn parse_while_loop() {
15031        let p = parse_ok("while ($x) { $x-- }");
15032        assert_eq!(p.statements.len(), 1);
15033    }
15034
15035    #[test]
15036    fn parse_until_loop() {
15037        let p = parse_ok("until ($x) { $x++ }");
15038        assert_eq!(p.statements.len(), 1);
15039    }
15040
15041    #[test]
15042    fn parse_for_c_style() {
15043        let p = parse_ok("for (my $i=0; $i<10; $i++) { 1 }");
15044        assert_eq!(p.statements.len(), 1);
15045    }
15046
15047    #[test]
15048    fn parse_foreach_loop() {
15049        let p = parse_ok("foreach my $x (@arr) { 1 }");
15050        assert_eq!(p.statements.len(), 1);
15051    }
15052
15053    #[test]
15054    fn parse_loop_with_label() {
15055        let p = parse_ok("OUTER: for my $i (1..10) { last OUTER }");
15056        assert_eq!(p.statements.len(), 1);
15057        assert_eq!(p.statements[0].label.as_deref(), Some("OUTER"));
15058    }
15059
15060    #[test]
15061    fn parse_begin_block() {
15062        let p = parse_ok("BEGIN { 1 }");
15063        assert_eq!(p.statements.len(), 1);
15064        matches!(&p.statements[0].kind, StmtKind::Begin(_));
15065    }
15066
15067    #[test]
15068    fn parse_end_block() {
15069        let p = parse_ok("END { 1 }");
15070        assert_eq!(p.statements.len(), 1);
15071        matches!(&p.statements[0].kind, StmtKind::End(_));
15072    }
15073
15074    #[test]
15075    fn parse_package_statement() {
15076        let p = parse_ok("package Foo::Bar");
15077        assert_eq!(p.statements.len(), 1);
15078        match &p.statements[0].kind {
15079            StmtKind::Package { name } => assert_eq!(name, "Foo::Bar"),
15080            _ => panic!("expected Package"),
15081        }
15082    }
15083
15084    #[test]
15085    fn parse_use_statement() {
15086        let p = parse_ok("use strict");
15087        assert_eq!(p.statements.len(), 1);
15088    }
15089
15090    #[test]
15091    fn parse_no_statement() {
15092        let p = parse_ok("no warnings");
15093        assert_eq!(p.statements.len(), 1);
15094    }
15095
15096    #[test]
15097    fn parse_require_bareword() {
15098        let p = parse_ok("require Foo::Bar");
15099        assert_eq!(p.statements.len(), 1);
15100    }
15101
15102    #[test]
15103    fn parse_require_string() {
15104        let p = parse_ok(r#"require "foo.pl""#);
15105        assert_eq!(p.statements.len(), 1);
15106    }
15107
15108    #[test]
15109    fn parse_eval_block() {
15110        let p = parse_ok("eval { 1 }");
15111        assert_eq!(p.statements.len(), 1);
15112    }
15113
15114    #[test]
15115    fn parse_eval_string() {
15116        let p = parse_ok(r#"eval "1 + 2""#);
15117        assert_eq!(p.statements.len(), 1);
15118    }
15119
15120    #[test]
15121    fn parse_qw_word_list() {
15122        let p = parse_ok("my @a = qw(foo bar baz)");
15123        assert_eq!(p.statements.len(), 1);
15124    }
15125
15126    #[test]
15127    fn parse_q_string() {
15128        let p = parse_ok("my $s = q{hello}");
15129        assert_eq!(p.statements.len(), 1);
15130    }
15131
15132    #[test]
15133    fn parse_qq_string() {
15134        let p = parse_ok(r#"my $s = qq(hello $x)"#);
15135        assert_eq!(p.statements.len(), 1);
15136    }
15137
15138    #[test]
15139    fn parse_regex_match() {
15140        let p = parse_ok(r#"$x =~ /foo/"#);
15141        assert_eq!(p.statements.len(), 1);
15142    }
15143
15144    #[test]
15145    fn parse_regex_substitution() {
15146        let p = parse_ok(r#"$x =~ s/foo/bar/g"#);
15147        assert_eq!(p.statements.len(), 1);
15148    }
15149
15150    #[test]
15151    fn parse_transliterate() {
15152        let p = parse_ok(r#"$x =~ tr/a-z/A-Z/"#);
15153        assert_eq!(p.statements.len(), 1);
15154    }
15155
15156    #[test]
15157    fn parse_ternary_operator() {
15158        let p = parse_ok("my $x = $a ? 1 : 2");
15159        assert_eq!(p.statements.len(), 1);
15160    }
15161
15162    #[test]
15163    fn parse_arrow_method_call() {
15164        let p = parse_ok("$obj->method()");
15165        assert_eq!(p.statements.len(), 1);
15166    }
15167
15168    #[test]
15169    fn parse_arrow_deref_hash() {
15170        let p = parse_ok("$r->{key}");
15171        assert_eq!(p.statements.len(), 1);
15172    }
15173
15174    #[test]
15175    fn parse_arrow_deref_array() {
15176        let p = parse_ok("$r->[0]");
15177        assert_eq!(p.statements.len(), 1);
15178    }
15179
15180    #[test]
15181    fn parse_chained_arrow_deref() {
15182        let p = parse_ok("$r->{a}[0]{b}");
15183        assert_eq!(p.statements.len(), 1);
15184    }
15185
15186    #[test]
15187    fn parse_my_multiple_vars() {
15188        let p = parse_ok("my ($a, $b, $c) = (1, 2, 3)");
15189        assert_eq!(p.statements.len(), 1);
15190    }
15191
15192    #[test]
15193    fn parse_our_scalar() {
15194        let p = parse_ok("our $VERSION = '1.0'");
15195        assert_eq!(p.statements.len(), 1);
15196    }
15197
15198    #[test]
15199    fn parse_local_scalar() {
15200        let p = parse_ok("local $/ = undef");
15201        assert_eq!(p.statements.len(), 1);
15202    }
15203
15204    #[test]
15205    fn parse_state_variable() {
15206        let p = parse_ok("fn Test::counter { state $n = 0; $n++ }");
15207        assert_eq!(p.statements.len(), 1);
15208    }
15209
15210    #[test]
15211    fn parse_postfix_if() {
15212        let p = parse_ok("print 1 if $x");
15213        assert_eq!(p.statements.len(), 1);
15214    }
15215
15216    #[test]
15217    fn parse_postfix_unless() {
15218        let p = parse_ok("die 'error' unless $ok");
15219        assert_eq!(p.statements.len(), 1);
15220    }
15221
15222    #[test]
15223    fn parse_postfix_while() {
15224        let p = parse_ok("$x++ while $x < 10");
15225        assert_eq!(p.statements.len(), 1);
15226    }
15227
15228    #[test]
15229    fn parse_postfix_for() {
15230        let p = parse_ok("print for @arr");
15231        assert_eq!(p.statements.len(), 1);
15232    }
15233
15234    #[test]
15235    fn parse_last_next_redo() {
15236        let p = parse_ok("for (@a) { next if $_ < 0; last if $_ > 10 }");
15237        assert_eq!(p.statements.len(), 1);
15238    }
15239
15240    #[test]
15241    fn parse_return_statement() {
15242        let p = parse_ok("fn foo { return 42 }");
15243        assert_eq!(p.statements.len(), 1);
15244    }
15245
15246    #[test]
15247    fn parse_wantarray() {
15248        let p = parse_ok("fn foo { wantarray ? @a : $a }");
15249        assert_eq!(p.statements.len(), 1);
15250    }
15251
15252    #[test]
15253    fn parse_caller_builtin() {
15254        let p = parse_ok("my @c = caller");
15255        assert_eq!(p.statements.len(), 1);
15256    }
15257
15258    #[test]
15259    fn parse_ref_to_array() {
15260        let p = parse_ok("my $r = \\@arr");
15261        assert_eq!(p.statements.len(), 1);
15262    }
15263
15264    #[test]
15265    fn parse_ref_to_hash() {
15266        let p = parse_ok("my $r = \\%hash");
15267        assert_eq!(p.statements.len(), 1);
15268    }
15269
15270    #[test]
15271    fn parse_ref_to_scalar() {
15272        let p = parse_ok("my $r = \\$x");
15273        assert_eq!(p.statements.len(), 1);
15274    }
15275
15276    #[test]
15277    fn parse_deref_scalar() {
15278        let p = parse_ok("my $v = $$r");
15279        assert_eq!(p.statements.len(), 1);
15280    }
15281
15282    #[test]
15283    fn parse_deref_array() {
15284        let p = parse_ok("my @a = @$r");
15285        assert_eq!(p.statements.len(), 1);
15286    }
15287
15288    #[test]
15289    fn parse_deref_hash() {
15290        let p = parse_ok("my %h = %$r");
15291        assert_eq!(p.statements.len(), 1);
15292    }
15293
15294    #[test]
15295    fn parse_blessed_ref() {
15296        let p = parse_ok("bless $r, 'Foo'");
15297        assert_eq!(p.statements.len(), 1);
15298    }
15299
15300    #[test]
15301    fn parse_heredoc_basic() {
15302        let p = parse_ok("my $s = <<END;\nfoo\nEND");
15303        assert_eq!(p.statements.len(), 1);
15304    }
15305
15306    #[test]
15307    fn parse_heredoc_quoted() {
15308        let p = parse_ok("my $s = <<'END';\nfoo\nEND");
15309        assert_eq!(p.statements.len(), 1);
15310    }
15311
15312    #[test]
15313    fn parse_do_block() {
15314        let p = parse_ok("my $x = do { 1 + 2 }");
15315        assert_eq!(p.statements.len(), 1);
15316    }
15317
15318    #[test]
15319    fn parse_do_file() {
15320        let p = parse_ok(r#"do "foo.pl""#);
15321        assert_eq!(p.statements.len(), 1);
15322    }
15323
15324    #[test]
15325    fn parse_map_expression() {
15326        let p = parse_ok("my @b = map { $_ * 2 } @a");
15327        assert_eq!(p.statements.len(), 1);
15328    }
15329
15330    #[test]
15331    fn parse_grep_expression() {
15332        let p = parse_ok("my @b = grep { $_ > 0 } @a");
15333        assert_eq!(p.statements.len(), 1);
15334    }
15335
15336    #[test]
15337    fn parse_sort_expression() {
15338        let p = parse_ok("my @b = sort { $a <=> $b } @a");
15339        assert_eq!(p.statements.len(), 1);
15340    }
15341
15342    #[test]
15343    fn parse_pipe_forward() {
15344        let p = parse_ok("@a |> map { $_ * 2 }");
15345        assert_eq!(p.statements.len(), 1);
15346    }
15347
15348    #[test]
15349    fn parse_expression_from_str_simple() {
15350        let e = parse_expression_from_str("$x + 1", "-e").unwrap();
15351        assert!(matches!(e.kind, ExprKind::BinOp { .. }));
15352    }
15353
15354    #[test]
15355    fn parse_expression_from_str_extra_tokens_error() {
15356        let err = parse_expression_from_str("$x; $y", "-e").unwrap_err();
15357        assert!(err.message.contains("Extra tokens"));
15358    }
15359
15360    #[test]
15361    fn parse_slice_indices_from_str_basic() {
15362        let indices = parse_slice_indices_from_str("0, 1, 2", "-e").unwrap();
15363        assert_eq!(indices.len(), 3);
15364    }
15365
15366    #[test]
15367    fn parse_format_value_line_empty() {
15368        let exprs = parse_format_value_line("").unwrap();
15369        assert!(exprs.is_empty());
15370    }
15371
15372    #[test]
15373    fn parse_format_value_line_single() {
15374        let exprs = parse_format_value_line("$x").unwrap();
15375        assert_eq!(exprs.len(), 1);
15376    }
15377
15378    #[test]
15379    fn parse_format_value_line_multiple() {
15380        let exprs = parse_format_value_line("$a, $b, $c").unwrap();
15381        assert_eq!(exprs.len(), 3);
15382    }
15383
15384    #[test]
15385    fn parse_unclosed_brace_error() {
15386        let err = parse_err("fn foo {");
15387        assert!(!err.is_empty());
15388    }
15389
15390    #[test]
15391    fn parse_unclosed_paren_error() {
15392        let err = parse_err("print (1, 2");
15393        assert!(!err.is_empty());
15394    }
15395
15396    #[test]
15397    fn parse_invalid_statement_error() {
15398        let err = parse_err("???");
15399        assert!(!err.is_empty());
15400    }
15401
15402    #[test]
15403    fn merge_expr_list_single() {
15404        let e = Expr {
15405            kind: ExprKind::Integer(1),
15406            line: 1,
15407        };
15408        let merged = merge_expr_list(vec![e.clone()]);
15409        matches!(merged.kind, ExprKind::Integer(1));
15410    }
15411
15412    #[test]
15413    fn merge_expr_list_multiple() {
15414        let e1 = Expr {
15415            kind: ExprKind::Integer(1),
15416            line: 1,
15417        };
15418        let e2 = Expr {
15419            kind: ExprKind::Integer(2),
15420            line: 1,
15421        };
15422        let merged = merge_expr_list(vec![e1, e2]);
15423        matches!(merged.kind, ExprKind::List(_));
15424    }
15425}