Skip to main content

stryke/
parser.rs

1use crate::ast::*;
2use crate::error::{ErrorKind, PerlError, PerlResult};
3use crate::lexer::{Lexer, LITERAL_AT_IN_DQUOTE, LITERAL_DOLLAR_IN_DQUOTE};
4use crate::token::Token;
5use crate::vm_helper::VMHelper;
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        "oursync" => StmtKind::OurSync(decls),
28        "local" => StmtKind::Local(decls),
29        "state" => StmtKind::State(decls),
30        _ => unreachable!("parse_my_our_local keyword"),
31    };
32    Statement {
33        label: None,
34        kind,
35        line,
36    }
37}
38
39fn destructure_stmt_die_string(line: usize, msg: &str) -> Statement {
40    Statement {
41        label: None,
42        kind: StmtKind::Expression(Expr {
43            kind: ExprKind::Die(vec![Expr {
44                kind: ExprKind::String(msg.to_string()),
45                line,
46            }]),
47            line,
48        }),
49        line,
50    }
51}
52
53fn destructure_stmt_unless_die(line: usize, cond: Expr, msg: &str) -> Statement {
54    Statement {
55        label: None,
56        kind: StmtKind::Unless {
57            condition: cond,
58            body: vec![destructure_stmt_die_string(line, msg)],
59            else_block: None,
60        },
61        line,
62    }
63}
64
65fn destructure_expr_scalar_tmp(name: &str, line: usize) -> Expr {
66    Expr {
67        kind: ExprKind::ScalarVar(name.to_string()),
68        line,
69    }
70}
71
72fn destructure_expr_array_len(tmp: &str, line: usize) -> Expr {
73    Expr {
74        kind: ExprKind::Deref {
75            expr: Box::new(destructure_expr_scalar_tmp(tmp, line)),
76            kind: Sigil::Array,
77        },
78        line,
79    }
80}
81
82pub struct Parser {
83    tokens: Vec<(Token, usize)>,
84    pos: usize,
85    /// Monotonic slot id for `rate_limit(...)` sliding-window state in the interpreter.
86    next_rate_limit_slot: u32,
87    /// When > 0, `expr` `(` is not parsed as [`ExprKind::IndirectCall`] — e.g. `sort $k (1)` must
88    /// treat `(1)` as the sort list, not `$k(1)`.
89    suppress_indirect_paren_call: u32,
90    /// When > 0, the current expression is being parsed as the RHS of `|>`
91    /// (pipe-forward). Builtins that normally require a list/string/second arg
92    /// (`map`, `grep`, `sort`, `join`, `reverse` / `reversed`, `split`, …) may accept a
93    /// placeholder when this flag is set, because [`Self::pipe_forward_apply`]
94    /// will substitute the piped value in afterwards.
95    pipe_rhs_depth: u32,
96    /// When > 0 we are parsing inside a `{ … }` block (function body, `map`/`grep`,
97    /// `for`, `if`, anonymous coderef, etc.). Inside any block, bare `_` is a topic
98    /// reference (`$_[0]`/`$_`), so `my $i = _` means "capture the topic" and must
99    /// NOT be auto-wrapped as an implicit zero-arg coderef. Only at the true top
100    /// level (depth 0 — module scope) is `_` unbound, allowing `my $f = _ * 2` to
101    /// parse as `my $f = fn { _ * 2 }`. Bumped in [`Self::parse_block`].
102    block_depth: u32,
103    /// When > 0, [`Self::parse_pipe_forward`] will **not** consume a trailing `|>`
104    /// and leaves it for an outer parser instead. Bumped while parsing paren-less
105    /// arg lists (`parse_list_until_terminator`, paren-less method args, `map`/`grep`
106    /// LIST, …) so `@a |> head 2 |> join "-"` chains left-associatively as
107    /// `(@a |> head 2) |> join "-"` instead of `head` swallowing the outer `|>`
108    /// as part of its first arg. Reset to 0 on entry to any parenthesized
109    /// arg list (`parse_arg_list`) so `head(2 |> foo, 3)` still works.
110    no_pipe_forward_depth: u32,
111    /// When > 0, `{` after a scalar / scalar deref is not `%hash{key}` / `->{}`, so
112    /// `if let` / `while let` scrutinees can be followed by `{ ... }`.
113    suppress_scalar_hash_brace: u32,
114    /// Counter for `while let` / similar desugar temps (`$__while_let_0`, …).
115    next_desugar_tmp: u32,
116    /// Source path for [`PerlError`] (matches lexer / `parse_with_file`).
117    error_file: String,
118    /// User-declared sub names (for allowing UDF to shadow stryke extensions in compat mode).
119    declared_subs: std::collections::HashSet<String>,
120    /// When > 0, `parse_named_expr` will not consume following barewords as paren-less
121    /// function arguments. Used by thread macro to prevent `t Color::Red p` from
122    /// interpreting `p` as an argument to the enum constructor instead of a stage.
123    suppress_parenless_call: u32,
124    /// Pre-built input expression for the next `parse_thread_macro_inner`
125    /// call. Used by `~p>` continuation parsing (`||>` / `|then|`) to
126    /// thread the par_reduce result into a normal `~>` continuation
127    /// without re-parsing a source expression.
128    pending_thread_input: Option<Expr>,
129    /// When > 0, `parse_multiplication` will not consume `Token::Slash` as division.
130    /// Used by thread macro so `/pattern/` is left for the stage parser to handle.
131    suppress_slash_as_div: u32,
132    /// When > 0, the lexer should not interpret `m/`, `s/`, etc. as regex-starters.
133    /// Used by thread macro to prevent `/m/` from being misparsed.
134    pub suppress_m_regex: u32,
135    /// When > 0, `parse_range` will not consume `:` as the short-form range operator.
136    /// Bumped while parsing the then-branch of a ternary `? :` so `a ? b : c` doesn't
137    /// misparse `b : c` as a range.
138    suppress_colon_range: u32,
139    /// Counter (depth-tracked like [`Self::suppress_colon_range`]) that
140    /// disables `~` as a range separator. Used inside paired `~...~` char-
141    /// index/slice subscripts so the closing `~` doesn't get eaten as a
142    /// range op. `:` range is still allowed inside (e.g. `$_~1:3~` is a
143    /// slice with a `:` range as the index).
144    suppress_tilde_range: u32,
145    /// When true, `pipe_forward_apply` uses thread-last semantics (append to args)
146    /// instead of thread-first (prepend). Set by `->>` thread macro.
147    thread_last_mode: bool,
148    /// When true, we're parsing a module (via `use`/`require`), not user code.
149    /// Modules are allowed to shadow builtins; user code is not (unless `--compat`).
150    pub parsing_module: bool,
151    /// `self.pos` immediately after consuming a paren-list close (`(EXPR)`,
152    /// `(EXPR, …)`, `()`) or `qw(…)` in `parse_primary`. The `x` operator
153    /// reads this at parse time to distinguish `(LIST) x N` (list repetition)
154    /// from `EXPR x N` (scalar string repetition). The compare is exact: any
155    /// postfix consumption (`->method()`, `[idx]`, …) advances `self.pos`
156    /// past this checkpoint, so list-repeat fires only when `x` is the very
157    /// next token after the closing paren.
158    list_construct_close_pos: Option<usize>,
159    /// Synthetic SubDecl statements queued by anonymous-sub overload handlers
160    /// (`use overload "+" => sub { ... }`) — drained at the end of
161    /// [`Self::parse_program`] and prepended to the top-level statements so
162    /// the package-qualified synthetic name resolves at runtime. (PARITY-012)
163    pending_synthetic_subs: Vec<Statement>,
164    /// Counter for unique anonymous-overload-handler names.
165    next_overload_anon_id: u32,
166    /// Token-vector indices where the lexer emitted a *bare* positional alias
167    /// (`_`, `_0`, `_1`, …) — i.e. without a leading `$` sigil. Populated by
168    /// [`crate::lexer::Lexer::tokenize`]. Consulted by [`Self::parse_my_our_local`]
169    /// to auto-wrap an RHS expression that contains free positional aliases
170    /// into an implicit zero-arg coderef, so `my $f = _ * 2` ≡
171    /// `my $f = fn { _ * 2 }`.
172    pub bare_positional_indices: std::collections::HashSet<usize>,
173}
174
175impl Parser {
176    pub fn new(tokens: Vec<(Token, usize)>) -> Self {
177        Self::new_with_file(tokens, "-e")
178    }
179
180    pub fn new_with_file(tokens: Vec<(Token, usize)>, file: impl Into<String>) -> Self {
181        Self {
182            tokens,
183            pos: 0,
184            next_rate_limit_slot: 0,
185            suppress_indirect_paren_call: 0,
186            pipe_rhs_depth: 0,
187            no_pipe_forward_depth: 0,
188            suppress_scalar_hash_brace: 0,
189            next_desugar_tmp: 0,
190            error_file: file.into(),
191            declared_subs: std::collections::HashSet::new(),
192            suppress_parenless_call: 0,
193            pending_thread_input: None,
194            suppress_slash_as_div: 0,
195            suppress_m_regex: 0,
196            suppress_colon_range: 0,
197            suppress_tilde_range: 0,
198            thread_last_mode: false,
199            pending_synthetic_subs: Vec::new(),
200            next_overload_anon_id: 0,
201            parsing_module: false,
202            list_construct_close_pos: None,
203            bare_positional_indices: std::collections::HashSet::new(),
204            block_depth: 0,
205        }
206    }
207
208    fn alloc_desugar_tmp(&mut self) -> u32 {
209        let n = self.next_desugar_tmp;
210        self.next_desugar_tmp = self.next_desugar_tmp.saturating_add(1);
211        n
212    }
213
214    /// True when we are currently parsing the RHS of a `|>` pipe-forward.
215    /// Used by builtins (`map`, `grep`, `sort`, `join`, …) to supply a
216    /// placeholder list instead of erroring on a missing operand.
217    #[inline]
218    fn in_pipe_rhs(&self) -> bool {
219        self.pipe_rhs_depth > 0
220    }
221
222    /// List-slurping builtin: the operand is entirely the LHS of `|>` (no following list tokens).
223    /// A newline after the builtin name also terminates the pipe stage (implicit semicolon).
224    fn pipe_supplies_slurped_list_operand(&self) -> bool {
225        self.in_pipe_rhs()
226            && (matches!(
227                self.peek(),
228                Token::Semicolon
229                    | Token::RBrace
230                    | Token::RParen
231                    | Token::Eof
232                    | Token::Comma
233                    | Token::PipeForward
234            ) || self.peek_line() > self.prev_line())
235    }
236
237    /// Empty placeholder list used as a stand-in for the list operand of
238    /// list-taking builtins when they appear on the RHS of `|>`.
239    /// [`Self::pipe_forward_apply`] rewrites this slot with the actual piped
240    /// value at desugar time, so the placeholder is never evaluated.
241    #[inline]
242    fn pipe_placeholder_list(&self, line: usize) -> Expr {
243        Expr {
244            kind: ExprKind::List(vec![]),
245            line,
246        }
247    }
248
249    /// List builtins that take `{ BLOCK }, LIST` and accept the threaded list at
250    /// `args[1]` via [`Self::pipe_forward_apply`]. Used by both the pipe-forward
251    /// dispatcher and `parse_thread_stage_with_block` so `~> @a NAME { ... }` and
252    /// `@a |> NAME { ... }` route through the same substitution.
253    fn is_block_then_list_pipe_builtin(name: &str) -> bool {
254        matches!(
255            name,
256            "pfirst"
257                | "pany"
258                | "any"
259                | "all"
260                | "none"
261                | "first"
262                | "take_while"
263                | "drop_while"
264                | "skip_while"
265                | "reject"
266                | "grepv"
267                | "tap"
268                | "peek"
269                | "group_by"
270                | "chunk_by"
271                | "partition"
272                | "min_by"
273                | "max_by"
274                | "zip_with"
275                | "count_by"
276        )
277    }
278
279    /// Lift a `Bareword("f")` to `FuncCall { f, [$_] }`.
280    ///
281    /// stryke extension contexts (map/grep/fore expression forms, pipe-forward)
282    /// call this so that `map sha512, @list` invokes `sha512($_)` for each
283    /// element instead of stringifying the bareword.  Non-bareword expressions
284    /// pass through unchanged.
285    ///
286    /// Also injects `$_` into known builtins that were parsed with zero
287    /// arguments (e.g. `fore unlink`, `map stat`) so they operate on the
288    /// topic variable instead of being no-ops.
289    fn lift_bareword_to_topic_call(expr: Expr) -> Expr {
290        let line = expr.line;
291        let topic = || Expr {
292            kind: ExprKind::ScalarVar("_".into()),
293            line,
294        };
295        match expr.kind {
296            ExprKind::Bareword(ref name) => Expr {
297                kind: ExprKind::FuncCall {
298                    name: name.clone(),
299                    args: vec![topic()],
300                },
301                line,
302            },
303            // Builtins that take Vec<Expr> args — inject $_ when empty.
304            ExprKind::Unlink(ref args) if args.is_empty() => Expr {
305                kind: ExprKind::Unlink(vec![topic()]),
306                line,
307            },
308            ExprKind::Chmod(ref args) if args.is_empty() => Expr {
309                kind: ExprKind::Chmod(vec![topic()]),
310                line,
311            },
312            // Builtins that take Box<Expr> — inject $_ when arg is implicit.
313            ExprKind::Stat(_) => expr,
314            ExprKind::Lstat(_) => expr,
315            ExprKind::Readlink(_) => expr,
316            // rev with empty list should use $_
317            ExprKind::Rev(ref inner) => {
318                if matches!(inner.kind, ExprKind::List(ref v) if v.is_empty()) {
319                    Expr {
320                        kind: ExprKind::Rev(Box::new(topic())),
321                        line,
322                    }
323                } else {
324                    expr
325                }
326            }
327            _ => expr,
328        }
329    }
330
331    /// `parse_assign_expr` with `no_pipe_forward_depth` bumped for the
332    /// duration, so any trailing `|>` is left to the enclosing parser instead
333    /// of being absorbed into this sub-expression. Used by paren-less arg
334    /// parsers (`parse_list_until_terminator`, `chunked`/`windowed` paren-less,
335    /// paren-less method args, …) so `@a |> head 2 |> join "-"` chains
336    /// left-associatively instead of letting `head`'s first arg swallow the
337    /// outer `|>`. The counter is restored on both success and error paths.
338    fn parse_assign_expr_stop_at_pipe(&mut self) -> PerlResult<Expr> {
339        self.no_pipe_forward_depth = self.no_pipe_forward_depth.saturating_add(1);
340        let r = self.parse_assign_expr();
341        self.no_pipe_forward_depth = self.no_pipe_forward_depth.saturating_sub(1);
342        r
343    }
344
345    fn syntax_err(&self, message: impl Into<String>, line: usize) -> PerlError {
346        PerlError::new(ErrorKind::Syntax, message, line, self.error_file.clone())
347    }
348
349    /// Coderef-in-block-position helper for tier-2 list builtins (`any`,
350    /// `all`, `none`, `first`, `take_while`, …). Returns `Some([f, list])`
351    /// when the next tokens look like `$f [,] LIST` (or `$f` alone in
352    /// pipe-RHS); `None` when the caller should fall through to the block
353    /// form. The first arg is any coderef-shaped expression — runtime
354    /// checks `as_code_ref()` and dispatches.
355    fn try_parse_coderef_listop_args(&mut self, line: usize) -> PerlResult<Option<Vec<Expr>>> {
356        if !matches!(self.peek(), Token::ScalarVar(_) | Token::Backslash) {
357            return Ok(None);
358        }
359        let f = self.parse_assign_expr_stop_at_pipe()?;
360        let _ = self.eat(&Token::Comma);
361        let list = if self.in_pipe_rhs()
362            && matches!(
363                self.peek(),
364                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
365            ) {
366            self.pipe_placeholder_list(line)
367        } else {
368            self.parse_expression()?
369        };
370        Ok(Some(vec![f, list]))
371    }
372
373    fn alloc_rate_limit_slot(&mut self) -> u32 {
374        let s = self.next_rate_limit_slot;
375        self.next_rate_limit_slot = self.next_rate_limit_slot.saturating_add(1);
376        s
377    }
378
379    fn peek(&self) -> &Token {
380        self.tokens
381            .get(self.pos)
382            .map(|(t, _)| t)
383            .unwrap_or(&Token::Eof)
384    }
385
386    fn peek_line(&self) -> usize {
387        self.tokens.get(self.pos).map(|(_, l)| *l).unwrap_or(0)
388    }
389
390    fn peek_at(&self, offset: usize) -> &Token {
391        self.tokens
392            .get(self.pos + offset)
393            .map(|(t, _)| t)
394            .unwrap_or(&Token::Eof)
395    }
396
397    fn advance(&mut self) -> (Token, usize) {
398        let tok = self
399            .tokens
400            .get(self.pos)
401            .cloned()
402            .unwrap_or((Token::Eof, 0));
403        self.pos += 1;
404        tok
405    }
406
407    /// Line number of the most recently consumed token (the token at `pos - 1`).
408    fn prev_line(&self) -> usize {
409        if self.pos > 0 {
410            self.tokens.get(self.pos - 1).map(|(_, l)| *l).unwrap_or(0)
411        } else {
412            0
413        }
414    }
415
416    /// Check if `{ ... }` starting at current position looks like a hashref rather than a block.
417    /// Heuristics (assuming current token is `{`):
418    /// - `{ bareword =>` → hashref
419    /// - `{ "string" =>` → hashref
420    /// - `{ $var =>` → hashref
421    /// - `{ 0 =>` → hashref (numeric key)
422    /// - `{ %hash }` or `{ %hash, ...}` → hashref (spread)
423    /// - `{ }` (empty) → hashref
424    fn looks_like_hashref(&self) -> bool {
425        debug_assert!(matches!(self.peek(), Token::LBrace));
426        let tok1 = self.peek_at(1);
427        let tok2 = self.peek_at(2);
428        match tok1 {
429            Token::RBrace => true,
430            Token::Ident(_)
431            | Token::SingleString(_)
432            | Token::DoubleString(_)
433            | Token::ScalarVar(_)
434            | Token::Integer(_) => matches!(tok2, Token::FatArrow),
435            Token::HashVar(_) => matches!(tok2, Token::RBrace | Token::Comma),
436            _ => false,
437        }
438    }
439
440    fn expect(&mut self, expected: &Token) -> PerlResult<usize> {
441        let (tok, line) = self.advance();
442        if std::mem::discriminant(&tok) == std::mem::discriminant(expected) {
443            Ok(line)
444        } else {
445            Err(self.syntax_err(format!("Expected {:?}, got {:?}", expected, tok), line))
446        }
447    }
448
449    fn eat(&mut self, expected: &Token) -> bool {
450        if std::mem::discriminant(self.peek()) == std::mem::discriminant(expected) {
451            self.advance();
452            true
453        } else {
454            false
455        }
456    }
457
458    fn at_eof(&self) -> bool {
459        matches!(self.peek(), Token::Eof)
460    }
461
462    /// True when a file test (`-d`, `-f`, …) may omit its operand and use `$_` (Perl filetest default).
463    fn filetest_allows_implicit_topic(tok: &Token) -> bool {
464        matches!(
465            tok,
466            Token::RParen
467                | Token::Semicolon
468                | Token::Comma
469                | Token::RBrace
470                | Token::Eof
471                | Token::LogAnd
472                | Token::LogOr
473                | Token::LogAndWord
474                | Token::LogOrWord
475                | Token::PipeForward
476        )
477    }
478
479    /// True when the next token is a statement-starting keyword on a *different*
480    /// line from `stmt_line`.  Used by `parse_use` / `parse_no` to stop parsing
481    /// import lists when semicolons are omitted (stryke extension).
482    fn next_is_new_stmt_keyword(&self, stmt_line: usize) -> bool {
483        // Semicolons-optional is a stryke extension; in compat mode, require them.
484        if crate::compat_mode() {
485            return false;
486        }
487        if self.peek_line() == stmt_line {
488            return false;
489        }
490        matches!(
491            self.peek(),
492            Token::Ident(ref kw) if matches!(kw.as_str(),
493                "use" | "no" | "my" | "our" | "local" | "sub" | "struct" | "enum"
494                | "if" | "unless" | "while" | "until" | "for" | "foreach"
495                | "return" | "last" | "next" | "redo" | "package" | "require"
496                | "BEGIN" | "END" | "UNITCHECK" | "frozen" | "const" | "typed"
497                // stryke-specific declaration keywords that start a new
498                // statement on a fresh line. Without these, a bare `use
499                // strict` / `use warnings` followed by `fn foo { ... }`
500                // on the next line swallows `foo` as an import argument.
501                | "fn" | "class" | "abstract" | "final" | "trait"
502                | "state" | "mysync" | "oursync"
503            )
504        )
505    }
506
507    /// True when the next token is on a different line from `stmt_line` and could
508    /// start a new statement. More permissive than `next_is_new_stmt_keyword` —
509    /// includes sigil-prefixed variables like `$var`, `@arr`, `%hash`.
510    fn next_is_new_statement_start(&self, stmt_line: usize) -> bool {
511        if crate::compat_mode() {
512            return false;
513        }
514        if self.peek_line() == stmt_line {
515            return false;
516        }
517        matches!(
518            self.peek(),
519            Token::ScalarVar(_)
520                | Token::DerefScalarVar(_)
521                | Token::ArrayVar(_)
522                | Token::HashVar(_)
523                | Token::LBrace
524        ) || self.next_is_new_stmt_keyword(stmt_line)
525    }
526
527    // ── Top level ──
528
529    pub fn parse_program(&mut self) -> PerlResult<Program> {
530        let mut statements = self.parse_statements()?;
531        // Prepend any synthetic SubDecl stubs queued by anonymous overload
532        // handlers so the package-qualified synthetic names resolve when the
533        // overload table is consulted at runtime. (PARITY-012)
534        if !self.pending_synthetic_subs.is_empty() {
535            let synthetics = std::mem::take(&mut self.pending_synthetic_subs);
536            let mut combined = Vec::with_capacity(synthetics.len() + statements.len());
537            combined.extend(synthetics);
538            combined.append(&mut statements);
539            statements = combined;
540        }
541        Ok(Program { statements })
542    }
543
544    /// Parse statements until EOF. Used by parse_program and parse_block_from_str.
545    pub fn parse_statements(&mut self) -> PerlResult<Vec<Statement>> {
546        let mut statements = Vec::new();
547        while !self.at_eof() {
548            if matches!(self.peek(), Token::Semicolon) {
549                let line = self.peek_line();
550                self.advance();
551                statements.push(Statement {
552                    label: None,
553                    kind: StmtKind::Empty,
554                    line,
555                });
556                continue;
557            }
558            statements.push(self.parse_statement()?);
559        }
560        Ok(statements)
561    }
562
563    // ── Statements ──
564
565    fn parse_statement(&mut self) -> PerlResult<Statement> {
566        let line = self.peek_line();
567
568        // Statement label `FOO:` / `boot:` / `BAR_BAZ:` (not `Foo::` — that is `Ident` + `::`).
569        // Uppercase-only was too strict: XSLoader.pm uses `boot:` before `my $xs = ...`.
570        let label = match self.peek().clone() {
571            Token::Ident(_) => {
572                if matches!(self.peek_at(1), Token::Colon)
573                    && !matches!(self.peek_at(2), Token::Colon)
574                {
575                    let (tok, _) = self.advance();
576                    let l = match tok {
577                        Token::Ident(l) => l,
578                        _ => unreachable!(),
579                    };
580                    self.advance(); // ':'
581                    Some(l)
582                } else {
583                    None
584                }
585            }
586            _ => None,
587        };
588
589        let mut stmt = match self.peek().clone() {
590            Token::FormatDecl { .. } => {
591                let tok_line = self.peek_line();
592                let (tok, _) = self.advance();
593                match tok {
594                    Token::FormatDecl { name, lines } => Statement {
595                        label: label.clone(),
596                        kind: StmtKind::FormatDecl { name, lines },
597                        line: tok_line,
598                    },
599                    _ => unreachable!(),
600                }
601            }
602            Token::Ident(ref kw) => match kw.as_str() {
603                "if" => self.parse_if()?,
604                "unless" => self.parse_unless()?,
605                "while" => {
606                    let mut s = self.parse_while()?;
607                    if let StmtKind::While {
608                        label: ref mut lbl, ..
609                    } = s.kind
610                    {
611                        *lbl = label.clone();
612                    }
613                    s
614                }
615                "until" => {
616                    let mut s = self.parse_until()?;
617                    if let StmtKind::Until {
618                        label: ref mut lbl, ..
619                    } = s.kind
620                    {
621                        *lbl = label.clone();
622                    }
623                    s
624                }
625                "for" => {
626                    let mut s = self.parse_for_or_foreach()?;
627                    match s.kind {
628                        StmtKind::For {
629                            label: ref mut lbl, ..
630                        }
631                        | StmtKind::Foreach {
632                            label: ref mut lbl, ..
633                        } => *lbl = label.clone(),
634                        _ => {}
635                    }
636                    s
637                }
638                "foreach" => {
639                    let mut s = self.parse_foreach()?;
640                    if let StmtKind::Foreach {
641                        label: ref mut lbl, ..
642                    } = s.kind
643                    {
644                        *lbl = label.clone();
645                    }
646                    s
647                }
648                "sub" => {
649                    if crate::no_interop_mode() {
650                        return Err(self.syntax_err(
651                            "stryke uses `fn` instead of `sub` (--no-interop is active)",
652                            self.peek_line(),
653                        ));
654                    }
655                    self.parse_sub_decl(true)?
656                }
657                "fn" => self.parse_sub_decl(false)?,
658                "struct" => {
659                    if crate::compat_mode() {
660                        return Err(self.syntax_err(
661                            "`struct` is a stryke extension (disabled by --compat)",
662                            self.peek_line(),
663                        ));
664                    }
665                    self.parse_struct_decl()?
666                }
667                "enum" => {
668                    if crate::compat_mode() {
669                        return Err(self.syntax_err(
670                            "`enum` is a stryke extension (disabled by --compat)",
671                            self.peek_line(),
672                        ));
673                    }
674                    self.parse_enum_decl()?
675                }
676                "class" => {
677                    if crate::compat_mode() {
678                        // TODO: parse Perl 5.38 class syntax with :isa()
679                        return Err(self.syntax_err(
680                            "Perl 5.38 `class` syntax not yet implemented in --compat mode",
681                            self.peek_line(),
682                        ));
683                    }
684                    self.parse_class_decl(false, false)?
685                }
686                "abstract" => {
687                    self.advance(); // abstract
688                    if !matches!(self.peek(), Token::Ident(ref s) if s == "class") {
689                        return Err(self.syntax_err(
690                            "`abstract` must be followed by `class`",
691                            self.peek_line(),
692                        ));
693                    }
694                    self.parse_class_decl(true, false)?
695                }
696                "final" => {
697                    self.advance(); // final
698                    if !matches!(self.peek(), Token::Ident(ref s) if s == "class") {
699                        return Err(self
700                            .syntax_err("`final` must be followed by `class`", self.peek_line()));
701                    }
702                    self.parse_class_decl(false, true)?
703                }
704                "trait" => {
705                    if crate::compat_mode() {
706                        return Err(self.syntax_err(
707                            "`trait` is a stryke extension (disabled by --compat)",
708                            self.peek_line(),
709                        ));
710                    }
711                    self.parse_trait_decl()?
712                }
713                "my" => self.parse_my_our_local("my", false)?,
714                "state" => self.parse_my_our_local("state", false)?,
715                "mysync" => {
716                    if crate::compat_mode() {
717                        return Err(self.syntax_err(
718                            "`mysync` is a stryke extension (disabled by --compat)",
719                            self.peek_line(),
720                        ));
721                    }
722                    self.parse_my_our_local("mysync", false)?
723                }
724                "oursync" => {
725                    if crate::compat_mode() {
726                        return Err(self.syntax_err(
727                            "`oursync` is a stryke extension (disabled by --compat)",
728                            self.peek_line(),
729                        ));
730                    }
731                    self.parse_my_our_local("oursync", false)?
732                }
733                "frozen" | "const" => {
734                    let leading = kw.as_str().to_string();
735                    if crate::compat_mode() {
736                        return Err(self.syntax_err(
737                            format!("`{leading}` is a stryke extension (disabled by --compat)"),
738                            self.peek_line(),
739                        ));
740                    }
741                    // `frozen my $x = val;` / `const my $x = val;` — the
742                    // two spellings are interchangeable (`const` is the
743                    // more-familiar name for new users). Expects `my`
744                    // to follow.
745                    self.advance(); // consume "frozen"/"const"
746                    if let Token::Ident(ref kw) = self.peek().clone() {
747                        if kw == "my" {
748                            // Accept type annotations the same way `typed
749                            // my $x : Int` does — `const`/`frozen` is
750                            // orthogonal to typing, and `: Type` after a
751                            // name is unambiguous in either form.
752                            let mut stmt = self.parse_my_our_local("my", true)?;
753                            if let StmtKind::My(ref mut decls) = stmt.kind {
754                                for decl in decls.iter_mut() {
755                                    decl.frozen = true;
756                                }
757                            }
758                            stmt
759                        } else {
760                            return Err(self.syntax_err(
761                                format!("Expected 'my' after '{leading}'"),
762                                self.peek_line(),
763                            ));
764                        }
765                    } else {
766                        return Err(self.syntax_err(
767                            format!("Expected 'my' after '{leading}'"),
768                            self.peek_line(),
769                        ));
770                    }
771                }
772                "typed" => {
773                    if crate::compat_mode() {
774                        return Err(self.syntax_err(
775                            "`typed` is a stryke extension (disabled by --compat)",
776                            self.peek_line(),
777                        ));
778                    }
779                    self.advance();
780                    if let Token::Ident(ref kw) = self.peek().clone() {
781                        if kw == "my" {
782                            self.parse_my_our_local("my", true)?
783                        } else {
784                            return Err(
785                                self.syntax_err("Expected 'my' after 'typed'", self.peek_line())
786                            );
787                        }
788                    } else {
789                        return Err(
790                            self.syntax_err("Expected 'my' after 'typed'", self.peek_line())
791                        );
792                    }
793                }
794                "our" => self.parse_my_our_local("our", false)?,
795                "local" => self.parse_my_our_local("local", false)?,
796                "package" => self.parse_package()?,
797                "use" => self.parse_use()?,
798                "no" => self.parse_no()?,
799                "return" => self.parse_return()?,
800                "last" => {
801                    self.advance();
802                    let lbl = if let Token::Ident(ref s) = self.peek() {
803                        if s.chars().all(|c| c.is_uppercase() || c == '_') {
804                            let (Token::Ident(l), _) = self.advance() else {
805                                unreachable!()
806                            };
807                            Some(l)
808                        } else {
809                            None
810                        }
811                    } else {
812                        None
813                    };
814                    let stmt = Statement {
815                        label: None,
816                        kind: StmtKind::Last(lbl.or(label.clone())),
817                        line,
818                    };
819                    self.parse_stmt_postfix_modifier(stmt)?
820                }
821                "next" => {
822                    self.advance();
823                    let lbl = if let Token::Ident(ref s) = self.peek() {
824                        if s.chars().all(|c| c.is_uppercase() || c == '_') {
825                            let (Token::Ident(l), _) = self.advance() else {
826                                unreachable!()
827                            };
828                            Some(l)
829                        } else {
830                            None
831                        }
832                    } else {
833                        None
834                    };
835                    let stmt = Statement {
836                        label: None,
837                        kind: StmtKind::Next(lbl.or(label.clone())),
838                        line,
839                    };
840                    self.parse_stmt_postfix_modifier(stmt)?
841                }
842                "redo" => {
843                    self.advance();
844                    self.eat(&Token::Semicolon);
845                    Statement {
846                        label: None,
847                        kind: StmtKind::Redo(label.clone()),
848                        line,
849                    }
850                }
851                "BEGIN" => {
852                    self.advance();
853                    let block = self.parse_block()?;
854                    Statement {
855                        label: None,
856                        kind: StmtKind::Begin(block),
857                        line,
858                    }
859                }
860                "END" => {
861                    self.advance();
862                    let block = self.parse_block()?;
863                    Statement {
864                        label: None,
865                        kind: StmtKind::End(block),
866                        line,
867                    }
868                }
869                "UNITCHECK" => {
870                    self.advance();
871                    let block = self.parse_block()?;
872                    Statement {
873                        label: None,
874                        kind: StmtKind::UnitCheck(block),
875                        line,
876                    }
877                }
878                "CHECK" => {
879                    self.advance();
880                    let block = self.parse_block()?;
881                    Statement {
882                        label: None,
883                        kind: StmtKind::Check(block),
884                        line,
885                    }
886                }
887                "INIT" => {
888                    self.advance();
889                    let block = self.parse_block()?;
890                    Statement {
891                        label: None,
892                        kind: StmtKind::Init(block),
893                        line,
894                    }
895                }
896                "goto" => {
897                    self.advance();
898                    let target = self.parse_expression()?;
899                    let stmt = Statement {
900                        label: None,
901                        kind: StmtKind::Goto {
902                            target: Box::new(target),
903                        },
904                        line,
905                    };
906                    // `goto $l if COND;` / `goto &$cr if defined &$cr;` (XSLoader.pm)
907                    self.parse_stmt_postfix_modifier(stmt)?
908                }
909                "continue" => {
910                    self.advance();
911                    let block = self.parse_block()?;
912                    Statement {
913                        label: None,
914                        kind: StmtKind::Continue(block),
915                        line,
916                    }
917                }
918                "before"
919                    if matches!(
920                        self.peek_at(1),
921                        Token::SingleString(_) | Token::DoubleString(_)
922                    ) =>
923                {
924                    self.parse_advice_decl(crate::ast::AdviceKind::Before)?
925                }
926                "after"
927                    if matches!(
928                        self.peek_at(1),
929                        Token::SingleString(_) | Token::DoubleString(_)
930                    ) =>
931                {
932                    self.parse_advice_decl(crate::ast::AdviceKind::After)?
933                }
934                "around"
935                    if matches!(
936                        self.peek_at(1),
937                        Token::SingleString(_) | Token::DoubleString(_)
938                    ) =>
939                {
940                    self.parse_advice_decl(crate::ast::AdviceKind::Around)?
941                }
942                "try" => self.parse_try_catch()?,
943                "defer" => self.parse_defer_stmt()?,
944                "tie" => self.parse_tie_stmt()?,
945                "given" => self.parse_given()?,
946                "when" => self.parse_when_stmt()?,
947                "default" => self.parse_default_stmt()?,
948                "eval_timeout" => self.parse_eval_timeout()?,
949                "do" => {
950                    if matches!(self.peek_at(1), Token::LBrace) {
951                        self.advance();
952                        let body = self.parse_block()?;
953                        if let Token::Ident(ref w) = self.peek().clone() {
954                            if w == "while" {
955                                self.advance();
956                                self.expect(&Token::LParen)?;
957                                let mut condition = self.parse_expression()?;
958                                Self::mark_match_scalar_g_for_boolean_condition(&mut condition);
959                                self.expect(&Token::RParen)?;
960                                self.eat(&Token::Semicolon);
961                                Statement {
962                                    label: label.clone(),
963                                    kind: StmtKind::DoWhile { body, condition },
964                                    line,
965                                }
966                            } else {
967                                let inner_line = body.first().map(|s| s.line).unwrap_or(line);
968                                let inner = Expr {
969                                    kind: ExprKind::CodeRef {
970                                        params: vec![],
971                                        body,
972                                    },
973                                    line: inner_line,
974                                };
975                                let expr = Expr {
976                                    kind: ExprKind::Do(Box::new(inner)),
977                                    line,
978                                };
979                                let stmt = Statement {
980                                    label: label.clone(),
981                                    kind: StmtKind::Expression(expr),
982                                    line,
983                                };
984                                // `do { } if EXPR` / `do { } unless EXPR` — postfix modifier, not a new `if (` statement.
985                                self.parse_stmt_postfix_modifier(stmt)?
986                            }
987                        } else {
988                            let inner_line = body.first().map(|s| s.line).unwrap_or(line);
989                            let inner = Expr {
990                                kind: ExprKind::CodeRef {
991                                    params: vec![],
992                                    body,
993                                },
994                                line: inner_line,
995                            };
996                            let expr = Expr {
997                                kind: ExprKind::Do(Box::new(inner)),
998                                line,
999                            };
1000                            let stmt = Statement {
1001                                label: label.clone(),
1002                                kind: StmtKind::Expression(expr),
1003                                line,
1004                            };
1005                            self.parse_stmt_postfix_modifier(stmt)?
1006                        }
1007                    } else {
1008                        if let Some(expr) = self.try_parse_bareword_stmt_call() {
1009                            let stmt = self.maybe_postfix_modifier(expr)?;
1010                            self.parse_stmt_postfix_modifier(stmt)?
1011                        } else {
1012                            let expr = self.parse_expression()?;
1013                            let stmt = self.maybe_postfix_modifier(expr)?;
1014                            self.parse_stmt_postfix_modifier(stmt)?
1015                        }
1016                    }
1017                }
1018                _ => {
1019                    // `foo;` or `{ foo }` — bareword statement is a zero-arg call (topic `$_` at runtime).
1020                    if let Some(expr) = self.try_parse_bareword_stmt_call() {
1021                        let stmt = self.maybe_postfix_modifier(expr)?;
1022                        self.parse_stmt_postfix_modifier(stmt)?
1023                    } else {
1024                        let expr = self.parse_expression()?;
1025                        let stmt = self.maybe_postfix_modifier(expr)?;
1026                        self.parse_stmt_postfix_modifier(stmt)?
1027                    }
1028                }
1029            },
1030            Token::LBrace => {
1031                // Disambiguate hashref `{ k => v }` from block `{ stmt; stmt }`.
1032                // If it looks like a hashref, parse as expression; otherwise parse as block.
1033                if self.looks_like_hashref() {
1034                    let expr = self.parse_expression()?;
1035                    let stmt = self.maybe_postfix_modifier(expr)?;
1036                    self.parse_stmt_postfix_modifier(stmt)?
1037                } else {
1038                    let block = self.parse_block()?;
1039                    let stmt = Statement {
1040                        label: None,
1041                        kind: StmtKind::Block(block),
1042                        line,
1043                    };
1044                    // `{ … } if EXPR` / `{ … } unless EXPR` — same postfix rule as `do { } if …` (not `if (`).
1045                    self.parse_stmt_postfix_modifier(stmt)?
1046                }
1047            }
1048            _ => {
1049                let expr = self.parse_expression()?;
1050                let stmt = self.maybe_postfix_modifier(expr)?;
1051                self.parse_stmt_postfix_modifier(stmt)?
1052            }
1053        };
1054
1055        stmt.label = label;
1056        Ok(stmt)
1057    }
1058
1059    /// Handle postfix if/unless on statement-level keywords like last/next.
1060    fn parse_stmt_postfix_modifier(&mut self, stmt: Statement) -> PerlResult<Statement> {
1061        let line = stmt.line;
1062        // Implicit semicolon: a modifier keyword on a new line is a new
1063        // statement, not a postfix modifier.  This prevents semicolon-less
1064        // code like `my $x = "val"\nif ($x) { ... }` from being mis-parsed
1065        // as `my $x = "val" if ($x) { ... }`.
1066        if self.peek_line() > self.prev_line() {
1067            self.eat(&Token::Semicolon);
1068            return Ok(stmt);
1069        }
1070        if let Token::Ident(ref kw) = self.peek().clone() {
1071            match kw.as_str() {
1072                "if" => {
1073                    self.advance();
1074                    let mut cond = self.parse_expression()?;
1075                    Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
1076                    self.eat(&Token::Semicolon);
1077                    return Ok(Statement {
1078                        label: None,
1079                        kind: StmtKind::If {
1080                            condition: cond,
1081                            body: vec![stmt],
1082                            elsifs: vec![],
1083                            else_block: None,
1084                        },
1085                        line,
1086                    });
1087                }
1088                "unless" => {
1089                    self.advance();
1090                    let mut cond = self.parse_expression()?;
1091                    Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
1092                    self.eat(&Token::Semicolon);
1093                    return Ok(Statement {
1094                        label: None,
1095                        kind: StmtKind::Unless {
1096                            condition: cond,
1097                            body: vec![stmt],
1098                            else_block: None,
1099                        },
1100                        line,
1101                    });
1102                }
1103                "while" | "until" | "for" | "foreach" => {
1104                    // `do { } for @a` / `{ } while COND` — same postfix forms as [`maybe_postfix_modifier`],
1105                    // not a new `for (` / `while (` statement (which would require `(` after `for`).
1106                    if let Some(expr) = Self::stmt_into_postfix_body_expr(stmt) {
1107                        let out = self.maybe_postfix_modifier(expr)?;
1108                        self.eat(&Token::Semicolon);
1109                        return Ok(out);
1110                    }
1111                    return Err(self.syntax_err(
1112                        format!("postfix `{}` is not supported on this statement form", kw),
1113                        self.peek_line(),
1114                    ));
1115                }
1116                // `{ } pmap @a` / `{ } pflat_map @a` / `{ } pfor @a` / `do { } …` — same shapes as prefix forms.
1117                "pmap" | "pflat_map" | "pgrep" | "pfor" | "preduce" | "pcache" => {
1118                    let line = stmt.line;
1119                    let block = self.stmt_into_parallel_block(stmt)?;
1120                    let which = kw.as_str();
1121                    self.advance();
1122                    self.eat(&Token::Comma);
1123                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
1124                    self.eat(&Token::Semicolon);
1125                    let list = Box::new(list);
1126                    let progress = progress.map(Box::new);
1127                    let kind = match which {
1128                        "pmap" => ExprKind::PMapExpr {
1129                            block,
1130                            list,
1131                            progress,
1132                            flat_outputs: false,
1133                            on_cluster: None,
1134                            stream: false,
1135                        },
1136                        "pflat_map" => ExprKind::PMapExpr {
1137                            block,
1138                            list,
1139                            progress,
1140                            flat_outputs: true,
1141                            on_cluster: None,
1142                            stream: false,
1143                        },
1144                        "pgrep" => ExprKind::PGrepExpr {
1145                            block,
1146                            list,
1147                            progress,
1148                            stream: false,
1149                        },
1150                        "pfor" => ExprKind::PForExpr {
1151                            block,
1152                            list,
1153                            progress,
1154                        },
1155                        "preduce" => ExprKind::PReduceExpr {
1156                            block,
1157                            list,
1158                            progress,
1159                        },
1160                        "pcache" => ExprKind::PcacheExpr {
1161                            block,
1162                            list,
1163                            progress,
1164                        },
1165                        _ => unreachable!(),
1166                    };
1167                    return Ok(Statement {
1168                        label: None,
1169                        kind: StmtKind::Expression(Expr { kind, line }),
1170                        line,
1171                    });
1172                }
1173                _ => {}
1174            }
1175        }
1176        self.eat(&Token::Semicolon);
1177        Ok(stmt)
1178    }
1179
1180    /// Block body for postfix `pmap` / `pfor` / … — bare `{ }`, `do { }`, or any expression
1181    /// statement (wrapped as a one-line block, e.g. `` `cmd` pfor @a ``).
1182    fn stmt_into_parallel_block(&self, stmt: Statement) -> PerlResult<Block> {
1183        let line = stmt.line;
1184        match stmt.kind {
1185            StmtKind::Block(block) => Ok(block),
1186            StmtKind::Expression(expr) => {
1187                if let ExprKind::Do(ref inner) = expr.kind {
1188                    if let ExprKind::CodeRef { ref body, .. } = inner.kind {
1189                        return Ok(body.clone());
1190                    }
1191                }
1192                Ok(vec![Statement {
1193                    label: None,
1194                    kind: StmtKind::Expression(expr),
1195                    line,
1196                }])
1197            }
1198            _ => Err(self.syntax_err(
1199                "postfix parallel op expects `do { }`, a bare `{ }` block, or an expression statement",
1200                line,
1201            )),
1202        }
1203    }
1204
1205    /// `StmtKind::Expression` or a bare block (`StmtKind::Block`) as an [`Expr`] for postfix
1206    /// `while` / `until` / `for` / `foreach` (mirrors `do { }` → [`ExprKind::Do`]\([`ExprKind::CodeRef`]\)).
1207    fn stmt_into_postfix_body_expr(stmt: Statement) -> Option<Expr> {
1208        match stmt.kind {
1209            StmtKind::Expression(expr) => Some(expr),
1210            StmtKind::Block(block) => {
1211                let line = stmt.line;
1212                let inner = Expr {
1213                    kind: ExprKind::CodeRef {
1214                        params: vec![],
1215                        body: block,
1216                    },
1217                    line,
1218                };
1219                Some(Expr {
1220                    kind: ExprKind::Do(Box::new(inner)),
1221                    line,
1222                })
1223            }
1224            _ => None,
1225        }
1226    }
1227
1228    /// Statement-modifier keywords that must not be consumed as part of a comma-separated list
1229    /// (same set as [`parse_list_until_terminator`]).
1230    fn peek_is_postfix_stmt_modifier_keyword(&self) -> bool {
1231        matches!(
1232            self.peek(),
1233            Token::Ident(ref kw)
1234                if matches!(
1235                    kw.as_str(),
1236                    "if" | "unless" | "while" | "until" | "for" | "foreach"
1237                )
1238        )
1239    }
1240
1241    /// Token classes whose precedence sits below a Perl-style named unary
1242    /// operator. When one of these is the next token after a unary keyword
1243    /// (`length`, `len`, `cnt`, …), the keyword takes no explicit argument
1244    /// and the surrounding expression continues. Mirrors the `parse_one_arg_or_default`
1245    /// boundary set; kept as a separate predicate so other parse paths can
1246    /// reuse it without committing to default-to-`$_` semantics.
1247    fn peek_is_named_unary_terminator(&self) -> bool {
1248        matches!(
1249            self.peek(),
1250            Token::Semicolon
1251                | Token::RBrace
1252                | Token::RParen
1253                | Token::RBracket
1254                | Token::Eof
1255                | Token::Comma
1256                | Token::FatArrow
1257                | Token::PipeForward
1258                | Token::Question
1259                | Token::Colon
1260                | Token::NumEq
1261                | Token::NumNe
1262                | Token::NumLt
1263                | Token::NumGt
1264                | Token::NumLe
1265                | Token::NumGe
1266                | Token::Spaceship
1267                | Token::StrEq
1268                | Token::StrNe
1269                | Token::StrLt
1270                | Token::StrGt
1271                | Token::StrLe
1272                | Token::StrGe
1273                | Token::StrCmp
1274                | Token::LogAnd
1275                | Token::LogOr
1276                | Token::LogAndWord
1277                | Token::LogOrWord
1278                | Token::DefinedOr
1279                | Token::Range
1280                | Token::RangeExclusive
1281                | Token::Assign
1282                | Token::PlusAssign
1283                | Token::MinusAssign
1284                | Token::MulAssign
1285                | Token::DivAssign
1286                | Token::ModAssign
1287                | Token::PowAssign
1288                | Token::DotAssign
1289                | Token::AndAssign
1290                | Token::OrAssign
1291                | Token::XorAssign
1292                | Token::DefinedOrAssign
1293                | Token::ShiftLeftAssign
1294                | Token::ShiftRightAssign
1295                | Token::BitAndAssign
1296                | Token::BitOrAssign
1297        )
1298    }
1299
1300    fn maybe_postfix_modifier(&mut self, expr: Expr) -> PerlResult<Statement> {
1301        let line = expr.line;
1302        // Implicit semicolon: modifier keyword on a new line starts a new statement.
1303        if self.peek_line() > self.prev_line() {
1304            return Ok(Statement {
1305                label: None,
1306                kind: StmtKind::Expression(expr),
1307                line,
1308            });
1309        }
1310        match self.peek() {
1311            Token::Ident(ref kw) => match kw.as_str() {
1312                "if" => {
1313                    self.advance();
1314                    let cond = self.parse_expression()?;
1315                    Ok(Statement {
1316                        label: None,
1317                        kind: StmtKind::Expression(Expr {
1318                            kind: ExprKind::PostfixIf {
1319                                expr: Box::new(expr),
1320                                condition: Box::new(cond),
1321                            },
1322                            line,
1323                        }),
1324                        line,
1325                    })
1326                }
1327                "unless" => {
1328                    self.advance();
1329                    let cond = self.parse_expression()?;
1330                    Ok(Statement {
1331                        label: None,
1332                        kind: StmtKind::Expression(Expr {
1333                            kind: ExprKind::PostfixUnless {
1334                                expr: Box::new(expr),
1335                                condition: Box::new(cond),
1336                            },
1337                            line,
1338                        }),
1339                        line,
1340                    })
1341                }
1342                "while" => {
1343                    self.advance();
1344                    let cond = self.parse_expression()?;
1345                    Ok(Statement {
1346                        label: None,
1347                        kind: StmtKind::Expression(Expr {
1348                            kind: ExprKind::PostfixWhile {
1349                                expr: Box::new(expr),
1350                                condition: Box::new(cond),
1351                            },
1352                            line,
1353                        }),
1354                        line,
1355                    })
1356                }
1357                "until" => {
1358                    self.advance();
1359                    let cond = self.parse_expression()?;
1360                    Ok(Statement {
1361                        label: None,
1362                        kind: StmtKind::Expression(Expr {
1363                            kind: ExprKind::PostfixUntil {
1364                                expr: Box::new(expr),
1365                                condition: Box::new(cond),
1366                            },
1367                            line,
1368                        }),
1369                        line,
1370                    })
1371                }
1372                "for" | "foreach" => {
1373                    self.advance();
1374                    let list = self.parse_expression()?;
1375                    Ok(Statement {
1376                        label: None,
1377                        kind: StmtKind::Expression(Expr {
1378                            kind: ExprKind::PostfixForeach {
1379                                expr: Box::new(expr),
1380                                list: Box::new(list),
1381                            },
1382                            line,
1383                        }),
1384                        line,
1385                    })
1386                }
1387                _ => Ok(Statement {
1388                    label: None,
1389                    kind: StmtKind::Expression(expr),
1390                    line,
1391                }),
1392            },
1393            _ => Ok(Statement {
1394                label: None,
1395                kind: StmtKind::Expression(expr),
1396                line,
1397            }),
1398        }
1399    }
1400
1401    /// `name;` or `name}` — a bare identifier statement is a sub call with no explicit args (`$_` implied).
1402    fn try_parse_bareword_stmt_call(&mut self) -> Option<Expr> {
1403        let saved = self.pos;
1404        let line = self.peek_line();
1405        let mut name = match self.peek() {
1406            Token::Ident(n) => n.clone(),
1407            _ => return None,
1408        };
1409        // Names that begin `parse_named_expr` (builtins / `undef` / …) must use that path, not a sub call.
1410        if name.starts_with('\x00') || !Self::bareword_stmt_may_be_sub(&name) {
1411            return None;
1412        }
1413        self.advance();
1414        while self.eat(&Token::PackageSep) {
1415            match self.advance() {
1416                (Token::Ident(part), _) => {
1417                    name = format!("{}::{}", name, part);
1418                }
1419                _ => {
1420                    self.pos = saved;
1421                    return None;
1422                }
1423            }
1424        }
1425        match self.peek() {
1426            Token::Semicolon | Token::RBrace => Some(Expr {
1427                kind: ExprKind::FuncCall { name, args: vec![] },
1428                line,
1429            }),
1430            _ => {
1431                self.pos = saved;
1432                None
1433            }
1434        }
1435    }
1436
1437    /// Map an operator-keyword token (the lexer converts `eq`, `ne`, …, `and`,
1438    /// `or`, `not`, `x` to dedicated tokens) back to its identifier spelling.
1439    /// Used in hash-key contexts where the bareword form is the user's intent.
1440    pub(crate) fn operator_keyword_to_ident_str(tok: &Token) -> Option<&'static str> {
1441        Some(match tok {
1442            Token::StrEq => "eq",
1443            Token::StrNe => "ne",
1444            Token::StrLt => "lt",
1445            Token::StrGt => "gt",
1446            Token::StrLe => "le",
1447            Token::StrGe => "ge",
1448            Token::StrCmp => "cmp",
1449            Token::LogAndWord => "and",
1450            Token::LogOrWord => "or",
1451            Token::LogNotWord => "not",
1452            Token::X => "x",
1453            _ => return None,
1454        })
1455    }
1456
1457    /// Bare names that resolve to the topic-slot scalar matrix:
1458    /// `_`, `_0`, `_1`, …, `_N`, plus `_<+`, `_N<+` for the 4-deep outer chain.
1459    /// These must NOT be treated as zero-arg sub calls — they're scalar var refs.
1460    pub(crate) fn is_underscore_topic_slot(name: &str) -> bool {
1461        if name == "_" {
1462            return true;
1463        }
1464        if !name.starts_with('_') || name.len() < 2 {
1465            return false;
1466        }
1467        let bytes = name.as_bytes();
1468        let mut i = 1;
1469        // Optional digit run (positional slot index).
1470        while i < bytes.len() && bytes[i].is_ascii_digit() {
1471            i += 1;
1472        }
1473        // Then any number of `<` chevrons (runtime cap at 5; lexer accepts more).
1474        let chevrons_start = i;
1475        while i < bytes.len() && bytes[i] == b'<' {
1476            i += 1;
1477        }
1478        // Must be one of: `_`, `_N`, `_<+`, `_N<+`. No other trailing chars.
1479        i == bytes.len() && (i > 1 || chevrons_start > 1)
1480    }
1481
1482    /// Bareword names that map to Perl special variables / filehandles /
1483    /// compile-time tokens. A user-defined sub with any of these names
1484    /// would shadow the special variable's expression-position usage and
1485    /// produce silently-broken code. Reject at parse time with a
1486    /// foot-gun error message.
1487    ///
1488    /// Sigil-form spellings (`$@`, `$!`, `@ARGV`, `%ENV`, etc.) are caught
1489    /// separately via the `parse_sub_decl` catch-all branch — those don't
1490    /// even lex as `Token::Ident` so they hit a different code path.
1491    pub(crate) fn is_reserved_special_var_name(name: &str) -> bool {
1492        matches!(
1493            name,
1494            // Standard filehandles (Perl: STDIN, STDOUT, STDERR, ARGV, …)
1495            "STDIN" | "STDOUT" | "STDERR" | "ARGV" | "ARGVOUT" | "DATA"
1496            // Package globals, normally accessed via sigils (@ARGV, %ENV,
1497            // @INC, %SIG, @ISA, %ENV, etc.) — bareword shadow is a foot-gun.
1498            // NOTE: `AUTOLOAD` is intentionally NOT in this list — `fn
1499            // AUTOLOAD { ... }` is the legitimate Perl idiom for handling
1500            // missing-method dispatch. The runtime sets `$AUTOLOAD` to the
1501            // missing sub's qualified name before invoking the user's
1502            // AUTOLOAD sub. Adding it here would break that mechanism.
1503            | "ENV" | "INC" | "SIG" | "ISA"
1504            | "EXPORT" | "EXPORT_OK" | "EXPORT_TAGS"
1505            | "VERSION"
1506            // Compile-time tokens (resolve to constants at parse time).
1507            | "__FILE__" | "__LINE__" | "__PACKAGE__" | "__SUB__"
1508            | "__DATA__" | "__END__"
1509        )
1510    }
1511
1512    /// Identifiers that start a [`parse_named_expr`] arm (builtins / special forms), not a bare sub call.
1513    fn bareword_stmt_may_be_sub(name: &str) -> bool {
1514        // Topic-slot scalar names (`_`, `_N`, `_<+`, `_N<+`) are scalar
1515        // variables, not zero-arg sub calls. Without this guard, the
1516        // statement-position parser would emit `Op::Call("_0", 0)` and fail
1517        // at runtime with "Undefined subroutine &_0".
1518        if Self::is_underscore_topic_slot(name) {
1519            return false;
1520        }
1521        !matches!(
1522            name,
1523            "__FILE__"
1524                | "__LINE__"
1525                | "abs"
1526                | "async"
1527                | "spawn"
1528                | "atan2"
1529                | "await"
1530                | "barrier"
1531                | "bless"
1532                | "caller"
1533                | "capture"
1534                | "cat"
1535                | "chdir"
1536                | "chmod"
1537                | "chomp"
1538                | "chop"
1539                | "chr"
1540                | "chown"
1541                | "closedir"
1542                | "close"
1543                | "collect"
1544                | "cos"
1545                | "crypt"
1546                | "defined"
1547                | "dec"
1548                | "delete"
1549                | "die"
1550                | "deque"
1551                | "do"
1552                | "each"
1553                | "eof"
1554                | "fore"
1555                | "eval"
1556                | "exec"
1557                | "exists"
1558                | "exit"
1559                | "exp"
1560                | "fan"
1561                | "fan_cap"
1562                | "fc"
1563                | "fetch_url"
1564                | "d"
1565                | "dirs"
1566                | "dr"
1567                | "f"
1568                | "fi"
1569                | "files"
1570                | "filesf"
1571                | "filter"
1572                | "fr"
1573                | "getcwd"
1574                | "glob_par"
1575                | "par_sed"
1576                | "glob"
1577                | "grep"
1578                | "greps"
1579                | "heap"
1580                | "hex"
1581                | "inc"
1582                | "index"
1583                | "int"
1584                | "join"
1585                | "keys"
1586                | "lcfirst"
1587                | "lc"
1588                | "length"
1589                | "link"
1590                | "log"
1591                | "lstat"
1592                | "map"
1593                | "flat_map"
1594                | "maps"
1595                | "flat_maps"
1596                | "flatten"
1597                | "frequencies"
1598                | "freq"
1599                | "pfrequencies"
1600                | "pfreq"
1601                | "interleave"
1602                | "ddump"
1603                | "stringify"
1604                | "str"
1605                | "s"
1606                | "input"
1607                | "lines"
1608                | "words"
1609                | "chars"
1610                | "digits"
1611                | "letters"
1612                | "letters_uc"
1613                | "letters_lc"
1614                | "punctuation"
1615                | "sentences"
1616                | "paragraphs"
1617                | "sections"
1618                | "numbers"
1619                | "graphemes"
1620                | "columns"
1621                | "trim"
1622                | "avg"
1623                | "top"
1624                | "pager"
1625                | "pg"
1626                | "less"
1627                | "count_by"
1628                | "to_file"
1629                | "to_json"
1630                | "to_csv"
1631                | "grep_v"
1632                | "select_keys"
1633                | "pluck"
1634                | "clamp"
1635                | "normalize"
1636                | "stddev"
1637                | "squared"
1638                | "square"
1639                | "cubed"
1640                | "cube"
1641                | "expt"
1642                | "pow"
1643                | "pw"
1644                | "snake_case"
1645                | "camel_case"
1646                | "kebab_case"
1647                | "to_toml"
1648                | "to_yaml"
1649                | "to_xml"
1650                | "to_html"
1651                | "to_markdown"
1652                | "xopen"
1653                | "clip"
1654                | "paste"
1655                | "to_table"
1656                | "sparkline"
1657                | "bar_chart"
1658                | "flame"
1659                | "set"
1660                | "list_count"
1661                | "list_size"
1662                | "count"
1663                | "size"
1664                | "cnt"
1665                | "len"
1666                | "all"
1667                | "any"
1668                | "none"
1669                | "take_while"
1670                | "drop_while"
1671                | "skip_while"
1672                | "skip"
1673                | "first_or"
1674                | "tap"
1675                | "peek"
1676                | "partition"
1677                | "min_by"
1678                | "max_by"
1679                | "zip_with"
1680                | "group_by"
1681                | "chunk_by"
1682                | "with_index"
1683                | "puniq"
1684                | "pfirst"
1685                | "pany"
1686                | "uniq"
1687                | "distinct"
1688                | "shuffle"
1689                | "shuffled"
1690                | "chunked"
1691                | "windowed"
1692                | "match"
1693                | "mkdir"
1694                | "every"
1695                | "gen"
1696                | "oct"
1697                | "open"
1698                | "p"
1699                | "opendir"
1700                | "ord"
1701                | "par_lines"
1702                | "par_walk"
1703                | "pipe"
1704                | "pipes"
1705                | "block_devices"
1706                | "char_devices"
1707                | "exe"
1708                | "executables"
1709                | "rate_limit"
1710                | "retry"
1711                | "pcache"
1712                | "pchannel"
1713                | "pfor"
1714                | "pgrep"
1715                | "pgreps"
1716                | "pipeline"
1717                | "pmap_chunked"
1718                | "pmap_reduce"
1719                | "par_reduce"
1720                | "pmap_on"
1721                | "pflat_map_on"
1722                | "pmap"
1723                | "pmaps"
1724                | "pflat_map"
1725                | "pflat_maps"
1726                | "pop"
1727                | "pos"
1728                | "ppool"
1729                | "preduce_init"
1730                | "preduce"
1731                | "pselect"
1732                | "printf"
1733                | "print"
1734                | "pr"
1735                | "psort"
1736                | "push"
1737                | "pwatch"
1738                | "rand"
1739                | "readdir"
1740                | "readlink"
1741                | "reduce"
1742                | "fold"
1743                | "inject"
1744                | "first"
1745                | "detect"
1746                | "find"
1747                | "find_all"
1748                | "ref"
1749                | "rename"
1750                | "require"
1751                | "rev"
1752                | "reverse"
1753                | "reversed"
1754                | "rewinddir"
1755                | "rindex"
1756                | "rmdir"
1757                | "rm"
1758                | "say"
1759                | "scalar"
1760                | "seekdir"
1761                | "shift"
1762                | "sin"
1763                | "slurp"
1764                | "sockets"
1765                | "sort"
1766                | "splice"
1767                | "splice_last"
1768                | "splice1"
1769                | "spl_last"
1770                | "split"
1771                | "sprintf"
1772                | "sqrt"
1773                | "srand"
1774                | "stat"
1775                | "study"
1776                | "substr"
1777                | "symlink"
1778                | "sym_links"
1779                | "system"
1780                | "telldir"
1781                | "timer"
1782                | "trace"
1783                | "ucfirst"
1784                | "uc"
1785                | "undef"
1786                | "umask"
1787                | "unlink"
1788                | "unshift"
1789                | "utime"
1790                | "values"
1791                | "wantarray"
1792                | "warn"
1793                | "watch"
1794                | "yield"
1795                | "sub"
1796        )
1797    }
1798
1799    fn parse_block(&mut self) -> PerlResult<Block> {
1800        self.expect(&Token::LBrace)?;
1801        // Statements inside a block are NOT pipe RHS - reset depth so nested `~>`
1802        // parses its own input instead of using `$_[0]` placeholder.
1803        let saved_pipe_rhs_depth = self.pipe_rhs_depth;
1804        self.pipe_rhs_depth = 0;
1805        self.block_depth += 1;
1806        let mut stmts = Vec::new();
1807        // `{ |$a, $b| body }` — Ruby-style block params.
1808        // Desugars to `my $a = $_` (1 param), `my $a = $a; my $b = $b` (2 — sort/reduce),
1809        // or `my $p = $_N` for positional N≥3.
1810        if let Some(param_stmts) = self.try_parse_block_params()? {
1811            stmts.extend(param_stmts);
1812        }
1813        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
1814            if self.eat(&Token::Semicolon) {
1815                continue;
1816            }
1817            stmts.push(self.parse_statement()?);
1818        }
1819        self.expect(&Token::RBrace)?;
1820        self.pipe_rhs_depth = saved_pipe_rhs_depth;
1821        self.block_depth -= 1;
1822        Self::default_topic_for_sole_bareword(&mut stmts);
1823        Ok(stmts)
1824    }
1825
1826    /// Try to parse `|$var1, $var2, ...|` at the start of a block.
1827    /// Returns `None` if the leading `|` is not block-param syntax.
1828    /// When successful, returns `my $var = <implicit>` assignment statements
1829    /// that alias the block's positional arguments.
1830    fn try_parse_block_params(&mut self) -> PerlResult<Option<Vec<Statement>>> {
1831        if !matches!(self.peek(), Token::BitOr) {
1832            return Ok(None);
1833        }
1834        // Lookahead: `| $scalar [, $scalar]* |` — verify before consuming.
1835        let mut i = 1; // skip the opening `|`
1836        loop {
1837            match self.peek_at(i) {
1838                Token::ScalarVar(_) => i += 1,
1839                _ => return Ok(None), // not `|$var...|`
1840            }
1841            match self.peek_at(i) {
1842                Token::BitOr => break,  // closing `|`
1843                Token::Comma => i += 1, // more params
1844                _ => return Ok(None),   // not block params
1845            }
1846        }
1847        // Confirmed — consume and build assignments.
1848        let line = self.peek_line();
1849        self.advance(); // eat opening `|`
1850        let mut names = Vec::new();
1851        loop {
1852            if let Token::ScalarVar(ref name) = self.peek().clone() {
1853                names.push(name.clone());
1854                self.advance();
1855            }
1856            if self.eat(&Token::BitOr) {
1857                break;
1858            }
1859            self.expect(&Token::Comma)?;
1860        }
1861        // Generate `my $name = <source>` for each param.
1862        // 1 param  → source is `$_` (map/grep/each/for topic)
1863        // 2 params → sources are `$a`, `$b` (sort/reduce)
1864        // N params → sources are `$_`, `$_1`, `$_2`, … (positional)
1865        let sources: Vec<&str> = match names.len() {
1866            1 => vec!["_"],
1867            2 => vec!["a", "b"],
1868            n => {
1869                // Can't return borrowed from a generated vec, handle below.
1870                let _ = n;
1871                vec![] // sentinel — handled in the else branch
1872            }
1873        };
1874        let mut stmts = Vec::with_capacity(names.len());
1875        if !sources.is_empty() {
1876            for (name, src) in names.iter().zip(sources.iter()) {
1877                stmts.push(Statement {
1878                    label: None,
1879                    kind: StmtKind::My(vec![VarDecl {
1880                        sigil: Sigil::Scalar,
1881                        name: name.clone(),
1882                        initializer: Some(Expr {
1883                            kind: ExprKind::ScalarVar(src.to_string()),
1884                            line,
1885                        }),
1886                        frozen: false,
1887                        type_annotation: None,
1888                    }]),
1889                    line,
1890                });
1891            }
1892        } else {
1893            // N≥3: positional `$_`, `$_1`, `$_2`, …
1894            for (idx, name) in names.iter().enumerate() {
1895                let src = if idx == 0 {
1896                    "_".to_string()
1897                } else {
1898                    format!("_{idx}")
1899                };
1900                stmts.push(Statement {
1901                    label: None,
1902                    kind: StmtKind::My(vec![VarDecl {
1903                        sigil: Sigil::Scalar,
1904                        name: name.clone(),
1905                        initializer: Some(Expr {
1906                            kind: ExprKind::ScalarVar(src),
1907                            line,
1908                        }),
1909                        frozen: false,
1910                        type_annotation: None,
1911                    }]),
1912                    line,
1913                });
1914            }
1915        }
1916        Ok(Some(stmts))
1917    }
1918
1919    /// Block shorthand: when the body is literally one bare builtin call
1920    /// (`{ uc }`, `{ basename }`, `{ to_json }`), inject `$_` as its first
1921    /// argument so `map { basename }` == `map { basename($_) }` uniformly.
1922    ///
1923    /// Without this, the ExprKind-modeled core names (`uc`/`lc`/`length`/…)
1924    /// default to `$_` via their own parse arms, but generic `FuncCall`-
1925    /// dispatched builtins (`basename`/`to_json`/`tj`/`bn`) are called with
1926    /// empty args and return the wrong value. This rewrite levels the
1927    /// playing field at parse time — no per-builtin handling needed.
1928    ///
1929    /// Narrow by design: fires only when the block has *exactly one*
1930    /// expression statement whose sole content is a known-bareword call
1931    /// with zero args. Multi-statement blocks and blocks with any other
1932    /// content are untouched.
1933    fn default_topic_for_sole_bareword(stmts: &mut [Statement]) {
1934        let [only] = stmts else { return };
1935        let StmtKind::Expression(ref mut expr) = only.kind else {
1936            return;
1937        };
1938        let topic_line = expr.line;
1939        let topic_arg = || Expr {
1940            kind: ExprKind::ScalarVar("_".to_string()),
1941            line: topic_line,
1942        };
1943        match expr.kind {
1944            // Zero-arg FuncCall whose name is a known builtin → inject `$_`.
1945            ExprKind::FuncCall {
1946                ref name,
1947                ref mut args,
1948            } if args.is_empty()
1949                && (Self::is_known_bareword(name) || Self::is_try_builtin_name(name)) =>
1950            {
1951                args.push(topic_arg());
1952            }
1953            // Lone bareword (the parser sometimes keeps a bareword as a
1954            // `Bareword` node instead of a zero-arg `FuncCall` —
1955            // e.g. `{ to_json }`, `{ ddump }`). Promote to a call.
1956            ExprKind::Bareword(ref name)
1957                if (Self::is_known_bareword(name) || Self::is_try_builtin_name(name)) =>
1958            {
1959                let n = name.clone();
1960                expr.kind = ExprKind::FuncCall {
1961                    name: n,
1962                    args: vec![topic_arg()],
1963                };
1964            }
1965            _ => {}
1966        }
1967    }
1968
1969    /// `defer { BLOCK }` — register a block to run when the current scope exits.
1970    /// Desugars to a `defer__internal(fn { BLOCK })` function call that the compiler
1971    /// handles specially by emitting Op::DeferBlock.
1972    fn parse_defer_stmt(&mut self) -> PerlResult<Statement> {
1973        let line = self.peek_line();
1974        self.advance(); // defer
1975        let body = self.parse_block()?;
1976        self.eat(&Token::Semicolon);
1977        // Desugar: defer { BLOCK } → defer__internal(fn { BLOCK })
1978        let coderef = Expr {
1979            kind: ExprKind::CodeRef {
1980                params: vec![],
1981                body,
1982            },
1983            line,
1984        };
1985        Ok(Statement {
1986            label: None,
1987            kind: StmtKind::Expression(Expr {
1988                kind: ExprKind::FuncCall {
1989                    name: "defer__internal".to_string(),
1990                    args: vec![coderef],
1991                },
1992                line,
1993            }),
1994            line,
1995        })
1996    }
1997
1998    /// `try { } catch ($err) { }` with optional `finally { }`
1999    fn parse_try_catch(&mut self) -> PerlResult<Statement> {
2000        let line = self.peek_line();
2001        self.advance(); // try
2002        let try_block = self.parse_block()?;
2003        match self.peek() {
2004            Token::Ident(ref k) if k == "catch" => {
2005                self.advance();
2006            }
2007            _ => {
2008                return Err(self.syntax_err("expected 'catch' after try block", self.peek_line()));
2009            }
2010        }
2011        self.expect(&Token::LParen)?;
2012        let catch_var = self.parse_scalar_var_name()?;
2013        self.expect(&Token::RParen)?;
2014        let catch_block = self.parse_block()?;
2015        let finally_block = match self.peek() {
2016            Token::Ident(ref k) if k == "finally" => {
2017                self.advance();
2018                Some(self.parse_block()?)
2019            }
2020            _ => None,
2021        };
2022        self.eat(&Token::Semicolon);
2023        Ok(Statement {
2024            label: None,
2025            kind: StmtKind::TryCatch {
2026                try_block,
2027                catch_var,
2028                catch_block,
2029                finally_block,
2030            },
2031            line,
2032        })
2033    }
2034
2035    /// `thread EXPR stage1 stage2 ...` — Clojure-style threading macro.
2036    /// Desugars to `EXPR |> stage1 |> stage2 |> ...`
2037    ///
2038    /// When `thread_last` is true (`->>` syntax), injects as last arg instead of first.
2039    ///
2040    /// When invoked as the RHS of `|>` (e.g. `LHS |> t s1 s2 ...`), the init
2041    /// is not parsed from tokens — using `parse_unary()` there lets the first
2042    /// bareword greedily consume the next token as its arg, which misparses
2043    /// `t inc pow($_, 2) p` as init=`inc(pow(…))` + stage=`p` instead of three
2044    /// separate stages. Instead, seed init with `$_[0]`, run every remaining
2045    /// token through the stage loop, and wrap the resulting chain in a
2046    /// `CodeRef`. The outer `pipe_forward_apply` then calls it with `lhs` as
2047    /// `$_[0]`, giving `LHS |> t s1 s2 s3` == `LHS |> s1 |> s2 |> s3`.
2048    fn parse_thread_macro(&mut self, _line: usize, thread_last: bool) -> PerlResult<Expr> {
2049        self.parse_thread_macro_inner(_line, thread_last, None)
2050    }
2051
2052    /// Shared core for `~>` / `~>>` / `~s>` / `~s>>`. When
2053    /// `parallel_collector` is `Some` (streaming-mode entry from `~s>` /
2054    /// `~s>>`), after each stage is parsed we push the (just-built) stage
2055    /// expression into the collector and reset `result` to `$_` so the
2056    /// next stage parses against a fresh topic. The collector ends up
2057    /// with one Expr per stage where each stage's input is `$_`, ready
2058    /// to be wrapped as a `fn { ... }` closure for the per-item
2059    /// streaming runtime (`_thread_par_run`).
2060    fn parse_thread_macro_inner(
2061        &mut self,
2062        _line: usize,
2063        thread_last: bool,
2064        mut parallel_collector: Option<&mut Vec<Expr>>,
2065    ) -> PerlResult<Expr> {
2066        // Set thread-last mode for pipe_forward_apply calls within this macro
2067        let saved_thread_last = self.thread_last_mode;
2068        self.thread_last_mode = thread_last;
2069
2070        let pipe_rhs_wrap = self.in_pipe_rhs();
2071        // `pending_thread_input` (set by `~p>` continuation parsing after
2072        // `||>` / `|then|`) supplies a pre-built input expression so we
2073        // skip parsing a source.
2074        let mut result = if let Some(pre) = self.pending_thread_input.take() {
2075            pre
2076        } else if pipe_rhs_wrap {
2077            Expr {
2078                kind: ExprKind::ArrayElement {
2079                    array: "_".to_string(),
2080                    index: Box::new(Expr {
2081                        kind: ExprKind::Integer(0),
2082                        line: _line,
2083                    }),
2084                },
2085                line: _line,
2086            }
2087        } else {
2088            // Suppress paren-less function calls so `t Color::Red p` parses
2089            // the enum variant without consuming `p` as an argument.
2090            self.suppress_parenless_call = self.suppress_parenless_call.saturating_add(1);
2091            let expr = self.parse_thread_input();
2092            self.suppress_parenless_call = self.suppress_parenless_call.saturating_sub(1);
2093            expr?
2094        };
2095        // Capture the source expression for parallel mode BEFORE any stage
2096        // is parsed, then reset `result` to `$_` so the first stage's parse
2097        // reads the topic instead of the source.
2098        let source_for_par = if parallel_collector.is_some() {
2099            let src = std::mem::replace(
2100                &mut result,
2101                Expr {
2102                    kind: ExprKind::ScalarVar("_".into()),
2103                    line: _line,
2104                },
2105            );
2106            Some(src)
2107        } else {
2108            None
2109        };
2110
2111        // Track line where the last stage ended (initially the input expression's line).
2112        let mut last_stage_end_line = self.prev_line();
2113
2114        // Parse stages until we hit a statement terminator
2115        loop {
2116            // Newline termination: if the next token is on a different line than where
2117            // the previous stage ended, the thread macro terminates. This allows
2118            // `~> @arr map { $_ * 2 }` on one line followed by `my @b = ...` on the next
2119            // without requiring a semicolon.
2120            if self.peek_line() > last_stage_end_line {
2121                break;
2122            }
2123
2124            // Check for terminators - |> ends thread and allows piping the result.
2125            // Variables ($x, @x, %x) and declaration keywords (my, our, local, state)
2126            // cannot be stages, so they implicitly terminate the thread macro.
2127            match self.peek() {
2128                Token::Semicolon
2129                | Token::RBrace
2130                | Token::RParen
2131                | Token::RBracket
2132                | Token::PipeForward
2133                | Token::Eof
2134                | Token::ScalarVar(_)
2135                | Token::ArrayVar(_)
2136                | Token::HashVar(_)
2137                | Token::Comma => break,
2138                // `||>` (LogOr + NumGt): chunk-parallel → sequential boundary
2139                // for `~p>` macros. Other thread macros never see this in
2140                // practice; if it appears, terminate the macro and let the
2141                // outer parser handle it.
2142                Token::LogOr if matches!(self.peek_at(1), Token::NumGt) => break,
2143                // `|then|` (BitOr + Ident("then") + BitOr): same boundary.
2144                Token::BitOr
2145                    if matches!(self.peek_at(1), Token::Ident(ref n) if n == "then")
2146                        && matches!(self.peek_at(2), Token::BitOr) =>
2147                {
2148                    break
2149                }
2150                Token::Ident(ref kw)
2151                    if matches!(
2152                        kw.as_str(),
2153                        "my" | "our"
2154                            | "local"
2155                            | "state"
2156                            | "if"
2157                            | "unless"
2158                            | "while"
2159                            | "until"
2160                            | "for"
2161                            | "foreach"
2162                            | "return"
2163                            | "last"
2164                            | "next"
2165                            | "redo"
2166                    ) =>
2167                {
2168                    break
2169                }
2170                _ => {}
2171            }
2172
2173            let stage_line = self.peek_line();
2174
2175            // Parse a stage and apply it to result via pipe
2176            match self.peek().clone() {
2177                // `>{ block }` — standalone anonymous block (sugar for fn { })
2178                Token::ArrowBrace => {
2179                    self.advance(); // consume `>{`
2180                    let mut stmts = Vec::new();
2181                    while !matches!(self.peek(), Token::RBrace | Token::Eof) {
2182                        if self.eat(&Token::Semicolon) {
2183                            continue;
2184                        }
2185                        stmts.push(self.parse_statement()?);
2186                    }
2187                    self.expect(&Token::RBrace)?;
2188                    let code_ref = Expr {
2189                        kind: ExprKind::CodeRef {
2190                            params: vec![],
2191                            body: stmts,
2192                        },
2193                        line: stage_line,
2194                    };
2195                    result = self.pipe_forward_apply(result, code_ref, stage_line)?;
2196                }
2197                // `sub { block }` — blocked in no-interop mode
2198                Token::Ident(ref name) if name == "sub" => {
2199                    if crate::no_interop_mode() {
2200                        return Err(self.syntax_err(
2201                            "stryke uses `fn {}` instead of `sub {}` (--no-interop)",
2202                            stage_line,
2203                        ));
2204                    }
2205                    self.advance(); // consume `sub`
2206                    let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
2207                    let body = self.parse_block()?;
2208                    let code_ref = Expr {
2209                        kind: ExprKind::CodeRef { params, body },
2210                        line: stage_line,
2211                    };
2212                    result = self.pipe_forward_apply(result, code_ref, stage_line)?;
2213                }
2214                // `fn { block }` — stryke anonymous function
2215                Token::Ident(ref name) if name == "fn" => {
2216                    self.advance(); // consume `fn`
2217                    let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
2218                    self.parse_sub_attributes()?;
2219                    let body = self.parse_fn_eq_body_or_block(false)?;
2220                    let code_ref = Expr {
2221                        kind: ExprKind::CodeRef { params, body },
2222                        line: stage_line,
2223                    };
2224                    result = self.pipe_forward_apply(result, code_ref, stage_line)?;
2225                }
2226                // `ident` possibly followed by block (or namespaced like `Foo::Bar::func`)
2227                Token::Ident(ref name) => {
2228                    let mut func_name = name.clone();
2229                    self.advance();
2230
2231                    // Collect namespaced function name (e.g., Rosetta::Stack::push)
2232                    while matches!(self.peek(), Token::PackageSep) {
2233                        self.advance(); // consume `::`
2234                        if let Token::Ident(ref part) = self.peek().clone() {
2235                            func_name.push_str("::");
2236                            func_name.push_str(part);
2237                            self.advance();
2238                        } else {
2239                            return Err(self.syntax_err(
2240                                format!(
2241                                    "Expected identifier after `::` in thread stage, got {:?}",
2242                                    self.peek()
2243                                ),
2244                                stage_line,
2245                            ));
2246                        }
2247                    }
2248
2249                    // Handle s/// and tr/// encoded tokens
2250                    if func_name.starts_with('\x00') {
2251                        let parts: Vec<&str> = func_name.split('\x00').collect();
2252                        if parts.len() >= 4 && parts[1] == "s" {
2253                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
2254                            let stage = Expr {
2255                                kind: ExprKind::Substitution {
2256                                    expr: Box::new(result.clone()),
2257                                    pattern: parts[2].to_string(),
2258                                    replacement: parts[3].to_string(),
2259                                    flags: format!("{}r", parts.get(4).unwrap_or(&"")),
2260                                    delim,
2261                                },
2262                                line: stage_line,
2263                            };
2264                            result = stage;
2265                            last_stage_end_line = self.prev_line();
2266                            continue;
2267                        }
2268                        if parts.len() >= 4 && parts[1] == "tr" {
2269                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
2270                            let stage = Expr {
2271                                kind: ExprKind::Transliterate {
2272                                    expr: Box::new(result.clone()),
2273                                    from: parts[2].to_string(),
2274                                    to: parts[3].to_string(),
2275                                    flags: format!("{}r", parts.get(4).unwrap_or(&"")),
2276                                    delim,
2277                                },
2278                                line: stage_line,
2279                            };
2280                            result = stage;
2281                            last_stage_end_line = self.prev_line();
2282                            continue;
2283                        }
2284                        return Err(
2285                            self.syntax_err("Unexpected encoded token in thread", stage_line)
2286                        );
2287                    }
2288
2289                    // `map +{ ... }` — hashref expression form (not a code block).
2290                    // The `+` disambiguates: `+{` is always a hashref constructor.
2291                    // Desugars to `MapExprComma` so pipe_forward_apply threads the
2292                    // list correctly: `t LIST map +{k => $_}` → `map +{k => $_}, LIST`.
2293                    if matches!(self.peek(), Token::Plus)
2294                        && matches!(self.peek_at(1), Token::LBrace)
2295                    {
2296                        self.advance(); // consume `+`
2297                        self.expect(&Token::LBrace)?;
2298                        // try_parse_hash_ref consumes the closing `}`
2299                        let pairs = self.try_parse_hash_ref()?;
2300                        let hashref_expr = Expr {
2301                            kind: ExprKind::HashRef(pairs),
2302                            line: stage_line,
2303                        };
2304                        let flatten_array_refs =
2305                            matches!(func_name.as_str(), "flat_map" | "flat_maps");
2306                        let stream = matches!(func_name.as_str(), "maps" | "flat_maps");
2307                        // Placeholder list — pipe_forward_apply replaces it with `result`.
2308                        let placeholder = Expr {
2309                            kind: ExprKind::Undef,
2310                            line: stage_line,
2311                        };
2312                        let map_node = Expr {
2313                            kind: ExprKind::MapExprComma {
2314                                expr: Box::new(hashref_expr),
2315                                list: Box::new(placeholder),
2316                                flatten_array_refs,
2317                                stream,
2318                            },
2319                            line: stage_line,
2320                        };
2321                        result = self.pipe_forward_apply(result, map_node, stage_line)?;
2322                    // `pmap_chunked CHUNK_SIZE { BLOCK }` — parallel chunked map
2323                    } else if func_name == "pmap_chunked" {
2324                        let chunk_size = self.parse_assign_expr()?;
2325                        let block = self.parse_block_or_bareword_block()?;
2326                        let placeholder = self.pipe_placeholder_list(stage_line);
2327                        let stage = Expr {
2328                            kind: ExprKind::PMapChunkedExpr {
2329                                chunk_size: Box::new(chunk_size),
2330                                block,
2331                                list: Box::new(placeholder),
2332                                progress: None,
2333                            },
2334                            line: stage_line,
2335                        };
2336                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2337                    // `preduce_init INIT { BLOCK }` — parallel reduce with init value
2338                    } else if func_name == "preduce_init" {
2339                        let init = self.parse_assign_expr()?;
2340                        let block = self.parse_block_or_bareword_block()?;
2341                        let placeholder = self.pipe_placeholder_list(stage_line);
2342                        let stage = Expr {
2343                            kind: ExprKind::PReduceInitExpr {
2344                                init: Box::new(init),
2345                                block,
2346                                list: Box::new(placeholder),
2347                                progress: None,
2348                            },
2349                            line: stage_line,
2350                        };
2351                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2352                    // `pmap_reduce { MAP } { REDUCE }` — parallel map-reduce
2353                    } else if func_name == "pmap_reduce" {
2354                        let map_block = self.parse_block_or_bareword_block()?;
2355                        let reduce_block = if matches!(self.peek(), Token::LBrace) {
2356                            self.parse_block()?
2357                        } else {
2358                            self.expect(&Token::Comma)?;
2359                            self.parse_block_or_bareword_cmp_block()?
2360                        };
2361                        let placeholder = self.pipe_placeholder_list(stage_line);
2362                        let stage = Expr {
2363                            kind: ExprKind::PMapReduceExpr {
2364                                map_block,
2365                                reduce_block,
2366                                list: Box::new(placeholder),
2367                                progress: None,
2368                            },
2369                            line: stage_line,
2370                        };
2371                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2372                    // `par_reduce { extract } [ { merge } ]` — chunk-extract-merge.
2373                    // First block runs per chunk in parallel; optional second
2374                    // block reduces pairwise across chunks (omit for auto-merge
2375                    // by result type).
2376                    } else if func_name == "par_reduce" {
2377                        let extract_block = self.parse_block_or_bareword_block()?;
2378                        let reduce_block = if matches!(self.peek(), Token::LBrace) {
2379                            Some(self.parse_block()?)
2380                        } else {
2381                            None
2382                        };
2383                        let placeholder = self.pipe_placeholder_list(stage_line);
2384                        let stage = Expr {
2385                            kind: ExprKind::ParReduceExpr {
2386                                extract_block,
2387                                reduce_block,
2388                                list: Box::new(placeholder),
2389                            },
2390                            line: stage_line,
2391                        };
2392                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2393                    // `pmap_on $cluster { BLOCK }` — parallel map dispatched to a remote
2394                    // cluster. Mirrors the `pmap_chunked` thread-stage shape; the cluster
2395                    // expression is parsed before the block, the threaded list slots in
2396                    // as the placeholder.
2397                    } else if func_name == "pmap_on" || func_name == "pflat_map_on" {
2398                        // Suppress `$cluster { ... }` auto-arrow (`$h->{...}`) so the
2399                        // brace opens the block, not a hash subscript.
2400                        self.suppress_scalar_hash_brace =
2401                            self.suppress_scalar_hash_brace.saturating_add(1);
2402                        let cluster = self.parse_assign_expr();
2403                        self.suppress_scalar_hash_brace =
2404                            self.suppress_scalar_hash_brace.saturating_sub(1);
2405                        let cluster = cluster?;
2406                        // Optional comma between cluster and block (matches the
2407                        // canonical `pmap_on $c, { BLOCK } @list` form in the LSP docs).
2408                        self.eat(&Token::Comma);
2409                        let block = self.parse_block_or_bareword_block()?;
2410                        let placeholder = self.pipe_placeholder_list(stage_line);
2411                        let stage = Expr {
2412                            kind: ExprKind::PMapExpr {
2413                                block,
2414                                list: Box::new(placeholder),
2415                                progress: None,
2416                                flat_outputs: func_name == "pflat_map_on",
2417                                on_cluster: Some(Box::new(cluster)),
2418                                stream: false,
2419                            },
2420                            line: stage_line,
2421                        };
2422                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2423                    // Check if followed by a block (like `filter { }`, `sort { }`, `map { }`)
2424                    } else if matches!(self.peek(), Token::LBrace) {
2425                        // Parse as a block-taking builtin
2426                        self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_add(1);
2427                        let stage = self.parse_thread_stage_with_block(&func_name, stage_line)?;
2428                        self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_sub(1);
2429                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2430                    } else if matches!(self.peek(), Token::LParen) {
2431                        // Special handling for join(sep) and split(pattern) in thread context.
2432                        // These take the threaded list/string as their data argument, not as $_.
2433                        if func_name == "join" {
2434                            self.advance(); // consume `(`
2435                            let separator = self.parse_assign_expr()?;
2436                            self.expect(&Token::RParen)?;
2437                            let placeholder = self.pipe_placeholder_list(stage_line);
2438                            let stage = Expr {
2439                                kind: ExprKind::JoinExpr {
2440                                    separator: Box::new(separator),
2441                                    list: Box::new(placeholder),
2442                                },
2443                                line: stage_line,
2444                            };
2445                            result = self.pipe_forward_apply(result, stage, stage_line)?;
2446                        } else if func_name == "split" {
2447                            self.advance(); // consume `(`
2448                            let pattern = self.parse_assign_expr()?;
2449                            let limit = if self.eat(&Token::Comma) {
2450                                Some(Box::new(self.parse_assign_expr()?))
2451                            } else {
2452                                None
2453                            };
2454                            self.expect(&Token::RParen)?;
2455                            let placeholder = Expr {
2456                                kind: ExprKind::ScalarVar("_".to_string()),
2457                                line: stage_line,
2458                            };
2459                            let stage = Expr {
2460                                kind: ExprKind::SplitExpr {
2461                                    pattern: Box::new(pattern),
2462                                    string: Box::new(placeholder),
2463                                    limit,
2464                                },
2465                                line: stage_line,
2466                            };
2467                            result = self.pipe_forward_apply(result, stage, stage_line)?;
2468                        } else {
2469                            // `name($_-bearing-args)` — parse explicit args, require at
2470                            // least one `$_` placeholder, then wrap as a `>{...}` block
2471                            // so the threaded value binds to `$_` at any position.
2472                            // Examples:
2473                            //   t 10 add2($_, 5) p      → add2(10, 5)
2474                            //   t 10 sub2(20, $_) p     → sub2(20, 10)
2475                            //   t 10 add3($_, 5, 10) p  → add3(10, 5, 10)
2476                            // To pass the threaded value as a sole arg, use bare form:
2477                            //   t 10 add2 p   (not `add2()`)
2478                            self.advance(); // consume `(`
2479                            let mut call_args = Vec::new();
2480                            while !matches!(self.peek(), Token::RParen | Token::Eof) {
2481                                call_args.push(self.parse_assign_expr()?);
2482                                if !self.eat(&Token::Comma) {
2483                                    break;
2484                                }
2485                            }
2486                            self.expect(&Token::RParen)?;
2487                            // If no `$_` placeholder, auto-inject threaded value.
2488                            // Thread-first: `t data to_file("/tmp/o.html")` → `to_file($_, "/tmp/o.html")`
2489                            // Thread-last: `->> data to_file("/tmp/o.html")` → `to_file("/tmp/o.html", $_)`
2490                            if !call_args.iter().any(Self::expr_contains_topic_var) {
2491                                let topic = Expr {
2492                                    kind: ExprKind::ScalarVar("_".to_string()),
2493                                    line: stage_line,
2494                                };
2495                                if self.thread_last_mode {
2496                                    call_args.push(topic);
2497                                } else {
2498                                    call_args.insert(0, topic);
2499                                }
2500                            }
2501                            let call_expr = Expr {
2502                                kind: ExprKind::FuncCall {
2503                                    name: func_name.clone(),
2504                                    args: call_args,
2505                                },
2506                                line: stage_line,
2507                            };
2508                            let code_ref = Expr {
2509                                kind: ExprKind::CodeRef {
2510                                    params: vec![],
2511                                    body: vec![Statement {
2512                                        label: None,
2513                                        kind: StmtKind::Expression(call_expr),
2514                                        line: stage_line,
2515                                    }],
2516                                },
2517                                line: stage_line,
2518                            };
2519                            result = self.pipe_forward_apply(result, code_ref, stage_line)?;
2520                        }
2521                    } else {
2522                        // Bare function name — handle unary builtins specially
2523                        result = self.thread_apply_bare_func(&func_name, result, stage_line)?;
2524                    }
2525                }
2526                // `/pattern/flags` — grep filter (desugar to `grep { /pattern/flags }`)
2527                Token::Regex(ref pattern, ref flags, delim) => {
2528                    let pattern = pattern.clone();
2529                    let flags = flags.clone();
2530                    self.advance();
2531                    result =
2532                        self.thread_regex_grep_stage(result, pattern, flags, delim, stage_line);
2533                }
2534                // Handle `/` that was lexed as Slash (division) because it followed a term.
2535                // In thread stage context, `/pattern/` should be a regex filter.
2536                Token::Slash => {
2537                    self.advance(); // consume opening /
2538
2539                    // Special case: if next token is Ident("m") or similar followed by Regex,
2540                    // the lexer interpreted `/m/` as `/ m/pattern/` where `m/` started a new regex.
2541                    // We need to handle this: the pattern is just "m" (or whatever the ident is).
2542                    if let Token::Ident(ref ident_s) = self.peek().clone() {
2543                        if matches!(ident_s.as_str(), "m" | "s" | "tr" | "y" | "qr")
2544                            && matches!(self.peek_at(1), Token::Regex(..))
2545                        {
2546                            // The `m` (or s/tr/y/qr) is our pattern, the Regex token was misparsed
2547                            self.advance(); // consume the ident
2548                                            // The Token::Regex after it was a misparsed `m/...` - we need to
2549                                            // extract what would have been the closing `/` situation.
2550                                            // Actually, the lexer consumed everything. Let's just use the ident
2551                                            // as the pattern and expect a closing slash.
2552                            if let Token::Regex(ref misparsed_pattern, ref misparsed_flags, _) =
2553                                self.peek().clone()
2554                            {
2555                                // The misparsed regex ate our closing `/`.
2556                                // For `/m/`, lexer saw `m/` and parsed until next `/`, finding nothing or wrong content.
2557                                // Actually for `/m/ less`, after Slash, lexer sees `m`, then `/`,
2558                                // interprets as m// regex start, reads until next `/` (none) -> error.
2559                                // So we shouldn't reach here if there was an error.
2560                                // But if lexer succeeded parsing `m/ less/` as regex, we'd have wrong pattern.
2561                                // This is getting complicated. Let me try a different approach.
2562                                // Just consume the Regex token and issue a warning? No, let's reconstruct.
2563                                // Skip for now and fall through to manual parsing.
2564                                let _ = (misparsed_pattern, misparsed_flags);
2565                            }
2566                        }
2567                    }
2568
2569                    // Manually parse the regex pattern from tokens until we hit another Slash
2570                    let mut pattern = String::new();
2571                    loop {
2572                        match self.peek().clone() {
2573                            Token::Slash => {
2574                                self.advance(); // consume closing /
2575                                break;
2576                            }
2577                            Token::Eof | Token::Semicolon | Token::Newline => {
2578                                return Err(self
2579                                    .syntax_err("Unterminated regex in thread stage", stage_line));
2580                            }
2581                            // Handle case where lexer misparsed m/pattern/ as Ident("m") + Regex
2582                            Token::Regex(ref inner_pattern, ref inner_flags, delim) => {
2583                                // This means `/m/` was lexed as Slash, then `m/` started a regex.
2584                                // The Regex token contains whatever was between the inner `m/` and closing `/`.
2585                                // For `/m/ less`, lexer would fail earlier. For `/m/i`, it might work weirdly.
2586                                // The safest: if we see a Regex token here and pattern is empty or just "m"/"s"/etc,
2587                                // treat the previous ident as the whole pattern and this Regex as misparsed.
2588                                // Actually, let's just prepend the ident we may have seen and use empty pattern.
2589                                // This is a lexer bug workaround.
2590                                if pattern.is_empty()
2591                                    || matches!(pattern.as_str(), "m" | "s" | "tr" | "y" | "qr")
2592                                {
2593                                    // The whole thing was probably `/X/` where X is m/s/tr/y/qr
2594                                    // and lexer misparsed. The Regex token is garbage.
2595                                    // Just use the ident as pattern and ignore this Regex.
2596                                    // But we already advanced past the ident...
2597                                    // This is messy. Let me try a cleaner approach.
2598                                    let _ = (inner_pattern, inner_flags, delim);
2599                                }
2600                                // For now, error out - this case is too complex
2601                                return Err(self.syntax_err(
2602                                    "Complex regex in thread stage - use m/pattern/ syntax instead",
2603                                    stage_line,
2604                                ));
2605                            }
2606                            Token::Ident(ref s) => {
2607                                pattern.push_str(s);
2608                                self.advance();
2609                            }
2610                            Token::Integer(n) => {
2611                                pattern.push_str(&n.to_string());
2612                                self.advance();
2613                            }
2614                            Token::ScalarVar(ref v) => {
2615                                pattern.push('$');
2616                                pattern.push_str(v);
2617                                self.advance();
2618                            }
2619                            Token::Dot => {
2620                                pattern.push('.');
2621                                self.advance();
2622                            }
2623                            Token::Star => {
2624                                pattern.push('*');
2625                                self.advance();
2626                            }
2627                            Token::Plus => {
2628                                pattern.push('+');
2629                                self.advance();
2630                            }
2631                            Token::Question => {
2632                                pattern.push('?');
2633                                self.advance();
2634                            }
2635                            Token::LParen => {
2636                                pattern.push('(');
2637                                self.advance();
2638                            }
2639                            Token::RParen => {
2640                                pattern.push(')');
2641                                self.advance();
2642                            }
2643                            Token::LBracket => {
2644                                pattern.push('[');
2645                                self.advance();
2646                            }
2647                            Token::RBracket => {
2648                                pattern.push(']');
2649                                self.advance();
2650                            }
2651                            Token::Backslash => {
2652                                pattern.push('\\');
2653                                self.advance();
2654                            }
2655                            Token::BitOr => {
2656                                pattern.push('|');
2657                                self.advance();
2658                            }
2659                            Token::Power => {
2660                                pattern.push_str("**");
2661                                self.advance();
2662                            }
2663                            Token::BitXor => {
2664                                pattern.push('^');
2665                                self.advance();
2666                            }
2667                            Token::Minus => {
2668                                pattern.push('-');
2669                                self.advance();
2670                            }
2671                            _ => {
2672                                return Err(self.syntax_err(
2673                                    format!("Unexpected token in regex pattern: {:?}", self.peek()),
2674                                    stage_line,
2675                                ));
2676                            }
2677                        }
2678                    }
2679                    // Parse optional flags (sequence of letters after closing /)
2680                    // Be careful: single letters like 'e' could be regex flags OR thread
2681                    // stages like `fore`/`e`. If followed by `{`, it's a stage, not a flag.
2682                    let mut flags = String::new();
2683                    if let Token::Ident(ref s) = self.peek().clone() {
2684                        let is_flag_only =
2685                            s.chars().all(|c| "gimsxecor".contains(c)) && s.len() <= 6;
2686                        let followed_by_brace = matches!(self.peek_at(1), Token::LBrace);
2687                        if is_flag_only && !followed_by_brace {
2688                            flags.push_str(s);
2689                            self.advance();
2690                        }
2691                    }
2692                    result = self.thread_regex_grep_stage(result, pattern, flags, '/', stage_line);
2693                }
2694                tok => {
2695                    return Err(self.syntax_err(
2696                        format!(
2697                            "thread: expected stage (ident, fn {{}}, s///, tr///, or /re/), got {:?}",
2698                            tok
2699                        ),
2700                        stage_line,
2701                    ));
2702                }
2703            };
2704            last_stage_end_line = self.prev_line();
2705            // Parallel mode: each iteration of the loop has produced a
2706            // stage expression where `$_` is the input. Push it into the
2707            // collector and reset `result` to `$_` so the next stage
2708            // parses against a fresh topic.
2709            if let Some(stages) = parallel_collector.as_mut() {
2710                let stage_body = std::mem::replace(
2711                    &mut result,
2712                    Expr {
2713                        kind: ExprKind::ScalarVar("_".into()),
2714                        line: stage_line,
2715                    },
2716                );
2717                stages.push(stage_body);
2718            }
2719        }
2720
2721        // Restore thread-last mode
2722        self.thread_last_mode = saved_thread_last;
2723
2724        // Parallel mode: lower to `_thread_par_run(source_expr, [stage_closures], thread_last)`.
2725        // The runtime treats the source value as a list-of-items and feeds
2726        // each item into stage 1 via a bounded channel. Each stage runs in
2727        // its own worker; stages are wrapped as `fn { body }` closures so
2728        // the runtime sets `$_` to the current item before invoking.
2729        if let Some(stages) = parallel_collector {
2730            let source_expr = source_for_par.unwrap_or(result);
2731            if stages.is_empty() {
2732                return Err(self.syntax_err(
2733                    "~p> / ~p>> require at least one stage after the source",
2734                    _line,
2735                ));
2736            }
2737            // Wrap each stage body in `[ ... ]` (an ArrayRef) so list-returning
2738            // ops like `map`/`grep` propagate their full output instead of
2739            // collapsing to a scalar count. The runtime worker peels one
2740            // level of array-ref via `map_flatten_outputs(true)` so each
2741            // element flows downstream as its own item.
2742            let stage_closures: Vec<Expr> = stages
2743                .drain(..)
2744                .map(|body| {
2745                    let body_line = body.line;
2746                    let wrapped = Expr {
2747                        kind: ExprKind::ArrayRef(vec![body]),
2748                        line: body_line,
2749                    };
2750                    Expr {
2751                        kind: ExprKind::CodeRef {
2752                            params: vec![],
2753                            body: vec![Statement {
2754                                label: None,
2755                                kind: StmtKind::Expression(wrapped),
2756                                line: body_line,
2757                            }],
2758                        },
2759                        line: body_line,
2760                    }
2761                })
2762                .collect();
2763            let stages_arr = Expr {
2764                kind: ExprKind::ArrayRef(stage_closures),
2765                line: _line,
2766            };
2767            let thread_last_flag = Expr {
2768                kind: ExprKind::Integer(if thread_last { 1 } else { 0 }),
2769                line: _line,
2770            };
2771            // Argument order: stages, thread_last, source... — source
2772            // is LAST so its list expansion (`(1,2,3)`, `@a`, ranges)
2773            // lands in the variadic tail. Pre-fix the source was first
2774            // and any list source flattened across the slot, breaking
2775            // the `args.len() == 3` invariant in `_thread_par_run` and
2776            // hitting "expected 3 args" for `~s> (1,2,3) sum` etc.
2777            return Ok(Expr {
2778                kind: ExprKind::FuncCall {
2779                    name: "_thread_par_run".into(),
2780                    args: vec![stages_arr, thread_last_flag, source_expr],
2781                },
2782                line: _line,
2783            });
2784        }
2785
2786        if pipe_rhs_wrap {
2787            // Wrap as `fn { …stages threaded from $_[0]… }` so the outer
2788            // `pipe_forward_apply` can invoke it with `lhs` as the arg.
2789            let body_line = result.line;
2790            return Ok(Expr {
2791                kind: ExprKind::CodeRef {
2792                    params: vec![],
2793                    body: vec![Statement {
2794                        label: None,
2795                        kind: StmtKind::Expression(result),
2796                        line: body_line,
2797                    }],
2798                },
2799                line: _line,
2800            });
2801        }
2802        Ok(result)
2803    }
2804
2805    /// Build a grep filter stage from a regex pattern for the thread macro.
2806    fn thread_regex_grep_stage(
2807        &self,
2808        list: Expr,
2809        pattern: String,
2810        flags: String,
2811        delim: char,
2812        line: usize,
2813    ) -> Expr {
2814        let topic = Expr {
2815            kind: ExprKind::ScalarVar("_".to_string()),
2816            line,
2817        };
2818        let match_expr = Expr {
2819            kind: ExprKind::Match {
2820                expr: Box::new(topic),
2821                pattern,
2822                flags,
2823                scalar_g: false,
2824                delim,
2825            },
2826            line,
2827        };
2828        let block = vec![Statement {
2829            label: None,
2830            kind: StmtKind::Expression(match_expr),
2831            line,
2832        }];
2833        Expr {
2834            kind: ExprKind::GrepExpr {
2835                block,
2836                list: Box::new(list),
2837                keyword: crate::ast::GrepBuiltinKeyword::Grep,
2838            },
2839            line,
2840        }
2841    }
2842
2843    /// Check whether an expression contains a `$_` reference anywhere in its sub-tree.
2844    /// Used by the thread macro to validate `name(args)` call-stages: the threaded
2845    /// value is bound to `$_` via a wrapping CodeRef, so at least one `$_` placeholder
2846    /// must appear in the args, otherwise the threaded value is silently dropped.
2847    ///
2848    /// Implementation uses Rust's `Debug` to serialize the entire sub-tree once and
2849    /// scan for the canonical `ScalarVar("_")` representation. This avoids a
2850    /// per-variant walker that would need to be updated whenever new `ExprKind`
2851    /// variants are added (and would silently miss any it forgot to handle).
2852    /// Parse-time perf is non-critical and the AST is small at this scope.
2853    fn expr_contains_topic_var(e: &Expr) -> bool {
2854        format!("{:?}", e).contains("ScalarVar(\"_\")")
2855    }
2856
2857    /// Apply a bare function name in thread context, handling unary builtins specially.
2858    fn thread_apply_bare_func(&self, name: &str, arg: Expr, line: usize) -> PerlResult<Expr> {
2859        let kind = match name {
2860            // String functions
2861            "uc" => ExprKind::Uc(Box::new(arg)),
2862            "lc" => ExprKind::Lc(Box::new(arg)),
2863            "ucfirst" | "ufc" => ExprKind::Ucfirst(Box::new(arg)),
2864            "lcfirst" | "lfc" => ExprKind::Lcfirst(Box::new(arg)),
2865            "fc" => ExprKind::Fc(Box::new(arg)),
2866            "chomp" => ExprKind::Chomp(Box::new(arg)),
2867            "chop" => ExprKind::Chop(Box::new(arg)),
2868            "length" => ExprKind::Length(Box::new(arg)),
2869            "len" | "cnt" => ExprKind::FuncCall {
2870                name: "count".to_string(),
2871                args: vec![arg],
2872            },
2873            "quotemeta" | "qm" => ExprKind::FuncCall {
2874                name: "quotemeta".to_string(),
2875                args: vec![arg],
2876            },
2877            // Numeric functions
2878            "abs" => ExprKind::Abs(Box::new(arg)),
2879            "int" => ExprKind::Int(Box::new(arg)),
2880            "sqrt" | "sq" => ExprKind::Sqrt(Box::new(arg)),
2881            "sin" => ExprKind::Sin(Box::new(arg)),
2882            "cos" => ExprKind::Cos(Box::new(arg)),
2883            "exp" => ExprKind::Exp(Box::new(arg)),
2884            "log" => ExprKind::Log(Box::new(arg)),
2885            "hex" => ExprKind::Hex(Box::new(arg)),
2886            "oct" => ExprKind::Oct(Box::new(arg)),
2887            "chr" => ExprKind::Chr(Box::new(arg)),
2888            "ord" => ExprKind::Ord(Box::new(arg)),
2889            // Type/ref functions
2890            "defined" | "def" => ExprKind::Defined(Box::new(arg)),
2891            "ref" => ExprKind::Ref(Box::new(arg)),
2892            "scalar" => {
2893                if crate::no_interop_mode() {
2894                    return Err(self.syntax_err(
2895                        "stryke uses `len` (also `cnt` / `count`) instead of `scalar` (--no-interop)",
2896                        line,
2897                    ));
2898                }
2899                ExprKind::ScalarContext(Box::new(arg))
2900            }
2901            // Array/hash functions
2902            "keys" => ExprKind::Keys(Box::new(arg)),
2903            "values" => ExprKind::Values(Box::new(arg)),
2904            "each" => ExprKind::Each(Box::new(arg)),
2905            "pop" => ExprKind::Pop(Box::new(arg)),
2906            "shift" => ExprKind::Shift(Box::new(arg)),
2907            "reverse" => {
2908                if crate::no_interop_mode() {
2909                    return Err(self.syntax_err(
2910                        "stryke uses `rev` instead of `reverse` (--no-interop)",
2911                        line,
2912                    ));
2913                }
2914                ExprKind::ReverseExpr(Box::new(arg))
2915            }
2916            "reversed" | "rv" | "rev" => ExprKind::Rev(Box::new(arg)),
2917            "sort" | "so" => ExprKind::SortExpr {
2918                cmp: None,
2919                list: Box::new(arg),
2920            },
2921            "psort" => ExprKind::PSortExpr {
2922                cmp: None,
2923                list: Box::new(arg),
2924                progress: None,
2925            },
2926            "uniq" | "distinct" | "uq" => ExprKind::FuncCall {
2927                name: "uniq".to_string(),
2928                args: vec![arg],
2929            },
2930            "trim" | "tm" => ExprKind::FuncCall {
2931                name: "trim".to_string(),
2932                args: vec![arg],
2933            },
2934            "flatten" | "fl" => ExprKind::FuncCall {
2935                name: "flatten".to_string(),
2936                args: vec![arg],
2937            },
2938            "compact" | "cpt" => ExprKind::FuncCall {
2939                name: "compact".to_string(),
2940                args: vec![arg],
2941            },
2942            "shuffle" | "shuf" => ExprKind::FuncCall {
2943                name: "shuffle".to_string(),
2944                args: vec![arg],
2945            },
2946            "frequencies" | "freq" | "frq" => ExprKind::FuncCall {
2947                name: "frequencies".to_string(),
2948                args: vec![arg],
2949            },
2950            "pfrequencies" | "pfreq" | "pfrq" => ExprKind::FuncCall {
2951                name: "pfrequencies".to_string(),
2952                args: vec![arg],
2953            },
2954            "dedup" | "dup" => ExprKind::FuncCall {
2955                name: "dedup".to_string(),
2956                args: vec![arg],
2957            },
2958            "enumerate" | "en" => ExprKind::FuncCall {
2959                name: "enumerate".to_string(),
2960                args: vec![arg],
2961            },
2962            "lines" | "ln" => ExprKind::FuncCall {
2963                name: "lines".to_string(),
2964                args: vec![arg],
2965            },
2966            "words" | "wd" => ExprKind::FuncCall {
2967                name: "words".to_string(),
2968                args: vec![arg],
2969            },
2970            "chars" | "ch" => ExprKind::FuncCall {
2971                name: "chars".to_string(),
2972                args: vec![arg],
2973            },
2974            "digits" | "dg" => ExprKind::FuncCall {
2975                name: "digits".to_string(),
2976                args: vec![arg],
2977            },
2978            "letters" | "lts" => ExprKind::FuncCall {
2979                name: "letters".to_string(),
2980                args: vec![arg],
2981            },
2982            "letters_uc" => ExprKind::FuncCall {
2983                name: "letters_uc".to_string(),
2984                args: vec![arg],
2985            },
2986            "letters_lc" => ExprKind::FuncCall {
2987                name: "letters_lc".to_string(),
2988                args: vec![arg],
2989            },
2990            "punctuation" | "punct" => ExprKind::FuncCall {
2991                name: "punctuation".to_string(),
2992                args: vec![arg],
2993            },
2994            "sentences" | "sents" => ExprKind::FuncCall {
2995                name: "sentences".to_string(),
2996                args: vec![arg],
2997            },
2998            "paragraphs" | "paras" => ExprKind::FuncCall {
2999                name: "paragraphs".to_string(),
3000                args: vec![arg],
3001            },
3002            "sections" | "sects" => ExprKind::FuncCall {
3003                name: "sections".to_string(),
3004                args: vec![arg],
3005            },
3006            "numbers" | "nums" => ExprKind::FuncCall {
3007                name: "numbers".to_string(),
3008                args: vec![arg],
3009            },
3010            "graphemes" | "grs" => ExprKind::FuncCall {
3011                name: "graphemes".to_string(),
3012                args: vec![arg],
3013            },
3014            "columns" | "cols" => ExprKind::FuncCall {
3015                name: "columns".to_string(),
3016                args: vec![arg],
3017            },
3018            // File functions
3019            "slurp" | "sl" => ExprKind::Slurp(Box::new(arg)),
3020            "chdir" => ExprKind::Chdir(Box::new(arg)),
3021            "stat" => ExprKind::Stat(Box::new(arg)),
3022            "lstat" => ExprKind::Lstat(Box::new(arg)),
3023            "readlink" => ExprKind::Readlink(Box::new(arg)),
3024            "readdir" => ExprKind::Readdir(Box::new(arg)),
3025            "close" => ExprKind::Close(Box::new(arg)),
3026            "basename" | "bn" => ExprKind::FuncCall {
3027                name: "basename".to_string(),
3028                args: vec![arg],
3029            },
3030            "dirname" | "dn" => ExprKind::FuncCall {
3031                name: "dirname".to_string(),
3032                args: vec![arg],
3033            },
3034            "realpath" | "rp" => ExprKind::FuncCall {
3035                name: "realpath".to_string(),
3036                args: vec![arg],
3037            },
3038            "which" | "wh" => ExprKind::FuncCall {
3039                name: "which".to_string(),
3040                args: vec![arg],
3041            },
3042            // Other
3043            "eval" => ExprKind::Eval(Box::new(arg)),
3044            "require" => ExprKind::Require(Box::new(arg)),
3045            "study" => ExprKind::Study(Box::new(arg)),
3046            // Case conversion
3047            "snake_case" | "sc" => ExprKind::FuncCall {
3048                name: "snake_case".to_string(),
3049                args: vec![arg],
3050            },
3051            "camel_case" | "cc" => ExprKind::FuncCall {
3052                name: "camel_case".to_string(),
3053                args: vec![arg],
3054            },
3055            "kebab_case" | "kc" => ExprKind::FuncCall {
3056                name: "kebab_case".to_string(),
3057                args: vec![arg],
3058            },
3059            // Serialization
3060            "to_json" | "tj" => ExprKind::FuncCall {
3061                name: "to_json".to_string(),
3062                args: vec![arg],
3063            },
3064            "to_yaml" | "ty" => ExprKind::FuncCall {
3065                name: "to_yaml".to_string(),
3066                args: vec![arg],
3067            },
3068            "to_toml" | "tt" => ExprKind::FuncCall {
3069                name: "to_toml".to_string(),
3070                args: vec![arg],
3071            },
3072            "to_csv" | "tc" => ExprKind::FuncCall {
3073                name: "to_csv".to_string(),
3074                args: vec![arg],
3075            },
3076            "to_xml" | "tx" => ExprKind::FuncCall {
3077                name: "to_xml".to_string(),
3078                args: vec![arg],
3079            },
3080            "to_html" | "th" => ExprKind::FuncCall {
3081                name: "to_html".to_string(),
3082                args: vec![arg],
3083            },
3084            "to_markdown" | "to_md" | "tmd" => ExprKind::FuncCall {
3085                name: "to_markdown".to_string(),
3086                args: vec![arg],
3087            },
3088            "xopen" | "xo" => ExprKind::FuncCall {
3089                name: "xopen".to_string(),
3090                args: vec![arg],
3091            },
3092            "clip" | "clipboard" | "pbcopy" => ExprKind::FuncCall {
3093                name: "clip".to_string(),
3094                args: vec![arg],
3095            },
3096            "to_table" | "table" | "tbl" => ExprKind::FuncCall {
3097                name: "to_table".to_string(),
3098                args: vec![arg],
3099            },
3100            "sparkline" | "spark" => ExprKind::FuncCall {
3101                name: "sparkline".to_string(),
3102                args: vec![arg],
3103            },
3104            "bar_chart" | "bars" => ExprKind::FuncCall {
3105                name: "bar_chart".to_string(),
3106                args: vec![arg],
3107            },
3108            "flame" | "flamechart" => ExprKind::FuncCall {
3109                name: "flame".to_string(),
3110                args: vec![arg],
3111            },
3112            "ddump" | "dd" => ExprKind::FuncCall {
3113                name: "ddump".to_string(),
3114                args: vec![arg],
3115            },
3116            "say" => {
3117                if crate::no_interop_mode() {
3118                    return Err(
3119                        self.syntax_err("stryke uses `p` instead of `say` (--no-interop)", line)
3120                    );
3121                }
3122                ExprKind::Say {
3123                    handle: None,
3124                    args: vec![arg],
3125                }
3126            }
3127            "p" => ExprKind::Say {
3128                handle: None,
3129                args: vec![arg],
3130            },
3131            "print" => ExprKind::Print {
3132                handle: None,
3133                args: vec![arg],
3134            },
3135            "warn" => ExprKind::Warn(vec![arg]),
3136            "die" => ExprKind::Die(vec![arg]),
3137            "stringify" | "str" => ExprKind::FuncCall {
3138                name: "stringify".to_string(),
3139                args: vec![arg],
3140            },
3141            "json_decode" | "jd" => ExprKind::FuncCall {
3142                name: "json_decode".to_string(),
3143                args: vec![arg],
3144            },
3145            "yaml_decode" | "yd" => ExprKind::FuncCall {
3146                name: "yaml_decode".to_string(),
3147                args: vec![arg],
3148            },
3149            "toml_decode" | "td" => ExprKind::FuncCall {
3150                name: "toml_decode".to_string(),
3151                args: vec![arg],
3152            },
3153            "xml_decode" | "xd" => ExprKind::FuncCall {
3154                name: "xml_decode".to_string(),
3155                args: vec![arg],
3156            },
3157            "json_encode" | "je" => ExprKind::FuncCall {
3158                name: "json_encode".to_string(),
3159                args: vec![arg],
3160            },
3161            "yaml_encode" | "ye" => ExprKind::FuncCall {
3162                name: "yaml_encode".to_string(),
3163                args: vec![arg],
3164            },
3165            "toml_encode" | "te" => ExprKind::FuncCall {
3166                name: "toml_encode".to_string(),
3167                args: vec![arg],
3168            },
3169            "xml_encode" | "xe" => ExprKind::FuncCall {
3170                name: "xml_encode".to_string(),
3171                args: vec![arg],
3172            },
3173            // Encoding
3174            "base64_encode" | "b64e" => ExprKind::FuncCall {
3175                name: "base64_encode".to_string(),
3176                args: vec![arg],
3177            },
3178            "base64_decode" | "b64d" => ExprKind::FuncCall {
3179                name: "base64_decode".to_string(),
3180                args: vec![arg],
3181            },
3182            "hex_encode" | "hxe" => ExprKind::FuncCall {
3183                name: "hex_encode".to_string(),
3184                args: vec![arg],
3185            },
3186            "hex_decode" | "hxd" => ExprKind::FuncCall {
3187                name: "hex_decode".to_string(),
3188                args: vec![arg],
3189            },
3190            "url_encode" | "uri_escape" | "ue" => ExprKind::FuncCall {
3191                name: "url_encode".to_string(),
3192                args: vec![arg],
3193            },
3194            "url_decode" | "uri_unescape" | "ud" => ExprKind::FuncCall {
3195                name: "url_decode".to_string(),
3196                args: vec![arg],
3197            },
3198            "gzip" | "gz" => ExprKind::FuncCall {
3199                name: "gzip".to_string(),
3200                args: vec![arg],
3201            },
3202            "gunzip" | "ugz" => ExprKind::FuncCall {
3203                name: "gunzip".to_string(),
3204                args: vec![arg],
3205            },
3206            "zstd" | "zst" => ExprKind::FuncCall {
3207                name: "zstd".to_string(),
3208                args: vec![arg],
3209            },
3210            "zstd_decode" | "uzst" => ExprKind::FuncCall {
3211                name: "zstd_decode".to_string(),
3212                args: vec![arg],
3213            },
3214            // Crypto
3215            "sha256" | "s256" => ExprKind::FuncCall {
3216                name: "sha256".to_string(),
3217                args: vec![arg],
3218            },
3219            "sha1" | "s1" => ExprKind::FuncCall {
3220                name: "sha1".to_string(),
3221                args: vec![arg],
3222            },
3223            "md5" | "m5" => ExprKind::FuncCall {
3224                name: "md5".to_string(),
3225                args: vec![arg],
3226            },
3227            "uuid" | "uid" => ExprKind::FuncCall {
3228                name: "uuid".to_string(),
3229                args: vec![arg],
3230            },
3231            // Datetime
3232            "datetime_utc" | "utc" => ExprKind::FuncCall {
3233                name: "datetime_utc".to_string(),
3234                args: vec![arg],
3235            },
3236            // Bare `e` / `fore` / `ep` in thread context: foreach element, say it.
3237            // `t @list e` == `@list |> e p` == `@list |> ep` == foreach (@list) { say }
3238            "e" | "fore" | "ep" => ExprKind::ForEachExpr {
3239                block: vec![Statement {
3240                    label: None,
3241                    kind: StmtKind::Expression(Expr {
3242                        kind: ExprKind::Say {
3243                            handle: None,
3244                            args: vec![Expr {
3245                                kind: ExprKind::ScalarVar("_".into()),
3246                                line,
3247                            }],
3248                        },
3249                        line,
3250                    }),
3251                    line,
3252                }],
3253                list: Box::new(arg),
3254            },
3255            // Default: generic function call
3256            _ => ExprKind::FuncCall {
3257                name: name.to_string(),
3258                args: vec![arg],
3259            },
3260        };
3261        Ok(Expr { kind, line })
3262    }
3263
3264    /// Parse a thread stage that has a block: `map { }`, `filter { }`, `sort { }`, etc.
3265    /// In thread context, we only parse the block - the list comes from the piped result.
3266    fn parse_thread_stage_with_block(&mut self, name: &str, line: usize) -> PerlResult<Expr> {
3267        let block = self.parse_block()?;
3268        // Use a placeholder for the list - pipe_forward_apply will replace it
3269        let placeholder = self.pipe_placeholder_list(line);
3270
3271        match name {
3272            "map" | "flat_map" | "maps" | "flat_maps" => {
3273                let flatten_array_refs = matches!(name, "flat_map" | "flat_maps");
3274                let stream = matches!(name, "maps" | "flat_maps");
3275                Ok(Expr {
3276                    kind: ExprKind::MapExpr {
3277                        block,
3278                        list: Box::new(placeholder),
3279                        flatten_array_refs,
3280                        stream,
3281                    },
3282                    line,
3283                })
3284            }
3285            "grep" | "greps" | "filter" | "fi" | "find_all" | "gr" => {
3286                let keyword = match name {
3287                    "grep" | "gr" => crate::ast::GrepBuiltinKeyword::Grep,
3288                    "greps" => crate::ast::GrepBuiltinKeyword::Greps,
3289                    "filter" | "fi" => crate::ast::GrepBuiltinKeyword::Filter,
3290                    "find_all" => crate::ast::GrepBuiltinKeyword::FindAll,
3291                    _ => unreachable!(),
3292                };
3293                Ok(Expr {
3294                    kind: ExprKind::GrepExpr {
3295                        block,
3296                        list: Box::new(placeholder),
3297                        keyword,
3298                    },
3299                    line,
3300                })
3301            }
3302            "sort" | "so" => Ok(Expr {
3303                kind: ExprKind::SortExpr {
3304                    cmp: Some(SortComparator::Block(block)),
3305                    list: Box::new(placeholder),
3306                },
3307                line,
3308            }),
3309            "reduce" | "rd" => Ok(Expr {
3310                kind: ExprKind::ReduceExpr {
3311                    block,
3312                    list: Box::new(placeholder),
3313                },
3314                line,
3315            }),
3316            "fore" | "e" | "ep" => Ok(Expr {
3317                kind: ExprKind::ForEachExpr {
3318                    block,
3319                    list: Box::new(placeholder),
3320                },
3321                line,
3322            }),
3323            "par" => Ok(Expr {
3324                kind: ExprKind::ParExpr {
3325                    block,
3326                    list: Box::new(placeholder),
3327                },
3328                line,
3329            }),
3330            "pmap" | "pflat_map" | "pmaps" | "pflat_maps" => Ok(Expr {
3331                kind: ExprKind::PMapExpr {
3332                    block,
3333                    list: Box::new(placeholder),
3334                    progress: None,
3335                    flat_outputs: name == "pflat_map" || name == "pflat_maps",
3336                    on_cluster: None,
3337                    stream: name == "pmaps" || name == "pflat_maps",
3338                },
3339                line,
3340            }),
3341            "pgrep" | "pgreps" => Ok(Expr {
3342                kind: ExprKind::PGrepExpr {
3343                    block,
3344                    list: Box::new(placeholder),
3345                    progress: None,
3346                    stream: name == "pgreps",
3347                },
3348                line,
3349            }),
3350            "pfor" => Ok(Expr {
3351                kind: ExprKind::PForExpr {
3352                    block,
3353                    list: Box::new(placeholder),
3354                    progress: None,
3355                },
3356                line,
3357            }),
3358            "preduce" => Ok(Expr {
3359                kind: ExprKind::PReduceExpr {
3360                    block,
3361                    list: Box::new(placeholder),
3362                    progress: None,
3363                },
3364                line,
3365            }),
3366            "pcache" => Ok(Expr {
3367                kind: ExprKind::PcacheExpr {
3368                    block,
3369                    list: Box::new(placeholder),
3370                    progress: None,
3371                },
3372                line,
3373            }),
3374            "psort" => Ok(Expr {
3375                kind: ExprKind::PSortExpr {
3376                    cmp: Some(block),
3377                    list: Box::new(placeholder),
3378                    progress: None,
3379                },
3380                line,
3381            }),
3382            _ => {
3383                // Generic: parse block and treat as FuncCall with code ref arg.
3384                // Block-then-list pipe builtins (`pfirst`, `any`, `take_while`, etc.)
3385                // need the threaded list slot pre-allocated at args[1] so
3386                // `pipe_forward_apply` can substitute the lhs there (parser.rs:5823).
3387                // For everything else, the generic pipe-forward arm prepends or
3388                // appends the lhs based on `thread_last_mode`.
3389                let code_ref = Expr {
3390                    kind: ExprKind::CodeRef {
3391                        params: vec![],
3392                        body: block,
3393                    },
3394                    line,
3395                };
3396                let args = if Self::is_block_then_list_pipe_builtin(name) {
3397                    vec![code_ref, placeholder]
3398                } else {
3399                    vec![code_ref]
3400                };
3401                Ok(Expr {
3402                    kind: ExprKind::FuncCall {
3403                        name: name.to_string(),
3404                        args,
3405                    },
3406                    line,
3407                })
3408            }
3409        }
3410    }
3411
3412    /// `tie %hash | tie @arr | tie $x , 'Class', ...args`
3413    fn parse_tie_stmt(&mut self) -> PerlResult<Statement> {
3414        let line = self.peek_line();
3415        self.advance(); // tie
3416                        // `tie my $x, Class` and `tie our $x, Class` — common Perl idiom.
3417                        // Desugar by emitting an implicit `my $x` (or `our $x`) declaration
3418                        // before the tie. The tie target then references the just-declared
3419                        // variable. Without this, `tie my $x, Class, ARGS` errors with
3420                        // "tie expects $scalar, @array, or %hash, got Ident(\"my\")".
3421        let mut implicit_decl: Option<Statement> = None;
3422        if let Token::Ident(kw) = self.peek().clone() {
3423            if matches!(kw.as_str(), "my" | "our") {
3424                let kw_line = self.peek_line();
3425                self.advance(); // my / our
3426                                // Read the variable being declared (must be Scalar/Array/Hash).
3427                let (decl_sigil, decl_name) = match self.peek().clone() {
3428                    Token::ScalarVar(s) => (Sigil::Scalar, s),
3429                    Token::ArrayVar(a) => (Sigil::Array, a),
3430                    Token::HashVar(h) => (Sigil::Hash, h),
3431                    tok => {
3432                        return Err(self.syntax_err(
3433                            format!("expected variable after `tie {}`, got {:?}", kw, tok),
3434                            self.peek_line(),
3435                        ));
3436                    }
3437                };
3438                let decls = vec![VarDecl {
3439                    sigil: decl_sigil,
3440                    name: decl_name.clone(),
3441                    initializer: None,
3442                    frozen: false,
3443                    type_annotation: None,
3444                }];
3445                implicit_decl = Some(Statement {
3446                    label: None,
3447                    kind: if kw == "my" {
3448                        StmtKind::My(decls)
3449                    } else {
3450                        StmtKind::Our(decls)
3451                    },
3452                    line: kw_line,
3453                });
3454                // Don't advance past the variable token here — fall through
3455                // to the existing match below so `target` is built from the
3456                // same token (the ScalarVar/ArrayVar/HashVar path will
3457                // advance and capture the name).
3458            }
3459        }
3460        let target = match self.peek().clone() {
3461            Token::HashVar(h) => {
3462                self.advance();
3463                TieTarget::Hash(h)
3464            }
3465            Token::ArrayVar(a) => {
3466                self.advance();
3467                TieTarget::Array(a)
3468            }
3469            Token::ScalarVar(s) => {
3470                self.advance();
3471                TieTarget::Scalar(s)
3472            }
3473            tok => {
3474                return Err(self.syntax_err(
3475                    format!("tie expects $scalar, @array, or %hash, got {:?}", tok),
3476                    self.peek_line(),
3477                ));
3478            }
3479        };
3480        self.expect(&Token::Comma)?;
3481        let class = self.parse_assign_expr()?;
3482        let mut args = Vec::new();
3483        while self.eat(&Token::Comma) {
3484            if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof) {
3485                break;
3486            }
3487            args.push(self.parse_assign_expr()?);
3488        }
3489        self.eat(&Token::Semicolon);
3490        let tie_stmt = Statement {
3491            label: None,
3492            kind: StmtKind::Tie {
3493                target,
3494                class,
3495                args,
3496            },
3497            line,
3498        };
3499        if let Some(decl) = implicit_decl {
3500            // Wrap the implicit `my $x` + tie in a `StmtGroup` so they live
3501            // in the same lexical block (the parser desugar is invisible to
3502            // callers; `StmtGroup` runs statements in order without a frame
3503            // push).
3504            Ok(Statement {
3505                label: None,
3506                kind: StmtKind::StmtGroup(vec![decl, tie_stmt]),
3507                line,
3508            })
3509        } else {
3510            Ok(tie_stmt)
3511        }
3512    }
3513
3514    /// `given (EXPR) { ... }`
3515    fn parse_given(&mut self) -> PerlResult<Statement> {
3516        let line = self.peek_line();
3517        self.advance();
3518        self.expect(&Token::LParen)?;
3519        let topic = self.parse_expression()?;
3520        self.expect(&Token::RParen)?;
3521        let body = self.parse_block()?;
3522        self.eat(&Token::Semicolon);
3523        Ok(Statement {
3524            label: None,
3525            kind: StmtKind::Given { topic, body },
3526            line,
3527        })
3528    }
3529
3530    /// `when (COND) { ... }` — only meaningful inside `given`
3531    fn parse_when_stmt(&mut self) -> PerlResult<Statement> {
3532        let line = self.peek_line();
3533        self.advance();
3534        self.expect(&Token::LParen)?;
3535        let cond = self.parse_expression()?;
3536        self.expect(&Token::RParen)?;
3537        let body = self.parse_block()?;
3538        self.eat(&Token::Semicolon);
3539        Ok(Statement {
3540            label: None,
3541            kind: StmtKind::When { cond, body },
3542            line,
3543        })
3544    }
3545
3546    /// `default { ... }` — only meaningful inside `given`
3547    fn parse_default_stmt(&mut self) -> PerlResult<Statement> {
3548        let line = self.peek_line();
3549        self.advance();
3550        let body = self.parse_block()?;
3551        self.eat(&Token::Semicolon);
3552        Ok(Statement {
3553            label: None,
3554            kind: StmtKind::DefaultCase { body },
3555            line,
3556        })
3557    }
3558
3559    /// `cond { EXPR => RESULT, ..., default => RESULT }`
3560    ///
3561    /// Desugars to an if/elsif/else chain at parse time.
3562    /// Each arm is `condition => { body }` or `condition => expr`.
3563    /// `default => ...` becomes the else branch.
3564    fn parse_cond_expr(&mut self, line: usize) -> PerlResult<Expr> {
3565        self.expect(&Token::LBrace)?;
3566
3567        let mut arms: Vec<(Expr, Block)> = Vec::new();
3568        let mut else_block: Option<Block> = None;
3569
3570        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
3571            let arm_line = self.peek_line();
3572
3573            // Check for `default =>`
3574            let is_default = matches!(self.peek(), Token::Ident(ref s) if s == "default")
3575                && matches!(self.peek_at(1), Token::FatArrow);
3576
3577            if is_default {
3578                self.advance(); // consume `default`
3579                self.advance(); // consume `=>`
3580                let body = if matches!(self.peek(), Token::LBrace) {
3581                    self.parse_block()?
3582                } else {
3583                    let expr = self.parse_assign_expr()?;
3584                    vec![Statement {
3585                        label: None,
3586                        kind: StmtKind::Expression(expr),
3587                        line: arm_line,
3588                    }]
3589                };
3590                else_block = Some(body);
3591                self.eat(&Token::Comma);
3592                break; // default must be last
3593            }
3594
3595            // Parse condition expression (stop before `=>`)
3596            let condition = self.parse_assign_expr()?;
3597            self.expect(&Token::FatArrow)?;
3598
3599            let body = if matches!(self.peek(), Token::LBrace) {
3600                self.parse_block()?
3601            } else {
3602                let expr = self.parse_assign_expr()?;
3603                vec![Statement {
3604                    label: None,
3605                    kind: StmtKind::Expression(expr),
3606                    line: arm_line,
3607                }]
3608            };
3609
3610            arms.push((condition, body));
3611            self.eat(&Token::Comma);
3612        }
3613
3614        self.expect(&Token::RBrace)?;
3615
3616        if arms.is_empty() {
3617            return Err(self.syntax_err("cond requires at least one condition arm", line));
3618        }
3619
3620        // Build if/elsif/else chain from the arms.
3621        let (first_cond, first_body) = arms.remove(0);
3622        let elsifs: Vec<(Expr, Block)> = arms;
3623
3624        // Wrap in a do-block so `cond { ... }` is an expression.
3625        let if_stmt = Statement {
3626            label: None,
3627            kind: StmtKind::If {
3628                condition: first_cond,
3629                body: first_body,
3630                elsifs,
3631                else_block,
3632            },
3633            line,
3634        };
3635        let inner = Expr {
3636            kind: ExprKind::CodeRef {
3637                params: vec![],
3638                body: vec![if_stmt],
3639            },
3640            line,
3641        };
3642        Ok(Expr {
3643            kind: ExprKind::Do(Box::new(inner)),
3644            line,
3645        })
3646    }
3647
3648    /// `match (EXPR) { PATTERN => EXPR, ... }`
3649    fn parse_algebraic_match_expr(&mut self, line: usize) -> PerlResult<Expr> {
3650        self.expect(&Token::LParen)?;
3651        let subject = self.parse_expression()?;
3652        self.expect(&Token::RParen)?;
3653        self.expect(&Token::LBrace)?;
3654        let mut arms = Vec::new();
3655        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
3656            if self.eat(&Token::Semicolon) {
3657                continue;
3658            }
3659            let pattern = self.parse_match_pattern()?;
3660            let guard = if matches!(self.peek(), Token::Ident(ref s) if s == "if") {
3661                self.advance();
3662                // Use assign-level parsing so `=>` after the guard is not consumed as a comma/fat-comma
3663                // separator (see [`Self::parse_comma_expr`]).
3664                Some(Box::new(self.parse_assign_expr()?))
3665            } else {
3666                None
3667            };
3668            self.expect(&Token::FatArrow)?;
3669            // Use assign-level parsing so commas separate arms, not `List` elements.
3670            let body = self.parse_assign_expr()?;
3671            arms.push(MatchArm {
3672                pattern,
3673                guard,
3674                body,
3675            });
3676            self.eat(&Token::Comma);
3677        }
3678        self.expect(&Token::RBrace)?;
3679        Ok(Expr {
3680            kind: ExprKind::AlgebraicMatch {
3681                subject: Box::new(subject),
3682                arms,
3683            },
3684            line,
3685        })
3686    }
3687
3688    fn parse_match_pattern(&mut self) -> PerlResult<MatchPattern> {
3689        match self.peek().clone() {
3690            Token::Regex(pattern, flags, _delim) => {
3691                self.advance();
3692                Ok(MatchPattern::Regex { pattern, flags })
3693            }
3694            Token::Ident(ref s) if s == "_" => {
3695                self.advance();
3696                Ok(MatchPattern::Any)
3697            }
3698            Token::Ident(ref s) if s == "Some" => {
3699                self.advance();
3700                self.expect(&Token::LParen)?;
3701                let name = self.parse_scalar_var_name()?;
3702                self.expect(&Token::RParen)?;
3703                Ok(MatchPattern::OptionSome(name))
3704            }
3705            Token::LBracket => self.parse_match_array_pattern(),
3706            Token::LBrace => self.parse_match_hash_pattern(),
3707            Token::LParen => {
3708                self.advance();
3709                let e = self.parse_expression()?;
3710                self.expect(&Token::RParen)?;
3711                Ok(MatchPattern::Value(Box::new(e)))
3712            }
3713            _ => {
3714                let e = self.parse_assign_expr()?;
3715                Ok(MatchPattern::Value(Box::new(e)))
3716            }
3717        }
3718    }
3719
3720    /// Contents of `[ ... ]` for algebraic array patterns and `sub ($a, [ ... ])` signatures.
3721    fn parse_match_array_elems_until_rbracket(&mut self) -> PerlResult<Vec<MatchArrayElem>> {
3722        let mut elems = Vec::new();
3723        if self.eat(&Token::RBracket) {
3724            return Ok(vec![]);
3725        }
3726        loop {
3727            if matches!(self.peek(), Token::Star) {
3728                self.advance();
3729                elems.push(MatchArrayElem::Rest);
3730                self.eat(&Token::Comma);
3731                if !matches!(self.peek(), Token::RBracket) {
3732                    return Err(self.syntax_err(
3733                        "`*` must be the last element in an array match pattern",
3734                        self.peek_line(),
3735                    ));
3736                }
3737                self.expect(&Token::RBracket)?;
3738                return Ok(elems);
3739            }
3740            if let Token::ArrayVar(name) = self.peek().clone() {
3741                self.advance();
3742                elems.push(MatchArrayElem::RestBind(name));
3743                self.eat(&Token::Comma);
3744                if !matches!(self.peek(), Token::RBracket) {
3745                    return Err(self.syntax_err(
3746                        "`@name` rest bind must be the last element in an array match pattern",
3747                        self.peek_line(),
3748                    ));
3749                }
3750                self.expect(&Token::RBracket)?;
3751                return Ok(elems);
3752            }
3753            if let Token::ScalarVar(name) = self.peek().clone() {
3754                self.advance();
3755                elems.push(MatchArrayElem::CaptureScalar(name));
3756                if self.eat(&Token::Comma) {
3757                    if matches!(self.peek(), Token::RBracket) {
3758                        break;
3759                    }
3760                    continue;
3761                }
3762                break;
3763            }
3764            let e = self.parse_assign_expr()?;
3765            elems.push(MatchArrayElem::Expr(e));
3766            if self.eat(&Token::Comma) {
3767                if matches!(self.peek(), Token::RBracket) {
3768                    break;
3769                }
3770                continue;
3771            }
3772            break;
3773        }
3774        self.expect(&Token::RBracket)?;
3775        Ok(elems)
3776    }
3777
3778    fn parse_match_array_pattern(&mut self) -> PerlResult<MatchPattern> {
3779        self.expect(&Token::LBracket)?;
3780        let elems = self.parse_match_array_elems_until_rbracket()?;
3781        Ok(MatchPattern::Array(elems))
3782    }
3783
3784    fn parse_match_hash_pattern(&mut self) -> PerlResult<MatchPattern> {
3785        self.expect(&Token::LBrace)?;
3786        let mut pairs = Vec::new();
3787        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
3788            if self.eat(&Token::Semicolon) {
3789                continue;
3790            }
3791            let key = self.parse_assign_expr()?;
3792            self.expect(&Token::FatArrow)?;
3793            match self.advance().0 {
3794                Token::Ident(ref s) if s == "_" => {
3795                    pairs.push(MatchHashPair::KeyOnly { key });
3796                }
3797                Token::ScalarVar(name) => {
3798                    pairs.push(MatchHashPair::Capture { key, name });
3799                }
3800                tok => {
3801                    return Err(self.syntax_err(
3802                        format!(
3803                            "hash match pattern must bind with `=> $name` or `=> _`, got {:?}",
3804                            tok
3805                        ),
3806                        self.peek_line(),
3807                    ));
3808                }
3809            }
3810            self.eat(&Token::Comma);
3811        }
3812        self.expect(&Token::RBrace)?;
3813        Ok(MatchPattern::Hash(pairs))
3814    }
3815
3816    /// `eval_timeout SECS { ... }`
3817    fn parse_eval_timeout(&mut self) -> PerlResult<Statement> {
3818        let line = self.peek_line();
3819        self.advance();
3820        let timeout = self.parse_postfix()?;
3821        let body = self.parse_block_or_bareword_block_no_args()?;
3822        self.eat(&Token::Semicolon);
3823        Ok(Statement {
3824            label: None,
3825            kind: StmtKind::EvalTimeout { timeout, body },
3826            line,
3827        })
3828    }
3829
3830    fn mark_match_scalar_g_for_boolean_condition(cond: &mut Expr) {
3831        match &mut cond.kind {
3832            ExprKind::Match {
3833                flags, scalar_g, ..
3834            } if flags.contains('g') => {
3835                *scalar_g = true;
3836            }
3837            ExprKind::UnaryOp {
3838                op: UnaryOp::LogNot,
3839                expr,
3840            } => {
3841                if let ExprKind::Match {
3842                    flags, scalar_g, ..
3843                } = &mut expr.kind
3844                {
3845                    if flags.contains('g') {
3846                        *scalar_g = true;
3847                    }
3848                }
3849            }
3850            _ => {}
3851        }
3852    }
3853
3854    fn parse_if(&mut self) -> PerlResult<Statement> {
3855        let line = self.peek_line();
3856        self.advance(); // 'if'
3857        if matches!(self.peek(), Token::Ident(ref s) if s == "let") {
3858            if crate::compat_mode() {
3859                return Err(self.syntax_err(
3860                    "`if let` is a stryke extension (disabled by --compat)",
3861                    line,
3862                ));
3863            }
3864            return self.parse_if_let(line);
3865        }
3866        self.expect(&Token::LParen)?;
3867        let mut cond = self.parse_expression()?;
3868        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
3869        self.expect(&Token::RParen)?;
3870        let body = self.parse_block()?;
3871
3872        let mut elsifs = Vec::new();
3873        let mut else_block = None;
3874
3875        loop {
3876            if let Token::Ident(ref kw) = self.peek().clone() {
3877                if kw == "elsif" {
3878                    self.advance();
3879                    self.expect(&Token::LParen)?;
3880                    let mut c = self.parse_expression()?;
3881                    Self::mark_match_scalar_g_for_boolean_condition(&mut c);
3882                    self.expect(&Token::RParen)?;
3883                    let b = self.parse_block()?;
3884                    elsifs.push((c, b));
3885                    continue;
3886                }
3887                if kw == "else" {
3888                    self.advance();
3889                    else_block = Some(self.parse_block()?);
3890                }
3891            }
3892            break;
3893        }
3894
3895        Ok(Statement {
3896            label: None,
3897            kind: StmtKind::If {
3898                condition: cond,
3899                body,
3900                elsifs,
3901                else_block,
3902            },
3903            line,
3904        })
3905    }
3906
3907    /// `if let PAT = EXPR { ... } [ else { ... } ]` — desugars to [`ExprKind::AlgebraicMatch`].
3908    fn parse_if_let(&mut self, line: usize) -> PerlResult<Statement> {
3909        self.advance(); // `let`
3910        let pattern = self.parse_match_pattern()?;
3911        self.expect(&Token::Assign)?;
3912        // Use assign-level parsing so a following `{ ... }` is the `if let` body, not an anon hash.
3913        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_add(1);
3914        let rhs = self.parse_assign_expr();
3915        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_sub(1);
3916        let rhs = rhs?;
3917        let then_block = self.parse_block()?;
3918        let else_block_opt = match self.peek().clone() {
3919            Token::Ident(ref kw) if kw == "else" => {
3920                self.advance();
3921                Some(self.parse_block()?)
3922            }
3923            Token::Ident(ref kw) if kw == "elsif" => {
3924                return Err(self.syntax_err(
3925                    "`if let` does not support `elsif`; use `else { }` or a full `match`",
3926                    self.peek_line(),
3927                ));
3928            }
3929            _ => None,
3930        };
3931        let then_expr = Self::expr_do_anon_block(then_block, line);
3932        let else_expr = if let Some(eb) = else_block_opt {
3933            Self::expr_do_anon_block(eb, line)
3934        } else {
3935            Expr {
3936                kind: ExprKind::Undef,
3937                line,
3938            }
3939        };
3940        let arms = vec![
3941            MatchArm {
3942                pattern,
3943                guard: None,
3944                body: then_expr,
3945            },
3946            MatchArm {
3947                pattern: MatchPattern::Any,
3948                guard: None,
3949                body: else_expr,
3950            },
3951        ];
3952        Ok(Statement {
3953            label: None,
3954            kind: StmtKind::Expression(Expr {
3955                kind: ExprKind::AlgebraicMatch {
3956                    subject: Box::new(rhs),
3957                    arms,
3958                },
3959                line,
3960            }),
3961            line,
3962        })
3963    }
3964
3965    fn expr_do_anon_block(block: Block, outer_line: usize) -> Expr {
3966        let inner_line = block.first().map(|s| s.line).unwrap_or(outer_line);
3967        Expr {
3968            kind: ExprKind::Do(Box::new(Expr {
3969                kind: ExprKind::CodeRef {
3970                    params: vec![],
3971                    body: block,
3972                },
3973                line: inner_line,
3974            })),
3975            line: outer_line,
3976        }
3977    }
3978
3979    fn parse_unless(&mut self) -> PerlResult<Statement> {
3980        let line = self.peek_line();
3981        self.advance(); // 'unless'
3982        self.expect(&Token::LParen)?;
3983        let mut cond = self.parse_expression()?;
3984        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
3985        self.expect(&Token::RParen)?;
3986        let body = self.parse_block()?;
3987        let else_block = if let Token::Ident(ref kw) = self.peek().clone() {
3988            if kw == "else" {
3989                self.advance();
3990                Some(self.parse_block()?)
3991            } else {
3992                None
3993            }
3994        } else {
3995            None
3996        };
3997        Ok(Statement {
3998            label: None,
3999            kind: StmtKind::Unless {
4000                condition: cond,
4001                body,
4002                else_block,
4003            },
4004            line,
4005        })
4006    }
4007
4008    fn parse_while(&mut self) -> PerlResult<Statement> {
4009        let line = self.peek_line();
4010        self.advance(); // 'while'
4011        if matches!(self.peek(), Token::Ident(ref s) if s == "let") {
4012            if crate::compat_mode() {
4013                return Err(self.syntax_err(
4014                    "`while let` is a stryke extension (disabled by --compat)",
4015                    line,
4016                ));
4017            }
4018            return self.parse_while_let(line);
4019        }
4020        self.expect(&Token::LParen)?;
4021        let mut cond = self.parse_expression()?;
4022        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
4023        self.expect(&Token::RParen)?;
4024        let body = self.parse_block()?;
4025        let continue_block = self.parse_optional_continue_block()?;
4026        Ok(Statement {
4027            label: None,
4028            kind: StmtKind::While {
4029                condition: cond,
4030                body,
4031                label: None,
4032                continue_block,
4033            },
4034            line,
4035        })
4036    }
4037
4038    /// `while let PAT = EXPR { ... }` — desugars to a `match` that returns 0/1 plus `unless ($tmp) { last }`
4039    /// so bytecode does not run `last` inside a tree-assisted [`Op::AlgebraicMatch`] arm.
4040    fn parse_while_let(&mut self, line: usize) -> PerlResult<Statement> {
4041        self.advance(); // `let`
4042        let pattern = self.parse_match_pattern()?;
4043        self.expect(&Token::Assign)?;
4044        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_add(1);
4045        let rhs = self.parse_assign_expr();
4046        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_sub(1);
4047        let rhs = rhs?;
4048        let mut user_body = self.parse_block()?;
4049        let continue_block = self.parse_optional_continue_block()?;
4050        user_body.push(Statement::new(
4051            StmtKind::Expression(Expr {
4052                kind: ExprKind::Integer(1),
4053                line,
4054            }),
4055            line,
4056        ));
4057        let tmp = format!("__while_let_{}", self.alloc_desugar_tmp());
4058        let match_expr = Expr {
4059            kind: ExprKind::AlgebraicMatch {
4060                subject: Box::new(rhs),
4061                arms: vec![
4062                    MatchArm {
4063                        pattern,
4064                        guard: None,
4065                        body: Self::expr_do_anon_block(user_body, line),
4066                    },
4067                    MatchArm {
4068                        pattern: MatchPattern::Any,
4069                        guard: None,
4070                        body: Expr {
4071                            kind: ExprKind::Integer(0),
4072                            line,
4073                        },
4074                    },
4075                ],
4076            },
4077            line,
4078        };
4079        let my_stmt = Statement::new(
4080            StmtKind::My(vec![VarDecl {
4081                sigil: Sigil::Scalar,
4082                name: tmp.clone(),
4083                initializer: Some(match_expr),
4084                frozen: false,
4085                type_annotation: None,
4086            }]),
4087            line,
4088        );
4089        let unless_last = Statement::new(
4090            StmtKind::Unless {
4091                condition: Expr {
4092                    kind: ExprKind::ScalarVar(tmp),
4093                    line,
4094                },
4095                body: vec![Statement::new(StmtKind::Last(None), line)],
4096                else_block: None,
4097            },
4098            line,
4099        );
4100        Ok(Statement::new(
4101            StmtKind::While {
4102                condition: Expr {
4103                    kind: ExprKind::Integer(1),
4104                    line,
4105                },
4106                body: vec![my_stmt, unless_last],
4107                label: None,
4108                continue_block,
4109            },
4110            line,
4111        ))
4112    }
4113
4114    fn parse_until(&mut self) -> PerlResult<Statement> {
4115        let line = self.peek_line();
4116        self.advance(); // 'until'
4117        self.expect(&Token::LParen)?;
4118        let mut cond = self.parse_expression()?;
4119        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
4120        self.expect(&Token::RParen)?;
4121        let body = self.parse_block()?;
4122        let continue_block = self.parse_optional_continue_block()?;
4123        Ok(Statement {
4124            label: None,
4125            kind: StmtKind::Until {
4126                condition: cond,
4127                body,
4128                label: None,
4129                continue_block,
4130            },
4131            line,
4132        })
4133    }
4134
4135    /// `continue { ... }` after a loop body (optional).
4136    fn parse_optional_continue_block(&mut self) -> PerlResult<Option<Block>> {
4137        if let Token::Ident(ref kw) = self.peek().clone() {
4138            if kw == "continue" {
4139                self.advance();
4140                return Ok(Some(self.parse_block()?));
4141            }
4142        }
4143        Ok(None)
4144    }
4145
4146    fn parse_for_or_foreach(&mut self) -> PerlResult<Statement> {
4147        let line = self.peek_line();
4148        self.advance(); // 'for'
4149
4150        // Peek to determine if C-style for or foreach
4151        // C-style: for (init; cond; step)
4152        // foreach-style: for $var (list) or for (list)
4153        match self.peek() {
4154            Token::LParen => {
4155                // Check if next after ( is a semicolon or an assignment — C-style
4156                // Or if it's a list — foreach-style
4157                // Heuristic: if the token after ( is 'my' or '$' followed by
4158                // content that contains ';', it's C-style.
4159                let saved = self.pos;
4160                self.advance(); // consume (
4161                                // Look for semicolon at paren depth 0
4162                let mut depth = 1;
4163                let mut has_semi = false;
4164                let mut scan = self.pos;
4165                while scan < self.tokens.len() {
4166                    match &self.tokens[scan].0 {
4167                        Token::LParen => depth += 1,
4168                        Token::RParen => {
4169                            depth -= 1;
4170                            if depth == 0 {
4171                                break;
4172                            }
4173                        }
4174                        Token::Semicolon if depth == 1 => {
4175                            has_semi = true;
4176                            break;
4177                        }
4178                        _ => {}
4179                    }
4180                    scan += 1;
4181                }
4182                self.pos = saved;
4183
4184                if has_semi {
4185                    self.parse_c_style_for(line)
4186                } else {
4187                    // foreach without explicit var — uses $_
4188                    self.expect(&Token::LParen)?;
4189                    let list = self.parse_expression()?;
4190                    self.expect(&Token::RParen)?;
4191                    let body = self.parse_block()?;
4192                    let continue_block = self.parse_optional_continue_block()?;
4193                    Ok(Statement {
4194                        label: None,
4195                        kind: StmtKind::Foreach {
4196                            var: "_".to_string(),
4197                            list,
4198                            body,
4199                            label: None,
4200                            continue_block,
4201                        },
4202                        line,
4203                    })
4204                }
4205            }
4206            Token::Ident(ref kw) if kw == "my" => {
4207                self.advance(); // 'my'
4208                let var = self.parse_scalar_var_name()?;
4209                self.expect(&Token::LParen)?;
4210                let list = self.parse_expression()?;
4211                self.expect(&Token::RParen)?;
4212                let body = self.parse_block()?;
4213                let continue_block = self.parse_optional_continue_block()?;
4214                Ok(Statement {
4215                    label: None,
4216                    kind: StmtKind::Foreach {
4217                        var,
4218                        list,
4219                        body,
4220                        label: None,
4221                        continue_block,
4222                    },
4223                    line,
4224                })
4225            }
4226            Token::ScalarVar(_) => {
4227                let var = self.parse_scalar_var_name()?;
4228                self.expect(&Token::LParen)?;
4229                let list = self.parse_expression()?;
4230                self.expect(&Token::RParen)?;
4231                let body = self.parse_block()?;
4232                let continue_block = self.parse_optional_continue_block()?;
4233                Ok(Statement {
4234                    label: None,
4235                    kind: StmtKind::Foreach {
4236                        var,
4237                        list,
4238                        body,
4239                        label: None,
4240                        continue_block,
4241                    },
4242                    line,
4243                })
4244            }
4245            _ => self.parse_c_style_for(line),
4246        }
4247    }
4248
4249    fn parse_c_style_for(&mut self, line: usize) -> PerlResult<Statement> {
4250        self.expect(&Token::LParen)?;
4251        let init = if self.eat(&Token::Semicolon) {
4252            None
4253        } else {
4254            let s = self.parse_statement()?;
4255            self.eat(&Token::Semicolon);
4256            Some(Box::new(s))
4257        };
4258        let mut condition = if matches!(self.peek(), Token::Semicolon) {
4259            None
4260        } else {
4261            Some(self.parse_expression()?)
4262        };
4263        if let Some(ref mut c) = condition {
4264            Self::mark_match_scalar_g_for_boolean_condition(c);
4265        }
4266        self.expect(&Token::Semicolon)?;
4267        let step = if matches!(self.peek(), Token::RParen) {
4268            None
4269        } else {
4270            Some(self.parse_expression()?)
4271        };
4272        self.expect(&Token::RParen)?;
4273        let body = self.parse_block()?;
4274        let continue_block = self.parse_optional_continue_block()?;
4275        Ok(Statement {
4276            label: None,
4277            kind: StmtKind::For {
4278                init,
4279                condition,
4280                step,
4281                body,
4282                label: None,
4283                continue_block,
4284            },
4285            line,
4286        })
4287    }
4288
4289    fn parse_foreach(&mut self) -> PerlResult<Statement> {
4290        let line = self.peek_line();
4291        self.advance(); // 'foreach'
4292        let var = match self.peek() {
4293            Token::Ident(ref kw) if kw == "my" => {
4294                self.advance();
4295                self.parse_scalar_var_name()?
4296            }
4297            Token::ScalarVar(_) => self.parse_scalar_var_name()?,
4298            _ => "_".to_string(),
4299        };
4300        self.expect(&Token::LParen)?;
4301        let list = self.parse_expression()?;
4302        self.expect(&Token::RParen)?;
4303        let body = self.parse_block()?;
4304        let continue_block = self.parse_optional_continue_block()?;
4305        Ok(Statement {
4306            label: None,
4307            kind: StmtKind::Foreach {
4308                var,
4309                list,
4310                body,
4311                label: None,
4312                continue_block,
4313            },
4314            line,
4315        })
4316    }
4317
4318    fn parse_scalar_var_name(&mut self) -> PerlResult<String> {
4319        match self.advance() {
4320            (Token::ScalarVar(name), _) => Ok(name),
4321            (tok, line) => {
4322                Err(self.syntax_err(format!("Expected scalar variable, got {:?}", tok), line))
4323            }
4324        }
4325    }
4326
4327    /// After `(` was consumed: Perl5 prototype characters until `)` (or `$)` + `{`).
4328    fn parse_legacy_sub_prototype_tail(&mut self) -> PerlResult<String> {
4329        let mut s = String::new();
4330        loop {
4331            match self.peek().clone() {
4332                Token::RParen => {
4333                    self.advance();
4334                    break;
4335                }
4336                Token::Eof => {
4337                    return Err(self.syntax_err(
4338                        "Unterminated sub prototype (expected ')' before end of input)",
4339                        self.peek_line(),
4340                    ));
4341                }
4342                Token::ScalarVar(v) if v == ")" => {
4343                    // Lexer merges `$` + `)` into one token (`$)`). In `sub name ($) {`, the
4344                    // closing `)` of the prototype is not a separate `RParen` — next is `{`.
4345                    self.advance();
4346                    s.push('$');
4347                    if matches!(self.peek(), Token::LBrace) {
4348                        break;
4349                    }
4350                }
4351                Token::Ident(i) => {
4352                    let i = i.clone();
4353                    self.advance();
4354                    s.push_str(&i);
4355                }
4356                Token::Semicolon => {
4357                    self.advance();
4358                    s.push(';');
4359                }
4360                Token::LParen => {
4361                    self.advance();
4362                    s.push('(');
4363                }
4364                Token::LBracket => {
4365                    self.advance();
4366                    s.push('[');
4367                }
4368                Token::RBracket => {
4369                    self.advance();
4370                    s.push(']');
4371                }
4372                Token::Backslash => {
4373                    self.advance();
4374                    s.push('\\');
4375                }
4376                Token::Comma => {
4377                    self.advance();
4378                    s.push(',');
4379                }
4380                Token::ScalarVar(v) => {
4381                    let v = v.clone();
4382                    self.advance();
4383                    s.push('$');
4384                    s.push_str(&v);
4385                }
4386                Token::ArrayVar(v) => {
4387                    let v = v.clone();
4388                    self.advance();
4389                    s.push('@');
4390                    s.push_str(&v);
4391                }
4392                // Bare `@` / `%` in prototypes (e.g. Try::Tiny's `sub try (&;@)`).
4393                Token::ArrayAt => {
4394                    self.advance();
4395                    s.push('@');
4396                }
4397                Token::HashVar(v) => {
4398                    let v = v.clone();
4399                    self.advance();
4400                    s.push('%');
4401                    s.push_str(&v);
4402                }
4403                Token::HashPercent => {
4404                    self.advance();
4405                    s.push('%');
4406                }
4407                Token::Plus => {
4408                    self.advance();
4409                    s.push('+');
4410                }
4411                Token::Minus => {
4412                    self.advance();
4413                    s.push('-');
4414                }
4415                Token::BitAnd => {
4416                    self.advance();
4417                    s.push('&');
4418                }
4419                tok => {
4420                    return Err(self.syntax_err(
4421                        format!("Unexpected token in sub prototype: {:?}", tok),
4422                        self.peek_line(),
4423                    ));
4424                }
4425            }
4426        }
4427        Ok(s)
4428    }
4429
4430    fn sub_signature_list_starts_here(&self) -> bool {
4431        match self.peek() {
4432            Token::LBrace | Token::LBracket => true,
4433            Token::ScalarVar(name) if name != "$$" && name != ")" => true,
4434            Token::ArrayVar(_) | Token::HashVar(_) => true,
4435            _ => false,
4436        }
4437    }
4438
4439    fn parse_sub_signature_hash_key(&mut self) -> PerlResult<String> {
4440        let (tok, line) = self.advance();
4441        match tok {
4442            Token::Ident(i) => Ok(i),
4443            Token::SingleString(s) | Token::DoubleString(s) => Ok(s),
4444            tok => Err(self.syntax_err(
4445                format!(
4446                    "sub signature: expected hash key (identifier or string), got {:?}",
4447                    tok
4448                ),
4449                line,
4450            )),
4451        }
4452    }
4453
4454    fn parse_sub_signature_param_list(&mut self) -> PerlResult<Vec<SubSigParam>> {
4455        let mut params = Vec::new();
4456        loop {
4457            if matches!(self.peek(), Token::RParen) {
4458                break;
4459            }
4460            match self.peek().clone() {
4461                Token::ScalarVar(name) => {
4462                    if name == "$$" || name == ")" {
4463                        return Err(self.syntax_err(
4464                            format!(
4465                                "`{name}` cannot start a stryke sub signature (use legacy prototype `($$)` etc.)"
4466                            ),
4467                            self.peek_line(),
4468                        ));
4469                    }
4470                    self.advance();
4471                    let ty = if self.eat(&Token::Colon) {
4472                        match self.peek() {
4473                            Token::Ident(ref tname) => {
4474                                let tname = tname.clone();
4475                                self.advance();
4476                                Some(match tname.as_str() {
4477                                    "Int" => PerlTypeName::Int,
4478                                    "Str" => PerlTypeName::Str,
4479                                    "Float" => PerlTypeName::Float,
4480                                    "Bool" => PerlTypeName::Bool,
4481                                    "Array" => PerlTypeName::Array,
4482                                    "Hash" => PerlTypeName::Hash,
4483                                    "Ref" => PerlTypeName::Ref,
4484                                    "Any" => PerlTypeName::Any,
4485                                    _ => PerlTypeName::Struct(tname),
4486                                })
4487                            }
4488                            _ => {
4489                                return Err(self.syntax_err(
4490                                    "expected type name after `:` in sub signature",
4491                                    self.peek_line(),
4492                                ));
4493                            }
4494                        }
4495                    } else {
4496                        None
4497                    };
4498                    // Check for default value: `$x = expr`
4499                    let default = if self.eat(&Token::Assign) {
4500                        Some(Box::new(self.parse_ternary()?))
4501                    } else {
4502                        None
4503                    };
4504                    params.push(SubSigParam::Scalar(name, ty, default));
4505                }
4506                Token::ArrayVar(name) => {
4507                    self.advance();
4508                    let default = if self.eat(&Token::Assign) {
4509                        Some(Box::new(self.parse_ternary()?))
4510                    } else {
4511                        None
4512                    };
4513                    params.push(SubSigParam::Array(name, default));
4514                }
4515                Token::HashVar(name) => {
4516                    self.advance();
4517                    let default = if self.eat(&Token::Assign) {
4518                        Some(Box::new(self.parse_ternary()?))
4519                    } else {
4520                        None
4521                    };
4522                    params.push(SubSigParam::Hash(name, default));
4523                }
4524                Token::LBracket => {
4525                    self.advance();
4526                    let elems = self.parse_match_array_elems_until_rbracket()?;
4527                    params.push(SubSigParam::ArrayDestruct(elems));
4528                }
4529                Token::LBrace => {
4530                    self.advance();
4531                    let mut pairs = Vec::new();
4532                    loop {
4533                        if matches!(self.peek(), Token::RBrace | Token::Eof) {
4534                            break;
4535                        }
4536                        if self.eat(&Token::Comma) {
4537                            continue;
4538                        }
4539                        let key = self.parse_sub_signature_hash_key()?;
4540                        self.expect(&Token::FatArrow)?;
4541                        let bind = self.parse_scalar_var_name()?;
4542                        pairs.push((key, bind));
4543                        self.eat(&Token::Comma);
4544                    }
4545                    self.expect(&Token::RBrace)?;
4546                    params.push(SubSigParam::HashDestruct(pairs));
4547                }
4548                tok => {
4549                    return Err(self.syntax_err(
4550                        format!(
4551                            "expected `$name`, `[ ... ]`, or `{{ ... }}` in sub signature, got {:?}",
4552                            tok
4553                        ),
4554                        self.peek_line(),
4555                    ));
4556                }
4557            }
4558            match self.peek() {
4559                Token::Comma => {
4560                    self.advance();
4561                    if matches!(self.peek(), Token::RParen) {
4562                        return Err(self.syntax_err(
4563                            "trailing `,` before `)` in sub signature",
4564                            self.peek_line(),
4565                        ));
4566                    }
4567                }
4568                Token::RParen => break,
4569                _ => {
4570                    return Err(self.syntax_err(
4571                        format!(
4572                            "expected `,` or `)` after sub signature parameter, got {:?}",
4573                            self.peek()
4574                        ),
4575                        self.peek_line(),
4576                    ));
4577                }
4578            }
4579        }
4580        Ok(params)
4581    }
4582
4583    /// Optional `sub` parens: either a Perl 5 prototype string or a stryke **`$name` / `{ k => $v }`** signature.
4584    fn parse_sub_sig_or_prototype_opt(&mut self) -> PerlResult<(Vec<SubSigParam>, Option<String>)> {
4585        if !matches!(self.peek(), Token::LParen) {
4586            return Ok((vec![], None));
4587        }
4588        self.advance();
4589        if matches!(self.peek(), Token::RParen) {
4590            self.advance();
4591            return Ok((vec![], Some(String::new())));
4592        }
4593        if self.sub_signature_list_starts_here() {
4594            let params = self.parse_sub_signature_param_list()?;
4595            self.expect(&Token::RParen)?;
4596            return Ok((params, None));
4597        }
4598        let proto = self.parse_legacy_sub_prototype_tail()?;
4599        Ok((vec![], Some(proto)))
4600    }
4601
4602    /// Optional subroutine attributes after name/prototype: `sub foo : lvalue { }`, `sub : ATTR(ARGS) { }`.
4603    fn parse_sub_attributes(&mut self) -> PerlResult<()> {
4604        while self.eat(&Token::Colon) {
4605            match self.advance() {
4606                (Token::Ident(_), _) => {}
4607                (tok, line) => {
4608                    return Err(self.syntax_err(
4609                        format!("Expected attribute name after `:`, got {:?}", tok),
4610                        line,
4611                    ));
4612                }
4613            }
4614            if self.eat(&Token::LParen) {
4615                let mut depth = 1usize;
4616                while depth > 0 {
4617                    match self.advance().0 {
4618                        Token::LParen => depth += 1,
4619                        Token::RParen => {
4620                            depth -= 1;
4621                        }
4622                        Token::Eof => {
4623                            return Err(self.syntax_err(
4624                                "Unterminated sub attribute argument list",
4625                                self.peek_line(),
4626                            ));
4627                        }
4628                        _ => {}
4629                    }
4630                }
4631            }
4632        }
4633        Ok(())
4634    }
4635
4636    /// After `fn` + optional `(SIG)` + attrs: `{ ... }` or stryke-only `= EXPR` (one assign-level
4637    /// expression; no top-level `,`). `sub` always requires `{ ... }`.
4638    fn parse_fn_eq_body_or_block(&mut self, is_sub_keyword: bool) -> PerlResult<Block> {
4639        if !is_sub_keyword && self.eat(&Token::Assign) {
4640            let expr = self.parse_assign_expr()?;
4641            if matches!(self.peek(), Token::Comma) {
4642                return Err(self.syntax_err(
4643                    "`fn ... =` allows only a single expression; use `fn ... { ... }` for multiple statements",
4644                    self.peek_line(),
4645                ));
4646            }
4647            let eline = expr.line;
4648            self.eat(&Token::Semicolon);
4649            let mut body = vec![Statement {
4650                label: None,
4651                kind: StmtKind::Expression(expr),
4652                line: eline,
4653            }];
4654            Self::default_topic_for_sole_bareword(&mut body);
4655            Ok(body)
4656        } else {
4657            self.parse_block()
4658        }
4659    }
4660
4661    fn parse_sub_decl(&mut self, is_sub_keyword: bool) -> PerlResult<Statement> {
4662        let line = self.peek_line();
4663        self.advance(); // 'sub' or 'fn'
4664        match self.peek().clone() {
4665            Token::Ident(_) => {
4666                let name = self.parse_package_qualified_identifier()?;
4667                // Topic-slot barewords (`_`, `_<`, `_<<`, `_<<<`, `_<<<<`,
4668                // `_0`, `_1`, …, `_N`, plus `_N<+` chain forms) are scalar
4669                // refs to the current/positional/outer topic. A user-defined
4670                // sub with any of these names — bare or package-qualified —
4671                // would shadow the topic in expression position and silently
4672                // break every `_`-aware builtin (`map { _ }`, `say _`,
4673                // `lc _`, …). Reject ALL forms at parse time, including
4674                // `Foo::_`, `Pkg::_0`, `My::Module::_<<<<`.
4675                let bare = name.rsplit("::").next().unwrap_or(&name);
4676                if Self::is_underscore_topic_slot(bare) {
4677                    return Err(self.syntax_err(
4678                        format!(
4679                            "`fn {}` would shadow the topic-slot scalar; pick a different name",
4680                            name
4681                        ),
4682                        line,
4683                    ));
4684                }
4685                if Self::is_reserved_special_var_name(bare) {
4686                    return Err(self.syntax_err(
4687                        format!(
4688                            "`fn {}` would shadow a Perl special variable / filehandle / compile-time token; pick a different name",
4689                            name
4690                        ),
4691                        line,
4692                    ));
4693                }
4694                // Allow shadowing builtins:
4695                // - In compat mode (full Perl 5)
4696                // - When parsing a module (imports should work)
4697                // Block shadowing:
4698                // - In user code (default mode, not parsing module)
4699                // - Always in no-interop mode
4700                let allow_shadow =
4701                    crate::compat_mode() || (self.parsing_module && !crate::no_interop_mode());
4702                if !allow_shadow {
4703                    self.check_udf_shadows_builtin(&name, line)?;
4704                }
4705                self.declared_subs.insert(name.clone());
4706                let (params, prototype) = self.parse_sub_sig_or_prototype_opt()?;
4707                self.parse_sub_attributes()?;
4708                let body = self.parse_fn_eq_body_or_block(is_sub_keyword)?;
4709                Ok(Statement {
4710                    label: None,
4711                    kind: StmtKind::SubDecl {
4712                        name,
4713                        params,
4714                        body,
4715                        prototype,
4716                    },
4717                    line,
4718                })
4719            }
4720            Token::LParen | Token::LBrace | Token::Colon => {
4721                // In no-interop mode, `sub {}` anonymous is not allowed — must use `fn {}`
4722                if is_sub_keyword && crate::no_interop_mode() {
4723                    return Err(self.syntax_err(
4724                        "stryke uses `fn {}` instead of `sub {}` (--no-interop)",
4725                        line,
4726                    ));
4727                }
4728                // Statement-level anonymous sub: `fn { }`, `sub () { }`, `sub :lvalue { }`
4729                let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
4730                self.parse_sub_attributes()?;
4731                let body = self.parse_fn_eq_body_or_block(is_sub_keyword)?;
4732                Ok(Statement {
4733                    label: None,
4734                    kind: StmtKind::Expression(Expr {
4735                        kind: ExprKind::CodeRef { params, body },
4736                        line,
4737                    }),
4738                    line,
4739                })
4740            }
4741            tok => {
4742                // Sigil-form topic-slot names (`fn $_`, `fn $_<`, `fn $_0`,
4743                // `fn @_`, `fn %_`, …) are also rejected with the same
4744                // foot-gun message as the bareword form. Without this branch
4745                // the user gets a confusing generic "Expected sub name" error.
4746                let topic_name = match &tok {
4747                    Token::ScalarVar(n) | Token::ArrayVar(n) | Token::HashVar(n)
4748                        if Self::is_underscore_topic_slot(n) =>
4749                    {
4750                        Some((
4751                            match &tok {
4752                                Token::ScalarVar(_) => '$',
4753                                Token::ArrayVar(_) => '@',
4754                                Token::HashVar(_) => '%',
4755                                _ => unreachable!(),
4756                            },
4757                            n.clone(),
4758                        ))
4759                    }
4760                    _ => None,
4761                };
4762                if let Some((sigil, n)) = topic_name {
4763                    return Err(self.syntax_err(
4764                        format!(
4765                            "`fn {}{}` would shadow the topic-slot scalar; pick a different name",
4766                            sigil, n
4767                        ),
4768                        self.peek_line(),
4769                    ));
4770                }
4771                // Sigil-form Perl special variables / globals — same foot-gun.
4772                // Catches `fn $@`, `fn $!`, `fn $/`, `fn $\\`, `fn $,`, `fn $;`,
4773                // `fn $"`, `fn $.`, `fn $0`, `fn $$`, `fn $?`, `fn $1`-`$9`,
4774                // `fn $^I`, `fn @ARGV`, `fn @INC`, `fn %ENV`, `fn %SIG`, etc.
4775                let special_var = match &tok {
4776                    Token::ScalarVar(n) | Token::ArrayVar(n) | Token::HashVar(n) => Some((
4777                        match &tok {
4778                            Token::ScalarVar(_) => '$',
4779                            Token::ArrayVar(_) => '@',
4780                            Token::HashVar(_) => '%',
4781                            _ => unreachable!(),
4782                        },
4783                        n.clone(),
4784                    )),
4785                    _ => None,
4786                };
4787                if let Some((sigil, n)) = special_var {
4788                    return Err(self.syntax_err(
4789                        format!(
4790                            "`fn {}{}` would shadow a Perl special variable / global; pick a different name",
4791                            sigil, n
4792                        ),
4793                        self.peek_line(),
4794                    ));
4795                }
4796                // After `fn`, `%` lexes as `Token::Percent` (modulo) rather
4797                // than a hash sigil — but `fn %ENV { }`, `fn %SIG { }`,
4798                // `fn %_ { }`, etc. all reach here. Emit the same foot-gun
4799                // message as the sigil-form catch above.
4800                if matches!(tok, Token::Percent) {
4801                    return Err(self.syntax_err(
4802                        "`fn %NAME` is not a valid sub declaration — `%name` would refer to a hash variable, not a sub name. To define a sub, use `fn NAME { ... }`",
4803                        self.peek_line(),
4804                    ));
4805                }
4806                Err(self.syntax_err(
4807                    format!("Expected sub name, `(`, `{{`, or `:`, got {:?}", tok),
4808                    self.peek_line(),
4809                ))
4810            }
4811        }
4812    }
4813
4814    /// `before|after|around "<glob>" { ... }` — register AOP advice.
4815    /// The pattern is a glob (`*`, `?`) matched against the called sub's bare name.
4816    fn parse_advice_decl(&mut self, kind: crate::ast::AdviceKind) -> PerlResult<Statement> {
4817        let line = self.peek_line();
4818        self.advance(); // before/after/around
4819        let pattern = match self.advance() {
4820            (Token::SingleString(s), _) | (Token::DoubleString(s), _) => s,
4821            (tok, err_line) => {
4822                return Err(self.syntax_err(
4823                    format!(
4824                        "Expected string-literal pattern after `{}`, got {:?}",
4825                        match kind {
4826                            crate::ast::AdviceKind::Before => "before",
4827                            crate::ast::AdviceKind::After => "after",
4828                            crate::ast::AdviceKind::Around => "around",
4829                        },
4830                        tok
4831                    ),
4832                    err_line,
4833                ));
4834            }
4835        };
4836        let body = self.parse_block()?;
4837        Ok(Statement {
4838            label: None,
4839            kind: StmtKind::AdviceDecl {
4840                kind,
4841                pattern,
4842                body,
4843            },
4844            line,
4845        })
4846    }
4847
4848    /// `struct Name { field => Type, ... ; fn method { } }`
4849    fn parse_struct_decl(&mut self) -> PerlResult<Statement> {
4850        let line = self.peek_line();
4851        self.advance(); // struct
4852        let name = self.parse_package_qualified_identifier().map_err(|_| {
4853            self.syntax_err(
4854                format!("Expected struct name, got {:?}", self.peek()),
4855                self.peek_line(),
4856            )
4857        })?;
4858        self.expect(&Token::LBrace)?;
4859        let mut fields = Vec::new();
4860        let mut methods = Vec::new();
4861        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
4862            // Check for method definition: `fn name { }` or `fn name { }`
4863            let is_method = match self.peek() {
4864                Token::Ident(s) => s == "fn" || s == "sub",
4865                _ => false,
4866            };
4867            if is_method {
4868                self.advance(); // fn/sub
4869                let method_name = match self.advance() {
4870                    (Token::Ident(n), _) => n,
4871                    (tok, err_line) => {
4872                        return Err(self
4873                            .syntax_err(format!("Expected method name, got {:?}", tok), err_line))
4874                    }
4875                };
4876                // Parse optional signature: `($self, $arg: Type, ...)`
4877                let params = if self.eat(&Token::LParen) {
4878                    let p = self.parse_sub_signature_param_list()?;
4879                    self.expect(&Token::RParen)?;
4880                    p
4881                } else {
4882                    Vec::new()
4883                };
4884                // parse_block handles its own { } delimiters
4885                let body = self.parse_block()?;
4886                methods.push(crate::ast::StructMethod {
4887                    name: method_name,
4888                    params,
4889                    body,
4890                });
4891                // Optional trailing comma/semicolon after method
4892                self.eat(&Token::Comma);
4893                self.eat(&Token::Semicolon);
4894                continue;
4895            }
4896
4897            let field_name = match self.advance() {
4898                (Token::Ident(n), _) => n,
4899                (tok, err_line) => {
4900                    return Err(
4901                        self.syntax_err(format!("Expected field name, got {:?}", tok), err_line)
4902                    )
4903                }
4904            };
4905            // Support both `field => Type` and bare `field` (implies Any type)
4906            let ty = if self.eat(&Token::FatArrow) {
4907                self.parse_type_name()?
4908            } else {
4909                crate::ast::PerlTypeName::Any
4910            };
4911            let default = if self.eat(&Token::Assign) {
4912                // Use parse_ternary to avoid consuming commas (next field separator)
4913                Some(self.parse_ternary()?)
4914            } else {
4915                None
4916            };
4917            fields.push(StructField {
4918                name: field_name,
4919                ty,
4920                default,
4921            });
4922            if !self.eat(&Token::Comma) {
4923                // Also allow semicolons as field separators
4924                self.eat(&Token::Semicolon);
4925            }
4926        }
4927        self.expect(&Token::RBrace)?;
4928        self.eat(&Token::Semicolon);
4929        Ok(Statement {
4930            label: None,
4931            kind: StmtKind::StructDecl {
4932                def: StructDef {
4933                    name,
4934                    fields,
4935                    methods,
4936                },
4937            },
4938            line,
4939        })
4940    }
4941
4942    /// `enum Name { Variant1, Variant2 => Type, ... }`
4943    fn parse_enum_decl(&mut self) -> PerlResult<Statement> {
4944        let line = self.peek_line();
4945        self.advance(); // enum
4946        let name = self.parse_package_qualified_identifier().map_err(|_| {
4947            self.syntax_err(
4948                format!("Expected enum name, got {:?}", self.peek()),
4949                self.peek_line(),
4950            )
4951        })?;
4952        self.expect(&Token::LBrace)?;
4953        let mut variants = Vec::new();
4954        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
4955            let variant_name = match self.advance() {
4956                (Token::Ident(n), _) => n,
4957                (tok, err_line) => {
4958                    return Err(
4959                        self.syntax_err(format!("Expected variant name, got {:?}", tok), err_line)
4960                    )
4961                }
4962            };
4963            let ty = if self.eat(&Token::FatArrow) {
4964                Some(self.parse_type_name()?)
4965            } else {
4966                None
4967            };
4968            variants.push(EnumVariant {
4969                name: variant_name,
4970                ty,
4971            });
4972            if !self.eat(&Token::Comma) {
4973                self.eat(&Token::Semicolon);
4974            }
4975        }
4976        self.expect(&Token::RBrace)?;
4977        self.eat(&Token::Semicolon);
4978        Ok(Statement {
4979            label: None,
4980            kind: StmtKind::EnumDecl {
4981                def: EnumDef { name, variants },
4982            },
4983            line,
4984        })
4985    }
4986
4987    /// `[abstract|final] class Name extends Parent impl Trait { fields; methods }`
4988    fn parse_class_decl(&mut self, is_abstract: bool, is_final: bool) -> PerlResult<Statement> {
4989        use crate::ast::{ClassDef, ClassField, ClassMethod, ClassStaticField, Visibility};
4990        let line = self.peek_line();
4991        self.advance(); // class
4992        let name = self.parse_package_qualified_identifier().map_err(|_| {
4993            self.syntax_err(
4994                format!("Expected class name, got {:?}", self.peek()),
4995                self.peek_line(),
4996            )
4997        })?;
4998
4999        // Parse `extends Parent1, Parent2` (each may be namespaced: `Foo::Base`)
5000        let mut extends = Vec::new();
5001        if matches!(self.peek(), Token::Ident(ref s) if s == "extends") {
5002            self.advance(); // extends
5003            loop {
5004                let parent = self.parse_package_qualified_identifier().map_err(|_| {
5005                    self.syntax_err(
5006                        format!(
5007                            "Expected parent class name after `extends`, got {:?}",
5008                            self.peek()
5009                        ),
5010                        self.peek_line(),
5011                    )
5012                })?;
5013                extends.push(parent);
5014                if !self.eat(&Token::Comma) {
5015                    break;
5016                }
5017            }
5018        }
5019
5020        // Parse `impl Trait1, Trait2` (each may be namespaced: `Foo::Trait`)
5021        let mut implements = Vec::new();
5022        if matches!(self.peek(), Token::Ident(ref s) if s == "impl") {
5023            self.advance(); // impl
5024            loop {
5025                let trait_name = self.parse_package_qualified_identifier().map_err(|_| {
5026                    self.syntax_err(
5027                        format!("Expected trait name after `impl`, got {:?}", self.peek()),
5028                        self.peek_line(),
5029                    )
5030                })?;
5031                implements.push(trait_name);
5032                if !self.eat(&Token::Comma) {
5033                    break;
5034                }
5035            }
5036        }
5037
5038        self.expect(&Token::LBrace)?;
5039        let mut fields = Vec::new();
5040        let mut methods = Vec::new();
5041        let mut static_fields = Vec::new();
5042
5043        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
5044            // Check for visibility modifier
5045            let visibility = match self.peek() {
5046                Token::Ident(ref s) if s == "pub" => {
5047                    self.advance();
5048                    Visibility::Public
5049                }
5050                Token::Ident(ref s) if s == "priv" => {
5051                    self.advance();
5052                    Visibility::Private
5053                }
5054                Token::Ident(ref s) if s == "prot" => {
5055                    self.advance();
5056                    Visibility::Protected
5057                }
5058                _ => Visibility::Public, // default public
5059            };
5060
5061            // Check for static field: `static name: Type = default`
5062            if matches!(self.peek(), Token::Ident(ref s) if s == "static") {
5063                self.advance(); // static
5064
5065                // Could be a static method (`static fn`) or static field
5066                if matches!(self.peek(), Token::Ident(ref s) if s == "fn" || s == "sub") {
5067                    // static fn is same as fn Self.name — handled below but not here
5068                    return Err(self.syntax_err(
5069                        "use `fn Self.name` for static methods, not `static fn`",
5070                        self.peek_line(),
5071                    ));
5072                }
5073
5074                let field_name = match self.advance() {
5075                    (Token::Ident(n), _) => n,
5076                    (tok, err_line) => {
5077                        return Err(self.syntax_err(
5078                            format!("Expected static field name, got {:?}", tok),
5079                            err_line,
5080                        ))
5081                    }
5082                };
5083
5084                let ty = if self.eat(&Token::Colon) {
5085                    self.parse_type_name()?
5086                } else {
5087                    crate::ast::PerlTypeName::Any
5088                };
5089
5090                let default = if self.eat(&Token::Assign) {
5091                    Some(self.parse_ternary()?)
5092                } else {
5093                    None
5094                };
5095
5096                static_fields.push(ClassStaticField {
5097                    name: field_name,
5098                    ty,
5099                    visibility,
5100                    default,
5101                });
5102
5103                if !self.eat(&Token::Comma) {
5104                    self.eat(&Token::Semicolon);
5105                }
5106                continue;
5107            }
5108
5109            // Check for `final` modifier before fn
5110            let method_is_final = matches!(self.peek(), Token::Ident(ref s) if s == "final");
5111            if method_is_final {
5112                self.advance(); // final
5113            }
5114
5115            // Check for method: `fn name` or `fn Self.name` (static)
5116            let is_method = matches!(self.peek(), Token::Ident(ref s) if s == "fn" || s == "sub");
5117            if is_method {
5118                self.advance(); // fn/sub
5119
5120                // Check for static method: `fn Self.name`
5121                let is_static = matches!(self.peek(), Token::Ident(ref s) if s == "Self");
5122                if is_static {
5123                    self.advance(); // Self
5124                    self.expect(&Token::Dot)?;
5125                }
5126
5127                let method_name = match self.advance() {
5128                    (Token::Ident(n), _) => n,
5129                    (tok, err_line) => {
5130                        return Err(self
5131                            .syntax_err(format!("Expected method name, got {:?}", tok), err_line))
5132                    }
5133                };
5134
5135                // Parse optional signature
5136                let params = if self.eat(&Token::LParen) {
5137                    let p = self.parse_sub_signature_param_list()?;
5138                    self.expect(&Token::RParen)?;
5139                    p
5140                } else {
5141                    Vec::new()
5142                };
5143
5144                // Body is optional (abstract method in trait has no body)
5145                let body = if matches!(self.peek(), Token::LBrace) {
5146                    Some(self.parse_block()?)
5147                } else {
5148                    None
5149                };
5150
5151                methods.push(ClassMethod {
5152                    name: method_name,
5153                    params,
5154                    body,
5155                    visibility,
5156                    is_static,
5157                    is_final: method_is_final,
5158                });
5159                self.eat(&Token::Comma);
5160                self.eat(&Token::Semicolon);
5161                continue;
5162            } else if method_is_final {
5163                return Err(self.syntax_err("`final` must be followed by `fn`", self.peek_line()));
5164            }
5165
5166            // Parse field: `name: Type = default`
5167            let field_name = match self.advance() {
5168                (Token::Ident(n), _) => n,
5169                (tok, err_line) => {
5170                    return Err(
5171                        self.syntax_err(format!("Expected field name, got {:?}", tok), err_line)
5172                    )
5173                }
5174            };
5175
5176            // Type after colon: `name: Type`
5177            let ty = if self.eat(&Token::Colon) {
5178                self.parse_type_name()?
5179            } else {
5180                crate::ast::PerlTypeName::Any
5181            };
5182
5183            // Default value after `=`
5184            let default = if self.eat(&Token::Assign) {
5185                Some(self.parse_ternary()?)
5186            } else {
5187                None
5188            };
5189
5190            fields.push(ClassField {
5191                name: field_name,
5192                ty,
5193                visibility,
5194                default,
5195            });
5196
5197            if !self.eat(&Token::Comma) {
5198                self.eat(&Token::Semicolon);
5199            }
5200        }
5201
5202        self.expect(&Token::RBrace)?;
5203        self.eat(&Token::Semicolon);
5204
5205        Ok(Statement {
5206            label: None,
5207            kind: StmtKind::ClassDecl {
5208                def: ClassDef {
5209                    name,
5210                    is_abstract,
5211                    is_final,
5212                    extends,
5213                    implements,
5214                    fields,
5215                    methods,
5216                    static_fields,
5217                },
5218            },
5219            line,
5220        })
5221    }
5222
5223    /// `trait Name { fn required; fn with_default { } }`
5224    fn parse_trait_decl(&mut self) -> PerlResult<Statement> {
5225        use crate::ast::{ClassMethod, TraitDef, Visibility};
5226        let line = self.peek_line();
5227        self.advance(); // trait
5228        let name = self.parse_package_qualified_identifier().map_err(|_| {
5229            self.syntax_err(
5230                format!("Expected trait name, got {:?}", self.peek()),
5231                self.peek_line(),
5232            )
5233        })?;
5234
5235        self.expect(&Token::LBrace)?;
5236        let mut methods = Vec::new();
5237
5238        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
5239            // Optional visibility
5240            let visibility = match self.peek() {
5241                Token::Ident(ref s) if s == "pub" => {
5242                    self.advance();
5243                    Visibility::Public
5244                }
5245                Token::Ident(ref s) if s == "priv" => {
5246                    self.advance();
5247                    Visibility::Private
5248                }
5249                Token::Ident(ref s) if s == "prot" => {
5250                    self.advance();
5251                    Visibility::Protected
5252                }
5253                _ => Visibility::Public,
5254            };
5255
5256            // Expect `fn` or `sub`
5257            if !matches!(self.peek(), Token::Ident(ref s) if s == "fn" || s == "sub") {
5258                return Err(self.syntax_err("Expected `fn` in trait definition", self.peek_line()));
5259            }
5260            self.advance(); // fn/sub
5261
5262            let method_name = match self.advance() {
5263                (Token::Ident(n), _) => n,
5264                (tok, err_line) => {
5265                    return Err(
5266                        self.syntax_err(format!("Expected method name, got {:?}", tok), err_line)
5267                    )
5268                }
5269            };
5270
5271            // Optional signature
5272            let params = if self.eat(&Token::LParen) {
5273                let p = self.parse_sub_signature_param_list()?;
5274                self.expect(&Token::RParen)?;
5275                p
5276            } else {
5277                Vec::new()
5278            };
5279
5280            // Body is optional (no body = abstract/required method)
5281            let body = if matches!(self.peek(), Token::LBrace) {
5282                Some(self.parse_block()?)
5283            } else {
5284                None
5285            };
5286
5287            methods.push(ClassMethod {
5288                name: method_name,
5289                params,
5290                body,
5291                visibility,
5292                is_static: false,
5293                is_final: false,
5294            });
5295
5296            self.eat(&Token::Comma);
5297            self.eat(&Token::Semicolon);
5298        }
5299
5300        self.expect(&Token::RBrace)?;
5301        self.eat(&Token::Semicolon);
5302
5303        Ok(Statement {
5304            label: None,
5305            kind: StmtKind::TraitDecl {
5306                def: TraitDef { name, methods },
5307            },
5308            line,
5309        })
5310    }
5311
5312    fn local_simple_target_to_var_decl(target: &Expr) -> Option<VarDecl> {
5313        match &target.kind {
5314            ExprKind::ScalarVar(name) => Some(VarDecl {
5315                sigil: Sigil::Scalar,
5316                name: name.clone(),
5317                initializer: None,
5318                frozen: false,
5319                type_annotation: None,
5320            }),
5321            ExprKind::ArrayVar(name) => Some(VarDecl {
5322                sigil: Sigil::Array,
5323                name: name.clone(),
5324                initializer: None,
5325                frozen: false,
5326                type_annotation: None,
5327            }),
5328            ExprKind::HashVar(name) => Some(VarDecl {
5329                sigil: Sigil::Hash,
5330                name: name.clone(),
5331                initializer: None,
5332                frozen: false,
5333                type_annotation: None,
5334            }),
5335            ExprKind::Typeglob(name) => Some(VarDecl {
5336                sigil: Sigil::Typeglob,
5337                name: name.clone(),
5338                initializer: None,
5339                frozen: false,
5340                type_annotation: None,
5341            }),
5342            _ => None,
5343        }
5344    }
5345
5346    fn parse_decl_array_destructure(
5347        &mut self,
5348        keyword: &str,
5349        line: usize,
5350    ) -> PerlResult<Statement> {
5351        self.expect(&Token::LBracket)?;
5352        let elems = self.parse_match_array_elems_until_rbracket()?;
5353        self.expect(&Token::Assign)?;
5354        self.suppress_scalar_hash_brace += 1;
5355        let rhs = self.parse_expression()?;
5356        self.suppress_scalar_hash_brace -= 1;
5357        let stmt = self.desugar_array_destructure(keyword, line, elems, rhs)?;
5358        self.parse_stmt_postfix_modifier(stmt)
5359    }
5360
5361    fn parse_decl_hash_destructure(&mut self, keyword: &str, line: usize) -> PerlResult<Statement> {
5362        let MatchPattern::Hash(pairs) = self.parse_match_hash_pattern()? else {
5363            unreachable!("parse_match_hash_pattern returns Hash");
5364        };
5365        self.expect(&Token::Assign)?;
5366        self.suppress_scalar_hash_brace += 1;
5367        let rhs = self.parse_expression()?;
5368        self.suppress_scalar_hash_brace -= 1;
5369        let stmt = self.desugar_hash_destructure(keyword, line, pairs, rhs)?;
5370        self.parse_stmt_postfix_modifier(stmt)
5371    }
5372
5373    fn desugar_array_destructure(
5374        &mut self,
5375        keyword: &str,
5376        line: usize,
5377        elems: Vec<MatchArrayElem>,
5378        rhs: Expr,
5379    ) -> PerlResult<Statement> {
5380        let tmp = format!("__stryke_ds_{}", self.alloc_desugar_tmp());
5381        let mut stmts: Vec<Statement> = Vec::new();
5382        stmts.push(destructure_stmt_from_var_decls(
5383            keyword,
5384            vec![VarDecl {
5385                sigil: Sigil::Scalar,
5386                name: tmp.clone(),
5387                initializer: Some(rhs),
5388                frozen: false,
5389                type_annotation: None,
5390            }],
5391            line,
5392        ));
5393
5394        let has_rest = elems
5395            .iter()
5396            .any(|e| matches!(e, MatchArrayElem::Rest | MatchArrayElem::RestBind(_)));
5397        let fixed_slots = elems
5398            .iter()
5399            .filter(|e| {
5400                matches!(
5401                    e,
5402                    MatchArrayElem::CaptureScalar(_) | MatchArrayElem::Expr(_)
5403                )
5404            })
5405            .count();
5406        if !has_rest {
5407            let cond = Expr {
5408                kind: ExprKind::BinOp {
5409                    left: Box::new(destructure_expr_array_len(&tmp, line)),
5410                    op: BinOp::NumEq,
5411                    right: Box::new(Expr {
5412                        kind: ExprKind::Integer(fixed_slots as i64),
5413                        line,
5414                    }),
5415                },
5416                line,
5417            };
5418            stmts.push(destructure_stmt_unless_die(
5419                line,
5420                cond,
5421                "array destructure: length mismatch",
5422            ));
5423        }
5424
5425        let mut idx: i64 = 0;
5426        for elem in elems {
5427            match elem {
5428                MatchArrayElem::Rest => break,
5429                MatchArrayElem::RestBind(name) => {
5430                    let list_source = Expr {
5431                        kind: ExprKind::Deref {
5432                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
5433                            kind: Sigil::Array,
5434                        },
5435                        line,
5436                    };
5437                    let last_ix = Expr {
5438                        kind: ExprKind::BinOp {
5439                            left: Box::new(destructure_expr_array_len(&tmp, line)),
5440                            op: BinOp::Sub,
5441                            right: Box::new(Expr {
5442                                kind: ExprKind::Integer(1),
5443                                line,
5444                            }),
5445                        },
5446                        line,
5447                    };
5448                    let range = Expr {
5449                        kind: ExprKind::Range {
5450                            from: Box::new(Expr {
5451                                kind: ExprKind::Integer(idx),
5452                                line,
5453                            }),
5454                            to: Box::new(last_ix),
5455                            exclusive: false,
5456                            step: None,
5457                        },
5458                        line,
5459                    };
5460                    let slice = Expr {
5461                        kind: ExprKind::AnonymousListSlice {
5462                            source: Box::new(list_source),
5463                            indices: vec![range],
5464                        },
5465                        line,
5466                    };
5467                    stmts.push(destructure_stmt_from_var_decls(
5468                        keyword,
5469                        vec![VarDecl {
5470                            sigil: Sigil::Array,
5471                            name,
5472                            initializer: Some(slice),
5473                            frozen: false,
5474                            type_annotation: None,
5475                        }],
5476                        line,
5477                    ));
5478                    break;
5479                }
5480                MatchArrayElem::CaptureScalar(name) => {
5481                    let arrow = Expr {
5482                        kind: ExprKind::ArrowDeref {
5483                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
5484                            index: Box::new(Expr {
5485                                kind: ExprKind::Integer(idx),
5486                                line,
5487                            }),
5488                            kind: DerefKind::Array,
5489                        },
5490                        line,
5491                    };
5492                    stmts.push(destructure_stmt_from_var_decls(
5493                        keyword,
5494                        vec![VarDecl {
5495                            sigil: Sigil::Scalar,
5496                            name,
5497                            initializer: Some(arrow),
5498                            frozen: false,
5499                            type_annotation: None,
5500                        }],
5501                        line,
5502                    ));
5503                    idx += 1;
5504                }
5505                MatchArrayElem::Expr(e) => {
5506                    let elem_subj = Expr {
5507                        kind: ExprKind::ArrowDeref {
5508                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
5509                            index: Box::new(Expr {
5510                                kind: ExprKind::Integer(idx),
5511                                line,
5512                            }),
5513                            kind: DerefKind::Array,
5514                        },
5515                        line,
5516                    };
5517                    let match_expr = Expr {
5518                        kind: ExprKind::AlgebraicMatch {
5519                            subject: Box::new(elem_subj),
5520                            arms: vec![
5521                                MatchArm {
5522                                    pattern: MatchPattern::Value(Box::new(e.clone())),
5523                                    guard: None,
5524                                    body: Expr {
5525                                        kind: ExprKind::Integer(0),
5526                                        line,
5527                                    },
5528                                },
5529                                MatchArm {
5530                                    pattern: MatchPattern::Any,
5531                                    guard: None,
5532                                    body: Expr {
5533                                        kind: ExprKind::Die(vec![Expr {
5534                                            kind: ExprKind::String(
5535                                                "array destructure: element pattern mismatch"
5536                                                    .to_string(),
5537                                            ),
5538                                            line,
5539                                        }]),
5540                                        line,
5541                                    },
5542                                },
5543                            ],
5544                        },
5545                        line,
5546                    };
5547                    stmts.push(Statement {
5548                        label: None,
5549                        kind: StmtKind::Expression(match_expr),
5550                        line,
5551                    });
5552                    idx += 1;
5553                }
5554            }
5555        }
5556
5557        Ok(Statement {
5558            label: None,
5559            kind: StmtKind::StmtGroup(stmts),
5560            line,
5561        })
5562    }
5563
5564    fn desugar_hash_destructure(
5565        &mut self,
5566        keyword: &str,
5567        line: usize,
5568        pairs: Vec<MatchHashPair>,
5569        rhs: Expr,
5570    ) -> PerlResult<Statement> {
5571        let tmp = format!("__stryke_ds_{}", self.alloc_desugar_tmp());
5572        let mut stmts: Vec<Statement> = Vec::new();
5573        stmts.push(destructure_stmt_from_var_decls(
5574            keyword,
5575            vec![VarDecl {
5576                sigil: Sigil::Scalar,
5577                name: tmp.clone(),
5578                initializer: Some(rhs),
5579                frozen: false,
5580                type_annotation: None,
5581            }],
5582            line,
5583        ));
5584
5585        for pair in pairs {
5586            match pair {
5587                MatchHashPair::KeyOnly { key } => {
5588                    let exists_op = Expr {
5589                        kind: ExprKind::Exists(Box::new(Expr {
5590                            kind: ExprKind::ArrowDeref {
5591                                expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
5592                                index: Box::new(key),
5593                                kind: DerefKind::Hash,
5594                            },
5595                            line,
5596                        })),
5597                        line,
5598                    };
5599                    stmts.push(destructure_stmt_unless_die(
5600                        line,
5601                        exists_op,
5602                        "hash destructure: missing required key",
5603                    ));
5604                }
5605                MatchHashPair::Capture { key, name } => {
5606                    let init = Expr {
5607                        kind: ExprKind::ArrowDeref {
5608                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
5609                            index: Box::new(key),
5610                            kind: DerefKind::Hash,
5611                        },
5612                        line,
5613                    };
5614                    stmts.push(destructure_stmt_from_var_decls(
5615                        keyword,
5616                        vec![VarDecl {
5617                            sigil: Sigil::Scalar,
5618                            name,
5619                            initializer: Some(init),
5620                            frozen: false,
5621                            type_annotation: None,
5622                        }],
5623                        line,
5624                    ));
5625                }
5626            }
5627        }
5628
5629        Ok(Statement {
5630            label: None,
5631            kind: StmtKind::StmtGroup(stmts),
5632            line,
5633        })
5634    }
5635
5636    fn parse_my_our_local(
5637        &mut self,
5638        keyword: &str,
5639        allow_type_annotation: bool,
5640    ) -> PerlResult<Statement> {
5641        let line = self.peek_line();
5642        self.advance(); // 'my'/'our'/'local'
5643
5644        if keyword == "local"
5645            && !matches!(self.peek(), Token::LParen | Token::LBracket | Token::LBrace)
5646        {
5647            let target = self.parse_postfix()?;
5648            let mut initializer: Option<Expr> = None;
5649            if self.eat(&Token::Assign) {
5650                initializer = Some(self.parse_expression()?);
5651            } else if matches!(
5652                self.peek(),
5653                Token::OrAssign | Token::DefinedOrAssign | Token::AndAssign
5654            ) {
5655                if matches!(&target.kind, ExprKind::Typeglob(_)) {
5656                    return Err(self.syntax_err(
5657                        "compound assignment on typeglob declaration is not supported",
5658                        self.peek_line(),
5659                    ));
5660                }
5661                let op = match self.peek().clone() {
5662                    Token::OrAssign => BinOp::LogOr,
5663                    Token::DefinedOrAssign => BinOp::DefinedOr,
5664                    Token::AndAssign => BinOp::LogAnd,
5665                    _ => unreachable!(),
5666                };
5667                self.advance();
5668                let rhs = self.parse_assign_expr()?;
5669                let tgt_line = target.line;
5670                initializer = Some(Expr {
5671                    kind: ExprKind::CompoundAssign {
5672                        target: Box::new(target.clone()),
5673                        op,
5674                        value: Box::new(rhs),
5675                    },
5676                    line: tgt_line,
5677                });
5678            }
5679
5680            let kind = if let Some(mut decl) = Self::local_simple_target_to_var_decl(&target) {
5681                decl.initializer = initializer;
5682                StmtKind::Local(vec![decl])
5683            } else {
5684                StmtKind::LocalExpr {
5685                    target,
5686                    initializer,
5687                }
5688            };
5689            let stmt = Statement {
5690                label: None,
5691                kind,
5692                line,
5693            };
5694            return self.parse_stmt_postfix_modifier(stmt);
5695        }
5696
5697        if matches!(self.peek(), Token::LBracket) {
5698            return self.parse_decl_array_destructure(keyword, line);
5699        }
5700        if matches!(self.peek(), Token::LBrace) {
5701            return self.parse_decl_hash_destructure(keyword, line);
5702        }
5703
5704        let mut decls = Vec::new();
5705
5706        if self.eat(&Token::LParen) {
5707            // my ($a, @b, %c)
5708            while !matches!(self.peek(), Token::RParen | Token::Eof) {
5709                let decl = self.parse_var_decl(allow_type_annotation)?;
5710                decls.push(decl);
5711                if !self.eat(&Token::Comma) {
5712                    break;
5713                }
5714            }
5715            self.expect(&Token::RParen)?;
5716        } else {
5717            decls.push(self.parse_var_decl(allow_type_annotation)?);
5718        }
5719
5720        // Optional initializer: my $x = expr — plus `our @EXPORT = our @EXPORT_OK = qw(...)` (Try::Tiny).
5721        if self.eat(&Token::Assign) {
5722            if keyword == "our" && decls.len() == 1 {
5723                while matches!(self.peek(), Token::Ident(ref i) if i == "our") {
5724                    self.advance();
5725                    decls.push(self.parse_var_decl(allow_type_annotation)?);
5726                    if !self.eat(&Token::Assign) {
5727                        return Err(self.syntax_err(
5728                            "expected `=` after `our` in chained our-declaration",
5729                            self.peek_line(),
5730                        ));
5731                    }
5732                }
5733            }
5734            let rhs_start_pos = self.pos;
5735            let mut val = self.parse_expression()?;
5736            // Stryke implicit-coderef sugar: `my $f = _ * 2;` ≡
5737            // `my $f = fn { _ * 2 };`. Triggers only when (a) the LHS is a
5738            // single scalar declaration, (b) the RHS contains at least one
5739            // *bare* positional alias (`_`, `_0`, `_1`, …; no `$` sigil), and
5740            // (c) the RHS isn't already a coderef-shaped value. Bare-positional
5741            // tracking comes from the lexer (see `Lexer::bare_positional_indices`)
5742            // so legitimate uses of `$_` inside e.g. `fn { my $x = $_; … }`
5743            // closures keep their Perl semantics.
5744            if !crate::compat_mode()
5745                && self.block_depth == 0
5746                && decls.len() == 1
5747                && matches!(decls[0].sigil, Sigil::Scalar)
5748                && !matches!(
5749                    val.kind,
5750                    ExprKind::CodeRef { .. }
5751                        | ExprKind::SubroutineRef(_)
5752                        | ExprKind::SubroutineCodeRef(_)
5753                        | ExprKind::DynamicSubCodeRef(_)
5754                )
5755            {
5756                let rhs_end_pos = self.pos;
5757                // Trigger only when the RHS *begins* with a bare positional
5758                // alias (e.g. `_ * 2`, `_1 + _2`). Restricting to the leading
5759                // token avoids false positives when bare `_` appears deeper in
5760                // unrelated grammar (most notably `match { _ => ... }` arm
5761                // patterns, which are wildcard patterns rather than topic
5762                // references).
5763                let rhs_has_bare_positional = self.bare_positional_indices.contains(&rhs_start_pos)
5764                    && rhs_start_pos < rhs_end_pos;
5765                if rhs_has_bare_positional {
5766                    let val_line = val.line;
5767                    val = Expr {
5768                        kind: ExprKind::CodeRef {
5769                            params: Vec::new(),
5770                            body: vec![Statement {
5771                                label: None,
5772                                kind: StmtKind::Expression(val),
5773                                line: val_line,
5774                            }],
5775                        },
5776                        line: val_line,
5777                    };
5778                }
5779            }
5780            // Validate assignment for single variable declarations (not destructuring)
5781            // `my ($a, $b) = (1, 2)` is destructuring, not scalar-from-list
5782            if !crate::compat_mode() && decls.len() == 1 {
5783                let decl = &decls[0];
5784                let target_kind = match decl.sigil {
5785                    Sigil::Scalar => ExprKind::ScalarVar(decl.name.clone()),
5786                    Sigil::Array => ExprKind::ArrayVar(decl.name.clone()),
5787                    Sigil::Hash => ExprKind::HashVar(decl.name.clone()),
5788                    Sigil::Typeglob => {
5789                        // Skip validation for typeglob
5790                        if decls.len() == 1 {
5791                            decls[0].initializer = Some(val);
5792                        } else {
5793                            for d in &mut decls {
5794                                d.initializer = Some(val.clone());
5795                            }
5796                        }
5797                        return Ok(Statement {
5798                            label: None,
5799                            kind: match keyword {
5800                                "my" => StmtKind::My(decls),
5801                                "mysync" => StmtKind::MySync(decls),
5802                                "our" => StmtKind::Our(decls),
5803                                "oursync" => StmtKind::OurSync(decls),
5804                                "local" => StmtKind::Local(decls),
5805                                "state" => StmtKind::State(decls),
5806                                _ => unreachable!(),
5807                            },
5808                            line,
5809                        });
5810                    }
5811                };
5812                let target = Expr {
5813                    kind: target_kind,
5814                    line,
5815                };
5816                self.validate_assignment(&target, &val, line)?;
5817            }
5818            if decls.len() == 1 {
5819                decls[0].initializer = Some(val);
5820            } else {
5821                for decl in &mut decls {
5822                    decl.initializer = Some(val.clone());
5823                }
5824            }
5825        } else if decls.len() == 1 {
5826            // `our $Verbose ||= 0` (Exporter.pm) — compound assign on a single decl
5827            let op = match self.peek().clone() {
5828                Token::OrAssign => Some(BinOp::LogOr),
5829                Token::DefinedOrAssign => Some(BinOp::DefinedOr),
5830                Token::AndAssign => Some(BinOp::LogAnd),
5831                _ => None,
5832            };
5833            if let Some(op) = op {
5834                let d = &decls[0];
5835                if matches!(d.sigil, Sigil::Typeglob) {
5836                    return Err(self.syntax_err(
5837                        "compound assignment on typeglob declaration is not supported",
5838                        self.peek_line(),
5839                    ));
5840                }
5841                self.advance();
5842                let rhs = self.parse_assign_expr()?;
5843                let target = Expr {
5844                    kind: match d.sigil {
5845                        Sigil::Scalar => ExprKind::ScalarVar(d.name.clone()),
5846                        Sigil::Array => ExprKind::ArrayVar(d.name.clone()),
5847                        Sigil::Hash => ExprKind::HashVar(d.name.clone()),
5848                        Sigil::Typeglob => unreachable!(),
5849                    },
5850                    line,
5851                };
5852                decls[0].initializer = Some(Expr {
5853                    kind: ExprKind::CompoundAssign {
5854                        target: Box::new(target),
5855                        op,
5856                        value: Box::new(rhs),
5857                    },
5858                    line,
5859                });
5860            }
5861        }
5862
5863        let kind = match keyword {
5864            "my" => StmtKind::My(decls),
5865            "mysync" => StmtKind::MySync(decls),
5866            "our" => StmtKind::Our(decls),
5867            "oursync" => StmtKind::OurSync(decls),
5868            "local" => StmtKind::Local(decls),
5869            "state" => StmtKind::State(decls),
5870            _ => unreachable!(),
5871        };
5872        let stmt = Statement {
5873            label: None,
5874            kind,
5875            line,
5876        };
5877        // `my $x = 1 if $y;` — statement modifier applies to the whole declaration (Perl).
5878        self.parse_stmt_postfix_modifier(stmt)
5879    }
5880
5881    fn parse_var_decl(&mut self, allow_type_annotation: bool) -> PerlResult<VarDecl> {
5882        let mut decl = match self.advance() {
5883            (Token::ScalarVar(name), _) => VarDecl {
5884                sigil: Sigil::Scalar,
5885                name,
5886                initializer: None,
5887                frozen: false,
5888                type_annotation: None,
5889            },
5890            (Token::ArrayVar(name), _) => VarDecl {
5891                sigil: Sigil::Array,
5892                name,
5893                initializer: None,
5894                frozen: false,
5895                type_annotation: None,
5896            },
5897            (Token::HashVar(name), line) => {
5898                if !crate::compat_mode() {
5899                    self.check_hash_shadows_reserved(&name, line)?;
5900                }
5901                VarDecl {
5902                    sigil: Sigil::Hash,
5903                    name,
5904                    initializer: None,
5905                    frozen: false,
5906                    type_annotation: None,
5907                }
5908            }
5909            (Token::Star, _line) => {
5910                let name = match self.advance() {
5911                    (Token::Ident(n), _) => n,
5912                    (tok, l) => {
5913                        return Err(self
5914                            .syntax_err(format!("Expected identifier after *, got {:?}", tok), l));
5915                    }
5916                };
5917                VarDecl {
5918                    sigil: Sigil::Typeglob,
5919                    name,
5920                    initializer: None,
5921                    frozen: false,
5922                    type_annotation: None,
5923                }
5924            }
5925            // `my ($a, undef, $c) = (1, 2, 3)` — Perl idiom for discarding a
5926            // slot in a list assignment. The interpreter treats `undef`-named
5927            // scalar decls as throwaway: declared into a unique sink so the
5928            // distribute-to-decls loop advances past the slot.
5929            (Token::Ident(ref kw), _) if kw == "undef" => VarDecl {
5930                sigil: Sigil::Scalar,
5931                // Synthesize a name that user code cannot reference. Each
5932                // sink slot in a list-assign gets its own unique name so the
5933                // declarations don't collide.
5934                name: format!("__undef_sink_{}", self.pos),
5935                initializer: None,
5936                frozen: false,
5937                type_annotation: None,
5938            },
5939            (tok, line) => {
5940                return Err(self.syntax_err(
5941                    format!("Expected variable in declaration, got {:?}", tok),
5942                    line,
5943                ));
5944            }
5945        };
5946        if allow_type_annotation && self.eat(&Token::Colon) {
5947            let ty = self.parse_type_name()?;
5948            if decl.sigil != Sigil::Scalar {
5949                return Err(self.syntax_err(
5950                    "`: Type` is only valid for scalar declarations (typed my $name : Int)",
5951                    self.peek_line(),
5952                ));
5953            }
5954            decl.type_annotation = Some(ty);
5955        }
5956        Ok(decl)
5957    }
5958
5959    fn parse_type_name(&mut self) -> PerlResult<PerlTypeName> {
5960        match self.advance() {
5961            (Token::Ident(name), _) => match name.as_str() {
5962                "Int" => Ok(PerlTypeName::Int),
5963                "Str" => Ok(PerlTypeName::Str),
5964                "Float" => Ok(PerlTypeName::Float),
5965                "Bool" => Ok(PerlTypeName::Bool),
5966                "Array" => Ok(PerlTypeName::Array),
5967                "Hash" => Ok(PerlTypeName::Hash),
5968                "Ref" => Ok(PerlTypeName::Ref),
5969                "Any" => Ok(PerlTypeName::Any),
5970                _ => Ok(PerlTypeName::Struct(name)),
5971            },
5972            (tok, err_line) => Err(self.syntax_err(
5973                format!("Expected type name after `:`, got {:?}", tok),
5974                err_line,
5975            )),
5976        }
5977    }
5978
5979    fn parse_package(&mut self) -> PerlResult<Statement> {
5980        let line = self.peek_line();
5981        self.advance(); // 'package'
5982        let name = match self.advance() {
5983            (Token::Ident(n), _) => n,
5984            (tok, line) => {
5985                return Err(self.syntax_err(format!("Expected package name, got {:?}", tok), line))
5986            }
5987        };
5988        // Handle Foo::Bar
5989        let mut full_name = name;
5990        while self.eat(&Token::PackageSep) {
5991            if let (Token::Ident(part), _) = self.advance() {
5992                full_name = format!("{}::{}", full_name, part);
5993            }
5994        }
5995        self.eat(&Token::Semicolon);
5996        Ok(Statement {
5997            label: None,
5998            kind: StmtKind::Package { name: full_name },
5999            line,
6000        })
6001    }
6002
6003    fn parse_use(&mut self) -> PerlResult<Statement> {
6004        let line = self.peek_line();
6005        self.advance(); // 'use'
6006        let (tok, tok_line) = self.advance();
6007        match tok {
6008            Token::Float(v) => {
6009                self.eat(&Token::Semicolon);
6010                Ok(Statement {
6011                    label: None,
6012                    kind: StmtKind::UsePerlVersion { version: v },
6013                    line,
6014                })
6015            }
6016            Token::Integer(n) => {
6017                if matches!(self.peek(), Token::Semicolon | Token::Eof) {
6018                    self.eat(&Token::Semicolon);
6019                    Ok(Statement {
6020                        label: None,
6021                        kind: StmtKind::UsePerlVersion { version: n as f64 },
6022                        line,
6023                    })
6024                } else {
6025                    Err(self.syntax_err(
6026                        format!("Expected ';' after use VERSION (got {:?})", self.peek()),
6027                        line,
6028                    ))
6029                }
6030            }
6031            Token::Ident(n) => {
6032                let mut full_name = n;
6033                while self.eat(&Token::PackageSep) {
6034                    if let (Token::Ident(part), _) = self.advance() {
6035                        full_name = format!("{}::{}", full_name, part);
6036                    }
6037                }
6038                if full_name == "overload" {
6039                    let mut pairs = Vec::new();
6040                    let mut parse_overload_pairs = |this: &mut Self| -> PerlResult<()> {
6041                        loop {
6042                            if matches!(this.peek(), Token::RParen | Token::Semicolon | Token::Eof)
6043                            {
6044                                break;
6045                            }
6046                            let key_e = this.parse_assign_expr()?;
6047                            this.expect(&Token::FatArrow)?;
6048                            let val_e = this.parse_assign_expr()?;
6049                            let key = this.expr_to_overload_key(&key_e)?;
6050                            let val = this.expr_to_overload_sub(&val_e)?;
6051                            pairs.push((key, val));
6052                            if !this.eat(&Token::Comma) {
6053                                break;
6054                            }
6055                        }
6056                        Ok(())
6057                    };
6058                    if self.eat(&Token::LParen) {
6059                        // `use overload ();` — common in JSON::PP and other modules.
6060                        parse_overload_pairs(self)?;
6061                        self.expect(&Token::RParen)?;
6062                    } else if !matches!(self.peek(), Token::Semicolon | Token::Eof) {
6063                        parse_overload_pairs(self)?;
6064                    }
6065                    self.eat(&Token::Semicolon);
6066                    return Ok(Statement {
6067                        label: None,
6068                        kind: StmtKind::UseOverload { pairs },
6069                        line,
6070                    });
6071                }
6072                let mut imports = Vec::new();
6073                if !matches!(self.peek(), Token::Semicolon | Token::Eof)
6074                    && !self.next_is_new_statement_start(tok_line)
6075                {
6076                    loop {
6077                        if matches!(self.peek(), Token::Semicolon | Token::Eof) {
6078                            break;
6079                        }
6080                        imports.push(self.parse_expression()?);
6081                        if !self.eat(&Token::Comma) {
6082                            break;
6083                        }
6084                    }
6085                }
6086                self.eat(&Token::Semicolon);
6087                Ok(Statement {
6088                    label: None,
6089                    kind: StmtKind::Use {
6090                        module: full_name,
6091                        imports,
6092                    },
6093                    line,
6094                })
6095            }
6096            other => Err(self.syntax_err(
6097                format!("Expected module name or version after use, got {:?}", other),
6098                tok_line,
6099            )),
6100        }
6101    }
6102
6103    fn parse_no(&mut self) -> PerlResult<Statement> {
6104        let line = self.peek_line();
6105        self.advance(); // 'no'
6106        let module = match self.advance() {
6107            (Token::Ident(n), tok_line) => (n, tok_line),
6108            (tok, line) => {
6109                return Err(self.syntax_err(
6110                    format!("Expected module name after no, got {:?}", tok),
6111                    line,
6112                ))
6113            }
6114        };
6115        let (module_name, tok_line) = module;
6116        let mut full_name = module_name;
6117        while self.eat(&Token::PackageSep) {
6118            if let (Token::Ident(part), _) = self.advance() {
6119                full_name = format!("{}::{}", full_name, part);
6120            }
6121        }
6122        let mut imports = Vec::new();
6123        if !matches!(self.peek(), Token::Semicolon | Token::Eof)
6124            && !self.next_is_new_statement_start(tok_line)
6125        {
6126            loop {
6127                if matches!(self.peek(), Token::Semicolon | Token::Eof) {
6128                    break;
6129                }
6130                imports.push(self.parse_expression()?);
6131                if !self.eat(&Token::Comma) {
6132                    break;
6133                }
6134            }
6135        }
6136        self.eat(&Token::Semicolon);
6137        Ok(Statement {
6138            label: None,
6139            kind: StmtKind::No {
6140                module: full_name,
6141                imports,
6142            },
6143            line,
6144        })
6145    }
6146
6147    fn parse_return(&mut self) -> PerlResult<Statement> {
6148        let line = self.peek_line();
6149        self.advance(); // 'return'
6150                        // No-value return: terminator tokens AND any postfix statement-modifier
6151                        // keyword (`if`/`unless`/`while`/`until`/`for`/`foreach`). Without this
6152                        // the postfix-modifier check below never fires for valueless returns —
6153                        // `parse_assign_expr` would see `if` and look it up as a sub call,
6154                        // producing the misleading "Undefined subroutine &if" error.
6155        let val = if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof)
6156            || self.peek_is_postfix_stmt_modifier_keyword()
6157        {
6158            None
6159        } else {
6160            // Parse the operand as a comma-list — Perl's `return` is a
6161            // list-operator, so `return 1, 2, 3` returns the list (1, 2, 3).
6162            // (BUG-010) Stay below pipe-forward and stop at postfix
6163            // statement-modifier keywords like `if` / `unless`.
6164            let first = self.parse_assign_expr()?;
6165            if matches!(self.peek(), Token::Comma | Token::FatArrow) {
6166                let mut items = vec![first];
6167                while self.eat(&Token::Comma) || self.eat(&Token::FatArrow) {
6168                    if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof)
6169                        || self.peek_is_postfix_stmt_modifier_keyword()
6170                    {
6171                        break;
6172                    }
6173                    items.push(self.parse_assign_expr()?);
6174                }
6175                let line = items.first().map(|e| e.line).unwrap_or(line);
6176                Some(Expr {
6177                    kind: ExprKind::List(items),
6178                    line,
6179                })
6180            } else {
6181                Some(first)
6182            }
6183        };
6184        // Check for postfix modifiers on return
6185        let stmt = Statement {
6186            label: None,
6187            kind: StmtKind::Return(val),
6188            line,
6189        };
6190        if let Token::Ident(ref kw) = self.peek().clone() {
6191            match kw.as_str() {
6192                "if" => {
6193                    self.advance();
6194                    let cond = self.parse_expression()?;
6195                    self.eat(&Token::Semicolon);
6196                    return Ok(Statement {
6197                        label: None,
6198                        kind: StmtKind::If {
6199                            condition: cond,
6200                            body: vec![stmt],
6201                            elsifs: vec![],
6202                            else_block: None,
6203                        },
6204                        line,
6205                    });
6206                }
6207                "unless" => {
6208                    self.advance();
6209                    let cond = self.parse_expression()?;
6210                    self.eat(&Token::Semicolon);
6211                    return Ok(Statement {
6212                        label: None,
6213                        kind: StmtKind::Unless {
6214                            condition: cond,
6215                            body: vec![stmt],
6216                            else_block: None,
6217                        },
6218                        line,
6219                    });
6220                }
6221                _ => {}
6222            }
6223        }
6224        self.eat(&Token::Semicolon);
6225        Ok(stmt)
6226    }
6227
6228    // ── Expressions (Pratt / precedence climbing) ──
6229
6230    fn parse_expression(&mut self) -> PerlResult<Expr> {
6231        self.parse_comma_expr()
6232    }
6233
6234    fn parse_comma_expr(&mut self) -> PerlResult<Expr> {
6235        // Word-op precedence (or/and/not) sits ABOVE assignment in Perl —
6236        // `EXPR or $err = $@` parses as `EXPR or ($err = $@)`, NOT
6237        // `(EXPR or $err) = $@`. Entering through `parse_or_word` here
6238        // (instead of `parse_assign_expr` directly) gives `or`/`and`/`not`
6239        // looser binding than `=`, matching `perlop`. The deeper chain
6240        // (`parse_not_word → parse_assign_expr → parse_ternary → … →
6241        // parse_log_or → …`) handles tighter operators normally.
6242        let expr = self.parse_or_word()?;
6243        let mut exprs = vec![expr];
6244        while self.eat(&Token::Comma) || self.eat(&Token::FatArrow) {
6245            if matches!(
6246                self.peek(),
6247                Token::RParen | Token::RBracket | Token::RBrace | Token::Semicolon | Token::Eof
6248            ) {
6249                break; // trailing comma
6250            }
6251            exprs.push(self.parse_or_word()?);
6252        }
6253        if exprs.len() == 1 {
6254            return Ok(exprs.pop().unwrap());
6255        }
6256        let line = exprs[0].line;
6257        Ok(Expr {
6258            kind: ExprKind::List(exprs),
6259            line,
6260        })
6261    }
6262
6263    fn parse_assign_expr(&mut self) -> PerlResult<Expr> {
6264        let expr = self.parse_ternary()?;
6265        let line = expr.line;
6266
6267        match self.peek().clone() {
6268            Token::Assign => {
6269                self.advance();
6270                let right = self.parse_assign_expr()?;
6271                // Desugar `$obj->field = value` into `$obj->field(value)` (setter call)
6272                if let ExprKind::MethodCall { ref args, .. } = expr.kind {
6273                    if args.is_empty() {
6274                        // Destructure again to take ownership
6275                        let ExprKind::MethodCall {
6276                            object,
6277                            method,
6278                            super_call,
6279                            ..
6280                        } = expr.kind
6281                        else {
6282                            unreachable!()
6283                        };
6284                        return Ok(Expr {
6285                            kind: ExprKind::MethodCall {
6286                                object,
6287                                method,
6288                                args: vec![right],
6289                                super_call,
6290                            },
6291                            line,
6292                        });
6293                    }
6294                }
6295                self.validate_assignment(&expr, &right, line)?;
6296                Ok(Expr {
6297                    kind: ExprKind::Assign {
6298                        target: Box::new(expr),
6299                        value: Box::new(right),
6300                    },
6301                    line,
6302                })
6303            }
6304            Token::PlusAssign => {
6305                self.advance();
6306                let r = self.parse_assign_expr()?;
6307                Ok(Expr {
6308                    kind: ExprKind::CompoundAssign {
6309                        target: Box::new(expr),
6310                        op: BinOp::Add,
6311                        value: Box::new(r),
6312                    },
6313                    line,
6314                })
6315            }
6316            Token::MinusAssign => {
6317                self.advance();
6318                let r = self.parse_assign_expr()?;
6319                Ok(Expr {
6320                    kind: ExprKind::CompoundAssign {
6321                        target: Box::new(expr),
6322                        op: BinOp::Sub,
6323                        value: Box::new(r),
6324                    },
6325                    line,
6326                })
6327            }
6328            Token::MulAssign => {
6329                self.advance();
6330                let r = self.parse_assign_expr()?;
6331                Ok(Expr {
6332                    kind: ExprKind::CompoundAssign {
6333                        target: Box::new(expr),
6334                        op: BinOp::Mul,
6335                        value: Box::new(r),
6336                    },
6337                    line,
6338                })
6339            }
6340            Token::DivAssign => {
6341                self.advance();
6342                let r = self.parse_assign_expr()?;
6343                Ok(Expr {
6344                    kind: ExprKind::CompoundAssign {
6345                        target: Box::new(expr),
6346                        op: BinOp::Div,
6347                        value: Box::new(r),
6348                    },
6349                    line,
6350                })
6351            }
6352            Token::ModAssign => {
6353                self.advance();
6354                let r = self.parse_assign_expr()?;
6355                Ok(Expr {
6356                    kind: ExprKind::CompoundAssign {
6357                        target: Box::new(expr),
6358                        op: BinOp::Mod,
6359                        value: Box::new(r),
6360                    },
6361                    line,
6362                })
6363            }
6364            Token::PowAssign => {
6365                self.advance();
6366                let r = self.parse_assign_expr()?;
6367                Ok(Expr {
6368                    kind: ExprKind::CompoundAssign {
6369                        target: Box::new(expr),
6370                        op: BinOp::Pow,
6371                        value: Box::new(r),
6372                    },
6373                    line,
6374                })
6375            }
6376            Token::DotAssign => {
6377                self.advance();
6378                let r = self.parse_assign_expr()?;
6379                Ok(Expr {
6380                    kind: ExprKind::CompoundAssign {
6381                        target: Box::new(expr),
6382                        op: BinOp::Concat,
6383                        value: Box::new(r),
6384                    },
6385                    line,
6386                })
6387            }
6388            Token::BitAndAssign => {
6389                self.advance();
6390                let r = self.parse_assign_expr()?;
6391                Ok(Expr {
6392                    kind: ExprKind::CompoundAssign {
6393                        target: Box::new(expr),
6394                        op: BinOp::BitAnd,
6395                        value: Box::new(r),
6396                    },
6397                    line,
6398                })
6399            }
6400            Token::BitOrAssign => {
6401                self.advance();
6402                let r = self.parse_assign_expr()?;
6403                Ok(Expr {
6404                    kind: ExprKind::CompoundAssign {
6405                        target: Box::new(expr),
6406                        op: BinOp::BitOr,
6407                        value: Box::new(r),
6408                    },
6409                    line,
6410                })
6411            }
6412            Token::XorAssign => {
6413                self.advance();
6414                let r = self.parse_assign_expr()?;
6415                Ok(Expr {
6416                    kind: ExprKind::CompoundAssign {
6417                        target: Box::new(expr),
6418                        op: BinOp::BitXor,
6419                        value: Box::new(r),
6420                    },
6421                    line,
6422                })
6423            }
6424            Token::ShiftLeftAssign => {
6425                self.advance();
6426                let r = self.parse_assign_expr()?;
6427                Ok(Expr {
6428                    kind: ExprKind::CompoundAssign {
6429                        target: Box::new(expr),
6430                        op: BinOp::ShiftLeft,
6431                        value: Box::new(r),
6432                    },
6433                    line,
6434                })
6435            }
6436            Token::ShiftRightAssign => {
6437                self.advance();
6438                let r = self.parse_assign_expr()?;
6439                Ok(Expr {
6440                    kind: ExprKind::CompoundAssign {
6441                        target: Box::new(expr),
6442                        op: BinOp::ShiftRight,
6443                        value: Box::new(r),
6444                    },
6445                    line,
6446                })
6447            }
6448            Token::OrAssign => {
6449                self.advance();
6450                let r = self.parse_assign_expr()?;
6451                Ok(Expr {
6452                    kind: ExprKind::CompoundAssign {
6453                        target: Box::new(expr),
6454                        op: BinOp::LogOr,
6455                        value: Box::new(r),
6456                    },
6457                    line,
6458                })
6459            }
6460            Token::DefinedOrAssign => {
6461                self.advance();
6462                let r = self.parse_assign_expr()?;
6463                Ok(Expr {
6464                    kind: ExprKind::CompoundAssign {
6465                        target: Box::new(expr),
6466                        op: BinOp::DefinedOr,
6467                        value: Box::new(r),
6468                    },
6469                    line,
6470                })
6471            }
6472            Token::AndAssign => {
6473                self.advance();
6474                let r = self.parse_assign_expr()?;
6475                Ok(Expr {
6476                    kind: ExprKind::CompoundAssign {
6477                        target: Box::new(expr),
6478                        op: BinOp::LogAnd,
6479                        value: Box::new(r),
6480                    },
6481                    line,
6482                })
6483            }
6484            _ => Ok(expr),
6485        }
6486    }
6487
6488    fn parse_ternary(&mut self) -> PerlResult<Expr> {
6489        let expr = self.parse_pipe_forward()?;
6490        if self.eat(&Token::Question) {
6491            let line = expr.line;
6492            self.suppress_colon_range = self.suppress_colon_range.saturating_add(1);
6493            let then_expr = self.parse_assign_expr();
6494            self.suppress_colon_range = self.suppress_colon_range.saturating_sub(1);
6495            let then_expr = then_expr?;
6496            self.expect(&Token::Colon)?;
6497            let else_expr = self.parse_assign_expr()?;
6498            return Ok(Expr {
6499                kind: ExprKind::Ternary {
6500                    condition: Box::new(expr),
6501                    then_expr: Box::new(then_expr),
6502                    else_expr: Box::new(else_expr),
6503                },
6504                line,
6505            });
6506        }
6507        Ok(expr)
6508    }
6509
6510    /// `EXPR |> CALL` — pipe-forward (F#/Elixir). Left-associative; the LHS is threaded
6511    /// in as the **first argument** of the RHS call at parse time (pure AST rewrite,
6512    /// no runtime cost). `x |> f(a, b)` → `f(x, a, b)`; `x |> f` → `f(x)`; chain
6513    /// `x |> f |> g(2)` → `g(f(x), 2)`. Precedence sits between `?:` and `||`, so
6514    /// `x + 1 |> f || y` parses as `f(x + 1) || y`.
6515    fn parse_pipe_forward(&mut self) -> PerlResult<Expr> {
6516        // After moving word-ops (or/and/not) above the assignment level,
6517        // pipe_forward must descend into `parse_range` (which itself
6518        // descends into `parse_log_or`) — calling `parse_or_word` here
6519        // would re-introduce `or` at a wrong place in the precedence chain
6520        // (it now sits above `parse_comma_expr`). We skip past `parse_range`
6521        // rather than `parse_log_or` so `..` stays reachable.
6522        let mut left = self.parse_range()?;
6523        // Inside a paren-less arg list, `|>` is a hard terminator for the
6524        // enclosing call — leave it for the outer `parse_pipe_forward` loop
6525        // so `qw(…) |> head 2 |> join "-"` chains left-to-right as
6526        // `(qw(…) |> head 2) |> join "-"` instead of `head` swallowing the
6527        // outer `|>` via its first-arg `parse_assign_expr`.
6528        if self.no_pipe_forward_depth > 0 {
6529            return Ok(left);
6530        }
6531        while matches!(self.peek(), Token::PipeForward) {
6532            if crate::compat_mode() {
6533                return Err(self.syntax_err(
6534                    "pipe-forward operator `|>` is a stryke extension (disabled by --compat)",
6535                    left.line,
6536                ));
6537            }
6538            let line = left.line;
6539            self.advance();
6540            // Set pipe-RHS context so list-taking builtins (`map`, `grep`,
6541            // `join`, …) accept a placeholder in place of their list operand.
6542            self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_add(1);
6543            // RHS of `|>` parses at the same precedence as the LHS — see
6544            // the comment at the top of `parse_pipe_forward` for why this
6545            // descends into `parse_range` instead of `parse_or_word`.
6546            let right_result = self.parse_range();
6547            self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_sub(1);
6548            let right = right_result?;
6549            left = self.pipe_forward_apply(left, right, line)?;
6550        }
6551        Ok(left)
6552    }
6553
6554    /// Desugar `lhs |> rhs`: thread `lhs` into the call that `rhs` represents as
6555    /// its **first** argument (Elixir / R / proposed-JS convention).
6556    ///
6557    /// The strategy depends on the shape of `rhs`:
6558    /// - Generic calls (`FuncCall`, `MethodCall`, `IndirectCall`) and variadic
6559    ///   builtins (`Print`, `Say`, `Printf`, `Die`, `Warn`, `Sprintf`, `System`,
6560    ///   `Exec`, `Unlink`, `Chmod`, `Chown`, `Glob`, …) — **prepend** `lhs` to
6561    ///   the args list. So `URL |> json_jq ".[]"` → `json_jq(URL, ".[]")`,
6562    ///   matching the `(data, filter)` signature the builtin expects.
6563    /// - Unary-style builtins (`Length`, `Abs`, `Lc`, `Uc`, `Defined`, `Ref`,
6564    ///   `Keys`, `Values`, `Pop`, `Shift`, …) — **replace** the sole operand with
6565    ///   `lhs` (these parse a single default `$_` when called without an arg, so
6566    ///   piping overrides that default; first-arg and last-arg are identical).
6567    /// - List-taking higher-order forms (`map`, `flat_map`, `grep`, `sort`, `join`, `reduce`, `fold`,
6568    ///   `pmap`, `pflat_map`, `pgrep`, `pfor`, …) — **replace** the `list` field with `lhs`, so
6569    ///   `@arr |> map { $_ * 2 }` becomes `map { $_ * 2 } @arr`.
6570    /// - `Bareword("f")` — lift to `FuncCall { f, [lhs] }`.
6571    /// - Scalar / deref / coderef expressions — wrap in `IndirectCall` with `lhs`
6572    ///   as the sole argument.
6573    /// - Ambiguous forms (binary ops, ternaries, literals, lists) — parse error,
6574    ///   since silently calling a non-callable at runtime would be worse.
6575    fn pipe_forward_apply(&self, lhs: Expr, rhs: Expr, line: usize) -> PerlResult<Expr> {
6576        let Expr { kind, line: rline } = rhs;
6577        let new_kind = match kind {
6578            // ── Generic / user-defined calls ───────────────────────────────────
6579            ExprKind::FuncCall { name, mut args } => {
6580                // Stryke builtins are unprefixed; `CORE::` callers route back to the
6581                // bare-name pipe-forward dispatch below.
6582                let dispatch_name: &str = name.strip_prefix("CORE::").unwrap_or(name.as_str());
6583                match dispatch_name {
6584                    "puniq" | "uniq" | "distinct" | "flatten" | "set" | "list_count"
6585                    | "list_size" | "count" | "size" | "cnt" | "len" | "with_index" | "shuffle"
6586                    | "shuffled" | "frequencies" | "freq" | "pfrequencies" | "pfreq"
6587                    | "interleave" | "ddump" | "stringify" | "str" | "lines" | "words"
6588                    | "chars" | "digits" | "letters" | "letters_uc" | "letters_lc"
6589                    | "punctuation" | "numbers" | "graphemes" | "columns" | "sentences"
6590                    | "paragraphs" | "sections" | "trim" | "avg" | "to_json" | "to_csv"
6591                    | "to_toml" | "to_yaml" | "to_xml" | "to_html" | "from_json" | "from_csv"
6592                    | "from_toml" | "from_yaml" | "from_xml" | "to_markdown" | "to_table"
6593                    | "xopen" | "clip" | "sparkline" | "bar_chart" | "flame" | "stddev"
6594                    | "squared" | "sq" | "square" | "cubed" | "cb" | "cube" | "normalize"
6595                    | "snake_case" | "camel_case" | "kebab_case" => {
6596                        if args.is_empty() {
6597                            args.push(lhs);
6598                        } else {
6599                            args[0] = lhs;
6600                        }
6601                    }
6602                    "chunked" | "windowed" => {
6603                        if args.is_empty() {
6604                            return Err(self.syntax_err(
6605                                "|>: chunked(N) / windowed(N) needs size — e.g. `@a |> windowed(2)`",
6606                                line,
6607                            ));
6608                        }
6609                        args.insert(0, lhs);
6610                    }
6611                    "reduce" | "fold" => {
6612                        args.push(lhs);
6613                    }
6614                    "grep_v" | "pluck" | "tee" | "nth" | "chunk" => {
6615                        // data |> grep_v "pattern" → grep_v("pattern", data...)
6616                        // data |> pluck "key" → pluck("key", data...)
6617                        // data |> tee "file" → tee("file", data...)
6618                        // data |> nth N → nth(N, data...)
6619                        // data |> chunk N → chunk(N, data...)
6620                        args.push(lhs);
6621                    }
6622                    "enumerate" | "dedup" => {
6623                        // data |> enumerate → enumerate(data)
6624                        // data |> dedup → dedup(data)
6625                        args.insert(0, lhs);
6626                    }
6627                    "clamp" => {
6628                        // data |> clamp MIN, MAX → clamp(MIN, MAX, data...)
6629                        args.push(lhs);
6630                    }
6631                    n if Self::is_block_then_list_pipe_builtin(n) => {
6632                        if args.len() < 2 {
6633                            return Err(self.syntax_err(
6634                                format!(
6635                                    "|>: `{name}` needs {{ BLOCK }}, LIST so the list can receive the pipe"
6636                                ),
6637                                line,
6638                            ));
6639                        }
6640                        args[1] = lhs;
6641                    }
6642                    "take" | "head" | "tail" | "drop" => {
6643                        if args.is_empty() {
6644                            return Err(self.syntax_err(
6645                                "|>: `{name}` needs N last — e.g. `@a |> take(3)` for `take(@a, 3)`",
6646                                line,
6647                            ));
6648                        }
6649                        // `LIST |> take N` → `take(LIST, N)` (prepend piped list before trailing count)
6650                        args.insert(0, lhs);
6651                    }
6652                    _ => {
6653                        if self.thread_last_mode {
6654                            args.push(lhs);
6655                        } else {
6656                            args.insert(0, lhs);
6657                        }
6658                    }
6659                }
6660                ExprKind::FuncCall { name, args }
6661            }
6662            ExprKind::MethodCall {
6663                object,
6664                method,
6665                mut args,
6666                super_call,
6667            } => {
6668                if self.thread_last_mode {
6669                    args.push(lhs);
6670                } else {
6671                    args.insert(0, lhs);
6672                }
6673                ExprKind::MethodCall {
6674                    object,
6675                    method,
6676                    args,
6677                    super_call,
6678                }
6679            }
6680            ExprKind::IndirectCall {
6681                target,
6682                mut args,
6683                ampersand,
6684                pass_caller_arglist: _,
6685            } => {
6686                if self.thread_last_mode {
6687                    args.push(lhs);
6688                } else {
6689                    args.insert(0, lhs);
6690                }
6691                ExprKind::IndirectCall {
6692                    target,
6693                    args,
6694                    ampersand,
6695                    // Prepending an explicit first arg means this is no longer
6696                    // "pass the caller's @_" — that form is only bare `&$cr`.
6697                    pass_caller_arglist: false,
6698                }
6699            }
6700
6701            // ── Print-like / diagnostic ops (variadic) ─────────────────────────
6702            ExprKind::Print { handle, mut args } => {
6703                if self.thread_last_mode {
6704                    args.push(lhs);
6705                } else {
6706                    args.insert(0, lhs);
6707                }
6708                ExprKind::Print { handle, args }
6709            }
6710            ExprKind::Say { handle, mut args } => {
6711                if self.thread_last_mode {
6712                    args.push(lhs);
6713                } else {
6714                    args.insert(0, lhs);
6715                }
6716                ExprKind::Say { handle, args }
6717            }
6718            ExprKind::Printf { handle, mut args } => {
6719                if self.thread_last_mode {
6720                    args.push(lhs);
6721                } else {
6722                    args.insert(0, lhs);
6723                }
6724                ExprKind::Printf { handle, args }
6725            }
6726            ExprKind::Die(mut args) => {
6727                if self.thread_last_mode {
6728                    args.push(lhs);
6729                } else {
6730                    args.insert(0, lhs);
6731                }
6732                ExprKind::Die(args)
6733            }
6734            ExprKind::Warn(mut args) => {
6735                if self.thread_last_mode {
6736                    args.push(lhs);
6737                } else {
6738                    args.insert(0, lhs);
6739                }
6740                ExprKind::Warn(args)
6741            }
6742
6743            // ── Sprintf: first-arg pipe threads lhs into the `format` slot ─────
6744            //   `"n=%d" |> sprintf(42)` → `sprintf("n=%d", 42)` is awkward,
6745            //   but piping the format string is the rarer case. Prepending
6746            //   to the values list gives `sprintf(format, lhs, ...args)` for
6747            //   the common `$n |> sprintf "count=%d"` case.
6748            ExprKind::Sprintf { format, mut args } => {
6749                if self.thread_last_mode {
6750                    args.push(lhs);
6751                } else {
6752                    args.insert(0, lhs);
6753                }
6754                ExprKind::Sprintf { format, args }
6755            }
6756
6757            // ── System / exec / globbing / filesystem variadics ────────────────
6758            ExprKind::System(mut args) => {
6759                if self.thread_last_mode {
6760                    args.push(lhs);
6761                } else {
6762                    args.insert(0, lhs);
6763                }
6764                ExprKind::System(args)
6765            }
6766            ExprKind::Exec(mut args) => {
6767                if self.thread_last_mode {
6768                    args.push(lhs);
6769                } else {
6770                    args.insert(0, lhs);
6771                }
6772                ExprKind::Exec(args)
6773            }
6774            ExprKind::Unlink(mut args) => {
6775                if self.thread_last_mode {
6776                    args.push(lhs);
6777                } else {
6778                    args.insert(0, lhs);
6779                }
6780                ExprKind::Unlink(args)
6781            }
6782            ExprKind::Chmod(mut args) => {
6783                if self.thread_last_mode {
6784                    args.push(lhs);
6785                } else {
6786                    args.insert(0, lhs);
6787                }
6788                ExprKind::Chmod(args)
6789            }
6790            ExprKind::Chown(mut args) => {
6791                if self.thread_last_mode {
6792                    args.push(lhs);
6793                } else {
6794                    args.insert(0, lhs);
6795                }
6796                ExprKind::Chown(args)
6797            }
6798            ExprKind::Glob(mut args) => {
6799                if self.thread_last_mode {
6800                    args.push(lhs);
6801                } else {
6802                    args.insert(0, lhs);
6803                }
6804                ExprKind::Glob(args)
6805            }
6806            ExprKind::Files(mut args) => {
6807                if self.thread_last_mode {
6808                    args.push(lhs);
6809                } else {
6810                    args.insert(0, lhs);
6811                }
6812                ExprKind::Files(args)
6813            }
6814            ExprKind::Filesf(mut args) => {
6815                if self.thread_last_mode {
6816                    args.push(lhs);
6817                } else {
6818                    args.insert(0, lhs);
6819                }
6820                ExprKind::Filesf(args)
6821            }
6822            ExprKind::FilesfRecursive(mut args) => {
6823                if self.thread_last_mode {
6824                    args.push(lhs);
6825                } else {
6826                    args.insert(0, lhs);
6827                }
6828                ExprKind::FilesfRecursive(args)
6829            }
6830            ExprKind::Dirs(mut args) => {
6831                if self.thread_last_mode {
6832                    args.push(lhs);
6833                } else {
6834                    args.insert(0, lhs);
6835                }
6836                ExprKind::Dirs(args)
6837            }
6838            ExprKind::DirsRecursive(mut args) => {
6839                if self.thread_last_mode {
6840                    args.push(lhs);
6841                } else {
6842                    args.insert(0, lhs);
6843                }
6844                ExprKind::DirsRecursive(args)
6845            }
6846            ExprKind::SymLinks(mut args) => {
6847                if self.thread_last_mode {
6848                    args.push(lhs);
6849                } else {
6850                    args.insert(0, lhs);
6851                }
6852                ExprKind::SymLinks(args)
6853            }
6854            ExprKind::Sockets(mut args) => {
6855                if self.thread_last_mode {
6856                    args.push(lhs);
6857                } else {
6858                    args.insert(0, lhs);
6859                }
6860                ExprKind::Sockets(args)
6861            }
6862            ExprKind::Pipes(mut args) => {
6863                if self.thread_last_mode {
6864                    args.push(lhs);
6865                } else {
6866                    args.insert(0, lhs);
6867                }
6868                ExprKind::Pipes(args)
6869            }
6870            ExprKind::BlockDevices(mut args) => {
6871                if self.thread_last_mode {
6872                    args.push(lhs);
6873                } else {
6874                    args.insert(0, lhs);
6875                }
6876                ExprKind::BlockDevices(args)
6877            }
6878            ExprKind::CharDevices(mut args) => {
6879                if self.thread_last_mode {
6880                    args.push(lhs);
6881                } else {
6882                    args.insert(0, lhs);
6883                }
6884                ExprKind::CharDevices(args)
6885            }
6886            ExprKind::GlobPar { mut args, progress } => {
6887                if self.thread_last_mode {
6888                    args.push(lhs);
6889                } else {
6890                    args.insert(0, lhs);
6891                }
6892                ExprKind::GlobPar { args, progress }
6893            }
6894            ExprKind::ParSed { mut args, progress } => {
6895                if self.thread_last_mode {
6896                    args.push(lhs);
6897                } else {
6898                    args.insert(0, lhs);
6899                }
6900                ExprKind::ParSed { args, progress }
6901            }
6902
6903            // ── Unary-style builtins: replace the lone operand with `lhs` ──────
6904            ExprKind::Length(_) => ExprKind::Length(Box::new(lhs)),
6905            ExprKind::Abs(_) => ExprKind::Abs(Box::new(lhs)),
6906            ExprKind::Int(_) => ExprKind::Int(Box::new(lhs)),
6907            ExprKind::Sqrt(_) => ExprKind::Sqrt(Box::new(lhs)),
6908            ExprKind::Sin(_) => ExprKind::Sin(Box::new(lhs)),
6909            ExprKind::Cos(_) => ExprKind::Cos(Box::new(lhs)),
6910            ExprKind::Exp(_) => ExprKind::Exp(Box::new(lhs)),
6911            ExprKind::Log(_) => ExprKind::Log(Box::new(lhs)),
6912            ExprKind::Hex(_) => ExprKind::Hex(Box::new(lhs)),
6913            ExprKind::Oct(_) => ExprKind::Oct(Box::new(lhs)),
6914            ExprKind::Lc(_) => ExprKind::Lc(Box::new(lhs)),
6915            ExprKind::Uc(_) => ExprKind::Uc(Box::new(lhs)),
6916            ExprKind::Lcfirst(_) => ExprKind::Lcfirst(Box::new(lhs)),
6917            ExprKind::Ucfirst(_) => ExprKind::Ucfirst(Box::new(lhs)),
6918            ExprKind::Fc(_) => ExprKind::Fc(Box::new(lhs)),
6919            ExprKind::Chr(_) => ExprKind::Chr(Box::new(lhs)),
6920            ExprKind::Ord(_) => ExprKind::Ord(Box::new(lhs)),
6921            ExprKind::Chomp(_) => ExprKind::Chomp(Box::new(lhs)),
6922            ExprKind::Chop(_) => ExprKind::Chop(Box::new(lhs)),
6923            ExprKind::Defined(_) => ExprKind::Defined(Box::new(lhs)),
6924            ExprKind::Ref(_) => ExprKind::Ref(Box::new(lhs)),
6925            ExprKind::ScalarContext(_) => ExprKind::ScalarContext(Box::new(lhs)),
6926            ExprKind::Keys(_) => ExprKind::Keys(Box::new(lhs)),
6927            ExprKind::Values(_) => ExprKind::Values(Box::new(lhs)),
6928            ExprKind::Each(_) => ExprKind::Each(Box::new(lhs)),
6929            ExprKind::Pop(_) => ExprKind::Pop(Box::new(lhs)),
6930            ExprKind::Shift(_) => ExprKind::Shift(Box::new(lhs)),
6931            ExprKind::Delete(_) => ExprKind::Delete(Box::new(lhs)),
6932            ExprKind::Exists(_) => ExprKind::Exists(Box::new(lhs)),
6933            ExprKind::ReverseExpr(_) => ExprKind::ReverseExpr(Box::new(lhs)),
6934            ExprKind::Rev(_) => ExprKind::Rev(Box::new(lhs)),
6935            ExprKind::Slurp(_) => ExprKind::Slurp(Box::new(lhs)),
6936            ExprKind::Capture(_) => ExprKind::Capture(Box::new(lhs)),
6937            ExprKind::Qx(_) => ExprKind::Qx(Box::new(lhs)),
6938            ExprKind::FetchUrl(_) => ExprKind::FetchUrl(Box::new(lhs)),
6939            ExprKind::Close(_) => ExprKind::Close(Box::new(lhs)),
6940            ExprKind::Chdir(_) => ExprKind::Chdir(Box::new(lhs)),
6941            ExprKind::Readdir(_) => ExprKind::Readdir(Box::new(lhs)),
6942            ExprKind::Closedir(_) => ExprKind::Closedir(Box::new(lhs)),
6943            ExprKind::Rewinddir(_) => ExprKind::Rewinddir(Box::new(lhs)),
6944            ExprKind::Telldir(_) => ExprKind::Telldir(Box::new(lhs)),
6945            ExprKind::Stat(_) => ExprKind::Stat(Box::new(lhs)),
6946            ExprKind::Lstat(_) => ExprKind::Lstat(Box::new(lhs)),
6947            ExprKind::Readlink(_) => ExprKind::Readlink(Box::new(lhs)),
6948            ExprKind::Study(_) => ExprKind::Study(Box::new(lhs)),
6949            ExprKind::Await(_) => ExprKind::Await(Box::new(lhs)),
6950            ExprKind::Eval(_) => ExprKind::Eval(Box::new(lhs)),
6951            ExprKind::Rand(_) => ExprKind::Rand(Some(Box::new(lhs))),
6952            ExprKind::Srand(_) => ExprKind::Srand(Some(Box::new(lhs))),
6953            ExprKind::Pos(_) => ExprKind::Pos(Some(Box::new(lhs))),
6954            ExprKind::Exit(_) => ExprKind::Exit(Some(Box::new(lhs))),
6955
6956            // ── Higher-order / list-taking forms: replace the `list` slot ──────
6957            ExprKind::MapExpr {
6958                block,
6959                list: _,
6960                flatten_array_refs,
6961                stream,
6962            } => ExprKind::MapExpr {
6963                block,
6964                list: Box::new(lhs),
6965                flatten_array_refs,
6966                stream,
6967            },
6968            ExprKind::MapExprComma {
6969                expr,
6970                list: _,
6971                flatten_array_refs,
6972                stream,
6973            } => ExprKind::MapExprComma {
6974                expr,
6975                list: Box::new(lhs),
6976                flatten_array_refs,
6977                stream,
6978            },
6979            ExprKind::GrepExpr {
6980                block,
6981                list: _,
6982                keyword,
6983            } => ExprKind::GrepExpr {
6984                block,
6985                list: Box::new(lhs),
6986                keyword,
6987            },
6988            ExprKind::GrepExprComma {
6989                expr,
6990                list: _,
6991                keyword,
6992            } => ExprKind::GrepExprComma {
6993                expr,
6994                list: Box::new(lhs),
6995                keyword,
6996            },
6997            ExprKind::ForEachExpr { block, list: _ } => ExprKind::ForEachExpr {
6998                block,
6999                list: Box::new(lhs),
7000            },
7001            ExprKind::SortExpr { cmp, list: _ } => ExprKind::SortExpr {
7002                cmp,
7003                list: Box::new(lhs),
7004            },
7005            ExprKind::JoinExpr { separator, list: _ } => ExprKind::JoinExpr {
7006                separator,
7007                list: Box::new(lhs),
7008            },
7009            ExprKind::ReduceExpr { block, list: _ } => ExprKind::ReduceExpr {
7010                block,
7011                list: Box::new(lhs),
7012            },
7013            ExprKind::PMapExpr {
7014                block,
7015                list: _,
7016                progress,
7017                flat_outputs,
7018                on_cluster,
7019                stream,
7020            } => ExprKind::PMapExpr {
7021                block,
7022                list: Box::new(lhs),
7023                progress,
7024                flat_outputs,
7025                on_cluster,
7026                stream,
7027            },
7028            ExprKind::ParExpr { block, list: _ } => ExprKind::ParExpr {
7029                block,
7030                list: Box::new(lhs),
7031            },
7032            ExprKind::ParReduceExpr {
7033                extract_block,
7034                reduce_block,
7035                list: _,
7036            } => ExprKind::ParReduceExpr {
7037                extract_block,
7038                reduce_block,
7039                list: Box::new(lhs),
7040            },
7041            ExprKind::PMapChunkedExpr {
7042                chunk_size,
7043                block,
7044                list: _,
7045                progress,
7046            } => ExprKind::PMapChunkedExpr {
7047                chunk_size,
7048                block,
7049                list: Box::new(lhs),
7050                progress,
7051            },
7052            ExprKind::PGrepExpr {
7053                block,
7054                list: _,
7055                progress,
7056                stream,
7057            } => ExprKind::PGrepExpr {
7058                block,
7059                list: Box::new(lhs),
7060                progress,
7061                stream,
7062            },
7063            ExprKind::PForExpr {
7064                block,
7065                list: _,
7066                progress,
7067            } => ExprKind::PForExpr {
7068                block,
7069                list: Box::new(lhs),
7070                progress,
7071            },
7072            ExprKind::PSortExpr {
7073                cmp,
7074                list: _,
7075                progress,
7076            } => ExprKind::PSortExpr {
7077                cmp,
7078                list: Box::new(lhs),
7079                progress,
7080            },
7081            ExprKind::PReduceExpr {
7082                block,
7083                list: _,
7084                progress,
7085            } => ExprKind::PReduceExpr {
7086                block,
7087                list: Box::new(lhs),
7088                progress,
7089            },
7090            ExprKind::PcacheExpr {
7091                block,
7092                list: _,
7093                progress,
7094            } => ExprKind::PcacheExpr {
7095                block,
7096                list: Box::new(lhs),
7097                progress,
7098            },
7099            ExprKind::PReduceInitExpr {
7100                init,
7101                block,
7102                list: _,
7103                progress,
7104            } => ExprKind::PReduceInitExpr {
7105                init,
7106                block,
7107                list: Box::new(lhs),
7108                progress,
7109            },
7110            ExprKind::PMapReduceExpr {
7111                map_block,
7112                reduce_block,
7113                list: _,
7114                progress,
7115            } => ExprKind::PMapReduceExpr {
7116                map_block,
7117                reduce_block,
7118                list: Box::new(lhs),
7119                progress,
7120            },
7121
7122            // ── Push / unshift: first arg is the array, so pipe the LHS
7123            //     into the **values** list — `"x" |> push(@arr)` → `push @arr, "x"`
7124            //     is unchanged, but `@arr |> push "x"` is unnatural; use push
7125            //     directly for that.
7126            ExprKind::Push { array, mut values } => {
7127                values.insert(0, lhs);
7128                ExprKind::Push { array, values }
7129            }
7130            ExprKind::Unshift { array, mut values } => {
7131                values.insert(0, lhs);
7132                ExprKind::Unshift { array, values }
7133            }
7134
7135            // ── Split: pipe the subject string — `$line |> split /,/` ─────────
7136            ExprKind::SplitExpr {
7137                pattern,
7138                string: _,
7139                limit,
7140            } => ExprKind::SplitExpr {
7141                pattern,
7142                string: Box::new(lhs),
7143                limit,
7144            },
7145
7146            // ── Regex ops: pipe the subject — `$str |> s/\n//g` ────────────────
7147            //    Auto-inject `r` flag so the substitution returns the modified
7148            //    string instead of the match count (non-destructive / Perl /r).
7149            ExprKind::Substitution {
7150                pattern,
7151                replacement,
7152                mut flags,
7153                expr: _,
7154                delim,
7155            } => {
7156                if !flags.contains('r') {
7157                    flags.push('r');
7158                }
7159                ExprKind::Substitution {
7160                    expr: Box::new(lhs),
7161                    pattern,
7162                    replacement,
7163                    flags,
7164                    delim,
7165                }
7166            }
7167            ExprKind::Transliterate {
7168                from,
7169                to,
7170                mut flags,
7171                expr: _,
7172                delim,
7173            } => {
7174                if !flags.contains('r') {
7175                    flags.push('r');
7176                }
7177                ExprKind::Transliterate {
7178                    expr: Box::new(lhs),
7179                    from,
7180                    to,
7181                    flags,
7182                    delim,
7183                }
7184            }
7185            ExprKind::Match {
7186                pattern,
7187                flags,
7188                scalar_g,
7189                expr: _,
7190                delim,
7191            } => ExprKind::Match {
7192                expr: Box::new(lhs),
7193                pattern,
7194                flags,
7195                scalar_g,
7196                delim,
7197            },
7198            // Bare `/regex/` (no explicit `m`): promote to Match on piped LHS
7199            ExprKind::Regex(pattern, flags) => ExprKind::Match {
7200                expr: Box::new(lhs),
7201                pattern,
7202                flags,
7203                scalar_g: false,
7204                delim: '/',
7205            },
7206
7207            // ── Bareword function name → plain unary call ──────────────────────
7208            ExprKind::Bareword(name) => match name.as_str() {
7209                "reverse" => {
7210                    if crate::no_interop_mode() {
7211                        return Err(self.syntax_err(
7212                            "stryke uses `rev` instead of `reverse` (--no-interop)",
7213                            line,
7214                        ));
7215                    }
7216                    ExprKind::ReverseExpr(Box::new(lhs))
7217                }
7218                "rv" | "reversed" | "rev" => ExprKind::Rev(Box::new(lhs)),
7219                "uq" | "uniq" | "distinct" => ExprKind::FuncCall {
7220                    name: "uniq".to_string(),
7221                    args: vec![lhs],
7222                },
7223                "fl" | "flatten" => ExprKind::FuncCall {
7224                    name: "flatten".to_string(),
7225                    args: vec![lhs],
7226                },
7227                _ => ExprKind::FuncCall {
7228                    name,
7229                    args: vec![lhs],
7230                },
7231            },
7232
7233            // ── Callable scalars / coderefs / derefs → IndirectCall ────────────
7234            kind @ (ExprKind::ScalarVar(_)
7235            | ExprKind::ArrayElement { .. }
7236            | ExprKind::HashElement { .. }
7237            | ExprKind::Deref { .. }
7238            | ExprKind::ArrowDeref { .. }
7239            | ExprKind::CodeRef { .. }
7240            | ExprKind::SubroutineRef(_)
7241            | ExprKind::SubroutineCodeRef(_)
7242            | ExprKind::DynamicSubCodeRef(_)) => ExprKind::IndirectCall {
7243                target: Box::new(Expr { kind, line: rline }),
7244                args: vec![lhs],
7245                ampersand: false,
7246                pass_caller_arglist: false,
7247            },
7248
7249            // `LHS |> >{ BLOCK }` — the `>{}` form is parsed everywhere as `Do(CodeRef)` (IIFE).
7250            // On the RHS of `|>` we want pipe-apply semantics instead: unwrap the Do and invoke
7251            // the inner coderef with `lhs` as `$_[0]`, matching `LHS |> fn { ... }`.
7252            ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. }) => {
7253                ExprKind::IndirectCall {
7254                    target: inner,
7255                    args: vec![lhs],
7256                    ampersand: false,
7257                    pass_caller_arglist: false,
7258                }
7259            }
7260
7261            other => {
7262                return Err(self.syntax_err(
7263                    format!(
7264                        "right-hand side of `|>` must be a call, builtin, or coderef \
7265                         expression (got {})",
7266                        Self::expr_kind_name(&other)
7267                    ),
7268                    line,
7269                ));
7270            }
7271        };
7272        Ok(Expr {
7273            kind: new_kind,
7274            line,
7275        })
7276    }
7277
7278    /// Short label for an `ExprKind` (used in `|>` error messages).
7279    fn expr_kind_name(kind: &ExprKind) -> &'static str {
7280        match kind {
7281            ExprKind::Integer(_) | ExprKind::Float(_) => "numeric literal",
7282            ExprKind::String(_) | ExprKind::InterpolatedString(_) => "string literal",
7283            ExprKind::BinOp { .. } => "binary expression",
7284            ExprKind::UnaryOp { .. } => "unary expression",
7285            ExprKind::Ternary { .. } => "ternary expression",
7286            ExprKind::Assign { .. } | ExprKind::CompoundAssign { .. } => "assignment",
7287            ExprKind::List(_) => "list expression",
7288            ExprKind::Range { .. } => "range expression",
7289            _ => "expression",
7290        }
7291    }
7292
7293    // or / not (lowest precedence word operators)
7294    fn parse_or_word(&mut self) -> PerlResult<Expr> {
7295        let mut left = self.parse_and_word()?;
7296        while matches!(self.peek(), Token::LogOrWord) {
7297            let line = left.line;
7298            self.advance();
7299            let right = self.parse_and_word()?;
7300            left = Expr {
7301                kind: ExprKind::BinOp {
7302                    left: Box::new(left),
7303                    op: BinOp::LogOrWord,
7304                    right: Box::new(right),
7305                },
7306                line,
7307            };
7308        }
7309        Ok(left)
7310    }
7311
7312    fn parse_and_word(&mut self) -> PerlResult<Expr> {
7313        let mut left = self.parse_not_word()?;
7314        while matches!(self.peek(), Token::LogAndWord) {
7315            let line = left.line;
7316            self.advance();
7317            let right = self.parse_not_word()?;
7318            left = Expr {
7319                kind: ExprKind::BinOp {
7320                    left: Box::new(left),
7321                    op: BinOp::LogAndWord,
7322                    right: Box::new(right),
7323                },
7324                line,
7325            };
7326        }
7327        Ok(left)
7328    }
7329
7330    fn parse_not_word(&mut self) -> PerlResult<Expr> {
7331        if matches!(self.peek(), Token::LogNotWord) {
7332            let line = self.peek_line();
7333            self.advance();
7334            let expr = self.parse_not_word()?;
7335            return Ok(Expr {
7336                kind: ExprKind::UnaryOp {
7337                    op: UnaryOp::LogNotWord,
7338                    expr: Box::new(expr),
7339                },
7340                line,
7341            });
7342        }
7343        // Descend into assignment level — `not` sits ABOVE `=` in Perl
7344        // precedence, so `not $x = 5` parses as `not ($x = 5)`.
7345        self.parse_assign_expr()
7346    }
7347
7348    fn parse_log_or(&mut self) -> PerlResult<Expr> {
7349        let mut left = self.parse_log_and()?;
7350        loop {
7351            let op = match self.peek() {
7352                Token::LogOr => BinOp::LogOr,
7353                Token::DefinedOr => BinOp::DefinedOr,
7354                _ => break,
7355            };
7356            let line = left.line;
7357            self.advance();
7358            let right = self.parse_log_and()?;
7359            left = Expr {
7360                kind: ExprKind::BinOp {
7361                    left: Box::new(left),
7362                    op,
7363                    right: Box::new(right),
7364                },
7365                line,
7366            };
7367        }
7368        Ok(left)
7369    }
7370
7371    fn parse_log_and(&mut self) -> PerlResult<Expr> {
7372        let mut left = self.parse_bit_or()?;
7373        while matches!(self.peek(), Token::LogAnd) {
7374            let line = left.line;
7375            self.advance();
7376            let right = self.parse_bit_or()?;
7377            left = Expr {
7378                kind: ExprKind::BinOp {
7379                    left: Box::new(left),
7380                    op: BinOp::LogAnd,
7381                    right: Box::new(right),
7382                },
7383                line,
7384            };
7385        }
7386        Ok(left)
7387    }
7388
7389    fn parse_bit_or(&mut self) -> PerlResult<Expr> {
7390        let mut left = self.parse_bit_xor()?;
7391        while matches!(self.peek(), Token::BitOr) {
7392            let line = left.line;
7393            self.advance();
7394            let right = self.parse_bit_xor()?;
7395            left = Expr {
7396                kind: ExprKind::BinOp {
7397                    left: Box::new(left),
7398                    op: BinOp::BitOr,
7399                    right: Box::new(right),
7400                },
7401                line,
7402            };
7403        }
7404        Ok(left)
7405    }
7406
7407    fn parse_bit_xor(&mut self) -> PerlResult<Expr> {
7408        let mut left = self.parse_bit_and()?;
7409        while matches!(self.peek(), Token::BitXor) {
7410            let line = left.line;
7411            self.advance();
7412            let right = self.parse_bit_and()?;
7413            left = Expr {
7414                kind: ExprKind::BinOp {
7415                    left: Box::new(left),
7416                    op: BinOp::BitXor,
7417                    right: Box::new(right),
7418                },
7419                line,
7420            };
7421        }
7422        Ok(left)
7423    }
7424
7425    fn parse_bit_and(&mut self) -> PerlResult<Expr> {
7426        let mut left = self.parse_equality()?;
7427        while matches!(self.peek(), Token::BitAnd) {
7428            let line = left.line;
7429            self.advance();
7430            let right = self.parse_equality()?;
7431            left = Expr {
7432                kind: ExprKind::BinOp {
7433                    left: Box::new(left),
7434                    op: BinOp::BitAnd,
7435                    right: Box::new(right),
7436                },
7437                line,
7438            };
7439        }
7440        Ok(left)
7441    }
7442
7443    fn parse_equality(&mut self) -> PerlResult<Expr> {
7444        let mut left = self.parse_comparison()?;
7445        loop {
7446            let op = match self.peek() {
7447                Token::NumEq => BinOp::NumEq,
7448                Token::NumNe => BinOp::NumNe,
7449                Token::StrEq => BinOp::StrEq,
7450                Token::StrNe => BinOp::StrNe,
7451                Token::Spaceship => BinOp::Spaceship,
7452                Token::StrCmp => BinOp::StrCmp,
7453                _ => break,
7454            };
7455            let line = left.line;
7456            self.advance();
7457            let right = self.parse_comparison()?;
7458            left = Expr {
7459                kind: ExprKind::BinOp {
7460                    left: Box::new(left),
7461                    op,
7462                    right: Box::new(right),
7463                },
7464                line,
7465            };
7466        }
7467        Ok(left)
7468    }
7469
7470    fn parse_comparison(&mut self) -> PerlResult<Expr> {
7471        let left = self.parse_shift()?;
7472        let first_op = match self.peek() {
7473            Token::NumLt => BinOp::NumLt,
7474            Token::NumGt => BinOp::NumGt,
7475            Token::NumLe => BinOp::NumLe,
7476            Token::NumGe => BinOp::NumGe,
7477            Token::StrLt => BinOp::StrLt,
7478            Token::StrGt => BinOp::StrGt,
7479            Token::StrLe => BinOp::StrLe,
7480            Token::StrGe => BinOp::StrGe,
7481            _ => return Ok(left),
7482        };
7483        let line = left.line;
7484        self.advance();
7485        let middle = self.parse_shift()?;
7486
7487        let second_op = match self.peek() {
7488            Token::NumLt => Some(BinOp::NumLt),
7489            Token::NumGt => Some(BinOp::NumGt),
7490            Token::NumLe => Some(BinOp::NumLe),
7491            Token::NumGe => Some(BinOp::NumGe),
7492            Token::StrLt => Some(BinOp::StrLt),
7493            Token::StrGt => Some(BinOp::StrGt),
7494            Token::StrLe => Some(BinOp::StrLe),
7495            Token::StrGe => Some(BinOp::StrGe),
7496            _ => None,
7497        };
7498
7499        if second_op.is_none() {
7500            return Ok(Expr {
7501                kind: ExprKind::BinOp {
7502                    left: Box::new(left),
7503                    op: first_op,
7504                    right: Box::new(middle),
7505                },
7506                line,
7507            });
7508        }
7509
7510        // Chained comparison: `a < b < c` → `(a < b) && (b < c)`
7511        // Collect all operands and operators for chains like `1 < x < 10 < y`
7512        let mut operands = vec![left, middle];
7513        let mut ops = vec![first_op];
7514
7515        loop {
7516            let op = match self.peek() {
7517                Token::NumLt => BinOp::NumLt,
7518                Token::NumGt => BinOp::NumGt,
7519                Token::NumLe => BinOp::NumLe,
7520                Token::NumGe => BinOp::NumGe,
7521                Token::StrLt => BinOp::StrLt,
7522                Token::StrGt => BinOp::StrGt,
7523                Token::StrLe => BinOp::StrLe,
7524                Token::StrGe => BinOp::StrGe,
7525                _ => break,
7526            };
7527            self.advance();
7528            ops.push(op);
7529            operands.push(self.parse_shift()?);
7530        }
7531
7532        // Build `(a op0 b) && (b op1 c) && (c op2 d) && ...`
7533        let mut result = Expr {
7534            kind: ExprKind::BinOp {
7535                left: Box::new(operands[0].clone()),
7536                op: ops[0],
7537                right: Box::new(operands[1].clone()),
7538            },
7539            line,
7540        };
7541
7542        for i in 1..ops.len() {
7543            let cmp = Expr {
7544                kind: ExprKind::BinOp {
7545                    left: Box::new(operands[i].clone()),
7546                    op: ops[i],
7547                    right: Box::new(operands[i + 1].clone()),
7548                },
7549                line,
7550            };
7551            result = Expr {
7552                kind: ExprKind::BinOp {
7553                    left: Box::new(result),
7554                    op: BinOp::LogAnd,
7555                    right: Box::new(cmp),
7556                },
7557                line,
7558            };
7559        }
7560
7561        Ok(result)
7562    }
7563
7564    fn parse_shift(&mut self) -> PerlResult<Expr> {
7565        let mut left = self.parse_addition()?;
7566        loop {
7567            let op = match self.peek() {
7568                Token::ShiftLeft => BinOp::ShiftLeft,
7569                Token::ShiftRight => BinOp::ShiftRight,
7570                _ => break,
7571            };
7572            let line = left.line;
7573            self.advance();
7574            let right = self.parse_addition()?;
7575            left = Expr {
7576                kind: ExprKind::BinOp {
7577                    left: Box::new(left),
7578                    op,
7579                    right: Box::new(right),
7580                },
7581                line,
7582            };
7583        }
7584        Ok(left)
7585    }
7586
7587    fn parse_addition(&mut self) -> PerlResult<Expr> {
7588        let mut left = self.parse_multiplication()?;
7589        loop {
7590            // Implicit semicolon: `-` or `+` on a new line is a unary operator on
7591            // the next statement, not a binary operator continuing this expression.
7592            let op = match self.peek() {
7593                Token::Plus if self.peek_line() == self.prev_line() => BinOp::Add,
7594                Token::Minus if self.peek_line() == self.prev_line() => BinOp::Sub,
7595                Token::Dot => BinOp::Concat,
7596                _ => break,
7597            };
7598            let line = left.line;
7599            self.advance();
7600            let right = self.parse_multiplication()?;
7601            left = Expr {
7602                kind: ExprKind::BinOp {
7603                    left: Box::new(left),
7604                    op,
7605                    right: Box::new(right),
7606                },
7607                line,
7608            };
7609        }
7610        Ok(left)
7611    }
7612
7613    fn parse_multiplication(&mut self) -> PerlResult<Expr> {
7614        let mut left = self.parse_regex_bind()?;
7615        loop {
7616            let op = match self.peek() {
7617                Token::Star => BinOp::Mul,
7618                Token::Slash if self.suppress_slash_as_div == 0 => BinOp::Div,
7619                // Implicit semicolon: `%` on a new line is a hash dereference or hash
7620                // sigil for the next statement, not modulo operator on this expression.
7621                Token::Percent if self.peek_line() == self.prev_line() => BinOp::Mod,
7622                Token::X => {
7623                    let line = left.line;
7624                    // List-repeat fires when the LHS was just closed by a
7625                    // list-constructor paren (`(EXPR)`, `(LIST)`, `()`) or
7626                    // `qw(...)`. `parse_primary` records the post-close
7627                    // position; an exact match against `self.pos` here means
7628                    // no postfix consumed any tokens between the close and
7629                    // the `x`, so the LHS is intrinsically a list construct.
7630                    let list_repeat = self.list_construct_close_pos == Some(self.pos);
7631                    self.advance();
7632                    let right = self.parse_regex_bind()?;
7633                    left = Expr {
7634                        kind: ExprKind::Repeat {
7635                            expr: Box::new(left),
7636                            count: Box::new(right),
7637                            list_repeat,
7638                        },
7639                        line,
7640                    };
7641                    continue;
7642                }
7643                _ => break,
7644            };
7645            let line = left.line;
7646            self.advance();
7647            let right = self.parse_regex_bind()?;
7648            left = Expr {
7649                kind: ExprKind::BinOp {
7650                    left: Box::new(left),
7651                    op,
7652                    right: Box::new(right),
7653                },
7654                line,
7655            };
7656        }
7657        Ok(left)
7658    }
7659
7660    fn parse_regex_bind(&mut self) -> PerlResult<Expr> {
7661        let left = self.parse_unary()?;
7662        match self.peek() {
7663            Token::BindMatch => {
7664                let line = left.line;
7665                self.advance();
7666                match self.peek().clone() {
7667                    Token::Regex(pattern, flags, delim) => {
7668                        self.advance();
7669                        Ok(Expr {
7670                            kind: ExprKind::Match {
7671                                expr: Box::new(left),
7672                                pattern,
7673                                flags,
7674                                scalar_g: false,
7675                                delim,
7676                            },
7677                            line,
7678                        })
7679                    }
7680                    Token::Ident(ref s) if s.starts_with('\x00') => {
7681                        let (Token::Ident(encoded), _) = self.advance() else {
7682                            unreachable!()
7683                        };
7684                        let parts: Vec<&str> = encoded.split('\x00').collect();
7685                        if parts.len() >= 4 && parts[1] == "s" {
7686                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
7687                            Ok(Expr {
7688                                kind: ExprKind::Substitution {
7689                                    expr: Box::new(left),
7690                                    pattern: parts[2].to_string(),
7691                                    replacement: parts[3].to_string(),
7692                                    flags: parts.get(4).unwrap_or(&"").to_string(),
7693                                    delim,
7694                                },
7695                                line,
7696                            })
7697                        } else if parts.len() >= 4 && parts[1] == "tr" {
7698                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
7699                            Ok(Expr {
7700                                kind: ExprKind::Transliterate {
7701                                    expr: Box::new(left),
7702                                    from: parts[2].to_string(),
7703                                    to: parts[3].to_string(),
7704                                    flags: parts.get(4).unwrap_or(&"").to_string(),
7705                                    delim,
7706                                },
7707                                line,
7708                            })
7709                        } else {
7710                            Err(self.syntax_err("Invalid regex binding", line))
7711                        }
7712                    }
7713                    _ => {
7714                        let rhs = self.parse_unary()?;
7715                        Ok(Expr {
7716                            kind: ExprKind::BinOp {
7717                                left: Box::new(left),
7718                                op: BinOp::BindMatch,
7719                                right: Box::new(rhs),
7720                            },
7721                            line,
7722                        })
7723                    }
7724                }
7725            }
7726            Token::BindNotMatch => {
7727                let line = left.line;
7728                self.advance();
7729                match self.peek().clone() {
7730                    Token::Regex(pattern, flags, delim) => {
7731                        self.advance();
7732                        Ok(Expr {
7733                            kind: ExprKind::UnaryOp {
7734                                op: UnaryOp::LogNot,
7735                                expr: Box::new(Expr {
7736                                    kind: ExprKind::Match {
7737                                        expr: Box::new(left),
7738                                        pattern,
7739                                        flags,
7740                                        scalar_g: false,
7741                                        delim,
7742                                    },
7743                                    line,
7744                                }),
7745                            },
7746                            line,
7747                        })
7748                    }
7749                    Token::Ident(ref s) if s.starts_with('\x00') => {
7750                        let (Token::Ident(encoded), _) = self.advance() else {
7751                            unreachable!()
7752                        };
7753                        let parts: Vec<&str> = encoded.split('\x00').collect();
7754                        if parts.len() >= 4 && parts[1] == "s" {
7755                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
7756                            Ok(Expr {
7757                                kind: ExprKind::UnaryOp {
7758                                    op: UnaryOp::LogNot,
7759                                    expr: Box::new(Expr {
7760                                        kind: ExprKind::Substitution {
7761                                            expr: Box::new(left),
7762                                            pattern: parts[2].to_string(),
7763                                            replacement: parts[3].to_string(),
7764                                            flags: parts.get(4).unwrap_or(&"").to_string(),
7765                                            delim,
7766                                        },
7767                                        line,
7768                                    }),
7769                                },
7770                                line,
7771                            })
7772                        } else if parts.len() >= 4 && parts[1] == "tr" {
7773                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
7774                            Ok(Expr {
7775                                kind: ExprKind::UnaryOp {
7776                                    op: UnaryOp::LogNot,
7777                                    expr: Box::new(Expr {
7778                                        kind: ExprKind::Transliterate {
7779                                            expr: Box::new(left),
7780                                            from: parts[2].to_string(),
7781                                            to: parts[3].to_string(),
7782                                            flags: parts.get(4).unwrap_or(&"").to_string(),
7783                                            delim,
7784                                        },
7785                                        line,
7786                                    }),
7787                                },
7788                                line,
7789                            })
7790                        } else {
7791                            Err(self.syntax_err("Invalid regex binding after !~", line))
7792                        }
7793                    }
7794                    _ => {
7795                        let rhs = self.parse_unary()?;
7796                        Ok(Expr {
7797                            kind: ExprKind::BinOp {
7798                                left: Box::new(left),
7799                                op: BinOp::BindNotMatch,
7800                                right: Box::new(rhs),
7801                            },
7802                            line,
7803                        })
7804                    }
7805                }
7806            }
7807            _ => Ok(left),
7808        }
7809    }
7810
7811    /// Parse thread macro input. Like `parse_range` but suppresses `/` as division
7812    /// so that `/pattern/` is left for the thread stage parser to handle as regex filter.
7813    fn parse_thread_input(&mut self) -> PerlResult<Expr> {
7814        self.suppress_slash_as_div = self.suppress_slash_as_div.saturating_add(1);
7815        let result = self.parse_range();
7816        self.suppress_slash_as_div = self.suppress_slash_as_div.saturating_sub(1);
7817        result
7818    }
7819
7820    /// Parse `~p>` / `~p>>` parallel-chunk thread-macros. Equivalent to
7821    /// `par_reduce { stage1 |> stage2 |> ... } SOURCE`, with optional
7822    /// `||>` or `|then|` mid-pipeline boundary that switches to a normal
7823    /// `~>` / `~>>` continuation operating on the auto-merged result.
7824    fn parse_thread_macro_chunk_par(&mut self, line: usize, thread_last: bool) -> PerlResult<Expr> {
7825        // Source: same parsing rules as `~>`.
7826        self.suppress_parenless_call = self.suppress_parenless_call.saturating_add(1);
7827        let source_expr = self.parse_thread_input();
7828        self.suppress_parenless_call = self.suppress_parenless_call.saturating_sub(1);
7829        let source_expr = source_expr?;
7830
7831        // Per-chunk stage chain: we want `result` to start as `$_` (topic)
7832        // since the par_reduce runtime binds the chunk to the topic. The
7833        // existing `pipe_rhs_wrap` path inits `result` to `$_[0]`, and
7834        // par_reduce's runtime also declares `@_` = [chunk] for the
7835        // worker invocation, so `$_[0]` == `$_` inside the block.
7836        self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_add(1);
7837        let chunk_chain = self.parse_thread_macro_inner(line, thread_last, None);
7838        self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_sub(1);
7839        let chunk_chain = chunk_chain?;
7840
7841        // `parse_thread_macro_inner` (under pipe_rhs_depth > 0) wraps its
7842        // result as `fn { ... stages applied to $_[0] ... }`. Unwrap to
7843        // get the bare Block (`Vec<Statement>`) for the `par_reduce`
7844        // extract slot.
7845        let extract_block: Block = match chunk_chain.kind {
7846            ExprKind::CodeRef { params: _, body } => body,
7847            _ => vec![Statement {
7848                label: None,
7849                kind: StmtKind::Expression(chunk_chain),
7850                line,
7851            }],
7852        };
7853
7854        let par_reduce = Expr {
7855            kind: ExprKind::ParReduceExpr {
7856                extract_block,
7857                reduce_block: None,
7858                list: Box::new(source_expr),
7859            },
7860            line,
7861        };
7862
7863        // Check for `||>` / `|then|` boundary; if present, parse the
7864        // continuation as a normal `~>` / `~>>` thread macro with the
7865        // par_reduce result as its input.
7866        if self.eat_chunk_par_split_boundary() {
7867            return self.parse_thread_macro_continuation(par_reduce, line, thread_last);
7868        }
7869        Ok(par_reduce)
7870    }
7871
7872    /// Parse a `~>` / `~>>` continuation after a `||>` / `|then|`
7873    /// chunk-parallel-to-sequential boundary. Reuses
7874    /// `parse_thread_macro_inner` with `result_init: Some(prior)` so the
7875    /// stage loop threads from the par_reduce result instead of parsing
7876    /// a fresh source expression.
7877    fn parse_thread_macro_continuation(
7878        &mut self,
7879        prior: Expr,
7880        line: usize,
7881        thread_last: bool,
7882    ) -> PerlResult<Expr> {
7883        self.pending_thread_input = Some(prior);
7884        let res = self.parse_thread_macro_inner(line, thread_last, None);
7885        self.pending_thread_input = None;
7886        res
7887    }
7888
7889    /// Try to consume `||>` (LogOr followed by `>`) or `|then|`
7890    /// (`Pipe Ident("then") Pipe`) as the chunk-parallel → sequential
7891    /// switch marker. Returns true if a boundary was consumed.
7892    fn eat_chunk_par_split_boundary(&mut self) -> bool {
7893        // `||>` = `LogOr` token (already merged in lex) followed by `>`.
7894        if matches!(self.peek(), Token::LogOr) && matches!(self.peek_at(1), Token::NumGt) {
7895            self.advance(); // ||
7896            self.advance(); // >
7897            return true;
7898        }
7899        // `|then|` = `BitOr` + `Ident("then")` + `BitOr`.
7900        if matches!(self.peek(), Token::BitOr) {
7901            if let Token::Ident(name) = self.peek_at(1).clone() {
7902                if name == "then" && matches!(self.peek_at(2), Token::BitOr) {
7903                    self.advance(); // |
7904                    self.advance(); // then
7905                    self.advance(); // |
7906                    return true;
7907                }
7908            }
7909        }
7910        false
7911    }
7912
7913    /// Perl `..` / `...` operator — precedence sits between `?:` and `||` (`perlop`), so
7914    /// `$x .. $x + 3` parses as `$x .. ($x + 3)` and `1..$n||5` parses as `1..($n||5)`. Both
7915    /// operands recurse through `parse_log_or`, which in turn walks down through all tighter
7916    /// operators (additive, multiplicative, regex bind, unary). Non-associative: the right
7917    /// operand is a single `parse_log_or` so `1..5..10` is a parse error in Perl, but we accept
7918    /// it greedily (left-associated) because the lexer already forbids `..` after a range RHS.
7919    fn parse_range(&mut self) -> PerlResult<Expr> {
7920        let left = self.parse_log_or()?;
7921        let line = left.line;
7922        // `1..10` (traditional inclusive) / `1...10` (exclusive) / `1:10`
7923        // (short form) / `1~10` (universal short form). The `~` separator
7924        // works for every range type and is the only viable separator for
7925        // IPv6 since IPv6 already uses `:` internally; `:` would collide.
7926        // It also dodges `!`'s collision with the `_!N!` paired char-index
7927        // syntax. Single-`~` (vs `!!!` triple) keeps the surface simple.
7928        let (exclusive, _colon_style) = if self.eat(&Token::RangeExclusive) {
7929            (true, false)
7930        } else if self.eat(&Token::Range) {
7931            (false, false)
7932        } else if self.suppress_colon_range == 0 && self.eat(&Token::Colon) {
7933            // `1:10` short form — only valid for numeric ranges, not ternary
7934            // Lookahead: must be followed by something that looks like a range endpoint
7935            (false, true)
7936        } else if self.suppress_tilde_range == 0 && self.eat(&Token::BitNot) {
7937            (false, true)
7938        } else {
7939            return Ok(left);
7940        };
7941        let right = self.parse_log_or()?;
7942        // Optional step: `1..100:2` / `1:100:2` / `IPV6~IPV6~STEP`. `~` is
7943        // gated by `suppress_tilde_range` so paired char-index (`$x~5~`)
7944        // doesn't get its closing delimiter eaten as a range op.
7945        let step = if self.eat(&Token::Colon)
7946            || (self.suppress_tilde_range == 0 && self.eat(&Token::BitNot))
7947        {
7948            Some(Box::new(self.parse_unary()?))
7949        } else {
7950            None
7951        };
7952        Ok(Expr {
7953            kind: ExprKind::Range {
7954                from: Box::new(left),
7955                to: Box::new(right),
7956                exclusive,
7957                step,
7958            },
7959            line,
7960        })
7961    }
7962
7963    /// `name` or `Foo::Bar::baz` — used after `sub`, unary `&`, etc.
7964    fn parse_package_qualified_identifier(&mut self) -> PerlResult<String> {
7965        let mut name = match self.advance() {
7966            (Token::Ident(n), _) => n,
7967            (tok, l) => {
7968                return Err(self.syntax_err(format!("Expected identifier, got {:?}", tok), l));
7969            }
7970        };
7971        while self.eat(&Token::PackageSep) {
7972            match self.advance() {
7973                (Token::Ident(part), _) => {
7974                    name.push_str("::");
7975                    name.push_str(&part);
7976                }
7977                // Topic-slot scalars (`_`, `_<<<<`, `_3`, etc.) lex as
7978                // `Token::ScalarVar` per the lexer's reservation. Accept
7979                // them as the trailing segment of a package-qualified
7980                // name so callers (e.g. `parse_sub_decl`) can reject the
7981                // full name with a friendly "would shadow topic-slot"
7982                // message rather than a generic "Expected identifier
7983                // after `::`" lexer-level error.
7984                (Token::ScalarVar(part), _) if Self::is_underscore_topic_slot(&part) => {
7985                    name.push_str("::");
7986                    name.push_str(&part);
7987                }
7988                (tok, l) => {
7989                    return Err(self
7990                        .syntax_err(format!("Expected identifier after `::`, got {:?}", tok), l));
7991                }
7992            }
7993        }
7994        Ok(name)
7995    }
7996
7997    /// After consuming unary `&`: `name` or `Foo::Bar::baz` (Perl `&foo` / `&Foo::bar`).
7998    fn parse_qualified_subroutine_name(&mut self) -> PerlResult<String> {
7999        self.parse_package_qualified_identifier()
8000    }
8001
8002    fn parse_unary(&mut self) -> PerlResult<Expr> {
8003        let line = self.peek_line();
8004        match self.peek().clone() {
8005            Token::Minus => {
8006                self.advance();
8007                let expr = self.parse_power()?;
8008                Ok(Expr {
8009                    kind: ExprKind::UnaryOp {
8010                        op: UnaryOp::Negate,
8011                        expr: Box::new(expr),
8012                    },
8013                    line,
8014                })
8015            }
8016            // Unary `+EXPR` — Perl uses this to disambiguate barewords in hash subscripts (`$h{+Foo}`)
8017            // and for scalar context; treat as a no-op on the parsed operand.
8018            // Special case: `+{ ... }` forces hashref interpretation (Perl idiom),
8019            // even when the body is a list-yielding expression like `+{ map { ... } @arr }`.
8020            // Without this, `{ map { ... } @arr }` falls back to block/CodeRef parsing
8021            // because the body doesn't fit `KEY => VAL` shape.
8022            Token::Plus => {
8023                self.advance();
8024                if matches!(self.peek(), Token::LBrace) {
8025                    let line = self.peek_line();
8026                    self.advance(); // consume {
8027                    return self.parse_forced_hashref_body(line);
8028                }
8029                self.parse_unary()
8030            }
8031            Token::LogNot => {
8032                self.advance();
8033                let expr = self.parse_unary()?;
8034                Ok(Expr {
8035                    kind: ExprKind::UnaryOp {
8036                        op: UnaryOp::LogNot,
8037                        expr: Box::new(expr),
8038                    },
8039                    line,
8040                })
8041            }
8042            Token::BitNot => {
8043                self.advance();
8044                let expr = self.parse_unary()?;
8045                Ok(Expr {
8046                    kind: ExprKind::UnaryOp {
8047                        op: UnaryOp::BitNot,
8048                        expr: Box::new(expr),
8049                    },
8050                    line,
8051                })
8052            }
8053            Token::Increment => {
8054                self.advance();
8055                let expr = self.parse_postfix()?;
8056                Ok(Expr {
8057                    kind: ExprKind::UnaryOp {
8058                        op: UnaryOp::PreIncrement,
8059                        expr: Box::new(expr),
8060                    },
8061                    line,
8062                })
8063            }
8064            Token::Decrement => {
8065                self.advance();
8066                let expr = self.parse_postfix()?;
8067                Ok(Expr {
8068                    kind: ExprKind::UnaryOp {
8069                        op: UnaryOp::PreDecrement,
8070                        expr: Box::new(expr),
8071                    },
8072                    line,
8073                })
8074            }
8075            Token::BitAnd => {
8076                // Unary `&name` / `&Pkg::name` (call / coderef); binary `&` is in `parse_bit_and`.
8077                // `&$coderef(...)` — call sub whose ref is in a scalar (core `B.pm` / `&$recurse($sym)`).
8078                self.advance();
8079                if matches!(self.peek(), Token::LBrace) {
8080                    self.advance();
8081                    let inner = self.parse_expression()?;
8082                    self.expect(&Token::RBrace)?;
8083                    return Ok(Expr {
8084                        kind: ExprKind::DynamicSubCodeRef(Box::new(inner)),
8085                        line,
8086                    });
8087                }
8088                if matches!(self.peek(), Token::Ident(_)) {
8089                    let name = self.parse_qualified_subroutine_name()?;
8090                    return Ok(Expr {
8091                        kind: ExprKind::SubroutineRef(name),
8092                        line,
8093                    });
8094                }
8095                let target = self.parse_primary()?;
8096                if matches!(self.peek(), Token::LParen) {
8097                    self.advance();
8098                    let args = self.parse_arg_list()?;
8099                    self.expect(&Token::RParen)?;
8100                    return Ok(Expr {
8101                        kind: ExprKind::IndirectCall {
8102                            target: Box::new(target),
8103                            args,
8104                            ampersand: true,
8105                            pass_caller_arglist: false,
8106                        },
8107                        line,
8108                    });
8109                }
8110                // `&$coderef` / `&{expr}` with no `(...)` — call with caller's @_ (Perl `&$sub`).
8111                Ok(Expr {
8112                    kind: ExprKind::IndirectCall {
8113                        target: Box::new(target),
8114                        args: vec![],
8115                        ampersand: true,
8116                        pass_caller_arglist: true,
8117                    },
8118                    line,
8119                })
8120            }
8121            Token::Backslash => {
8122                self.advance();
8123                let expr = self.parse_unary()?;
8124                if let ExprKind::SubroutineRef(name) = expr.kind {
8125                    return Ok(Expr {
8126                        kind: ExprKind::SubroutineCodeRef(name),
8127                        line,
8128                    });
8129                }
8130                if matches!(expr.kind, ExprKind::DynamicSubCodeRef(_)) {
8131                    return Ok(expr);
8132                }
8133                // `\` uses `ScalarRef`; array/hash vars and `\@{...}` lower to binding or alias refs.
8134                Ok(Expr {
8135                    kind: ExprKind::ScalarRef(Box::new(expr)),
8136                    line,
8137                })
8138            }
8139            Token::FileTest(op) => {
8140                self.advance();
8141                // Perl: `-d` with no operand uses `$_` (e.g. `if (-d)` inside `for` / `while read`).
8142                let expr = if Self::filetest_allows_implicit_topic(self.peek()) {
8143                    Expr {
8144                        kind: ExprKind::ScalarVar("_".into()),
8145                        line: self.peek_line(),
8146                    }
8147                } else {
8148                    self.parse_unary()?
8149                };
8150                Ok(Expr {
8151                    kind: ExprKind::FileTest {
8152                        op,
8153                        expr: Box::new(expr),
8154                    },
8155                    line,
8156                })
8157            }
8158            _ => self.parse_power(),
8159        }
8160    }
8161
8162    fn parse_power(&mut self) -> PerlResult<Expr> {
8163        let left = self.parse_postfix()?;
8164        if matches!(self.peek(), Token::Power) {
8165            let line = left.line;
8166            self.advance();
8167            let right = self.parse_unary()?; // right-associative
8168            return Ok(Expr {
8169                kind: ExprKind::BinOp {
8170                    left: Box::new(left),
8171                    op: BinOp::Pow,
8172                    right: Box::new(right),
8173                },
8174                line,
8175            });
8176        }
8177        Ok(left)
8178    }
8179
8180    fn parse_postfix(&mut self) -> PerlResult<Expr> {
8181        let mut expr = self.parse_primary()?;
8182        loop {
8183            match self.peek().clone() {
8184                Token::Increment => {
8185                    // Implicit semicolon: `++` on a new line is a prefix operator
8186                    // on the next statement, not postfix on the previous expression.
8187                    if self.peek_line() > self.prev_line() {
8188                        break;
8189                    }
8190                    let line = expr.line;
8191                    self.advance();
8192                    expr = Expr {
8193                        kind: ExprKind::PostfixOp {
8194                            expr: Box::new(expr),
8195                            op: PostfixOp::Increment,
8196                        },
8197                        line,
8198                    };
8199                }
8200                Token::Decrement => {
8201                    // Implicit semicolon: `--` on a new line is a prefix operator
8202                    // on the next statement, not postfix on the previous expression.
8203                    if self.peek_line() > self.prev_line() {
8204                        break;
8205                    }
8206                    let line = expr.line;
8207                    self.advance();
8208                    expr = Expr {
8209                        kind: ExprKind::PostfixOp {
8210                            expr: Box::new(expr),
8211                            op: PostfixOp::Decrement,
8212                        },
8213                        line,
8214                    };
8215                }
8216                Token::LParen => {
8217                    if self.suppress_indirect_paren_call > 0 {
8218                        break;
8219                    }
8220                    // Implicit semicolon: `(` on a new line after an expression
8221                    // is a new statement, not a postfix code-ref call.
8222                    // e.g.  `my $x = $ENV{"KEY"}\n($y =~ s/.../.../)`
8223                    if self.peek_line() > self.prev_line() {
8224                        break;
8225                    }
8226                    let line = expr.line;
8227                    self.advance();
8228                    let args = self.parse_arg_list()?;
8229                    self.expect(&Token::RParen)?;
8230                    expr = Expr {
8231                        kind: ExprKind::IndirectCall {
8232                            target: Box::new(expr),
8233                            args,
8234                            ampersand: false,
8235                            pass_caller_arglist: false,
8236                        },
8237                        line,
8238                    };
8239                }
8240                Token::Arrow => {
8241                    let line = expr.line;
8242                    self.advance();
8243                    match self.peek().clone() {
8244                        Token::LBracket => {
8245                            self.advance();
8246                            let index = self.parse_expression()?;
8247                            self.expect(&Token::RBracket)?;
8248                            expr = Expr {
8249                                kind: ExprKind::ArrowDeref {
8250                                    expr: Box::new(expr),
8251                                    index: Box::new(index),
8252                                    kind: DerefKind::Array,
8253                                },
8254                                line,
8255                            };
8256                        }
8257                        Token::LBrace => {
8258                            self.advance();
8259                            let key = self.parse_hash_subscript_key()?;
8260                            self.expect(&Token::RBrace)?;
8261                            expr = Expr {
8262                                kind: ExprKind::ArrowDeref {
8263                                    expr: Box::new(expr),
8264                                    index: Box::new(key),
8265                                    kind: DerefKind::Hash,
8266                                },
8267                                line,
8268                            };
8269                        }
8270                        Token::LParen => {
8271                            self.advance();
8272                            let args = self.parse_arg_list()?;
8273                            self.expect(&Token::RParen)?;
8274                            expr = Expr {
8275                                kind: ExprKind::ArrowDeref {
8276                                    expr: Box::new(expr),
8277                                    index: Box::new(Expr {
8278                                        kind: ExprKind::List(args),
8279                                        line,
8280                                    }),
8281                                    kind: DerefKind::Call,
8282                                },
8283                                line,
8284                            };
8285                        }
8286                        Token::Ident(method) => {
8287                            self.advance();
8288                            if method == "SUPER" {
8289                                self.expect(&Token::PackageSep)?;
8290                                let real_method = match self.advance() {
8291                                    (Token::Ident(n), _) => n,
8292                                    (tok, l) => {
8293                                        return Err(self.syntax_err(
8294                                            format!(
8295                                                "Expected method name after SUPER::, got {:?}",
8296                                                tok
8297                                            ),
8298                                            l,
8299                                        ));
8300                                    }
8301                                };
8302                                let args = if self.eat(&Token::LParen) {
8303                                    let a = self.parse_arg_list()?;
8304                                    self.expect(&Token::RParen)?;
8305                                    a
8306                                } else {
8307                                    self.parse_method_arg_list_no_paren()?
8308                                };
8309                                expr = Expr {
8310                                    kind: ExprKind::MethodCall {
8311                                        object: Box::new(expr),
8312                                        method: real_method,
8313                                        args,
8314                                        super_call: true,
8315                                    },
8316                                    line,
8317                                };
8318                            } else {
8319                                let mut method_name = method;
8320                                while self.eat(&Token::PackageSep) {
8321                                    match self.advance() {
8322                                        (Token::Ident(part), _) => {
8323                                            method_name.push_str("::");
8324                                            method_name.push_str(&part);
8325                                        }
8326                                        (tok, l) => {
8327                                            return Err(self.syntax_err(
8328                                                format!(
8329                                                    "Expected identifier after :: in method name, got {:?}",
8330                                                    tok
8331                                                ),
8332                                                l,
8333                                            ));
8334                                        }
8335                                    }
8336                                }
8337                                let args = if self.eat(&Token::LParen) {
8338                                    let a = self.parse_arg_list()?;
8339                                    self.expect(&Token::RParen)?;
8340                                    a
8341                                } else {
8342                                    self.parse_method_arg_list_no_paren()?
8343                                };
8344                                expr = Expr {
8345                                    kind: ExprKind::MethodCall {
8346                                        object: Box::new(expr),
8347                                        method: method_name,
8348                                        args,
8349                                        super_call: false,
8350                                    },
8351                                    line,
8352                                };
8353                            }
8354                        }
8355                        // Postfix dereference (Perl 5.20+, default 5.24+):
8356                        //   `$ref->@*`         — full array      ≡ `@{$ref}`
8357                        //   `$ref->@[i,j]`     — array slice     ≡ `@{$ref}[i,j]`
8358                        //   `$ref->@{k,l}`     — hash slice (vals) ≡ `@{$ref}{k,l}`
8359                        //   `$ref->%*`         — full hash       ≡ `%{$ref}`
8360                        Token::ArrayAt => {
8361                            self.advance(); // consume `@`
8362                            match self.peek().clone() {
8363                                Token::Star => {
8364                                    self.advance();
8365                                    expr = Expr {
8366                                        kind: ExprKind::Deref {
8367                                            expr: Box::new(expr),
8368                                            kind: Sigil::Array,
8369                                        },
8370                                        line,
8371                                    };
8372                                }
8373                                Token::LBracket => {
8374                                    self.advance();
8375                                    let indices = self.parse_slice_arg_list(false)?;
8376                                    self.expect(&Token::RBracket)?;
8377                                    let source = Expr {
8378                                        kind: ExprKind::Deref {
8379                                            expr: Box::new(expr),
8380                                            kind: Sigil::Array,
8381                                        },
8382                                        line,
8383                                    };
8384                                    expr = Expr {
8385                                        kind: ExprKind::AnonymousListSlice {
8386                                            source: Box::new(source),
8387                                            indices,
8388                                        },
8389                                        line,
8390                                    };
8391                                }
8392                                Token::LBrace => {
8393                                    self.advance();
8394                                    let keys = self.parse_slice_arg_list(true)?;
8395                                    self.expect(&Token::RBrace)?;
8396                                    expr = Expr {
8397                                        kind: ExprKind::HashSliceDeref {
8398                                            container: Box::new(expr),
8399                                            keys,
8400                                        },
8401                                        line,
8402                                    };
8403                                }
8404                                tok => {
8405                                    return Err(self.syntax_err(
8406                                        format!(
8407                                            "Expected `*`, `[…]`, or `{{…}}` after `->@`, got {:?}",
8408                                            tok
8409                                        ),
8410                                        line,
8411                                    ));
8412                                }
8413                            }
8414                        }
8415                        Token::HashPercent => {
8416                            self.advance(); // consume `%`
8417                            match self.peek().clone() {
8418                                Token::Star => {
8419                                    self.advance();
8420                                    expr = Expr {
8421                                        kind: ExprKind::Deref {
8422                                            expr: Box::new(expr),
8423                                            kind: Sigil::Hash,
8424                                        },
8425                                        line,
8426                                    };
8427                                }
8428                                tok => {
8429                                    return Err(self.syntax_err(
8430                                        format!("Expected `*` after `->%`, got {:?}", tok),
8431                                        line,
8432                                    ));
8433                                }
8434                            }
8435                        }
8436                        // `x` is lexed as `Token::X` (repeat op); after `->` it is a method name.
8437                        Token::X => {
8438                            self.advance();
8439                            let args = if self.eat(&Token::LParen) {
8440                                let a = self.parse_arg_list()?;
8441                                self.expect(&Token::RParen)?;
8442                                a
8443                            } else {
8444                                self.parse_method_arg_list_no_paren()?
8445                            };
8446                            expr = Expr {
8447                                kind: ExprKind::MethodCall {
8448                                    object: Box::new(expr),
8449                                    method: "x".to_string(),
8450                                    args,
8451                                    super_call: false,
8452                                },
8453                                line,
8454                            };
8455                        }
8456                        _ => break,
8457                    }
8458                }
8459                Token::LBracket => {
8460                    // Implicit semicolon: `[` on a new line is a new statement (array literal),
8461                    // not an array subscript on the preceding expression.
8462                    if self.peek_line() > self.prev_line() {
8463                        break;
8464                    }
8465                    // `$a[i]` — or chained `$r->{k}[i]` / `$a[1][2]` — or list slice `(sort ...)[0]`.
8466                    let line = expr.line;
8467                    if matches!(expr.kind, ExprKind::ScalarVar(_)) {
8468                        if let ExprKind::ScalarVar(ref name) = expr.kind {
8469                            let name = name.clone();
8470                            self.advance();
8471                            let index = self.parse_expression()?;
8472                            self.expect(&Token::RBracket)?;
8473                            expr = Expr {
8474                                kind: ExprKind::ArrayElement {
8475                                    array: name,
8476                                    index: Box::new(index),
8477                                },
8478                                line,
8479                            };
8480                        }
8481                    } else if postfix_lbracket_is_arrow_container(&expr) {
8482                        self.advance();
8483                        let indices = self.parse_arg_list()?;
8484                        self.expect(&Token::RBracket)?;
8485                        expr = Expr {
8486                            kind: ExprKind::ArrowDeref {
8487                                expr: Box::new(expr),
8488                                index: Box::new(Expr {
8489                                    kind: ExprKind::List(indices),
8490                                    line,
8491                                }),
8492                                kind: DerefKind::Array,
8493                            },
8494                            line,
8495                        };
8496                    } else {
8497                        self.advance();
8498                        let indices = self.parse_arg_list()?;
8499                        self.expect(&Token::RBracket)?;
8500                        expr = Expr {
8501                            kind: ExprKind::AnonymousListSlice {
8502                                source: Box::new(expr),
8503                                indices,
8504                            },
8505                            line,
8506                        };
8507                    }
8508                }
8509                Token::LBrace => {
8510                    if self.suppress_scalar_hash_brace > 0 {
8511                        break;
8512                    }
8513                    // Implicit semicolon: `{` on a new line is a new statement (block/hashref),
8514                    // not a hash subscript on the preceding expression.
8515                    if self.peek_line() > self.prev_line() {
8516                        break;
8517                    }
8518                    // `$h{k}`, or chained `$h{k2}{k3}` / `$r->{a}{b}` / `$a[0]{k}` — second+ `{…}` is
8519                    // hash subscript on the scalar value (same as `-> { … }` without extra `->`).
8520                    let line = expr.line;
8521                    let is_scalar_named_hash = matches!(expr.kind, ExprKind::ScalarVar(_));
8522                    let is_chainable_hash_subscript = is_scalar_named_hash
8523                        || matches!(
8524                            expr.kind,
8525                            ExprKind::HashElement { .. }
8526                                | ExprKind::ArrayElement { .. }
8527                                | ExprKind::ArrowDeref { .. }
8528                                | ExprKind::Deref {
8529                                    kind: Sigil::Scalar,
8530                                    ..
8531                                }
8532                        );
8533                    if !is_chainable_hash_subscript {
8534                        break;
8535                    }
8536                    self.advance();
8537                    let key = self.parse_hash_subscript_key()?;
8538                    self.expect(&Token::RBrace)?;
8539                    expr = if is_scalar_named_hash {
8540                        if let ExprKind::ScalarVar(ref name) = expr.kind {
8541                            let name = name.clone();
8542                            // Perl: `$_ { k }` means `$_->{k}` (implicit arrow), not the `%_` stash hash.
8543                            if name == "_" {
8544                                Expr {
8545                                    kind: ExprKind::ArrowDeref {
8546                                        expr: Box::new(Expr {
8547                                            kind: ExprKind::ScalarVar("_".into()),
8548                                            line,
8549                                        }),
8550                                        index: Box::new(key),
8551                                        kind: DerefKind::Hash,
8552                                    },
8553                                    line,
8554                                }
8555                            } else {
8556                                Expr {
8557                                    kind: ExprKind::HashElement {
8558                                        hash: name,
8559                                        key: Box::new(key),
8560                                    },
8561                                    line,
8562                                }
8563                            }
8564                        } else {
8565                            unreachable!("is_scalar_named_hash implies ScalarVar");
8566                        }
8567                    } else {
8568                        Expr {
8569                            kind: ExprKind::ArrowDeref {
8570                                expr: Box::new(expr),
8571                                index: Box::new(key),
8572                                kind: DerefKind::Hash,
8573                            },
8574                            line,
8575                        }
8576                    };
8577                }
8578                Token::LogNot | Token::BitNot => {
8579                    // Stryke universal string-subscript sugar — paired `!…!`
8580                    // OR paired `~…~`: `$VAR!N!`, `$VAR~N~`, `$VAR!1:5:2!`,
8581                    // `_!N!`, `_~from:to:step~`. Returns substring of the
8582                    // scalar (Unicode chars).  Distinct from `[N]` which has
8583                    // Perl's `@VAR[N]` / `$_[N]` semantics. Both forms work on
8584                    // any scalar (named or topic) without colliding: `!` and
8585                    // `~` after a value have no current postfix meaning (`!=`
8586                    // / `!~` are pre-merged binary tokens; `~` is prefix-only
8587                    // bit-not). The opening and closing delimiter must match.
8588                    //
8589                    // Implementation: rewrite to ArrayElement with a
8590                    // synthetic name `__topicstr__$NAME`. The interpreter
8591                    // and VM strip the prefix and dispatch to char-of-string
8592                    // (and slice-of-string for Range indices).
8593                    if !matches!(expr.kind, ExprKind::ScalarVar(_)) {
8594                        break;
8595                    }
8596                    if self.peek_line() > self.prev_line() {
8597                        break;
8598                    }
8599                    let opener = self.peek().clone();
8600                    let line = expr.line;
8601                    let name = if let ExprKind::ScalarVar(ref n) = expr.kind {
8602                        n.clone()
8603                    } else {
8604                        unreachable!()
8605                    };
8606                    self.advance(); // consume opening `!` or `~`
8607                                    // Suppress `~` as a range separator while parsing the
8608                                    // paired index — `$_~5~` would otherwise consume the
8609                                    // closing `~` as a range op. `:` is still allowed so
8610                                    // `$_~1:3~` (slice with `:` range index) keeps working.
8611                    self.suppress_tilde_range = self.suppress_tilde_range.saturating_add(1);
8612                    let index_result = self.parse_expression();
8613                    self.suppress_tilde_range = self.suppress_tilde_range.saturating_sub(1);
8614                    let index = index_result?;
8615                    let close_match = matches!(
8616                        (&opener, self.peek()),
8617                        (Token::LogNot, Token::LogNot) | (Token::BitNot, Token::BitNot)
8618                    );
8619                    if !close_match {
8620                        let want = if matches!(opener, Token::LogNot) {
8621                            "!"
8622                        } else {
8623                            "~"
8624                        };
8625                        return Err(self.syntax_err(
8626                            format!("expected closing `{}` for string subscript", want),
8627                            self.peek_line(),
8628                        ));
8629                    }
8630                    self.advance(); // consume closing delimiter
8631                    expr = Expr {
8632                        kind: ExprKind::ArrayElement {
8633                            array: format!("__topicstr__{}", name),
8634                            index: Box::new(index),
8635                        },
8636                        line,
8637                    };
8638                }
8639                _ => break,
8640            }
8641        }
8642        Ok(expr)
8643    }
8644
8645    fn parse_primary(&mut self) -> PerlResult<Expr> {
8646        let line = self.peek_line();
8647        // `my $x = …` (or `our` / `state` / `local`) used inside an expression —
8648        // typically `if (my $x = …)` / `while (my $line = <FH>)`.  Returns the
8649        // assigned value(s); has the side effect of declaring the variable in
8650        // the current scope.  See `ExprKind::MyExpr`.
8651        if let Token::Ident(ref kw) = self.peek().clone() {
8652            if matches!(kw.as_str(), "my" | "our" | "state" | "local") {
8653                let kw_owned = kw.clone();
8654                // Parse exactly like the statement form via `parse_my_our_local`,
8655                // then unwrap the resulting `StmtKind::*` back into a list of
8656                // `VarDecl`s for the expression node.  This re-uses the full
8657                // syntax (typed sigs, list destructuring, type annotations).
8658                let saved_pos = self.pos;
8659                let stmt = self.parse_my_our_local(&kw_owned, false)?;
8660                let decls = match stmt.kind {
8661                    StmtKind::My(d)
8662                    | StmtKind::Our(d)
8663                    | StmtKind::State(d)
8664                    | StmtKind::Local(d) => d,
8665                    _ => {
8666                        // `local *FOO = …` / non-decl forms — fall back to the
8667                        // statement parser (already advanced); restore position
8668                        // and let the surrounding code handle it as a statement
8669                        // by erroring loudly here.
8670                        self.pos = saved_pos;
8671                        return Err(self.syntax_err(
8672                            "`my`/`our`/`local` in expression must declare variables",
8673                            line,
8674                        ));
8675                    }
8676                };
8677                return Ok(Expr {
8678                    kind: ExprKind::MyExpr {
8679                        keyword: kw_owned,
8680                        decls,
8681                    },
8682                    line,
8683                });
8684            }
8685        }
8686        match self.peek().clone() {
8687            Token::Integer(n) => {
8688                self.advance();
8689                Ok(Expr {
8690                    kind: ExprKind::Integer(n),
8691                    line,
8692                })
8693            }
8694            Token::Float(f) => {
8695                self.advance();
8696                Ok(Expr {
8697                    kind: ExprKind::Float(f),
8698                    line,
8699                })
8700            }
8701            // `>{ BLOCK }` — IIFE block expression (immediately-invoked anonymous sub).
8702            // Valid in any expression position; evaluates the block and yields its last value.
8703            // In thread-macro stage position (`EXPR |>` already consumed by the stage loop in
8704            // `parse_thread_macro`), the explicit branch at ~1417 wins and the block is
8705            // instead pipe-applied as a coderef — that path is never reached from here.
8706            Token::ArrowBrace => {
8707                self.advance();
8708                let mut stmts = Vec::new();
8709                while !matches!(self.peek(), Token::RBrace | Token::Eof) {
8710                    if self.eat(&Token::Semicolon) {
8711                        continue;
8712                    }
8713                    stmts.push(self.parse_statement()?);
8714                }
8715                self.expect(&Token::RBrace)?;
8716                let inner_line = stmts.first().map(|s| s.line).unwrap_or(line);
8717                let inner = Expr {
8718                    kind: ExprKind::CodeRef {
8719                        params: vec![],
8720                        body: stmts,
8721                    },
8722                    line: inner_line,
8723                };
8724                Ok(Expr {
8725                    kind: ExprKind::Do(Box::new(inner)),
8726                    line,
8727                })
8728            }
8729            Token::Star => {
8730                self.advance();
8731                if matches!(self.peek(), Token::LBrace) {
8732                    self.advance();
8733                    let inner = self.parse_expression()?;
8734                    self.expect(&Token::RBrace)?;
8735                    return Ok(Expr {
8736                        kind: ExprKind::Deref {
8737                            expr: Box::new(inner),
8738                            kind: Sigil::Typeglob,
8739                        },
8740                        line,
8741                    });
8742                }
8743                // `*$_{$k}`, `*${expr}`, `*$foo` — typeglob from a sigil expression (Perl 5 `*$globref`).
8744                if matches!(
8745                    self.peek(),
8746                    Token::ScalarVar(_)
8747                        | Token::ArrayVar(_)
8748                        | Token::HashVar(_)
8749                        | Token::DerefScalarVar(_)
8750                        | Token::HashPercent
8751                ) {
8752                    let inner = self.parse_postfix()?;
8753                    return Ok(Expr {
8754                        kind: ExprKind::TypeglobExpr(Box::new(inner)),
8755                        line,
8756                    });
8757                }
8758                // `x` tokenizes as `Token::X` (repeat op) — still a valid package/typeglob name.
8759                let mut full_name = match self.advance() {
8760                    (Token::Ident(n), _) => n,
8761                    (Token::X, _) => "x".to_string(),
8762                    (tok, l) => {
8763                        return Err(self
8764                            .syntax_err(format!("Expected identifier after *, got {:?}", tok), l));
8765                    }
8766                };
8767                while self.eat(&Token::PackageSep) {
8768                    match self.advance() {
8769                        (Token::Ident(part), _) => {
8770                            full_name = format!("{}::{}", full_name, part);
8771                        }
8772                        (Token::X, _) => {
8773                            full_name = format!("{}::x", full_name);
8774                        }
8775                        (tok, l) => {
8776                            return Err(self.syntax_err(
8777                                format!("Expected identifier after :: in typeglob, got {:?}", tok),
8778                                l,
8779                            ));
8780                        }
8781                    }
8782                }
8783                Ok(Expr {
8784                    kind: ExprKind::Typeglob(full_name),
8785                    line,
8786                })
8787            }
8788            Token::SingleString(s) => {
8789                self.advance();
8790                Ok(Expr {
8791                    kind: ExprKind::String(s),
8792                    line,
8793                })
8794            }
8795            Token::DoubleString(s) => {
8796                self.advance();
8797                self.parse_interpolated_string(&s, line)
8798            }
8799            Token::BacktickString(s) => {
8800                self.advance();
8801                let inner = self.parse_interpolated_string(&s, line)?;
8802                Ok(Expr {
8803                    kind: ExprKind::Qx(Box::new(inner)),
8804                    line,
8805                })
8806            }
8807            Token::HereDoc(_, body, interpolate) => {
8808                self.advance();
8809                if interpolate {
8810                    self.parse_interpolated_string(&body, line)
8811                } else {
8812                    Ok(Expr {
8813                        kind: ExprKind::String(body),
8814                        line,
8815                    })
8816                }
8817            }
8818            Token::Regex(pattern, flags, _delim) => {
8819                self.advance();
8820                Ok(Expr {
8821                    kind: ExprKind::Regex(pattern, flags),
8822                    line,
8823                })
8824            }
8825            Token::QW(words) => {
8826                self.advance();
8827                // `qw(a b c) x N` is list-repeat in Perl even without explicit
8828                // outer parens — `qw(...)` is itself a list constructor.
8829                self.list_construct_close_pos = Some(self.pos);
8830                Ok(Expr {
8831                    kind: ExprKind::QW(words),
8832                    line,
8833                })
8834            }
8835            Token::DerefScalarVar(name) => {
8836                self.advance();
8837                Ok(Expr {
8838                    kind: ExprKind::Deref {
8839                        expr: Box::new(Expr {
8840                            kind: ExprKind::ScalarVar(name),
8841                            line,
8842                        }),
8843                        kind: Sigil::Scalar,
8844                    },
8845                    line,
8846                })
8847            }
8848            Token::ScalarVar(name) => {
8849                self.advance();
8850                Ok(Expr {
8851                    kind: ExprKind::ScalarVar(name),
8852                    line,
8853                })
8854            }
8855            Token::ArrayVar(name) => {
8856                self.advance();
8857                // Check for slice: @arr[...] (array slice) or @hash{...} (hash slice)
8858                match self.peek() {
8859                    Token::LBracket => {
8860                        self.advance();
8861                        let indices = self.parse_slice_arg_list(false)?;
8862                        self.expect(&Token::RBracket)?;
8863                        Ok(Expr {
8864                            kind: ExprKind::ArraySlice {
8865                                array: name,
8866                                indices,
8867                            },
8868                            line,
8869                        })
8870                    }
8871                    Token::LBrace if self.suppress_scalar_hash_brace == 0 => {
8872                        self.advance();
8873                        let keys = self.parse_slice_arg_list(true)?;
8874                        self.expect(&Token::RBrace)?;
8875                        Ok(Expr {
8876                            kind: ExprKind::HashSlice { hash: name, keys },
8877                            line,
8878                        })
8879                    }
8880                    _ => Ok(Expr {
8881                        kind: ExprKind::ArrayVar(name),
8882                        line,
8883                    }),
8884                }
8885            }
8886            Token::HashVar(name) => {
8887                self.advance();
8888                // `%h{KEYS}` — Perl 5.20+ key-value slice. Parser-level
8889                // disambiguation: `%h` immediately followed by `{` is a kv-
8890                // slice; `%h` alone (or followed by `=`, list ops, etc.) is
8891                // the bare hash. (BUG-008)
8892                if matches!(self.peek(), Token::LBrace) && self.suppress_scalar_hash_brace == 0 {
8893                    self.advance(); // {
8894                    let keys = self.parse_slice_arg_list(true)?;
8895                    self.expect(&Token::RBrace)?;
8896                    return Ok(Expr {
8897                        kind: ExprKind::HashKvSlice { hash: name, keys },
8898                        line,
8899                    });
8900                }
8901                Ok(Expr {
8902                    kind: ExprKind::HashVar(name),
8903                    line,
8904                })
8905            }
8906            Token::HashPercent => {
8907                // `%$href` — hash ref deref; `%{ $expr }` — symbolic / braced form
8908                self.advance();
8909                if matches!(self.peek(), Token::ScalarVar(_)) {
8910                    let n = match self.advance() {
8911                        (Token::ScalarVar(n), _) => n,
8912                        (tok, l) => {
8913                            return Err(self.syntax_err(
8914                                format!("Expected scalar variable after %%, got {:?}", tok),
8915                                l,
8916                            ));
8917                        }
8918                    };
8919                    return Ok(Expr {
8920                        kind: ExprKind::Deref {
8921                            expr: Box::new(Expr {
8922                                kind: ExprKind::ScalarVar(n),
8923                                line,
8924                            }),
8925                            kind: Sigil::Hash,
8926                        },
8927                        line,
8928                    });
8929                }
8930                // `%[a => 1, b => 2]` — sugar for `%{+{a=>1,b=>2}}`: dereference an
8931                // anonymous hashref inline, using `[...]` as the delimiter to avoid
8932                // the block-vs-hashref ambiguity that `%{a=>1}` has in real Perl.
8933                // Real Perl errors on `%[...]` syntactically, so no compat risk.
8934                if matches!(self.peek(), Token::LBracket) {
8935                    self.advance();
8936                    let pairs = self.parse_hashref_pairs_until(&Token::RBracket)?;
8937                    self.expect(&Token::RBracket)?;
8938                    let href = Expr {
8939                        kind: ExprKind::HashRef(pairs),
8940                        line,
8941                    };
8942                    return Ok(Expr {
8943                        kind: ExprKind::Deref {
8944                            expr: Box::new(href),
8945                            kind: Sigil::Hash,
8946                        },
8947                        line,
8948                    });
8949                }
8950                self.expect(&Token::LBrace)?;
8951                // Peek to disambiguate `%{ $ref }` (deref a hashref expression) from
8952                // `%{ k => v }` (inline hash literal). Real Perl's block-vs-hashref
8953                // heuristic is famously unreliable — when the first non-whitespace
8954                // token is an ident/string followed by `=>`, treat the whole thing
8955                // as a hashref literal to make `%{a=>1,b=>2}` work reliably.
8956                let looks_like_pair = matches!(
8957                    self.peek(),
8958                    Token::Ident(_) | Token::SingleString(_) | Token::DoubleString(_)
8959                ) && matches!(self.peek_at(1), Token::FatArrow);
8960                let inner = if looks_like_pair {
8961                    let pairs = self.parse_hashref_pairs_until(&Token::RBrace)?;
8962                    Expr {
8963                        kind: ExprKind::HashRef(pairs),
8964                        line,
8965                    }
8966                } else {
8967                    self.parse_expression()?
8968                };
8969                self.expect(&Token::RBrace)?;
8970                Ok(Expr {
8971                    kind: ExprKind::Deref {
8972                        expr: Box::new(inner),
8973                        kind: Sigil::Hash,
8974                    },
8975                    line,
8976                })
8977            }
8978            Token::ArrayAt => {
8979                self.advance();
8980                // `@{ $expr }` / `@{ "Pkg::NAME" }` — symbolic array (e.g. `@{"$pkg\::EXPORT"}` in Exporter.pm)
8981                if matches!(self.peek(), Token::LBrace) {
8982                    self.advance();
8983                    let inner = self.parse_expression()?;
8984                    self.expect(&Token::RBrace)?;
8985                    return Ok(Expr {
8986                        kind: ExprKind::Deref {
8987                            expr: Box::new(inner),
8988                            kind: Sigil::Array,
8989                        },
8990                        line,
8991                    });
8992                }
8993                // `@[a, b, c]` — sugar for `@{[a, b, c]}`: dereference an
8994                // anonymous arrayref inline. Real Perl rejects `@[...]` at
8995                // the parser level, so this extension has no compat risk.
8996                if matches!(self.peek(), Token::LBracket) {
8997                    self.advance();
8998                    let mut elems = Vec::new();
8999                    if !matches!(self.peek(), Token::RBracket) {
9000                        elems.push(self.parse_assign_expr()?);
9001                        while self.eat(&Token::Comma) {
9002                            if matches!(self.peek(), Token::RBracket) {
9003                                break;
9004                            }
9005                            elems.push(self.parse_assign_expr()?);
9006                        }
9007                    }
9008                    self.expect(&Token::RBracket)?;
9009                    let aref = Expr {
9010                        kind: ExprKind::ArrayRef(elems),
9011                        line,
9012                    };
9013                    return Ok(Expr {
9014                        kind: ExprKind::Deref {
9015                            expr: Box::new(aref),
9016                            kind: Sigil::Array,
9017                        },
9018                        line,
9019                    });
9020                }
9021                // `@$arr` — array dereference; `@$h{k1,k2}` — hash slice via hashref
9022                let container = match self.peek().clone() {
9023                    Token::ScalarVar(n) => {
9024                        self.advance();
9025                        Expr {
9026                            kind: ExprKind::ScalarVar(n),
9027                            line,
9028                        }
9029                    }
9030                    _ => {
9031                        return Err(self.syntax_err(
9032                            "Expected `$name`, `{`, or `[` after `@` (e.g. `@$aref`, `@{expr}`, `@[1,2,3]`, or `@$href{keys}`)",
9033                            line,
9034                        ));
9035                    }
9036                };
9037                if matches!(self.peek(), Token::LBrace) {
9038                    self.advance();
9039                    let keys = self.parse_slice_arg_list(true)?;
9040                    self.expect(&Token::RBrace)?;
9041                    return Ok(Expr {
9042                        kind: ExprKind::HashSliceDeref {
9043                            container: Box::new(container),
9044                            keys,
9045                        },
9046                        line,
9047                    });
9048                }
9049                Ok(Expr {
9050                    kind: ExprKind::Deref {
9051                        expr: Box::new(container),
9052                        kind: Sigil::Array,
9053                    },
9054                    line,
9055                })
9056            }
9057            Token::LParen => {
9058                self.advance();
9059                if matches!(self.peek(), Token::RParen) {
9060                    self.advance();
9061                    // Empty `() x 3` is a no-op list repeat — record the close
9062                    // position so `Token::X` knows the LHS was a list literal.
9063                    self.list_construct_close_pos = Some(self.pos);
9064                    return Ok(Expr {
9065                        kind: ExprKind::List(vec![]),
9066                        line,
9067                    });
9068                }
9069                // Inside parens, pipe-forward is allowed even if we're in a
9070                // paren-less arg context. Save and restore no_pipe_forward_depth.
9071                let saved_no_pipe = self.no_pipe_forward_depth;
9072                self.no_pipe_forward_depth = 0;
9073                let expr = self.parse_expression();
9074                self.no_pipe_forward_depth = saved_no_pipe;
9075                let expr = expr?;
9076                self.expect(&Token::RParen)?;
9077                // Mark this paren as a list-constructor for the `x` operator
9078                // (parse_multiplication compares `self.pos` at the X token to
9079                // this checkpoint). Function-call parens (`f(args)`) don't
9080                // reach this branch; they're parsed by the call machinery.
9081                self.list_construct_close_pos = Some(self.pos);
9082                Ok(expr)
9083            }
9084            Token::LBracket => {
9085                self.advance();
9086                let elems = self.parse_arg_list()?;
9087                self.expect(&Token::RBracket)?;
9088                Ok(Expr {
9089                    kind: ExprKind::ArrayRef(elems),
9090                    line,
9091                })
9092            }
9093            Token::LBrace => {
9094                // Could be hash ref or block — disambiguate
9095                self.advance();
9096                // Try to parse as hash ref: { key => val, ... }
9097                let saved = self.pos;
9098                match self.try_parse_hash_ref() {
9099                    Ok(pairs) => Ok(Expr {
9100                        kind: ExprKind::HashRef(pairs),
9101                        line,
9102                    }),
9103                    Err(_) => {
9104                        self.pos = saved;
9105                        // Parse as block, wrap in code ref
9106                        let mut stmts = Vec::new();
9107                        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
9108                            if self.eat(&Token::Semicolon) {
9109                                continue;
9110                            }
9111                            stmts.push(self.parse_statement()?);
9112                        }
9113                        self.expect(&Token::RBrace)?;
9114                        Ok(Expr {
9115                            kind: ExprKind::CodeRef {
9116                                params: vec![],
9117                                body: stmts,
9118                            },
9119                            line,
9120                        })
9121                    }
9122                }
9123            }
9124            Token::Diamond => {
9125                self.advance();
9126                Ok(Expr {
9127                    kind: ExprKind::ReadLine(None),
9128                    line,
9129                })
9130            }
9131            Token::ReadLine(handle) => {
9132                self.advance();
9133                Ok(Expr {
9134                    kind: ExprKind::ReadLine(Some(handle)),
9135                    line,
9136                })
9137            }
9138
9139            // Named functions / builtins
9140            Token::ThreadArrow => {
9141                self.advance();
9142                self.parse_thread_macro(line, false)
9143            }
9144            Token::ThreadArrowLast => {
9145                self.advance();
9146                self.parse_thread_macro(line, true)
9147            }
9148            Token::ThreadArrowStream => {
9149                self.advance();
9150                let mut stages = Vec::new();
9151                self.parse_thread_macro_inner(line, false, Some(&mut stages))
9152            }
9153            Token::ThreadArrowStreamLast => {
9154                self.advance();
9155                let mut stages = Vec::new();
9156                self.parse_thread_macro_inner(line, true, Some(&mut stages))
9157            }
9158            Token::ThreadArrowPar => {
9159                self.advance();
9160                self.parse_thread_macro_chunk_par(line, false)
9161            }
9162            Token::ThreadArrowParLast => {
9163                self.advance();
9164                self.parse_thread_macro_chunk_par(line, true)
9165            }
9166            Token::Ident(ref name) => {
9167                let name = name.clone();
9168                // Handle s///
9169                if name.starts_with('\x00') {
9170                    self.advance();
9171                    let parts: Vec<&str> = name.split('\x00').collect();
9172                    if parts.len() >= 4 && parts[1] == "s" {
9173                        let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
9174                        return Ok(Expr {
9175                            kind: ExprKind::Substitution {
9176                                expr: Box::new(Expr {
9177                                    kind: ExprKind::ScalarVar("_".into()),
9178                                    line,
9179                                }),
9180                                pattern: parts[2].to_string(),
9181                                replacement: parts[3].to_string(),
9182                                flags: parts.get(4).unwrap_or(&"").to_string(),
9183                                delim,
9184                            },
9185                            line,
9186                        });
9187                    }
9188                    if parts.len() >= 4 && parts[1] == "tr" {
9189                        let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
9190                        return Ok(Expr {
9191                            kind: ExprKind::Transliterate {
9192                                expr: Box::new(Expr {
9193                                    kind: ExprKind::ScalarVar("_".into()),
9194                                    line,
9195                                }),
9196                                from: parts[2].to_string(),
9197                                to: parts[3].to_string(),
9198                                flags: parts.get(4).unwrap_or(&"").to_string(),
9199                                delim,
9200                            },
9201                            line,
9202                        });
9203                    }
9204                    return Err(self.syntax_err("Unexpected encoded token", line));
9205                }
9206                self.parse_named_expr(name)
9207            }
9208
9209            // `%name` when lexer emitted `Token::Percent` (due to preceding term context)
9210            // instead of `Token::HashVar`. This happens after `t` (thread macro) etc.
9211            Token::Percent => {
9212                self.advance();
9213                match self.peek().clone() {
9214                    Token::Ident(name) => {
9215                        self.advance();
9216                        Ok(Expr {
9217                            kind: ExprKind::HashVar(name),
9218                            line,
9219                        })
9220                    }
9221                    Token::ScalarVar(n) => {
9222                        self.advance();
9223                        Ok(Expr {
9224                            kind: ExprKind::Deref {
9225                                expr: Box::new(Expr {
9226                                    kind: ExprKind::ScalarVar(n),
9227                                    line,
9228                                }),
9229                                kind: Sigil::Hash,
9230                            },
9231                            line,
9232                        })
9233                    }
9234                    Token::LBrace => {
9235                        self.advance();
9236                        let looks_like_pair = matches!(
9237                            self.peek(),
9238                            Token::Ident(_) | Token::SingleString(_) | Token::DoubleString(_)
9239                        ) && matches!(self.peek_at(1), Token::FatArrow);
9240                        let inner = if looks_like_pair {
9241                            let pairs = self.parse_hashref_pairs_until(&Token::RBrace)?;
9242                            Expr {
9243                                kind: ExprKind::HashRef(pairs),
9244                                line,
9245                            }
9246                        } else {
9247                            self.parse_expression()?
9248                        };
9249                        self.expect(&Token::RBrace)?;
9250                        Ok(Expr {
9251                            kind: ExprKind::Deref {
9252                                expr: Box::new(inner),
9253                                kind: Sigil::Hash,
9254                            },
9255                            line,
9256                        })
9257                    }
9258                    Token::LBracket => {
9259                        self.advance();
9260                        let pairs = self.parse_hashref_pairs_until(&Token::RBracket)?;
9261                        self.expect(&Token::RBracket)?;
9262                        let href = Expr {
9263                            kind: ExprKind::HashRef(pairs),
9264                            line,
9265                        };
9266                        Ok(Expr {
9267                            kind: ExprKind::Deref {
9268                                expr: Box::new(href),
9269                                kind: Sigil::Hash,
9270                            },
9271                            line,
9272                        })
9273                    }
9274                    tok => Err(self.syntax_err(
9275                        format!(
9276                            "Expected identifier, `$`, `{{`, or `[` after `%`, got {:?}",
9277                            tok
9278                        ),
9279                        line,
9280                    )),
9281                }
9282            }
9283
9284            tok => Err(self.syntax_err(format!("Unexpected token {:?}", tok), line)),
9285        }
9286    }
9287
9288    fn parse_named_expr(&mut self, mut name: String) -> PerlResult<Expr> {
9289        let line = self.peek_line();
9290        self.advance(); // consume the ident
9291        while self.eat(&Token::PackageSep) {
9292            match self.advance() {
9293                (Token::Ident(part), _) => {
9294                    name = format!("{}::{}", name, part);
9295                }
9296                (tok, err_line) => {
9297                    return Err(self.syntax_err(
9298                        format!("Expected identifier after `::`, got {:?}", tok),
9299                        err_line,
9300                    ));
9301                }
9302            }
9303        }
9304
9305        // Fat-arrow auto-quoting: ANY bareword (including keywords/builtins)
9306        // before `=>` is treated as a string key, matching Perl 5 semantics.
9307        // e.g. `(print => 1, pr => "x", sort => 3)` are all valid hash pairs.
9308        // Stryke exception: topic-slot barewords (`_`, `_<`, `_0`, `_0<`, …) are
9309        // scalar references to the topic / positional / outer-topic chain — they
9310        // must evaluate as the topic value, not the literal name.
9311        if matches!(self.peek(), Token::FatArrow) && !Self::is_underscore_topic_slot(&name) {
9312            return Ok(Expr {
9313                kind: ExprKind::String(name),
9314                line,
9315            });
9316        }
9317
9318        if crate::compat_mode() {
9319            if let Some(ext) = Self::stryke_extension_name(&name) {
9320                if !self.declared_subs.contains(&name) {
9321                    return Err(self.syntax_err(
9322                        format!("`{ext}` is a stryke extension (disabled by --compat)"),
9323                        line,
9324                    ));
9325                }
9326            }
9327        }
9328
9329        // `CORE::length(...)` etc. — strip the explicit core-dispatch prefix so
9330        // the keyword arms below match the bare name and produce the same
9331        // `ExprKind::Length` / `ExprKind::Print` / etc. as the unprefixed form.
9332        // Matches Perl 5's `CORE::` namespace, which routes back to the
9333        // built-in implementation regardless of any same-named user sub.
9334        // (PARITY-011)
9335        if let Some(rest) = name.strip_prefix("CORE::") {
9336            name = rest.to_string();
9337        }
9338
9339        match name.as_str() {
9340            "__FILE__" => Ok(Expr {
9341                kind: ExprKind::MagicConst(MagicConstKind::File),
9342                line,
9343            }),
9344            "__LINE__" => Ok(Expr {
9345                kind: ExprKind::MagicConst(MagicConstKind::Line),
9346                line,
9347            }),
9348            "__SUB__" => Ok(Expr {
9349                kind: ExprKind::MagicConst(MagicConstKind::Sub),
9350                line,
9351            }),
9352            "stdin" => Ok(Expr {
9353                kind: ExprKind::FuncCall {
9354                    name: "stdin".into(),
9355                    args: vec![],
9356                },
9357                line,
9358            }),
9359            "range" => {
9360                let args = self.parse_builtin_args()?;
9361                Ok(Expr {
9362                    kind: ExprKind::FuncCall {
9363                        name: "range".into(),
9364                        args,
9365                    },
9366                    line,
9367                })
9368            }
9369            "print" | "pr" => self.parse_print_like(|h, a| ExprKind::Print { handle: h, args: a }),
9370            "say" => {
9371                if crate::no_interop_mode() {
9372                    return Err(
9373                        self.syntax_err("stryke uses `p` instead of `say` (--no-interop)", line)
9374                    );
9375                }
9376                self.parse_print_like(|h, a| ExprKind::Say { handle: h, args: a })
9377            }
9378            "p" => self.parse_print_like(|h, a| ExprKind::Say { handle: h, args: a }),
9379            "printf" => self.parse_print_like(|h, a| ExprKind::Printf { handle: h, args: a }),
9380            "die" => {
9381                let args = self.parse_list_until_terminator()?;
9382                Ok(Expr {
9383                    kind: ExprKind::Die(args),
9384                    line,
9385                })
9386            }
9387            "warn" => {
9388                let args = self.parse_list_until_terminator()?;
9389                Ok(Expr {
9390                    kind: ExprKind::Warn(args),
9391                    line,
9392                })
9393            }
9394            // `croak` / `confess` — `Carp` builtins available without `use Carp`
9395            // (matches the doc claim in `lsp.rs:1243`). For now both desugar to
9396            // `die` — TODO: croak should report caller's file/line, confess
9397            // should append a full stack trace.
9398            "croak" | "confess" => {
9399                let args = self.parse_list_until_terminator()?;
9400                Ok(Expr {
9401                    kind: ExprKind::Die(args),
9402                    line,
9403                })
9404            }
9405            // `carp` / `cluck` — `Carp` warning siblings of `croak`/`confess`.
9406            "carp" | "cluck" => {
9407                let args = self.parse_list_until_terminator()?;
9408                Ok(Expr {
9409                    kind: ExprKind::Warn(args),
9410                    line,
9411                })
9412            }
9413            "chomp" => {
9414                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9415                    return Ok(e);
9416                }
9417                let a = self.parse_one_arg_or_default()?;
9418                Ok(Expr {
9419                    kind: ExprKind::Chomp(Box::new(a)),
9420                    line,
9421                })
9422            }
9423            "chop" => {
9424                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9425                    return Ok(e);
9426                }
9427                let a = self.parse_one_arg_or_default()?;
9428                Ok(Expr {
9429                    kind: ExprKind::Chop(Box::new(a)),
9430                    line,
9431                })
9432            }
9433            "length" => {
9434                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9435                    return Ok(e);
9436                }
9437                let a = self.parse_one_arg_or_default()?;
9438                Ok(Expr {
9439                    kind: ExprKind::Length(Box::new(a)),
9440                    line,
9441                })
9442            }
9443            "defined" => {
9444                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9445                    return Ok(e);
9446                }
9447                // Named-unary precedence: `defined X && Y` is `(defined X) && Y`,
9448                // not `defined(X && Y)`. The default `parse_one_arg_or_default`
9449                // path is greedy (calls `parse_assign_expr_stop_at_pipe`), which
9450                // would let `&&` bind into the argument and silently make
9451                // `defined $h{k} && $h{k} > 0`-style guards always-true when the
9452                // hash element existed. `parse_named_unary_arg` stops at shift
9453                // level so logical operators stay outside.
9454                let a = if matches!(
9455                    self.peek(),
9456                    Token::Semicolon
9457                        | Token::RBrace
9458                        | Token::RParen
9459                        | Token::RBracket
9460                        | Token::Eof
9461                        | Token::Comma
9462                        | Token::FatArrow
9463                        | Token::PipeForward
9464                        | Token::Question
9465                        | Token::Colon
9466                        | Token::NumEq
9467                        | Token::NumNe
9468                        | Token::NumLt
9469                        | Token::NumGt
9470                        | Token::NumLe
9471                        | Token::NumGe
9472                        | Token::Spaceship
9473                        | Token::StrEq
9474                        | Token::StrNe
9475                        | Token::StrLt
9476                        | Token::StrGt
9477                        | Token::StrLe
9478                        | Token::StrGe
9479                        | Token::StrCmp
9480                        | Token::LogAnd
9481                        | Token::LogOr
9482                        | Token::LogNot
9483                        | Token::LogAndWord
9484                        | Token::LogOrWord
9485                        | Token::LogNotWord
9486                        | Token::DefinedOr
9487                        | Token::Range
9488                        | Token::RangeExclusive
9489                        | Token::Assign
9490                        | Token::PlusAssign
9491                        | Token::MinusAssign
9492                        | Token::MulAssign
9493                        | Token::DivAssign
9494                        | Token::ModAssign
9495                        | Token::PowAssign
9496                        | Token::DotAssign
9497                        | Token::AndAssign
9498                        | Token::OrAssign
9499                        | Token::XorAssign
9500                        | Token::DefinedOrAssign
9501                        | Token::ShiftLeftAssign
9502                        | Token::ShiftRightAssign
9503                        | Token::BitAndAssign
9504                        | Token::BitOrAssign
9505                ) {
9506                    Expr {
9507                        kind: ExprKind::ScalarVar("_".into()),
9508                        line: self.peek_line(),
9509                    }
9510                } else if matches!(self.peek(), Token::LParen)
9511                    && matches!(self.peek_at(1), Token::RParen)
9512                {
9513                    let pl = self.peek_line();
9514                    self.advance();
9515                    self.advance();
9516                    Expr {
9517                        kind: ExprKind::ScalarVar("_".into()),
9518                        line: pl,
9519                    }
9520                } else {
9521                    self.parse_named_unary_arg()?
9522                };
9523                Ok(Expr {
9524                    kind: ExprKind::Defined(Box::new(a)),
9525                    line,
9526                })
9527            }
9528            "ref" => {
9529                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9530                    return Ok(e);
9531                }
9532                let a = self.parse_one_arg_or_default()?;
9533                Ok(Expr {
9534                    kind: ExprKind::Ref(Box::new(a)),
9535                    line,
9536                })
9537            }
9538            "undef" => {
9539                // `undef $var` sets `$var` to undef — but a variable on a new line
9540                // is a separate statement (implicit semicolon), not an argument.
9541                if self.peek_line() == self.prev_line()
9542                    && matches!(
9543                        self.peek(),
9544                        Token::ScalarVar(_) | Token::ArrayVar(_) | Token::HashVar(_)
9545                    )
9546                {
9547                    let target = self.parse_primary()?;
9548                    return Ok(Expr {
9549                        kind: ExprKind::Assign {
9550                            target: Box::new(target),
9551                            value: Box::new(Expr {
9552                                kind: ExprKind::Undef,
9553                                line,
9554                            }),
9555                        },
9556                        line,
9557                    });
9558                }
9559                Ok(Expr {
9560                    kind: ExprKind::Undef,
9561                    line,
9562                })
9563            }
9564            "scalar" => {
9565                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9566                    return Ok(e);
9567                }
9568                if crate::no_interop_mode() {
9569                    return Err(self.syntax_err(
9570                        "stryke uses `len` (also `cnt` / `count`) instead of `scalar` (--no-interop)",
9571                        line,
9572                    ));
9573                }
9574                let a = self.parse_one_arg_or_default()?;
9575                Ok(Expr {
9576                    kind: ExprKind::ScalarContext(Box::new(a)),
9577                    line,
9578                })
9579            }
9580            "abs" => {
9581                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9582                    return Ok(e);
9583                }
9584                let a = self.parse_one_arg_or_default()?;
9585                Ok(Expr {
9586                    kind: ExprKind::Abs(Box::new(a)),
9587                    line,
9588                })
9589            }
9590            // stryke unary numeric extensions — treat like `abs` so a bare
9591            // identifier in `map { inc }` / `for (…) { p inc }` becomes a
9592            // call with implicit `$_` rather than falling through to the
9593            // generic `Bareword` arm (which stringifies to `"inc"`).
9594            "inc" | "dec" => {
9595                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9596                    return Ok(e);
9597                }
9598                let a = self.parse_one_arg_or_default()?;
9599                Ok(Expr {
9600                    kind: ExprKind::FuncCall {
9601                        name,
9602                        args: vec![a],
9603                    },
9604                    line,
9605                })
9606            }
9607            "int" => {
9608                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9609                    return Ok(e);
9610                }
9611                let a = self.parse_one_arg_or_default()?;
9612                Ok(Expr {
9613                    kind: ExprKind::Int(Box::new(a)),
9614                    line,
9615                })
9616            }
9617            "sqrt" => {
9618                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9619                    return Ok(e);
9620                }
9621                let a = self.parse_one_arg_or_default()?;
9622                Ok(Expr {
9623                    kind: ExprKind::Sqrt(Box::new(a)),
9624                    line,
9625                })
9626            }
9627            "sin" => {
9628                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9629                    return Ok(e);
9630                }
9631                let a = self.parse_one_arg_or_default()?;
9632                Ok(Expr {
9633                    kind: ExprKind::Sin(Box::new(a)),
9634                    line,
9635                })
9636            }
9637            "cos" => {
9638                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9639                    return Ok(e);
9640                }
9641                let a = self.parse_one_arg_or_default()?;
9642                Ok(Expr {
9643                    kind: ExprKind::Cos(Box::new(a)),
9644                    line,
9645                })
9646            }
9647            "atan2" => {
9648                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9649                    return Ok(e);
9650                }
9651                let args = self.parse_builtin_args()?;
9652                if args.len() != 2 {
9653                    return Err(self.syntax_err("atan2 requires two arguments", line));
9654                }
9655                Ok(Expr {
9656                    kind: ExprKind::Atan2 {
9657                        y: Box::new(args[0].clone()),
9658                        x: Box::new(args[1].clone()),
9659                    },
9660                    line,
9661                })
9662            }
9663            "exp" => {
9664                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9665                    return Ok(e);
9666                }
9667                let a = self.parse_one_arg_or_default()?;
9668                Ok(Expr {
9669                    kind: ExprKind::Exp(Box::new(a)),
9670                    line,
9671                })
9672            }
9673            "log" => {
9674                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9675                    return Ok(e);
9676                }
9677                let a = self.parse_one_arg_or_default()?;
9678                Ok(Expr {
9679                    kind: ExprKind::Log(Box::new(a)),
9680                    line,
9681                })
9682            }
9683            "input" => {
9684                let args = if matches!(
9685                    self.peek(),
9686                    Token::Semicolon
9687                        | Token::RBrace
9688                        | Token::RParen
9689                        | Token::Eof
9690                        | Token::Comma
9691                        | Token::PipeForward
9692                ) {
9693                    vec![]
9694                } else if matches!(self.peek(), Token::LParen) {
9695                    self.advance();
9696                    if matches!(self.peek(), Token::RParen) {
9697                        self.advance();
9698                        vec![]
9699                    } else {
9700                        let a = self.parse_expression()?;
9701                        self.expect(&Token::RParen)?;
9702                        vec![a]
9703                    }
9704                } else {
9705                    let a = self.parse_one_arg()?;
9706                    vec![a]
9707                };
9708                Ok(Expr {
9709                    kind: ExprKind::FuncCall {
9710                        name: "input".to_string(),
9711                        args,
9712                    },
9713                    line,
9714                })
9715            }
9716            "rand" => {
9717                if matches!(
9718                    self.peek(),
9719                    Token::Semicolon
9720                        | Token::RBrace
9721                        | Token::RParen
9722                        | Token::Eof
9723                        | Token::Comma
9724                        | Token::PipeForward
9725                ) {
9726                    Ok(Expr {
9727                        kind: ExprKind::Rand(None),
9728                        line,
9729                    })
9730                } else if matches!(self.peek(), Token::LParen) {
9731                    self.advance();
9732                    if matches!(self.peek(), Token::RParen) {
9733                        self.advance();
9734                        Ok(Expr {
9735                            kind: ExprKind::Rand(None),
9736                            line,
9737                        })
9738                    } else {
9739                        let a = self.parse_expression()?;
9740                        self.expect(&Token::RParen)?;
9741                        Ok(Expr {
9742                            kind: ExprKind::Rand(Some(Box::new(a))),
9743                            line,
9744                        })
9745                    }
9746                } else {
9747                    let a = self.parse_one_arg()?;
9748                    Ok(Expr {
9749                        kind: ExprKind::Rand(Some(Box::new(a))),
9750                        line,
9751                    })
9752                }
9753            }
9754            "srand" => {
9755                if matches!(
9756                    self.peek(),
9757                    Token::Semicolon
9758                        | Token::RBrace
9759                        | Token::RParen
9760                        | Token::Eof
9761                        | Token::Comma
9762                        | Token::PipeForward
9763                ) {
9764                    Ok(Expr {
9765                        kind: ExprKind::Srand(None),
9766                        line,
9767                    })
9768                } else if matches!(self.peek(), Token::LParen) {
9769                    self.advance();
9770                    if matches!(self.peek(), Token::RParen) {
9771                        self.advance();
9772                        Ok(Expr {
9773                            kind: ExprKind::Srand(None),
9774                            line,
9775                        })
9776                    } else {
9777                        let a = self.parse_expression()?;
9778                        self.expect(&Token::RParen)?;
9779                        Ok(Expr {
9780                            kind: ExprKind::Srand(Some(Box::new(a))),
9781                            line,
9782                        })
9783                    }
9784                } else {
9785                    let a = self.parse_one_arg()?;
9786                    Ok(Expr {
9787                        kind: ExprKind::Srand(Some(Box::new(a))),
9788                        line,
9789                    })
9790                }
9791            }
9792            "hex" => {
9793                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9794                    return Ok(e);
9795                }
9796                let a = self.parse_one_arg_or_default()?;
9797                Ok(Expr {
9798                    kind: ExprKind::Hex(Box::new(a)),
9799                    line,
9800                })
9801            }
9802            "oct" => {
9803                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9804                    return Ok(e);
9805                }
9806                let a = self.parse_one_arg_or_default()?;
9807                Ok(Expr {
9808                    kind: ExprKind::Oct(Box::new(a)),
9809                    line,
9810                })
9811            }
9812            "chr" => {
9813                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9814                    return Ok(e);
9815                }
9816                let a = self.parse_one_arg_or_default()?;
9817                Ok(Expr {
9818                    kind: ExprKind::Chr(Box::new(a)),
9819                    line,
9820                })
9821            }
9822            "ord" => {
9823                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9824                    return Ok(e);
9825                }
9826                let a = self.parse_one_arg_or_default()?;
9827                Ok(Expr {
9828                    kind: ExprKind::Ord(Box::new(a)),
9829                    line,
9830                })
9831            }
9832            "lc" => {
9833                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9834                    return Ok(e);
9835                }
9836                let a = self.parse_one_arg_or_default()?;
9837                Ok(Expr {
9838                    kind: ExprKind::Lc(Box::new(a)),
9839                    line,
9840                })
9841            }
9842            "uc" => {
9843                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9844                    return Ok(e);
9845                }
9846                let a = self.parse_one_arg_or_default()?;
9847                Ok(Expr {
9848                    kind: ExprKind::Uc(Box::new(a)),
9849                    line,
9850                })
9851            }
9852            "lcfirst" => {
9853                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9854                    return Ok(e);
9855                }
9856                let a = self.parse_one_arg_or_default()?;
9857                Ok(Expr {
9858                    kind: ExprKind::Lcfirst(Box::new(a)),
9859                    line,
9860                })
9861            }
9862            "ucfirst" => {
9863                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9864                    return Ok(e);
9865                }
9866                let a = self.parse_one_arg_or_default()?;
9867                Ok(Expr {
9868                    kind: ExprKind::Ucfirst(Box::new(a)),
9869                    line,
9870                })
9871            }
9872            "fc" => {
9873                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9874                    return Ok(e);
9875                }
9876                let a = self.parse_one_arg_or_default()?;
9877                Ok(Expr {
9878                    kind: ExprKind::Fc(Box::new(a)),
9879                    line,
9880                })
9881            }
9882            "crypt" => {
9883                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9884                    return Ok(e);
9885                }
9886                let args = self.parse_builtin_args()?;
9887                if args.len() != 2 {
9888                    return Err(self.syntax_err("crypt requires two arguments", line));
9889                }
9890                Ok(Expr {
9891                    kind: ExprKind::Crypt {
9892                        plaintext: Box::new(args[0].clone()),
9893                        salt: Box::new(args[1].clone()),
9894                    },
9895                    line,
9896                })
9897            }
9898            "pos" => {
9899                if matches!(
9900                    self.peek(),
9901                    Token::Semicolon
9902                        | Token::RBrace
9903                        | Token::RParen
9904                        | Token::Eof
9905                        | Token::Comma
9906                        | Token::PipeForward
9907                ) {
9908                    Ok(Expr {
9909                        kind: ExprKind::Pos(None),
9910                        line,
9911                    })
9912                } else if matches!(self.peek(), Token::Assign) {
9913                    // Perl: `pos = EXPR` is `pos($_) = EXPR` (Text::Balanced `_eb_delims`).
9914                    self.advance();
9915                    let rhs = self.parse_assign_expr()?;
9916                    Ok(Expr {
9917                        kind: ExprKind::Assign {
9918                            target: Box::new(Expr {
9919                                kind: ExprKind::Pos(Some(Box::new(Expr {
9920                                    kind: ExprKind::ScalarVar("_".into()),
9921                                    line,
9922                                }))),
9923                                line,
9924                            }),
9925                            value: Box::new(rhs),
9926                        },
9927                        line,
9928                    })
9929                } else if matches!(self.peek(), Token::LParen) {
9930                    self.advance();
9931                    if matches!(self.peek(), Token::RParen) {
9932                        self.advance();
9933                        Ok(Expr {
9934                            kind: ExprKind::Pos(None),
9935                            line,
9936                        })
9937                    } else {
9938                        let a = self.parse_expression()?;
9939                        self.expect(&Token::RParen)?;
9940                        Ok(Expr {
9941                            kind: ExprKind::Pos(Some(Box::new(a))),
9942                            line,
9943                        })
9944                    }
9945                } else {
9946                    let saved = self.pos;
9947                    let subj = self.parse_unary()?;
9948                    if matches!(self.peek(), Token::Assign) {
9949                        self.advance();
9950                        let rhs = self.parse_assign_expr()?;
9951                        Ok(Expr {
9952                            kind: ExprKind::Assign {
9953                                target: Box::new(Expr {
9954                                    kind: ExprKind::Pos(Some(Box::new(subj))),
9955                                    line,
9956                                }),
9957                                value: Box::new(rhs),
9958                            },
9959                            line,
9960                        })
9961                    } else {
9962                        self.pos = saved;
9963                        let a = self.parse_one_arg()?;
9964                        Ok(Expr {
9965                            kind: ExprKind::Pos(Some(Box::new(a))),
9966                            line,
9967                        })
9968                    }
9969                }
9970            }
9971            "study" => {
9972                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9973                    return Ok(e);
9974                }
9975                let a = self.parse_one_arg_or_default()?;
9976                Ok(Expr {
9977                    kind: ExprKind::Study(Box::new(a)),
9978                    line,
9979                })
9980            }
9981            "push" => {
9982                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9983                    return Ok(e);
9984                }
9985                let args = self.parse_builtin_args()?;
9986                let (first, rest) = args
9987                    .split_first()
9988                    .ok_or_else(|| self.syntax_err("push requires arguments", line))?;
9989                // Perl 5.24+ rejects `push SCALAR, ...` at parse time. Reject any
9990                // first arg that is unambiguously a scalar (literal scalar var or
9991                // numeric/string literal). Array refs (`@$x`), bindings, slices,
9992                // and `our @a` style remain permitted.
9993                if matches!(
9994                    first.kind,
9995                    ExprKind::ScalarVar(_)
9996                        | ExprKind::Integer(_)
9997                        | ExprKind::Float(_)
9998                        | ExprKind::String(_)
9999                ) {
10000                    return Err(self
10001                        .syntax_err("Experimental push on scalar is now forbidden", line)
10002                        .with_near("at EOF"));
10003                }
10004                Ok(Expr {
10005                    kind: ExprKind::Push {
10006                        array: Box::new(first.clone()),
10007                        values: rest.to_vec(),
10008                    },
10009                    line,
10010                })
10011            }
10012            "pop" => {
10013                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10014                    return Ok(e);
10015                }
10016                let a = self.parse_one_arg_or_argv()?;
10017                Ok(Expr {
10018                    kind: ExprKind::Pop(Box::new(a)),
10019                    line,
10020                })
10021            }
10022            "shift" => {
10023                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10024                    return Ok(e);
10025                }
10026                let a = self.parse_one_arg_or_argv()?;
10027                Ok(Expr {
10028                    kind: ExprKind::Shift(Box::new(a)),
10029                    line,
10030                })
10031            }
10032            "unshift" => {
10033                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10034                    return Ok(e);
10035                }
10036                let args = self.parse_builtin_args()?;
10037                let (first, rest) = args
10038                    .split_first()
10039                    .ok_or_else(|| self.syntax_err("unshift requires arguments", line))?;
10040                Ok(Expr {
10041                    kind: ExprKind::Unshift {
10042                        array: Box::new(first.clone()),
10043                        values: rest.to_vec(),
10044                    },
10045                    line,
10046                })
10047            }
10048            "splice" => {
10049                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10050                    return Ok(e);
10051                }
10052                let args = self.parse_builtin_args()?;
10053                let mut iter = args.into_iter();
10054                let array = Box::new(
10055                    iter.next()
10056                        .ok_or_else(|| self.syntax_err("splice requires arguments", line))?,
10057                );
10058                let offset = iter.next().map(Box::new);
10059                let length = iter.next().map(Box::new);
10060                let replacement: Vec<Expr> = iter.collect();
10061                Ok(Expr {
10062                    kind: ExprKind::Splice {
10063                        array,
10064                        offset,
10065                        length,
10066                        replacement,
10067                    },
10068                    line,
10069                })
10070            }
10071            // `splice_last(@a, off[, n])` is the stryke spelling of Perl's
10072            // `scalar splice(@a, off, n)` — returns the LAST removed element
10073            // (or undef if nothing was removed). Desugars to `tail(splice(...))`
10074            // so the array is still mutated in place.
10075            "splice_last" | "splice1" | "spl_last" => {
10076                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10077                    return Ok(e);
10078                }
10079                let args = self.parse_builtin_args()?;
10080                let mut iter = args.into_iter();
10081                let array = Box::new(
10082                    iter.next()
10083                        .ok_or_else(|| self.syntax_err("splice_last requires arguments", line))?,
10084                );
10085                let offset = iter.next().map(Box::new);
10086                let length = iter.next().map(Box::new);
10087                let replacement: Vec<Expr> = iter.collect();
10088                let splice_expr = Expr {
10089                    kind: ExprKind::Splice {
10090                        array,
10091                        offset,
10092                        length,
10093                        replacement,
10094                    },
10095                    line,
10096                };
10097                Ok(Expr {
10098                    kind: ExprKind::FuncCall {
10099                        name: "tail".to_string(),
10100                        args: vec![splice_expr],
10101                    },
10102                    line,
10103                })
10104            }
10105            "delete" => {
10106                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10107                    return Ok(e);
10108                }
10109                let a = self.parse_postfix()?;
10110                Ok(Expr {
10111                    kind: ExprKind::Delete(Box::new(a)),
10112                    line,
10113                })
10114            }
10115            "exists" => {
10116                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10117                    return Ok(e);
10118                }
10119                let a = self.parse_postfix()?;
10120                Ok(Expr {
10121                    kind: ExprKind::Exists(Box::new(a)),
10122                    line,
10123                })
10124            }
10125            "keys" => {
10126                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10127                    return Ok(e);
10128                }
10129                let a = self.parse_one_arg_or_default()?;
10130                Ok(Expr {
10131                    kind: ExprKind::Keys(Box::new(a)),
10132                    line,
10133                })
10134            }
10135            "values" => {
10136                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10137                    return Ok(e);
10138                }
10139                let a = self.parse_one_arg_or_default()?;
10140                Ok(Expr {
10141                    kind: ExprKind::Values(Box::new(a)),
10142                    line,
10143                })
10144            }
10145            "each" => {
10146                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10147                    return Ok(e);
10148                }
10149                let a = self.parse_one_arg_or_default()?;
10150                Ok(Expr {
10151                    kind: ExprKind::Each(Box::new(a)),
10152                    line,
10153                })
10154            }
10155            "fore" | "e" | "ep" => {
10156                // `fore { BLOCK } LIST` / `ep` — forEach expression (pipe-forward friendly)
10157                if matches!(self.peek(), Token::LBrace) {
10158                    let (block, list) = self.parse_block_list()?;
10159                    Ok(Expr {
10160                        kind: ExprKind::ForEachExpr {
10161                            block,
10162                            list: Box::new(list),
10163                        },
10164                        line,
10165                    })
10166                } else if self.in_pipe_rhs() {
10167                    // `|> ep` — bare ep at end of pipe: default to `say $_`
10168                    // `|> fore say` / `|> e say` — blockless pipe form: wrap EXPR into a synthetic block
10169                    let is_terminal = matches!(
10170                        self.peek(),
10171                        Token::Semicolon
10172                            | Token::RParen
10173                            | Token::Eof
10174                            | Token::PipeForward
10175                            | Token::RBrace
10176                    );
10177                    let block = if name == "ep" && is_terminal {
10178                        vec![Statement {
10179                            label: None,
10180                            kind: StmtKind::Expression(Expr {
10181                                kind: ExprKind::Say {
10182                                    handle: None,
10183                                    args: vec![Expr {
10184                                        kind: ExprKind::ScalarVar("_".into()),
10185                                        line,
10186                                    }],
10187                                },
10188                                line,
10189                            }),
10190                            line,
10191                        }]
10192                    } else {
10193                        let expr = self.parse_assign_expr_stop_at_pipe()?;
10194                        let expr = Self::lift_bareword_to_topic_call(expr);
10195                        vec![Statement {
10196                            label: None,
10197                            kind: StmtKind::Expression(expr),
10198                            line,
10199                        }]
10200                    };
10201                    let list = self.pipe_placeholder_list(line);
10202                    Ok(Expr {
10203                        kind: ExprKind::ForEachExpr {
10204                            block,
10205                            list: Box::new(list),
10206                        },
10207                        line,
10208                    })
10209                } else {
10210                    // Two surface forms share this branch:
10211                    //   `fore EXPR, LIST` — comma form (explicit per-item EXPR + list)
10212                    //   `ep LIST`         — list-only form: print each item with `say $_`
10213                    // We disambiguate by peeking after the first parsed expression:
10214                    // if the next token is a comma we're in the EXPR-then-LIST form;
10215                    // otherwise the first parse *was* the LIST and we default the
10216                    // block to `say $_` (only for `ep` — `fore`/`e` keep their
10217                    // explicit-expression contract).
10218                    let expr = self.parse_assign_expr()?;
10219                    let expr = Self::lift_bareword_to_topic_call(expr);
10220                    if !matches!(self.peek(), Token::Comma) && name == "ep" {
10221                        let block = vec![Statement {
10222                            label: None,
10223                            kind: StmtKind::Expression(Expr {
10224                                kind: ExprKind::Say {
10225                                    handle: None,
10226                                    args: vec![Expr {
10227                                        kind: ExprKind::ScalarVar("_".into()),
10228                                        line,
10229                                    }],
10230                                },
10231                                line,
10232                            }),
10233                            line,
10234                        }];
10235                        return Ok(Expr {
10236                            kind: ExprKind::ForEachExpr {
10237                                block,
10238                                list: Box::new(expr),
10239                            },
10240                            line,
10241                        });
10242                    }
10243                    self.expect(&Token::Comma)?;
10244                    let list_parts = self.parse_list_until_terminator()?;
10245                    let list_expr = if list_parts.len() == 1 {
10246                        list_parts.into_iter().next().unwrap()
10247                    } else {
10248                        Expr {
10249                            kind: ExprKind::List(list_parts),
10250                            line,
10251                        }
10252                    };
10253                    let block = vec![Statement {
10254                        label: None,
10255                        kind: StmtKind::Expression(expr),
10256                        line,
10257                    }];
10258                    Ok(Expr {
10259                        kind: ExprKind::ForEachExpr {
10260                            block,
10261                            list: Box::new(list_expr),
10262                        },
10263                        line,
10264                    })
10265                }
10266            }
10267            "rev" => {
10268                // `rev` — context-aware reverse: string in scalar, list in list context.
10269                // List-operator precedence (so `rev 1..3` parses as `rev(1..3)`, not
10270                // `(rev 1)..3`). Defaults to $_ when no argument given.
10271                // Only use pipe placeholder when directly in pipe RHS (not inside a block).
10272                // RBrace means we're inside a block like `map { rev }` - use $_ default.
10273                let prev = self.prev_line();
10274                let a = if self.in_pipe_rhs()
10275                    && (matches!(
10276                        self.peek(),
10277                        Token::Semicolon | Token::RParen | Token::Eof | Token::PipeForward
10278                    ) || self.peek_line() > prev)
10279                {
10280                    self.pipe_placeholder_list(line)
10281                } else if self.peek_line() > prev {
10282                    // Newline boundary: argument is on a later line —
10283                    // default to `$_` so the next statement parses as
10284                    // its own thing instead of being slurped as the
10285                    // implicit operand. (Same rule as
10286                    // `parse_one_arg_or_default`.)
10287                    Expr {
10288                        kind: ExprKind::ScalarVar("_".into()),
10289                        line: prev,
10290                    }
10291                } else if matches!(
10292                    self.peek(),
10293                    Token::Semicolon
10294                        | Token::RBrace
10295                        | Token::RParen
10296                        | Token::RBracket
10297                        | Token::Eof
10298                        | Token::Comma
10299                        | Token::FatArrow
10300                        | Token::PipeForward
10301                ) {
10302                    Expr {
10303                        kind: ExprKind::ScalarVar("_".into()),
10304                        line: self.peek_line(),
10305                    }
10306                } else if matches!(self.peek(), Token::LParen)
10307                    && matches!(self.peek_at(1), Token::RParen)
10308                {
10309                    // `rev()` — empty parens default to `$_` (matches Perl's
10310                    // `length()` / `uc()` etc. and the `|> rev()` pipe form).
10311                    let pl = self.peek_line();
10312                    self.advance(); // (
10313                    self.advance(); // )
10314                    Expr {
10315                        kind: ExprKind::ScalarVar("_".into()),
10316                        line: pl,
10317                    }
10318                } else {
10319                    self.parse_one_arg()?
10320                };
10321                Ok(Expr {
10322                    kind: ExprKind::Rev(Box::new(a)),
10323                    line,
10324                })
10325            }
10326            "reverse" => {
10327                if crate::no_interop_mode() {
10328                    return Err(self.syntax_err(
10329                        "stryke uses `rev` instead of `reverse` (--no-interop)",
10330                        line,
10331                    ));
10332                }
10333                // On the RHS of `|>`, the operand is supplied by the piped LHS.
10334                let a = if self.in_pipe_rhs()
10335                    && matches!(
10336                        self.peek(),
10337                        Token::Semicolon
10338                            | Token::RBrace
10339                            | Token::RParen
10340                            | Token::Eof
10341                            | Token::PipeForward
10342                    ) {
10343                    self.pipe_placeholder_list(line)
10344                } else {
10345                    self.parse_one_arg()?
10346                };
10347                Ok(Expr {
10348                    kind: ExprKind::ReverseExpr(Box::new(a)),
10349                    line,
10350                })
10351            }
10352            "reversed" | "rv" => {
10353                // On the RHS of `|>`, the operand is supplied by the piped LHS.
10354                let a = if self.in_pipe_rhs()
10355                    && matches!(
10356                        self.peek(),
10357                        Token::Semicolon
10358                            | Token::RBrace
10359                            | Token::RParen
10360                            | Token::Eof
10361                            | Token::PipeForward
10362                    ) {
10363                    self.pipe_placeholder_list(line)
10364                } else {
10365                    self.parse_one_arg()?
10366                };
10367                Ok(Expr {
10368                    kind: ExprKind::Rev(Box::new(a)),
10369                    line,
10370                })
10371            }
10372            "join" => {
10373                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10374                    return Ok(e);
10375                }
10376                let args = self.parse_builtin_args()?;
10377                if args.is_empty() {
10378                    return Err(self.syntax_err("join requires separator and list", line));
10379                }
10380                // `@list |> join(",")` — list slot is filled by the piped LHS.
10381                if args.len() < 2 && !self.in_pipe_rhs() {
10382                    return Err(self.syntax_err("join requires separator and list", line));
10383                }
10384                Ok(Expr {
10385                    kind: ExprKind::JoinExpr {
10386                        separator: Box::new(args[0].clone()),
10387                        list: Box::new(Expr {
10388                            kind: ExprKind::List(args[1..].to_vec()),
10389                            line,
10390                        }),
10391                    },
10392                    line,
10393                })
10394            }
10395            "split" => {
10396                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10397                    return Ok(e);
10398                }
10399                let args = self.parse_builtin_args()?;
10400                let pattern = args.first().cloned().unwrap_or(Expr {
10401                    kind: ExprKind::String(" ".into()),
10402                    line,
10403                });
10404                let string = args.get(1).cloned().unwrap_or(Expr {
10405                    kind: ExprKind::ScalarVar("_".into()),
10406                    line,
10407                });
10408                let limit = args.get(2).cloned().map(Box::new);
10409                Ok(Expr {
10410                    kind: ExprKind::SplitExpr {
10411                        pattern: Box::new(pattern),
10412                        string: Box::new(string),
10413                        limit,
10414                    },
10415                    line,
10416                })
10417            }
10418            "substr" => {
10419                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10420                    return Ok(e);
10421                }
10422                let args = self.parse_builtin_args()?;
10423                Ok(Expr {
10424                    kind: ExprKind::Substr {
10425                        string: Box::new(args[0].clone()),
10426                        offset: Box::new(args[1].clone()),
10427                        length: args.get(2).cloned().map(Box::new),
10428                        replacement: args.get(3).cloned().map(Box::new),
10429                    },
10430                    line,
10431                })
10432            }
10433            "index" => {
10434                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10435                    return Ok(e);
10436                }
10437                let args = self.parse_builtin_args()?;
10438                Ok(Expr {
10439                    kind: ExprKind::Index {
10440                        string: Box::new(args[0].clone()),
10441                        substr: Box::new(args[1].clone()),
10442                        position: args.get(2).cloned().map(Box::new),
10443                    },
10444                    line,
10445                })
10446            }
10447            "rindex" => {
10448                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10449                    return Ok(e);
10450                }
10451                let args = self.parse_builtin_args()?;
10452                Ok(Expr {
10453                    kind: ExprKind::Rindex {
10454                        string: Box::new(args[0].clone()),
10455                        substr: Box::new(args[1].clone()),
10456                        position: args.get(2).cloned().map(Box::new),
10457                    },
10458                    line,
10459                })
10460            }
10461            "sprintf" => {
10462                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10463                    return Ok(e);
10464                }
10465                let args = self.parse_builtin_args()?;
10466                let (first, rest) = args
10467                    .split_first()
10468                    .ok_or_else(|| self.syntax_err("sprintf requires format", line))?;
10469                Ok(Expr {
10470                    kind: ExprKind::Sprintf {
10471                        format: Box::new(first.clone()),
10472                        args: rest.to_vec(),
10473                    },
10474                    line,
10475                })
10476            }
10477            "map" | "flat_map" | "maps" | "flat_maps" => {
10478                let flatten_array_refs = matches!(name.as_str(), "flat_map" | "flat_maps");
10479                let stream = matches!(name.as_str(), "maps" | "flat_maps");
10480                if matches!(self.peek(), Token::LBrace) {
10481                    let (block, list) = self.parse_block_list()?;
10482                    Ok(Expr {
10483                        kind: ExprKind::MapExpr {
10484                            block,
10485                            list: Box::new(list),
10486                            flatten_array_refs,
10487                            stream,
10488                        },
10489                        line,
10490                    })
10491                } else {
10492                    let expr = self.parse_assign_expr_stop_at_pipe()?;
10493                    // Lift bareword to FuncCall($_) so `map sha512, @list`
10494                    // calls sha512($_) for each element instead of stringifying.
10495                    let expr = Self::lift_bareword_to_topic_call(expr);
10496                    let list_expr = if self.pipe_supplies_slurped_list_operand() {
10497                        self.pipe_placeholder_list(line)
10498                    } else {
10499                        self.expect(&Token::Comma)?;
10500                        let list_parts = self.parse_list_until_terminator()?;
10501                        if list_parts.len() == 1 {
10502                            list_parts.into_iter().next().unwrap()
10503                        } else {
10504                            Expr {
10505                                kind: ExprKind::List(list_parts),
10506                                line,
10507                            }
10508                        }
10509                    };
10510                    Ok(Expr {
10511                        kind: ExprKind::MapExprComma {
10512                            expr: Box::new(expr),
10513                            list: Box::new(list_expr),
10514                            flatten_array_refs,
10515                            stream,
10516                        },
10517                        line,
10518                    })
10519                }
10520            }
10521            "cond" => {
10522                if crate::compat_mode() {
10523                    return Err(self
10524                        .syntax_err("`cond` is a stryke extension (disabled by --compat)", line));
10525                }
10526                self.parse_cond_expr(line)
10527            }
10528            "match" => {
10529                if crate::compat_mode() {
10530                    return Err(self.syntax_err(
10531                        "algebraic `match` is a stryke extension (disabled by --compat)",
10532                        line,
10533                    ));
10534                }
10535                self.parse_algebraic_match_expr(line)
10536            }
10537            "grep" | "greps" | "filter" | "fi" | "find_all" => {
10538                let keyword = match name.as_str() {
10539                    "grep" => crate::ast::GrepBuiltinKeyword::Grep,
10540                    "greps" => crate::ast::GrepBuiltinKeyword::Greps,
10541                    "filter" | "fi" => crate::ast::GrepBuiltinKeyword::Filter,
10542                    "find_all" => crate::ast::GrepBuiltinKeyword::FindAll,
10543                    _ => unreachable!(),
10544                };
10545                if matches!(self.peek(), Token::LBrace) {
10546                    let (block, list) = self.parse_block_list()?;
10547                    Ok(Expr {
10548                        kind: ExprKind::GrepExpr {
10549                            block,
10550                            list: Box::new(list),
10551                            keyword,
10552                        },
10553                        line,
10554                    })
10555                } else {
10556                    let expr = self.parse_assign_expr_stop_at_pipe()?;
10557                    if self.pipe_supplies_slurped_list_operand() {
10558                        // Pipe-RHS blockless form: `|> grep EXPR`
10559                        // For literals, desugar to `$_ eq/== EXPR` so
10560                        // `|> filter 't'` keeps only elements equal to 't'.
10561                        // For regexes, desugar to `$_ =~ EXPR`.
10562                        let list = self.pipe_placeholder_list(line);
10563                        let topic = Expr {
10564                            kind: ExprKind::ScalarVar("_".into()),
10565                            line,
10566                        };
10567                        let test = match &expr.kind {
10568                            ExprKind::Integer(_) | ExprKind::Float(_) => Expr {
10569                                kind: ExprKind::BinOp {
10570                                    op: BinOp::NumEq,
10571                                    left: Box::new(topic),
10572                                    right: Box::new(expr),
10573                                },
10574                                line,
10575                            },
10576                            ExprKind::String(_) | ExprKind::InterpolatedString(_) => Expr {
10577                                kind: ExprKind::BinOp {
10578                                    op: BinOp::StrEq,
10579                                    left: Box::new(topic),
10580                                    right: Box::new(expr),
10581                                },
10582                                line,
10583                            },
10584                            ExprKind::Regex { .. } => Expr {
10585                                kind: ExprKind::BinOp {
10586                                    op: BinOp::BindMatch,
10587                                    left: Box::new(topic),
10588                                    right: Box::new(expr),
10589                                },
10590                                line,
10591                            },
10592                            _ => {
10593                                // Non-literal (e.g. `defined`, scalar coderef var,
10594                                // hash slot): lift barewords to topic-call, then
10595                                // route through GrepExprComma so the runtime
10596                                // coderef-dispatch in Op::GrepWithExpr handles
10597                                // both truthiness AND coderef-call uniformly.
10598                                let expr = Self::lift_bareword_to_topic_call(expr);
10599                                return Ok(Expr {
10600                                    kind: ExprKind::GrepExprComma {
10601                                        expr: Box::new(expr),
10602                                        list: Box::new(list),
10603                                        keyword,
10604                                    },
10605                                    line,
10606                                });
10607                            }
10608                        };
10609                        let block = vec![Statement {
10610                            label: None,
10611                            kind: StmtKind::Expression(test),
10612                            line,
10613                        }];
10614                        Ok(Expr {
10615                            kind: ExprKind::GrepExpr {
10616                                block,
10617                                list: Box::new(list),
10618                                keyword,
10619                            },
10620                            line,
10621                        })
10622                    } else {
10623                        let expr = Self::lift_bareword_to_topic_call(expr);
10624                        self.expect(&Token::Comma)?;
10625                        let list_parts = self.parse_list_until_terminator()?;
10626                        let list_expr = if list_parts.len() == 1 {
10627                            list_parts.into_iter().next().unwrap()
10628                        } else {
10629                            Expr {
10630                                kind: ExprKind::List(list_parts),
10631                                line,
10632                            }
10633                        };
10634                        Ok(Expr {
10635                            kind: ExprKind::GrepExprComma {
10636                                expr: Box::new(expr),
10637                                list: Box::new(list_expr),
10638                                keyword,
10639                            },
10640                            line,
10641                        })
10642                    }
10643                }
10644            }
10645            "sort" => {
10646                use crate::ast::SortComparator;
10647                if matches!(self.peek(), Token::LBrace) {
10648                    let block = self.parse_block()?;
10649                    let block_end_line = self.prev_line();
10650                    let _ = self.eat(&Token::Comma);
10651                    let list = if self.in_pipe_rhs()
10652                        && (matches!(
10653                            self.peek(),
10654                            Token::Semicolon
10655                                | Token::RBrace
10656                                | Token::RParen
10657                                | Token::Eof
10658                                | Token::PipeForward
10659                        ) || self.peek_line() > block_end_line)
10660                    {
10661                        self.pipe_placeholder_list(line)
10662                    } else {
10663                        self.parse_expression()?
10664                    };
10665                    Ok(Expr {
10666                        kind: ExprKind::SortExpr {
10667                            cmp: Some(SortComparator::Block(block)),
10668                            list: Box::new(list),
10669                        },
10670                        line,
10671                    })
10672                } else if matches!(self.peek(), Token::ScalarVar(ref v) if v == "a" || v == "b") {
10673                    // Blockless comparator: `sort $a <=> $b, @list`
10674                    let block = self.parse_block_or_bareword_cmp_block()?;
10675                    let _ = self.eat(&Token::Comma);
10676                    let list = if self.in_pipe_rhs()
10677                        && matches!(
10678                            self.peek(),
10679                            Token::Semicolon
10680                                | Token::RBrace
10681                                | Token::RParen
10682                                | Token::Eof
10683                                | Token::PipeForward
10684                        ) {
10685                        self.pipe_placeholder_list(line)
10686                    } else {
10687                        self.parse_expression()?
10688                    };
10689                    Ok(Expr {
10690                        kind: ExprKind::SortExpr {
10691                            cmp: Some(SortComparator::Block(block)),
10692                            list: Box::new(list),
10693                        },
10694                        line,
10695                    })
10696                } else if matches!(self.peek(), Token::ScalarVar(_)) {
10697                    // `sort $coderef (LIST)` — comparator is first; list often parenthesized.
10698                    // Pipe-RHS form `|> sort $coderef` uses placeholder LHS as the list.
10699                    self.suppress_indirect_paren_call =
10700                        self.suppress_indirect_paren_call.saturating_add(1);
10701                    let code = self.parse_assign_expr()?;
10702                    self.suppress_indirect_paren_call =
10703                        self.suppress_indirect_paren_call.saturating_sub(1);
10704                    let _ = self.eat(&Token::Comma);
10705                    let list = if self.in_pipe_rhs()
10706                        && matches!(
10707                            self.peek(),
10708                            Token::Semicolon
10709                                | Token::RBrace
10710                                | Token::RParen
10711                                | Token::Eof
10712                                | Token::PipeForward
10713                        ) {
10714                        self.pipe_placeholder_list(line)
10715                    } else if matches!(self.peek(), Token::LParen) {
10716                        self.advance();
10717                        let e = self.parse_expression()?;
10718                        self.expect(&Token::RParen)?;
10719                        e
10720                    } else {
10721                        self.parse_expression()?
10722                    };
10723                    Ok(Expr {
10724                        kind: ExprKind::SortExpr {
10725                            cmp: Some(SortComparator::Code(Box::new(code))),
10726                            list: Box::new(list),
10727                        },
10728                        line,
10729                    })
10730                } else if matches!(self.peek(), Token::Ident(ref name) if !Self::is_known_bareword(name))
10731                {
10732                    // Blockless comparator via bare sub name: `sort my_cmp @list`
10733                    let block = self.parse_block_or_bareword_cmp_block()?;
10734                    let _ = self.eat(&Token::Comma);
10735                    let list = if self.in_pipe_rhs()
10736                        && matches!(
10737                            self.peek(),
10738                            Token::Semicolon
10739                                | Token::RBrace
10740                                | Token::RParen
10741                                | Token::Eof
10742                                | Token::PipeForward
10743                        ) {
10744                        self.pipe_placeholder_list(line)
10745                    } else {
10746                        self.parse_expression()?
10747                    };
10748                    Ok(Expr {
10749                        kind: ExprKind::SortExpr {
10750                            cmp: Some(SortComparator::Block(block)),
10751                            list: Box::new(list),
10752                        },
10753                        line,
10754                    })
10755                } else {
10756                    // Bare `sort` with no comparator and no list: only allowed
10757                    // as the RHS of `|>`, where the list comes from the LHS.
10758                    let list = if self.in_pipe_rhs()
10759                        && matches!(
10760                            self.peek(),
10761                            Token::Semicolon
10762                                | Token::RBrace
10763                                | Token::RParen
10764                                | Token::Eof
10765                                | Token::PipeForward
10766                        ) {
10767                        self.pipe_placeholder_list(line)
10768                    } else {
10769                        self.parse_expression()?
10770                    };
10771                    Ok(Expr {
10772                        kind: ExprKind::SortExpr {
10773                            cmp: None,
10774                            list: Box::new(list),
10775                        },
10776                        line,
10777                    })
10778                }
10779            }
10780            "reduce" | "fold" | "inject" => {
10781                let (block, list) = self.parse_block_list()?;
10782                Ok(Expr {
10783                    kind: ExprKind::ReduceExpr {
10784                        block,
10785                        list: Box::new(list),
10786                    },
10787                    line,
10788                })
10789            }
10790            // Parallel extensions
10791            "pmap" => {
10792                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10793                Ok(Expr {
10794                    kind: ExprKind::PMapExpr {
10795                        block,
10796                        list: Box::new(list),
10797                        progress: progress.map(Box::new),
10798                        flat_outputs: false,
10799                        on_cluster: None,
10800                        stream: false,
10801                    },
10802                    line,
10803                })
10804            }
10805            "pmap_on" => {
10806                let (cluster, block, list, progress) =
10807                    self.parse_cluster_block_then_list_optional_progress()?;
10808                Ok(Expr {
10809                    kind: ExprKind::PMapExpr {
10810                        block,
10811                        list: Box::new(list),
10812                        progress: progress.map(Box::new),
10813                        flat_outputs: false,
10814                        on_cluster: Some(Box::new(cluster)),
10815                        stream: false,
10816                    },
10817                    line,
10818                })
10819            }
10820            "pflat_map" => {
10821                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10822                Ok(Expr {
10823                    kind: ExprKind::PMapExpr {
10824                        block,
10825                        list: Box::new(list),
10826                        progress: progress.map(Box::new),
10827                        flat_outputs: true,
10828                        on_cluster: None,
10829                        stream: false,
10830                    },
10831                    line,
10832                })
10833            }
10834            "pflat_map_on" => {
10835                let (cluster, block, list, progress) =
10836                    self.parse_cluster_block_then_list_optional_progress()?;
10837                Ok(Expr {
10838                    kind: ExprKind::PMapExpr {
10839                        block,
10840                        list: Box::new(list),
10841                        progress: progress.map(Box::new),
10842                        flat_outputs: true,
10843                        on_cluster: Some(Box::new(cluster)),
10844                        stream: false,
10845                    },
10846                    line,
10847                })
10848            }
10849            "pmaps" => {
10850                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10851                Ok(Expr {
10852                    kind: ExprKind::PMapExpr {
10853                        block,
10854                        list: Box::new(list),
10855                        progress: progress.map(Box::new),
10856                        flat_outputs: false,
10857                        on_cluster: None,
10858                        stream: true,
10859                    },
10860                    line,
10861                })
10862            }
10863            "pflat_maps" => {
10864                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10865                Ok(Expr {
10866                    kind: ExprKind::PMapExpr {
10867                        block,
10868                        list: Box::new(list),
10869                        progress: progress.map(Box::new),
10870                        flat_outputs: true,
10871                        on_cluster: None,
10872                        stream: true,
10873                    },
10874                    line,
10875                })
10876            }
10877            "pgreps" => {
10878                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10879                Ok(Expr {
10880                    kind: ExprKind::PGrepExpr {
10881                        block,
10882                        list: Box::new(list),
10883                        progress: progress.map(Box::new),
10884                        stream: true,
10885                    },
10886                    line,
10887                })
10888            }
10889            "pmap_chunked" => {
10890                let chunk_size = self.parse_assign_expr()?;
10891                let block = self.parse_block_or_bareword_block()?;
10892                self.eat(&Token::Comma);
10893                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10894                Ok(Expr {
10895                    kind: ExprKind::PMapChunkedExpr {
10896                        chunk_size: Box::new(chunk_size),
10897                        block,
10898                        list: Box::new(list),
10899                        progress: progress.map(Box::new),
10900                    },
10901                    line,
10902                })
10903            }
10904            "pgrep" => {
10905                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10906                Ok(Expr {
10907                    kind: ExprKind::PGrepExpr {
10908                        block,
10909                        list: Box::new(list),
10910                        progress: progress.map(Box::new),
10911                        stream: false,
10912                    },
10913                    line,
10914                })
10915            }
10916            "pfor" => {
10917                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10918                Ok(Expr {
10919                    kind: ExprKind::PForExpr {
10920                        block,
10921                        list: Box::new(list),
10922                        progress: progress.map(Box::new),
10923                    },
10924                    line,
10925                })
10926            }
10927            "par_lines" | "par_walk" => {
10928                let args = self.parse_builtin_args()?;
10929                if args.len() < 2 {
10930                    return Err(
10931                        self.syntax_err(format!("{} requires at least two arguments", name), line)
10932                    );
10933                }
10934
10935                if name == "par_lines" {
10936                    Ok(Expr {
10937                        kind: ExprKind::ParLinesExpr {
10938                            path: Box::new(args[0].clone()),
10939                            callback: Box::new(args[1].clone()),
10940                            progress: None,
10941                        },
10942                        line,
10943                    })
10944                } else {
10945                    Ok(Expr {
10946                        kind: ExprKind::ParWalkExpr {
10947                            path: Box::new(args[0].clone()),
10948                            callback: Box::new(args[1].clone()),
10949                            progress: None,
10950                        },
10951                        line,
10952                    })
10953                }
10954            }
10955            "pwatch" | "watch" => {
10956                let args = self.parse_builtin_args()?;
10957                if args.len() < 2 {
10958                    return Err(
10959                        self.syntax_err(format!("{} requires at least two arguments", name), line)
10960                    );
10961                }
10962                Ok(Expr {
10963                    kind: ExprKind::PwatchExpr {
10964                        path: Box::new(args[0].clone()),
10965                        callback: Box::new(args[1].clone()),
10966                    },
10967                    line,
10968                })
10969            }
10970            "fan" => {
10971                // fan { BLOCK }            — no count, block body
10972                // fan COUNT { BLOCK }      — count + block body
10973                // fan EXPR;                — no count, blockless body (wrap EXPR as block)
10974                // fan COUNT EXPR;          — count + blockless body
10975                // Optional: `, progress => EXPR` or `progress => EXPR` (no comma before progress)
10976                let (count, block) = self.parse_fan_count_and_block(line)?;
10977                let progress = self.parse_fan_optional_progress("fan")?;
10978                Ok(Expr {
10979                    kind: ExprKind::FanExpr {
10980                        count,
10981                        block,
10982                        progress,
10983                        capture: false,
10984                    },
10985                    line,
10986                })
10987            }
10988            "fan_cap" => {
10989                let (count, block) = self.parse_fan_count_and_block(line)?;
10990                let progress = self.parse_fan_optional_progress("fan_cap")?;
10991                Ok(Expr {
10992                    kind: ExprKind::FanExpr {
10993                        count,
10994                        block,
10995                        progress,
10996                        capture: true,
10997                    },
10998                    line,
10999                })
11000            }
11001            "async" => {
11002                if !matches!(self.peek(), Token::LBrace) {
11003                    return Err(self.syntax_err("async must be followed by { BLOCK }", line));
11004                }
11005                let block = self.parse_block()?;
11006                Ok(Expr {
11007                    kind: ExprKind::AsyncBlock { body: block },
11008                    line,
11009                })
11010            }
11011            "spawn" => {
11012                if !matches!(self.peek(), Token::LBrace) {
11013                    return Err(self.syntax_err("spawn must be followed by { BLOCK }", line));
11014                }
11015                let block = self.parse_block()?;
11016                Ok(Expr {
11017                    kind: ExprKind::SpawnBlock { body: block },
11018                    line,
11019                })
11020            }
11021            "trace" => {
11022                if !matches!(self.peek(), Token::LBrace) {
11023                    return Err(self.syntax_err("trace must be followed by { BLOCK }", line));
11024                }
11025                let block = self.parse_block()?;
11026                Ok(Expr {
11027                    kind: ExprKind::Trace { body: block },
11028                    line,
11029                })
11030            }
11031            "timer" => {
11032                let block = self.parse_block_or_bareword_block_no_args()?;
11033                Ok(Expr {
11034                    kind: ExprKind::Timer { body: block },
11035                    line,
11036                })
11037            }
11038            "bench" => {
11039                let block = self.parse_block_or_bareword_block_no_args()?;
11040                let times = Box::new(self.parse_expression()?);
11041                Ok(Expr {
11042                    kind: ExprKind::Bench { body: block, times },
11043                    line,
11044                })
11045            }
11046            "spinner" => {
11047                // `spinner "msg" { BLOCK }` or `spinner { BLOCK }`
11048                let (message, body) = if matches!(self.peek(), Token::LBrace) {
11049                    let body = self.parse_block()?;
11050                    (
11051                        Box::new(Expr {
11052                            kind: ExprKind::String("working".to_string()),
11053                            line,
11054                        }),
11055                        body,
11056                    )
11057                } else {
11058                    let msg = self.parse_assign_expr()?;
11059                    let body = self.parse_block()?;
11060                    (Box::new(msg), body)
11061                };
11062                Ok(Expr {
11063                    kind: ExprKind::Spinner { message, body },
11064                    line,
11065                })
11066            }
11067            "thread" | "t" => {
11068                // `thread EXPR stage1 stage2 ...` — threading macro (thread-first)
11069                // `t` is a short alias for `thread`
11070                // Each stage is either:
11071                //   - `ident` — bare function call
11072                //   - `ident { block }` — function with block arg
11073                //   - `ident arg1 arg2 { block }` — function with args and optional block
11074                //   - `fn { block }` — standalone anonymous block
11075                //   - `>{ block }` — shorthand for standalone anonymous block
11076                // Desugars to: EXPR |> stage1 |> stage2 |> ...
11077                self.parse_thread_macro(line, false)
11078            }
11079            "retry" => {
11080                // `retry { BLOCK }` or `retry BAREWORD` — bareword becomes zero-arg call.
11081                // An optional comma before `times` is allowed in both forms.
11082                let body = if matches!(self.peek(), Token::LBrace) {
11083                    self.parse_block()?
11084                } else {
11085                    let bw_line = self.peek_line();
11086                    let Token::Ident(ref name) = self.peek().clone() else {
11087                        return Err(self
11088                            .syntax_err("retry: expected block or bareword function name", line));
11089                    };
11090                    let name = name.clone();
11091                    self.advance();
11092                    vec![Statement::new(
11093                        StmtKind::Expression(Expr {
11094                            kind: ExprKind::FuncCall { name, args: vec![] },
11095                            line: bw_line,
11096                        }),
11097                        bw_line,
11098                    )]
11099                };
11100                self.eat(&Token::Comma);
11101                match self.peek() {
11102                    Token::Ident(ref s) if s == "times" => {
11103                        self.advance();
11104                    }
11105                    _ => {
11106                        return Err(self.syntax_err("retry: expected `times =>` after block", line));
11107                    }
11108                }
11109                self.expect(&Token::FatArrow)?;
11110                let times = Box::new(self.parse_assign_expr()?);
11111                let mut backoff = RetryBackoff::None;
11112                if self.eat(&Token::Comma) {
11113                    match self.peek() {
11114                        Token::Ident(ref s) if s == "backoff" => {
11115                            self.advance();
11116                        }
11117                        _ => {
11118                            return Err(
11119                                self.syntax_err("retry: expected `backoff =>` after comma", line)
11120                            );
11121                        }
11122                    }
11123                    self.expect(&Token::FatArrow)?;
11124                    let Token::Ident(mode) = self.peek().clone() else {
11125                        return Err(self.syntax_err(
11126                            "retry: expected backoff mode (none, linear, exponential)",
11127                            line,
11128                        ));
11129                    };
11130                    backoff = match mode.as_str() {
11131                        "none" => RetryBackoff::None,
11132                        "linear" => RetryBackoff::Linear,
11133                        "exponential" => RetryBackoff::Exponential,
11134                        _ => {
11135                            return Err(
11136                                self.syntax_err(format!("retry: invalid backoff `{mode}`"), line)
11137                            );
11138                        }
11139                    };
11140                    self.advance();
11141                }
11142                Ok(Expr {
11143                    kind: ExprKind::RetryBlock {
11144                        body,
11145                        times,
11146                        backoff,
11147                    },
11148                    line,
11149                })
11150            }
11151            "rate_limit" => {
11152                self.expect(&Token::LParen)?;
11153                let max = Box::new(self.parse_assign_expr()?);
11154                self.expect(&Token::Comma)?;
11155                let window = Box::new(self.parse_assign_expr()?);
11156                self.expect(&Token::RParen)?;
11157                let body = self.parse_block_or_bareword_block_no_args()?;
11158                let slot = self.alloc_rate_limit_slot();
11159                Ok(Expr {
11160                    kind: ExprKind::RateLimitBlock {
11161                        slot,
11162                        max,
11163                        window,
11164                        body,
11165                    },
11166                    line,
11167                })
11168            }
11169            "every" => {
11170                // `every("500ms") { BLOCK }` or `every "500ms" BODY` — parens optional.
11171                // Body consumes `|>` (every is an infinite loop, not a pipeable source).
11172                let has_paren = self.eat(&Token::LParen);
11173                let interval = Box::new(self.parse_assign_expr()?);
11174                if has_paren {
11175                    self.expect(&Token::RParen)?;
11176                }
11177                let body = if matches!(self.peek(), Token::LBrace) {
11178                    self.parse_block()?
11179                } else {
11180                    let bline = self.peek_line();
11181                    let expr = self.parse_assign_expr()?;
11182                    vec![Statement::new(StmtKind::Expression(expr), bline)]
11183                };
11184                Ok(Expr {
11185                    kind: ExprKind::EveryBlock { interval, body },
11186                    line,
11187                })
11188            }
11189            "gen" => {
11190                if !matches!(self.peek(), Token::LBrace) {
11191                    return Err(self.syntax_err("gen must be followed by { BLOCK }", line));
11192                }
11193                let body = self.parse_block()?;
11194                Ok(Expr {
11195                    kind: ExprKind::GenBlock { body },
11196                    line,
11197                })
11198            }
11199            "yield" => {
11200                let e = self.parse_assign_expr()?;
11201                Ok(Expr {
11202                    kind: ExprKind::Yield(Box::new(e)),
11203                    line,
11204                })
11205            }
11206            "await" => {
11207                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11208                    return Ok(e);
11209                }
11210                // `await` defaults to `$_` so `map { await } @tasks` works
11211                // (Perl-style topic-defaulting unary).
11212                let a = self.parse_one_arg_or_default()?;
11213                Ok(Expr {
11214                    kind: ExprKind::Await(Box::new(a)),
11215                    line,
11216                })
11217            }
11218            "slurp" | "cat" | "c" => {
11219                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11220                    return Ok(e);
11221                }
11222                let a = self.parse_one_arg_or_default()?;
11223                Ok(Expr {
11224                    kind: ExprKind::Slurp(Box::new(a)),
11225                    line,
11226                })
11227            }
11228            "capture" => {
11229                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11230                    return Ok(e);
11231                }
11232                let a = self.parse_one_arg()?;
11233                Ok(Expr {
11234                    kind: ExprKind::Capture(Box::new(a)),
11235                    line,
11236                })
11237            }
11238            "fetch_url" => {
11239                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11240                    return Ok(e);
11241                }
11242                let a = self.parse_one_arg()?;
11243                Ok(Expr {
11244                    kind: ExprKind::FetchUrl(Box::new(a)),
11245                    line,
11246                })
11247            }
11248            "pchannel" => {
11249                let capacity = if self.eat(&Token::LParen) {
11250                    if matches!(self.peek(), Token::RParen) {
11251                        self.advance();
11252                        None
11253                    } else {
11254                        let e = self.parse_expression()?;
11255                        self.expect(&Token::RParen)?;
11256                        Some(Box::new(e))
11257                    }
11258                } else {
11259                    None
11260                };
11261                Ok(Expr {
11262                    kind: ExprKind::Pchannel { capacity },
11263                    line,
11264                })
11265            }
11266            "psort" => {
11267                if matches!(self.peek(), Token::LBrace)
11268                    || matches!(self.peek(), Token::ScalarVar(ref v) if v == "a" || v == "b")
11269                    || matches!(self.peek(), Token::Ident(ref name) if !Self::is_known_bareword(name))
11270                {
11271                    let block = self.parse_block_or_bareword_cmp_block()?;
11272                    // Mirror `sort`'s pipe-RHS handling — after the block,
11273                    // a newline (or any standard terminator token) inside a
11274                    // `|> psort { ... }` chain means the list comes from the
11275                    // pipe LHS, not from continued parsing into the next
11276                    // statement. Without this check `(@list) |> psort {
11277                    // _0 <=> _1 }\nmy $n = ...` silently swallowed `my $n =
11278                    // ...` as the list operand.
11279                    let block_end_line = self.prev_line();
11280                    self.eat(&Token::Comma);
11281                    let use_placeholder = self.in_pipe_rhs()
11282                        && (matches!(
11283                            self.peek(),
11284                            Token::Semicolon
11285                                | Token::RBrace
11286                                | Token::RParen
11287                                | Token::Eof
11288                                | Token::PipeForward
11289                        ) || self.peek_line() > block_end_line);
11290                    let (list, progress) = if use_placeholder {
11291                        (self.pipe_placeholder_list(line), None)
11292                    } else {
11293                        self.parse_assign_expr_list_optional_progress()?
11294                    };
11295                    Ok(Expr {
11296                        kind: ExprKind::PSortExpr {
11297                            cmp: Some(block),
11298                            list: Box::new(list),
11299                            progress: progress.map(Box::new),
11300                        },
11301                        line,
11302                    })
11303                } else {
11304                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
11305                    Ok(Expr {
11306                        kind: ExprKind::PSortExpr {
11307                            cmp: None,
11308                            list: Box::new(list),
11309                            progress: progress.map(Box::new),
11310                        },
11311                        line,
11312                    })
11313                }
11314            }
11315            "preduce" => {
11316                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11317                Ok(Expr {
11318                    kind: ExprKind::PReduceExpr {
11319                        block,
11320                        list: Box::new(list),
11321                        progress: progress.map(Box::new),
11322                    },
11323                    line,
11324                })
11325            }
11326            "preduce_init" => {
11327                let (init, block, list, progress) =
11328                    self.parse_init_block_then_list_optional_progress()?;
11329                Ok(Expr {
11330                    kind: ExprKind::PReduceInitExpr {
11331                        init: Box::new(init),
11332                        block,
11333                        list: Box::new(list),
11334                        progress: progress.map(Box::new),
11335                    },
11336                    line,
11337                })
11338            }
11339            "pmap_reduce" => {
11340                let map_block = self.parse_block_or_bareword_block()?;
11341                // After the map block, expect either a `{ REDUCE }` block, or
11342                // after an eaten comma, a blockless reduce expr (`$a + $b`).
11343                let reduce_block = if matches!(self.peek(), Token::LBrace) {
11344                    self.parse_block()?
11345                } else {
11346                    // comma separates blockless map from blockless reduce
11347                    self.expect(&Token::Comma)?;
11348                    self.parse_block_or_bareword_cmp_block()?
11349                };
11350                self.eat(&Token::Comma);
11351                let line = self.peek_line();
11352                if let Token::Ident(ref kw) = self.peek().clone() {
11353                    if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
11354                        self.advance();
11355                        self.expect(&Token::FatArrow)?;
11356                        let prog = self.parse_assign_expr()?;
11357                        return Ok(Expr {
11358                            kind: ExprKind::PMapReduceExpr {
11359                                map_block,
11360                                reduce_block,
11361                                list: Box::new(Expr {
11362                                    kind: ExprKind::List(vec![]),
11363                                    line,
11364                                }),
11365                                progress: Some(Box::new(prog)),
11366                            },
11367                            line,
11368                        });
11369                    }
11370                }
11371                if matches!(
11372                    self.peek(),
11373                    Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
11374                ) {
11375                    return Ok(Expr {
11376                        kind: ExprKind::PMapReduceExpr {
11377                            map_block,
11378                            reduce_block,
11379                            list: Box::new(Expr {
11380                                kind: ExprKind::List(vec![]),
11381                                line,
11382                            }),
11383                            progress: None,
11384                        },
11385                        line,
11386                    });
11387                }
11388                let mut parts = vec![self.parse_assign_expr()?];
11389                loop {
11390                    if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
11391                        break;
11392                    }
11393                    if matches!(
11394                        self.peek(),
11395                        Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
11396                    ) {
11397                        break;
11398                    }
11399                    if let Token::Ident(ref kw) = self.peek().clone() {
11400                        if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
11401                            self.advance();
11402                            self.expect(&Token::FatArrow)?;
11403                            let prog = self.parse_assign_expr()?;
11404                            return Ok(Expr {
11405                                kind: ExprKind::PMapReduceExpr {
11406                                    map_block,
11407                                    reduce_block,
11408                                    list: Box::new(merge_expr_list(parts)),
11409                                    progress: Some(Box::new(prog)),
11410                                },
11411                                line,
11412                            });
11413                        }
11414                    }
11415                    parts.push(self.parse_assign_expr()?);
11416                }
11417                Ok(Expr {
11418                    kind: ExprKind::PMapReduceExpr {
11419                        map_block,
11420                        reduce_block,
11421                        list: Box::new(merge_expr_list(parts)),
11422                        progress: None,
11423                    },
11424                    line,
11425                })
11426            }
11427            "puniq" => {
11428                if self.pipe_supplies_slurped_list_operand() {
11429                    return Ok(Expr {
11430                        kind: ExprKind::FuncCall {
11431                            name: "puniq".to_string(),
11432                            args: vec![],
11433                        },
11434                        line,
11435                    });
11436                }
11437                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
11438                let mut args = vec![list];
11439                if let Some(p) = progress {
11440                    args.push(p);
11441                }
11442                Ok(Expr {
11443                    kind: ExprKind::FuncCall {
11444                        name: "puniq".to_string(),
11445                        args,
11446                    },
11447                    line,
11448                })
11449            }
11450            "pfirst" => {
11451                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11452                let cr = Expr {
11453                    kind: ExprKind::CodeRef {
11454                        params: vec![],
11455                        body: block,
11456                    },
11457                    line,
11458                };
11459                let mut args = vec![cr, list];
11460                if let Some(p) = progress {
11461                    args.push(p);
11462                }
11463                Ok(Expr {
11464                    kind: ExprKind::FuncCall {
11465                        name: "pfirst".to_string(),
11466                        args,
11467                    },
11468                    line,
11469                })
11470            }
11471            "pany" => {
11472                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11473                let cr = Expr {
11474                    kind: ExprKind::CodeRef {
11475                        params: vec![],
11476                        body: block,
11477                    },
11478                    line,
11479                };
11480                let mut args = vec![cr, list];
11481                if let Some(p) = progress {
11482                    args.push(p);
11483                }
11484                Ok(Expr {
11485                    kind: ExprKind::FuncCall {
11486                        name: "pany".to_string(),
11487                        args,
11488                    },
11489                    line,
11490                })
11491            }
11492            "uniq" | "distinct" => {
11493                if self.pipe_supplies_slurped_list_operand() {
11494                    return Ok(Expr {
11495                        kind: ExprKind::FuncCall {
11496                            name: name.clone(),
11497                            args: vec![],
11498                        },
11499                        line,
11500                    });
11501                }
11502                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
11503                if progress.is_some() {
11504                    return Err(self.syntax_err(
11505                        "`progress =>` is not supported for uniq (use puniq for parallel + progress)",
11506                        line,
11507                    ));
11508                }
11509                Ok(Expr {
11510                    kind: ExprKind::FuncCall {
11511                        name: name.clone(),
11512                        args: vec![list],
11513                    },
11514                    line,
11515                })
11516            }
11517            "flatten" => {
11518                if self.pipe_supplies_slurped_list_operand() {
11519                    return Ok(Expr {
11520                        kind: ExprKind::FuncCall {
11521                            name: "flatten".to_string(),
11522                            args: vec![],
11523                        },
11524                        line,
11525                    });
11526                }
11527                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
11528                if progress.is_some() {
11529                    return Err(self.syntax_err("`progress =>` is not supported for flatten", line));
11530                }
11531                Ok(Expr {
11532                    kind: ExprKind::FuncCall {
11533                        name: "flatten".to_string(),
11534                        args: vec![list],
11535                    },
11536                    line,
11537                })
11538            }
11539            "set" => {
11540                if self.pipe_supplies_slurped_list_operand() {
11541                    return Ok(Expr {
11542                        kind: ExprKind::FuncCall {
11543                            name: "set".to_string(),
11544                            args: vec![],
11545                        },
11546                        line,
11547                    });
11548                }
11549                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
11550                if progress.is_some() {
11551                    return Err(self.syntax_err("`progress =>` is not supported for set", line));
11552                }
11553                Ok(Expr {
11554                    kind: ExprKind::FuncCall {
11555                        name: "set".to_string(),
11556                        args: vec![list],
11557                    },
11558                    line,
11559                })
11560            }
11561            // `size` is the file-size builtin (Perl `-s`), not a list-count alias.
11562            // Defaults to `$_` when no arg is given, like `length`. See
11563            // `builtin_file_size` in builtins.rs for the runtime behavior.
11564            "size" => {
11565                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11566                    return Ok(e);
11567                }
11568                if self.pipe_supplies_slurped_list_operand() {
11569                    return Ok(Expr {
11570                        kind: ExprKind::FuncCall {
11571                            name: "size".to_string(),
11572                            args: vec![],
11573                        },
11574                        line,
11575                    });
11576                }
11577                let a = self.parse_one_arg_or_default()?;
11578                Ok(Expr {
11579                    kind: ExprKind::FuncCall {
11580                        name: "size".to_string(),
11581                        args: vec![a],
11582                    },
11583                    line,
11584                })
11585            }
11586            "list_count" | "list_size" | "count" | "len" | "cnt" => {
11587                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11588                    return Ok(e);
11589                }
11590                if self.pipe_supplies_slurped_list_operand() {
11591                    return Ok(Expr {
11592                        kind: ExprKind::FuncCall {
11593                            name: name.clone(),
11594                            args: vec![],
11595                        },
11596                        line,
11597                    });
11598                }
11599                // `len(EXPR)` / `cnt(EXPR)` / `count(EXPR)` with a tight `(` —
11600                // the parens are function-call syntax, not a parenthesized
11601                // list: stop the argument at `)` so `len(@a) % 2 == 1` is
11602                // `(len(@a)) % 2 == 1`, not `len(@a % 2 == 1)`. Empty parens
11603                // `len()` collapse to a zero-arg call (use the piped operand
11604                // or `$_`). Bare `len` followed by a low-precedence operator
11605                // (`==`, `&&`, `?`, …) also defaults to a zero-arg call so
11606                // `{ len == 0 }` works as a block predicate on the topic.
11607                // Bare `len EXPR` (no parens, e.g. `len @arr`) goes through
11608                // the greedy list-arg parser; this means `len @a + len @b`
11609                // is `len(@a + len(@b))` (returning the length of the sum
11610                // string), not `(len @a) + (len @b)`. Use explicit parens
11611                // when combining `len` with `+`, `-`, comparisons, etc.
11612                let args = if matches!(self.peek(), Token::LParen) {
11613                    self.advance();
11614                    if matches!(self.peek(), Token::RParen) {
11615                        self.advance();
11616                        Vec::new()
11617                    } else {
11618                        let inner = self.parse_expression()?;
11619                        self.expect(&Token::RParen)?;
11620                        vec![inner]
11621                    }
11622                } else if self.peek_is_named_unary_terminator() {
11623                    Vec::new()
11624                } else {
11625                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
11626                    if progress.is_some() {
11627                        return Err(self.syntax_err(
11628                            "`progress =>` is not supported for list_count / list_size / count / cnt",
11629                            line,
11630                        ));
11631                    }
11632                    vec![list]
11633                };
11634                Ok(Expr {
11635                    kind: ExprKind::FuncCall {
11636                        name: name.clone(),
11637                        args,
11638                    },
11639                    line,
11640                })
11641            }
11642            "shuffle" | "shuffled" => {
11643                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11644                    return Ok(e);
11645                }
11646                if self.pipe_supplies_slurped_list_operand() {
11647                    return Ok(Expr {
11648                        kind: ExprKind::FuncCall {
11649                            name: "shuffle".to_string(),
11650                            args: vec![],
11651                        },
11652                        line,
11653                    });
11654                }
11655                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
11656                if progress.is_some() {
11657                    return Err(self.syntax_err("`progress =>` is not supported for shuffle", line));
11658                }
11659                Ok(Expr {
11660                    kind: ExprKind::FuncCall {
11661                        name: "shuffle".to_string(),
11662                        args: vec![list],
11663                    },
11664                    line,
11665                })
11666            }
11667            "chunked" => {
11668                let mut parts = Vec::new();
11669                if self.eat(&Token::LParen) {
11670                    if !matches!(self.peek(), Token::RParen) {
11671                        parts.push(self.parse_assign_expr()?);
11672                        while self.eat(&Token::Comma) {
11673                            if matches!(self.peek(), Token::RParen) {
11674                                break;
11675                            }
11676                            parts.push(self.parse_assign_expr()?);
11677                        }
11678                    }
11679                    self.expect(&Token::RParen)?;
11680                } else {
11681                    // Paren-less `chunked N`: `|>` is a hard terminator, not
11682                    // an operator inside the arg (see
11683                    // `parse_assign_expr_stop_at_pipe`).
11684                    parts.push(self.parse_assign_expr_stop_at_pipe()?);
11685                    loop {
11686                        if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
11687                            break;
11688                        }
11689                        if matches!(
11690                            self.peek(),
11691                            Token::Semicolon
11692                                | Token::RBrace
11693                                | Token::RParen
11694                                | Token::Eof
11695                                | Token::PipeForward
11696                        ) {
11697                            break;
11698                        }
11699                        if self.peek_is_postfix_stmt_modifier_keyword() {
11700                            break;
11701                        }
11702                        parts.push(self.parse_assign_expr_stop_at_pipe()?);
11703                    }
11704                }
11705                if parts.len() == 1 {
11706                    let n = parts.pop().unwrap();
11707                    return Ok(Expr {
11708                        kind: ExprKind::FuncCall {
11709                            name: "chunked".to_string(),
11710                            args: vec![n],
11711                        },
11712                        line,
11713                    });
11714                }
11715                if parts.is_empty() {
11716                    return Ok(Expr {
11717                        kind: ExprKind::FuncCall {
11718                            name: "chunked".to_string(),
11719                            args: parts,
11720                        },
11721                        line,
11722                    });
11723                }
11724                if parts.len() == 2 {
11725                    let n = parts.pop().unwrap();
11726                    let list = parts.pop().unwrap();
11727                    return Ok(Expr {
11728                        kind: ExprKind::FuncCall {
11729                            name: "chunked".to_string(),
11730                            args: vec![list, n],
11731                        },
11732                        line,
11733                    });
11734                }
11735                Err(self.syntax_err(
11736                    "chunked: use LIST |> chunked(N) or chunked((1,2,3), 2)",
11737                    line,
11738                ))
11739            }
11740            "windowed" => {
11741                let mut parts = Vec::new();
11742                if self.eat(&Token::LParen) {
11743                    if !matches!(self.peek(), Token::RParen) {
11744                        parts.push(self.parse_assign_expr()?);
11745                        while self.eat(&Token::Comma) {
11746                            if matches!(self.peek(), Token::RParen) {
11747                                break;
11748                            }
11749                            parts.push(self.parse_assign_expr()?);
11750                        }
11751                    }
11752                    self.expect(&Token::RParen)?;
11753                } else {
11754                    // Paren-less `windowed N`: same `|>`-terminator rule as
11755                    // `chunked` above.
11756                    parts.push(self.parse_assign_expr_stop_at_pipe()?);
11757                    loop {
11758                        if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
11759                            break;
11760                        }
11761                        if matches!(
11762                            self.peek(),
11763                            Token::Semicolon
11764                                | Token::RBrace
11765                                | Token::RParen
11766                                | Token::Eof
11767                                | Token::PipeForward
11768                        ) {
11769                            break;
11770                        }
11771                        if self.peek_is_postfix_stmt_modifier_keyword() {
11772                            break;
11773                        }
11774                        parts.push(self.parse_assign_expr_stop_at_pipe()?);
11775                    }
11776                }
11777                if parts.len() == 1 {
11778                    let n = parts.pop().unwrap();
11779                    return Ok(Expr {
11780                        kind: ExprKind::FuncCall {
11781                            name: "windowed".to_string(),
11782                            args: vec![n],
11783                        },
11784                        line,
11785                    });
11786                }
11787                if parts.is_empty() {
11788                    return Ok(Expr {
11789                        kind: ExprKind::FuncCall {
11790                            name: "windowed".to_string(),
11791                            args: parts,
11792                        },
11793                        line,
11794                    });
11795                }
11796                if parts.len() == 2 {
11797                    let n = parts.pop().unwrap();
11798                    let list = parts.pop().unwrap();
11799                    return Ok(Expr {
11800                        kind: ExprKind::FuncCall {
11801                            name: "windowed".to_string(),
11802                            args: vec![list, n],
11803                        },
11804                        line,
11805                    });
11806                }
11807                Err(self.syntax_err(
11808                    "windowed: use LIST |> windowed(N) or windowed((1,2,3), 2)",
11809                    line,
11810                ))
11811            }
11812            "any" | "all" | "none" => {
11813                // `any(CODEREF, LIST)` with parens — parse as normal call.
11814                if matches!(self.peek(), Token::LParen) {
11815                    self.advance();
11816                    let args = self.parse_arg_list()?;
11817                    self.expect(&Token::RParen)?;
11818                    return Ok(Expr {
11819                        kind: ExprKind::FuncCall {
11820                            name: name.clone(),
11821                            args,
11822                        },
11823                        line,
11824                    });
11825                }
11826                // Coderef-in-block-position: `any $f LIST` / `any $f, LIST` /
11827                // `LIST |> any $f`. Same shape as the block form but uses a
11828                // value expression where `{ BLOCK }` would go.
11829                if let Some(args) = self.try_parse_coderef_listop_args(line)? {
11830                    return Ok(Expr {
11831                        kind: ExprKind::FuncCall {
11832                            name: name.clone(),
11833                            args,
11834                        },
11835                        line,
11836                    });
11837                }
11838                // `any BLOCK LIST` without parens.
11839                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11840                if progress.is_some() {
11841                    return Err(self.syntax_err(
11842                        "`progress =>` is not supported for any/all/none (use pany for parallel + progress)",
11843                        line,
11844                    ));
11845                }
11846                let cr = Expr {
11847                    kind: ExprKind::CodeRef {
11848                        params: vec![],
11849                        body: block,
11850                    },
11851                    line,
11852                };
11853                Ok(Expr {
11854                    kind: ExprKind::FuncCall {
11855                        name: name.clone(),
11856                        args: vec![cr, list],
11857                    },
11858                    line,
11859                })
11860            }
11861            // Ruby `detect` / `find` — same as `first` (first element matching block).
11862            "first" | "detect" | "find" => {
11863                // `first(CODEREF, LIST)` with parens — parse as normal call.
11864                if matches!(self.peek(), Token::LParen) {
11865                    self.advance();
11866                    let args = self.parse_arg_list()?;
11867                    self.expect(&Token::RParen)?;
11868                    return Ok(Expr {
11869                        kind: ExprKind::FuncCall {
11870                            name: "first".to_string(),
11871                            args,
11872                        },
11873                        line,
11874                    });
11875                }
11876                // Coderef-in-block-position: `first $f LIST` / `LIST |> first $f`.
11877                if let Some(args) = self.try_parse_coderef_listop_args(line)? {
11878                    return Ok(Expr {
11879                        kind: ExprKind::FuncCall {
11880                            name: "first".to_string(),
11881                            args,
11882                        },
11883                        line,
11884                    });
11885                }
11886                // `first BLOCK LIST` without parens.
11887                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11888                if progress.is_some() {
11889                    return Err(self.syntax_err(
11890                        "`progress =>` is not supported for first/detect/find (use pfirst for parallel + progress)",
11891                        line,
11892                    ));
11893                }
11894                let cr = Expr {
11895                    kind: ExprKind::CodeRef {
11896                        params: vec![],
11897                        body: block,
11898                    },
11899                    line,
11900                };
11901                Ok(Expr {
11902                    kind: ExprKind::FuncCall {
11903                        name: "first".to_string(),
11904                        args: vec![cr, list],
11905                    },
11906                    line,
11907                })
11908            }
11909            "take_while" | "drop_while" | "skip_while" | "reject" | "grepv" | "tap" | "peek"
11910            | "partition" | "min_by" | "max_by" | "zip_with" | "count_by" => {
11911                // Coderef-in-block-position: `take_while $f LIST` etc.
11912                if let Some(args) = self.try_parse_coderef_listop_args(line)? {
11913                    return Ok(Expr {
11914                        kind: ExprKind::FuncCall {
11915                            name: name.to_string(),
11916                            args,
11917                        },
11918                        line,
11919                    });
11920                }
11921                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11922                if progress.is_some() {
11923                    return Err(
11924                        self.syntax_err(format!("`progress =>` is not supported for {name}"), line)
11925                    );
11926                }
11927                let cr = Expr {
11928                    kind: ExprKind::CodeRef {
11929                        params: vec![],
11930                        body: block,
11931                    },
11932                    line,
11933                };
11934                Ok(Expr {
11935                    kind: ExprKind::FuncCall {
11936                        name: name.to_string(),
11937                        args: vec![cr, list],
11938                    },
11939                    line,
11940                })
11941            }
11942            "group_by" | "chunk_by" => {
11943                if matches!(self.peek(), Token::LBrace) {
11944                    let (block, list) = self.parse_block_list()?;
11945                    let cr = Expr {
11946                        kind: ExprKind::CodeRef {
11947                            params: vec![],
11948                            body: block,
11949                        },
11950                        line,
11951                    };
11952                    Ok(Expr {
11953                        kind: ExprKind::FuncCall {
11954                            name: name.to_string(),
11955                            args: vec![cr, list],
11956                        },
11957                        line,
11958                    })
11959                } else {
11960                    let key_expr = self.parse_assign_expr()?;
11961                    self.expect(&Token::Comma)?;
11962                    let list_parts = self.parse_list_until_terminator()?;
11963                    let list_expr = if list_parts.len() == 1 {
11964                        list_parts.into_iter().next().unwrap()
11965                    } else {
11966                        Expr {
11967                            kind: ExprKind::List(list_parts),
11968                            line,
11969                        }
11970                    };
11971                    Ok(Expr {
11972                        kind: ExprKind::FuncCall {
11973                            name: name.to_string(),
11974                            args: vec![key_expr, list_expr],
11975                        },
11976                        line,
11977                    })
11978                }
11979            }
11980            "with_index" => {
11981                if self.pipe_supplies_slurped_list_operand() {
11982                    return Ok(Expr {
11983                        kind: ExprKind::FuncCall {
11984                            name: "with_index".to_string(),
11985                            args: vec![],
11986                        },
11987                        line,
11988                    });
11989                }
11990                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
11991                if progress.is_some() {
11992                    return Err(
11993                        self.syntax_err("`progress =>` is not supported for with_index", line)
11994                    );
11995                }
11996                Ok(Expr {
11997                    kind: ExprKind::FuncCall {
11998                        name: "with_index".to_string(),
11999                        args: vec![list],
12000                    },
12001                    line,
12002                })
12003            }
12004            "pcache" => {
12005                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
12006                Ok(Expr {
12007                    kind: ExprKind::PcacheExpr {
12008                        block,
12009                        list: Box::new(list),
12010                        progress: progress.map(Box::new),
12011                    },
12012                    line,
12013                })
12014            }
12015            "pselect" => {
12016                let paren = self.eat(&Token::LParen);
12017                let (receivers, timeout) = self.parse_comma_expr_list_with_timeout_tail(paren)?;
12018                if paren {
12019                    self.expect(&Token::RParen)?;
12020                }
12021                if receivers.is_empty() {
12022                    return Err(self.syntax_err("pselect needs at least one receiver", line));
12023                }
12024                Ok(Expr {
12025                    kind: ExprKind::PselectExpr {
12026                        receivers,
12027                        timeout: timeout.map(Box::new),
12028                    },
12029                    line,
12030                })
12031            }
12032            "open" => {
12033                let paren = matches!(self.peek(), Token::LParen);
12034                if paren {
12035                    self.advance();
12036                }
12037                if matches!(self.peek(), Token::Ident(ref s) if s == "my") {
12038                    self.advance();
12039                    let name = self.parse_scalar_var_name()?;
12040                    self.expect(&Token::Comma)?;
12041                    let mode = self.parse_assign_expr()?;
12042                    let file = if self.eat(&Token::Comma) {
12043                        Some(self.parse_assign_expr()?)
12044                    } else {
12045                        None
12046                    };
12047                    if paren {
12048                        self.expect(&Token::RParen)?;
12049                    }
12050                    Ok(Expr {
12051                        kind: ExprKind::Open {
12052                            handle: Box::new(Expr {
12053                                kind: ExprKind::OpenMyHandle { name },
12054                                line,
12055                            }),
12056                            mode: Box::new(mode),
12057                            file: file.map(Box::new),
12058                        },
12059                        line,
12060                    })
12061                } else {
12062                    let args = if paren {
12063                        self.parse_arg_list()?
12064                    } else {
12065                        self.parse_list_until_terminator()?
12066                    };
12067                    if paren {
12068                        self.expect(&Token::RParen)?;
12069                    }
12070                    if args.len() < 2 {
12071                        return Err(self.syntax_err("open requires at least 2 arguments", line));
12072                    }
12073                    Ok(Expr {
12074                        kind: ExprKind::Open {
12075                            handle: Box::new(args[0].clone()),
12076                            mode: Box::new(args[1].clone()),
12077                            file: args.get(2).cloned().map(Box::new),
12078                        },
12079                        line,
12080                    })
12081                }
12082            }
12083            "close" => {
12084                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12085                    return Ok(e);
12086                }
12087                let a = self.parse_one_arg_or_default()?;
12088                Ok(Expr {
12089                    kind: ExprKind::Close(Box::new(a)),
12090                    line,
12091                })
12092            }
12093            "opendir" => {
12094                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12095                    return Ok(e);
12096                }
12097                let args = self.parse_builtin_args()?;
12098                if args.len() != 2 {
12099                    return Err(self.syntax_err("opendir requires two arguments", line));
12100                }
12101                Ok(Expr {
12102                    kind: ExprKind::Opendir {
12103                        handle: Box::new(args[0].clone()),
12104                        path: Box::new(args[1].clone()),
12105                    },
12106                    line,
12107                })
12108            }
12109            "readdir" => {
12110                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12111                    return Ok(e);
12112                }
12113                let a = self.parse_one_arg()?;
12114                Ok(Expr {
12115                    kind: ExprKind::Readdir(Box::new(a)),
12116                    line,
12117                })
12118            }
12119            "closedir" => {
12120                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12121                    return Ok(e);
12122                }
12123                let a = self.parse_one_arg()?;
12124                Ok(Expr {
12125                    kind: ExprKind::Closedir(Box::new(a)),
12126                    line,
12127                })
12128            }
12129            "rewinddir" => {
12130                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12131                    return Ok(e);
12132                }
12133                let a = self.parse_one_arg()?;
12134                Ok(Expr {
12135                    kind: ExprKind::Rewinddir(Box::new(a)),
12136                    line,
12137                })
12138            }
12139            "telldir" => {
12140                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12141                    return Ok(e);
12142                }
12143                let a = self.parse_one_arg()?;
12144                Ok(Expr {
12145                    kind: ExprKind::Telldir(Box::new(a)),
12146                    line,
12147                })
12148            }
12149            "seekdir" => {
12150                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12151                    return Ok(e);
12152                }
12153                let args = self.parse_builtin_args()?;
12154                if args.len() != 2 {
12155                    return Err(self.syntax_err("seekdir requires two arguments", line));
12156                }
12157                Ok(Expr {
12158                    kind: ExprKind::Seekdir {
12159                        handle: Box::new(args[0].clone()),
12160                        position: Box::new(args[1].clone()),
12161                    },
12162                    line,
12163                })
12164            }
12165            "eof" => {
12166                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12167                    return Ok(e);
12168                }
12169                if matches!(self.peek(), Token::LParen) {
12170                    self.advance();
12171                    if matches!(self.peek(), Token::RParen) {
12172                        self.advance();
12173                        Ok(Expr {
12174                            kind: ExprKind::Eof(None),
12175                            line,
12176                        })
12177                    } else {
12178                        let a = self.parse_expression()?;
12179                        self.expect(&Token::RParen)?;
12180                        Ok(Expr {
12181                            kind: ExprKind::Eof(Some(Box::new(a))),
12182                            line,
12183                        })
12184                    }
12185                } else {
12186                    Ok(Expr {
12187                        kind: ExprKind::Eof(None),
12188                        line,
12189                    })
12190                }
12191            }
12192            "system" => {
12193                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12194                    return Ok(e);
12195                }
12196                let args = self.parse_builtin_args()?;
12197                Ok(Expr {
12198                    kind: ExprKind::System(args),
12199                    line,
12200                })
12201            }
12202            "exec" => {
12203                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12204                    return Ok(e);
12205                }
12206                let args = self.parse_builtin_args()?;
12207                Ok(Expr {
12208                    kind: ExprKind::Exec(args),
12209                    line,
12210                })
12211            }
12212            "eval" => {
12213                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12214                    return Ok(e);
12215                }
12216                let a = if matches!(self.peek(), Token::LBrace) {
12217                    let block = self.parse_block()?;
12218                    Expr {
12219                        kind: ExprKind::CodeRef {
12220                            params: vec![],
12221                            body: block,
12222                        },
12223                        line,
12224                    }
12225                } else {
12226                    self.parse_one_arg_or_default()?
12227                };
12228                Ok(Expr {
12229                    kind: ExprKind::Eval(Box::new(a)),
12230                    line,
12231                })
12232            }
12233            "do" => {
12234                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12235                    return Ok(e);
12236                }
12237                let a = self.parse_one_arg()?;
12238                Ok(Expr {
12239                    kind: ExprKind::Do(Box::new(a)),
12240                    line,
12241                })
12242            }
12243            "require" => {
12244                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12245                    return Ok(e);
12246                }
12247                let a = self.parse_one_arg()?;
12248                Ok(Expr {
12249                    kind: ExprKind::Require(Box::new(a)),
12250                    line,
12251                })
12252            }
12253            "exit" => {
12254                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12255                    return Ok(e);
12256                }
12257                if matches!(
12258                    self.peek(),
12259                    Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
12260                ) {
12261                    Ok(Expr {
12262                        kind: ExprKind::Exit(None),
12263                        line,
12264                    })
12265                } else {
12266                    let a = self.parse_one_arg()?;
12267                    Ok(Expr {
12268                        kind: ExprKind::Exit(Some(Box::new(a))),
12269                        line,
12270                    })
12271                }
12272            }
12273            "chdir" => {
12274                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12275                    return Ok(e);
12276                }
12277                let a = self.parse_one_arg_or_default()?;
12278                Ok(Expr {
12279                    kind: ExprKind::Chdir(Box::new(a)),
12280                    line,
12281                })
12282            }
12283            "mkdir" => {
12284                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12285                    return Ok(e);
12286                }
12287                let args = self.parse_builtin_args()?;
12288                Ok(Expr {
12289                    kind: ExprKind::Mkdir {
12290                        path: Box::new(args[0].clone()),
12291                        mode: args.get(1).cloned().map(Box::new),
12292                    },
12293                    line,
12294                })
12295            }
12296            "unlink" | "rm" => {
12297                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12298                    return Ok(e);
12299                }
12300                let args = self.parse_builtin_args()?;
12301                Ok(Expr {
12302                    kind: ExprKind::Unlink(args),
12303                    line,
12304                })
12305            }
12306            "rename" => {
12307                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12308                    return Ok(e);
12309                }
12310                let args = self.parse_builtin_args()?;
12311                if args.len() != 2 {
12312                    return Err(self.syntax_err("rename requires two arguments", line));
12313                }
12314                Ok(Expr {
12315                    kind: ExprKind::Rename {
12316                        old: Box::new(args[0].clone()),
12317                        new: Box::new(args[1].clone()),
12318                    },
12319                    line,
12320                })
12321            }
12322            "chmod" => {
12323                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12324                    return Ok(e);
12325                }
12326                let args = self.parse_builtin_args()?;
12327                if args.len() < 2 {
12328                    return Err(self.syntax_err("chmod requires mode and at least one file", line));
12329                }
12330                Ok(Expr {
12331                    kind: ExprKind::Chmod(args),
12332                    line,
12333                })
12334            }
12335            "chown" => {
12336                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12337                    return Ok(e);
12338                }
12339                let args = self.parse_builtin_args()?;
12340                if args.len() < 3 {
12341                    return Err(
12342                        self.syntax_err("chown requires uid, gid, and at least one file", line)
12343                    );
12344                }
12345                Ok(Expr {
12346                    kind: ExprKind::Chown(args),
12347                    line,
12348                })
12349            }
12350            "stat" => {
12351                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12352                    return Ok(e);
12353                }
12354                let args = self.parse_builtin_args()?;
12355                let arg = if args.len() == 1 {
12356                    args[0].clone()
12357                } else if args.is_empty() {
12358                    Expr {
12359                        kind: ExprKind::ScalarVar("_".into()),
12360                        line,
12361                    }
12362                } else {
12363                    return Err(self.syntax_err("stat requires zero or one argument", line));
12364                };
12365                Ok(Expr {
12366                    kind: ExprKind::Stat(Box::new(arg)),
12367                    line,
12368                })
12369            }
12370            "lstat" => {
12371                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12372                    return Ok(e);
12373                }
12374                let args = self.parse_builtin_args()?;
12375                let arg = if args.len() == 1 {
12376                    args[0].clone()
12377                } else if args.is_empty() {
12378                    Expr {
12379                        kind: ExprKind::ScalarVar("_".into()),
12380                        line,
12381                    }
12382                } else {
12383                    return Err(self.syntax_err("lstat requires zero or one argument", line));
12384                };
12385                Ok(Expr {
12386                    kind: ExprKind::Lstat(Box::new(arg)),
12387                    line,
12388                })
12389            }
12390            "link" => {
12391                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12392                    return Ok(e);
12393                }
12394                let args = self.parse_builtin_args()?;
12395                if args.len() != 2 {
12396                    return Err(self.syntax_err("link requires two arguments", line));
12397                }
12398                Ok(Expr {
12399                    kind: ExprKind::Link {
12400                        old: Box::new(args[0].clone()),
12401                        new: Box::new(args[1].clone()),
12402                    },
12403                    line,
12404                })
12405            }
12406            "symlink" => {
12407                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12408                    return Ok(e);
12409                }
12410                let args = self.parse_builtin_args()?;
12411                if args.len() != 2 {
12412                    return Err(self.syntax_err("symlink requires two arguments", line));
12413                }
12414                Ok(Expr {
12415                    kind: ExprKind::Symlink {
12416                        old: Box::new(args[0].clone()),
12417                        new: Box::new(args[1].clone()),
12418                    },
12419                    line,
12420                })
12421            }
12422            "readlink" => {
12423                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12424                    return Ok(e);
12425                }
12426                let args = self.parse_builtin_args()?;
12427                let arg = if args.len() == 1 {
12428                    args[0].clone()
12429                } else if args.is_empty() {
12430                    Expr {
12431                        kind: ExprKind::ScalarVar("_".into()),
12432                        line,
12433                    }
12434                } else {
12435                    return Err(self.syntax_err("readlink requires zero or one argument", line));
12436                };
12437                Ok(Expr {
12438                    kind: ExprKind::Readlink(Box::new(arg)),
12439                    line,
12440                })
12441            }
12442            "files" => {
12443                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12444                    return Ok(e);
12445                }
12446                let args = self.parse_builtin_args()?;
12447                Ok(Expr {
12448                    kind: ExprKind::Files(args),
12449                    line,
12450                })
12451            }
12452            "filesf" | "f" => {
12453                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12454                    return Ok(e);
12455                }
12456                let args = self.parse_builtin_args()?;
12457                Ok(Expr {
12458                    kind: ExprKind::Filesf(args),
12459                    line,
12460                })
12461            }
12462            "fr" => {
12463                let args = self.parse_builtin_args()?;
12464                Ok(Expr {
12465                    kind: ExprKind::FilesfRecursive(args),
12466                    line,
12467                })
12468            }
12469            "dirs" | "d" => {
12470                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12471                    return Ok(e);
12472                }
12473                let args = self.parse_builtin_args()?;
12474                Ok(Expr {
12475                    kind: ExprKind::Dirs(args),
12476                    line,
12477                })
12478            }
12479            "dr" => {
12480                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12481                    return Ok(e);
12482                }
12483                let args = self.parse_builtin_args()?;
12484                Ok(Expr {
12485                    kind: ExprKind::DirsRecursive(args),
12486                    line,
12487                })
12488            }
12489            "sym_links" => {
12490                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12491                    return Ok(e);
12492                }
12493                let args = self.parse_builtin_args()?;
12494                Ok(Expr {
12495                    kind: ExprKind::SymLinks(args),
12496                    line,
12497                })
12498            }
12499            "sockets" => {
12500                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12501                    return Ok(e);
12502                }
12503                let args = self.parse_builtin_args()?;
12504                Ok(Expr {
12505                    kind: ExprKind::Sockets(args),
12506                    line,
12507                })
12508            }
12509            "pipes" => {
12510                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12511                    return Ok(e);
12512                }
12513                let args = self.parse_builtin_args()?;
12514                Ok(Expr {
12515                    kind: ExprKind::Pipes(args),
12516                    line,
12517                })
12518            }
12519            "block_devices" => {
12520                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12521                    return Ok(e);
12522                }
12523                let args = self.parse_builtin_args()?;
12524                Ok(Expr {
12525                    kind: ExprKind::BlockDevices(args),
12526                    line,
12527                })
12528            }
12529            "char_devices" => {
12530                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12531                    return Ok(e);
12532                }
12533                let args = self.parse_builtin_args()?;
12534                Ok(Expr {
12535                    kind: ExprKind::CharDevices(args),
12536                    line,
12537                })
12538            }
12539            "exe" | "executables" => {
12540                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12541                    return Ok(e);
12542                }
12543                let args = self.parse_builtin_args()?;
12544                Ok(Expr {
12545                    kind: ExprKind::Executables(args),
12546                    line,
12547                })
12548            }
12549            "glob" => {
12550                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12551                    return Ok(e);
12552                }
12553                let args = self.parse_builtin_args()?;
12554                Ok(Expr {
12555                    kind: ExprKind::Glob(args),
12556                    line,
12557                })
12558            }
12559            "glob_par" => {
12560                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12561                    return Ok(e);
12562                }
12563                let (args, progress) = self.parse_glob_par_or_par_sed_args()?;
12564                Ok(Expr {
12565                    kind: ExprKind::GlobPar { args, progress },
12566                    line,
12567                })
12568            }
12569            "par_sed" => {
12570                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12571                    return Ok(e);
12572                }
12573                let (args, progress) = self.parse_glob_par_or_par_sed_args()?;
12574                Ok(Expr {
12575                    kind: ExprKind::ParSed { args, progress },
12576                    line,
12577                })
12578            }
12579            "bless" => {
12580                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12581                    return Ok(e);
12582                }
12583                let args = self.parse_builtin_args()?;
12584                Ok(Expr {
12585                    kind: ExprKind::Bless {
12586                        ref_expr: Box::new(args[0].clone()),
12587                        class: args.get(1).cloned().map(Box::new),
12588                    },
12589                    line,
12590                })
12591            }
12592            "caller" => {
12593                if matches!(self.peek(), Token::LParen) {
12594                    self.advance();
12595                    if matches!(self.peek(), Token::RParen) {
12596                        self.advance();
12597                        Ok(Expr {
12598                            kind: ExprKind::Caller(None),
12599                            line,
12600                        })
12601                    } else {
12602                        let a = self.parse_expression()?;
12603                        self.expect(&Token::RParen)?;
12604                        Ok(Expr {
12605                            kind: ExprKind::Caller(Some(Box::new(a))),
12606                            line,
12607                        })
12608                    }
12609                } else {
12610                    Ok(Expr {
12611                        kind: ExprKind::Caller(None),
12612                        line,
12613                    })
12614                }
12615            }
12616            "wantarray" => {
12617                if matches!(self.peek(), Token::LParen) {
12618                    self.advance();
12619                    self.expect(&Token::RParen)?;
12620                }
12621                Ok(Expr {
12622                    kind: ExprKind::Wantarray,
12623                    line,
12624                })
12625            }
12626            "sub" => {
12627                // In no-interop mode, `sub {}` is not valid — must use `fn {}`
12628                if crate::no_interop_mode() {
12629                    return Err(self.syntax_err(
12630                        "stryke uses `fn {}` instead of `sub {}` (--no-interop)",
12631                        line,
12632                    ));
12633                }
12634                // Anonymous sub — optional prototype `sub () { }` (e.g. Carp.pm `*X = sub () { 1 }`)
12635                let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
12636                let body = self.parse_block()?;
12637                Ok(Expr {
12638                    kind: ExprKind::CodeRef { params, body },
12639                    line,
12640                })
12641            }
12642            "fn" => {
12643                // Anonymous fn — stryke syntax for anonymous subroutines
12644                let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
12645                self.parse_sub_attributes()?;
12646                let body = self.parse_fn_eq_body_or_block(false)?;
12647                Ok(Expr {
12648                    kind: ExprKind::CodeRef { params, body },
12649                    line,
12650                })
12651            }
12652            _ => {
12653                // Generic function call
12654                // Check for fat arrow (bareword string in hash) — except for
12655                // topic-slot barewords (`_`, `_<`, `_0`, `_0<`, …), which must
12656                // resolve to the topic value, not the literal name.
12657                if matches!(self.peek(), Token::FatArrow) && !Self::is_underscore_topic_slot(&name)
12658                {
12659                    return Ok(Expr {
12660                        kind: ExprKind::String(name),
12661                        line,
12662                    });
12663                }
12664                // Bare `_` in expression position → topic variable `$_`.
12665                // Allows concise blocks: `map { _ * 2 }`, `fi { _ > 5 }`.
12666                // Also handles the outer-topic chain: `_<`, `_<<`, `_<<<`,
12667                // `_<<<<` for 1..4 frames up — and the positional matrix:
12668                // `_0<<<<`, `_1<<<<`, `_N<<<<` (N positionals × 5 levels).
12669                // `_0` is canonically aliased to `_` at every level (see
12670                // `Scope::set_closure_args`).
12671                //
12672                // Stryke string-index sugar: `_[N]` (bareword, no sigil) is
12673                // an alias for `_!N!` — char-of-topic substring. The sigil
12674                // form `$_[N]` keeps Perl's `@_`-access semantics (first
12675                // positional arg). We dispatch here, before the generic
12676                // ArrayElement path, so the AST for `_[N]` carries the
12677                // synthetic `__topicstr__$NAME` flag the interpreter / VM
12678                // strip and route to char-of-string.
12679                if Self::is_underscore_topic_slot(&name) {
12680                    if matches!(self.peek(), Token::LBracket) && self.peek_line() == line {
12681                        self.advance(); // [
12682                        let index = self.parse_expression()?;
12683                        self.expect(&Token::RBracket)?;
12684                        return Ok(Expr {
12685                            kind: ExprKind::ArrayElement {
12686                                array: format!("__topicstr__{}", name),
12687                                index: Box::new(index),
12688                            },
12689                            line,
12690                        });
12691                    }
12692                    return Ok(Expr {
12693                        kind: ExprKind::ScalarVar(name.clone()),
12694                        line,
12695                    });
12696                }
12697                // Function call with optional parens
12698                if matches!(self.peek(), Token::LParen) {
12699                    self.advance();
12700                    let args = self.parse_arg_list()?;
12701                    self.expect(&Token::RParen)?;
12702                    Ok(Expr {
12703                        kind: ExprKind::FuncCall { name, args },
12704                        line,
12705                    })
12706                } else if self.peek().is_term_start()
12707                    && !(matches!(self.peek(), Token::Ident(ref kw) if kw == "sub")
12708                        && matches!(self.peek_at(1), Token::Ident(_)))
12709                    && !(self.suppress_parenless_call > 0 && matches!(self.peek(), Token::Ident(_)))
12710                    && !(matches!(self.peek(), Token::LBrace)
12711                        && self.peek_line() > self.prev_line())
12712                    && !(matches!(self.peek(), Token::BitNot)
12713                        && self.suppress_tilde_range == 0
12714                        && matches!(
12715                            self.peek_at(1),
12716                            Token::Ident(_) | Token::Integer(_) | Token::Float(_)
12717                        ))
12718                {
12719                    // Perl allows func arg without parens
12720                    // Guard: `sub <name> { }` is a named sub declaration (new
12721                    // statement), not an argument to the preceding call.
12722                    // Guard: suppress_parenless_call > 0 with Ident prevents consuming
12723                    // barewords (used by thread macro so `t Color::Red p` treats
12724                    // `p` as a stage, not an argument to the enum variant), but
12725                    // still allows `{` for struct/hash literals like `t Foo { x => 1 } p`.
12726                    // Guard: `{` on a new line is a new statement (hashref/block),
12727                    // not an argument to the preceding bareword call.
12728                    // Guard: `~Ident` / `~Integer` / `~Float` after a bareword is
12729                    // the universal-tilde range separator (`I~M~5`, `Mon~Fri`,
12730                    // `Jan~Dec~2`), not unary BitNot of an arg. Bail to Bareword
12731                    // so the outer `parse_range` consumes `~` as the range op.
12732                    let args = self.parse_list_until_terminator()?;
12733                    Ok(Expr {
12734                        kind: ExprKind::FuncCall { name, args },
12735                        line,
12736                    })
12737                } else {
12738                    // No parens, no visible arguments — emit a Bareword.
12739                    // At runtime, Bareword tries sub resolution first (zero-arg
12740                    // call) and falls back to a string value.  stryke extension
12741                    // contexts (pipe-forward, map/fore) lift Bareword → FuncCall
12742                    // with `$_` injection separately.
12743                    Ok(Expr {
12744                        kind: ExprKind::Bareword(name),
12745                        line,
12746                    })
12747                }
12748            }
12749        }
12750    }
12751
12752    fn parse_print_like(
12753        &mut self,
12754        make: impl FnOnce(Option<String>, Vec<Expr>) -> ExprKind,
12755    ) -> PerlResult<Expr> {
12756        let line = self.peek_line();
12757        // Check for filehandle: print STDERR "msg"  /  print $fh "msg"
12758        let handle = if let Token::Ident(ref h) = self.peek().clone() {
12759            if h.chars().all(|c| c.is_uppercase() || c == '_')
12760                && !matches!(self.peek(), Token::LParen)
12761            {
12762                let h = h.clone();
12763                let saved = self.pos;
12764                self.advance();
12765                // Verify next token is a term start (not operator).
12766                // Guard: `~Ident` / `~Integer` / `~Float` is a universal-tilde
12767                // range separator (`p I~M~5`, `p Mon~Fri`), not unary BitNot of
12768                // an arg. Bail filehandle detection so the bareword `I` flows
12769                // into the regular expression path where `parse_range` consumes
12770                // `~` as the range op.
12771                let is_tilde_range_after = matches!(self.peek(), Token::BitNot)
12772                    && self.suppress_tilde_range == 0
12773                    && matches!(
12774                        self.peek_at(1),
12775                        Token::Ident(_) | Token::Integer(_) | Token::Float(_)
12776                    );
12777                if !is_tilde_range_after
12778                    && (self.peek().is_term_start()
12779                        || matches!(
12780                            self.peek(),
12781                            Token::DoubleString(_)
12782                                | Token::BacktickString(_)
12783                                | Token::SingleString(_)
12784                        ))
12785                {
12786                    Some(h)
12787                } else {
12788                    self.pos = saved;
12789                    None
12790                }
12791            } else {
12792                None
12793            }
12794        } else if let Token::ScalarVar(ref v) = self.peek().clone() {
12795            // `print $fh "msg"` — scalar variable as indirect filehandle.
12796            // Treat as handle when the next token (after $var) is a term-start or
12797            // string literal *without* a preceding comma/operator, matching Perl's
12798            // indirect-object heuristic.
12799            // Exclude `$_` — it's virtually always the topic variable, not a handle.
12800            // Exclude `[` and `{` — those are array/hash subscripts on the variable
12801            // itself (`print $F[0]`, `print $h{k}`), not separate print arguments.
12802            // Exclude statement modifiers (`if`/`unless`/`while`/`until`/`for`/`foreach`)
12803            // — `print $_ if COND` prints `$_` to STDOUT, not to a handle named `$_`.
12804            let v = v.clone();
12805            if v == "_" {
12806                None
12807            } else {
12808                let saved = self.pos;
12809                self.advance();
12810                let next = self.peek().clone();
12811                let is_stmt_modifier = matches!(&next, Token::Ident(kw)
12812                    if matches!(kw.as_str(), "if" | "unless" | "while" | "until" | "for" | "foreach"));
12813                if !is_stmt_modifier
12814                    && !matches!(next, Token::LBracket | Token::LBrace)
12815                    && (next.is_term_start()
12816                        || matches!(
12817                            next,
12818                            Token::DoubleString(_)
12819                                | Token::BacktickString(_)
12820                                | Token::SingleString(_)
12821                        ))
12822                {
12823                    // Next token looks like a print argument — $var is the handle.
12824                    Some(format!("${v}"))
12825                } else {
12826                    self.pos = saved;
12827                    None
12828                }
12829            }
12830        } else {
12831            None
12832        };
12833        // `print()` / `say()` / `printf()` — empty parens default to `$_`,
12834        // matching Perl 5: `perldoc -f print` / `-f say` say "If no arguments
12835        // are given, prints $_." (Same convention as the topic-default unary
12836        // builtins handled in `parse_one_arg_or_default`.)
12837        let args =
12838            if matches!(self.peek(), Token::LParen) && matches!(self.peek_at(1), Token::RParen) {
12839                let line_topic = self.peek_line();
12840                self.advance(); // (
12841                self.advance(); // )
12842                vec![Expr {
12843                    kind: ExprKind::ScalarVar("_".into()),
12844                    line: line_topic,
12845                }]
12846            } else {
12847                self.parse_list_until_terminator()?
12848            };
12849        Ok(Expr {
12850            kind: make(handle, args),
12851            line,
12852        })
12853    }
12854
12855    fn parse_block_list(&mut self) -> PerlResult<(Block, Expr)> {
12856        let block = self.parse_block()?;
12857        let block_end_line = self.prev_line();
12858        self.eat(&Token::Comma);
12859        // On the RHS of `|>`, the list operand is supplied by the piped LHS
12860        // and will be substituted at desugar time — accept a placeholder when
12861        // we're at a terminator here or on a new line (implicit semicolon).
12862        if self.in_pipe_rhs()
12863            && (matches!(
12864                self.peek(),
12865                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
12866            ) || self.peek_line() > block_end_line)
12867        {
12868            let line = self.peek_line();
12869            return Ok((block, self.pipe_placeholder_list(line)));
12870        }
12871        let list = self.parse_expression()?;
12872        Ok((block, list))
12873    }
12874
12875    /// Comma-separated expressions with optional trailing `timeout => SECS` (for `pselect`).
12876    /// When `paren` is true, stops at `)` as well as normal terminators.
12877    fn parse_comma_expr_list_with_timeout_tail(
12878        &mut self,
12879        paren: bool,
12880    ) -> PerlResult<(Vec<Expr>, Option<Expr>)> {
12881        let mut parts = vec![self.parse_assign_expr()?];
12882        loop {
12883            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
12884                break;
12885            }
12886            if paren && matches!(self.peek(), Token::RParen) {
12887                break;
12888            }
12889            if matches!(
12890                self.peek(),
12891                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
12892            ) {
12893                break;
12894            }
12895            if self.peek_is_postfix_stmt_modifier_keyword() {
12896                break;
12897            }
12898            if let Token::Ident(ref kw) = self.peek().clone() {
12899                if kw == "timeout" && matches!(self.peek_at(1), Token::FatArrow) {
12900                    self.advance();
12901                    self.expect(&Token::FatArrow)?;
12902                    let t = self.parse_assign_expr()?;
12903                    return Ok((parts, Some(t)));
12904                }
12905            }
12906            parts.push(self.parse_assign_expr()?);
12907        }
12908        Ok((parts, None))
12909    }
12910
12911    /// `preduce_init EXPR, BLOCK, LIST` with optional `, progress => EXPR`.
12912    fn parse_init_block_then_list_optional_progress(
12913        &mut self,
12914    ) -> PerlResult<(Expr, Block, Expr, Option<Expr>)> {
12915        let init = self.parse_assign_expr()?;
12916        self.expect(&Token::Comma)?;
12917        let block = self.parse_block_or_bareword_block()?;
12918        self.eat(&Token::Comma);
12919        let line = self.peek_line();
12920        if let Token::Ident(ref kw) = self.peek().clone() {
12921            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
12922                self.advance();
12923                self.expect(&Token::FatArrow)?;
12924                let prog = self.parse_assign_expr()?;
12925                return Ok((
12926                    init,
12927                    block,
12928                    Expr {
12929                        kind: ExprKind::List(vec![]),
12930                        line,
12931                    },
12932                    Some(prog),
12933                ));
12934            }
12935        }
12936        if matches!(
12937            self.peek(),
12938            Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
12939        ) {
12940            return Ok((
12941                init,
12942                block,
12943                Expr {
12944                    kind: ExprKind::List(vec![]),
12945                    line,
12946                },
12947                None,
12948            ));
12949        }
12950        let mut parts = vec![self.parse_assign_expr()?];
12951        loop {
12952            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
12953                break;
12954            }
12955            if matches!(
12956                self.peek(),
12957                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
12958            ) {
12959                break;
12960            }
12961            if self.peek_is_postfix_stmt_modifier_keyword() {
12962                break;
12963            }
12964            if let Token::Ident(ref kw) = self.peek().clone() {
12965                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
12966                    self.advance();
12967                    self.expect(&Token::FatArrow)?;
12968                    let prog = self.parse_assign_expr()?;
12969                    return Ok((init, block, merge_expr_list(parts), Some(prog)));
12970                }
12971            }
12972            parts.push(self.parse_assign_expr()?);
12973        }
12974        Ok((init, block, merge_expr_list(parts), None))
12975    }
12976
12977    /// `pmap_on CLUSTER { BLOCK } LIST [, progress => EXPR]` — cluster expr, then same tail as [`Self::parse_block_then_list_optional_progress`].
12978    fn parse_cluster_block_then_list_optional_progress(
12979        &mut self,
12980    ) -> PerlResult<(Expr, Block, Expr, Option<Expr>)> {
12981        // `pmap_on $c { BLOCK } @list` — suppress `$c { ... }` hash-subscript
12982        // auto-arrow so the brace opens the BLOCK, not a `$c->{...}` deref.
12983        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_add(1);
12984        let cluster = self.parse_assign_expr();
12985        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_sub(1);
12986        let cluster = cluster?;
12987        // Accept the canonical `pmap_on $c, { BLOCK } @list` LSP-doc form too.
12988        self.eat(&Token::Comma);
12989        let block = self.parse_block_or_bareword_block()?;
12990        let block_end_line = self.prev_line();
12991        self.eat(&Token::Comma);
12992        let line = self.peek_line();
12993        if let Token::Ident(ref kw) = self.peek().clone() {
12994            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
12995                self.advance();
12996                self.expect(&Token::FatArrow)?;
12997                let prog = self.parse_assign_expr_stop_at_pipe()?;
12998                return Ok((
12999                    cluster,
13000                    block,
13001                    Expr {
13002                        kind: ExprKind::List(vec![]),
13003                        line,
13004                    },
13005                    Some(prog),
13006                ));
13007            }
13008        }
13009        let empty_list_ok = matches!(
13010            self.peek(),
13011            Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
13012        ) || (self.in_pipe_rhs()
13013            && (matches!(self.peek(), Token::Comma) || self.peek_line() > block_end_line));
13014        if empty_list_ok {
13015            return Ok((
13016                cluster,
13017                block,
13018                Expr {
13019                    kind: ExprKind::List(vec![]),
13020                    line,
13021                },
13022                None,
13023            ));
13024        }
13025        let mut parts = vec![self.parse_assign_expr_stop_at_pipe()?];
13026        loop {
13027            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
13028                break;
13029            }
13030            if matches!(
13031                self.peek(),
13032                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
13033            ) {
13034                break;
13035            }
13036            if self.peek_is_postfix_stmt_modifier_keyword() {
13037                break;
13038            }
13039            if let Token::Ident(ref kw) = self.peek().clone() {
13040                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
13041                    self.advance();
13042                    self.expect(&Token::FatArrow)?;
13043                    let prog = self.parse_assign_expr_stop_at_pipe()?;
13044                    return Ok((cluster, block, merge_expr_list(parts), Some(prog)));
13045                }
13046            }
13047            parts.push(self.parse_assign_expr_stop_at_pipe()?);
13048        }
13049        Ok((cluster, block, merge_expr_list(parts), None))
13050    }
13051
13052    /// Like [`parse_block_list`] but supports a trailing `, progress => EXPR`
13053    /// (`pmap`, `pgrep`, `preduce`, `pfor`, `pcache`, `psort`, …).
13054    ///
13055    /// Always invoked for paren-less trailing forms (`pmap { … } LIST`,
13056    /// `pmap { … } LIST, progress => EXPR`), so `|>` must terminate the whole
13057    /// stage — individual list parts and the progress value parse through
13058    /// [`Self::parse_assign_expr_stop_at_pipe`] to keep pipe-forward
13059    /// left-associative in `@a |> pmap { $_ * 2 }, progress => 0 |> join ','`.
13060    fn parse_block_then_list_optional_progress(
13061        &mut self,
13062    ) -> PerlResult<(Block, Expr, Option<Expr>)> {
13063        let block = self.parse_block_or_bareword_block()?;
13064        let block_end_line = self.prev_line();
13065        self.eat(&Token::Comma);
13066        let line = self.peek_line();
13067        if let Token::Ident(ref kw) = self.peek().clone() {
13068            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
13069                self.advance();
13070                self.expect(&Token::FatArrow)?;
13071                let prog = self.parse_assign_expr_stop_at_pipe()?;
13072                return Ok((
13073                    block,
13074                    Expr {
13075                        kind: ExprKind::List(vec![]),
13076                        line,
13077                    },
13078                    Some(prog),
13079                ));
13080            }
13081        }
13082        // An empty list operand is allowed when the next token terminates the
13083        // enclosing context. Inside a pipe-forward RHS, a trailing `,` also
13084        // counts — `foo(bar, @a |> pmap { $_ * 2 }, baz)`. `|>` is also a
13085        // terminator — left-associative chaining leaves the outer `|>` for
13086        // the enclosing pipe-forward loop. A newline after the block also
13087        // terminates in pipe-RHS — the LHS supplies the list, so we must NOT
13088        // greedily eat the next statement (matches `parse_block_list`).
13089        let empty_list_ok = matches!(
13090            self.peek(),
13091            Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
13092        ) || (self.in_pipe_rhs()
13093            && (matches!(self.peek(), Token::Comma) || self.peek_line() > block_end_line));
13094        if empty_list_ok {
13095            return Ok((
13096                block,
13097                Expr {
13098                    kind: ExprKind::List(vec![]),
13099                    line,
13100                },
13101                None,
13102            ));
13103        }
13104        let mut parts = vec![self.parse_assign_expr_stop_at_pipe()?];
13105        loop {
13106            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
13107                break;
13108            }
13109            if matches!(
13110                self.peek(),
13111                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
13112            ) {
13113                break;
13114            }
13115            if self.peek_is_postfix_stmt_modifier_keyword() {
13116                break;
13117            }
13118            if let Token::Ident(ref kw) = self.peek().clone() {
13119                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
13120                    self.advance();
13121                    self.expect(&Token::FatArrow)?;
13122                    let prog = self.parse_assign_expr_stop_at_pipe()?;
13123                    return Ok((block, merge_expr_list(parts), Some(prog)));
13124                }
13125            }
13126            parts.push(self.parse_assign_expr_stop_at_pipe()?);
13127        }
13128        Ok((block, merge_expr_list(parts), None))
13129    }
13130
13131    /// Parse fan/fan_cap arguments: optional count + block or blockless expression.
13132    fn parse_fan_count_and_block(&mut self, line: usize) -> PerlResult<(Option<Box<Expr>>, Block)> {
13133        // `fan { BLOCK }` — no count
13134        if matches!(self.peek(), Token::LBrace) {
13135            let block = self.parse_block()?;
13136            return Ok((None, block));
13137        }
13138        let saved = self.pos;
13139        // Not a brace — first expr could be count or body
13140        let first = self.parse_postfix()?;
13141        if matches!(self.peek(), Token::LBrace) {
13142            // `fan COUNT { BLOCK }`
13143            let block = self.parse_block()?;
13144            Ok((Some(Box::new(first)), block))
13145        } else if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof)
13146            || (matches!(self.peek(), Token::Comma)
13147                && matches!(self.peek_at(1), Token::Ident(ref kw) if kw == "progress"))
13148        {
13149            // `fan EXPR;` — no count, first is the body
13150            let block = self.bareword_to_no_arg_block(first);
13151            Ok((None, block))
13152        } else if matches!(first.kind, ExprKind::Integer(_)) {
13153            // `fan COUNT EXPR` or `fan COUNT, EXPR` — integer count + body
13154            self.eat(&Token::Comma);
13155            let body = self.parse_fan_blockless_body(line)?;
13156            Ok((Some(Box::new(first)), body))
13157        } else {
13158            // Non-integer first (e.g. `$_`) followed by binary op (e.g. `* $_`)
13159            // — backtrack and re-parse as a full body expression.
13160            self.pos = saved;
13161            let body = self.parse_fan_blockless_body(line)?;
13162            Ok((None, body))
13163        }
13164    }
13165
13166    /// Parse a blockless fan/fan_cap body as a full expression (not just postfix).
13167    fn parse_fan_blockless_body(&mut self, line: usize) -> PerlResult<Block> {
13168        if matches!(self.peek(), Token::LBrace) {
13169            return self.parse_block();
13170        }
13171        // Check for bareword (zero-arg sub call) terminated by ; } EOF , or pipe
13172        if let Token::Ident(ref name) = self.peek().clone() {
13173            if matches!(
13174                self.peek_at(1),
13175                Token::Comma | Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
13176            ) {
13177                let name = name.clone();
13178                self.advance();
13179                let body = Expr {
13180                    kind: ExprKind::FuncCall { name, args: vec![] },
13181                    line,
13182                };
13183                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
13184            }
13185        }
13186        // Full expression (handles `$_ * $_`, `$_ + 1`, etc.)
13187        let expr = self.parse_assign_expr_stop_at_pipe()?;
13188        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
13189    }
13190
13191    /// Wrap a parsed expression as a single-statement block, converting bare
13192    /// identifiers to zero-arg calls (`work` → `work()`).
13193    fn bareword_to_no_arg_block(&self, expr: Expr) -> Block {
13194        let line = expr.line;
13195        let body = match &expr.kind {
13196            ExprKind::Bareword(name) => Expr {
13197                kind: ExprKind::FuncCall {
13198                    name: name.clone(),
13199                    args: vec![],
13200                },
13201                line,
13202            },
13203            _ => expr,
13204        };
13205        vec![Statement::new(StmtKind::Expression(body), line)]
13206    }
13207
13208    /// Parse either a `{ BLOCK }` or a bare expression and wrap it as a synthetic block.
13209    ///
13210    /// When the next token is `{`, delegates to [`Self::parse_block`].
13211    /// Otherwise parses a single postfix expression and wraps it as a call
13212    /// with `$_` as argument (for barewords) or a plain expression statement:
13213    ///
13214    /// - Bareword `foo` → `{ foo($_) }`
13215    /// - Other expr     → `{ EXPR }`
13216    fn parse_block_or_bareword_block(&mut self) -> PerlResult<Block> {
13217        if matches!(self.peek(), Token::LBrace) {
13218            return self.parse_block();
13219        }
13220        let line = self.peek_line();
13221        // A lone identifier followed by a list-terminator is a bare sub name:
13222        // `pmap double, @list` → block is `{ double($_) }`, rest is list.
13223        if let Token::Ident(ref name) = self.peek().clone() {
13224            if matches!(
13225                self.peek_at(1),
13226                Token::Comma | Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
13227            ) {
13228                let name = name.clone();
13229                self.advance();
13230                let body = Expr {
13231                    kind: ExprKind::FuncCall {
13232                        name,
13233                        args: vec![Expr {
13234                            kind: ExprKind::ScalarVar("_".to_string()),
13235                            line,
13236                        }],
13237                    },
13238                    line,
13239                };
13240                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
13241            }
13242        }
13243        // Not a simple bareword — parse as expression (e.g. `$_ * 2`, `uc $_`)
13244        let expr = self.parse_assign_expr_stop_at_pipe()?;
13245        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
13246    }
13247
13248    /// Like [`parse_block_or_bareword_block`] but for fan/timer/bench where the
13249    /// bare function takes no args (body runs stand-alone, not per-element).
13250    /// Only consumes a single bareword identifier — does NOT let `parse_primary`
13251    /// greedily swallow subsequent tokens as function arguments.
13252    fn parse_block_or_bareword_block_no_args(&mut self) -> PerlResult<Block> {
13253        if matches!(self.peek(), Token::LBrace) {
13254            return self.parse_block();
13255        }
13256        let line = self.peek_line();
13257        if let Token::Ident(ref name) = self.peek().clone() {
13258            if matches!(
13259                self.peek_at(1),
13260                Token::Comma
13261                    | Token::Semicolon
13262                    | Token::RBrace
13263                    | Token::Eof
13264                    | Token::PipeForward
13265                    | Token::Integer(_)
13266            ) {
13267                let name = name.clone();
13268                self.advance();
13269                let body = Expr {
13270                    kind: ExprKind::FuncCall { name, args: vec![] },
13271                    line,
13272                };
13273                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
13274            }
13275        }
13276        let expr = self.parse_postfix()?;
13277        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
13278    }
13279
13280    /// Returns true if `name` is a Perl keyword/builtin that should NOT be
13281    /// treated as a bare sub name (e.g. inside `sort`).
13282    /// True for any bareword the parser treats as a known builtin / keyword —
13283    /// Perl 5 core *or* a stryke extension. Used to suppress "call as user
13284    /// sub" interpretations (e.g. `sort my_cmp @list` only treats `my_cmp`
13285    /// as a comparator name if it *isn't* a known bareword). Previously named
13286    /// `is_perl_keyword`, which was misleading.
13287    fn is_known_bareword(name: &str) -> bool {
13288        Self::is_perl5_core(name) || Self::stryke_extension_name(name).is_some()
13289    }
13290
13291    /// True iff `name` appears as any spelling (primary *or* alias) in a
13292    /// `try_builtin` match arm. Picks up the ~300 aliases that don't show
13293    /// up in the parser-level keyword lists but are still callable at
13294    /// runtime — so `map { tj }` can default to `tj($_)` the same way
13295    /// `map { to_json }` does.
13296    fn is_try_builtin_name(name: &str) -> bool {
13297        crate::builtins::BUILTIN_ARMS
13298            .iter()
13299            .any(|arm| arm.contains(&name))
13300    }
13301
13302    /// True iff `name` is a Perl 5 core keyword/builtin (as shipped in stock
13303    /// `perl`). Extensions (`pmap`, `fan`, `timer`, …) are *not* included
13304    /// here — those live in `stryke_extension_name`. `%stryke::perl_compats`
13305    /// is derived from this list by `build.rs`.
13306    fn is_perl5_core(name: &str) -> bool {
13307        matches!(
13308            name,
13309            // ── array / list ────────────────────────────────────────────
13310            "map" | "grep" | "sort" | "reverse" | "join" | "split"
13311            | "push" | "pop" | "shift" | "unshift" | "splice"
13312            | "splice_last" | "splice1" | "spl_last"
13313            | "pack" | "unpack"
13314            | "unpack_first" | "unpack1" | "up1"
13315            // ── hash ────────────────────────────────────────────────────
13316            | "keys" | "values" | "each"
13317            // ── string ──────────────────────────────────────────────────
13318            | "chomp" | "chop" | "chr" | "ord" | "hex" | "oct"
13319            | "lc" | "uc" | "lcfirst" | "ucfirst"
13320            | "length" | "substr" | "index" | "rindex"
13321            | "sprintf" | "printf" | "print" | "say"
13322            | "pos" | "quotemeta" | "study"
13323            // ── numeric ─────────────────────────────────────────────────
13324            | "abs" | "int" | "sqrt" | "sin" | "cos" | "atan2"
13325            | "exp" | "log" | "rand" | "srand"
13326            // ── time ────────────────────────────────────────────────────
13327            | "time" | "localtime" | "gmtime"
13328            // ── type / reflection ───────────────────────────────────────
13329            | "defined" | "undef" | "ref" | "scalar" | "wantarray"
13330            | "caller" | "delete" | "exists" | "bless" | "prototype"
13331            | "tie" | "untie" | "tied"
13332            // ── io ──────────────────────────────────────────────────────
13333            | "open" | "close" | "read" | "readline" | "write" | "seek" | "tell"
13334            | "eof" | "binmode" | "getc" | "fileno" | "truncate"
13335            | "format" | "formline" | "select" | "vec"
13336            | "sysopen" | "sysread" | "sysseek" | "syswrite"
13337            // ── filesystem ──────────────────────────────────────────────
13338            | "stat" | "lstat" | "rename" | "unlink" | "utime"
13339            | "mkdir" | "rmdir" | "chdir" | "chmod" | "chown"
13340            | "glob" | "opendir" | "readdir" | "closedir"
13341            | "link" | "readlink" | "symlink"
13342            // ── ipc ─────────────────────────────────────────────────────
13343            | "fcntl" | "flock" | "ioctl" | "pipe" | "dbmopen" | "dbmclose"
13344            // ── sysv ipc ────────────────────────────────────────────────
13345            | "msgctl" | "msgget" | "msgrcv" | "msgsnd"
13346            | "semctl" | "semget" | "semop"
13347            | "shmctl" | "shmget" | "shmread" | "shmwrite"
13348            // ── process / system ────────────────────────────────────────
13349            | "system" | "exec" | "exit" | "die" | "warn" | "dump"
13350            | "fork" | "wait" | "waitpid" | "kill" | "alarm" | "sleep"
13351            | "chroot" | "times" | "umask" | "reset"
13352            | "getpgrp" | "setpgrp" | "getppid"
13353            | "getpriority" | "setpriority"
13354            // ── socket ──────────────────────────────────────────────────
13355            | "socket" | "socketpair" | "connect" | "listen" | "accept" | "shutdown"
13356            | "send" | "recv" | "bind" | "setsockopt" | "getsockopt"
13357            | "getpeername" | "getsockname"
13358            // ── posix metadata ──────────────────────────────────────────
13359            | "getpwnam" | "getpwuid" | "getpwent" | "setpwent"
13360            | "getgrnam" | "getgrgid" | "getgrent" | "setgrent"
13361            | "getlogin"
13362            | "gethostbyname" | "gethostbyaddr" | "gethostent"
13363            | "getnetbyname" | "getnetent"
13364            | "getprotobyname" | "getprotoent"
13365            | "getservbyname" | "getservent"
13366            | "sethostent" | "setnetent" | "setprotoent" | "setservent"
13367            | "endpwent" | "endgrent"
13368            | "endhostent" | "endnetent" | "endprotoent" | "endservent"
13369            // ── control flow ────────────────────────────────────────────
13370            | "return" | "do" | "eval" | "require"
13371            | "my" | "our" | "local" | "use" | "no"
13372            | "sub" | "if" | "unless" | "while" | "until"
13373            | "for" | "foreach" | "last" | "next" | "redo" | "goto"
13374            | "not" | "and" | "or"
13375            // ── quoting ─────────────────────────────────────────────────
13376            | "qw" | "qq" | "q"
13377            // ── phase blocks ────────────────────────────────────────────
13378            | "BEGIN" | "END"
13379        )
13380    }
13381
13382    /// If `name` is a stryke-only extension keyword/builtin, return it; else `None`.
13383    /// Used by `--compat` to reject extensions at parse time.
13384    fn stryke_extension_name(name: &str) -> Option<&str> {
13385        match name {
13386            // ── aop ────────────────────────────────────────────────────────
13387            | "proceed" | "intercept_list" | "intercept_remove" | "intercept_clear"
13388            // ── parallel ────────────────────────────────────────────────────
13389            | "pmap" | "pmap_on" | "pflat_map" | "pflat_map_on" | "pmap_chunked"
13390            | "pgrep" | "pfor" | "psort" | "preduce" | "preduce_init" | "pmap_reduce"
13391            | "pcache" | "pchannel" | "pselect" | "puniq" | "pfirst" | "pany"
13392            | "fan" | "fan_cap" | "par_lines" | "par_walk" | "par_sed"
13393            | "par_find_files" | "par_line_count" | "pwatch" | "par_pipeline_stream"
13394            | "glob_par" | "ppool" | "barrier" | "pipeline" | "cluster"
13395            | "pmaps" | "pflat_maps" | "pgreps"
13396            // ── functional / iterator ───────────────────────────────────────
13397            | "fore" | "e" | "ep" | "flat_map" | "flat_maps" | "maps" | "filter" | "fi" | "find_all" | "reduce" | "fold"
13398            | "inject" | "collect" | "uniq" | "distinct" | "any" | "all" | "none"
13399            | "first" | "detect" | "find" | "compact" | "concat" | "chain" | "reject" | "grepv" | "flatten" | "set"
13400            | "min_by" | "max_by" | "sort_by" | "tally" | "find_index"
13401            | "each_with_index" | "count" | "cnt" |"len" | "group_by" | "chunk_by"
13402            | "zip" | "chunk" | "chunked" | "sliding_window" | "windowed"
13403            | "enumerate" | "with_index" | "shuffle" | "shuffled"| "heap"
13404            | "take_while" | "drop_while" | "skip_while" | "tap" | "peek" | "partition"
13405            | "zip_with" | "count_by" | "skip" | "first_or"
13406            // ── pipeline / string helpers ───────────────────────────────────
13407            | "input" | "lines" | "words" | "chars" | "cindex" | "crindex"
13408            | "digits" | "letters" | "letters_uc" | "letters_lc"
13409            | "punctuation" | "punct"
13410            | "sentences" | "sents"
13411            | "paragraphs" | "paras" | "sections" | "sects"
13412            | "numbers" | "nums" | "graphemes" | "grs" | "columns" | "cols"
13413            | "trim" | "avg" | "stddev"
13414            | "squared" | "sq" | "square" | "cubed" | "cb" | "cube" | "expt" | "pow" | "pw"
13415            | "normalize" | "snake_case" | "camel_case" | "kebab_case"
13416            | "frequencies" | "freq" | "pfrequencies" | "pfreq"
13417            | "interleave" | "ddump" | "stringify" | "str" | "top"
13418            | "to_json" | "to_csv" | "to_toml" | "to_yaml" | "to_xml"
13419            | "to_html" | "to_markdown" | "to_table" | "xopen"
13420            | "from_json" | "from_csv" | "from_toml" | "from_yaml" | "from_xml"
13421            | "clip" | "clipboard" | "paste" | "pbcopy" | "pbpaste" | "preview"
13422            | "sparkline" | "spark" | "bar_chart" | "bars" | "flame" | "flamechart"
13423            | "histo" | "gauge" | "spinner" | "spinner_start" | "spinner_stop"
13424            | "to_hash" | "to_set"
13425            | "to_file" | "read_lines" | "append_file" | "write_json" | "read_json"
13426            | "tempfile" | "tempdir" | "list_count" | "list_size" | "size"
13427            | "clamp" | "grep_v" | "select_keys" | "pluck" | "glob_match" | "which_all"
13428            | "dedup" | "nth" | "tail" | "take" | "drop" | "tee" | "range"
13429            | "inc" | "dec" | "elapsed"
13430            // ── filesystem extensions ───────────────────────────────────────
13431            | "files" | "filesf" | "f" | "fr" | "dirs" | "d" | "dr" | "sym_links"
13432            | "sockets" | "pipes" | "block_devices" | "char_devices" | "exe" | "executables"
13433            | "basename" | "dirname" | "fileparse" | "realpath" | "canonpath"
13434            | "copy" | "move" | "spurt" | "spit" | "read_bytes" | "which"
13435            | "getcwd" | "touch" | "gethostname" | "uname"
13436            // ── data / network ──────────────────────────────────────────────
13437            | "csv_read" | "csv_write" | "dataframe" | "sqlite"
13438            | "fetch" | "fetch_json" | "fetch_async" | "fetch_async_json"
13439            | "par_fetch" | "par_csv_read" | "par_pipeline"
13440            | "json_encode" | "json_decode" | "json_jq"
13441            | "http_request" | "serve" | "ssh"
13442            | "html_parse" | "css_select" | "xml_parse" | "xpath"
13443            | "smtp_send"
13444            | "net_interfaces" | "net_ipv4" | "net_ipv6" | "net_mac"
13445            | "net_public_ip" | "net_dns" | "net_reverse_dns"
13446            | "net_ping" | "net_port_open" | "net_ports_scan"
13447            | "net_latency" | "net_download" | "net_headers"
13448            | "net_dns_servers" | "net_gateway" | "net_whois" | "net_hostname"
13449            // ── git ─────────────────────────────────────────────────────────
13450            | "git_log" | "git_status" | "git_diff" | "git_branches"
13451            | "git_tags" | "git_blame" | "git_authors" | "git_files"
13452            | "git_show" | "git_root"
13453            // ── audio / media ───────────────────────────────────────────────
13454            | "audio_convert" | "audio_info" | "id3_read" | "id3_write"
13455            // ── pdf ─────────────────────────────────────────────────────────
13456            | "to_pdf" | "pdf_text" | "pdf_pages"
13457            // ── serialization (stryke-only encoders) ────────────────────────
13458            | "toml_encode" | "toml_decode"
13459            | "yaml_encode" | "yaml_decode"
13460            | "xml_encode" | "xml_decode"
13461            // ── crypto / encoding ───────────────────────────────────────────
13462            | "md5" | "sha1" | "sha224" | "sha256" | "sha384" | "sha512"
13463            | "sha3_256" | "s3_256" | "sha3_512" | "s3_512"
13464            | "shake128" | "shake256"
13465            | "hmac_sha256" | "hmac_sha1" | "hmac_sha384" | "hmac_sha512" | "hmac_md5"
13466            | "uuid" | "crc32"
13467            | "blake2b" | "b2b" | "blake2s" | "b2s" | "blake3" | "b3"
13468            | "ripemd160" | "rmd160" | "md4"
13469            | "xxh32" | "xxhash32" | "xxh64" | "xxhash64" | "xxh3" | "xxhash3" | "xxh3_128" | "xxhash3_128"
13470            | "murmur3" | "murmur3_32" | "murmur3_128"
13471            | "siphash" | "siphash_keyed"
13472            | "hkdf_sha256" | "hkdf" | "hkdf_sha512"
13473            | "poly1305" | "poly1305_mac"
13474            | "base32_encode" | "b32e" | "base32_decode" | "b32d"
13475            | "base58_encode" | "b58e" | "base58_decode" | "b58d"
13476            | "totp" | "totp_generate" | "totp_verify" | "hotp" | "hotp_generate"
13477            | "aes_cbc_encrypt" | "aes_cbc_enc" | "aes_cbc_decrypt" | "aes_cbc_dec"
13478            | "blowfish_encrypt" | "bf_enc" | "blowfish_decrypt" | "bf_dec"
13479            | "des3_encrypt" | "3des_enc" | "tdes_enc" | "des3_decrypt" | "3des_dec" | "tdes_dec"
13480            | "twofish_encrypt" | "tf_enc" | "twofish_decrypt" | "tf_dec"
13481            | "camellia_encrypt" | "cam_enc" | "camellia_decrypt" | "cam_dec"
13482            | "cast5_encrypt" | "cast5_enc" | "cast5_decrypt" | "cast5_dec"
13483            | "salsa20" | "salsa20_encrypt" | "salsa20_decrypt"
13484            | "xsalsa20" | "xsalsa20_encrypt" | "xsalsa20_decrypt"
13485            | "secretbox" | "secretbox_seal" | "secretbox_open"
13486            | "nacl_box_keygen" | "box_keygen" | "nacl_box" | "nacl_box_seal" | "box_seal"
13487            | "nacl_box_open" | "box_open"
13488            | "qr_ascii" | "qr" | "qr_png" | "qr_svg"
13489            | "barcode_code128" | "code128" | "barcode_code39" | "code39"
13490            | "barcode_ean13" | "ean13" | "barcode_svg"
13491            | "argon2_hash" | "argon2" | "argon2_verify"
13492            | "bcrypt_hash" | "bcrypt" | "bcrypt_verify"
13493            | "scrypt_hash" | "scrypt" | "scrypt_verify"
13494            | "pbkdf2" | "pbkdf2_derive"
13495            | "random_bytes" | "randbytes" | "random_bytes_hex" | "randhex"
13496            | "aes_encrypt" | "aes_enc" | "aes_decrypt" | "aes_dec"
13497            | "chacha_encrypt" | "chacha_enc" | "chacha_decrypt" | "chacha_dec"
13498            | "rsa_keygen" | "rsa_encrypt" | "rsa_enc" | "rsa_decrypt" | "rsa_dec"
13499            | "rsa_encrypt_pkcs1" | "rsa_decrypt_pkcs1" | "rsa_sign" | "rsa_verify"
13500            | "ecdsa_p256_keygen" | "p256_keygen" | "ecdsa_p256_sign" | "p256_sign"
13501            | "ecdsa_p256_verify" | "p256_verify"
13502            | "ecdsa_p384_keygen" | "p384_keygen" | "ecdsa_p384_sign" | "p384_sign"
13503            | "ecdsa_p384_verify" | "p384_verify"
13504            | "ecdsa_secp256k1_keygen" | "secp256k1_keygen"
13505            | "ecdsa_secp256k1_sign" | "secp256k1_sign"
13506            | "ecdsa_secp256k1_verify" | "secp256k1_verify"
13507            | "ecdh_p256" | "p256_dh" | "ecdh_p384" | "p384_dh"
13508            | "ed25519_keygen" | "ed_keygen" | "ed25519_sign" | "ed_sign"
13509            | "ed25519_verify" | "ed_verify"
13510            | "x25519_keygen" | "x_keygen" | "x25519_dh" | "x_dh"
13511            | "base64_encode" | "base64_decode"
13512            | "hex_encode" | "hex_decode"
13513            | "url_encode" | "url_decode"
13514            | "gzip" | "gunzip" | "gz" | "ugz" | "zstd" | "zstd_decode" | "zst" | "uzst"
13515            | "brotli" | "br" | "brotli_decode" | "ubr"
13516            | "xz" | "lzma" | "xz_decode" | "unxz" | "unlzma"
13517            | "bzip2" | "bz2" | "bzip2_decode" | "bunzip2" | "ubz2"
13518            | "lz4" | "lz4_decode" | "unlz4"
13519            | "snappy" | "snp" | "snappy_decode" | "unsnappy"
13520            | "lzw" | "lzw_decode" | "unlzw"
13521            | "tar_create" | "tar" | "tar_extract" | "untar" | "tar_list"
13522            | "tar_gz_create" | "tgz" | "tar_gz_extract" | "untgz"
13523            | "zip_create" | "zip_archive" | "zip_extract" | "unzip_archive" | "zip_list"
13524            // ── special math functions ────────────────────────────────────────
13525            | "erf" | "erfc" | "gamma" | "tgamma" | "lgamma" | "ln_gamma"
13526            | "digamma" | "psi" | "beta_fn" | "lbeta" | "ln_beta"
13527            | "betainc" | "beta_reg" | "gammainc" | "gamma_li"
13528            | "gammaincc" | "gamma_ui" | "gammainc_reg" | "gamma_lr"
13529            | "gammaincc_reg" | "gamma_ur"
13530            // ── date / time ─────────────────────────────────────────────────
13531            | "datetime_utc" | "datetime_now_tz" | "now"
13532            | "datetime_format_tz" | "datetime_add_seconds"
13533            | "datetime_from_epoch"
13534            | "datetime_parse_rfc3339" | "datetime_parse_local"
13535            | "datetime_strftime"
13536            | "dateseq" | "dategrep" | "dateround" | "datesort"
13537            // ── jwt ─────────────────────────────────────────────────────────
13538            | "jwt_encode" | "jwt_decode" | "jwt_decode_unsafe"
13539            // ── logging ─────────────────────────────────────────────────────
13540            | "log_info" | "log_warn" | "log_error"
13541            | "log_debug" | "log_trace" | "log_json" | "log_level"
13542            // ── concurrency / timing ────────────────────────────────────────
13543            | "async" | "spawn" | "trace" | "timer" | "bench"
13544            | "eval_timeout" | "retry" | "rate_limit" | "every"
13545            | "gen" | "watch"
13546            // ── caching ────────────────────────────────────────────────────────
13547            | "cache_clear" | "cache_exists" | "cache_stats" | "cacheview"
13548            // ── testing framework ────────────────────────────────────────────
13549            | "assert_eq" | "assert_ne" | "assert_ok" | "assert_err"
13550            | "assert_true" | "assert_false"
13551            | "assert_gt" | "assert_lt" | "assert_ge" | "assert_le"
13552            | "assert_match" | "assert_contains" | "assert_near" | "assert_dies"
13553            | "test_run" | "run_tests" | "test_skip" | "skip_test" | "skip_assert"
13554            // ── system info ─────────────────────────────────────────────────
13555            | "mounts" | "du" | "du_tree" | "process_list"
13556            | "thread_count" | "pool_info" | "par_bench"
13557            // ── stress testing ──────────────────────────────────────────────
13558            | "stress_cpu" | "scpu" | "stress_mem" | "smem"
13559            | "stress_io" | "sio" | "stress_test" | "st"
13560            | "heat" | "fire" | "fire_and_forget" | "pin"
13561            // ── I/O extensions ──────────────────────────────────────────────
13562            | "slurp" | "cat" | "c" | "capture" | "pager" | "pg" | "less"
13563            | "stdin"
13564            // ── internal ────────────────────────────────────────────────────
13565            | "__stryke_rust_compile"
13566            | "vec_set_value"
13567            // ── short aliases ───────────────────────────────────────────────
13568            | "p" | "rev"
13569            // ── trivial numeric / predicate builtins ────────────────────────
13570            | "even" | "odd" | "zero" | "nonzero"
13571            | "positive" | "pos_n" | "negative" | "neg_n"
13572            | "sign" | "negate" | "double" | "triple" | "half"
13573            | "identity" | "id"
13574            | "round" | "floor" | "ceil" | "ceiling" | "trunc" | "truncn"
13575            | "gcd" | "lcm" | "min2" | "max2"
13576            | "log2" | "log10" | "hypot"
13577            | "rad_to_deg" | "r2d" | "deg_to_rad" | "d2r"
13578            | "pow2" | "abs_diff"
13579            | "factorial" | "fact" | "fibonacci" | "fib"
13580            | "is_prime" | "is_square" | "is_power_of_two" | "is_pow2"
13581            | "cbrt" | "exp2" | "percent" | "pct" | "inverse"
13582            | "median" | "mode_val" | "variance"
13583            // ── trivial string ops ──────────────────────────────────────────
13584            | "is_empty" | "is_blank" | "is_numeric"
13585            | "is_upper" | "is_lower" | "is_alpha" | "is_digit" | "is_alnum"
13586            | "is_space" | "is_whitespace"
13587            | "starts_with" | "sw" | "ends_with" | "ew" | "contains"
13588            | "capitalize" | "cap" | "swap_case" | "repeat"
13589            | "title_case" | "title" | "squish"
13590            | "pad_left" | "lpad" | "pad_right" | "rpad" | "center"
13591            | "truncate_at" | "shorten" | "reverse_str" | "rev_str"
13592            | "char_count" | "word_count" | "wc" | "line_count" | "lc_lines"
13593            // ── trivial type predicates ─────────────────────────────────────
13594            | "is_array" | "is_arrayref" | "is_hash" | "is_hashref"
13595            | "is_code" | "is_coderef" | "is_ref"
13596            | "is_undef" | "is_defined" | "is_def"
13597            | "is_string" | "is_str" | "is_int" | "is_integer" | "is_float"
13598            // ── hash helpers ────────────────────────────────────────────────
13599            | "invert" | "merge_hash"
13600            | "hash_map_values" | "hash_filter_keys" | "hash_filter_values"
13601            | "has_key" | "hk" | "has_any_key" | "has_all_keys"
13602            // ── boolean combinators ─────────────────────────────────────────
13603            | "both" | "either" | "neither" | "xor_bool" | "bool_to_int" | "b2i"
13604            // ── collection helpers (trivial) ────────────────────────────────
13605            | "riffle" | "intersperse" | "every_nth"
13606            | "drop_n" | "take_n" | "rotate" | "swap_pairs"
13607            // ── base conversion ─────────────────────────────────────────────
13608            | "to_bin" | "bin_of" | "to_hex" | "hex_of" | "to_oct" | "oct_of"
13609            | "from_bin" | "from_hex" | "from_oct" | "to_base" | "from_base"
13610            | "bits_count" | "popcount" | "leading_zeros" | "lz"
13611            | "trailing_zeros" | "tz" | "bit_length" | "bitlen"
13612            // ── bit ops ─────────────────────────────────────────────────────
13613            | "bit_and" | "bit_or" | "bit_xor" | "bit_not"
13614            | "shift_left" | "shl" | "shift_right" | "shr"
13615            | "bit_set" | "bit_clear" | "bit_toggle" | "bit_test"
13616            // ── unit conversions: temperature ───────────────────────────────
13617            | "c_to_f" | "f_to_c" | "c_to_k" | "k_to_c" | "f_to_k" | "k_to_f"
13618            // ── unit conversions: distance ──────────────────────────────────
13619            | "miles_to_km" | "km_to_miles" | "miles_to_m" | "m_to_miles"
13620            | "feet_to_m" | "m_to_feet" | "inches_to_cm" | "cm_to_inches"
13621            | "yards_to_m" | "m_to_yards"
13622            // ── unit conversions: mass ──────────────────────────────────────
13623            | "kg_to_lbs" | "lbs_to_kg" | "g_to_oz" | "oz_to_g"
13624            | "stone_to_kg" | "kg_to_stone"
13625            // ── unit conversions: digital ───────────────────────────────────
13626            | "bytes_to_kb" | "b_to_kb" | "kb_to_bytes" | "kb_to_b"
13627            | "bytes_to_mb" | "mb_to_bytes" | "bytes_to_gb" | "gb_to_bytes"
13628            | "kb_to_mb" | "mb_to_gb"
13629            | "bits_to_bytes" | "bytes_to_bits"
13630            // ── unit conversions: time ──────────────────────────────────────
13631            | "seconds_to_minutes" | "s_to_m" | "minutes_to_seconds" | "m_to_s"
13632            | "seconds_to_hours" | "hours_to_seconds"
13633            | "seconds_to_days" | "days_to_seconds"
13634            | "minutes_to_hours" | "hours_to_minutes"
13635            | "hours_to_days" | "days_to_hours"
13636            // ── date helpers ────────────────────────────────────────────────
13637            | "is_leap_year" | "is_leap" | "days_in_month"
13638            | "month_name" | "month_short"
13639            | "weekday_name" | "weekday_short" | "quarter_of"
13640            // ── now / timestamp ─────────────────────────────────────────────
13641            | "now_ms" | "now_us" | "now_ns"
13642            | "unix_epoch" | "epoch" | "unix_epoch_ms" | "epoch_ms"
13643            // ── color / ANSI ────────────────────────────────────────────────
13644            | "rgb_to_hex" | "hex_to_rgb"
13645            | "ansi_red" | "ansi_green" | "ansi_yellow" | "ansi_blue"
13646            | "ansi_magenta" | "ansi_cyan" | "ansi_white" | "ansi_black"
13647            | "ansi_bold" | "ansi_dim" | "ansi_underline" | "ansi_reverse"
13648            | "strip_ansi"
13649            | "red" | "green" | "yellow" | "blue" | "magenta" | "purple" | "cyan"
13650            | "white" | "black" | "bold" | "dim" | "italic" | "underline"
13651            | "strikethrough" | "ansi_off" | "off" | "gray" | "grey"
13652            | "bright_red" | "bright_green" | "bright_yellow" | "bright_blue"
13653            | "bright_magenta" | "bright_cyan" | "bright_white"
13654            | "bg_red" | "bg_green" | "bg_yellow" | "bg_blue"
13655            | "bg_magenta" | "bg_cyan" | "bg_white" | "bg_black"
13656            | "red_bold" | "bold_red" | "green_bold" | "bold_green"
13657            | "yellow_bold" | "bold_yellow" | "blue_bold" | "bold_blue"
13658            | "magenta_bold" | "bold_magenta" | "cyan_bold" | "bold_cyan"
13659            | "white_bold" | "bold_white"
13660            | "blink" | "rapid_blink" | "hidden" | "overline"
13661            | "bg_bright_red" | "bg_bright_green" | "bg_bright_yellow" | "bg_bright_blue"
13662            | "bg_bright_magenta" | "bg_bright_cyan" | "bg_bright_white"
13663            | "rgb" | "bg_rgb" | "color256" | "c256" | "bg_color256" | "bg_c256"
13664            // ── network / validation ────────────────────────────────────────
13665            | "ipv4_to_int" | "int_to_ipv4"
13666            | "is_valid_ipv4" | "is_valid_ipv6" | "is_valid_email" | "is_valid_url"
13667            // ── path helpers ────────────────────────────────────────────────
13668            | "path_ext" | "path_stem" | "path_parent" | "path_join" | "path_split"
13669            | "strip_prefix" | "strip_suffix" | "ensure_prefix" | "ensure_suffix"
13670            // ── functional primitives ───────────────────────────────────────
13671            | "const_fn" | "always_true" | "always_false"
13672            | "flip_args" | "first_arg" | "second_arg" | "last_arg"
13673            // ── more list helpers ───────────────────────────────────────────
13674            | "count_eq" | "count_ne" | "all_eq"
13675            | "all_distinct" | "all_unique" | "has_duplicates"
13676            | "sum_of" | "product_of" | "max_of" | "min_of" | "range_of"
13677            // ── string quote / escape ───────────────────────────────────────
13678            | "quote" | "single_quote" | "unquote"
13679            | "extract_between" | "ellipsis"
13680            // ── random ──────────────────────────────────────────────────────
13681            | "coin_flip" | "dice_roll"
13682            | "random_int" | "random_float" | "random_bool"
13683            | "random_choice" | "random_between"
13684            | "random_string" | "random_alpha" | "random_digit"
13685            // ── symbol table ────────────────────────────────────────────────
13686            | "refresh_stashes"
13687            // ── system introspection ────────────────────────────────────────
13688            | "os_name" | "os_arch" | "num_cpus"
13689            | "pid" | "ppid" | "uid" | "gid"
13690            | "username" | "home_dir" | "temp_dir"
13691            | "mem_total" | "mem_free" | "mem_used"
13692            | "swap_total" | "swap_free" | "swap_used"
13693            | "disk_total" | "disk_free" | "disk_avail" | "disk_used"
13694            | "load_avg" | "sys_uptime" | "page_size"
13695            | "os_version" | "os_family" | "endianness" | "pointer_width"
13696            | "proc_mem" | "rss"
13697            // ── collection more ─────────────────────────────────────────────
13698            | "transpose" | "unzip"
13699            | "run_length_encode" | "rle" | "run_length_decode" | "rld"
13700            | "sliding_pairs" | "consecutive_eq" | "flatten_deep"
13701            // ── trig / math (batch 2) ───────────────────────────────────────
13702            | "tan" | "asin" | "acos" | "atan"
13703            | "sinh" | "cosh" | "tanh" | "asinh" | "acosh" | "atanh"
13704            | "sqr" | "cube_fn"
13705            | "mod_op" | "ceil_div" | "floor_div"
13706            | "is_finite" | "is_infinite" | "is_inf" | "is_nan"
13707            | "degrees" | "radians"
13708            | "min_abs" | "max_abs"
13709            | "saturate" | "sat01" | "wrap_around"
13710            // ── string (batch 2) ────────────────────────────────────────────
13711            | "rot13" | "rot47" | "caesar_shift" | "reverse_words"
13712            | "count_vowels" | "count_consonants" | "is_vowel" | "is_consonant"
13713            | "first_word" | "last_word"
13714            | "left_str" | "head_str" | "right_str" | "tail_str" | "mid_str"
13715            | "lowercase" | "uppercase"
13716            | "pascal_case" | "pc_case"
13717            | "constant_case" | "upper_snake" | "dot_case" | "path_case"
13718            | "is_palindrome" | "hamming_distance"
13719            | "longest_common_prefix" | "lcp"
13720            | "ascii_ord" | "ascii_chr" | "count_char" | "indexes_of"
13721            | "replace_first" | "replace_all_str"
13722            | "contains_any" | "contains_all"
13723            | "starts_with_any" | "ends_with_any"
13724            // ── predicates (batch 2) ────────────────────────────────────────
13725            | "is_pair" | "is_triple"
13726            | "is_sorted" | "is_asc" | "is_sorted_desc" | "is_desc"
13727            | "is_empty_arr" | "is_empty_hash"
13728            | "is_subset" | "is_superset" | "is_permutation"
13729            // ── collection (batch 2) ────────────────────────────────────────
13730            | "first_eq" | "last_eq"
13731            | "index_of" | "last_index_of" | "positions_of"
13732            | "batch" | "binary_search" | "bsearch" | "linear_search" | "lsearch"
13733            | "distinct_count" | "longest" | "shortest"
13734            | "array_union" | "list_union"
13735            | "array_intersection" | "list_intersection"
13736            | "array_difference" | "list_difference"
13737            | "symmetric_diff" | "group_of_n" | "chunk_n"
13738            | "repeat_list" | "cycle_n" | "random_sample" | "sample_n"
13739            // ── hash ops (batch 2) ──────────────────────────────────────────
13740            | "pick_keys" | "pick" | "omit_keys" | "omit"
13741            | "map_keys_fn" | "map_values_fn"
13742            | "hash_size" | "hash_from_pairs" | "pairs_from_hash"
13743            | "hash_eq" | "keys_sorted" | "values_sorted" | "remove_keys"
13744            // ── date (batch 2) ──────────────────────────────────────────────
13745            | "today" | "yesterday" | "tomorrow" | "is_weekend" | "is_weekday"
13746            // ── json helpers ────────────────────────────────────────────────
13747            | "json_pretty" | "json_minify" | "escape_json" | "json_escape"
13748            // ── process / env ───────────────────────────────────────────────
13749            | "cmd_exists" | "env_get" | "env_has" | "env_keys"
13750            | "argc" | "script_name"
13751            | "has_stdin_tty" | "has_stdout_tty" | "has_stderr_tty"
13752            // ── id helpers ──────────────────────────────────────────────────
13753            | "uuid_v4" | "nanoid" | "short_id" | "is_uuid" | "token"
13754            // ── url / email parts ───────────────────────────────────────────
13755            | "email_domain" | "email_local"
13756            | "url_host" | "url_path" | "url_query" | "url_scheme"
13757            // ── file stat / path ────────────────────────────────────────────
13758            | "file_size" | "fsize" | "file_mtime" | "mtime"
13759            | "file_atime" | "atime" | "file_ctime" | "ctime"
13760            | "is_symlink" | "is_readable" | "is_writable" | "is_executable"
13761            | "path_is_abs" | "path_is_rel"
13762            // ── stats / sort / array / format / cmp / regex / time conv / volume / force ──
13763            | "min_max" | "percentile" | "harmonic_mean" | "geometric_mean" | "zscore"
13764            | "sorted" | "sorted_desc" | "sorted_nums" | "sorted_by_length"
13765            | "reverse_list" | "list_reverse"
13766            | "without" | "without_nth" | "take_last" | "drop_last"
13767            | "pairwise" | "zipmap"
13768            | "format_bytes" | "human_bytes"
13769            | "format_duration" | "human_duration"
13770            | "format_number" | "group_number"
13771            | "format_percent" | "pad_number"
13772            | "spaceship" | "cmp_num" | "cmp_str"
13773            | "compare_versions" | "version_cmp"
13774            | "hash_insert" | "hash_update" | "hash_delete"
13775            | "matches_regex" | "re_match"
13776            | "count_regex_matches" | "regex_extract"
13777            | "regex_split_str" | "regex_replace_str"
13778            | "shuffle_chars" | "random_char" | "nth_word"
13779            | "head_lines" | "tail_lines" | "count_substring"
13780            | "is_valid_hex" | "hex_upper" | "hex_lower"
13781            | "ms_to_s" | "s_to_ms" | "ms_to_ns" | "ns_to_ms"
13782            | "us_to_ns" | "ns_to_us"
13783            | "liters_to_gallons" | "gallons_to_liters"
13784            | "liters_to_ml" | "ml_to_liters"
13785            | "cups_to_ml" | "ml_to_cups"
13786            | "newtons_to_lbf" | "lbf_to_newtons"
13787            | "joules_to_cal" | "cal_to_joules"
13788            | "watts_to_hp" | "hp_to_watts"
13789            | "pascals_to_psi" | "psi_to_pascals"
13790            | "bar_to_pascals" | "pascals_to_bar"
13791            // ── algebraic match ─────────────────────────────────────────────
13792            | "match"
13793            // ── clojure stdlib (only names not matched above) ─────────────────
13794            | "fst" | "rest" | "rst" | "second" | "snd"
13795            | "last_clj" | "lastc" | "butlast" | "bl"
13796            | "ffirst" | "ffs" | "fnext" | "fne" | "nfirst" | "nfs" | "nnext" | "nne"
13797            | "cons" | "conj"
13798            | "peek_clj" | "pkc" | "pop_clj" | "popc"
13799            | "some" | "not_any" | "not_every"
13800            | "comp" | "compose" | "partial" | "constantly" | "complement" | "compl"
13801            | "fnil" | "juxt"
13802            | "memoize" | "memo" | "curry" | "once"
13803            | "deep_clone" | "dclone" | "deep_merge" | "dmerge" | "deep_equal" | "deq"
13804            | "iterate" | "iter" | "repeatedly" | "rptd" | "cycle" | "cyc"
13805            | "mapcat" | "mcat" | "keep" | "kp" | "remove_clj" | "remc"
13806            | "reductions" | "rdcs"
13807            | "partition_by" | "pby" | "partition_all" | "pall"
13808            | "split_at" | "spat" | "split_with" | "spw"
13809            | "assoc" | "dissoc" | "get_in" | "gin" | "assoc_in" | "ain" | "update_in" | "uin"
13810            | "into" | "empty_clj" | "empc" | "seq" | "vec_clj" | "vecc"
13811            | "apply" | "appl"
13812            // ── python/ruby stdlib ───────────────────────────────────────────
13813            | "divmod" | "dm" | "accumulate" | "accum" | "starmap" | "smap"
13814            | "zip_longest" | "zipl" | "zip_fill" | "zipf" | "combinations" | "comb" | "permutations" | "perm"
13815            | "cartesian_product" | "cprod" | "compress" | "cmpr" | "filterfalse" | "falf"
13816            | "islice" | "isl" | "chain_from" | "chfr" | "pairwise_iter" | "pwi"
13817            | "tee_iter" | "teei" | "groupby_iter" | "gbi"
13818            | "each_slice" | "eslice" | "each_cons" | "econs"
13819            | "one" | "none_match" | "nonem"
13820            | "find_index_fn" | "fidx" | "rindex_fn" | "ridx"
13821            | "minmax" | "mmx" | "minmax_by" | "mmxb"
13822            | "dig" | "values_at" | "vat" | "fetch_val" | "fv" | "slice_arr" | "sla"
13823            | "transform_keys" | "tkeys" | "transform_values" | "tvals"
13824            | "sum_by" | "sumb" | "uniq_by" | "uqb"
13825            | "flat_map_fn" | "fmf" | "then_fn" | "thfn" | "times_fn" | "timf"
13826            | "step" | "upto" | "downto"
13827            // ── javascript array/object methods ─────────────────────────────
13828            | "find_last" | "fndl" | "find_last_index" | "fndli"
13829            | "at_index" | "ati" | "replace_at" | "repa"
13830            | "to_sorted" | "tsrt" | "to_reversed" | "trev" | "to_spliced" | "tspl"
13831            | "flat_depth" | "fltd" | "fill_arr" | "filla" | "includes_val" | "incv"
13832            | "object_keys" | "okeys" | "object_values" | "ovals"
13833            | "object_entries" | "oents" | "object_from_entries" | "ofents"
13834            // ── haskell list functions ──────────────────────────────────────
13835            | "span_fn" | "spanf" | "break_fn" | "brkf" | "group_runs" | "gruns"
13836            | "nub" | "sort_on" | "srton"
13837            | "intersperse_val" | "isp" | "intercalate" | "ical"
13838            | "replicate_val" | "repv" | "elem_of" | "elof" | "not_elem" | "ntelm"
13839            | "lookup_assoc" | "lkpa" | "scanl" | "scanr" | "unfoldr" | "unfr"
13840            // ── rust iterator methods ───────────────────────────────────────
13841            | "find_map" | "fndm" | "filter_map" | "fltm" | "fold_right" | "fldr"
13842            | "partition_either" | "peith" | "try_fold" | "tfld"
13843            | "map_while" | "mapw" | "inspect" | "insp"
13844            // ── ruby enumerable extras ──────────────────────────────────────
13845            | "tally_by" | "talb" | "sole" | "chunk_while" | "chkw" | "count_while" | "cntw"
13846            // ── go/general functional utilities ─────────────────────────────
13847            | "insert_at" | "insa" | "delete_at" | "dela" | "update_at" | "upda"
13848            | "split_on" | "spon" | "words_from" | "wfrm" | "unwords" | "unwds"
13849            | "lines_from" | "lfrm" | "unlines" | "unlns"
13850            | "window_n" | "winn" | "adjacent_pairs" | "adjp"
13851            | "zip_all" | "zall" | "unzip_pairs" | "uzp"
13852            | "interpose" | "ipos" | "partition_n" | "partn"
13853            | "map_indexed" | "mapi" | "reduce_indexed" | "redi" | "filter_indexed" | "flti"
13854            | "group_by_fn" | "gbf" | "index_by" | "idxb" | "associate" | "assoc_fn"
13855            // ── additional missing stdlib functions ─────────────────────────
13856            | "combinations_rep" | "combrep" | "inits" | "tails" | "subsequences" | "subseqs"
13857            | "nub_by" | "nubb" | "slice_when" | "slcw" | "slice_before" | "slcb" | "slice_after" | "slca"
13858            | "each_with_object" | "ewo" | "reduce_right" | "redr"
13859            | "is_sorted_by" | "issrtb" | "intersperse_with" | "ispw"
13860            | "running_reduce" | "runred" | "windowed_circular" | "wincirc"
13861            | "distinct_by" | "distb" | "average" | "mean" | "copy_within" | "cpyw"
13862            | "and_list" | "andl" | "or_list" | "orl" | "concat_map" | "cmap"
13863            | "elem_index" | "elidx" | "elem_indices" | "elidxs" | "find_indices" | "fndidxs"
13864            | "delete_first" | "delfst" | "delete_by" | "delby" | "insert_sorted" | "inssrt"
13865            | "union_list" | "unionl" | "intersect_list" | "intl"
13866            | "maximum_by" | "maxby" | "minimum_by" | "minby" | "batched" | "btch"
13867            // ── Extended stdlib: Text Processing ─────────────────────────────
13868            | "match_all" | "mall" | "capture_groups" | "capg" | "is_match" | "ism"
13869            | "split_regex" | "splre" | "replace_regex" | "replre"
13870            | "is_ascii" | "isasc" | "to_ascii" | "toasc"
13871            | "char_at" | "chat" | "code_point_at" | "cpat" | "from_code_point" | "fcp"
13872            | "normalize_spaces" | "nrmsp" | "remove_whitespace" | "rmws"
13873            | "pluralize" | "plur" | "ordinalize" | "ordn"
13874            | "parse_int" | "pint" | "parse_float" | "pflt" | "parse_bool" | "pbool"
13875            | "levenshtein" | "lev" | "soundex" | "sdx" | "similarity" | "sim"
13876            | "common_prefix" | "cpfx" | "common_suffix" | "csfx"
13877            | "wrap_text" | "wrpt" | "dedent" | "ddt" | "indent" | "idt"
13878            // ── Extended stdlib: Advanced Numeric ────────────────────────────
13879            | "lerp" | "inv_lerp" | "ilerp" | "smoothstep" | "smst" | "remap"
13880            | "dot_product" | "dotp" | "cross_product" | "crossp"
13881            | "matrix_mul" | "matmul" | "mm"
13882            | "magnitude" | "mag" | "normalize_vec" | "nrmv"
13883            | "distance" | "dist" | "manhattan_distance" | "mdist"
13884            | "covariance" | "cov" | "correlation" | "corr"
13885            | "iqr" | "quantile" | "qntl" | "quantiles" | "qntls"
13886            | "lsp_completion_words" | "lsp_words"
13887            | "doctor" | "health"
13888            | "clamp_int" | "clpi"
13889            | "in_range" | "inrng" | "wrap_range" | "wrprng"
13890            | "sum_squares" | "sumsq" | "rms" | "cumsum" | "csum" | "cumprod" | "cprod_acc" | "diff"
13891            // ── Extended stdlib: Date/Time ───────────────────────────────────
13892            | "add_days" | "addd" | "add_hours" | "addh" | "add_minutes" | "addm"
13893            | "diff_days" | "diffd" | "diff_hours" | "diffh"
13894            | "start_of_day" | "sod" | "end_of_day" | "eod"
13895            | "start_of_hour" | "soh" | "start_of_minute" | "som"
13896            // ── Extended stdlib: Encoding/Hashing ────────────────────────────
13897            | "urle" | "urld"
13898            | "html_encode" | "htmle" | "html_decode" | "htmld"
13899            | "adler32" | "adl32" | "fnv1a" | "djb2"
13900            // ── Extended stdlib: Validation ──────────────────────────────────
13901            | "is_credit_card" | "iscc" | "is_isbn10" | "isbn10" | "is_isbn13" | "isbn13"
13902            | "is_iban" | "isiban" | "is_hex_str" | "ishex" | "is_binary_str" | "isbin"
13903            | "is_octal_str" | "isoct" | "is_json" | "isjson" | "is_base64" | "isb64"
13904            | "is_semver" | "issv" | "is_slug" | "isslug" | "slugify" | "slug"
13905            // ── Extended stdlib: Collection Advanced ─────────────────────────
13906            | "mode_stat" | "mstat" | "sampn" | "weighted_sample" | "wsamp"
13907            | "shuffle_arr" | "shuf" | "argmax" | "amax" | "argmin" | "amin"
13908            | "argsort" | "asrt" | "rank" | "rnk" | "dense_rank" | "drnk"
13909            | "partition_point" | "ppt" | "lower_bound" | "lbound"
13910            | "upper_bound" | "ubound" | "equal_range" | "eqrng"
13911            // ── Extended stdlib: Matrix Operations ───────────────────────────
13912            | "matrix_add" | "madd" | "matrix_sub" | "msub" | "matrix_mult" | "mmult"
13913            | "matrix_scalar" | "mscal" | "matrix_identity" | "mident"
13914            | "matrix_zeros" | "mzeros" | "matrix_ones" | "mones"
13915            | "matrix_diag" | "mdiag" | "matrix_trace" | "mtrace"
13916            | "matrix_row" | "mrow" | "matrix_col" | "mcol"
13917            | "matrix_shape" | "mshape" | "matrix_det" | "mdet"
13918            | "matrix_scale" | "mat_scale" | "diagonal" | "diag"
13919            // ── Extended stdlib: Graph Algorithms ────────────────────────────
13920            | "topological_sort" | "toposort" | "bfs_traverse" | "bfs"
13921            | "dfs_traverse" | "dfs" | "shortest_path_bfs" | "spbfs"
13922            | "connected_components_graph" | "ccgraph"
13923            | "has_cycle_graph" | "hascyc" | "is_bipartite_graph" | "isbip"
13924            // ── Extended stdlib: Data Validation ─────────────────────────────
13925            | "is_ipv4_addr" | "isip4" | "is_ipv6_addr" | "isip6"
13926            | "is_mac_addr" | "ismac" | "is_port_num" | "isport"
13927            | "is_hostname_valid" | "ishost"
13928            | "is_iso_date" | "isisodt" | "is_iso_time" | "isisotm"
13929            | "is_iso_datetime" | "isisodtm"
13930            | "is_phone_num" | "isphone" | "is_us_zip" | "iszip"
13931            // ── Extended stdlib: String Utilities Novel ──────────────────────
13932            | "word_wrap_text" | "wwrap" | "center_text" | "ctxt"
13933            | "ljust_text" | "ljt" | "rjust_text" | "rjt" | "zfill_num" | "zfill"
13934            | "remove_all_str" | "rmall" | "replace_n_times" | "repln"
13935            | "find_all_indices" | "fndalli"
13936            | "text_between" | "txbtwn" | "text_before" | "txbef" | "text_after" | "txaft"
13937            | "text_before_last" | "txbefl" | "text_after_last" | "txaftl"
13938            // ── Extended stdlib: Math Novel ──────────────────────────────────
13939            | "is_even_num" | "iseven" | "is_odd_num" | "isodd"
13940            | "is_positive_num" | "ispos" | "is_negative_num" | "isneg"
13941            | "is_zero_num" | "iszero" | "is_whole_num" | "iswhole"
13942            | "log_with_base" | "logb" | "nth_root_of" | "nroot"
13943            | "frac_part" | "fracp" | "reciprocal_of" | "recip"
13944            | "copy_sign" | "cpsgn" | "fused_mul_add" | "fmadd"
13945            | "floor_mod" | "fmod" | "floor_div_op" | "fdivop"
13946            | "signum_of" | "sgnum" | "midpoint_of" | "midpt"
13947            // ── Extended stdlib batch 3: Array Analysis ──────────────────────
13948            | "longest_run" | "lrun" | "longest_increasing" | "linc"
13949            | "longest_decreasing" | "ldec" | "max_sum_subarray" | "maxsub"
13950            | "majority_element" | "majority" | "kth_largest" | "kthl"
13951            | "kth_smallest" | "kths" | "count_inversions" | "cinv"
13952            | "is_monotonic" | "ismono" | "equilibrium_index" | "eqidx"
13953            // ── Extended stdlib batch 3: Set Operations ──────────────────────
13954            | "jaccard_index" | "jaccard" | "dice_coefficient" | "dicecoef"
13955            | "overlap_coefficient" | "overlapcoef"
13956            | "power_set" | "powerset" | "cartesian_power" | "cartpow"
13957            // ── Extended stdlib batch 3: Advanced String ─────────────────────
13958            | "is_isogram" | "isiso" | "is_heterogram" | "ishet"
13959            | "hamdist" | "jaro_similarity" | "jarosim"
13960            | "longest_common_substring" | "lcsub"
13961            | "longest_common_subsequence" | "lcseq"
13962            | "count_words" | "wcount" | "count_lines" | "lcount"
13963            | "count_chars" | "ccount" | "count_bytes" | "bcount"
13964            // ── Extended stdlib batch 3: More Math ───────────────────────────
13965            | "binomial" | "binom" | "catalan" | "catn" | "pascal_row" | "pascrow"
13966            | "is_coprime" | "iscopr" | "euler_totient" | "etot"
13967            | "mobius" | "mob" | "is_squarefree" | "issqfr"
13968            | "digital_root" | "digroot" | "is_narcissistic" | "isnarc"
13969            | "is_harshad" | "isharsh" | "is_kaprekar" | "iskap"
13970            // ── Extended stdlib batch 3: Date/Time Additional ────────────────
13971            | "day_of_year" | "doy" | "week_of_year" | "woy"
13972            | "days_in_month_fn" | "daysinmo" | "is_valid_date" | "isvdate"
13973            | "age_in_years" | "ageyrs"
13974            // ── functional combinators ──────────────────────────────────────
13975
13976            | "when_true" | "when_false" | "if_else" | "clamp_fn"
13977            | "attempt" | "try_fn" | "safe_div" | "safe_mod" | "safe_sqrt" | "safe_log"
13978            | "juxt2" | "juxt3" | "tap_val" | "debug_val" | "converge"
13979            | "iterate_n" | "unfold" | "arity_of" | "is_callable"
13980            | "coalesce" | "default_to" | "fallback"
13981            | "apply_list" | "zip_apply" | "scan"
13982            | "keep_if" | "reject_if" | "group_consecutive"
13983            | "after_n" | "before_n" | "clamp_list" | "normalize_list" | "softmax"
13984
13985            // ── matrix / linear algebra ─────────────────────────────────────
13986
13987
13988            | "matrix_multiply" | "mat_mul"
13989            | "identity_matrix" | "eye" | "zeros_matrix" | "zeros" | "ones_matrix" | "ones"
13990
13991
13992
13993            | "vec_normalize" | "unit_vec" | "vec_add" | "vec_sub" | "vec_scale"
13994            | "linspace" | "arange"
13995            // ── more regex ──────────────────────────────────────────────────
13996            | "re_test" | "re_find_all" | "re_groups" | "re_escape"
13997            | "re_split_limit" | "glob_to_regex" | "is_regex_valid"
13998            // ── more process / system ───────────────────────────────────────
13999            | "cwd" | "pwd_str" | "cpu_count" | "is_root" | "uptime_secs"
14000            | "env_pairs" | "env_set" | "env_remove" | "hostname_str" | "is_tty" | "signal_name"
14001            // ── data structure helpers ───────────────────────────────────────
14002            | "stack_new" | "queue_new" | "lru_new"
14003            | "counter" | "counter_most_common" | "defaultdict" | "ordered_set"
14004            | "bitset_new" | "bitset_set" | "bitset_test" | "bitset_clear"
14005            // ── trivial numeric helpers (batch 4) ─────────────────────────────
14006            | "abs_ceil" | "abs_each" | "abs_floor" | "ceil_each" | "dec_each"
14007            | "double_each" | "floor_each" | "half_each" | "inc_each" | "length_each"
14008            | "negate_each" | "not_each" | "offset_each" | "reverse_each" | "round_each"
14009            | "scale_each" | "sqrt_each" | "square_each" | "to_float_each" | "to_int_each"
14010            | "trim_each" | "type_each" | "upcase_each" | "downcase_each" | "bool_each"
14011            // ── math / physics constants ──────────────────────────────────────
14012            | "avogadro" | "boltzmann" | "golden_ratio" | "gravity" | "ln10" | "ln2"
14013            | "planck" | "speed_of_light" | "sqrt2"
14014            // ── physics formulas ──────────────────────────────────────────────
14015            | "bmi_calc" | "compound_interest" | "dew_point" | "discount_amount"
14016            | "force_mass_acc" | "freq_wavelength" | "future_value" | "haversine"
14017            | "heat_index" | "kinetic_energy" | "margin_price" | "markup_price"
14018            | "mortgage_payment" | "ohms_law_i" | "ohms_law_r" | "ohms_law_v"
14019            | "potential_energy" | "present_value" | "simple_interest" | "speed_distance_time"
14020            | "tax_amount" | "tip_amount" | "wavelength_freq" | "wind_chill"
14021            // ── math functions ────────────────────────────────────────────────
14022            | "angle_between_deg" | "approx_eq" | "chebyshev_distance" | "copysign"
14023            | "cosine_similarity" | "cube_root" | "entropy" | "float_bits" | "fma"
14024            | "int_bits" | "jaccard_similarity" | "log_base" | "mae" | "mse" | "nth_root"
14025            | "r_squared" | "reciprocal" | "relu" | "rmse" | "rotate_point" | "round_to"
14026            | "sigmoid" | "signum" | "square_root"
14027            // ── sequences ─────────────────────────────────────────────────────
14028            | "cubes_seq" | "fibonacci_seq" | "powers_of_seq" | "primes_seq"
14029            | "squares_seq" | "triangular_seq"
14030            // ── string helpers (batch 4) ──────────────────────────────────────
14031            | "alternate_case" | "angle_bracket" | "bracket" | "byte_length"
14032            | "bytes_to_hex_str" | "camel_words" | "char_length" | "chars_to_string"
14033            | "chomp_str" | "chop_str" | "filter_chars" | "from_csv_line" | "hex_to_bytes"
14034            | "insert_str" | "intersperse_char" | "ljust" | "map_chars" | "mirror_string"
14035            | "normalize_whitespace" | "only_alnum" | "only_alpha" | "only_ascii"
14036            | "only_digits" | "parenthesize" | "remove_str" | "repeat_string" | "rjust"
14037            | "sentence_case" | "string_count" | "string_sort" | "string_to_chars"
14038            | "string_unique_chars" | "substring" | "to_csv_line" | "trim_left" | "trim_right"
14039            | "xor_strings"
14040            // ── list helpers (batch 4) ─────────────────────────────────────────
14041            | "adjacent_difference" | "append_elem" | "consecutive_pairs" | "contains_elem"
14042            | "count_elem" | "drop_every" | "duplicate_count" | "elem_at" | "find_first"
14043            | "first_elem" | "flatten_once" | "fold_left" | "from_digits" | "from_pairs"
14044            | "group_by_size" | "hash_from_list"
14045            | "hash_merge_deep" | "hash_to_list" | "hash_zip" | "head_n" | "histogram_bins"
14046            | "index_of_elem" | "init_list" | "interleave_lists" | "last_elem" | "least_common"
14047            | "list_compact" | "list_eq" | "list_flatten_deep" | "max_list" | "mean_list"
14048            | "min_list" | "mode_list" | "most_common" | "partition_two" | "prefix_sums"
14049            | "prepend" | "product_list" | "remove_at" | "remove_elem" | "remove_first_elem"
14050            | "repeat_elem" | "running_max" | "running_min" | "sample_one" | "scan_left"
14051            | "second_elem" | "span" | "suffix_sums" | "sum_list" | "tail_n" | "take_every"
14052            | "third_elem" | "to_array" | "to_pairs" | "trimmed_mean" | "unique_count_of"
14053            | "wrap_index" | "digits_of"
14054            // ── predicates (batch 4) ──────────────────────────────────────────
14055            | "all_match" | "any_match" | "is_between" | "is_blank_or_nil" | "is_divisible_by"
14056            | "is_email" | "is_even" | "is_falsy" | "is_fibonacci" | "is_hex_color"
14057            | "is_in_range" | "is_ipv4" | "is_multiple_of" | "is_negative" | "is_nil"
14058            | "is_nonzero" | "is_odd" | "is_perfect_square" | "is_positive" | "is_power_of"
14059            | "is_prefix" | "is_present" | "is_strictly_decreasing" | "is_strictly_increasing"
14060            | "is_suffix" | "is_triangular" | "is_truthy" | "is_url" | "is_whole" | "is_zero"
14061            // ── counters (batch 4) ────────────────────────────────────────────
14062            | "count_digits" | "count_letters" | "count_lower" | "count_match"
14063            | "count_punctuation" | "count_spaces" | "count_upper" | "defined_count"
14064            | "empty_count" | "falsy_count" | "nonempty_count" | "numeric_count"
14065            | "truthy_count" | "undef_count"
14066            // ── conversion / utility (batch 4) ────────────────────────────────
14067            | "assert_type" | "between" | "clamp_each" | "die_if" | "die_unless"
14068            | "join_colons" | "join_commas" | "join_dashes" | "join_dots" | "join_lines"
14069            | "join_pipes" | "join_slashes" | "join_spaces" | "join_tabs" | "measure"
14070            | "max_float" | "min_float" | "noop_val" | "nop" | "pass" | "pred" | "succ"
14071            | "tap_debug" | "to_bool" | "to_float" | "to_int" | "to_string" | "void"
14072            | "range_exclusive" | "range_inclusive"
14073            // ── math / numeric (uncategorized batch) ────────────────────────────
14074            | "aliquot_sum" | "autocorrelation" | "bell_number" | "cagr" | "coeff_of_variation"
14075            | "collatz_length" | "collatz_sequence" | "convolution" | "cross_entropy"
14076            | "depreciation_double" | "depreciation_linear" | "discount" | "divisors"
14077            | "epsilon" | "euclidean_distance" | "euler_number" | "exponential_moving_average"
14078            | "f64_max" | "f64_min" | "fft_magnitude" | "goldbach" | "i64_max" | "i64_min"
14079            | "kurtosis" | "linear_regression" | "look_and_say" | "lucas" | "luhn_check"
14080            | "mean_absolute_error" | "mean_squared_error" | "median_absolute_deviation"
14081            | "minkowski_distance" | "moving_average" | "multinomial" | "neg_inf" | "npv"
14082            | "num_divisors" | "partition_number" | "pascals_triangle" | "skewness"
14083            | "standard_error" | "subfactorial" | "sum_divisors" | "totient_sum"
14084            | "tribonacci" | "weighted_mean" | "winsorize"
14085            // ── statistics (extended) ─────────────────────────────────────────
14086            | "chi_square_stat" | "describe" | "five_number_summary"
14087            | "gini" | "gini_coefficient" | "lorenz_curve" | "outliers_iqr"
14088            | "percentile_rank" | "quartiles" | "sample_stddev" | "sample_variance"
14089            | "spearman_correlation" | "t_test_one_sample" | "t_test_two_sample"
14090            | "z_score" | "z_scores"
14091            // ── number theory / primes ──────────────────────────────────────────
14092            | "abundant_numbers" | "deficient_numbers" | "is_abundant" | "is_deficient"
14093            | "is_pentagonal" | "is_perfect" | "is_smith" | "next_prime" | "nth_prime"
14094            | "pentagonal_number" | "perfect_numbers" | "prev_prime" | "prime_factors"
14095            | "prime_pi" | "primes_up_to" | "triangular_number" | "twin_primes"
14096            // ── geometry / physics ──────────────────────────────────────────────
14097            | "area_circle" | "area_ellipse" | "area_rectangle" | "area_trapezoid" | "area_triangle"
14098            | "bearing" | "circumference" | "cone_volume" | "cylinder_volume" | "heron_area"
14099            | "midpoint" | "perimeter_rectangle" | "perimeter_triangle" | "point_distance"
14100            | "polygon_area" | "slope" | "sphere_surface" | "sphere_volume" | "triangle_hypotenuse"
14101            // ── geometry (extended) ───────────────────────────────────────────
14102            | "angle_between" | "arc_length" | "bounding_box" | "centroid"
14103            | "circle_from_three_points" | "convex_hull" | "ellipse_perimeter"
14104            | "frustum_volume" | "haversine_distance" | "line_intersection"
14105            | "point_in_polygon" | "polygon_perimeter" | "pyramid_volume"
14106            | "reflect_point" | "scale_point" | "sector_area"
14107            | "torus_surface" | "torus_volume" | "translate_point"
14108            | "vector_angle" | "vector_cross" | "vector_dot" | "vector_magnitude" | "vector_normalize"
14109            // ── constants ───────────────────────────────────────────────────────
14110            | "avogadro_number" | "boltzmann_constant" | "electron_mass" | "elementary_charge"
14111            | "gravitational_constant" | "phi" | "pi" | "planck_constant" | "proton_mass"
14112            | "sol" | "tau"
14113            // ── finance ─────────────────────────────────────────────────────────
14114            | "bac_estimate" | "bmi" | "break_even" | "margin" | "markup" | "roi" | "tax" | "tip"
14115            // ── finance (extended) ────────────────────────────────────────────
14116            | "amortization_schedule" | "black_scholes_call" | "black_scholes_put"
14117            | "bond_price" | "bond_yield" | "capm" | "continuous_compound"
14118            | "discounted_payback" | "duration" | "irr"
14119            | "max_drawdown" | "modified_duration" | "nper" | "num_periods" | "payback_period"
14120            | "pmt" | "pv" | "rule_of_72" | "sharpe_ratio" | "sortino_ratio"
14121            | "wacc" | "xirr"
14122            // ── string processing (uncategorized batch) ─────────────────────────
14123            | "acronym" | "atbash" | "bigrams" | "camel_to_snake" | "char_frequencies"
14124            | "chunk_string" | "collapse_whitespace" | "dedent_text" | "indent_text"
14125            | "initials" | "leetspeak" | "mask_string" | "ngrams" | "pig_latin"
14126            | "remove_consonants" | "remove_vowels" | "reverse_each_word" | "snake_to_camel"
14127            | "sort_words" | "string_distance" | "string_multiply" | "strip_html"
14128            | "trigrams" | "unique_words" | "word_frequencies" | "zalgo"
14129            // ── encoding / phonetics ────────────────────────────────────────────
14130            | "braille_encode" | "double_metaphone" | "metaphone" | "morse_decode"
14131            | "morse_encode" | "nato_phonetic" | "phonetic_digit" | "subscript" | "superscript"
14132            | "to_emoji_num"
14133            // ── roman numerals ──────────────────────────────────────────────────
14134            | "int_to_roman" | "roman_add" | "roman_numeral_list" | "roman_to_int"
14135            // ── base / gray code ────────────────────────────────────────────────
14136            | "base_convert" | "binary_to_gray" | "gray_code_sequence" | "gray_to_binary"
14137            // ── color operations ────────────────────────────────────────────────
14138            | "ansi_256" | "ansi_truecolor" | "color_blend" | "color_complement"
14139            | "color_darken" | "color_distance" | "color_grayscale" | "color_invert"
14140            | "color_lighten" | "hsl_to_rgb" | "hsv_to_rgb" | "random_color"
14141            | "rgb_to_hsl" | "rgb_to_hsv"
14142            // ── matrix operations (uncategorized batch) ─────────────────────────
14143            | "matrix_flatten" | "matrix_from_rows" | "matrix_hadamard" | "matrix_inverse"
14144            | "matrix_map" | "matrix_max" | "matrix_min" | "matrix_power" | "matrix_sum"
14145            | "matrix_transpose"
14146            // ── array / list operations (uncategorized batch) ───────────────────
14147            | "binary_insert" | "bucket" | "clamp_array" | "group_consecutive_by"
14148            | "histogram" | "merge_sorted" | "next_permutation" | "normalize_array"
14149            | "normalize_range" | "peak_detect" | "range_compress" | "range_expand"
14150            | "reservoir_sample" | "run_length_decode_str" | "run_length_encode_str"
14151            | "zero_crossings"
14152            // ── DSP / signal (extended) ───────────────────────────────────────
14153            | "apply_window" | "bandpass_filter" | "cross_correlation" | "dft"
14154            | "downsample" | "energy" | "envelope" | "highpass_filter" | "idft"
14155            | "lowpass_filter" | "median_filter" | "normalize_signal" | "phase_spectrum"
14156            | "power_spectrum" | "resample" | "spectral_centroid" | "spectrogram" | "upsample"
14157            | "window_blackman" | "window_hamming" | "window_hann" | "window_kaiser"
14158            // ── validation predicates (uncategorized batch) ─────────────────────
14159            | "is_anagram" | "is_balanced_parens" | "is_control" | "is_numeric_string"
14160            | "is_pangram" | "is_printable" | "is_valid_cidr" | "is_valid_cron"
14161            | "is_valid_hex_color" | "is_valid_latitude" | "is_valid_longitude" | "is_valid_mime"
14162            // ── algorithms / puzzles ────────────────────────────────────────────
14163            | "eval_rpn" | "fizzbuzz" | "game_of_life_step" | "mandelbrot_char"
14164            | "sierpinski" | "tower_of_hanoi" | "truth_table"
14165            // ── misc / utility ──────────────────────────────────────────────────
14166            | "byte_size" | "degrees_to_compass" | "to_string_val" | "type_of"
14167            // ── math formulas ───────────────────────────────────────────────────
14168            | "quadratic_roots" | "quadratic_discriminant" | "arithmetic_series"
14169            | "geometric_series" | "stirling_approx"
14170            | "double_factorial" | "rising_factorial" | "falling_factorial"
14171            | "gamma_approx" | "erf_approx" | "normal_pdf" | "normal_cdf"
14172            | "poisson_pmf" | "exponential_pdf" | "inverse_lerp"
14173            | "map_range"
14174            // ── physics formulas ────────────────────────────────────────────────
14175            | "momentum" | "impulse" | "work" | "power_phys" | "torque" | "angular_velocity"
14176            | "centripetal_force" | "escape_velocity" | "orbital_velocity" | "orbital_period"
14177            | "gravitational_force" | "coulomb_force" | "electric_field" | "capacitance"
14178            | "capacitor_energy" | "inductor_energy" | "resonant_frequency"
14179            | "rc_time_constant" | "rl_time_constant" | "impedance_rlc"
14180            | "relativistic_mass" | "lorentz_factor" | "time_dilation" | "length_contraction"
14181            | "relativistic_energy" | "rest_energy" | "de_broglie_wavelength"
14182            | "photon_energy" | "photon_energy_wavelength" | "schwarzschild_radius"
14183            | "stefan_boltzmann" | "wien_displacement" | "ideal_gas_pressure" | "ideal_gas_volume"
14184            | "projectile_range" | "projectile_max_height" | "projectile_time"
14185            | "spring_force" | "spring_energy" | "pendulum_period" | "doppler_frequency"
14186            | "decibel_ratio" | "snells_law" | "brewster_angle" | "critical_angle"
14187            | "lens_power" | "thin_lens" | "magnification_lens"
14188            // ── math constants ──────────────────────────────────────────────────
14189            | "euler_mascheroni" | "apery_constant" | "feigenbaum_delta" | "feigenbaum_alpha"
14190            | "catalan_constant" | "khinchin_constant" | "glaisher_constant"
14191            | "plastic_number" | "silver_ratio" | "supergolden_ratio"
14192            // ── physics constants ───────────────────────────────────────────────
14193            | "vacuum_permittivity" | "vacuum_permeability" | "coulomb_constant"
14194            | "fine_structure_constant" | "rydberg_constant" | "bohr_radius"
14195            | "bohr_magneton" | "nuclear_magneton" | "stefan_boltzmann_constant"
14196            | "wien_constant" | "gas_constant" | "faraday_constant" | "neutron_mass"
14197            | "atomic_mass_unit" | "earth_mass" | "earth_radius" | "sun_mass" | "sun_radius"
14198            | "astronomical_unit" | "light_year" | "parsec" | "hubble_constant"
14199            | "planck_length" | "planck_time" | "planck_mass" | "planck_temperature"
14200            // ── linear algebra (extended) ──────────────────────────────────
14201            | "matrix_solve" | "msolve" | "solve"
14202            | "matrix_lu" | "mlu" | "matrix_qr" | "mqr"
14203            | "matrix_eigenvalues" | "meig" | "eigenvalues" | "eig"
14204            | "matrix_norm" | "mnorm" | "matrix_cond" | "mcond" | "cond"
14205            | "matrix_pinv" | "mpinv" | "pinv"
14206            | "matrix_cholesky" | "mchol" | "cholesky"
14207            | "matrix_det_general" | "mdetg" | "det"
14208            // ── statistics tests (extended) ────────────────────────────────
14209            | "welch_ttest" | "welcht" | "paired_ttest" | "pairedt"
14210            | "cohen_d" | "cohend" | "anova_oneway" | "anova" | "anova1"
14211            | "spearman_corr" | "rho" | "kendall_tau" | "kendall" | "ktau"
14212            | "confidence_interval" | "ci"
14213            // ── distributions (extended) ──────────────────────────────────
14214            | "beta_pdf" | "betapdf" | "gamma_pdf" | "gammapdf"
14215            | "chi2_pdf" | "chi2pdf" | "chi_squared_pdf"
14216            | "t_pdf" | "tpdf" | "student_pdf"
14217            | "f_pdf" | "fpdf" | "fisher_pdf"
14218            | "lognormal_pdf" | "lnormpdf" | "weibull_pdf" | "weibpdf"
14219            | "cauchy_pdf" | "cauchypdf" | "laplace_pdf" | "laplacepdf"
14220            | "pareto_pdf" | "paretopdf"
14221            // ── interpolation & curve fitting ─────────────────────────────
14222            | "lagrange_interp" | "lagrange" | "linterp"
14223            | "cubic_spline" | "cspline" | "spline"
14224            | "poly_eval" | "polyval" | "polynomial_fit" | "polyfit"
14225            // ── numerical integration & differentiation ───────────────────
14226            | "trapz" | "trapezoid" | "simpson" | "simps"
14227            | "numerical_diff" | "numdiff" | "diff_array"
14228            | "cumtrapz" | "cumulative_trapz"
14229            // ── optimization / root finding ────────────────────────────────
14230            | "bisection" | "bisect" | "newton_method" | "newton" | "newton_raphson"
14231            | "golden_section" | "golden" | "gss"
14232            // ── ODE solvers ───────────────────────────────────────────────
14233            | "rk4" | "runge_kutta" | "rk4_ode" | "euler_ode" | "euler_method"
14234            // ── graph algorithms (extended) ────────────────────────────────
14235            | "dijkstra" | "shortest_path" | "bellman_ford" | "bellmanford"
14236            | "floyd_warshall" | "floydwarshall" | "apsp"
14237            | "prim_mst" | "mst" | "prim"
14238            // ── trig extensions ───────────────────────────────────────────
14239            | "cot" | "sec" | "csc" | "acot" | "asec" | "acsc" | "sinc" | "versin" | "versine"
14240            // ── ML activation functions ───────────────────────────────────
14241            | "leaky_relu" | "lrelu" | "elu" | "selu" | "gelu"
14242            | "silu" | "swish" | "mish" | "softplus"
14243            | "hard_sigmoid" | "hardsigmoid" | "hard_swish" | "hardswish"
14244            // ── special functions ─────────────────────────────────────────
14245            | "bessel_j0" | "j0" | "bessel_j1" | "j1"
14246            | "lambert_w" | "lambertw" | "productlog"
14247            // ── Wolfram-Math parity: Bessel/Airy/Hankel/Struve/Kelvin ─────
14248            | "bessel_j" | "bessel_y" | "bessel_i" | "bessel_k"
14249            | "hankel_h1" | "hankel_h2" | "bessel_j_zero"
14250            | "airy_ai" | "airy_bi" | "airy_ai_prime" | "airy_bi_prime"
14251            | "spherical_bessel_j" | "spherical_bessel_y"
14252            | "struve_h" | "struve_l" | "kelvin_ber" | "kelvin_bei"
14253            // ── orthogonal polynomials ────────────────────────────────────
14254            | "legendre_p" | "legendre_q" | "assoc_legendre_p"
14255            | "hermite_h" | "hermite_he" | "laguerre_l" | "assoc_laguerre_l"
14256            | "jacobi_p" | "gegenbauer_c" | "chebyshev_t" | "chebyshev_u"
14257            | "spherical_harmonic_y" | "zernike_r"
14258            // ── elliptic integrals + Jacobi/Weierstrass/theta ─────────────
14259            | "elliptic_k" | "elliptic_e" | "elliptic_pi" | "elliptic_f"
14260            | "elliptic_e_inc" | "elliptic_pi_inc"
14261            | "carlson_rf" | "carlson_rd" | "carlson_rj"
14262            | "jacobi_sn" | "jacobi_cn" | "jacobi_dn" | "jacobi_am"
14263            | "elliptic_theta"
14264            | "weierstrass_p" | "weierstrass_zeta" | "weierstrass_sigma"
14265            // ── zeta / polylog / Lerch ────────────────────────────────────
14266            | "zeta" | "riemann_zeta" | "hurwitz_zeta"
14267            | "polylog" | "dilog" | "lerch_phi"
14268            | "riemann_siegel_z" | "riemann_siegel_theta"
14269            | "dirichlet_eta" | "dirichlet_beta"
14270            // ── hypergeometric ────────────────────────────────────────────
14271            | "hypergeometric_2f1" | "hyper_2f1"
14272            | "hypergeometric_1f1" | "hyper_1f1" | "kummer_m"
14273            | "hypergeometric_0f1" | "hyper_0f1"
14274            | "hypergeometric_pfq" | "hyper_pfq"
14275            | "hypergeometric_u" | "tricomi_u"
14276            // ── modular forms ─────────────────────────────────────────────
14277            | "dedekind_eta" | "klein_j" | "klein_invariant_j"
14278            | "modular_lambda" | "ramanujan_tau"
14279            // ── integrals: Si / Ci / Ei / Li / Fresnel ────────────────────
14280            | "sin_integral" | "si_int" | "cos_integral" | "ci_int"
14281            | "sinh_integral" | "shi_int" | "cosh_integral" | "chi_int"
14282            | "exp_integral_e" | "ei_n" | "exp_integral_ei" | "ei_int"
14283            | "log_integral" | "li_int" | "fresnel_s" | "fresnel_c"
14284            // ── number-theory gaps ────────────────────────────────────────
14285            | "jacobi_symbol" | "kronecker_symbol"
14286            | "primitive_root" | "multiplicative_order"
14287            | "mangoldt_lambda" | "von_mangoldt" | "carmichael_lambda"
14288            | "squares_r" | "thue_morse" | "rudin_shapiro"
14289            | "farey_sequence" | "farey"
14290            | "frobenius_number" | "frobenius_solve" | "stern_brocot"
14291            // ── combinatorial gaps ────────────────────────────────────────
14292            | "stirling_s1" | "stirling_first" | "bell_polynomial_b" | "bell_y"
14293            | "clebsch_gordan" | "three_j_symbol" | "wigner_3j"
14294            | "six_j_symbol" | "wigner_6j" | "nine_j_symbol" | "wigner_9j"
14295            | "debruijn_sequence" | "debruijn" | "wigner_d"
14296            // ── q-series, Mittag-Leffler, Coulomb wave ────────────────────
14297            | "q_pochhammer" | "q_factorial" | "q_binomial"
14298            | "q_hypergeometric_pfq"
14299            | "mittag_leffler_e" | "mittag_leffler"
14300            | "coulomb_wave_f" | "coulomb_wave_g"
14301            // ── inverse special functions ─────────────────────────────────
14302            | "inverse_erf" | "erfinv" | "inverse_erfc" | "erfcinv"
14303            | "inverse_gamma_regularized" | "gamma_lr_inv"
14304            | "inverse_beta_regularized" | "beta_reg_inv"
14305            | "inverse_jacobi_sn"
14306            // ── piecewise / symbolic primitives ───────────────────────────
14307            | "dirac_delta" | "heaviside_theta" | "heaviside"
14308            | "unit_box" | "unit_triangle"
14309            | "square_wave" | "triangle_wave" | "sawtooth_wave" | "dirac_comb"
14310            // ── Tier A: number theory extensions ──────────────────────────
14311            | "liouville_lambda" | "jordan_totient" | "ramanujan_sum"
14312            | "cyclotomic_polynomial" | "cyclotomic" | "legendre_symbol"
14313            | "pythagorean_triple_q" | "gen_pythagorean_triple"
14314            | "sophie_germain_q" | "mersenne_q"
14315            | "lucas_lehmer_test" | "lucas_lehmer"
14316            | "continued_fraction" | "from_continued_fraction" | "convergents"
14317            | "best_rational_approximation" | "best_rational"
14318            // ── Tier B: combinatorial sequences ───────────────────────────
14319            | "motzkin_number" | "motzkin"
14320            | "narayana_number" | "narayana"
14321            | "delannoy_number" | "delannoy"
14322            | "schroder_number" | "schroder" | "large_schroder"
14323            | "small_schroder_number" | "small_schroder"
14324            | "eulerian_number"
14325            | "bernoulli_polynomial" | "euler_polynomial"
14326            | "pell_number" | "pell" | "pell_lucas_number" | "pell_lucas"
14327            | "perrin_number" | "perrin" | "padovan_number" | "padovan"
14328            // ── Tier C: linear algebra extras ─────────────────────────────
14329            | "kronecker_product" | "tensor_product" | "tensor_contract"
14330            | "matrix_rank" | "mrank"
14331            | "companion_matrix" | "companion"
14332            | "characteristic_polynomial" | "charpoly"
14333            | "singular_values" | "svals"
14334            | "nullspace" | "null_space" | "kernel"
14335            // ── Tier D: polynomial algebra ────────────────────────────────
14336            | "polynomial_gcd" | "polygcd"
14337            | "polynomial_quotient" | "polyquot"
14338            | "polynomial_remainder" | "polyrem"
14339            | "polynomial_resultant" | "resultant"
14340            | "polynomial_discriminant" | "discriminant"
14341            | "polynomial_roots" | "polyroots"
14342            // ── Tier E: more distributions ────────────────────────────────
14343            | "gumbel_pdf" | "gumbel_cdf" | "gumbel_quantile"
14344            | "frechet_pdf" | "frechet_cdf" | "frechet_quantile"
14345            | "logistic_pdf" | "logistic_cdf" | "logistic_quantile"
14346            | "rayleigh_pdf" | "rayleigh_cdf" | "rayleigh_quantile"
14347            | "inverse_gamma_pdf" | "inverse_gamma_cdf" | "inverse_gamma_quantile"
14348            | "kumaraswamy_pdf" | "kumaraswamy_cdf" | "kumaraswamy_quantile"
14349            // ── Tier F: Mathieu ───────────────────────────────────────────
14350            | "mathieu_a" | "mathieu_characteristic_a"
14351            | "mathieu_ce" | "mathieu_se"
14352            // ── Tier G: Heun general ──────────────────────────────────────
14353            | "heun_g"
14354            // ── Tier H: wavelets ──────────────────────────────────────────
14355            | "haar_transform" | "haar" | "haar_inverse" | "ihaar"
14356            | "daubechies_db4" | "db4" | "daubechies_db4_inverse" | "idb4"
14357            // ── Tier I: graph algorithms ──────────────────────────────────
14358            | "topo_sort_adj"
14359            | "scc_tarjan" | "tarjan_scc" | "strongly_connected"
14360            | "bipartite_q" | "is_bipartite"
14361            | "max_flow_edmonds_karp" | "max_flow" | "edmonds_karp"
14362            | "min_cut" | "eccentricity"
14363            | "graph_diameter" | "graph_radius"
14364            // ── Tier J: misc fillers ──────────────────────────────────────
14365            | "stieltjes_constant" | "stieltjes"
14366            | "gauss_sum" | "kloosterman_sum"
14367            | "eta_quotient" | "root_approximant"
14368            // ── Batch 3: vector calculus ──────────────────────────────────
14369            | "numerical_gradient" | "ngrad"
14370            | "numerical_jacobian" | "njac"
14371            | "numerical_hessian" | "nhess"
14372            | "numerical_divergence" | "ndiv"
14373            | "numerical_curl" | "ncurl"
14374            | "numerical_laplacian" | "nlap"
14375            // ── Batch 3: optimization ─────────────────────────────────────
14376            | "nelder_mead" | "simplex_min"
14377            | "gradient_descent" | "gd_min"
14378            | "bfgs_minimize" | "bfgs"
14379            | "levenberg_marquardt" | "lev_marq" | "lm_min"
14380            | "conjugate_gradient" | "cg_solve"
14381            | "least_squares" | "lstsq"
14382            // ── Batch 3: integration extras ───────────────────────────────
14383            | "romberg" | "romberg_int"
14384            | "gauss_legendre_quad" | "glquad" | "gl_quad"
14385            | "monte_carlo_integrate" | "mc_int"
14386            | "adaptive_simpson" | "asimp"
14387            // ── Batch 3: LA extras ────────────────────────────────────────
14388            | "lu_decompose" | "ludec"
14389            | "qr_decompose" | "qrdec"
14390            | "householder_reflector" | "householder"
14391            | "givens_rotation" | "givens"
14392            | "forward_substitute" | "fwdsub"
14393            | "back_substitute" | "backsub"
14394            | "hessenberg_reduce" | "hessen"
14395            // ── Batch 3: polynomial helpers ───────────────────────────────
14396            | "poly_derivative" | "polyder"
14397            | "poly_integrate" | "polyint"
14398            | "poly_compose" | "poly_eval_horner" | "horner"
14399            | "pade_approximant" | "pade"
14400            // ── Batch 3: quaternions ──────────────────────────────────────
14401            | "quat_mul" | "quat_conj" | "quat_norm" | "quat_inv"
14402            | "quat_from_axis_angle" | "axis_angle_to_quat"
14403            | "quat_to_axis_angle"
14404            | "quat_to_matrix" | "quat_from_matrix" | "matrix_to_quat"
14405            | "quat_slerp" | "slerp"
14406            | "euler_zyx_to_matrix" | "matrix_to_euler_zyx"
14407            | "rotate_3d_vec"
14408            // ── Batch 3: information theory ───────────────────────────────
14409            | "kl_divergence" | "kl_div"
14410            | "js_divergence" | "js_div"
14411            | "mutual_information" | "mi"
14412            | "cross_entropy_arr" | "cross_entropy_dist"
14413            | "renyi_entropy" | "tsallis_entropy"
14414            // ── Batch 3: quantum ──────────────────────────────────────────
14415            | "pauli_x" | "pauli_y" | "pauli_z"
14416            | "pauli_id" | "pauli_i" | "pauli_identity"
14417            | "ket_bra" | "density_matrix" | "expectation_value" | "expval"
14418            | "commutator" | "anticommutator"
14419            | "partial_trace" | "ptrace"
14420            | "von_neumann_entropy" | "vn_entropy"
14421            // ── Batch 3: stat mech ────────────────────────────────────────
14422            | "bose_einstein" | "fermi_dirac"
14423            | "maxwell_boltzmann_speed" | "mb_speed"
14424            | "partition_function" | "z_partition"
14425            | "helmholtz_free_energy" | "free_energy_f"
14426            | "boltzmann_factor"
14427            | "einstein_specific_heat" | "einstein_cv"
14428            // ── Batch 3: optics ───────────────────────────────────────────
14429            | "fresnel_reflection_te" | "fresnel_reflection_tm"
14430            | "fresnel_transmission_te" | "fresnel_transmission_tm"
14431            | "abcd_thin_lens" | "abcd_free_space"
14432            | "gaussian_beam_q"
14433            // ── Batch 3: astrodynamics ────────────────────────────────────
14434            | "kepler_solve"
14435            | "true_to_eccentric" | "eccentric_to_mean"
14436            | "julian_date" | "j_date"
14437            | "jd_to_gregorian" | "jd_to_date"
14438            | "sidereal_time_gmst" | "gmst"
14439            | "vis_viva" | "orbital_period_kepler"
14440            | "orbital_elements_to_state" | "elem_to_state"
14441            // ── Batch 3: time series ──────────────────────────────────────
14442            | "kalman_step" | "kalman_filter"
14443            | "exponential_smoothing" | "exp_smooth"
14444            | "holt_winters" | "arma_yw_fit" | "ar_yw"
14445            // ── Batch 3: graph centrality ─────────────────────────────────
14446            | "pagerank" | "betweenness_centrality" | "closeness_centrality"
14447            | "eigenvector_centrality" | "degree_centrality" | "triangle_count"
14448            // ── Batch 3: random samplers ──────────────────────────────────
14449            | "rgumbel" | "rfrechet" | "rrayleigh"
14450            | "rlogistic" | "rkumaraswamy" | "rinverse_gamma" | "rinvgamma"
14451            // ── Batch 3: 2D geometry ──────────────────────────────────────
14452            | "graham_scan" | "convex_hull_2d"
14453            | "line_line_intersect_2d" | "ll_intersect_2d"
14454            | "point_segment_distance" | "p_seg_dist"
14455            // ── Batch 4: auto-diff ────────────────────────────────────────
14456            | "forward_diff" | "fdiff"
14457            | "forward_diff_grad" | "fdiff_grad"
14458            // ── Batch 4: stat tests ───────────────────────────────────────
14459            | "bartlett_test" | "levene_test"
14460            | "fishers_exact_test_2x2" | "fishers_exact"
14461            | "mcnemar_test"
14462            | "runs_test" | "wald_wolfowitz"
14463            | "friedman_test" | "kruskal_wallis_test" | "kruskal"
14464            | "sign_test"
14465            | "anderson_darling_normality" | "ad_normality"
14466            | "jarque_bera_test" | "jb_test"
14467            | "ljung_box_test" | "ljung_box"
14468            | "durbin_watson_stat" | "durbin_watson"
14469            // ── Batch 4: distance metrics ─────────────────────────────────
14470            | "mahalanobis_distance" | "mahalanobis_dist"
14471            | "cosine_distance" | "canberra_distance"
14472            | "bray_curtis_distance" | "bray_curtis"
14473            | "l1_distance"
14474            | "chi_squared_distance"
14475            // ── Batch 4: more distributions ───────────────────────────────
14476            | "multivariate_normal_pdf" | "mvn_pdf"
14477            | "multivariate_normal_sample" | "rmvn"
14478            | "dirichlet_pdf" | "dirichlet_sample" | "rdirichlet"
14479            | "skellam_pmf"
14480            | "inverse_gaussian_pdf" | "wald_pdf"
14481            | "inverse_gaussian_cdf" | "wald_cdf"
14482            | "inverse_gaussian_sample" | "rwald"
14483            | "non_central_chi2_pdf" | "ncchi2_pdf"
14484            // ── Batch 4: matrix functions ─────────────────────────────────
14485            | "matrix_exp" | "expm" | "matrix_log" | "logm"
14486            | "matrix_sqrt" | "sqrtm" | "matrix_sin" | "sinm"
14487            | "matrix_cos" | "cosm"
14488            // ── Batch 4: adaptive ODE ─────────────────────────────────────
14489            | "rk45_dormand_prince" | "rk45" | "dopri5"
14490            | "midpoint_step" | "ode_midpoint"
14491            | "heun_step" | "ode_heun"
14492            | "verlet_step" | "ode_verlet"
14493            // ── Batch 4: GLM ──────────────────────────────────────────────
14494            | "logistic_regression" | "logit_fit"
14495            | "poisson_regression"
14496            | "ridge_regression" | "ridge"
14497            | "lasso_coord" | "lasso"
14498            // ── Batch 4: bootstrap/resampling ─────────────────────────────
14499            | "bootstrap_mean_ci" | "boot_mean_ci"
14500            | "jackknife_estimate" | "jackknife"
14501            | "permutation_test_diff" | "perm_test_diff"
14502            // ── Batch 4: time series extras ───────────────────────────────
14503            | "acf_at_lag" | "diff_op" | "lag_op"
14504            | "decompose_classical" | "decompose_ts"
14505            // ── Batch 4: combinatorial generators ─────────────────────────
14506            | "combinations_list" | "permutations_list"
14507            | "cyclic_permutations" | "subsets_of_size"
14508            // ── Batch 4: DP utilities ─────────────────────────────────────
14509            | "longest_increasing_subseq" | "lis"
14510            | "knapsack_01" | "knapsack"
14511            | "subset_sum_target" | "subset_sum"
14512            | "coin_change_min" | "coin_change_minimum"
14513            | "edit_distance_levenshtein" | "edit_distance"
14514            // ── Batch 4: ML metrics ───────────────────────────────────────
14515            | "one_hot_encode" | "onehot" | "label_encode"
14516            | "categorical_cross_entropy" | "cce"
14517            | "classification_metrics" | "binary_metrics"
14518            | "roc_auc" | "auroc"
14519            // ── Batch 4: DSP / image filters ──────────────────────────────
14520            | "gaussian_blur_kernel" | "sobel_x" | "sobel_y"
14521            | "prewitt_x" | "prewitt_y"
14522            | "laplacian_of_gaussian" | "log_kernel"
14523            // ── Batch 4: stochastic processes ─────────────────────────────
14524            | "brownian_path" | "wiener_path"
14525            | "geometric_brownian_path" | "gbm_path"
14526            | "poisson_process" | "random_walk_1d"
14527            // ── Batch 4: compression / info ───────────────────────────────
14528            | "lempel_ziv_complexity" | "lz_complexity"
14529            | "huffman_code_lengths" | "huffman"
14530            | "shannon_entropy_rate" | "block_entropy_rate"
14531            // ── Batch 4: physics / quantum ────────────────────────────────
14532            | "planck_blackbody" | "blackbody"
14533            | "rayleigh_jeans" | "compton_shift"
14534            | "rydberg_energy"
14535            | "hydrogen_radial_wavefunction" | "h_rad_psi"
14536            // ── Batch 4: number theory / algebra ──────────────────────────
14537            | "integer_log" | "ilog"
14538            | "aks_primality" | "aks"
14539            | "elliptic_curve_add" | "ec_add"
14540            | "berlekamp_massey" | "bm_lfsr"
14541            | "bezout_coefficients" | "bezout" | "extended_euclid"
14542            // ── Batch 5: CAS-lite ─────────────────────────────────────────
14543            | "factor_quadratic" | "complete_square"
14544            | "partial_fraction_simple" | "partial_fraction"
14545            // ── Batch 5: more quadrature ──────────────────────────────────
14546            | "gauss_chebyshev_quad" | "gc_quad"
14547            | "gauss_hermite_quad" | "gh_quad"
14548            | "gauss_laguerre_quad" | "glag_quad"
14549            | "clenshaw_curtis_quad" | "cc_quad"
14550            | "tanh_sinh_quad" | "ts_quad"
14551            | "gauss_legendre_2d" | "gl_2d"
14552            | "monte_carlo_2d" | "mc_2d"
14553            // ── Batch 5: more optimization ────────────────────────────────
14554            | "simulated_annealing" | "sa_min"
14555            | "simplex_lp" | "lp_simplex"
14556            | "particle_swarm" | "pso_min"
14557            // ── Batch 5: distributions ────────────────────────────────────
14558            | "gev_pdf" | "gev_cdf" | "gev_sample" | "rgev"
14559            | "gen_pareto_pdf" | "gen_pareto_cdf"
14560            | "gen_pareto_sample" | "rgenpareto"
14561            | "skew_normal_pdf" | "skew_normal_cdf"
14562            | "mixture_normal_pdf"
14563            | "categorical_sample" | "rcat"
14564            | "multinomial_pmf" | "multinomial_sample" | "rmultinom"
14565            | "truncated_normal_pdf"
14566            | "truncated_normal_sample" | "rtnorm"
14567            // ── Batch 5: clustering ───────────────────────────────────────
14568            | "dbscan" | "gmm_em_1d" | "gmm_1d"
14569            | "silhouette_score"
14570            | "davies_bouldin_index" | "db_index"
14571            | "calinski_harabasz_index" | "ch_index"
14572            | "mds_2d" | "pcoa_2d" | "mean_shift"
14573            // ── Batch 5: NN primitives ────────────────────────────────────
14574            | "batch_norm" | "layer_norm"
14575            | "dropout_mask"
14576            | "max_pool_1d" | "avg_pool_1d"
14577            | "attention_softmax" | "positional_encoding"
14578            | "glorot_init" | "xavier_init"
14579            | "he_init" | "kaiming_init"
14580            | "adam_step" | "rmsprop_step"
14581            // ── Batch 5: time series ──────────────────────────────────────
14582            | "ewma" | "ccf" | "periodogram"
14583            | "welch_psd" | "welch"
14584            | "lag_features"
14585            // ── Batch 5: image processing ─────────────────────────────────
14586            | "median_filter_2d"
14587            | "threshold_otsu" | "otsu"
14588            | "histogram_equalize" | "hist_eq"
14589            | "erode_2d" | "dilate_2d"
14590            // ── Batch 5: losses ───────────────────────────────────────────
14591            | "mse_loss" | "mae_loss" | "huber_loss"
14592            // ── Batch 5: spatial ──────────────────────────────────────────
14593            | "vincenty_distance" | "vincenty"
14594            | "mercator_project"
14595            | "destination_from_bearing" | "dest_bearing"
14596            // ── Batch 5: integer sequences ────────────────────────────────
14597            | "recaman" | "recaman_seq"
14598            | "sylvester" | "sylvester_seq"
14599            | "happy_q" | "is_happy"
14600            | "amicable_pair_q"
14601            | "aliquot_sequence"
14602            | "magic_constant"
14603            // ── Batch 5: graph metrics ────────────────────────────────────
14604            | "clustering_coefficient_local" | "cc_local"
14605            | "clustering_coefficient_global" | "cc_global"
14606            | "assortativity" | "common_neighbors" | "jaccard_neighbors"
14607            | "adamic_adar"
14608            | "preferential_attachment_score" | "pa_score"
14609            // ── Batch 5: 3D geometry ──────────────────────────────────────
14610            | "triangle_3d_normal" | "triangle_3d_area"
14611            | "tetrahedron_volume"
14612            | "plane_from_3_points" | "plane_from_pts"
14613            | "point_to_plane_distance" | "pt_plane_dist"
14614            | "ray_triangle_intersect" | "moller_trumbore"
14615            | "ray_sphere_intersect" | "aabb_overlap"
14616            // ── Batch 5: iterative solvers ────────────────────────────────
14617            | "gauss_seidel"
14618            | "jacobi_iteration" | "jacobi_solve"
14619            | "sor_solve" | "sor"
14620            | "thomas_tridiag_solve" | "thomas"
14621            | "richardson_extrapolation" | "richardson"
14622            | "finite_difference_5pt" | "fd5pt"
14623            // ── Batch 5: crypto / algebra ─────────────────────────────────
14624            | "tonelli_shanks_sqrt" | "tonelli_shanks"
14625            | "baby_step_giant_step" | "bsgs"
14626            | "pollard_rho_factor" | "pollard_rho"
14627            | "modular_lcm" | "mlcm"
14628            | "crt_general" | "crt_arbitrary"
14629            // ── Batch 5: physics / chemistry ──────────────────────────────
14630            | "van_der_waals_p" | "vdw_pressure"
14631            | "nernst_equation" | "nernst"
14632            | "arrhenius_rate" | "arrhenius"
14633            | "reduced_mass"
14634            | "ph_to_concentration" | "ph_to_h"
14635            // ── Batch 6: MCMC / SDE / HMM ─────────────────────────────────
14636            | "metropolis_hastings" | "mh_sampler"
14637            | "gibbs_sampler_step" | "gibbs_step"
14638            | "euler_maruyama" | "em_sde"
14639            | "milstein" | "milstein_sde"
14640            | "ornstein_uhlenbeck_path" | "ou_path"
14641            | "hmm_forward" | "hmm_viterbi" | "hmm_backward"
14642            // ── Batch 6: survival / alignment ─────────────────────────────
14643            | "kaplan_meier" | "km_estimator" | "log_rank_test"
14644            | "needleman_wunsch" | "nw_align"
14645            | "smith_waterman" | "sw_align"
14646            // ── Batch 6: chemistry ────────────────────────────────────────
14647            | "gibbs_free_energy" | "delta_g"
14648            | "henderson_hasselbalch" | "hh_eq"
14649            | "radioactive_decay"
14650            | "half_life_to_constant" | "hl_to_lambda"
14651            // ── Batch 6: control theory ───────────────────────────────────
14652            | "pid_step"
14653            | "transfer_function_eval" | "tf_eval"
14654            | "bode_magnitude_db" | "bode_mag_db"
14655            | "bode_phase_deg"
14656            | "lqr_2x2"
14657            // ── Batch 6: game theory ──────────────────────────────────────
14658            | "nash_eq_2x2" | "nash_2x2"
14659            | "shapley_value" | "expected_utility"
14660            // ── Batch 6: operations research ──────────────────────────────
14661            | "hungarian_assignment" | "hungarian"
14662            | "tsp_nearest_neighbor" | "tsp_nn"
14663            | "vertex_cover_2approx" | "vc_2approx"
14664            // ── Batch 6: PDE ──────────────────────────────────────────────
14665            | "heat_eq_1d" | "wave_eq_1d"
14666            | "laplace_2d_jacobi" | "laplace_jacobi"
14667            // ── Batch 6: Bayesian conjugate ───────────────────────────────
14668            | "beta_binomial_update"
14669            | "normal_normal_update"
14670            | "gamma_poisson_update"
14671            | "dirichlet_multinomial_update"
14672            // ── Batch 6: quantum gates ────────────────────────────────────
14673            | "hadamard_gate" | "h_gate"
14674            | "cnot_gate" | "cx_gate"
14675            | "swap_gate" | "cz_gate"
14676            | "qft_matrix" | "phase_gate"
14677            | "s_gate" | "t_gate"
14678            // ── Batch 6: splines ──────────────────────────────────────────
14679            | "bezier_eval"
14680            | "catmull_rom_eval" | "cmr_eval"
14681            | "cubic_hermite_eval" | "ch_eval"
14682            | "bspline_basis" | "nik_basis"
14683            // ── Batch 6: music ────────────────────────────────────────────
14684            | "freq_to_midi" | "midi_to_freq"
14685            | "equal_temperament_freq"
14686            | "cents_difference" | "cents_diff"
14687            // ── Batch 6: astronomy ────────────────────────────────────────
14688            | "redshift_z" | "hubble_distance" | "luminosity_distance"
14689            // ── Batch 6: fluid dynamics ───────────────────────────────────
14690            | "reynolds_number" | "mach_number"
14691            | "prandtl_number" | "bernoulli_velocity"
14692            // ── Batch 6: distributions ────────────────────────────────────
14693            | "negative_binomial_pmf" | "nb_pmf"
14694            | "hypergeometric_pmf"
14695            | "beta_binomial_pmf" | "bb_pmf"
14696            | "von_mises_pdf" | "vmf_pdf"
14697            // ── Batch 6: random graphs ────────────────────────────────────
14698            | "erdos_renyi_random" | "erdos_renyi"
14699            | "barabasi_albert_random" | "barabasi_albert"
14700            | "watts_strogatz_random" | "watts_strogatz"
14701            // ── Batch 6: color science ────────────────────────────────────
14702            | "rgb_to_lab" | "lab_to_rgb"
14703            | "kelvin_to_rgb" | "color_temp_rgb"
14704            // ── Batch 6: integer sequences ────────────────────────────────
14705            | "bell_triangle" | "surjection_count"
14706            | "distinct_partition_count" | "q_partition"
14707            | "fibonacci_q" | "is_fib_number"
14708            // ── Batch 7: stats / divergences / distribs / physics / astro / chem ──
14709            | "bonferroni_correction" | "bonferroni"
14710            | "benjamini_hochberg" | "bh_fdr"
14711            | "tukey_hsd"
14712            | "hellinger_distance"
14713            | "wasserstein_1d" | "earth_movers_1d"
14714            | "chi_squared_divergence"
14715            | "beta_geometric_pmf"
14716            | "generalized_gamma_pdf" | "gengamma_pdf"
14717            | "zip_pmf" | "zero_inflated_poisson_pmf"
14718            | "stefan_boltzmann_luminosity" | "stellar_luminosity"
14719            | "photon_momentum" | "photon_energy_ev"
14720            | "dipole_radiation_power" | "larmor_power"
14721            | "parallax_to_distance" | "hawking_temperature"
14722            | "roche_limit" | "apparent_magnitude" | "distance_modulus"
14723            | "beer_lambert" | "absorbance"
14724            | "rate_law_n"
14725            | "freezing_point_depression" | "fpd"
14726            | "mixed_nash_2x2" | "minimax_2x2"
14727            // ── Batch 7: graphics / DSP / image / clustering / combinatorics / NT ─
14728            | "barycentric_coords_2d" | "barycentric_2d"
14729            | "bresenham_line" | "bilinear_interp_2d"
14730            | "point_in_polygon_2d"
14731            | "hilbert_transform" | "cepstrum"
14732            | "butterworth_lowpass_coeffs" | "butter_lp"
14733            | "savitzky_golay_coeffs" | "sg_coeffs"
14734            | "savitzky_golay_filter" | "sg_filter"
14735            | "canny_edge_intensity" | "canny_intensity"
14736            | "bilateral_filter_basic" | "bilateral_filter"
14737            | "kmeans_pp_init" | "kpp_init"
14738            | "elbow_score" | "wcss"
14739            | "young_tableaux_count" | "syt_count"
14740            | "euler_alt_permutation" | "euler_zigzag"
14741            | "genocchi_number" | "lattice_paths_count"
14742            | "tetration"
14743            | "ackermann_limited" | "ackermann"
14744            | "perfect_power_q" | "b_smooth_q"
14745            // ── Batch 7: networks / crypto / quantum / geom / TS ──────────
14746            | "k_core"
14747            | "rich_club_coefficient" | "rich_club"
14748            | "rsa_basic_encrypt" | "rsa_enc_int"
14749            | "rsa_basic_decrypt" | "rsa_dec_int"
14750            | "dh_shared_secret"
14751            | "bell_state_phi_plus" | "bell_phi_plus"
14752            | "bell_state_psi_minus" | "bell_psi_minus"
14753            | "density_matrix_purity" | "rho_purity"
14754            | "concurrence_2qubit"
14755            | "point_in_circle"
14756            | "circle_circle_intersect_2d"
14757            | "polygon_centroid"
14758            | "sutherland_hodgman_clip" | "sh_clip"
14759            | "kalman_rts_smoother" | "rts_smoother"
14760            // ── Batch 8: bioinformatics ───────────────────────────────────
14761            | "gc_content" | "codon_to_aa"
14762            | "reverse_complement_dna" | "rev_comp_dna"
14763            | "hamming_dna"
14764            | "blosum62_pair_score" | "blosum62"
14765            | "kmer_count"
14766            // ── Batch 8: geographic ───────────────────────────────────────
14767            | "great_circle_bearing" | "gc_bearing"
14768            | "midpoint_lat_lon" | "mid_geo"
14769            | "utm_zone_for"
14770            | "area_polygon_lat_lon" | "geo_polygon_area"
14771            // ── Batch 8: finance ──────────────────────────────────────────
14772            | "crr_binomial_option" | "crr_option"
14773            | "bond_price_clean"
14774            | "bond_yield_to_maturity" | "bond_ytm"
14775            | "modified_duration_bond"
14776            | "convexity_bond" | "bond_convexity"
14777            // ── Batch 8: image quality ────────────────────────────────────
14778            | "ssim" | "psnr" | "mssim"
14779            // ── Batch 8: acoustics ────────────────────────────────────────
14780            | "db_spl_from_pa" | "db_spl"
14781            | "a_weighting_factor" | "a_weight"
14782            | "octave_band_center" | "octave_center"
14783            | "semitone_ratio"
14784            // ── Batch 8: genetics ─────────────────────────────────────────
14785            | "hardy_weinberg"
14786            | "expected_heterozygosity" | "het_e"
14787            | "fst_simple"
14788            | "allele_frequencies"
14789            // ── Batch 8: epidemiology ─────────────────────────────────────
14790            | "sir_step" | "sir_r0" | "doubling_time"
14791            // ── Batch 8: economics ────────────────────────────────────────
14792            | "theil_index"
14793            | "herfindahl_hirschman" | "hhi"
14794            | "atkinson_index"
14795            | "lorenz_curve_points"
14796            // ── Batch 8: APL/J primitives ─────────────────────────────────
14797            | "iota_range" | "iota"
14798            | "reshape_array" | "reshape"
14799            | "grade_up" | "grade_asc"
14800            | "grade_down" | "grade_desc"
14801            // ── Batch 8: plasma physics ───────────────────────────────────
14802            | "plasma_frequency" | "omega_p"
14803            | "debye_length" | "lambda_d"
14804            | "cyclotron_frequency" | "omega_c"
14805            | "larmor_radius" | "gyroradius"
14806            // ── Batch 8: string similarity ────────────────────────────────
14807            | "jaro_winkler_similarity" | "jaro_winkler"
14808            | "metaphone_simple"
14809            // ── Batch 8: rating systems ───────────────────────────────────
14810            | "elo_rating_update" | "elo"
14811            | "glicko_rating_update" | "glicko"
14812            | "dice_sum_pmf"
14813            // ── Batch 8: effect sizes ─────────────────────────────────────
14814            | "cohens_d" | "effect_size_d"
14815            | "cliff_delta"
14816            | "vargha_delaney_a12" | "a12"
14817            // ── Batch 8: control transient ────────────────────────────────
14818            | "step_response_2nd_order" | "step_2nd"
14819            | "overshoot_2nd_order" | "overshoot_pct"
14820            // ── Batch 8: matrix norms ─────────────────────────────────────
14821            | "frobenius_norm"
14822            | "spectral_norm" | "operator_norm_2"
14823            | "trace_matrix" | "tr_mat"
14824            // ── Batch 8: networks ─────────────────────────────────────────
14825            | "homophily_index" | "homophily"
14826            | "dyad_census" | "triad_census"
14827            // ── Batch 8: misc ─────────────────────────────────────────────
14828            | "sigmoid_inverse" | "logit"
14829            // ── Batch 9: list / string / date / color / music / astro / perm / linguistics / regression / combinatorics / PRNG ──
14830            | "partition_at" | "drop_at" | "insert_at_idx"
14831            | "replace_at_index" | "set_at"
14832            | "swap_indices" | "nth_largest" | "nth_smallest"
14833            | "position_of_all_matching" | "positions_of_all"
14834            | "string_take_first" | "string_take_last"
14835            | "string_drop_first" | "string_drop_last"
14836            | "pluralize_simple"
14837            | "singularize_simple" | "singularize"
14838            | "capitalize_words" | "title_words"
14839            | "format_table_simple" | "ascii_table"
14840            | "days_between" | "weeks_between"
14841            | "months_between" | "years_between"
14842            | "first_of_month" | "last_of_month"
14843            | "day_of_week_iso" | "iso_dow"
14844            | "easter_sunday" | "chinese_zodiac"
14845            | "iso_week_number" | "iso_week"
14846            | "relative_luminance" | "wcag_luminance"
14847            | "contrast_ratio_wcag" | "wcag_contrast"
14848            | "delta_e_76" | "delta_e"
14849            | "color_blend_t" | "lerp_color"
14850            | "chord_to_freqs" | "scale_to_intervals"
14851            | "interval_semitones"
14852            | "transpose_freq_semitones" | "transpose_semi"
14853            | "bpm_to_period" | "midi_to_pitch_class"
14854            | "key_signature_for" | "circle_of_fifths_step"
14855            | "moon_phase" | "equation_of_time"
14856            | "solar_declination" | "sidereal_day_period" | "ecliptic_obliquity"
14857            | "permutation_order"
14858            | "permutation_parity" | "perm_sign"
14859            | "identity_permutation"
14860            | "permutation_compose" | "perm_mul"
14861            | "flesch_reading_ease" | "flesch_kincaid_grade"
14862            | "gunning_fog"
14863            | "automated_readability_index" | "ari"
14864            | "lix"
14865            | "adjusted_r_squared" | "adj_r2"
14866            | "aic" | "bic"
14867            | "residuals_compute" | "compute_residuals"
14868            | "composition_count" | "weak_composition_count"
14869            | "necklace_count" | "bracelet_count"
14870            | "multiset_permutations_count" | "multinomial_count"
14871            | "pearson_hash_byte" | "pearson_hash"
14872            | "xorshift32_step" | "lcg_next_u32"
14873            | "fisher_yates_shuffle"
14874            // ── Batch 10 ──────────────────────────────────────────────────
14875            | "tetrahedral_number" | "square_pyramidal_number"
14876            | "octahedral_number" | "pentagonal_pyramidal_number"
14877            | "cake_number" | "cuban_number" | "centered_hexagonal_number"
14878            | "carmichael_q" | "is_carmichael"
14879            | "sphenic_q" | "is_sphenic"
14880            | "seven_smooth_q" | "is_7_smooth"
14881            | "cartesian_product_n" | "cart_n"
14882            | "multiset_union" | "multiset_intersection" | "multiset_difference"
14883            | "polynomial_roots_dk" | "durand_kerner"
14884            | "lin_bairstow_step" | "bairstow"
14885            | "heap_sift_down"
14886            | "fenwick_build" | "bit_build"
14887            | "fenwick_query" | "bit_query"
14888            | "segment_tree_sum" | "seg_sum"
14889            | "kmp_failure" | "kmp"
14890            | "z_array" | "z_func"
14891            | "suffix_array_naive"
14892            | "manacher_radii" | "manacher"
14893            | "rabin_karp_hash" | "lcp_array"
14894            | "regex_escape_simple"
14895            | "horspool_search" | "bm_horspool"
14896            | "lpt_schedule" | "lpt"
14897            | "johnsons_rule" | "johnson_2m"
14898            | "bit_reverse_32" | "bit_reverse"
14899            | "bin_to_gray" | "gray_to_bin"
14900            | "swap_bits_pos" | "swap_bits"
14901            | "hamming_weight" | "popcnt"
14902            | "hamming_distance_int" | "hamdist_int"
14903            | "internal_rate_of_return"
14904            | "modified_irr" | "mirr"
14905            | "payback_period_simple" | "payback_simple"
14906            | "rfc3339_format" | "rfc3339"
14907            | "rfc3339_parse"
14908            | "iso_ordinal_date" | "ordinal_date"
14909            // ── Batch 11 ──────────────────────────────────────────────────
14910            | "lazy_caterer" | "central_polygonal"
14911            | "centered_square" | "centered_triangular" | "centered_pentagonal"
14912            | "star_number" | "dodecahedral_number" | "icosahedral_number"
14913            | "pronic_number" | "squared_triangular"
14914            | "woodall_number" | "cullen_number"
14915            | "repunit" | "repdigit" | "kaprekar_routine_step"
14916            | "smith_q"
14917            | "keith_q" | "is_keith"
14918            | "armstrong_q" | "is_armstrong"
14919            | "fnv1a_hash" | "djb2_hash"
14920            | "jenkins_one_at_a_time" | "jenkins_oat"
14921            | "murmurhash3_x32"
14922            | "adler32_hash" | "crc16_ccitt"
14923            | "vec_dot"
14924            | "l1_norm" | "l2_norm" | "vec_l2"
14925            | "linf_norm" | "max_norm" | "lp_norm"
14926            | "unit_vector"
14927            | "vector_project" | "proj" | "vector_reject"
14928            | "orthogonalize_vectors" | "gram_schmidt"
14929            | "outer_product" | "vec_outer"
14930            | "matrix_diagonal" | "mdiagvec"
14931            | "matrix_anti_diagonal"
14932            | "matrix_symmetric_q" | "matrix_orthogonal_q"
14933            | "geometric_mean_arr" | "harmonic_mean_arr"
14934            | "quadratic_mean_arr" | "lehmer_mean"
14935            | "running_mean" | "running_variance"
14936            | "outlier_iqr_q" | "z_score_robust"
14937            | "geometric_sequence" | "arithmetic_sequence"
14938            | "log_sum_exp" | "lse"
14939            | "log_sigmoid" | "log1p_exp"
14940            | "string_chars"
14941            | "string_words_count" | "word_count_simple"
14942            | "string_lines_count" | "line_count_simple"
14943            | "string_intersperse" | "string_replicate"
14944            | "string_uniq_chars" | "string_letter_frequency"
14945            | "anagram_q" | "is_anagram_q"
14946            | "string_take_while" | "string_drop_while"
14947            | "string_split_at_first" | "string_partition_at_word"
14948            // ── Batch 12 ──────────────────────────────────────────────────
14949 | "relativistic_kinetic"
14950            | "lorentz_factor_v" | "doppler_relativistic"
14951            | "drag_force_quadratic" | "terminal_velocity"
14952            | "carnot_efficiency" | "otto_efficiency"
14953            | "brayton_efficiency" | "diesel_efficiency"
14954            | "specific_heat_const_v" | "speed_of_sound_ideal"
14955            | "kepler_period_au" | "synodic_period"
14956            | "hill_radius" | "jeans_length"
14957            | "chandrasekhar_mass" | "eddington_luminosity"
14958            | "schwarzschild_radius_m" | "gravity_at_radius"
14959            | "gravitational_pe"
14960            | "freefall_time" | "pendulum_freq" | "spring_period"
14961            | "centripetal_accel" | "lens_focal_length"
14962            | "avogadros_number" | "boltzmann_const"
14963            | "planck_const_h" | "gas_constant_r"
14964            | "concentration_dilute" | "partial_pressure"
14965            | "mole_fraction" | "molarity" | "molality"
14966            | "normality_chem" | "ionic_strength"
14967 | "titration_volume"
14968            | "atomic_radius_pm" | "de_broglie_wavelength_kg"
14969 | "lotka_volterra_step"
14970            | "michaelis_menten" | "hill_equation"
14971            | "lineweaver_burk" | "eadie_hofstee_y"
14972            | "arrhenius_temp_q10"
14973            | "body_surface_area_dubois" | "bsa_dubois"
14974            | "bmr_harris_benedict_male" | "bmr_harris_benedict_female"
14975            | "max_heart_rate" | "target_heart_rate"
14976            | "vo2_max_estimate" | "pulse_pressure"
14977            | "mean_arterial_pressure" | "map_bp"
14978            | "dew_point_magnus" | "heat_index_celsius"
14979            | "wind_chill_celsius" | "pressure_altitude_m"
14980            | "density_altitude_m" | "saturation_vapor_pressure"
14981            | "humidex" | "utci_simple"
14982            | "resistance_parallel" | "r_parallel"
14983            | "resistance_series" | "r_series"
14984            | "capacitance_parallel" | "c_parallel"
14985            | "capacitance_series" | "c_series"
14986            | "inductance_parallel" | "l_parallel"
14987            | "inductance_series" | "l_series"
14988            | "voltage_divider" | "current_divider"
14989            | "lc_resonant" | "q_factor_rlc"
14990            | "skin_depth" | "wire_resistance"
14991            | "motor_torque" | "efficiency_ratio"
14992            | "dB_voltage" | "db_voltage"
14993            | "dB_power" | "db_power"
14994            // ── Batch 13 ──────────────────────────────────────────────────
14995            | "bfs_distances" | "dfs_preorder" | "connected_components"
14996            | "graph_is_tree" | "graph_density"
14997            | "graph_average_degree" | "graph_max_degree" | "graph_min_degree"
14998            | "graph_complement"
14999            | "in_degree_directed" | "out_degree_directed"
15000            | "graph_eccentricity_all" | "is_connected"
15001            | "articulation_points" | "bridges_edges"
15002            | "eulerian_path_q" | "hamiltonian_brute"
15003            | "string_to_charcodes" | "charcodes_to_string"
15004            | "string_xor"
15005            | "string_camel_to_snake" | "string_snake_to_camel"
15006            | "string_kebab_to_snake" | "string_snake_to_kebab"
15007            | "palindromic_q" | "substring_count"
15008            | "string_truncate_ellipsis" | "string_expand_tabs"
15009            | "string_normalize_spaces"
15010 | "days_in_year" | "quarter_of_year"
15011            | "zeller_day_of_week" | "age_from_birthdate"
15012            | "business_days_between" | "unix_epoch_to_iso"
15013            | "loan_payment_pmt" | "loan_balance"
15014            | "amortization_total_interest"
15015            | "apr_to_apy" | "apy_to_apr"
15016            | "compound_interest_periods" | "simple_interest_compute"
15017
15018            | "perpetuity_value" | "growing_perpetuity"
15019            | "annuity_present_value" | "annuity_future_value"
15020            | "capm_expected_return"
15021            | "treynor_ratio"
15022            | "jensens_alpha" | "information_ratio"
15023            | "friction_factor_laminar" | "swamee_jain_factor"
15024            | "pipe_pressure_drop" | "orifice_velocity"
15025            | "chezy_velocity" | "manning_velocity"
15026            | "froude_number" | "weber_number" | "grashof_number"
15027            | "nusselt_dittus_boelter"
15028            // ── Batches 14-16 ─────────────────────────────────────────────
15029            | "mollweide_project" | "robinson_project" | "sinusoidal_project"
15030            | "equirectangular_project" | "lambert_azimuthal_project" | "albers_conic_project"
15031            | "geohash_encode" | "geohash_decode" | "geohash_neighbor" | "geohash_bbox"
15032            | "gabor_kernel" | "unsharp_mask_kernel" | "emboss_kernel"
15033            | "box_blur_kernel" | "motion_blur_kernel" | "sharpen_kernel"
15034            | "edge_detect_kernel" | "sobel_diagonal_kernel" | "haar_2d_step"
15035            | "db4_coeffs" | "db6_coeffs" | "sym4_coeffs" | "coif1_coeffs"
15036            | "aes_sbox_byte" | "aes_inv_sbox_byte"
15037            | "chacha20_qround" | "xtea_round" | "speck_round" | "simon_round"
15038            | "kepler_hyperbolic" | "hohmann_dv1" | "hohmann_dv2" | "hohmann_total"
15039            | "bielliptic_total" | "lambert_simple"
15040            | "horizon_distance" | "solar_zenith_angle" | "air_mass_kasten"
15041            | "solar_constant" | "julian_centuries_j2000"
15042            | "mean_solar_longitude" | "mean_solar_anomaly" | "lst_to_solar"
15043            | "ra_dec_to_az_alt" | "ecliptic_to_equatorial" | "equatorial_to_galactic"
15044            | "orbital_eccentricity" | "semi_major_axis"
15045            | "specific_orbital_energy" | "specific_angular_momentum"
15046            | "toffoli_gate" | "ccx_gate" | "fredkin_gate" | "cswap_gate"
15047            | "iswap_gate" | "sqrt_swap_gate"
15048            | "rx_gate" | "ry_gate" | "rz_gate"
15049            | "ghz_state_n" | "w_state_n"
15050            | "depolarizing_channel" | "dephasing_channel" | "amplitude_damping_channel"
15051            | "quantum_fidelity_pure" | "trace_distance"
15052            | "bell_inequality_chsh" | "pauli_decomposition_2x2"
15053            | "quantum_relative_entropy" | "qft_4_real"
15054            | "bwt_encode" | "bwt_decode" | "mtf_encode" | "mtf_decode"
15055
15056            | "lyndon_factorize" | "christoffel_word" | "sturmian_word"
15057            | "z_function_alt" | "period_of_string" | "borders_of_string"
15058            | "thue_morse_string" | "fibonacci_word"
15059            | "mann_kendall_tau" | "theil_sen_slope" | "hodges_lehmann"
15060            | "huber_m_estimator" | "winsorized_variance_arr"
15061            | "bowley_skewness" | "pearson_skewness_2"
15062            | "concordance_correlation" | "quantile_p"
15063            | "label_propagation_step" | "modularity_q"
15064            | "clique_count_3" | "local_efficiency" | "global_efficiency"
15065            | "diameter_unweighted"
15066            | "aitken_delta_squared" | "wynn_epsilon"
15067            | "shanks_transform" | "levin_t_transform"
15068            | "harmonic_seq_sum" | "alternating_seq_sum"
15069            // ── Batches 17-18 ─────────────────────────────────────────────
15070            | "sparse_csr_build" | "sparse_csr_mul_vec" | "sparse_density"
15071            | "lower_triangular_q" | "upper_triangular_q"
15072            | "diagonal_dominance_q" | "matrix_zero_q" | "matrix_identity_q"
15073            | "matrix_random_uniform" | "matrix_random_normal"
15074            | "andrew_monotone_chain" | "polygon_area_signed"
15075            | "polygon_convex_q" | "iou_2d_axis_aligned" | "hausdorff_distance_2d"
15076            | "minkowski_sum_simple" | "circle_3_points"
15077            | "polygon_winding_number" | "segment_length"
15078            | "segments_parallel_q" | "segments_perpendicular_q"
15079            | "burr_xii_pdf" | "burr_xii_cdf" | "dagum_pdf" | "lomax_pdf"
15080            | "birnbaum_saunders_pdf" | "tukey_lambda_quantile"
15081            | "half_cauchy_pdf" | "half_logistic_pdf" | "reciprocal_pdf"
15082            | "levy_pdf" | "voigt_profile_simple"
15083            | "gompertz_pdf" | "inverse_weibull_pdf"
15084            | "log_gamma_simple" | "inverse_chi2_pdf"
15085            | "poly1305_block_step" | "x25519_field_mul" | "curve25519_mul_simple"
15086            | "secp256k1_y_recover" | "hmac_step_xor"
15087            | "pkcs7_pad" | "pkcs7_unpad" | "xor_byte_string"
15088 | "atbash_cipher"
15089            | "vigenere_encrypt" | "vigenere_decrypt" | "xor_brute_keylen"
15090            | "arima_diff" | "seasonal_diff"
15091            | "garch_step" | "egarch_step"
15092            | "realized_volatility" | "max_drawdown_arr"
15093            | "calmar_ratio" | "omega_ratio" | "kelly_criterion"
15094            | "var_historical" | "cvar_historical"
15095            | "graph_degree_distribution" | "graph_count_edges"
15096            | "graph_bipartite_match_simple" | "graph_count_triangles"
15097            | "graph_avg_clustering" | "graph_transitivity"
15098            | "graph_max_clique_brute" | "graph_independent_set_brute"
15099            | "graph_count_paths_length_k" | "graph_pagerank_simple"
15100            // ── batch 19: integration / ODE / root finding / optimization ─
15101            | "boole_rule" | "boole_int"
15102            | "gauss_legendre_5" | "gl5"
15103            | "gauss_kronrod_15" | "gk15"
15104
15105            | "midpoint_rule"
15106            | "adams_bashforth_4" | "ab4"
15107            | "heun_method" | "rk45_cash_karp" | "rkck"
15108            | "milne_pc" | "milne"
15109            | "modified_midpoint_ode" | "modmidpoint"
15110            | "backward_euler" | "implicit_euler"
15111            | "crank_nicolson_ode" | "cn_ode"
15112            | "brent_root" | "brent" | "ridders_root" | "ridders"
15113            | "steffensen_root" | "steffensen" | "halley_root" | "halley"
15114            | "householder_root" | "muller_root" | "muller"
15115            | "regula_falsi" | "false_position"
15116            | "secant_root" | "secant"
15117            | "anderson_step" | "aberth_step" | "inverse_quad_interp"
15118            | "lm_step" | "gradient_descent_step"
15119 | "nesterov_step" | "adagrad_step"
15120            | "cg_beta_pr" | "cg_beta_fr" | "bfgs_h_update_1d"
15121            | "wolfe_strong_q" | "dogleg_step"
15122            | "nelder_mead_reflect" | "nelder_mead_expand" | "nelder_mead_contract"
15123            | "sa_accept_prob" | "sa_boltzmann_temp" | "sa_cauchy_temp"
15124            | "sa_geometric_temp" | "acceptance_target"
15125            // ── batch 20: financial pricing models ────────────────────────
15126            | "bs_call" | "blackscholes_call" | "bs_put" | "blackscholes_put"
15127 | "bs_theta_call" | "bs_rho_call"
15128 | "bachelier_call" | "black76_call"
15129            | "crr_american_call" | "crr_american_put" | "jr_european_call"
15130            | "trinomial_call" | "heston_price_simple" | "sabr_implied_vol"
15131            | "merton_jump_call" | "asian_call_mc" | "barrier_up_out_call"
15132            | "digital_call" | "lookback_call"
15133            | "macaulay_duration" | "forward_rate"
15134            | "discount_continuous" | "ytm_newton"
15135            | "vasicek_bond" | "cir_bond" | "hull_white_drift"
15136            | "cds_upfront" | "black_karasinski_drift" | "quanto_adjustment"
15137            | "fx_forward" | "garman_kohlhagen_call" | "margrabe" | "stulz_min_call"
15138            | "sharpe_annualized"
15139            | "jensen_alpha" | "modified_sharpe"
15140            // ── batch 21: chemistry ───────────────────────────────────────
15141            | "ph_from_h" | "poh_from_oh" | "pka_from_ka"
15142 | "henderson_base"
15143            | "arrhenius_k" | "eyring_k"
15144            | "first_order_concentration" | "first_order_half_life"
15145            | "second_order_concentration" | "second_order_half_life"
15146            | "zero_order_concentration"
15147
15148            | "ideal_gas_n" | "redlich_kwong_p"
15149            | "compressibility_z"
15150            | "kc_from_rates" | "kp_from_kc" | "reaction_quotient" | "rxn_q"
15151            | "le_chatelier_dir"
15152            | "dg_from_k" | "k_from_dg" | "vant_hoff" | "clausius_clapeyron" | "antoine_p"
15153 | "emf_from_half_cells" | "faraday_mass_deposited"
15154 | "transmittance" | "ksp_from_concs"
15155 | "debye_huckel"
15156            | "cp_monatomic_ideal" | "cv_monatomic_ideal"
15157            | "heat_capacity_q" | "calorimeter_dt" | "enthalpy_reaction"
15158            | "avogadro_count" | "moles_from_mass"
15159            | "dilution_v2" | "raoult_law" | "bp_elevation" | "fp_depression"
15160            | "osmotic_pressure" | "rydberg_lambda" | "bohr_radius_n"
15161            | "bohr_energy_ev" | "photon_energy_freq" | "photon_energy_lambda"
15162            | "de_broglie"
15163            // ── batch 22: biology / ecology ───────────────────────────────
15164 | "logistic_growth_step" | "logistic_growth_analytic"
15165            | "gompertz_growth_step" | "allee_growth_step"
15166 | "growth_rate_from_ratio"
15167 | "seir_step" | "seird_step" | "sis_step"
15168            | "r0_basic" | "rt_effective" | "herd_immunity_threshold" | "generation_time"
15169 | "inverse_simpson"
15170            | "pielou_evenness" | "margalef_richness" | "menhinick_richness"
15171            | "berger_parker" | "sorensen_dice"
15172            | "rao_quadratic_entropy"
15173 | "selection_step" | "nei_genetic_distance"
15174            | "effective_pop_size" | "carrying_capacity_from_data"
15175            | "petersen_estimator" | "chapman_estimator"
15176            | "lv_competition_step"
15177            | "holling_type1" | "holling_type2" | "holling_type3"
15178            | "leslie_step" | "net_reproductive_rate" | "generation_time_demo"
15179            | "finite_rate_lambda" | "kleibers_law" | "bergmann_adjust"
15180            | "q10" | "species_area" | "intrinsic_growth_rate"
15181            | "macarthur_wilson_immigration" | "macarthur_wilson_extinction"
15182            | "island_equilibrium"
15183            // ── batch 23: EM / optics / relativity ────────────────────────
15184 | "efield_point" | "epotential_point"
15185 | "capacitor_charge"
15186            | "ohm_voltage" | "power_vi" | "power_i2r"
15187
15188 | "capacitance_parallel_sum"
15189            | "bfield_wire" | "bfield_solenoid" | "lorentz_force_mag"
15190 | "faraday_emf"
15191 | "lc_frequency" | "lc_omega"
15192            | "rc_tau" | "rl_tau"
15193            | "poynting_magnitude" | "em_intensity" | "radiation_pressure"
15194            | "em_wavelength" | "em_frequency"
15195            | "snell_theta2"
15196            | "index_from_speed" | "fresnel_reflection_normal"
15197            | "fresnel_rs" | "fresnel_rp"
15198            | "lensmaker" | "thin_lens_v" | "mirror_equation_v"
15199            | "lens_magnification" | "diffraction_grating_angle"
15200            | "single_slit_min" | "rayleigh_resolution"
15201            | "lorentz_gamma"
15202            | "rel_momentum" | "rel_ke" | "rel_total_energy" | "rel_energy_pm"
15203            | "relativistic_doppler" | "rel_velocity_add"
15204
15205            | "wave_string_speed" | "sound_solid" | "sound_gas"
15206            | "doppler_classical" | "standing_wave_fundamental"
15207            | "open_pipe_harmonic" | "closed_pipe_harmonic"
15208            | "sound_db"
15209            | "alfven_speed"
15210            | "grav_time_dilation" | "grav_redshift"
15211            // ── batch 24: graph algorithms ────────────────────────────────
15212            | "kosaraju_scc" | "bridges"
15213            | "max_flow_ek" | "min_cut_value" | "hopcroft_karp"
15214
15215 | "katz_centrality" | "hits_simple"
15216            | "pagerank_damped" | "cc_count" | "cc_labels"
15217            | "topological_sort_kahn" | "has_cycle_directed" | "has_cycle_undirected"
15218 | "diameter_bfs" | "radius_bfs"
15219            | "num_edges" | "k_coreness"
15220            | "greedy_coloring" | "chromatic_number_greedy"
15221            | "sum_degrees" | "avg_degree" | "max_degree"
15222            | "is_tree" | "girth"
15223            // ── batch 25: signal processing ───────────────────────────────
15224            | "hamming_window" | "hann_window" | "blackman_window"
15225            | "blackman_harris_window" | "bartlett_window" | "welch_window"
15226            | "kaiser_window" | "tukey_window" | "gaussian_window"
15227            | "hilbert_envelope"
15228            | "biquad_step" | "biquad_lowpass_coeffs" | "biquad_highpass_coeffs"
15229            | "biquad_bandpass_coeffs" | "biquad_notch_coeffs" | "biquad_allpass_coeffs"
15230            | "biquad_peak_coeffs" | "biquad_lowshelf_coeffs" | "biquad_highshelf_coeffs"
15231            | "butterworth_prewarp" | "butterworth_order"
15232            | "fir_moving_average" | "fir_lowpass_design"
15233 | "spectrogram_simple"
15234            | "zero_pad" | "resample_nearest" | "resample_linear" | "quantize"
15235            | "mu_law_encode" | "mu_law_decode" | "a_law_encode" | "a_law_decode"
15236            | "chirp_linear"
15237            // ── batch 26: cryptography deep ───────────────────────────────
15238            | "fnv1a_32" | "fnv1a_64" | "sdbm_hash"
15239            | "siphash24"
15240            | "pbkdf2_hmac_step" | "scrypt_round" | "bcrypt_cost_iters"
15241            | "argon2_block_mix" | "hkdf_expand_step"
15242            | "lfsr_galois_step" | "mt19937_temper" | "xorshift64" | "xorshift32"
15243            | "pcg32_step" | "lcg_numrec_step" | "splitmix64_step" | "wyhash_mix"
15244
15245            | "xor_cipher_byte"
15246            | "railfence_encrypt" | "beaufort" | "affine_encrypt" | "substitution_encrypt"
15247            | "letter_frequency" | "english_chi2" | "index_of_coincidence" | "kasiski_repeats"
15248            | "deterministic_prime" | "dh_shared" | "rsa_encrypt_simple"
15249            | "monobit_test" | "approximate_entropy"
15250            // ── batch 27: ML extensions ───────────────────────────────────
15251            | "gini_impurity" | "entropy_bits" | "information_gain" | "gain_ratio"
15252            | "nb_gaussian_likelihood" | "nb_bernoulli_likelihood" | "nb_multinomial_log_likelihood"
15253            | "adaboost_alpha" | "hinge_loss" | "squared_hinge"
15254            | "logistic_loss"
15255 | "sigmoid_grad" | "tanh_grad"
15256 | "relu_grad"
15257 | "softsign" | "prelu" | "threshold_act"
15258            | "confusion_counts" | "mcc" | "f_beta" | "specificity"
15259            | "balanced_accuracy" | "cohen_kappa" | "brier_score" | "log_loss"
15260            | "tversky" | "mahalanobis_1d"
15261 | "log_softmax" | "one_hot" | "topk_indices"
15262            | "minmax_scale" | "zscore_norm" | "robust_scale"
15263            // ── batch 28: geometry / topology ─────────────────────────────
15264            | "triangle_area_heron" | "triangle_area_pts"
15265            | "triangle_inradius" | "triangle_circumradius"
15266            | "regular_ngon_area" | "regular_ngon_inradius" | "regular_ngon_circumradius"
15267 | "n_ball_volume"
15268 | "cylinder_surface" | "cone_surface"
15269
15270            | "ellipsoid_volume" | "ellipsoid_surface_approx"
15271            | "dist_point_line_2d" | "dist_point_plane_3d" | "closest_pt_segment_2d"
15272            | "bbox_from_points"
15273 | "euclidean_distance_nd"
15274 | "hamming_distance_str"
15275 | "great_circle_law_of_cos"
15276            | "initial_bearing" | "midpoint_great_circle"
15277            | "shoelace_area" | "polygon_is_convex" | "convex_hull_jarvis"
15278            | "euler_characteristic" | "genus_from_euler"
15279            | "spherical_triangle_area" | "polygon_with_holes_area" | "picks_theorem"
15280            | "centroid_nd" | "covariance_matrix_pts" | "simplex_volume_3d"
15281            // ── batch 29: special functions extra ─────────────────────────
15282            | "hyper2f1" | "hyper1f1" | "hyper0f1" | "pochhammer"
15283            | "mathieu_ce0" | "mathieu_se1" | "parabolic_d0" | "parabolic_d1"
15284            | "whittaker_m" | "struve_h0" | "struve_h1"
15285            | "lambert_w0" | "wright_omega"
15286            | "sinhc" | "cosh_minus1_over_x2"
15287            | "sine_integral_si" | "cosine_integral_ci" | "exp_integral_e1"
15288 | "dawson_function" | "owen_t"
15289            | "spherical_bessel_j0" | "spherical_bessel_j1"
15290            | "spherical_bessel_y0" | "spherical_bessel_y1"
15291            | "mod_sph_bessel_i0" | "mod_sph_bessel_i1" | "mod_sph_bessel_k0"
15292            | "coulomb_f0"
15293            | "polylog_li2" | "polylog_n"
15294
15295 | "ti2" | "clausen_cl2"
15296            | "bose_einstein_g" | "fermi_dirac_int"
15297            | "theta3" | "theta2"
15298            | "jacobi_sn_small_q" | "jacobi_cn_small_q" | "jacobi_dn_small_q"
15299            | "riemann_xi" | "bessel_jn_general" | "bessel_in_general"
15300            // ── batch 30: astronomy / music / color / units ───────────────
15301 | "absolute_magnitude"
15302            | "pc_to_ly" | "ly_to_pc" | "pc_to_au" | "au_to_m"
15303            | "solar_mass_to_kg" | "solar_luminosity_to_w"
15304            | "hubble_distance_mpc" | "comoving_distance_approx" | "critical_density"
15305            | "et_freq_ratio" | "midi_to_hz" | "hz_to_midi" | "cents_between"
15306            | "just_intonation_ratio" | "pythagorean_ratio"
15307            | "beat_frequency" | "bpm_to_spb" | "note_name_to_midi"
15308 | "rgb_to_yiq" | "rgb_to_yuv601"
15309            | "srgb_to_xyz" | "xyz_to_lab" | "delta_e_94"
15310
15311
15312            | "feet_to_meters" | "meters_to_feet"
15313            | "lb_to_kg" | "kg_to_lb"
15314            | "mph_to_kmh" | "kmh_to_mph" | "mps_to_kmh" | "kmh_to_mps" | "knots_to_kmh"
15315 | "atm_to_pa" | "pa_to_atm" | "mmhg_to_pa"
15316            | "ev_to_joules" | "joules_to_ev" | "btu_to_joules" | "kwh_to_joules"
15317            | "bpm_to_midi_tick_us" | "iso226_phon_adjustment"
15318            | "db_to_amp" | "amp_to_db"
15319            | "roman_encode" | "roman_decode" | "number_to_english"
15320            // ── batch 31: cosmology / GR / FLRW ───────────────────────────
15321            | "hubble_lcdm" | "hubble_time" | "hubble_distance_si" | "critical_density_si"
15322            | "comoving_distance" | "angular_diameter_distance"
15323            | "lookback_time" | "age_at_z" | "scale_factor" | "redshift_from_a"
15324            | "omega_m_at_z" | "lcdm_eos" | "cpl_w" | "deceleration_q"
15325            | "schwarzschild_radius_kg" | "kerr_ergosphere_eq" | "kerr_horizon"
15326 | "bh_entropy" | "bh_evaporation_time"
15327            | "schwarzschild_isco" | "photon_sphere_radius"
15328            | "tidal_force" | "grav_dilation_factor" | "lense_thirring_omega"
15329            | "gw_strain_amplitude" | "chirp_mass" | "grav_binding_energy"
15330            | "roche_limit_rigid" | "roche_limit_fluid"
15331            | "lagrange_l1" | "sphere_of_influence"
15332            | "freefall_velocity_schwarzschild" | "einstein_ring_radius"
15333            | "microlensing_magnification" | "cosmic_distance_modulus_si"
15334            | "cmb_temperature" | "cmb_temperature_at_z"
15335 | "stefan_boltzmann_si" | "planck_spectral_radiance"
15336            | "schwarzschild_g_tt" | "schwarzschild_g_rr" | "kretschmann_schwarzschild"
15337            | "hill_velocity" | "vacuum_energy_density"
15338            | "sound_horizon_recomb" | "bao_scale_today" | "sigma8_default"
15339            | "lensing_convergence" | "sigma_crit"
15340            | "perihelion_precession" | "shapiro_delay" | "light_deflection_angle"
15341 | "tov_mass_limit"
15342            | "main_sequence_lifetime" | "schwarzschild_freefall_time"
15343            | "friedmann_density_total" | "cosmological_constant"
15344
15345 | "planck_energy"
15346            // ── batch 32: quantum mechanics deep ──────────────────────────
15347            | "pure_state_density" | "purity"
15348            | "linear_entropy" | "quantum_mutual_info"
15349 | "eof_from_concurrence"
15350            | "bell_state_index" | "chsh_expectation" | "tsirelson_bound"
15351            | "pauli_real_part" | "pauli_y_imag"
15352            | "bloch_to_density_real" | "bloch_purity_check"
15353            | "fidelity_pure_real" | "l1_coherence" | "relative_entropy_coherence"
15354            | "kraus_apply" | "bit_flip_prob" | "phase_flip_prob"
15355            | "depolarizing_density_2x2" | "amplitude_damping_excited"
15356            | "quantum_fisher_info" | "cramer_rao_bound" | "squeezing_db" | "heisenberg_min"
15357            | "coherent_mean_photons" | "thermal_mean_photons" | "poisson_photon_pmf"
15358            | "bose_einstein_pmf" | "mandel_q" | "g2_zero"
15359            | "free_particle_energy" | "infinite_well_energy" | "harmonic_oscillator_energy"
15360            | "hydrogen_energy_n" | "stark_shift_linear"
15361            | "zeeman_energy" | "larmor_frequency" | "rabi_frequency"
15362            | "schrodinger_step_real" | "probability_density" | "state_norm" | "state_normalize"
15363 | "quantum_variance" | "spin_casimir"
15364            | "cg_simple" | "wigner_3j_bound" | "qho_ground_state"
15365            | "tunneling_prob" | "gamow_factor" | "compton_wavelength" | "uncertainty_position"
15366            | "berry_phase_spin_half" | "zeno_survival" | "decoherence_time"
15367            | "ramsey_visibility" | "fermi_golden_rule"
15368            // ── batch 33: bioinformatics deep ─────────────────────────────
15369            | "needleman_wunsch_score" | "smith_waterman_score" | "pam250_score"
15370            | "tanimoto_bits" | "translate_dna" | "transcribe_dna_rna" | "reverse_transcribe"
15371            | "at_content" | "tm_wallace" | "tm_marmur" | "codon_adaptation_index"
15372            | "kmer_jaccard" | "sequence_shannon_info" | "pwm_score"
15373            | "msa_column_entropy" | "seq_logo_information"
15374 | "damerau_levenshtein" | "lcs_length"
15375 | "hirschberg_lcs_length" | "common_kmers"
15376            | "jukes_cantor_distance" | "kimura_2p_distance" | "felsenstein_step"
15377            | "branch_length_substitutions" | "num_unrooted_trees" | "bayes_posterior"
15378            | "hw_expected_counts" | "allele_frequency" | "ld_d" | "ld_r_squared"
15379 | "heterozygosity" | "ne_from_variance"
15380            | "expected_coverage" | "lander_waterman_gaps"
15381            | "bh_adjusted_p" | "zscore_count"
15382 | "go_enrichment_p" | "blosum45_score"
15383            | "henikoff_weight" | "hamming_protein" | "codon_usage_variance"
15384            | "dnds_ratio" | "mutation_rate" | "tajimas_d" | "wattersons_theta"
15385            | "coalescent_expected_time" | "coalescent_tree_length" | "nm_from_fst"
15386            // ── batch 34: ODE advanced ────────────────────────────────────
15387            | "bdf1_step" | "bdf2_step" | "bdf3_step" | "bdf4_step" | "bdf5_step" | "bdf6_step"
15388            | "ab1_step" | "ab2_step" | "ab3_step"
15389            | "am2_step" | "am3_step" | "am4_step"
15390            | "ros2_step" | "imex_euler_step" | "symplectic_euler_step"
15391            | "leapfrog_step" | "stormer_verlet_step"
15392            | "rk4_single" | "dopri5_combine" | "rkf45_error"
15393            | "lobatto_iiia_2" | "lobatto_iiic_3" | "gauss_irk_2_stage" | "magnus_1st"
15394            | "euler_lte" | "trapezoidal_lte" | "pi_step_size"
15395            | "stiffness_ratio" | "spectral_radius"
15396            | "heun_euler_step" | "bogacki_shampine_step" | "verner_8_combine"
15397            | "rk_combine" | "ab_coeff_sum"
15398            | "newmark_beta_step" | "wilson_theta_step"
15399            | "strang_split" | "lie_split"
15400            | "exp_euler_step" | "etd_rk2" | "dde_euler_step"
15401            | "em_step" | "milstein_step" | "heun_sde_step" | "stratonovich_correction"
15402            | "predictor_corrector" | "numerical_jacobian_col"
15403            | "cn_coefficient" | "imex_theta_split" | "bulirsch_stoer_step"
15404            | "cfl_number" | "diffusion_stability"
15405            | "lax_friedrichs_flux" | "lax_wendroff_flux"
15406            | "van_leer_limiter" | "minmod_limiter" | "superbee_limiter" | "mc_limiter"
15407            // ── batch 35: cryptanalysis & number theory deep ──────────────
15408            | "pollard_p_minus_1" | "fermat_factor"
15409            | "trial_smallest_factor" | "bsgs_discrete_log"
15410            | "mertens" | "liouville"
15411            | "is_b_smooth" | "primorial_n"
15412            | "pseudoprime_base2" | "strong_pseudoprime"
15413            | "aks_witness_count" | "qs_relation"
15414            | "index_calculus_naive" | "lll_2x2_step" | "coppersmith_bound"
15415            | "shor_period_prob" | "rsa_d_from_e" | "dh_secret"
15416            | "elgamal_encrypt" | "ecc_point_double" | "continued_fraction_sqrt"
15417            | "pell_fundamental" | "sum_two_squares" | "class_number_bound"
15418            | "smith_normal_2x2_step" | "regulator_naive"
15419            | "power_residue_check" | "wieferich_check" | "wilson_test"
15420            | "goldbach_pair" | "english_likeness" | "xor_break_singlebyte"
15421            | "bit_reverse_64"
15422            | "gf256_multiply" | "hash_combine"
15423            // ── batch 36: econometrics ────────────────────────────────────
15424            | "arch_lm_test" | "breusch_pagan_test" | "white_robust_se"
15425            | "newey_west_se" | "hansen_j_test" | "gmm_moment_condition"
15426            | "hausman_test" | "breusch_godfrey_test" | "box_pierce_test"
15427            | "adf_test_stat" | "pp_test_stat" | "kpss_test_stat"
15428            | "dickey_fuller_critical" | "engle_granger_step"
15429            | "johansen_trace_step" | "vecm_alpha_beta"
15430            | "panel_within_estimator" | "panel_between_estimator"
15431            | "panel_random_effects" | "arellano_bond_step"
15432            | "ols_estimator" | "ols_residual_variance" | "ols_r_squared"
15433            | "ols_adjusted_r2" | "akaike_info_crit" | "bayesian_info_crit"
15434            | "hannan_quinn_ic" | "f_statistic_pooled" | "breusch_pagan_lm"
15435            | "ramsey_reset_test" | "chow_test_stat" | "white_test_stat"
15436            | "goldfeld_quandt" | "wald_test_stat" | "score_test_stat"
15437            | "likelihood_ratio_test" | "two_sls_iv" | "iv_estimator"
15438            | "mle_normal_log_lik" | "mle_exponential_log_lik"
15439            | "mle_poisson_log_lik" | "gmm_moment_function"
15440            | "pooling_test_stat" | "heteroskedasticity_test"
15441            | "robust_se_huber_white" | "bootstrap_se_estimate"
15442            | "heckman_correction" | "tobit_log_likelihood"
15443            | "probit_log_likelihood" | "logit_log_likelihood"
15444            | "multinomial_logit_prob" | "ordered_probit_threshold"
15445            | "panel_var_step" | "impulse_response_step"
15446            | "variance_decomposition" | "granger_causality_chi2"
15447            | "cointegration_residual" | "error_correction_step"
15448            | "random_walk_innovation" | "random_walk_drift_step"
15449            | "ar_model_likelihood" | "ma_model_likelihood"
15450            | "arma_model_innovation"
15451            // ── batch 37: algebraic topology, knot theory, lie algebras ───
15452            | "euler_char_complex" | "betti_zero" | "betti_one" | "betti_two"
15453            | "genus_surface" | "chern_first_2d" | "genus_curve_arith"
15454            | "genus_curve_geo" | "hodge_diamond_value" | "poincare_duality"
15455            | "fundamental_group_zn" | "homology_rank" | "cohomology_rank"
15456            | "homotopy_group_sphere_pi" | "mapping_class_torus"
15457            | "linking_number_two" | "writhe_polygon" | "torsion_coefficient"
15458            | "simplex_volume_n" | "simplicial_volume" | "nerve_complex_count"
15459            | "cech_zero_cohomology" | "de_rham_zero"
15460            | "poincare_polynomial_eval" | "chromatic_homology_rank"
15461            | "khovanov_q_grading" | "hochschild_zero" | "cyclic_homology_step"
15462            | "group_cohomology_dim" | "group_homology_dim"
15463            | "abelianization_quotient" | "free_group_rank_lower"
15464            | "nilpotency_class_lower" | "solvable_length_upper"
15465            | "schreier_index" | "todd_genus_eval" | "hirzebruch_signature"
15466            | "chern_simons_action" | "gauss_bonnet_total"
15467            | "seifert_genus_lower" | "alexander_polynomial_at_one"
15468            | "jones_polynomial_at_minus_one" | "jones_polynomial_at_i"
15469            | "homfly_evaluation" | "kauffman_bracket_eval"
15470            | "cabling_pair_signature" | "seifert_form_2x2"
15471            | "turaev_alexander_step" | "v_polynomial_eval"
15472            | "polynomial_jones_skein" | "delta_complex_count"
15473            | "poset_zeta_two" | "mobius_poset_two" | "mobius_function_pair"
15474            | "mobius_inversion_step" | "incidence_algebra_dim"
15475            | "quiver_path_count" | "representation_dim_step"
15476            | "weyl_group_order" | "root_system_count"
15477            | "cartan_determinant_a2" | "cartan_matrix_b2"
15478            | "killing_form_su2" | "casimir_eigenvalue_su2"
15479            | "universal_enveloping_dim" | "verma_character_step"
15480            | "plethystic_substitution_value" | "schur_polynomial_eval"
15481            | "hall_inner_product_two" | "plactic_class_size"
15482            | "robinson_schensted_pair" | "yamanouchi_word_count"
15483            | "rsk_size" | "character_su2" | "character_sun"
15484            | "quantum_dimension_su2" | "quantum_dimension_q"
15485            | "fusion_rule_su2_step" | "modular_data_s_value"
15486            | "modular_data_t_value" | "verlinde_count_step"
15487            | "quantum_invariant_eval" | "operad_count_two"
15488            | "moduli_dimension_curves" | "hodge_polynomial_eval"
15489            | "mirror_symmetry_check" | "gromov_witten_invariant"
15490            | "donaldson_invariant" | "seiberg_witten_value"
15491            | "floer_homology_rank" | "khovanov_rasmussen_s"
15492            | "ozsvath_szabo_tau" | "heegaard_genus_lower"
15493            | "fintushel_stern_step" | "bauer_furuta_step"
15494            | "geometric_intersection_number"
15495            | "algebraic_intersection_number"
15496            // ── batch 38: electrochemistry, batteries, fuel cells ─────────
15497            | "nernst_potential_full" | "electrode_potential_step"
15498            | "exchange_current_density" | "butler_volmer_current"
15499            | "tafel_anodic_current" | "tafel_cathodic_current"
15500            | "mass_transport_overpotential" | "limiting_current_density"
15501            | "diffusion_layer_thickness" | "faradaic_efficiency"
15502            | "coulombic_efficiency_cell" | "energy_efficiency_cell"
15503            | "voltaic_efficiency" | "charge_capacity_battery"
15504            | "energy_density_battery" | "power_density_battery"
15505            | "specific_capacity_active" | "columbic_capacity_lihalfcell"
15506            | "ragone_point" | "peukert_capacity" | "peukert_exponent_fit"
15507            | "shepherd_voltage_step" | "nernst_planck_flux"
15508            | "debye_length_electrolyte" | "debye_huckel_activity"
15509            | "gouy_chapman_potential" | "stern_layer_capacitance"
15510            | "double_layer_capacitance" | "helmholtz_capacitance"
15511            | "zeta_potential_estimate" | "electroosmotic_velocity"
15512            | "hagen_poiseuille_eo" | "diffuse_layer_thickness"
15513            | "poisson_boltzmann_step" | "linearized_pb_step"
15514            | "electrochem_impedance_z" | "randles_circuit_z"
15515            | "warburg_impedance" | "cole_cole_eis" | "nyquist_phase"
15516            | "charge_transfer_resistance" | "solution_resistance_estimate"
15517            | "ionic_conductivity_arrhenius" | "nernst_einstein_diffusivity"
15518            | "walden_product" | "kohlrausch_law"
15519            | "onsager_relation_two_species" | "trasatti_voltammetry_charge"
15520            | "randles_sevcik_peak" | "levich_current_rde"
15521            | "koutecky_levich_intercept" | "mott_schottky_capacitance"
15522            | "flat_band_potential" | "schottky_barrier_height"
15523            | "photocurrent_density" | "quantum_efficiency_photo"
15524            | "overall_efficiency_pec" | "fuel_cell_polarization"
15525            | "electrolyzer_voltage" | "faraday_efficiency_h2"
15526            | "overpotential_oer" | "overpotential_her"
15527            | "electrocrystallization_step" | "nucleation_rate_constant"
15528            | "metal_corrosion_rate" | "pourbaix_line_value"
15529            | "mixed_potential_step" | "electrochemiluminescence_yield"
15530            | "solid_electrolyte_capacity" | "ionic_liquid_viscosity_step"
15531            | "lithium_ion_diffusivity" | "soc_estimate_coulomb"
15532            | "soh_capacity_fade" | "ocv_lithium_ion_step"
15533            | "state_of_charge_kalman" | "thermal_runaway_threshold"
15534            | "joule_heating_battery" | "calorimetric_heat_battery"
15535            | "abuse_test_voltage" | "swelling_strain_step"
15536            | "sei_resistance_growth" | "binder_content_optimal"
15537            | "porosity_active_layer" | "tortuosity_estimate_bruggeman"
15538            | "electrolyte_decomposition_temp" | "gibbs_thomson_undercooling"
15539            | "nernst_diffusion_layer" | "diff_coeff_aqueous_estimate"
15540            | "salt_activity_coefficient" | "mean_activity_coeff_pitzer"
15541            | "osmotic_coefficient_pitzer" | "debye_huckel_screening_factor"
15542            | "ph_at_isoelectric" | "buffer_capacity_acid_base"
15543            | "henderson_hasselbalch_solve" | "titration_endpoint_index"
15544            // ── batch 39: tensor calculus, GR, differential geometry ──────
15545            | "tensor_contract_two" | "tensor_outer_two" | "tensor_trace_index"
15546            | "tensor_symmetrize_two" | "tensor_antisymmetrize_two"
15547            | "levi_civita_three" | "levi_civita_four"
15548            | "kronecker_three" | "kronecker_four"
15549            | "metric_minkowski_eta_step" | "metric_schwarzschild_step"
15550            | "metric_kerr_step_simple" | "metric_frw_lapse"
15551            | "christoffel_first_kind_step" | "christoffel_second_kind_step"
15552            | "riemann_tensor_step_zero" | "riemann_curvature_normal_form"
15553            | "ricci_tensor_step_zero" | "scalar_curvature_step"
15554            | "einstein_tensor_step" | "weyl_tensor_step_zero"
15555            | "schouten_tensor_step" | "geodesic_equation_step_zero"
15556            | "parallel_transport_step" | "covariant_derivative_step"
15557            | "christoffel_symbol_normalize" | "ricci_identity_step"
15558            | "bianchi_first_identity_check" | "bianchi_second_identity_check"
15559            | "killing_vector_lie_step" | "lie_derivative_scalar_step"
15560            | "lie_derivative_vector_step" | "exterior_derivative_one_form"
15561            | "hodge_star_one_form" | "codifferential_step"
15562            | "laplace_de_rham_step" | "volume_form_riemannian"
15563            | "hodge_inner_product_one" | "sectional_curvature_two_plane"
15564            | "gauss_codazzi_step" | "mainardi_codazzi_step"
15565            | "weingarten_map_step" | "shape_operator_eig"
15566            | "mean_curvature_step" | "gaussian_curvature_step"
15567            | "extrinsic_principal_curv" | "intrinsic_principal_curv"
15568            | "geodesic_curvature_step" | "darboux_frame_step"
15569            | "fermi_normal_step" | "synge_world_function"
15570            | "raychaudhuri_step" | "expansion_scalar_step"
15571            | "shear_tensor_step" | "twist_tensor_step"
15572            | "optical_scalars_step" | "peeling_step_psi4"
15573            | "ads_metric_step" | "de_sitter_metric_step"
15574            | "warped_product_step_zero" | "kaluza_klein_step"
15575            | "brans_dicke_step" | "horndeski_step"
15576            | "einstein_dilaton_step" | "gauss_bonnet_term_2d"
15577            | "chern_pontryagin_4d_step" | "adm_mass_step"
15578            | "komar_mass_step" | "bondi_mass_step"
15579            | "brown_york_quasilocal" | "isolated_horizon_charge"
15580            | "trapped_surface_check" | "apparent_horizon_step"
15581            | "event_horizon_check" | "cosmological_constant_term"
15582            | "de_sitter_radius_step" | "anti_de_sitter_radius_step"
15583            | "penrose_diagram_factor" | "conformal_compactification_step"
15584            | "schwarzschild_kruskal_step" | "gullstrand_painleve_step"
15585            | "kerr_newman_charge_term" | "boyer_lindquist_step"
15586            | "hartle_thorne_metric" | "oppenheimer_volkoff_step"
15587            | "post_newtonian_step" | "shapiro_delay_step"
15588            | "mercury_perihelion_advance"
15589            | "gravitational_wave_quadrupole"
15590            | "plus_polarization_amp" | "cross_polarization_amp"
15591            | "chirp_mass_inspiral_step" | "isco_radius_kerr_step"
15592            | "spin_orbit_coupling_term" | "spin_spin_coupling_term"
15593            | "hawking_area_increase" | "unruh_temperature_full"
15594            | "bekenstein_entropy_step" | "holographic_entanglement_step"
15595            | "ryu_takayanagi_step" | "swampland_distance_check"
15596            // ── batch 40: information theory, coding, signal processing ──
15597            | "conditional_entropy_step" | "joint_entropy_step"
15598            | "relative_entropy_kl" | "mutual_information_step"
15599            | "chain_rule_entropy" | "fano_inequality_bound"
15600            | "data_processing_inequality" | "arithmetic_coding_interval"
15601            | "range_coding_step" | "golomb_rice_code"
15602            | "elias_gamma_code" | "elias_delta_code" | "exp_golomb_code"
15603            | "fibonacci_code" | "shannon_fano_elias_code"
15604            | "huffman_balanced_step" | "arithmetic_decode_interval"
15605            | "range_decode_step" | "universal_code_length"
15606            | "ziv_lempel_estimate" | "lz77_match_length"
15607            | "lz78_dictionary_growth" | "lzw_step_dict"
15608            | "ppm_predict_prob" | "deflate_huffman_lit"
15609            | "brotli_distance_code_count" | "zstd_window_size_log"
15610            | "mpeg_quant_value" | "jpeg_zig_zag_index"
15611            | "jpeg_dct_8x8_quant" | "hadamard_walsh_transform_step"
15612            | "karhunen_loeve_step" | "discrete_haar_step"
15613            | "db4_wavelet_step" | "biorthogonal_step"
15614            | "beylkin_wavelet_step" | "coiflet_wavelet_step"
15615            | "mallat_pyramid_step" | "threshold_soft_value"
15616            | "threshold_hard_value" | "median_filter_window"
15617            | "mean_filter_window" | "gaussian_filter_window"
15618            | "unsharp_mask_step" | "sobel_kernel_value"
15619            | "prewitt_kernel_value" | "roberts_kernel_value"
15620            | "laplacian_kernel_value" | "canny_threshold_step"
15621            | "hough_accumulator_step" | "ransac_iteration_count"
15622            | "optical_flow_lk_step" | "horn_schunck_step"
15623            | "kalman_predict_state" | "kalman_update_state"
15624            | "particle_filter_resample" | "unscented_sigma_point"
15625            | "ekf_jacobian_step" | "markov_decision_value"
15626            | "bellman_equation_step" | "q_learning_update"
15627            | "policy_iteration_step" | "value_iteration_step"
15628            | "sarsa_update" | "double_q_learning_step"
15629            | "ucb1_action_value" | "thompson_sample_beta"
15630            | "boltzmann_softmax_action" | "explore_exploit_epsilon"
15631            | "montecarlo_returns_step" | "td_zero_update"
15632            | "td_lambda_update" | "gradient_temporal_diff"
15633            | "deep_q_target" | "ddpg_critic_loss_step"
15634            | "ppo_clip_term" | "trpo_kl_constraint"
15635            | "a3c_advantage_step" | "ppo_advantage_step"
15636            | "gae_advantage_step" | "generalized_advantage"
15637            | "information_bottleneck_step" | "free_energy_principle"
15638            | "fisher_info_metric" | "kullback_jensen_div"
15639            | "hellinger_distance_step" | "total_variation_distance"
15640            | "bhattacharyya_coefficient" | "wasserstein_dist_emp"
15641            | "chisquare_metric" | "hellinger_kernel"
15642            | "jensen_shannon_div" | "renyi_divergence_step"
15643            | "amari_alpha_div" | "csiszar_phi_div"
15644            | "sinkhorn_iteration_step" | "sliced_wasserstein"
15645            | "gromov_wasserstein_step" | "spectral_signature_match"
15646            | "mfcc_coeff_step" | "chroma_feature_step"
15647            // ── batch 41: combinatorial optimization, scheduling ──────────
15648            | "tsp_lower_bound_mst" | "tsp_held_karp_step"
15649            | "christofides_ratio_bound" | "two_opt_swap_delta"
15650            | "or_opt_delta" | "three_opt_delta" | "lin_kernighan_step"
15651            | "nearest_neighbor_tour_step" | "greedy_edge_tour"
15652            | "nearest_insertion_step" | "farthest_insertion_step"
15653            | "cheapest_insertion_step" | "max_flow_ford_fulkerson_step"
15654            | "edmonds_karp_step" | "dinic_blocking_flow"
15655            | "push_relabel_step" | "boykov_kolmogorov_step"
15656            | "mincut_stoer_wagner" | "gomory_hu_step"
15657            | "karger_contract_edge" | "karger_min_cut_count"
15658            | "maximum_bipartite_matching" | "hopcroft_karp_phase"
15659            | "blossom_match_step" | "weighted_match_kuhn_step"
15660            | "hungarian_method_step" | "ap_jonker_volgenant_step"
15661            | "assignment_lower_bound" | "job_shop_makespan_lower"
15662            | "flow_shop_johnson_step" | "parallel_machine_lpt"
15663            | "parallel_machine_spt" | "list_scheduling_step"
15664            | "graham_2approx_bound" | "chc_bound_makespan"
15665            | "bin_packing_first_fit" | "bin_packing_best_fit"
15666            | "bin_packing_next_fit" | "bin_packing_lower_bound_l1"
15667            | "multidim_packing_step" | "knapsack_01_dp_value"
15668            | "knapsack_unbounded_dp" | "knapsack_fractional_step"
15669            | "knapsack_branch_bound" | "knapsack_lp_relaxation"
15670            | "multi_knapsack_step" | "quadratic_assignment_step"
15671            | "qap_lower_bound" | "graph_coloring_dsatur_step"
15672            | "graph_coloring_welsh_powell"
15673            | "graph_coloring_brooks_bound" | "graph_coloring_lp_bound"
15674            | "fractional_chromatic_lower" | "list_coloring_step"
15675            | "edge_coloring_vizing_step" | "clique_number_lower"
15676            | "independence_number_upper" | "vertex_cover_lp_round"
15677            | "dominating_set_greedy_step" | "dominating_set_lp_bound"
15678            | "set_cover_greedy_step" | "set_cover_lp_round"
15679            | "hitting_set_greedy" | "weighted_set_cover_step"
15680            | "matroid_greedy_step" | "matroid_intersection_step"
15681            | "submodular_greedy_step" | "submodular_curvature_bound"
15682            | "nemhauser_wolsey_bound" | "lp_relax_round"
15683            | "branch_and_bound_step" | "cutting_plane_step"
15684            | "gomory_cut_step" | "chvatal_gomory_cut"
15685            | "mixed_integer_round_up" | "mixed_integer_round_down"
15686            | "sos_constraint_check" | "column_generation_step"
15687            | "benders_decomposition_step" | "dantzig_wolfe_step"
15688            | "lagrangian_relax_step" | "lagrangian_dual_step"
15689            | "subgradient_step_size" | "nonlinear_dual_step"
15690            | "augmented_lagrangian_step" | "admm_primal_step"
15691            | "admm_dual_step" | "proximal_gradient_step"
15692            | "nesterov_accelerate_step" | "fista_step" | "ista_step"
15693            | "mirror_descent_step" | "frank_wolfe_step"
15694            | "conditional_gradient_step" | "greedy_set_cover_round"
15695            | "local_search_swap_step" | "tabu_search_move_score"
15696            | "simulated_annealing_step" | "genetic_crossover_one_point"
15697            | "mutation_bit_flip_prob" | "roulette_wheel_select_index"
15698            // ── batch 42: climate, fluids, atmospheric ────────────────────
15699            | "stefan_boltzmann_radiation" | "emissivity_grey_body"
15700            | "albedo_blackbody_balance" | "solar_constant_at_distance"
15701            | "total_solar_irradiance_step" | "absorbed_short_wave"
15702            | "emitted_long_wave" | "clausius_clapeyron_full"
15703            | "relative_humidity_step" | "dewpoint_temperature_full"
15704            | "wet_bulb_potential" | "virtual_temperature_full"
15705            | "density_altitude_full" | "geopotential_height_full"
15706            | "geometric_height_full" | "adiabatic_lapse_rate_dry"
15707            | "adiabatic_lapse_rate_moist" | "brunt_vaisala_full"
15708            | "richardson_number_step" | "gradient_richardson_full"
15709            | "flux_richardson_full" | "turbulent_kinetic_energy_step"
15710            | "mixing_length_prandtl" | "monin_obukhov_length"
15711            | "similarity_function_phi" | "log_law_wind_profile"
15712            | "power_law_wind_profile" | "ekman_layer_depth"
15713            | "ekman_pumping_step" | "geostrophic_wind_step"
15714            | "gradient_wind_step" | "thermal_wind_step"
15715            | "quasi_geostrophic_omega" | "omega_equation_step"
15716            | "potential_temperature_step" | "equivalent_potential_temp"
15717            | "saturation_equivalent_pt" | "ipv_potential_vorticity"
15718            | "ertel_pv_step" | "absolute_vorticity_step"
15719            | "relative_vorticity_step" | "divergence_omega_step"
15720            | "streamfunction_step" | "velocity_potential_step"
15721            | "helmholtz_decomp_step" | "courant_friedrichs_lewy"
15722            | "peclet_number_step" | "prandtl_number_step"
15723            | "reynolds_full_number" | "schmidt_number_step"
15724            | "sherwood_number_step" | "nusselt_full_number"
15725            | "grashof_number_step" | "rayleigh_number_step"
15726            | "weber_number_step" | "froude_number_step"
15727            | "strouhal_full" | "mach_full_step"
15728            | "biot_number_step" | "fourier_number_step"
15729            | "turbulence_intensity_step" | "hurst_exponent_estimate"
15730            | "detrended_fluct_alpha" | "power_spectrum_slope"
15731            | "spectral_kappa_minus53" | "batchelor_scale_step"
15732            | "kolmogorov_microscale" | "taylor_microscale_step"
15733            | "integral_length_scale" | "turbulent_dissipation_eps"
15734            | "isotropic_relation_check" | "sst_anomaly_step"
15735            | "enso_index_step" | "amo_index_step" | "nao_index_step"
15736            | "soi_oscillation_index" | "pdo_index_step" | "mjo_phase_step"
15737            | "walker_circulation_step" | "hadley_cell_max_lat"
15738            | "ferrel_cell_step" | "itcz_position_lat" | "trade_wind_speed"
15739            | "westerlies_jet_speed" | "polar_vortex_radius"
15740            | "arctic_oscillation_step" | "indian_monsoon_index"
15741            | "african_monsoon_index" | "qbo_oscillation_step"
15742            | "solar_cycle_phase" | "sunspot_relative_number"
15743            | "geomagnetic_kp_index" | "ozone_dobson_total"
15744            | "chlorine_radical_decay" | "montreal_protocol_track"
15745            | "co2_growth_rate_step" | "methane_growth_rate"
15746            | "aerosol_optical_depth" | "ice_age_milankovitch"
15747            | "greenhouse_forcing_step"
15748            // ── batch 43: game theory, mechanism design, social choice ────
15749            | "game_two_player_value" | "nash_equilibrium_pair"
15750            | "mixed_strategy_value" | "zero_sum_minmax"
15751            | "saddle_point_check" | "correlated_equilibrium_value"
15752            | "shapley_value_two_step" | "banzhaf_index_two"
15753            | "nucleolus_lp_step" | "core_membership_check"
15754            | "imputation_efficient_check" | "imputation_individual_rational"
15755            | "prisoners_dilemma_payoff" | "matching_pennies_payoff"
15756            | "chicken_game_payoff" | "stag_hunt_payoff"
15757            | "battle_sexes_payoff" | "public_goods_game_payoff"
15758            | "tragedy_commons_metric" | "ultimatum_acceptance_prob"
15759            | "dictator_game_share" | "trust_game_repayment"
15760            | "cooperative_game_value" | "characteristic_function"
15761            | "bargaining_set_check" | "kalai_smorodinsky_step"
15762            | "nash_bargaining_solution" | "egalitarian_solution"
15763            | "utilitarian_solution" | "social_welfare_sum"
15764            | "arrow_impossibility_check" | "gibbard_satterthwaite_check"
15765            | "borda_count_step" | "condorcet_winner_check"
15766            | "plurality_winner_step" | "kemeny_score_step"
15767            | "dodgson_swap_count" | "coombs_runoff_step"
15768            | "single_transferable_vote" | "range_voting_score"
15769            | "approval_voting_max" | "schulze_method_step"
15770            | "copeland_score_step" | "black_method_winner"
15771            | "median_voter_step" | "hotelling_location_step"
15772            | "arrow_pareto_check" | "fair_division_envy_free"
15773            | "proportional_share" | "maximin_share"
15774            | "egalitarian_split" | "nash_social_welfare"
15775            | "divisible_goods_proportional" | "indivisible_envy_free_check"
15776            | "adjusted_winner_pct" | "sealed_bid_first_price"
15777            | "sealed_bid_second_price" | "english_auction_step"
15778            | "dutch_auction_step" | "all_pay_auction_step"
15779            | "vcg_payment_step" | "revenue_equivalence_check"
15780            | "truthful_mechanism_check" | "incentive_compatibility_check"
15781            | "mechanism_design_obj" | "double_auction_step"
15782            | "combinatorial_auction_step" | "posted_price_offer_accept"
15783            | "matching_market_step" | "deferred_acceptance_step"
15784            | "boston_mechanism_step" | "top_trading_cycles_step"
15785            | "school_choice_match" | "roommate_match_step"
15786            | "network_formation_step" | "coordination_game_payoff"
15787            | "evolutionary_stable_strategy" | "replicator_dynamics_step"
15788            | "hawk_dove_payoff" | "fictitious_play_step"
15789            | "best_response_dynamic" | "quantal_response_logit"
15790            | "level_k_step" | "cognitive_hierarchy_step"
15791            | "sequential_eq_check" | "subgame_perfect_eq"
15792            | "stackelberg_step" | "cournot_quantity_step"
15793            | "bertrand_price_step" | "hotelling_price_step"
15794            | "collusion_payoff_step" | "folk_theorem_value"
15795            | "repeated_game_avg_payoff" | "discount_factor_step"
15796            | "trigger_strategy_payoff" | "grim_trigger_step"
15797            | "tit_for_tat_step" | "prisoners_repeated_eq"
15798            | "mertens_zamir_step" | "ex_post_value_check"
15799            | "ex_ante_value_check" | "common_knowledge_iterations"
15800            // ── batch 44: symbolic CAS, decompositions, projections ───────
15801            | "cas_simplify_term" | "cas_expand_two_terms"
15802            | "cas_factor_quadratic" | "cas_partial_fraction_simple"
15803            | "cas_polynomial_gcd_step" | "cas_polynomial_div_step"
15804            | "cas_lagrange_interpolate" | "cas_chebyshev_eval"
15805            | "cas_legendre_eval" | "cas_hermite_eval"
15806            | "cas_laguerre_eval" | "cas_jacobi_eval"
15807            | "cas_gegenbauer_eval" | "cas_taylor_coefficient"
15808            | "cas_padé_diagonal" | "cas_continued_fraction_step"
15809            | "cas_resultant_two" | "cas_subresultant_two"
15810            | "cas_groebner_lt_step" | "cas_buchberger_step"
15811            | "cas_macaulay_matrix_step" | "cas_modular_inverse"
15812            | "cas_extended_euclid_step" | "cas_smith_normal_step"
15813            | "cas_hermite_normal_step" | "cas_radical_simplify"
15814            | "cas_minimal_polynomial" | "cas_gcd_polynomial_step"
15815            | "cas_resultant_x_y" | "cas_solve_linear"
15816            | "cas_solve_quadratic" | "cas_solve_cubic"
15817            | "cas_solve_quartic" | "cas_solve_polynomial_n"
15818            | "cas_root_isolate_step" | "cas_sturm_sequence_step"
15819            | "cas_descartes_rule_count" | "cas_companion_matrix_root"
15820            | "cas_polynomial_roots_kahan"
15821            | "cas_eigenvalue_inverse_iteration" | "cas_qr_iteration_step"
15822            | "cas_jacobi_eigen_step" | "cas_lanczos_iteration_step"
15823            | "cas_arnoldi_iteration_step" | "cas_givens_rotation_apply"
15824            | "cas_householder_reflection" | "cas_modified_gram_schmidt"
15825            | "cas_classical_gram_schmidt" | "cas_rank_revealing_qr"
15826            | "cas_pivoted_lu_step" | "cas_block_lu_step"
15827            | "cas_cholesky_step" | "cas_modified_cholesky"
15828            | "cas_ldlt_step" | "cas_bunch_kaufman_step"
15829            | "cas_woodbury_identity" | "cas_matrix_pencil_step"
15830            | "cas_generalized_eigen" | "cas_singular_value_step"
15831            | "cas_truncated_svd_value" | "cas_pseudoinverse_step"
15832            | "cas_polar_decomposition" | "cas_schur_decomposition_step"
15833            | "cas_quasi_triangular" | "cas_riccati_continuous_step"
15834            | "cas_riccati_discrete_step" | "cas_lyapunov_continuous_step"
15835            | "cas_lyapunov_discrete_step" | "cas_sylvester_equation_step"
15836            | "cas_kronecker_product_step" | "cas_vec_operator_step"
15837            | "cas_matrix_function_step" | "cas_matrix_log_step"
15838            | "cas_matrix_exp_pade" | "cas_matrix_sqrt_step"
15839            | "cas_drazin_inverse_step" | "cas_moore_penrose_step"
15840            | "cas_least_squares_solve" | "cas_total_least_squares"
15841            | "cas_constrained_ls_step" | "cas_truncated_lsq"
15842            | "cas_regularized_lsq_tikhonov" | "cas_basis_pursuit_step"
15843            | "cas_lasso_soft_threshold" | "cas_elastic_net_step"
15844            | "cas_omp_step" | "cas_iht_iteration"
15845            | "cas_cosamp_step" | "cas_admm_lasso_step"
15846            | "cas_proximal_l1_step" | "cas_proximal_l2_step"
15847            | "cas_proximal_l_inf_step" | "cas_indicator_simplex_proj"
15848            | "cas_proj_l1_ball" | "cas_proj_l2_ball"
15849            | "cas_proj_box" | "cas_proj_psd_cone"
15850            | "cas_proj_soc_step" | "cas_proj_exp_cone"
15851            | "cas_dykstra_step" | "cas_alternating_projection"
15852            | "cas_polya_enumeration_step" | "cas_burnside_count_step"
15853            // ── batch 45: ML primitives — activations, losses, optimizers ─
15854            | "ml_relu_step" | "ml_leaky_relu_step" | "ml_elu_step"
15855            | "ml_selu_step" | "ml_gelu_step" | "ml_swish_step"
15856            | "ml_mish_step" | "ml_softplus_step" | "ml_softsign_step"
15857            | "ml_hard_sigmoid" | "ml_hard_tanh" | "ml_prelu_step"
15858            | "ml_celu_step" | "ml_silu_step" | "ml_logsumexp_step"
15859            | "ml_log_softmax_step" | "ml_log_sigmoid"
15860            | "ml_glu_step" | "ml_geglu_step" | "ml_swiglu_step"
15861            | "ml_attention_score_step" | "ml_scaled_dot_product"
15862            | "ml_multihead_avg" | "ml_softmax_temperature"
15863            | "ml_dropout_mask_prob" | "ml_layer_norm_step"
15864            | "ml_batch_norm_step" | "ml_group_norm_step"
15865            | "ml_rms_norm_step" | "ml_instance_norm_step"
15866            | "ml_weight_norm_step" | "ml_spectral_norm_step"
15867            | "ml_l2_normalize_step" | "ml_huber_loss_step"
15868            | "ml_smooth_l1_loss" | "ml_focal_loss_step"
15869            | "ml_dice_loss_step" | "ml_iou_loss_step"
15870            | "ml_giou_loss_step" | "ml_diou_loss_step"
15871            | "ml_ciou_loss_step" | "ml_contrastive_loss"
15872            | "ml_triplet_loss_step" | "ml_arcface_loss_step"
15873            | "ml_center_loss_step" | "ml_kl_divergence_loss"
15874            | "ml_cross_entropy_loss" | "ml_binary_cross_entropy"
15875            | "ml_label_smoothing" | "ml_mixup_lambda"
15876            | "ml_cutmix_box_iou" | "ml_random_erasing_step"
15877            | "ml_cosine_lr_schedule" | "ml_warmup_lr_step"
15878            | "ml_step_lr_schedule" | "ml_exponential_lr"
15879            | "ml_polynomial_lr" | "ml_one_cycle_lr"
15880            | "ml_inverse_sqrt_lr" | "ml_cyclic_lr_step"
15881            | "ml_sgd_step" | "ml_momentum_step"
15882            | "ml_nesterov_momentum" | "ml_adagrad_step"
15883            | "ml_rmsprop_step" | "ml_adam_step"
15884            | "ml_adamw_step" | "ml_adamax_step"
15885            | "ml_nadam_step" | "ml_radam_step"
15886            | "ml_lookahead_step" | "ml_lamb_step"
15887            | "ml_lars_step" | "ml_yogi_step"
15888            | "ml_amsgrad_step" | "ml_adabelief_step"
15889            | "ml_shampoo_step" | "ml_lion_step"
15890            | "ml_sophia_step" | "ml_gradient_clip_norm"
15891            | "ml_gradient_clip_value" | "ml_gradient_accumulate"
15892            | "ml_gradient_centralize" | "ml_weight_decay_step"
15893            | "ml_he_init_value" | "ml_xavier_init_value"
15894            | "ml_glorot_init_value" | "ml_orthogonal_init"
15895            | "ml_truncnormal_init" | "ml_kaiming_init"
15896            | "ml_lecun_init_value" | "ml_zero_init"
15897            | "ml_constant_init" | "ml_uniform_init"
15898            | "ml_one_hot_index" | "ml_label_to_id"
15899            | "ml_id_to_label_step" | "ml_token_logit_top_k"
15900            | "ml_topk_argmax" | "ml_nucleus_sample_p"
15901            | "ml_temperature_decay" | "ml_repetition_penalty"
15902            | "ml_eos_logit_boost"
15903            // ── batch 46: NLP — ranking, similarity, language models ──────
15904            | "nlp_bm25_score" | "nlp_tf_idf_step" | "nlp_okapi_score"
15905            | "nlp_word_freq_value" | "nlp_doc_freq_step"
15906            | "nlp_inverse_doc_freq" | "nlp_cosine_similarity_two"
15907            | "nlp_jaccard_similarity_two" | "nlp_overlap_coefficient"
15908            | "nlp_dice_coefficient_two" | "nlp_simpson_coefficient"
15909            | "nlp_levenshtein_dist" | "nlp_damerau_levenshtein"
15910            | "nlp_jaro_distance" | "nlp_jaro_winkler"
15911            | "nlp_hamming_distance" | "nlp_lcs_length" | "nlp_lcs_ratio"
15912            | "nlp_meteor_score" | "nlp_bleu_score_n"
15913            | "nlp_rouge_score_n" | "nlp_chrf_score" | "nlp_ter_score"
15914            | "nlp_wer_score" | "nlp_cer_score" | "nlp_perplexity_value"
15915            | "nlp_bits_per_character" | "nlp_char_ngram_count"
15916            | "nlp_word_ngram_count" | "nlp_skip_gram_count"
15917            | "nlp_byte_pair_merge_step" | "nlp_wordpiece_score"
15918            | "nlp_unigram_lm_score" | "nlp_kneser_ney_step"
15919            | "nlp_witten_bell_step" | "nlp_good_turing_count"
15920            | "nlp_laplace_smoothing" | "nlp_lidstone_smoothing"
15921            | "nlp_jelinek_mercer" | "nlp_dirichlet_smoothing"
15922            | "nlp_query_likelihood_step" | "nlp_kl_lm_div"
15923            | "nlp_pmi_score" | "nlp_npmi_score"
15924            | "nlp_chi2_collocation" | "nlp_loglikelihood_collocation"
15925            | "nlp_t_score_collocation" | "nlp_dunning_log_likelihood"
15926            | "nlp_lda_alpha_step" | "nlp_lda_beta_step"
15927            | "nlp_lda_topic_dist" | "nlp_plsa_step"
15928            | "nlp_word2vec_skipgram_loss" | "nlp_word2vec_cbow_loss"
15929            | "nlp_glove_loss_step" | "nlp_fasttext_subword_count"
15930            | "nlp_byte_level_bpe_step" | "nlp_sentencepiece_score"
15931            | "nlp_unigram_subword_loss" | "nlp_subword_regularization"
15932            | "nlp_pointwise_attn_score" | "nlp_relative_position_bias"
15933            | "nlp_alibi_position_bias" | "nlp_rope_rotary_angle"
15934            | "nlp_rope_apply_step" | "nlp_position_encoding_sin"
15935            | "nlp_position_encoding_cos" | "nlp_pe_freq_band"
15936            | "nlp_max_seq_len_check" | "nlp_token_drop_rate"
15937            | "nlp_byte_frequency" | "nlp_char_frequency"
15938            | "nlp_punct_ratio" | "nlp_uppercase_ratio"
15939            | "nlp_digit_ratio" | "nlp_emoji_ratio"
15940            | "nlp_url_count" | "nlp_email_count" | "nlp_phone_count"
15941            | "nlp_hashtag_count" | "nlp_mention_count"
15942            | "nlp_token_overlap_two" | "nlp_word_mover_dist"
15943            | "nlp_sif_weight_step" | "nlp_doc_embedding_avg"
15944            | "nlp_attention_pool_step" | "nlp_max_pool_step"
15945            | "nlp_avg_pool_step" | "nlp_sum_pool_step"
15946            | "nlp_self_attn_compute_step" | "nlp_cross_attn_compute_step"
15947            | "nlp_window_attn_step" | "nlp_strided_attn_step"
15948            | "nlp_block_attn_step" | "nlp_sliding_window_step"
15949            | "nlp_local_attn_step" | "nlp_dilated_attn_step"
15950            | "nlp_global_attn_step" | "nlp_sparse_attn_score"
15951            | "nlp_linformer_step" | "nlp_performer_step"
15952            | "nlp_reformer_step" | "nlp_longformer_step"
15953            | "nlp_bigbird_step" | "nlp_routing_attn_step"
15954            // ── batch 47: graphics, geometry, ray tracing, BRDF, color ────
15955            | "gfx_perspective_proj_x" | "gfx_perspective_proj_y"
15956            | "gfx_orthographic_proj" | "gfx_view_matrix_step"
15957            | "gfx_lookat_forward" | "gfx_lookat_right" | "gfx_lookat_up"
15958            | "gfx_quat_to_axis_angle" | "gfx_axis_angle_to_quat"
15959            | "gfx_quat_slerp_step" | "gfx_quat_nlerp_step"
15960            | "gfx_quat_dot_two" | "gfx_quat_inverse_step"
15961            | "gfx_quat_to_euler_pitch" | "gfx_quat_to_euler_yaw"
15962            | "gfx_quat_to_euler_roll" | "gfx_euler_to_quat_x"
15963            | "gfx_euler_to_quat_y" | "gfx_euler_to_quat_z"
15964            | "gfx_euler_to_quat_w" | "gfx_rotation_matrix_xx"
15965            | "gfx_rotation_matrix_yy" | "gfx_rotation_matrix_zz"
15966            | "gfx_translation_matrix_step" | "gfx_scale_matrix_step"
15967            | "gfx_shear_matrix_xy" | "gfx_homogeneous_divide"
15968            | "gfx_screen_space_x" | "gfx_screen_space_y"
15969            | "gfx_ndc_to_screen_x" | "gfx_ndc_to_screen_y"
15970            | "gfx_screen_to_ndc_x" | "gfx_screen_to_ndc_y"
15971            | "gfx_clip_polygon_step" | "gfx_sutherland_hodgman"
15972            | "gfx_cohen_sutherland_code" | "gfx_liang_barsky_t"
15973            | "gfx_bresenham_step_x" | "gfx_bresenham_step_y"
15974            | "gfx_xiaolin_wu_intensity" | "gfx_aabb_intersect_check"
15975            | "gfx_obb_overlap_step" | "gfx_sphere_intersect_t"
15976            | "gfx_ray_triangle_t" | "gfx_ray_plane_t" | "gfx_ray_box_t"
15977            | "gfx_ray_sphere_t" | "gfx_ray_disk_t"
15978            | "gfx_ray_cylinder_t" | "gfx_ray_cone_t"
15979            | "gfx_ray_ellipsoid_t" | "gfx_ray_torus_t_approx"
15980            | "gfx_barycentric_alpha" | "gfx_barycentric_beta"
15981            | "gfx_barycentric_gamma" | "gfx_phong_diffuse_step"
15982            | "gfx_phong_specular_step" | "gfx_phong_ambient_step"
15983            | "gfx_blinn_specular_step" | "gfx_lambert_term"
15984            | "gfx_oren_nayar_term" | "gfx_cook_torrance_d_ggx"
15985            | "gfx_cook_torrance_g_smith" | "gfx_cook_torrance_f_schlick"
15986            | "gfx_disney_principled_d" | "gfx_microfacet_brdf_step"
15987            | "gfx_subsurface_scattering_term" | "gfx_translucent_falloff"
15988            | "gfx_normal_distribution_ggx"
15989            | "gfx_geometric_attenuation_smith"
15990            | "gfx_fresnel_dielectric_step" | "gfx_fresnel_conductor_step"
15991            | "gfx_index_of_refraction" | "gfx_snells_law_angle"
15992            | "gfx_total_internal_reflection" | "gfx_refract_direction_x"
15993            | "gfx_reflect_direction_x" | "gfx_environment_map_uv_u"
15994            | "gfx_environment_map_uv_v" | "gfx_cube_map_face_index"
15995            | "gfx_octahedral_encode_x" | "gfx_octahedral_encode_y"
15996            | "gfx_spherical_harmonic_y00" | "gfx_spherical_harmonic_y10"
15997            | "gfx_spherical_harmonic_y11" | "gfx_spherical_harmonic_y20"
15998            | "gfx_zonal_harmonic_step" | "gfx_irradiance_sh_eval"
15999            | "gfx_radiance_sh_eval" | "gfx_skybox_uv_u" | "gfx_skybox_uv_v"
16000            | "gfx_tonemap_reinhard" | "gfx_tonemap_aces"
16001            | "gfx_tonemap_uncharted2" | "gfx_tonemap_filmic"
16002            | "gfx_gamma_correct_step" | "gfx_srgb_to_linear"
16003            | "gfx_linear_to_srgb" | "gfx_dither_bayer_4x4"
16004            | "gfx_dither_floyd_steinberg" | "gfx_oklab_l_step"
16005            | "gfx_oklab_a_step" | "gfx_oklab_b_step"
16006            | "gfx_oklch_chroma" | "gfx_oklch_hue"
16007            | "gfx_pcg_hash_step" | "gfx_xorshift_step"
16008            | "gfx_halton_step" | "gfx_sobol_step"
16009            | "gfx_van_der_corput" | "gfx_low_discrepancy_step"
16010            | "gfx_blue_noise_value" | "gfx_perlin_noise_step"
16011            | "gfx_simplex_noise_step" | "gfx_fbm_noise_step"
16012            | "gfx_worley_noise_step" | "gfx_voronoi_distance"
16013            | "gfx_curl_noise_step" | "gfx_gradient_noise_step"
16014            | "gfx_value_noise_step" | "gfx_signed_distance_box"
16015            | "gfx_signed_distance_sphere" | "gfx_signed_distance_capsule"
16016            // ── batch 48: database internals, distributed systems ─────────
16017            | "db_b_tree_split" | "db_b_tree_merge"
16018            | "db_lsm_compaction_step" | "db_skiplist_height_pick"
16019            | "db_bloom_filter_bit_index" | "db_cuckoo_filter_fingerprint"
16020            | "db_quotient_filter_canonical" | "db_count_min_sketch_bin"
16021            | "db_hyperloglog_register_max" | "db_min_hash_value"
16022            | "db_simhash_bit" | "db_consistent_hash_index"
16023            | "db_rendezvous_hash_score" | "db_jump_hash_bucket"
16024            | "db_maglev_hash_step" | "db_lru_cache_eviction_age"
16025            | "db_lfu_cache_decay" | "db_arc_cache_score"
16026            | "db_clock_cache_hand" | "db_tinylfu_admit_score"
16027            | "db_w_tinylfu_freq" | "db_buffer_pool_score"
16028            | "db_query_plan_cost_step" | "db_join_selectivity_step"
16029            | "db_index_seek_cost" | "db_seq_scan_cost"
16030            | "db_index_scan_cost" | "db_sort_cost_estimate"
16031            | "db_hash_join_cost" | "db_merge_join_cost"
16032            | "db_nested_loop_cost" | "db_query_cardinality"
16033            | "db_histogram_bucket_index" | "db_quantile_estimate_p99"
16034            | "db_t_digest_centroid" | "db_kll_quantile_step"
16035            | "db_dd_sketch_bin" | "db_reservoir_sample_index"
16036            | "db_chao_estimator_step" | "db_jaccard_minhash_estimate"
16037            | "db_distinct_estimate_lpc" | "db_distinct_estimate_hll"
16038            | "db_throttle_token_step" | "db_leaky_bucket_step"
16039            | "db_token_bucket_step" | "db_circuit_breaker_step"
16040            | "db_two_phase_commit_step" | "db_three_phase_commit_step"
16041            | "db_paxos_propose_id" | "db_raft_term_advance"
16042            | "db_raft_log_match_check" | "db_zab_epoch_step"
16043            | "db_chubby_lease_step" | "db_logical_clock_step"
16044            | "db_lamport_timestamp" | "db_vector_clock_merge"
16045            | "db_hybrid_logical_clock" | "db_crdt_g_counter_merge"
16046            | "db_crdt_pn_counter_merge" | "db_crdt_lww_register_merge"
16047            | "db_crdt_set_or_merge" | "db_consensus_quorum_size"
16048            | "db_replication_lag_step" | "db_partitions_for_n"
16049            | "db_consistent_lookup_id" | "db_chord_finger_index"
16050            | "db_kademlia_xor_distance" | "db_pastry_routing_step"
16051            | "db_dht_replicate_factor" | "db_partition_failure_check"
16052            | "db_byzantine_quorum_size" | "db_pbft_view_change"
16053            | "db_honey_badger_step" | "db_avalanche_query_step"
16054            | "db_quorum_intersection_check" | "db_anti_entropy_step"
16055            | "db_merkle_node_hash" | "db_merkle_path_verify"
16056            | "db_gossip_fanout_step" | "db_anti_entropy_pull_step"
16057            | "db_split_brain_check" | "db_clock_skew_estimate"
16058            | "db_freshness_score" | "db_read_repair_step"
16059            | "db_hinted_handoff_step" | "db_compaction_score"
16060            | "db_levelled_compaction_step" | "db_size_tiered_compaction"
16061            | "db_universal_compaction_step" | "db_write_amplification"
16062            | "db_read_amplification" | "db_space_amplification"
16063            | "db_block_cache_hit_rate" | "db_page_cache_eviction_age"
16064            | "db_wal_fsync_cost" | "db_group_commit_count"
16065            | "db_replica_lag_threshold" | "db_synchronous_commit_check"
16066            | "db_async_commit_check" | "db_eventual_consistency_check"
16067            | "db_strong_consistency_check" | "db_linearizability_check"
16068            | "db_causal_consistency_check"
16069            // ── batch 49: networking — TCP, AQM, MIMO, queueing ───────────
16070            | "net_tcp_cwnd_step" | "net_tcp_ssthresh_update"
16071            | "net_tcp_reno_step" | "net_tcp_cubic_step"
16072            | "net_tcp_bbr_step" | "net_tcp_vegas_step"
16073            | "net_tcp_westwood_step" | "net_tcp_compound_step"
16074            | "net_tcp_dctcp_step" | "net_tcp_yeah_step"
16075            | "net_tcp_htcp_step" | "net_tcp_hybla_step"
16076            | "net_tcp_illinois_step" | "net_tcp_lp_step"
16077            | "net_tcp_scalable_step" | "net_tcp_veno_step"
16078            | "net_aiad_step" | "net_aimd_step"
16079            | "net_miad_step" | "net_mimd_step"
16080            | "net_aqm_red_drop_prob" | "net_aqm_codel_target"
16081            | "net_aqm_pie_drop_rate" | "net_aqm_fq_codel_step"
16082            | "net_aqm_blue_step" | "net_aqm_choke_step"
16083            | "net_aqm_sfq_step" | "net_aqm_drr_step"
16084            | "net_aqm_wrr_step" | "net_token_rate_limit"
16085            | "net_traffic_shaper_step" | "net_priority_queue_index"
16086            | "net_packet_loss_estimate" | "net_jitter_estimate"
16087            | "net_latency_avg" | "net_rtt_smoothed"
16088            | "net_rtt_variation" | "net_rto_compute"
16089            | "net_bandwidth_delay_product" | "net_path_capacity_kleinrock"
16090            | "net_loss_rate_to_throughput" | "net_throughput_padhye"
16091            | "net_throughput_mathis" | "net_throughput_response"
16092            | "net_router_buffer_size" | "net_drop_tail_check"
16093            | "net_burst_size_compute" | "net_packet_pacing_step"
16094            | "net_link_capacity_share" | "net_proportional_fair_share"
16095            | "net_max_min_fair_step" | "net_alpha_fair_step"
16096            | "net_kelly_pricing_step" | "net_network_utility_max"
16097            | "net_lyapunov_drift_plus_penalty" | "net_backpressure_step"
16098            | "net_max_weight_match" | "net_qcsma_propose"
16099            | "net_csma_back_off" | "net_alohanet_throughput"
16100            | "net_slotted_aloha_throughput" | "net_csma_efficiency"
16101            | "net_token_ring_efficiency" | "net_polling_efficiency"
16102            | "net_radio_path_loss" | "net_friis_received_power"
16103            | "net_two_ray_ground_loss" | "net_okumura_hata_loss"
16104            | "net_log_distance_path" | "net_shadowing_normal"
16105            | "net_rician_k_factor" | "net_rayleigh_envelope"
16106            | "net_doppler_shift" | "net_capacity_shannon"
16107            | "net_mimo_capacity_step" | "net_zero_forcing_beam"
16108            | "net_mmse_beam_step" | "net_water_filling_power"
16109            | "net_amc_threshold_index" | "net_harq_combining_gain"
16110            | "net_turbo_decode_iter" | "net_ldpc_iteration_step"
16111            | "net_polar_decode_step" | "net_viterbi_step"
16112            | "net_bcjr_step" | "net_outage_probability"
16113            | "net_diversity_gain" | "net_array_gain"
16114            | "net_multiplexing_gain" | "net_coding_gain"
16115            | "net_pruning_gain" | "net_macro_diversity_step"
16116            | "net_micro_diversity_step" | "net_handoff_threshold"
16117            | "net_call_admission_check" | "net_blocking_probability"
16118            | "net_erlang_b_formula" | "net_erlang_c_formula"
16119            | "net_engset_formula" | "net_little_law_l"
16120            | "net_throughput_law" | "net_response_time_law"
16121            | "net_utilization_law" | "net_forced_flow_law"
16122            // ── batch 50: OS internals — schedulers, I/O, memory ──────────
16123            | "os_priority_aging_step" | "os_mlfq_demote_step"
16124            | "os_mlfq_promote_step" | "os_round_robin_quantum"
16125            | "os_completely_fair_vruntime" | "os_lottery_ticket_count"
16126            | "os_stride_pass_step" | "os_eevdf_eligible"
16127            | "os_cfs_load_balance_step" | "os_eas_energy_estimate"
16128            | "os_smt_threading_share" | "os_numa_node_distance"
16129            | "os_cpu_affinity_score" | "os_thread_migration_cost"
16130            | "os_load_average_decay" | "os_runqueue_depth"
16131            | "os_io_scheduler_deadline" | "os_io_scheduler_cfq_step"
16132            | "os_io_scheduler_noop_step" | "os_io_scheduler_bfq_step"
16133            | "os_io_scheduler_kyber_step" | "os_io_scheduler_mq_deadline"
16134            | "os_anticipation_window" | "os_elevator_step"
16135            | "os_disk_seek_time" | "os_disk_rotational_lat"
16136            | "os_disk_transfer_time" | "os_pre_fetch_window"
16137            | "os_buffer_cache_pages" | "os_dirty_page_threshold"
16138            | "os_writeback_step" | "os_swappiness_factor"
16139            | "os_kswapd_wake_threshold" | "os_oom_score_step"
16140            | "os_page_replacement_lru" | "os_page_replacement_clock"
16141            | "os_page_replacement_2q" | "os_working_set_size"
16142            | "os_thrashing_threshold" | "os_demand_paging_step"
16143            | "os_copy_on_write_check" | "os_zero_page_optimization"
16144            | "os_huge_page_threshold" | "os_transparent_hugepage"
16145            | "os_kasan_shadow_offset" | "os_kfence_check"
16146            | "os_kfence_alloc_index" | "os_slub_object_size_round"
16147            | "os_slab_color_offset" | "os_per_cpu_cache_size"
16148            | "os_buddy_order_pick" | "os_compact_memory_step"
16149            | "os_kvm_vmcs_field_offset" | "os_apic_irq_priority"
16150            | "os_msi_x_vector_count" | "os_iommu_domain_step"
16151            | "os_pci_bus_address" | "os_acpi_state_transition"
16152            | "os_cpufreq_governor_step" | "os_intel_pstate_target"
16153            | "os_amd_pstate_target" | "os_thermal_zone_trip"
16154            | "os_throttle_temperature" | "os_battery_capacity_pct"
16155            | "os_powertop_score" | "os_idle_state_select"
16156            | "os_c_state_residency" | "os_p_state_voltage"
16157            | "os_dvfs_step" | "os_voltage_scaling_step"
16158            | "os_frequency_scaling_step" | "os_inotify_event_count"
16159            | "os_epoll_ctl_count" | "os_io_uring_sqe_count"
16160            | "os_io_uring_cqe_count" | "os_kqueue_event_count"
16161            | "os_systemd_journal_size" | "os_dmesg_severity_level"
16162            | "os_audit_event_priority" | "os_apparmor_profile_active"
16163            | "os_selinux_context_match" | "os_smack_label_compare"
16164            | "os_capability_check" | "os_seccomp_filter_step"
16165            | "os_namespace_isolation" | "os_cgroup_v1_count"
16166            | "os_cgroup_v2_count" | "os_pid_max_value"
16167            | "os_thread_max_value" | "os_file_max_value"
16168            | "os_open_files_count" | "os_socket_max_value"
16169            | "os_inotify_max_watches" | "os_oom_kill_score"
16170            | "os_zswap_compress_ratio" | "os_zram_compress_ratio"
16171            | "os_swap_pressure_score" | "os_pressure_stall_step"
16172            | "os_psi_avg10_step" | "os_psi_avg60_step"
16173            | "os_psi_avg300_step" | "os_load_proc_avg"
16174            | "os_load_user_avg" | "os_load_iowait_avg"
16175            // ── batch 51: security — KDFs, MFA, PKI, web sec, TLS ─────────
16176            | "sec_argon2_memcost" | "sec_argon2_timecost"
16177            | "sec_argon2_parallelism" | "sec_argon2_block_step"
16178            | "sec_pbkdf2_iter" | "sec_scrypt_n_param"
16179            | "sec_scrypt_r_param" | "sec_scrypt_p_param"
16180            | "sec_balloon_hash_step" | "sec_yescrypt_step"
16181            | "sec_bcrypt_cost_factor" | "sec_bcrypt_round_step"
16182            | "sec_password_strength_zxcvbn" | "sec_haveibeenpwned_check"
16183            | "sec_diceware_word_index" | "sec_xkcd_passphrase_score"
16184            | "sec_passphrase_entropy" | "sec_chosen_charset_strength"
16185            | "sec_keystroke_timing_var" | "sec_2fa_totp_window"
16186            | "sec_totp_drift_check" | "sec_hotp_counter_step"
16187            | "sec_yubikey_otp_check" | "sec_webauthn_attestation_check"
16188            | "sec_fido2_assertion_check" | "sec_certificate_chain_depth"
16189            | "sec_revocation_ocsp_check" | "sec_crl_age_seconds"
16190            | "sec_pki_path_validate" | "sec_x509_subject_match"
16191            | "sec_san_match_count" | "sec_basic_constraints_ca"
16192            | "sec_pinning_compare" | "sec_certificate_transparency"
16193            | "sec_dane_tlsa_match" | "sec_hpkp_pin_match"
16194            | "sec_csp_directive_match" | "sec_csrf_token_match"
16195            | "sec_cors_origin_match" | "sec_xss_filter_score"
16196            | "sec_html_escape_check" | "sec_url_safe_encode_check"
16197            | "sec_path_traversal_detect" | "sec_sqli_pattern_score"
16198            | "sec_xxe_pattern_score" | "sec_xxe_dtd_check"
16199            | "sec_command_injection_score" | "sec_idor_check"
16200            | "sec_jwt_alg_safe" | "sec_jwt_kid_match"
16201            | "sec_jwt_signature_verify" | "sec_oauth2_state_validate"
16202            | "sec_oauth2_pkce_step" | "sec_oauth_nonce_check"
16203            | "sec_session_lifetime" | "sec_idle_timeout_step"
16204            | "sec_login_throttle_step" | "sec_account_lockout_step"
16205            | "sec_password_history_check" | "sec_complexity_policy_score"
16206            | "sec_dictionary_attack_check" | "sec_brute_force_attempts"
16207            | "sec_credential_stuffing_score" | "sec_kerberos_ticket_age"
16208            | "sec_kerberos_pac_check" | "sec_kerberos_pre_auth"
16209            | "sec_ldap_bind_step" | "sec_radius_auth_step"
16210            | "sec_diameter_avp_step" | "sec_saml_assertion_age"
16211            | "sec_oidc_id_token_age" | "sec_acme_dns_challenge"
16212            | "sec_dnssec_signature_check" | "sec_spf_pass_check"
16213            | "sec_dkim_signature_check" | "sec_dmarc_policy_check"
16214            | "sec_arc_chain_step" | "sec_smtp_ssl_check"
16215            | "sec_imap_starttls_check" | "sec_pop3_security_step"
16216            | "sec_tls_alert_severity" | "sec_tls13_handshake_step"
16217            | "sec_tls12_handshake_step" | "sec_tls11_deprecation_check"
16218            | "sec_ssl3_disabled_check" | "sec_cipher_suite_strength"
16219            | "sec_cbc_mac_block_count" | "sec_gcm_iv_unique_check"
16220            | "sec_chachapoly_nonce_check" | "sec_x25519_clamping_step"
16221            | "sec_ed25519_signature_step" | "sec_ed448_signature_step"
16222            | "sec_p384_curve_step" | "sec_secp256k1_step"
16223            | "sec_blake3_chunk_step" | "sec_keccak_round_step"
16224            | "sec_sha3_padding_step" | "sec_argon2_state_advance"
16225            | "sec_chacha20_quarterround" | "sec_aes_round_step"
16226            | "sec_aes_keyschedule_step" | "sec_des_round_step"
16227            | "sec_blowfish_round_step" | "sec_serpent_round_step"
16228            | "sec_twofish_round_step"
16229            // ── batch 52: calendrical algorithms ──────────────────────────
16230            | "fixed_from_gregorian" | "gregorian_from_fixed"
16231            | "fixed_from_julian" | "julian_from_fixed"
16232            | "iso_week_date" | "hebrew_leap_year"
16233            | "hebrew_year_length" | "fixed_from_hebrew"
16234            | "islamic_leap_year" | "fixed_from_islamic"
16235            | "persian_arithmetic_leap" | "fixed_from_persian"
16236            | "coptic_from_fixed" | "ethiopic_from_fixed"
16237            | "french_revolutionary_leap" | "fixed_from_french"
16238            | "chinese_year_zodiac" | "chinese_lunation_winter"
16239            | "hindu_solar_year" | "hindu_lunisolar_month"
16240            | "maya_long_count_from_fixed" | "mayan_haab_from_fixed"
16241            | "mayan_tzolkin_from_fixed" | "badi_year_from_fixed"
16242            | "bahai_from_fixed" | "easter_gregorian_year"
16243            | "easter_orthodox_year" | "easter_julian_year"
16244            | "day_of_week_zeller" | "iso_day_number"
16245            | "weekday_name_short" | "leap_year_gregorian"
16246
16247            // ── batch 53: R / SciPy distributions and tests ───────────────
16248            | "dnorm" | "dt" | "df_dist" | "dchisq"
16249            | "glm" | "aov" | "shapiro_wilk" | "anderson_darling"
16250            | "kolmogorov_smirnov" | "spearmanr" | "kendalltau" | "pearsonr"
16251            | "mannwhitneyu" | "wilcoxon" | "kruskal_h"
16252
16253            // ── batch 54: APL/J/K array primitives ────────────────────────
16254            | "iota_n" | "reduce_axis" | "scan_axis" | "fold_axis"
16255            | "rotate_axis" | "transpose_axis" | "reshape_dim"
16256            | "encode_base" | "decode_base" | "nub_list" | "nub_count"
16257            | "membership_idx" | "deal_n_k" | "roll_n"
16258            | "permute_idx" | "invert_perm"
16259
16260            // ── batch 55: astronomy / astrometry ──────────────────────────
16261            | "julian_day" | "jd_to_calendar" | "tt_to_tdb"
16262            | "ra_dec_to_alt_az" | "alt_az_to_ra_dec"
16263            | "precession_iau2006" | "nutation_iau2000a"
16264            | "aberration_annual" | "proper_motion_apply"
16265            | "parallax_correction" | "sun_position_low" | "sun_distance_au"
16266            | "moon_position_low" | "moon_phase_age" | "lunation_index"
16267            | "eclipse_magnitude" | "saros_cycle" | "metonic_cycle"
16268            | "orbit_kepler3" | "orbital_period_au" | "orbit_eccentric_anomaly"
16269            | "escape_velocity_body" | "hill_sphere_radius" | "tisserand_param"
16270            | "tle_mean_motion" | "sgp4_propagate_step" | "airy_disk_radius"
16271            | "rayleigh_criterion" | "strehl_ratio" | "au_to_km"
16272
16273            // ── batch 56: sports analytics — ratings & sabermetric ────────
16274            | "elo_expected" | "elo_update" | "glicko_rating"
16275            | "trueskill_update" | "trueskill_match_quality"
16276            | "pythagorean_expectation" | "war_above_replacement"
16277            | "woba_weight" | "wrc_plus" | "ops_plus" | "era_plus"
16278            | "fip" | "xfip" | "siera" | "babip" | "wpa"
16279            | "win_probability" | "leverage_index" | "clutch_score"
16280            | "shooting_pct" | "save_pct" | "corsi_for" | "fenwick_for"
16281            | "goals_above_avg" | "tackle_efficiency" | "yards_per_attempt"
16282            | "qbr_metric" | "epa_per_play"
16283
16284            // ── batch 57: Excel/Sheets + bond/loan financial ──────────────
16285            | "vlookup" | "hlookup" | "xlookup" | "index_match"
16286            | "indirect" | "choose" | "offset"
16287            | "sumif" | "countif" | "averageif"
16288            | "sumifs" | "countifs" | "averageifs"
16289            | "sumproduct" | "rank_eq" | "rank_avg" | "percentrank"
16290            | "quartile_inc" | "quartile_exc"
16291            | "xnpv" | "ppmt" | "ipmt" | "rate"
16292            | "macauley_duration" | "convexity" | "yield_to_maturity"
16293            | "accrued_interest" | "clean_price" | "dirty_price"
16294            | "coupon_count" | "skill_score" | "reliability_diagram"
16295            | "taylor_diagram_score"
16296
16297            // ── batch 58: GIS — geohash, H3, S2, UTM, projections ─────────
16298            | "geohash_neighbors" | "h3_index" | "h3_geo_to_h3"
16299            | "h3_h3_to_geo" | "h3_k_ring" | "h3_neighbor" | "h3_resolution"
16300            | "s2_cell_id" | "s2_cell_at_lat_lng" | "s2_cell_neighbors"
16301            | "utm_from_lat_lng" | "utm_to_lat_lng"
16302            | "mgrs_encode" | "mgrs_decode"
16303            | "lat_lng_to_xy_mercator" | "lat_lng_to_xy_lambert"
16304            | "haversine_dist" | "vincenty_dist" | "andoyer_dist"
16305            | "rhumb_line_bearing"
16306            | "destination_point" | "tile_xyz_to_lat_lng" | "lat_lng_to_tile_xyz"
16307            | "polygon_winding_order" | "point_in_polygon_ray"
16308            | "point_in_polygon_winding" | "segment_intersection"
16309            | "segment_distance_point" | "convex_hull_chan"
16310
16311            // ── batch 59: robotics & control ──────────────────────────────
16312            | "pid_anti_windup" | "pid_ziegler_nichols"
16313            | "smith_predictor_step" | "lqr_gain_continuous"
16314            | "lqr_gain_discrete" | "lqg_step" | "h_infinity_norm"
16315            | "bode_gain_margin" | "bode_phase_margin"
16316            | "nyquist_encirclement" | "nichols_chart_step"
16317            | "servo_position_velocity" | "servo_torque_step"
16318            | "imu_madgwick_step" | "imu_mahony_step" | "quaternion_from_imu"
16319            | "denavit_hartenberg_h" | "forward_kinematics_dh"
16320            | "inverse_kinematics_2link" | "jacobian_2dof"
16321            | "manipulability_yoshikawa" | "singularity_check_2link"
16322            | "path_dubins_lsl" | "path_dubins_rsr" | "path_reeds_shepp"
16323            | "rrt_extend" | "rrt_star_rewire" | "prm_node_connect"
16324
16325            // ── batch 60: actuarial science ───────────────────────────────
16326            | "life_expectancy_e0" | "force_of_mortality" | "select_ultimate"
16327            | "annuity_due_an" | "annuity_immediate_an"
16328            | "term_life_a_n_t" | "whole_life_a"
16329            | "endowment_pure_e" | "endowment_combined_a"
16330            | "premium_net" | "level_premium"
16331            | "reserve_prospective" | "reserve_retrospective"
16332            | "gross_premium_load" | "experience_factor"
16333            | "mortality_table_q" | "select_period_step"
16334            | "multi_decrement_q" | "multi_state_pij"
16335            | "credibility_buhlmann" | "loss_severity_lognormal"
16336            | "loss_frequency_poisson" | "ruin_probability_lundberg"
16337            | "cramer_lundberg_step" | "bornhuetter_ferguson"
16338            | "chain_ladder_step" | "ibnr_estimate" | "run_off_triangle_step"
16339
16340            // ── batch 61: epidemiology / public health ────────────────────
16341            | "r_naught_basic" | "r_effective_t" | "doubling_time_growth"
16342            | "sirs_step" | "seirs_step" | "susceptible_to_infected"
16343            | "attack_rate" | "vaccination_coverage_required"
16344            | "cfr_case_fatality" | "ifr_infection_fatality"
16345            | "dalys_disability_weight" | "qaly_lifetime" | "ylll_pml"
16346            | "rt_serial_interval" | "generation_time_step"
16347            | "gini_inequality_health" | "standardized_mortality_smr"
16348            | "indirect_age_adjusted" | "direct_age_adjusted"
16349            | "odds_ratio_2x2" | "risk_ratio_2x2" | "number_needed_to_treat"
16350            | "attributable_fraction_pop" | "preventive_fraction"
16351            | "contact_tracing_eff" | "cluster_attack_rate"
16352            | "transmission_pair_index"
16353
16354            // ── batch 62: archive/encoding format primitives ──────────────
16355            | "tar_header_checksum" | "tar_pad_512" | "tar_member_record"
16356            | "zip_local_header" | "zip_central_dir" | "zip_eocd"
16357            | "gzip_member_step" | "gzip_crc32_init" | "gzip_isize"
16358            | "deflate_dynamic_huffman" | "deflate_static_block"
16359            | "lz4_block_step" | "lz4_match_offset"
16360            | "zstd_frame_header" | "brotli_huffman_table"
16361            | "brotli_meta_block" | "lzma_range_step"
16362            | "quoted_printable_encode" | "uuencode_step"
16363            | "modhex_encode" | "percent_encode_full"
16364            | "punycode_encode" | "idn_to_ascii" | "idn_to_unicode"
16365            | "msgpack_pack_int" | "msgpack_pack_str"
16366            | "cbor_encode_uint" | "cbor_encode_str"
16367
16368            // ── batch 63: chemistry & biochemistry ────────────────────────
16369            | "molecular_weight_compound" | "molarity_dilution"
16370            | "gas_constant_value" | "eyring_rate" | "van_t_hoff_kp"
16371            | "henderson_buffer" | "titration_ph_endpoint"
16372            | "isoelectric_point_protein" | "ka_to_pka" | "pkb_to_kb"
16373            | "amphoteric_check" | "oxidation_number"
16374            | "half_reaction_balance" | "redox_potential_cell"
16375            | "electrolysis_mass" | "spectrophotometer_beer_lambert"
16376            | "epsilon_extinction" | "transmittance_to_a"
16377            | "crystal_field_ligand" | "jahn_teller_check"
16378            | "vsepr_geometry" | "lewis_dot_count"
16379            | "formal_charge" | "resonance_count"
16380            | "ramachandran_phi_psi" | "rg_radius_of_gyration"
16381            | "spectroscopic_factor" | "avogadro_constant"
16382
16383            // ── batch 64: music theory ────────────────────────────────────
16384            | "cents_between_freqs" | "note_name_from_midi"
16385            | "interval_quality_size" | "scale_pitches_major"
16386            | "scale_pitches_minor" | "mode_pitches_dorian"
16387            | "mode_pitches_phrygian" | "mode_pitches_lydian"
16388            | "chord_root_inversion" | "chord_quality_classify"
16389            | "chord_voicing_close" | "key_signature_sharps"
16390            | "key_signature_flats" | "tempo_to_ms" | "beat_to_seconds"
16391            | "time_sig_subdivision" | "equal_tempered_freq"
16392            | "just_intonation_freq" | "pythagorean_freq"
16393            | "mean_tone_freq" | "werckmeister_iii" | "kirnberger_iii"
16394            | "dynamics_db_level" | "harmonics_partial"
16395
16396            // ── batch 65: geology, seismology, mineralogy ─────────────────
16397            | "moment_magnitude_mw" | "richter_local_ml"
16398            | "surface_wave_ms" | "body_wave_mb"
16399            | "gutenberg_richter_b" | "omori_aftershock"
16400            | "pga_attenuation" | "arias_intensity" | "shake_map_pga"
16401            | "liquefaction_potential_index" | "spt_n_correction"
16402            | "mineral_mohs_hardness" | "streak_color_index"
16403            | "specific_gravity_water" | "feldspar_classify"
16404            | "silicate_classify" | "igneous_qapf"
16405            | "metamorphic_grade" | "crustal_density_depth"
16406            | "pwave_velocity_depth" | "swave_velocity_depth"
16407            | "gradient_geothermal" | "heat_flow_radiogenic"
16408
16409            // ── batch 66: BLAS / LAPACK ───────────────────────────────────
16410            | "dgemm" | "sgemm" | "zgemm" | "cgemm"
16411            | "dgemv" | "sgemv" | "dtrsm" | "strsm"
16412            | "dgesv" | "dgetrf" | "dgeqrf" | "dgesvd"
16413            | "dsyevd" | "dpotrf" | "daxpy" | "ddot"
16414            | "dnrm2" | "dscal" | "dasum" | "idamax"
16415            | "dsyrk" | "dgerqf" | "dorgqr" | "dorglq"
16416            | "drot" | "drotg" | "dpbsv" | "dgbsv"
16417            | "dtbsv" | "dtrsv" | "ddrot" | "dgemm3m"
16418            | "dgels" | "dgelsd"
16419
16420            // ── batch 67: logic, proof, SAT/SMT, type theory ──────────────
16421            | "cnf_unit_propagate" | "cnf_pure_literal_elim"
16422            | "cnf_dpll_branch" | "dpll_clause_learning"
16423            | "two_watched_literals" | "walksat_step"
16424            | "resolution_step" | "subsumption_check"
16425            | "tableau_branch_close" | "sequent_left_intro"
16426            | "sequent_right_intro" | "nbe_normalize"
16427            | "church_numeral_n" | "encode_pair" | "encode_succ"
16428            | "simply_typed_check" | "hindley_milner_step"
16429            | "unification_robinson" | "bdd_apply" | "bdd_restrict"
16430            | "bdd_quantify" | "aig_simplify_step"
16431            | "smt_qf_lia_solve_step" | "smt_qf_uf_combine"
16432            | "model_checking_ctl" | "model_checking_ltl"
16433            | "bisimulation_step" | "coq_tactic_apply"
16434            | "coq_unify_term" | "refl_check" | "sym_check" | "trans_check"
16435
16436            // ── batch 68: compilers / parsing ─────────────────────────────
16437            | "nfa_to_dfa" | "subset_construction"
16438            | "dfa_minimize_hopcroft" | "regex_to_nfa_thompson"
16439            | "glushkov_construction" | "brzozowski_derivative"
16440            | "ll1_first_set" | "ll1_follow_set" | "ll1_predict_table"
16441            | "lr0_items_step" | "lalr_lookahead_compute"
16442            | "lr1_canonical_collection"
16443            | "earley_scan" | "earley_predict" | "earley_complete"
16444            | "packrat_parse_step" | "ascent_parser_step"
16445            | "pratt_parse_step" | "shunting_yard_step"
16446            | "regex_compile_thompson" | "regex_match_dfa"
16447            | "lex_keyword_classify"
16448            | "peg_seq" | "peg_choice" | "peg_repeat" | "peg_lookahead"
16449            | "dfa_simulate_step" | "bytecode_disasm_step"
16450            | "ssa_phi_insert" | "dom_tree_idom" | "dominance_frontier"
16451
16452            // ── batch 69: computational linguistics ───────────────────────
16453            | "porter_stem_step" | "snowball_stem_english"
16454            | "snowball_stem_french" | "lemmatize_wordnet"
16455            | "lemmatize_lemmy" | "stem_lancaster"
16456            | "soundex_phonetic" | "metaphone_phonetic"
16457            | "caverphone_2" | "nysiis_phonetic"
16458            | "match_rating_codex" | "daitch_mokotoff"
16459            | "viterbi_pos_tag" | "forward_backward_pos"
16460            | "crf_log_likelihood" | "bigram_perplexity"
16461            | "trigram_perplexity" | "ner_bilou_decode"
16462            | "constituency_cyk" | "dependency_parse_eisner"
16463            | "transition_arc_eager" | "transition_arc_standard"
16464            | "word_alignment_ibm1" | "word_alignment_ibm2"
16465            | "lexicalized_parse" | "coreference_singleton"
16466            | "anaphora_distance" | "head_finding_collins"
16467            | "tree_kernel_collins"
16468
16469            // ── batch 70: Postgres SQL strings, JSON, aggregates ─────────
16470            | "btrim" | "translate" | "ascii"
16471            | "regexp_split" | "regexp_matches" | "regexp_replace"
16472            | "json_build_object" | "jsonb_set"
16473            | "json_array_length" | "json_extract_path"
16474            | "json_strip_nulls" | "jsonb_pretty"
16475            | "jsonb_path_query" | "json_each"
16476            | "jsonb_array_length" | "jsonb_object_keys"
16477            | "jsonb_typeof" | "array_to_jsonb"
16478            | "ts_match" | "ts_rank" | "ts_headline"
16479            | "substring_similarity" | "levenshtein_dist"
16480            | "word_similarity" | "strict_word_similarity"
16481            | "hstore_to_array" | "array_to_hstore"
16482            | "string_agg" | "array_agg"
16483            | "corr_agg" | "covar_pop" | "covar_samp"
16484            | "regr_slope" | "regr_intercept" | "regr_r2"
16485            | "percentile_cont" | "percentile_disc" | "mode_agg"
16486            | "array_to_string" | "array_position" | "array_positions"
16487            | "array_remove" | "array_replace"
16488            | "xmlforest" | "xmlagg"
16489
16490            // ── batch 71: Redis-flavour primitives ────────────────────────
16491            | "zadd" | "zrem" | "zrangebyscore"
16492            | "zrank" | "zrevrank" | "zincrby"
16493            | "zcard" | "zcount" | "zlexcount"
16494            | "lpush" | "rpush" | "lrange" | "lrem"
16495            | "hset" | "hget" | "hgetall" | "hlen"
16496            | "hkeys" | "hvals" | "hmset" | "hincrby"
16497            | "sadd" | "srem" | "smembers"
16498            | "sinter" | "sunion" | "sdiff"
16499            | "scard" | "sismember" | "spop"
16500            | "setex" | "setnx" | "expire"
16501            | "ttl" | "pttl" | "persist"
16502            | "incr" | "decr" | "incrby" | "decrby"
16503            | "getset" | "mset" | "mget" | "renamenx"
16504            | "dbsize" | "type_redis" | "exists_key"
16505            | "strlen" | "getrange" | "setrange" | "append_redis"
16506            | "bitcount" | "bitop" | "bitpos"
16507            | "pfadd" | "pfcount"
16508            | "geoadd" | "geodist" | "geohash"
16509            | "xadd" | "xlen" | "xrange"
16510            | "object_encoding" | "debug_object" | "cluster_slots"
16511
16512            // ── batch 72: NumPy + scipy.special ──────────────────────────
16513            | "argpartition" | "bincount" | "nonzero_count"
16514            | "flatnonzero" | "searchsorted" | "digitize"
16515            | "histogram_bin_edges" | "unique_count"
16516            | "polyfit_rmse"
16517            | "ellipk" | "ellipe"
16518            | "hyp1f1" | "hyp2f1" | "mathieu_b"
16519            | "spherical_jn" | "spherical_yn"
16520            | "jv" | "yn" | "iv" | "kv"
16521            | "airyai" | "airybi"
16522            | "polygamma" | "trigamma" | "loggamma"
16523            | "factorial2" | "factorialk"
16524            | "owens_t" | "marcum_q" | "voigt_profile"
16525            | "chebyt" | "chebyu" | "sph_harm"
16526            | "wofz" | "erfcx" | "erfi" | "dawsn"
16527            | "interp1d"
16528            | "convolve_full" | "convolve_valid" | "correlate_full"
16529            | "kron_product"
16530            | "simpson_rule" | "romberg_quad" | "fixed_quad"
16531            | "ode45_step" | "ode_lsoda" | "solve_ivp_step"
16532            | "root_brentq" | "root_newton" | "root_secant"
16533            | "fmin_powell" | "fmin_cobyla"
16534
16535            // ── batch 73: economics + game theory ─────────────────────────
16536            | "cobb_douglas" | "ces_production"
16537            | "leontief_input" | "leontief_output"
16538            | "slutsky_decompose"
16539            | "marshallian_demand" | "hicksian_demand"
16540            | "expenditure_function" | "indirect_utility"
16541            | "gale_shapley_step" | "deferred_acceptance"
16542            | "top_trading_cycle" | "vcg_payment" | "myerson_optimal"
16543            | "gini_market" | "hhi_concentration"
16544            | "cournot_eq" | "stackelberg_eq" | "bertrand_eq"
16545            | "monopoly_lerner"
16546            | "consumer_surplus" | "producer_surplus"
16547            | "deadweight_loss" | "tax_incidence"
16548            | "pareto_efficiency" | "edgeworth_box_alloc"
16549            | "social_welfare_utilitarian"
16550            | "social_welfare_rawls" | "social_welfare_nash"
16551            | "arrow_independence"
16552            | "vickrey_auction" | "first_price_seal"
16553            | "english_auction" | "dutch_auction"
16554            | "core_coalition" | "stable_matching_count"
16555            | "gale_optimal" | "pareto_dominance"
16556            | "lerner_index"
16557            | "price_elasticity" | "supply_elasticity"
16558            | "income_elasticity" | "engel_curve" | "cross_elasticity"
16559            | "diff_in_diff" | "did_estimator" | "rdd_estimate"
16560            // ── batch 74: SciPy.signal — DSP filters, windows, transforms ──
16561            | "hann_w" | "hamming_w" | "blackman_w" | "barthann_w"
16562            | "nuttall_w" | "flattop_w" | "parzen_window" | "tukey_w"
16563            | "taylor_window" | "dpss_window" | "kaiserord_step"
16564            | "butter_lp_re" | "butter_hp_mag"
16565            | "cheby1_lp" | "cheby2_lp" | "ellip_lp" | "bessel_lp"
16566            | "notch_filter"
16567            | "sosfilt_step" | "lfilter_zi_init" | "filtfilt_pad"
16568            | "freqz_eval" | "freqs_eval" | "group_delay_eval"
16569            | "impulse_response_n"
16570            | "tf2zpk_step" | "zpk2tf_step" | "tf2sos_step"
16571            | "zpk2sos_step" | "sos2tf_step"
16572            | "bilinear_xform" | "bilinear_zpk_xform"
16573            | "firwin_lowpass" | "firwin_highpass"
16574            | "firwin_bandpass" | "firwin_bandstop"
16575            | "firwin2_freq" | "remez_design"
16576            | "stft_step" | "istft_step"
16577            | "cwt_morlet" | "ricker_wavelet" | "mexican_hat_wavelet"
16578            | "coherence_xy" | "csd_xy" | "welch_psd_avg"
16579            | "periodogram_basic" | "lombscargle_freq"
16580            | "hilbert_signal" | "envelope_amplitude"
16581            | "deconvolve_step" | "fftconvolve_step" | "oaconvolve_step"
16582            | "upfirdn_step" | "resample_poly_step" | "decimate_step"
16583            | "savgol_coef" | "detrend_linear"
16584            | "wiener_filter" | "medfilt_1d" | "peak_widths_at"
16585            // ── batch 75: NetworkX graph algorithms ───────────────────────
16586            | "dijkstra_relax" | "bellman_ford_relax"
16587            | "floyd_warshall_step" | "johnson_reweight"
16588            | "astar_search" | "bidirectional_dijkstra"
16589            | "yen_k_shortest" | "ida_star"
16590            | "bfs_count" | "dfs_postorder_done" | "topo_kahn_step"
16591            | "tarjan_scc_step" | "kosaraju_step"
16592            | "kruskal_step" | "prim_step" | "boruvka_step"
16593            | "reverse_delete_step"
16594            | "ford_fulkerson_step" | "edmonds_karp_bfs"
16595            | "dinic_step" | "push_relabel_relabel"
16596            | "stoer_wagner_step" | "karger_step"
16597            | "pagerank_iter" | "hits_authority" | "hits_hub"
16598            | "personalized_pagerank"
16599            | "centrality_degree" | "centrality_closeness"
16600            | "centrality_betweenness" | "centrality_eigenvector"
16601            | "centrality_katz" | "harmonic_centrality" | "load_centrality"
16602            | "clustering_coefficient" | "triangles_count" | "transitivity"
16603            | "modularity_score" | "louvain_gain"
16604            | "label_propagation" | "girvan_newman"
16605            | "articulation_point" | "bridge_edge"
16606            | "edge_connectivity" | "vertex_connectivity"
16607            | "biconnected_components"
16608            | "gx_diameter" | "gx_radius" | "gx_eccentricity"
16609            | "warshall_step"
16610            | "tsp_held_karp" | "tsp_nn_step" | "tsp_christofides"
16611            | "graph_coloring_greedy" | "welsh_powell"
16612            | "vf2_consistent" | "subgraph_isomorphism"
16613            | "hungarian_step" | "hopcroft_karp_step"
16614            | "bron_kerbosch"
16615            | "min_vertex_cover" | "max_independent_set"
16616            | "dominating_set_greedy" | "hamiltonian_path"
16617            | "min_steiner_tree" | "k_shortest_spanning"
16618            | "random_walk_hitting" | "simrank"
16619            // ── batch 76: Pandas DataFrame ops ────────────────────────────
16620            | "df_groupby" | "df_aggregate" | "df_apply"
16621            | "df_transform" | "df_pivot" | "df_pivot_table"
16622            | "df_melt" | "df_stack" | "df_unstack"
16623            | "df_explode" | "df_get_dummies" | "df_crosstab"
16624            | "df_merge" | "df_join" | "df_concat"
16625            | "df_resample" | "df_rolling" | "df_expanding"
16626            | "df_ewm" | "df_shift" | "df_diff"
16627            | "df_pct_change" | "df_corr" | "df_cov"
16628            | "df_corrwith" | "df_describe" | "df_kurtosis"
16629            | "df_skew" | "df_sem" | "df_mad"
16630            | "df_dropna" | "df_fillna" | "df_interpolate"
16631            | "df_replace" | "df_isnull" | "df_notnull"
16632            | "df_sort_values" | "df_rank" | "df_quantile"
16633            | "df_value_counts" | "df_sample" | "df_nlargest"
16634            | "df_nsmallest" | "df_idxmax" | "df_idxmin"
16635            | "df_clip" | "df_round" | "df_to_datetime"
16636            | "df_to_timedelta" | "df_to_numeric" | "df_eval"
16637            | "df_query" | "df_filter" | "df_drop_duplicates"
16638            | "df_duplicated" | "df_set_index" | "df_reset_index"
16639            // ── batch 77: PIL/OpenCV image processing ─────────────────────
16640            | "image_resize" | "image_grayscale" | "image_threshold"
16641            | "image_blur_gaussian" | "image_blur_box" | "image_sharpen"
16642            | "image_edge_canny" | "image_edge_sobel" | "image_edge_laplacian"
16643            | "image_dilate" | "image_erode" | "image_morphology_open"
16644            | "image_morphology_close" | "image_histogram" | "image_equalize"
16645            | "image_clahe" | "image_contrast" | "image_brightness"
16646            | "image_gamma" | "image_invert" | "image_sepia"
16647            | "image_posterize" | "image_solarize" | "convolve_2d"
16648            | "filter_median" | "filter_bilateral" | "filter_nlmeans"
16649            | "gabor_filter" | "hog_features" | "harris_corners"
16650            | "shi_tomasi_corners" | "sift_keypoints" | "orb_keypoints"
16651            | "surf_keypoints" | "template_match" | "face_detect_haar"
16652            | "watershed_segment" | "slic_superpixels" | "felzenszwalb_segment"
16653            | "graph_cut_segment" | "hough_lines" | "hough_circles"
16654            | "ransac_homography" | "optical_flow_lk" | "optical_flow_farneback"
16655            | "corner_subpix" | "image_rotate" | "image_flip_h"
16656            | "image_flip_v" | "image_emboss" | "image_motion_blur"
16657            // ── batch 78: statsmodels ─
16658            | "arima_fit" | "arima_forecast" | "arma_order_select"
16659            | "sarimax_fit" | "garch_fit" | "ewma_smooth"
16660            | "holt_winters_additive" | "holt_winters_multiplicative" | "kalman_filter_step"
16661            | "kalman_smoother_step" | "var_fit" | "vecm_fit"
16662            | "johansen_test" | "phillips_perron" | "adfuller"
16663            | "kpss_test" | "breusch_godfrey" | "ljung_box_q"
16664            | "durbin_watson_d" | "granger_causality" | "cointegration_eg"
16665            | "seasonal_decompose" | "stl_decompose" | "acf_basis"
16666            | "pacf_basis" | "moving_average_filter" | "exp_smooth_simple"
16667            | "exp_smooth_double" | "markov_switching_ar" | "markov_switching_mr"
16668            | "arch_lm" | "state_space_kalman" | "ucm_unobserved_components"
16669            | "spectral_density_estimate" | "bayesian_step" | "pivoted_cholesky_var"
16670            // ── batch 79: sklearn ─
16671            | "sk_logistic_predict" | "sk_logistic_fit" | "sk_random_forest_fit"
16672            | "sk_gbt_fit" | "sk_xgb_fit" | "sk_lightgbm_fit"
16673            | "sk_svm_fit" | "sk_kmeans_fit" | "sk_dbscan_fit"
16674            | "sk_agglomerative_fit" | "sk_pca_fit" | "sk_tsne_fit"
16675            | "sk_umap_fit" | "sk_isolation_forest_fit" | "sk_lof_fit"
16676            | "sk_kfold_split" | "sk_stratified_kfold" | "sk_cross_val_score"
16677            | "sk_grid_search" | "sk_random_search" | "sk_bayes_search"
16678            | "sk_pipeline_fit" | "sk_standard_scaler" | "sk_min_max_scaler"
16679            | "sk_robust_scaler" | "sk_quantile_transform" | "sk_power_transform"
16680            | "sk_one_hot" | "sk_ordinal_encode" | "sk_label_encode"
16681            | "sk_tfidf" | "sk_count_vectorize" | "sk_silhouette"
16682            | "sk_calinski_harabasz" | "sk_davies_bouldin" | "sk_adjusted_rand"
16683            | "sk_mutual_info" | "sk_lda_topic" | "sk_nmf_topic"
16684            | "sk_word2vec_train" | "sk_doc2vec_train" | "sk_naive_bayes_predict"
16685            | "sk_knn_predict" | "sk_decision_tree_split"
16686            // ── batch 80: quantum ─
16687            | "qubit_x" | "qubit_y" | "qubit_z"
16688            | "qubit_h" | "qubit_s" | "qubit_t"
16689            | "qubit_rx" | "qubit_ry" | "qubit_rz"
16690            | "qubit_u3" | "qubit_u2" | "qubit_u1"
16691            | "qubit_phase" | "qubit_cnot" | "qubit_cz"
16692            | "qubit_swap" | "qubit_ccx" | "qubit_measure"
16693            | "qubit_reset" | "bell_state" | "ghz_state"
16694            | "w_state" | "qft" | "inverse_qft"
16695            | "grover_iter" | "shor_period" | "vqe_step"
16696            | "qaoa_step" | "qpe_iteration" | "pauli_string_expect"
16697            | "circuit_depth" | "circuit_width" | "gate_decompose"
16698            | "ancilla_alloc" | "bloch_sphere_x" | "bloch_sphere_z"
16699            | "density_matrix_purity_q" | "entanglement_entropy" | "quantum_teleportation"
16700            | "superdense_coding" | "noise_model_depolarize"
16701            // ── batch 81: b81-misc-utility ─
16702            | "mirr_excel" | "accrint" | "cumipmt"
16703            | "cumprinc" | "dollarde" | "dollarfr"
16704            | "received" | "yieldmat" | "yielddisc"
16705            | "duration_macaulay" | "mduration" | "odddyield"
16706            | "disc_excel" | "effect" | "nominal"
16707            | "intrate" | "price_disc" | "cityhash64"
16708            | "farmhash_64" | "metro_hash_64" | "spookyhash_128"
16709            | "t1ha" | "highway_hash" | "fnv0_32"
16710            | "lose_lose"
16711            | "oat_hash" | "lz4_encode_block" | "snappy_encode"
16712            | "zstd_encode_step" | "brotli_encode_meta" | "lzma_encode_step"
16713            | "bz2_encode_step" | "lzo_encode_step" | "deflate_encode_huffman"
16714            | "lzw_encode" | "gzip_encode_step" | "uri_template_expand"
16715            | "uri_resolve" | "uri_normalize" | "percent_decode_url"
16716            | "url_encode_form" | "url_decode_form" | "punycode_decode_step"
16717            | "idn_normalize" | "url_origin" | "etag_validate"
16718            | "cache_control_parse" | "vary_match" | "content_negotiate"
16719            | "accept_lang_pick" | "range_header_parse" | "if_match_check"
16720            | "if_none_match_check" | "digest_auth_quote" | "www_auth_parse"
16721            // ── batch 82: b82-misc-utility ─
16722            | "iso8601_duration_parse" | "iso8601_duration_to_seconds" | "rrule_next_occurrence"
16723            | "cron_next_fire" | "date_round_iso" | "week_number_iso"
16724            | "fiscal_year_us" | "age_at_date" | "easter_western"
16725            | "easter_orthodox_year_2" | "chinese_new_year" | "solstice_winter"
16726            | "equinox_spring" | "rgb_to_oklab" | "oklab_to_rgb"
16727            | "rgb_to_cmyk" | "cmyk_to_rgb" | "rgb_to_xyz"
16728            | "xyz_to_rgb" | "rgb_to_yuv" | "yuv_to_rgb"
16729            | "luminance_relative" | "contrast_ratio" | "wcag_pass"
16730            | "color_temperature_kelvin" | "delta_e76" | "delta_e94"
16731            | "delta_e2000" | "color_blend_alpha" | "isbn10_check"
16732            | "isbn13_check" | "ean13_check" | "upc_check"
16733            | "eth_addr_check" | "btc_addr_check" | "ssn_check"
16734            | "vin_check" | "imei_check" | "iban_check"
16735            | "cusip_check" | "kde_silverman_bw" | "kde_scott_bw"
16736            | "kde_bandwidth_lscv" | "kde_epanechnikov" | "kde_gaussian_2d"
16737            | "kde_uniform" | "kde_triangular" | "kde_biweight"
16738            | "kde_triweight" | "kde_cosine" | "kde_logistic_kernel"
16739            // ── number theory (extended) ──────────────────────────────────
16740            | "mod_exp" | "modexp" | "powmod"
16741            | "mod_inv" | "modinv" | "chinese_remainder" | "crt"
16742            | "miller_rabin" | "millerrabin" | "is_probable_prime"
16743            // ── combinatorics (extended) ──────────────────────────────────
16744            | "derangements" | "stirling2" | "stirling_second"
16745            | "bernoulli_number" | "bernoulli" | "harmonic_number" | "harmonic"
16746            // ── physics (new) ─────────────────────────────────────────────
16747            | "drag_force" | "fdrag" | "ideal_gas" | "pv_nrt"
16748            // ── financial greeks & risk ───────────────────────────────────
16749            | "bs_delta" | "bsdelta" | "option_delta"
16750            | "bs_gamma" | "bsgamma" | "option_gamma"
16751            | "bs_vega" | "bsvega" | "option_vega"
16752            | "bs_theta" | "bstheta" | "option_theta"
16753            | "bs_rho" | "bsrho" | "option_rho"
16754            | "bond_duration" | "mac_duration"
16755            // ── DSP extensions ────────────────────────────────────────────
16756            | "dct" | "idct" | "goertzel" | "chirp" | "chirp_signal"
16757            // ── encoding extensions ───────────────────────────────────────
16758            | "base85_encode" | "b85e" | "ascii85_encode" | "a85e"
16759            | "base85_decode" | "b85d" | "ascii85_decode" | "a85d"
16760            // ── R base: distributions ─────────────────────────────────────
16761            | "pnorm" | "qnorm" | "pbinom" | "dbinom" | "ppois"
16762            | "punif" | "pexp" | "pweibull" | "plnorm" | "pcauchy"
16763            // ── R base: matrix ops ────────────────────────────────────────
16764            | "rbind" | "cbind"
16765            | "row_sums" | "rowSums" | "col_sums" | "colSums"
16766            | "row_means" | "rowMeans" | "col_means" | "colMeans"
16767            | "outer" | "crossprod" | "tcrossprod"
16768            | "nrow" | "ncol" | "prop_table" | "proptable"
16769            // ── R base: vector ops ────────────────────────────────────────
16770            | "cummax" | "cummin" | "scale_vec" | "scale"
16771            | "which_fn" | "tabulate"
16772            | "duplicated" | "duped" | "rev_vec"
16773            | "seq_fn" | "rep_fn" | "rep"
16774            | "cut_bins" | "cut" | "find_interval" | "findInterval"
16775            | "ecdf_fn" | "ecdf" | "density_est" | "density"
16776            | "embed_ts" | "embed"
16777            // ── R base: stats tests ───────────────────────────────────────
16778            | "shapiro_test" | "shapiro" | "ks_test" | "ks"
16779            | "wilcox_test" | "wilcox" | "mann_whitney"
16780            | "prop_test" | "proptest" | "binom_test" | "binomtest"
16781            // ── R base: apply / functional ────────────────────────────────
16782            | "sapply" | "tapply" | "do_call" | "docall"
16783            // ── R base: ML / clustering ───────────────────────────────────
16784            | "kmeans" | "prcomp" | "pca"
16785            // ── R base: random generators ─────────────────────────────────
16786            | "rnorm" | "runif" | "rexp" | "rbinom" | "rpois" | "rgeom"
16787            | "rgamma" | "rbeta" | "rchisq" | "rt" | "rf"
16788            | "rweibull" | "rlnorm" | "rcauchy"
16789            // ── R base: quantile functions ────────────────────────────────
16790            | "qunif" | "qexp" | "qweibull" | "qlnorm" | "qcauchy"
16791            // ── R base: additional CDFs ───────────────────────────────────
16792            | "pgamma" | "pbeta" | "pchisq" | "pt_cdf" | "pt" | "pf_cdf" | "pf"
16793            // ── R base: additional PMFs ───────────────────────────────────
16794            | "dgeom" | "dunif" | "dnbinom" | "dhyper"
16795            // ── R base: smoothing / interpolation ─────────────────────────
16796            | "lowess" | "loess" | "approx_fn" | "approx"
16797            // ── R base: linear models ─────────────────────────────────────
16798            | "lm_fit" | "lm"
16799            // ── R base: remaining quantiles ───────────────────────────────
16800            | "qgamma" | "qbeta" | "qchisq" | "qt_fn" | "qt" | "qf_fn" | "qf"
16801            | "qbinom" | "qpois"
16802            // ── R base: time series ───────────────────────────────────────
16803            | "acf_fn" | "acf" | "pacf_fn" | "pacf"
16804            | "diff_lag" | "diff_ts" | "ts_filter" | "filter_ts"
16805            // ── R base: regression diagnostics ────────────────────────────
16806            | "predict_lm" | "predict" | "confint_lm" | "confint"
16807            // ── R base: multivariate stats ────────────────────────────────
16808            | "cor_matrix" | "cor_mat" | "cov_matrix" | "cov_mat"
16809            | "mahalanobis" | "mahal" | "dist_matrix" | "dist_mat"
16810            | "hclust" | "cutree" | "weighted_var" | "wvar" | "cov2cor"
16811            // ── SVG plotting ──────────────────────────────────────────────
16812            | "scatter_svg" | "scatter_plot" | "line_svg" | "line_plot"
16813            | "plot_svg" | "hist_svg" | "histogram_svg"
16814            | "boxplot_svg" | "box_plot" | "bar_svg" | "barchart_svg"
16815            | "pie_svg" | "pie_chart" | "heatmap_svg" | "heatmap"
16816            | "donut_svg" | "donut" | "area_svg" | "area_chart"
16817            | "hbar_svg" | "hbar" | "radar_svg" | "radar" | "spider"
16818            | "candlestick_svg" | "candlestick" | "ohlc"
16819            | "violin_svg" | "violin" | "cor_heatmap" | "cor_matrix_svg"
16820            | "stacked_bar_svg" | "stacked_bar"
16821            | "wordcloud_svg" | "wordcloud" | "wcloud"
16822            | "treemap_svg" | "treemap"
16823            | "pvw"
16824            // ── Cyberpunk terminal art ────────────────────────────────
16825            | "cyber_city" | "cyber_grid" | "cyber_rain" | "matrix_rain"
16826            | "cyber_glitch" | "glitch_text" | "cyber_banner" | "neon_banner"
16827            | "cyber_circuit" | "cyber_skull" | "cyber_eye"
16828            // ── AI primitives (docs/AI_PRIMITIVES.md) ─────────────────
16829            | "ai" | "ai_agent" | "prompt" | "stream_prompt" | "stream_prompt_cb"
16830            | "tokens_of"
16831            | "ai_estimate" | "ai_cost" | "ai_history" | "ai_history_clear"
16832            | "ai_cache_clear" | "ai_cache_size"
16833            | "ai_mock_install" | "ai_mock_clear"
16834            | "ai_config_get" | "ai_config_set" | "ai_routing_get" | "ai_routing_set"
16835            | "ai_register_tool" | "ai_unregister_tool" | "ai_clear_tools" | "ai_tools_list"
16836            | "ai_filter" | "ai_map" | "ai_classify" | "ai_match" | "ai_sort" | "ai_dedupe"
16837            | "ai_extract" | "ai_summarize" | "ai_translate" | "ai_template"
16838            | "ai_session_new" | "ai_session_send" | "ai_session_history"
16839            | "ai_session_close" | "ai_session_reset"
16840            | "ai_session_export" | "ai_session_import"
16841            | "ai_memory_save" | "ai_memory_recall" | "ai_memory_forget"
16842            | "ai_memory_count" | "ai_memory_clear"
16843            | "ai_vision" | "ai_pdf" | "ai_grounded" | "ai_citations"
16844            | "ai_transcribe" | "ai_speak" | "ai_image" | "ai_image_edit" | "ai_image_variation"
16845            | "ai_models" | "ai_describe" | "ai_pricing" | "ai_dashboard"
16846            | "ai_moderate" | "ai_chunk" | "ai_warm" | "ai_compare"
16847            | "ai_last_thinking" | "ai_budget" | "ai_batch" | "ai_pmap"
16848            | "ai_file_upload" | "ai_file_list" | "ai_file_get" | "ai_file_delete"
16849            | "ai_file_anthropic_upload" | "ai_file_anthropic_list" | "ai_file_anthropic_delete"
16850            | "vec_cosine" | "vec_search" | "vec_topk"
16851            // ── AI tool specs ────────────────────────────────────────
16852            | "web_search_tool" | "fetch_url_tool" | "read_file_tool" | "run_code_tool"
16853            // ── MCP (Model Context Protocol) ─────────────────────────
16854            | "mcp_connect" | "mcp_close" | "mcp_tools" | "mcp_call"
16855            | "mcp_resource" | "mcp_resources" | "mcp_prompt" | "mcp_prompts"
16856            | "mcp_attach_to_ai" | "mcp_detach_from_ai" | "mcp_attached"
16857            | "mcp_server_start" | "mcp_serve_registered_tools"
16858            // ── PTY / expect (docs/expect-feature-idea.md) ────────────
16859            | "pty_spawn" | "pty_send" | "pty_read" | "pty_expect" | "pty_expect_table"
16860            | "pty_buffer" | "pty_alive" | "pty_eof" | "pty_close" | "pty_interact"
16861            | "pty_strip_ansi" | "pty_after_eof" | "pty_pending_events"
16862            // ── Stress / telemetry extensions ─────────────────────────
16863            | "stress_fp" | "stress_int" | "stress_cache" | "stress_branch"
16864            | "stress_sort" | "stress_alloc" | "stress_mmap" | "stress_disk"
16865            | "stress_iops" | "stress_net" | "stress_http" | "stress_dns"
16866            | "stress_fork" | "stress_thread" | "stress_aes" | "stress_compress"
16867            | "stress_regex" | "stress_json" | "stress_burst" | "stress_ramp"
16868            | "stress_oscillate" | "stress_all" | "stress_temp" | "stress_thermal_zones"
16869            | "stress_freq" | "stress_throttled" | "stress_load" | "stress_meminfo"
16870            | "stress_cores" | "stress_arm_kill_switch" | "stress_killed"
16871            | "stress_disarm_kill_switch"
16872            | "stress_metrics_record" | "stress_metrics_clear" | "stress_metrics_count"
16873            | "stress_metrics_export" | "stress_metrics_prometheus"
16874            | "stress_metrics_json" | "stress_metrics_csv" | "stress_metrics_watch"
16875            // ── Compliance / secrets ─────────────────────────────────
16876            | "audit_log" | "audit_log_path"
16877            | "secrets_encrypt" | "secrets_decrypt" | "secrets_random_key" | "secrets_kdf"
16878            // ── Web framework (docs/WEB_FRAMEWORK.md) ─────────────────
16879            | "web_route" | "web_resources" | "web_root" | "web_routes_table"
16880            | "web_application_config" | "web_boot_application"
16881            | "web_render" | "web_render_partial" | "web_redirect"
16882            | "web_json" | "web_text" | "web_csv" | "web_markdown"
16883            | "web_params" | "web_request" | "web_set_header" | "web_status"
16884            | "web_before_action" | "web_after_action"
16885            | "web_session" | "web_session_set" | "web_session_get" | "web_session_clear"
16886            | "web_signed" | "web_unsigned"
16887            | "web_cookies" | "web_set_cookie"
16888            | "web_flash" | "web_flash_set" | "web_flash_get"
16889            | "web_validate" | "web_permit"
16890            | "web_password_hash" | "web_password_verify"
16891            | "web_token_for" | "web_token_consume" | "web_csrf_meta_tag"
16892            | "web_security_headers" | "web_can"
16893            | "web_h" | "web_truncate" | "web_pluralize" | "web_time_ago_in_words"
16894            | "web_image_tag" | "web_link_to" | "web_button_to"
16895            | "web_form_with" | "web_form_close"
16896            | "web_text_field" | "web_text_area" | "web_check_box"
16897            | "web_stylesheet_link_tag" | "web_javascript_link_tag"
16898            | "web_yield_content" | "web_content_for"
16899            | "web_etag" | "web_cache_get" | "web_cache_set"
16900            | "web_cache_delete" | "web_cache_clear"
16901            | "web_db_connect" | "web_db_execute" | "web_db_query"
16902            | "web_db_begin" | "web_db_commit" | "web_db_rollback"
16903            | "web_create_table" | "web_drop_table"
16904            | "web_add_column" | "web_remove_column"
16905            | "web_migrate" | "web_rollback"
16906            | "web_model_all" | "web_model_find" | "web_model_first" | "web_model_last"
16907            | "web_model_where" | "web_model_create" | "web_model_update"
16908            | "web_model_destroy" | "web_model_count" | "web_model_increment"
16909            | "web_model_paginate" | "web_model_search" | "web_model_soft_destroy"
16910            | "web_model_with"
16911            | "web_jobs_init" | "web_job_enqueue" | "web_job_dequeue"
16912            | "web_job_complete" | "web_job_fail"
16913            | "web_jobs_list" | "web_jobs_stats" | "web_job_purge"
16914            | "web_jsonapi_resource" | "web_jsonapi_collection" | "web_jsonapi_error"
16915            | "web_bearer_token" | "web_jwt_encode" | "web_jwt_decode"
16916            | "web_otp_secret" | "web_otp_generate" | "web_otp_verify"
16917            | "web_uuid" | "web_now" | "web_log" | "web_rate_limit"
16918            | "web_t" | "web_load_locale" | "web_openapi"
16919            | "web_faker_int" | "web_faker_email" | "web_faker_name"
16920            | "web_faker_sentence" | "web_faker_paragraph"
16921            => Some(name),
16922            _ => None,
16923        }
16924    }
16925
16926    /// Reserved hash names that cannot be shadowed by user declarations.
16927    /// These are stryke's reflection hashes populated from builtins metadata.
16928    fn is_reserved_hash_name(name: &str) -> bool {
16929        matches!(
16930            name,
16931            "b" | "pc"
16932                | "e"
16933                | "a"
16934                | "d"
16935                | "c"
16936                | "p"
16937                | "k"
16938                | "all"
16939                | "stryke::builtins"
16940                | "stryke::perl_compats"
16941                | "stryke::extensions"
16942                | "stryke::aliases"
16943                | "stryke::descriptions"
16944                | "stryke::categories"
16945                | "stryke::primaries"
16946                | "stryke::keywords"
16947                | "stryke::all"
16948        )
16949    }
16950
16951    /// Check if a UDF name shadows a stryke builtin and error if so.
16952    /// Called only in non-compat mode — compat mode allows shadowing for Perl 5 parity.
16953    /// Reserved words that cannot be used as function names because they are
16954    /// lexer-level operators or language keywords that would be mis-tokenized.
16955    const RESERVED_FUNCTION_NAMES: &'static [&'static str] = &[
16956        "y",
16957        "tr",
16958        "s",
16959        "m",
16960        "q",
16961        "qq",
16962        "qw",
16963        "qx",
16964        "qr",
16965        "if",
16966        "unless",
16967        "while",
16968        "until",
16969        "for",
16970        "foreach",
16971        "given",
16972        "when",
16973        "else",
16974        "elsif",
16975        "do",
16976        "eval",
16977        "return",
16978        "last",
16979        "next",
16980        "redo",
16981        "goto",
16982        "my",
16983        "our",
16984        "local",
16985        "state",
16986        "sub",
16987        "fn",
16988        "class",
16989        "struct",
16990        "enum",
16991        "trait",
16992        "use",
16993        "no",
16994        "require",
16995        "package",
16996        "BEGIN",
16997        "END",
16998        "CHECK",
16999        "INIT",
17000        "UNITCHECK",
17001        "and",
17002        "or",
17003        "not",
17004        "x",
17005        "eq",
17006        "ne",
17007        "lt",
17008        "gt",
17009        "le",
17010        "ge",
17011        "cmp",
17012    ];
17013
17014    fn check_udf_shadows_builtin(&self, name: &str, line: usize) -> PerlResult<()> {
17015        // Only check bare names, not namespaced ones (Foo::y is allowed)
17016        if !name.contains("::") {
17017            if Self::RESERVED_FUNCTION_NAMES.contains(&name) {
17018                return Err(self.syntax_err(
17019                    format!("`{name}` is a reserved word and cannot be used as a function name"),
17020                    line,
17021                ));
17022            }
17023            if Self::is_known_bareword(name)
17024                || Self::is_try_builtin_name(name)
17025                || crate::list_builtins::is_list_builtin_name(name)
17026            {
17027                return Err(self.syntax_err(
17028                    format!(
17029"`{name}` is a stryke builtin and cannot be redefined (this is not Perl 5; use `fn` not `sub`, or pass --compat)"
17030                    ),
17031                    line,
17032                ));
17033            }
17034        }
17035        Ok(())
17036    }
17037
17038    /// Check if a hash name shadows a reserved stryke hash and error if so.
17039    /// Called only in non-compat mode.
17040    fn check_hash_shadows_reserved(&self, name: &str, line: usize) -> PerlResult<()> {
17041        if Self::is_reserved_hash_name(name) {
17042            return Err(self.syntax_err(
17043                format!(
17044"`%{name}` is a stryke reserved hash and cannot be redefined (this is not Perl 5; pass --compat for Perl 5 mode)"
17045                ),
17046                line,
17047            ));
17048        }
17049        Ok(())
17050    }
17051
17052    /// Validate assignment to %hash in non-compat mode.
17053    /// Rejects: scalar, string, arrayref, hashref, coderef, undef, odd-length list.
17054    fn validate_hash_assignment(&self, value: &Expr, line: usize) -> PerlResult<()> {
17055        match &value.kind {
17056            ExprKind::Integer(_) | ExprKind::Float(_) => {
17057                return Err(self.syntax_err(
17058                    "cannot assign scalar to hash — use %h = (key => value) or %h = %{$hashref}",
17059                    line,
17060                ));
17061            }
17062            ExprKind::String(_) | ExprKind::InterpolatedString(_) | ExprKind::Bareword(_) => {
17063                return Err(self.syntax_err(
17064                    "cannot assign string to hash — use %h = (key => value) or %h = %{$hashref}",
17065                    line,
17066                ));
17067            }
17068            ExprKind::ArrayRef(_) => {
17069                return Err(self.syntax_err(
17070                    "cannot assign arrayref to hash — use %h = @{$arrayref} for even-length list",
17071                    line,
17072                ));
17073            }
17074            ExprKind::ScalarRef(inner) => {
17075                if matches!(inner.kind, ExprKind::ArrayVar(_)) {
17076                    return Err(self.syntax_err(
17077                        "cannot assign \\@array to hash — use %h = @array for even-length list",
17078                        line,
17079                    ));
17080                }
17081                if matches!(inner.kind, ExprKind::HashVar(_)) {
17082                    return Err(self.syntax_err(
17083                        "cannot assign \\%hash to hash — use %h = %other directly",
17084                        line,
17085                    ));
17086                }
17087            }
17088            ExprKind::HashRef(_) => {
17089                return Err(self.syntax_err(
17090                    "cannot assign hashref to hash — use %h = %{$hashref} to dereference",
17091                    line,
17092                ));
17093            }
17094            ExprKind::CodeRef { .. } => {
17095                return Err(self.syntax_err("cannot assign coderef to hash", line));
17096            }
17097            ExprKind::Undef => {
17098                return Err(
17099                    self.syntax_err("cannot assign undef to hash — use %h = () to empty", line)
17100                );
17101            }
17102            ExprKind::List(items)
17103                if items.len() % 2 != 0
17104                    && !items.iter().any(|e| {
17105                        matches!(
17106                            e.kind,
17107                            ExprKind::ArrayVar(_)
17108                                | ExprKind::HashVar(_)
17109                                | ExprKind::FuncCall { .. }
17110                                | ExprKind::Deref { .. }
17111                                | ExprKind::ScalarVar(_)
17112                        )
17113                    }) =>
17114            {
17115                return Err(self.syntax_err(
17116                        format!(
17117                            "odd-length list ({} elements) in hash assignment — missing value for last key",
17118                            items.len()
17119                        ),
17120                        line,
17121                    ));
17122            }
17123            _ => {}
17124        }
17125        Ok(())
17126    }
17127
17128    /// Validate assignment to @array in non-compat mode.
17129    /// Rejects: undef (likely a mistake — use `@a = ()` to empty).
17130    /// Note: bare scalars like `@a = 2` are allowed since Perl coerces them to single-element lists.
17131    /// Note: `@a = {hashref}` is allowed as a common pattern for single-element arrays.
17132    fn validate_array_assignment(&self, value: &Expr, line: usize) -> PerlResult<()> {
17133        if let ExprKind::Undef = &value.kind {
17134            return Err(
17135                self.syntax_err("cannot assign undef to array — use @a = () to empty", line)
17136            );
17137        }
17138        Ok(())
17139    }
17140
17141    /// Validate assignment to $scalar in non-compat mode.
17142    /// Rejects: list literals (Perl 5 silently returns last element — footgun).
17143    fn validate_scalar_assignment(&self, value: &Expr, line: usize) -> PerlResult<()> {
17144        if let ExprKind::List(items) = &value.kind {
17145            if items.len() > 1 {
17146                return Err(self.syntax_err(
17147                    format!(
17148                        "cannot assign {}-element list to scalar — Perl 5 silently takes last element; use ($x) = (list) or $x = $list[-1]",
17149                        items.len()
17150                    ),
17151                    line,
17152                ));
17153            }
17154        }
17155        Ok(())
17156    }
17157
17158    /// Validate an assignment based on target type (in non-compat mode only).
17159    fn validate_assignment(&self, target: &Expr, value: &Expr, line: usize) -> PerlResult<()> {
17160        if crate::compat_mode() {
17161            return Ok(());
17162        }
17163        match &target.kind {
17164            ExprKind::HashVar(_) => self.validate_hash_assignment(value, line),
17165            ExprKind::ArrayVar(_) => self.validate_array_assignment(value, line),
17166            ExprKind::ScalarVar(_) => self.validate_scalar_assignment(value, line),
17167            _ => Ok(()),
17168        }
17169    }
17170
17171    /// Parse a block OR a blockless comparison expression for sort/psort/heap.
17172    /// Blockless: `$a <=> $b` or `$a cmp $b` or any expression → wrapped as a Block.
17173    /// Also accepts a bare function name: `psort my_cmp, @list`.
17174    fn parse_block_or_bareword_cmp_block(&mut self) -> PerlResult<Block> {
17175        if matches!(self.peek(), Token::LBrace) {
17176            return self.parse_block();
17177        }
17178        let line = self.peek_line();
17179        // Bare sub name: `psort my_cmp, @list`
17180        if let Token::Ident(ref name) = self.peek().clone() {
17181            if matches!(
17182                self.peek_at(1),
17183                Token::Comma | Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
17184            ) {
17185                let name = name.clone();
17186                self.advance();
17187                let body = Expr {
17188                    kind: ExprKind::FuncCall {
17189                        name,
17190                        args: vec![
17191                            Expr {
17192                                kind: ExprKind::ScalarVar("a".to_string()),
17193                                line,
17194                            },
17195                            Expr {
17196                                kind: ExprKind::ScalarVar("b".to_string()),
17197                                line,
17198                            },
17199                        ],
17200                    },
17201                    line,
17202                };
17203                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
17204            }
17205        }
17206        // Blockless expression: `$a <=> $b`, `$b cmp $a`, etc.
17207        let expr = self.parse_assign_expr_stop_at_pipe()?;
17208        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
17209    }
17210
17211    /// After `fan` / `fan_cap` `{ BLOCK }`, optional `, progress => EXPR` or `progress => EXPR` (no comma).
17212    fn parse_fan_optional_progress(
17213        &mut self,
17214        which: &'static str,
17215    ) -> PerlResult<Option<Box<Expr>>> {
17216        let line = self.peek_line();
17217        if self.eat(&Token::Comma) {
17218            match self.peek() {
17219                Token::Ident(ref kw)
17220                    if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) =>
17221                {
17222                    self.advance();
17223                    self.expect(&Token::FatArrow)?;
17224                    return Ok(Some(Box::new(self.parse_assign_expr()?)));
17225                }
17226                _ => {
17227                    return Err(self.syntax_err(
17228                        format!("{which}: expected `progress => EXPR` after comma"),
17229                        line,
17230                    ));
17231                }
17232            }
17233        }
17234        if let Token::Ident(ref kw) = self.peek().clone() {
17235            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
17236                self.advance();
17237                self.expect(&Token::FatArrow)?;
17238                return Ok(Some(Box::new(self.parse_assign_expr()?)));
17239            }
17240        }
17241        Ok(None)
17242    }
17243
17244    /// Comma-separated assign expressions with optional trailing `, progress => EXPR`
17245    /// (for `pmap_chunked`, `psort`, etc.).
17246    ///
17247    /// Paren-less — individual parts parse through
17248    /// [`Self::parse_assign_expr_stop_at_pipe`] so a trailing `|>` is left for
17249    /// the enclosing pipe-forward loop (left-associative chaining).
17250    fn parse_assign_expr_list_optional_progress(&mut self) -> PerlResult<(Expr, Option<Expr>)> {
17251        // On the RHS of `|>`, list-taking builtins may be written bare with no
17252        // operand — `@a |> uniq`, `@a |> flatten`, `foo(bar, @a |> psort)`, etc.
17253        // When the next token is a list-terminator, yield an empty placeholder
17254        // list; [`Self::pipe_forward_apply`] substitutes the piped LHS at
17255        // desugar time, so the placeholder is never evaluated.
17256        if self.in_pipe_rhs()
17257            && matches!(
17258                self.peek(),
17259                Token::Semicolon
17260                    | Token::RBrace
17261                    | Token::RParen
17262                    | Token::Eof
17263                    | Token::PipeForward
17264                    | Token::Comma
17265            )
17266        {
17267            return Ok((self.pipe_placeholder_list(self.peek_line()), None));
17268        }
17269        let mut parts = vec![self.parse_assign_expr_stop_at_pipe()?];
17270        loop {
17271            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
17272                break;
17273            }
17274            if matches!(
17275                self.peek(),
17276                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
17277            ) {
17278                break;
17279            }
17280            if self.peek_is_postfix_stmt_modifier_keyword() {
17281                break;
17282            }
17283            if let Token::Ident(ref kw) = self.peek().clone() {
17284                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
17285                    self.advance();
17286                    self.expect(&Token::FatArrow)?;
17287                    let prog = self.parse_assign_expr_stop_at_pipe()?;
17288                    return Ok((merge_expr_list(parts), Some(prog)));
17289                }
17290            }
17291            parts.push(self.parse_assign_expr_stop_at_pipe()?);
17292        }
17293        Ok((merge_expr_list(parts), None))
17294    }
17295
17296    fn parse_one_arg(&mut self) -> PerlResult<Expr> {
17297        if matches!(self.peek(), Token::LParen) {
17298            self.advance();
17299            let expr = self.parse_expression()?;
17300            self.expect(&Token::RParen)?;
17301            Ok(expr)
17302        } else {
17303            self.parse_assign_expr_stop_at_pipe()
17304        }
17305    }
17306
17307    /// Bare argument for a Perl-5 named unary operator (`defined`, `length`,
17308    /// `abs`, `scalar`, `ref`, `keys`, `values`, etc.). Named unary precedence
17309    /// sits between shift (`<<`/`>>`) and comparison (`<`/`>`), so we parse
17310    /// only down to shift level. The surrounding `&&` / `||` / `==` / `<` /
17311    /// equality / logical / ternary stay outside the unary's argument.
17312    /// Without this `defined $x && Y` mis-parsed as `defined($x && Y)` and
17313    /// silently returned true whenever `$x` was defined — see the skip-list
17314    /// debugging write-up. Same scope rule for `length` etc.
17315    fn parse_named_unary_arg(&mut self) -> PerlResult<Expr> {
17316        if matches!(self.peek(), Token::LParen) {
17317            self.advance();
17318            let expr = self.parse_expression()?;
17319            self.expect(&Token::RParen)?;
17320            Ok(expr)
17321        } else {
17322            self.parse_shift()
17323        }
17324    }
17325
17326    fn parse_one_arg_or_default(&mut self) -> PerlResult<Expr> {
17327        // Treat a line boundary as a hard arg terminator: if the next
17328        // token is on a *later* line than the named-unary keyword we
17329        // just consumed, default the operand to `$_` and stop. Without
17330        // this, `my $x = uc` followed by `my $y = 5` on the next line
17331        // mis-parses by silently swallowing `my $y = 5` as the implicit
17332        // argument to `uc`. Stryke (like Perl/shell) terminates
17333        // statements at newline; continuation requires explicit `\`.
17334        // The check skips when the *next* token is itself a binary /
17335        // postfix operator that legitimately continues the expression
17336        // (handled by the existing operator stop-list below).
17337        let prev = self.prev_line();
17338        if self.peek_line() > prev {
17339            return Ok(Expr {
17340                kind: ExprKind::ScalarVar("_".into()),
17341                line: prev,
17342            });
17343        }
17344        // Default to `$_` when the next token cannot start an argument expression
17345        // because it has lower precedence than a named unary operator. Perl 5
17346        // named unary precedence sits above ternary / comparison / logical / bitwise
17347        // / assignment / list ops; everything below should terminate the implicit
17348        // argument and let the surrounding expression continue.
17349        // See `perldoc perlop` ("Named Unary Operators").
17350        if matches!(
17351            self.peek(),
17352            // Statement / list / call boundaries
17353            Token::Semicolon
17354                | Token::RBrace
17355                | Token::RParen
17356                | Token::RBracket
17357                | Token::Eof
17358                | Token::Comma
17359                | Token::FatArrow
17360                | Token::PipeForward
17361            // Ternary `? :`
17362                | Token::Question
17363                | Token::Colon
17364            // Comparison / equality (numeric + string)
17365                | Token::NumEq | Token::NumNe | Token::NumLt | Token::NumGt
17366                | Token::NumLe | Token::NumGe | Token::Spaceship
17367                | Token::StrEq | Token::StrNe | Token::StrLt | Token::StrGt
17368                | Token::StrLe | Token::StrGe | Token::StrCmp
17369            // Logical (symbolic and word forms) + defined-or
17370                | Token::LogAnd | Token::LogOr | Token::LogNot
17371                | Token::LogAndWord | Token::LogOrWord | Token::LogNotWord
17372                | Token::DefinedOr
17373            // Range (lower precedence than named unary)
17374                | Token::Range | Token::RangeExclusive
17375            // Assignment (any compound form)
17376                | Token::Assign | Token::PlusAssign | Token::MinusAssign
17377                | Token::MulAssign | Token::DivAssign | Token::ModAssign
17378                | Token::PowAssign | Token::DotAssign | Token::AndAssign
17379                | Token::OrAssign | Token::XorAssign | Token::DefinedOrAssign
17380                | Token::ShiftLeftAssign | Token::ShiftRightAssign
17381                | Token::BitAndAssign | Token::BitOrAssign
17382        ) {
17383            return Ok(Expr {
17384                kind: ExprKind::ScalarVar("_".into()),
17385                line: self.peek_line(),
17386            });
17387        }
17388        // `f()` — empty parens default to `$_`, matching Perl 5 semantics.
17389        // `perldoc -f length`: "If EXPR is omitted, returns the length of $_."
17390        // Perl accepts both `length` and `length()` as `length($_)`.
17391        if matches!(self.peek(), Token::LParen) && matches!(self.peek_at(1), Token::RParen) {
17392            let line = self.peek_line();
17393            self.advance(); // (
17394            self.advance(); // )
17395            return Ok(Expr {
17396                kind: ExprKind::ScalarVar("_".into()),
17397                line,
17398            });
17399        }
17400        // Named-unary precedence: parenless arg only goes down to shift level,
17401        // so surrounding `eq` / `==` / `?:` / `&&` / `||` stay outside. Without
17402        // this, `ref $x eq "FOO"` mis-parses as `ref ($x eq "FOO")`.
17403        // (PARITY-016 — also fixes `length $s == 3 ? "Y" : "N"` etc.)
17404        self.parse_named_unary_arg()
17405    }
17406
17407    /// Array operand for `shift` / `pop`: default `@_`, or `shift(@a)` / `shift()` (empty parens = `@_`).
17408    fn parse_one_arg_or_argv(&mut self) -> PerlResult<Expr> {
17409        let line = self.prev_line(); // line where shift/pop keyword was
17410        if matches!(self.peek(), Token::LParen) {
17411            self.advance();
17412            if matches!(self.peek(), Token::RParen) {
17413                self.advance();
17414                return Ok(Expr {
17415                    kind: ExprKind::ArrayVar("_".into()),
17416                    line: self.peek_line(),
17417                });
17418            }
17419            let expr = self.parse_expression()?;
17420            self.expect(&Token::RParen)?;
17421            return Ok(expr);
17422        }
17423        // Implicit semicolon: if next token is on a different line, don't consume it
17424        if matches!(
17425            self.peek(),
17426            Token::Semicolon
17427                | Token::RBrace
17428                | Token::RParen
17429                | Token::Eof
17430                | Token::Comma
17431                | Token::PipeForward
17432        ) || self.peek_line() > line
17433        {
17434            Ok(Expr {
17435                kind: ExprKind::ArrayVar("_".into()),
17436                line,
17437            })
17438        } else {
17439            self.parse_assign_expr()
17440        }
17441    }
17442
17443    fn parse_builtin_args(&mut self) -> PerlResult<Vec<Expr>> {
17444        if matches!(self.peek(), Token::LParen) {
17445            self.advance();
17446            let args = self.parse_arg_list()?;
17447            self.expect(&Token::RParen)?;
17448            Ok(args)
17449        } else if self.suppress_parenless_call > 0 && matches!(self.peek(), Token::Ident(_)) {
17450            // In thread context, don't consume barewords as arguments
17451            // so `t filesf sorted ep` parses `sorted` as a stage, not an arg to filesf
17452            Ok(vec![])
17453        } else {
17454            self.parse_list_until_terminator()
17455        }
17456    }
17457
17458    /// Check if the next token is `=>` (fat arrow). If so, the preceding bareword
17459    /// should be treated as an auto-quoted string (hash key), not a function call.
17460    /// Returns `Some(Expr::String(name))` if fat arrow follows, `None` otherwise.
17461    #[inline]
17462    fn fat_arrow_autoquote(&self, name: &str, line: usize) -> Option<Expr> {
17463        if matches!(self.peek(), Token::FatArrow) {
17464            Some(Expr {
17465                kind: ExprKind::String(name.to_string()),
17466                line,
17467            })
17468        } else {
17469            None
17470        }
17471    }
17472
17473    /// Parse a hash subscript key inside `{…}`.
17474    ///
17475    /// Perl auto-quotes a single bareword before `}`, even for keywords:
17476    /// `$h{print}`, `$r->{f}` etc. all yield the string key. Stryke also
17477    /// auto-quotes the string-comparison and word-logical operator tokens
17478    /// (`eq`, `ne`, `lt`, `gt`, `le`, `ge`, `cmp`, `and`, `or`, `not`, `x`)
17479    /// here — the lexer eagerly converts those identifiers to operator tokens,
17480    /// but inside `{…}` followed by `}` they're plainly hash keys.
17481    /// Stryke exception: topic-slot barewords (`_`, `_<`, `_0`, `_0<`, …)
17482    /// resolve to the topic value, not the literal name — `$h{_<}` ≡ `$h{$_<}`.
17483    fn parse_hash_subscript_key(&mut self) -> PerlResult<Expr> {
17484        let line = self.peek_line();
17485        if let Token::Ident(ref k) = self.peek().clone() {
17486            if matches!(self.peek_at(1), Token::RBrace) && !Self::is_underscore_topic_slot(k) {
17487                let s = k.clone();
17488                self.advance();
17489                return Ok(Expr {
17490                    kind: ExprKind::String(s),
17491                    line,
17492                });
17493            }
17494        }
17495        if matches!(self.peek_at(1), Token::RBrace) {
17496            if let Some(s) = Self::operator_keyword_to_ident_str(self.peek()) {
17497                self.advance();
17498                return Ok(Expr {
17499                    kind: ExprKind::String(s.to_string()),
17500                    line,
17501                });
17502            }
17503        }
17504        self.parse_expression()
17505    }
17506
17507    /// `progress` introducing the optional `progress => EXPR` suffix for `glob_par` / `par_sed`.
17508    #[inline]
17509    fn peek_is_glob_par_progress_kw(&self) -> bool {
17510        matches!(self.peek(), Token::Ident(ref kw) if kw == "progress")
17511            && matches!(self.peek_at(1), Token::FatArrow)
17512    }
17513
17514    /// Pattern list for `glob_par` / `par_sed` inside `(...)`, stopping before `)` or `progress =>`.
17515    fn parse_pattern_list_until_rparen_or_progress(&mut self) -> PerlResult<Vec<Expr>> {
17516        let mut args = Vec::new();
17517        loop {
17518            if matches!(self.peek(), Token::RParen | Token::Eof) {
17519                break;
17520            }
17521            if self.peek_is_glob_par_progress_kw() {
17522                break;
17523            }
17524            args.push(self.parse_assign_expr()?);
17525            match self.peek() {
17526                Token::RParen => break,
17527                Token::Comma => {
17528                    self.advance();
17529                    if matches!(self.peek(), Token::RParen) {
17530                        break;
17531                    }
17532                    if self.peek_is_glob_par_progress_kw() {
17533                        break;
17534                    }
17535                }
17536                _ => {
17537                    return Err(self.syntax_err(
17538                        "expected `,`, `)`, or `progress =>` after argument in `glob_par` / `par_sed`",
17539                        self.peek_line(),
17540                    ));
17541                }
17542            }
17543        }
17544        Ok(args)
17545    }
17546
17547    /// Paren-less pattern list for `glob_par` / `par_sed`, stopping before stmt end or `progress =>`.
17548    fn parse_pattern_list_glob_par_bare(&mut self) -> PerlResult<Vec<Expr>> {
17549        let mut args = Vec::new();
17550        loop {
17551            if matches!(
17552                self.peek(),
17553                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
17554            ) {
17555                break;
17556            }
17557            if self.peek_is_postfix_stmt_modifier_keyword() {
17558                break;
17559            }
17560            if self.peek_is_glob_par_progress_kw() {
17561                break;
17562            }
17563            args.push(self.parse_assign_expr()?);
17564            if !self.eat(&Token::Comma) {
17565                break;
17566            }
17567            if self.peek_is_glob_par_progress_kw() {
17568                break;
17569            }
17570        }
17571        Ok(args)
17572    }
17573
17574    /// `glob_pat EXPR, ...` or `glob_pat(...)` plus optional `, progress => EXPR` / inner `progress =>`.
17575    fn parse_glob_par_or_par_sed_args(&mut self) -> PerlResult<(Vec<Expr>, Option<Box<Expr>>)> {
17576        if matches!(self.peek(), Token::LParen) {
17577            self.advance();
17578            let args = self.parse_pattern_list_until_rparen_or_progress()?;
17579            let progress = if self.peek_is_glob_par_progress_kw() {
17580                self.advance();
17581                self.expect(&Token::FatArrow)?;
17582                Some(Box::new(self.parse_assign_expr()?))
17583            } else {
17584                None
17585            };
17586            self.expect(&Token::RParen)?;
17587            Ok((args, progress))
17588        } else {
17589            let args = self.parse_pattern_list_glob_par_bare()?;
17590            // Comma after the last pattern was consumed inside `parse_pattern_list_glob_par_bare`.
17591            let progress = if self.peek_is_glob_par_progress_kw() {
17592                self.advance();
17593                self.expect(&Token::FatArrow)?;
17594                Some(Box::new(self.parse_assign_expr()?))
17595            } else {
17596                None
17597            };
17598            Ok((args, progress))
17599        }
17600    }
17601
17602    pub(crate) fn parse_arg_list(&mut self) -> PerlResult<Vec<Expr>> {
17603        let mut args = Vec::new();
17604        // Inside `(...)`, `|>` is a normal operator again (e.g. `f(2 |> g, 3)`),
17605        // so shadow any outer paren-less-arg suppression from
17606        // `no_pipe_forward_depth`. Saturating so nested mixes are safe.
17607        let saved_no_pf = self.no_pipe_forward_depth;
17608        self.no_pipe_forward_depth = 0;
17609        while !matches!(
17610            self.peek(),
17611            Token::RParen | Token::RBracket | Token::RBrace | Token::Eof
17612        ) {
17613            let arg = match self.parse_assign_expr() {
17614                Ok(e) => e,
17615                Err(err) => {
17616                    self.no_pipe_forward_depth = saved_no_pf;
17617                    return Err(err);
17618                }
17619            };
17620            args.push(arg);
17621            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
17622                break;
17623            }
17624        }
17625        self.no_pipe_forward_depth = saved_no_pf;
17626        Ok(args)
17627    }
17628
17629    /// Parse a comma-separated list of slice subscript args. Each arg may be a regular
17630    /// expression, a closed range (`1:3`, `1..3:2`), or an open-ended Python-style colon
17631    /// range (`:`, `::`, `:N`, `N:`, `::-1`, `:N:M`, `N::M`, `::M`). Open-ended forms
17632    /// produce `ExprKind::SliceRange`; closed `1:3` produces `ExprKind::Range` (legacy).
17633    ///
17634    /// `is_hash` enables fat-comma-style bareword auto-quoting for endpoints — `{a:c:1}`
17635    /// treats `a` and `c` as string keys without quoting (cannot be a function call;
17636    /// use `func():other` if you actually want to invoke).
17637    pub(crate) fn parse_slice_arg_list(&mut self, is_hash: bool) -> PerlResult<Vec<Expr>> {
17638        let mut args = Vec::new();
17639        let saved_no_pf = self.no_pipe_forward_depth;
17640        self.no_pipe_forward_depth = 0;
17641        while !matches!(
17642            self.peek(),
17643            Token::RParen | Token::RBracket | Token::RBrace | Token::Eof
17644        ) {
17645            let arg = match self.parse_slice_arg(is_hash) {
17646                Ok(e) => e,
17647                Err(err) => {
17648                    self.no_pipe_forward_depth = saved_no_pf;
17649                    return Err(err);
17650                }
17651            };
17652            args.push(arg);
17653            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
17654                break;
17655            }
17656        }
17657        self.no_pipe_forward_depth = saved_no_pf;
17658        Ok(args)
17659    }
17660
17661    /// Parse one slice subscript argument (see [`Self::parse_slice_arg_list`]).
17662    fn parse_slice_arg(&mut self, is_hash: bool) -> PerlResult<Expr> {
17663        let line = self.peek_line();
17664
17665        // Open-start: `:` or `::` immediately
17666        if matches!(self.peek(), Token::Colon) {
17667            self.advance();
17668            return self.finish_slice_range(None, false, is_hash, line);
17669        }
17670        if matches!(self.peek(), Token::PackageSep) {
17671            self.advance();
17672            return self.finish_slice_range(None, true, is_hash, line);
17673        }
17674
17675        // Parse FROM with `:` suppressed inside `parse_range` so it doesn't get
17676        // consumed as a colon-range there — we want to handle the colon ourselves.
17677        self.suppress_colon_range = self.suppress_colon_range.saturating_add(1);
17678        let result = self.parse_slice_endpoint(is_hash);
17679        self.suppress_colon_range = self.suppress_colon_range.saturating_sub(1);
17680        let from_expr = result?;
17681
17682        // Trailing `:` or `::` after the FROM endpoint?
17683        if matches!(self.peek(), Token::Colon) {
17684            self.advance();
17685            return self.finish_slice_range(Some(Box::new(from_expr)), false, is_hash, line);
17686        }
17687        if matches!(self.peek(), Token::PackageSep) {
17688            self.advance();
17689            return self.finish_slice_range(Some(Box::new(from_expr)), true, is_hash, line);
17690        }
17691
17692        Ok(from_expr)
17693    }
17694
17695    /// After consuming the first colon (or `::` pair), parse the rest of the slice range.
17696    /// `double` is true if we just consumed `::` — TO is implicit `None`, the next
17697    /// expression (if any) is STEP.
17698    ///
17699    /// Returns `ExprKind::Range` for fully-closed forms (legacy compatibility) and
17700    /// `ExprKind::SliceRange` whenever any endpoint is omitted (open-ended).
17701    fn finish_slice_range(
17702        &mut self,
17703        from: Option<Box<Expr>>,
17704        double: bool,
17705        is_hash: bool,
17706        line: usize,
17707    ) -> PerlResult<Expr> {
17708        let (to, step) = if double {
17709            // `::` so TO is implicit; STEP is whatever (if anything) follows.
17710            let step_v = self.parse_slice_optional_endpoint(is_hash)?;
17711            (None, step_v)
17712        } else {
17713            // single `:` — parse TO, then optional `:STEP`.
17714            let to_v = self.parse_slice_optional_endpoint(is_hash)?;
17715            let step_v = if matches!(self.peek(), Token::Colon) {
17716                self.advance();
17717                self.parse_slice_optional_endpoint(is_hash)?
17718            } else if matches!(self.peek(), Token::PackageSep) {
17719                return Err(
17720                    self.syntax_err("Unexpected `::` after slice TO endpoint".to_string(), line)
17721                );
17722            } else {
17723                None
17724            };
17725            (to_v, step_v)
17726        };
17727
17728        // Closed form (both endpoints present) — produce a regular `Range` so the
17729        // rest of the compiler/VM keeps reusing existing range-expansion paths.
17730        if let (Some(f), Some(t)) = (from.as_ref(), to.as_ref()) {
17731            return Ok(Expr {
17732                kind: ExprKind::Range {
17733                    from: f.clone(),
17734                    to: t.clone(),
17735                    exclusive: false,
17736                    step,
17737                },
17738                line,
17739            });
17740        }
17741
17742        Ok(Expr {
17743            kind: ExprKind::SliceRange { from, to, step },
17744            line,
17745        })
17746    }
17747
17748    /// Parse an optional slice endpoint: returns `None` if the next token closes the slice
17749    /// arg (`,`, `]`, `}`, or another `:`). Otherwise parses an endpoint expression.
17750    fn parse_slice_optional_endpoint(&mut self, is_hash: bool) -> PerlResult<Option<Box<Expr>>> {
17751        if matches!(
17752            self.peek(),
17753            Token::Colon
17754                | Token::PackageSep
17755                | Token::Comma
17756                | Token::RBracket
17757                | Token::RBrace
17758                | Token::Eof
17759        ) {
17760            return Ok(None);
17761        }
17762        self.suppress_colon_range = self.suppress_colon_range.saturating_add(1);
17763        let r = self.parse_slice_endpoint(is_hash);
17764        self.suppress_colon_range = self.suppress_colon_range.saturating_sub(1);
17765        Ok(Some(Box::new(r?)))
17766    }
17767
17768    /// Parse a single slice endpoint expression. For hash slices, a bareword `Ident`
17769    /// followed by `:`, `::`, `,`, `]`, or `}` auto-quotes (fat-comma style); otherwise
17770    /// fall through to standard expression parsing. For array slices, no auto-quote.
17771    fn parse_slice_endpoint(&mut self, is_hash: bool) -> PerlResult<Expr> {
17772        if is_hash {
17773            if let Token::Ident(name) = self.peek().clone() {
17774                if matches!(
17775                    self.peek_at(1),
17776                    Token::Colon
17777                        | Token::PackageSep
17778                        | Token::Comma
17779                        | Token::RBracket
17780                        | Token::RBrace
17781                ) {
17782                    let line = self.peek_line();
17783                    self.advance();
17784                    return Ok(Expr {
17785                        kind: ExprKind::String(name),
17786                        line,
17787                    });
17788                }
17789            }
17790        }
17791        self.parse_assign_expr()
17792    }
17793
17794    /// Arguments for `->name` / `->SUPER::name` **without** `(...)`. Unlike `die foo + 1`
17795    /// (unary `+` on `1` passed to `foo`), Perl treats `$o->meth + 5` as infix `+` after a
17796    /// no-arg method call; we must not consume that `+` as the start of a first argument.
17797    fn parse_method_arg_list_no_paren(&mut self) -> PerlResult<Vec<Expr>> {
17798        let mut args = Vec::new();
17799        let call_line = self.prev_line();
17800        loop {
17801            // `$g->next { ... }` — `{` starts the enclosing statement's block, not an anonymous
17802            // hash argument to `next` (paren-less method call has no args here).
17803            if args.is_empty() && matches!(self.peek(), Token::LBrace) {
17804                break;
17805            }
17806            if matches!(
17807                self.peek(),
17808                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
17809            ) {
17810                break;
17811            }
17812            if let Token::Ident(ref kw) = self.peek().clone() {
17813                if matches!(
17814                    kw.as_str(),
17815                    "if" | "unless" | "while" | "until" | "for" | "foreach"
17816                ) {
17817                    break;
17818                }
17819            }
17820            // `foo($obj->meth, $x)` — comma separates *outer* args; it is not the start of a
17821            // paren-less method argument (those use spaces: `$obj->meth $a, $b`).
17822            if args.is_empty()
17823                && (self.peek_method_arg_infix_terminator() || matches!(self.peek(), Token::Comma))
17824            {
17825                break;
17826            }
17827            // Implicit semicolon: if no args collected yet and next token is on a different
17828            // line, treat newline as statement boundary. Allows `$p->method\nnext_stmt`.
17829            if args.is_empty() && self.peek_line() > call_line {
17830                break;
17831            }
17832            args.push(self.parse_assign_expr()?);
17833            if !self.eat(&Token::Comma) {
17834                break;
17835            }
17836        }
17837        Ok(args)
17838    }
17839
17840    /// Tokens that end a paren-less method arg list when no comma-separated args yet (infix on
17841    /// the whole `->meth` expression).
17842    fn peek_method_arg_infix_terminator(&self) -> bool {
17843        matches!(
17844            self.peek(),
17845            Token::Plus
17846                | Token::Minus
17847                | Token::Star
17848                | Token::Slash
17849                | Token::Percent
17850                | Token::Power
17851                | Token::Dot
17852                | Token::X
17853                | Token::NumEq
17854                | Token::NumNe
17855                | Token::NumLt
17856                | Token::NumGt
17857                | Token::NumLe
17858                | Token::NumGe
17859                | Token::Spaceship
17860                | Token::StrEq
17861                | Token::StrNe
17862                | Token::StrLt
17863                | Token::StrGt
17864                | Token::StrLe
17865                | Token::StrGe
17866                | Token::StrCmp
17867                | Token::LogAnd
17868                | Token::LogOr
17869                | Token::LogAndWord
17870                | Token::LogOrWord
17871                | Token::DefinedOr
17872                | Token::BitAnd
17873                | Token::BitOr
17874                | Token::BitXor
17875                | Token::ShiftLeft
17876                | Token::ShiftRight
17877                | Token::Range
17878                | Token::RangeExclusive
17879                | Token::BindMatch
17880                | Token::BindNotMatch
17881                | Token::Arrow
17882                // `($a->b) ? $a->c : $a->d` — `->c` must not slurp the ternary `:` / `?`.
17883                | Token::Question
17884                | Token::Colon
17885                // Assignment operators: `$obj->field = val` is setter sugar, not method arg.
17886                | Token::Assign
17887                | Token::PlusAssign
17888                | Token::MinusAssign
17889                | Token::MulAssign
17890                | Token::DivAssign
17891                | Token::ModAssign
17892                | Token::PowAssign
17893                | Token::DotAssign
17894                | Token::AndAssign
17895                | Token::OrAssign
17896                | Token::XorAssign
17897                | Token::DefinedOrAssign
17898                | Token::ShiftLeftAssign
17899                | Token::ShiftRightAssign
17900                | Token::BitAndAssign
17901                | Token::BitOrAssign
17902        )
17903    }
17904
17905    fn parse_list_until_terminator(&mut self) -> PerlResult<Vec<Expr>> {
17906        let mut args = Vec::new();
17907        // Line of the last consumed token (the keyword / function name that
17908        // triggered this arg parse).  Used for implicit-semicolon: if no args
17909        // have been parsed yet and the next token is on a *different* line,
17910        // treat the newline as a statement boundary and stop.
17911        let call_line = self.prev_line();
17912        loop {
17913            if matches!(
17914                self.peek(),
17915                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
17916            ) {
17917                break;
17918            }
17919            // Check for postfix modifiers — stop before `expr for LIST` / `expr if COND` etc.
17920            if let Token::Ident(ref kw) = self.peek().clone() {
17921                if matches!(
17922                    kw.as_str(),
17923                    "if" | "unless" | "while" | "until" | "for" | "foreach"
17924                ) {
17925                    break;
17926                }
17927            }
17928            // Implicit semicolons: if no args have been collected yet and the
17929            // next token is on a different line from the call keyword, treat
17930            // the newline as a statement boundary.  This prevents paren-less
17931            // calls (`say`, `print`, user subs) from greedily swallowing the
17932            // *next* statement when the author omitted a semicolon.
17933            // After a comma continuation, multi-line arg lists still work.
17934            if args.is_empty() && self.peek_line() > call_line {
17935                break;
17936            }
17937            // Paren-less builtin args: `|>` terminates the whole call list, so
17938            // individual args must not absorb a following `|>`.
17939            args.push(self.parse_assign_expr_stop_at_pipe()?);
17940            if !self.eat(&Token::Comma) {
17941                break;
17942            }
17943        }
17944        Ok(args)
17945    }
17946
17947    /// Body of `+{ ... }` — Perl's force-hashref idiom. The opening `+` and `{`
17948    /// have already been consumed. Tries the normal `KEY => VAL, …` shape first
17949    /// (so `+{ a => 1, b => 2 }` is identical to `{ a => 1, b => 2 }`); on
17950    /// failure falls back to "single list-yielding expression treated as a
17951    /// flat key/value spread" so `+{ map { (k, v) } LIST }` works without
17952    /// the user needing a temp `my %h = ...; \%h` shuffle.
17953    fn parse_forced_hashref_body(&mut self, line: usize) -> PerlResult<Expr> {
17954        let saved = self.pos;
17955        if let Ok(pairs) = self.try_parse_hash_ref() {
17956            return Ok(Expr {
17957                kind: ExprKind::HashRef(pairs),
17958                line,
17959            });
17960        }
17961        // Empty `+{}` is the empty hashref.
17962        self.pos = saved;
17963        if matches!(self.peek(), Token::RBrace) {
17964            self.advance();
17965            return Ok(Expr {
17966                kind: ExprKind::HashRef(vec![]),
17967                line,
17968            });
17969        }
17970        // Single expression — eval as list, flatten into key/value pairs via the
17971        // existing __HASH_SPREAD__ sentinel that `ExprKind::HashRef` already
17972        // handles in [`Interpreter::eval_expr`].
17973        let inner = self.parse_expression()?;
17974        self.expect(&Token::RBrace)?;
17975        let sentinel_key = Expr {
17976            kind: ExprKind::String("__HASH_SPREAD__".into()),
17977            line,
17978        };
17979        Ok(Expr {
17980            kind: ExprKind::HashRef(vec![(sentinel_key, inner)]),
17981            line,
17982        })
17983    }
17984
17985    fn try_parse_hash_ref(&mut self) -> PerlResult<Vec<(Expr, Expr)>> {
17986        let mut pairs = Vec::new();
17987        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
17988            // Perl autoquotes a bareword immediately before `=>` (hash key), even for keywords like
17989            // `pos`, `bless`, `return` — see Text::Balanced `_failmsg` (`pos => $pos`).
17990            // Stryke exception: topic-slot barewords (`_`, `_<`, `_0`, `_0<`, `_!N!`, …)
17991            // resolve to the topic value, not the literal name — `{ _ => 1 }` ≡ `{ $_ => 1 }`.
17992            let line = self.peek_line();
17993            let key = if let Token::Ident(ref name) = self.peek().clone() {
17994                if matches!(self.peek_at(1), Token::FatArrow)
17995                    && !Self::is_underscore_topic_slot(name)
17996                {
17997                    self.advance();
17998                    Expr {
17999                        kind: ExprKind::String(name.clone()),
18000                        line,
18001                    }
18002                } else {
18003                    self.parse_assign_expr()?
18004                }
18005            } else {
18006                self.parse_assign_expr()?
18007            };
18008            // If the key expression is a hash/array variable and is followed by `}` or `,`
18009            // with no `=>`, treat the whole thing as a hash-from-expression construction.
18010            // This handles `{ %a }`, `{ %a, key => val }`, etc.
18011            if matches!(self.peek(), Token::RBrace | Token::Comma)
18012                && matches!(
18013                    key.kind,
18014                    ExprKind::HashVar(_)
18015                        | ExprKind::Deref {
18016                            kind: Sigil::Hash,
18017                            ..
18018                        }
18019                )
18020            {
18021                // Synthesize a pair whose key/value is spread from the hash expression.
18022                // Use a sentinel "spread" pair: key=the hash expr, value=undef.
18023                // The evaluator will flatten this.
18024                let sentinel_key = Expr {
18025                    kind: ExprKind::String("__HASH_SPREAD__".into()),
18026                    line,
18027                };
18028                pairs.push((sentinel_key, key));
18029                self.eat(&Token::Comma);
18030                continue;
18031            }
18032            // Expect => or , after key
18033            if self.eat(&Token::FatArrow) || self.eat(&Token::Comma) {
18034                let val = self.parse_assign_expr()?;
18035                pairs.push((key, val));
18036                self.eat(&Token::Comma);
18037            } else {
18038                return Err(self.syntax_err("Expected => or , in hash ref", key.line));
18039            }
18040        }
18041        self.expect(&Token::RBrace)?;
18042        Ok(pairs)
18043    }
18044
18045    /// Parse `key => val, key => val, ...` up to (but not consuming) `term`.
18046    /// Used by the `%[…]` and `%{k=>v,…}` sugar to build an inline hashref
18047    /// AST node, sidestepping the block/hashref ambiguity that `try_parse_hash_ref`
18048    /// navigates. Caller expects and consumes `term` itself.
18049    fn parse_hashref_pairs_until(&mut self, term: &Token) -> PerlResult<Vec<(Expr, Expr)>> {
18050        let mut pairs = Vec::new();
18051        while !matches!(&self.peek(), t if std::mem::discriminant(*t) == std::mem::discriminant(term))
18052            && !matches!(self.peek(), Token::Eof)
18053        {
18054            let line = self.peek_line();
18055            let key = if let Token::Ident(ref name) = self.peek().clone() {
18056                if matches!(self.peek_at(1), Token::FatArrow)
18057                    && !Self::is_underscore_topic_slot(name)
18058                {
18059                    self.advance();
18060                    Expr {
18061                        kind: ExprKind::String(name.clone()),
18062                        line,
18063                    }
18064                } else {
18065                    self.parse_assign_expr()?
18066                }
18067            } else {
18068                self.parse_assign_expr()?
18069            };
18070            if self.eat(&Token::FatArrow) || self.eat(&Token::Comma) {
18071                let val = self.parse_assign_expr()?;
18072                pairs.push((key, val));
18073                self.eat(&Token::Comma);
18074            } else {
18075                return Err(self.syntax_err("Expected => or , in hash ref", key.line));
18076            }
18077        }
18078        Ok(pairs)
18079    }
18080
18081    /// Inside an interpolated string, after a `$name`/`${EXPR}`/`$name[i]`/`$name{k}` base
18082    /// expression, consume any chain of `->[…]`, `->{…}`, **adjacent** `[…]`, or `{…}`
18083    /// subscripts. Perl auto-implies `->` between consecutive subscripts, so
18084    /// `$matrix[1][1]` is `$matrix[1]->[1]` and `$h{a}{b}` is `$h{a}->{b}`.
18085    /// Each step wraps the current expression in an `ArrowDeref`.
18086    fn interp_chain_subscripts(
18087        &self,
18088        chars: &[char],
18089        i: &mut usize,
18090        mut base: Expr,
18091        line: usize,
18092    ) -> Expr {
18093        loop {
18094            // Optional `->` connector
18095            let (after, requires_subscript) =
18096                if *i + 1 < chars.len() && chars[*i] == '-' && chars[*i + 1] == '>' {
18097                    (*i + 2, true)
18098                } else {
18099                    (*i, false)
18100                };
18101            if after >= chars.len() {
18102                break;
18103            }
18104            match chars[after] {
18105                '[' => {
18106                    *i = after + 1;
18107                    let mut idx_str = String::new();
18108                    while *i < chars.len() && chars[*i] != ']' {
18109                        idx_str.push(chars[*i]);
18110                        *i += 1;
18111                    }
18112                    if *i < chars.len() {
18113                        *i += 1;
18114                    }
18115                    let idx_expr = if let Some(rest) = idx_str.strip_prefix('$') {
18116                        Expr {
18117                            kind: ExprKind::ScalarVar(rest.to_string()),
18118                            line,
18119                        }
18120                    } else if let Ok(n) = idx_str.parse::<i64>() {
18121                        Expr {
18122                            kind: ExprKind::Integer(n),
18123                            line,
18124                        }
18125                    } else {
18126                        Expr {
18127                            kind: ExprKind::String(idx_str),
18128                            line,
18129                        }
18130                    };
18131                    base = Expr {
18132                        kind: ExprKind::ArrowDeref {
18133                            expr: Box::new(base),
18134                            index: Box::new(idx_expr),
18135                            kind: DerefKind::Array,
18136                        },
18137                        line,
18138                    };
18139                }
18140                '{' => {
18141                    *i = after + 1;
18142                    let mut key = String::new();
18143                    let mut depth = 1usize;
18144                    while *i < chars.len() && depth > 0 {
18145                        if chars[*i] == '{' {
18146                            depth += 1;
18147                        } else if chars[*i] == '}' {
18148                            depth -= 1;
18149                            if depth == 0 {
18150                                break;
18151                            }
18152                        }
18153                        key.push(chars[*i]);
18154                        *i += 1;
18155                    }
18156                    if *i < chars.len() {
18157                        *i += 1;
18158                    }
18159                    let key_expr = if let Some(rest) = key.strip_prefix('$') {
18160                        Expr {
18161                            kind: ExprKind::ScalarVar(rest.to_string()),
18162                            line,
18163                        }
18164                    } else {
18165                        Expr {
18166                            kind: ExprKind::String(key),
18167                            line,
18168                        }
18169                    };
18170                    base = Expr {
18171                        kind: ExprKind::ArrowDeref {
18172                            expr: Box::new(base),
18173                            index: Box::new(key_expr),
18174                            kind: DerefKind::Hash,
18175                        },
18176                        line,
18177                    };
18178                }
18179                _ => {
18180                    if requires_subscript {
18181                        // `->method()` etc — not interpolated, leave for literal output.
18182                    }
18183                    break;
18184                }
18185            }
18186        }
18187        base
18188    }
18189
18190    /// Reject `$a` / `$b` references in `--no-interop` mode (lexer catches them
18191    /// outside double-quoted strings; this catches the in-string interpolation
18192    /// path which has its own parser bypassing `Token::ScalarVar`).
18193    fn no_interop_check_scalar_var_name(&self, name: &str, line: usize) -> PerlResult<()> {
18194        if crate::no_interop_mode() && (name == "a" || name == "b") {
18195            return Err(self.syntax_err(
18196                format!(
18197                    "stryke uses `$_0` / `$_1` instead of `${}` (--no-interop is active)",
18198                    name
18199                ),
18200                line,
18201            ));
18202        }
18203        Ok(())
18204    }
18205
18206    fn parse_interpolated_string(&self, s: &str, line: usize) -> PerlResult<Expr> {
18207        // Parse $var and @var inside double-quoted strings
18208        let mut parts = Vec::new();
18209        let mut literal = String::new();
18210        let chars: Vec<char> = s.chars().collect();
18211        let mut i = 0;
18212
18213        'istr: while i < chars.len() {
18214            if chars[i] == LITERAL_DOLLAR_IN_DQUOTE {
18215                literal.push('$');
18216                i += 1;
18217                continue;
18218            }
18219            if chars[i] == LITERAL_AT_IN_DQUOTE {
18220                literal.push('@');
18221                i += 1;
18222                continue;
18223            }
18224            // "\\$x" in source: one backslash in the string, then interpolate $x (Perl double-quoted string).
18225            if chars[i] == '\\' && i + 1 < chars.len() && chars[i + 1] == '$' {
18226                literal.push('\\');
18227                i += 1;
18228                // i now points at '$' — fall through to $ handling below
18229            }
18230            if chars[i] == '$' && i + 1 < chars.len() {
18231                if !literal.is_empty() {
18232                    parts.push(StringPart::Literal(std::mem::take(&mut literal)));
18233                }
18234                i += 1; // past `$`
18235                        // Perl allows whitespace between `$` and the variable name (`$ foo` → `$foo`).
18236                while i < chars.len() && chars[i].is_whitespace() {
18237                    i += 1;
18238                }
18239                if i >= chars.len() {
18240                    return Err(self.syntax_err("Final $ should be \\$ or $name", line));
18241                }
18242                // `$#name` — last index of `@name` (Perl `$#array`).
18243                if chars[i] == '#' {
18244                    i += 1;
18245                    let mut sname = String::from("#");
18246                    while i < chars.len()
18247                        && (chars[i].is_alphanumeric() || chars[i] == '_' || chars[i] == ':')
18248                    {
18249                        sname.push(chars[i]);
18250                        i += 1;
18251                    }
18252                    while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
18253                        sname.push_str("::");
18254                        i += 2;
18255                        while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
18256                            sname.push(chars[i]);
18257                            i += 1;
18258                        }
18259                    }
18260                    self.no_interop_check_scalar_var_name(&sname, line)?;
18261                    parts.push(StringPart::ScalarVar(sname));
18262                    continue;
18263                }
18264                // `$$` — process id (Perl `$$`), only when the two `$` are adjacent (no whitespace
18265                // between) and the second `$` is not followed by a word character or digit (`$$x`
18266                // / `$$_` / `$$0` are `$` + `$x` / `$_` / `$0`).
18267                if chars[i] == '$' {
18268                    let next_c = chars.get(i + 1).copied();
18269                    let is_pid = match next_c {
18270                        None => true,
18271                        Some(c)
18272                            if !c.is_ascii_digit() && !matches!(c, 'A'..='Z' | 'a'..='z' | '_') =>
18273                        {
18274                            true
18275                        }
18276                        _ => false,
18277                    };
18278                    if is_pid {
18279                        parts.push(StringPart::ScalarVar("$$".to_string()));
18280                        i += 1; // consume second `$`
18281                        continue;
18282                    }
18283                    i += 1; // skip second `$` — same as a single `$` before the identifier
18284                }
18285                if chars[i] == '{' {
18286                    // `${…}` — braced variable OR expression interpolation.
18287                    //   `${name}`              → ScalarVar(name)        (Perl standard)
18288                    //   `${$ref}` / `${\EXPR}` → deref the expression   (Perl standard)
18289                    //   `${name}[idx]` / `${name}{k}` / `${$r}[i]` …    chain after `}`
18290                    // stryke's prior `#{expr}` form remains supported elsewhere.
18291                    i += 1;
18292                    let mut inner = String::new();
18293                    let mut depth = 1usize;
18294                    while i < chars.len() && depth > 0 {
18295                        match chars[i] {
18296                            '{' => depth += 1,
18297                            '}' => {
18298                                depth -= 1;
18299                                if depth == 0 {
18300                                    break;
18301                                }
18302                            }
18303                            _ => {}
18304                        }
18305                        inner.push(chars[i]);
18306                        i += 1;
18307                    }
18308                    if i < chars.len() {
18309                        i += 1; // skip closing }
18310                    }
18311
18312                    // Distinguish "name" from "expression". If trimmed inner starts with
18313                    // `$`, `\`, or contains operator/punctuation chars, treat as Perl
18314                    // expression and emit a scalar deref. Otherwise, plain variable name.
18315                    let trimmed = inner.trim();
18316                    let is_expr = trimmed.starts_with('$')
18317                        || trimmed.starts_with('\\')
18318                        || trimmed.starts_with('@')   // `${@arr}` rare but valid
18319                        || trimmed.starts_with('%')   // `${%h}`   rare but valid
18320                        || trimmed.contains(['(', '+', '-', '*', '/', '.', '?', '&', '|']);
18321                    let mut base: Expr = if is_expr {
18322                        // Re-parse the inner content as a Perl expression. Wrap in
18323                        // `Deref { kind: Sigil::Scalar }` to dereference the resulting
18324                        // scalar reference (Perl: `${$r}` ≡ `$$r`).
18325                        match parse_expression_from_str(trimmed, "<interp>") {
18326                            Ok(e) => Expr {
18327                                kind: ExprKind::Deref {
18328                                    expr: Box::new(e),
18329                                    kind: Sigil::Scalar,
18330                                },
18331                                line,
18332                            },
18333                            Err(_) => Expr {
18334                                kind: ExprKind::ScalarVar(inner.clone()),
18335                                line,
18336                            },
18337                        }
18338                    } else {
18339                        // Treat as a plain (possibly qualified) variable name.
18340                        self.no_interop_check_scalar_var_name(&inner, line)?;
18341                        Expr {
18342                            kind: ExprKind::ScalarVar(inner),
18343                            line,
18344                        }
18345                    };
18346
18347                    // After `${…}` we may see `[idx]` / `{key}` for indexing into the
18348                    // dereferenced array/hash (`${$ar}[1]`, `${$hr}{k}`), and arrow
18349                    // chains thereafter.
18350                    base = self.interp_chain_subscripts(&chars, &mut i, base, line);
18351                    parts.push(StringPart::Expr(base));
18352                } else if chars[i] == '^' {
18353                    // `$^V`, `$^O`, … — name stored as `^V`, `^O`, … (see [`Interpreter::get_special_var`]).
18354                    let mut name = String::from("^");
18355                    i += 1;
18356                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
18357                        name.push(chars[i]);
18358                        i += 1;
18359                    }
18360                    if i < chars.len() && chars[i] == '{' {
18361                        i += 1; // skip {
18362                        let mut key = String::new();
18363                        let mut depth = 1;
18364                        while i < chars.len() && depth > 0 {
18365                            if chars[i] == '{' {
18366                                depth += 1;
18367                            } else if chars[i] == '}' {
18368                                depth -= 1;
18369                                if depth == 0 {
18370                                    break;
18371                                }
18372                            }
18373                            key.push(chars[i]);
18374                            i += 1;
18375                        }
18376                        if i < chars.len() {
18377                            i += 1;
18378                        }
18379                        let key_expr = if let Some(rest) = key.strip_prefix('$') {
18380                            Expr {
18381                                kind: ExprKind::ScalarVar(rest.to_string()),
18382                                line,
18383                            }
18384                        } else {
18385                            Expr {
18386                                kind: ExprKind::String(key),
18387                                line,
18388                            }
18389                        };
18390                        parts.push(StringPart::Expr(Expr {
18391                            kind: ExprKind::HashElement {
18392                                hash: name,
18393                                key: Box::new(key_expr),
18394                            },
18395                            line,
18396                        }));
18397                    } else if i < chars.len() && chars[i] == '[' {
18398                        i += 1;
18399                        let mut idx_str = String::new();
18400                        while i < chars.len() && chars[i] != ']' {
18401                            idx_str.push(chars[i]);
18402                            i += 1;
18403                        }
18404                        if i < chars.len() {
18405                            i += 1;
18406                        }
18407                        let idx_expr = if let Some(rest) = idx_str.strip_prefix('$') {
18408                            Expr {
18409                                kind: ExprKind::ScalarVar(rest.to_string()),
18410                                line,
18411                            }
18412                        } else if let Ok(n) = idx_str.parse::<i64>() {
18413                            Expr {
18414                                kind: ExprKind::Integer(n),
18415                                line,
18416                            }
18417                        } else {
18418                            Expr {
18419                                kind: ExprKind::String(idx_str),
18420                                line,
18421                            }
18422                        };
18423                        parts.push(StringPart::Expr(Expr {
18424                            kind: ExprKind::ArrayElement {
18425                                array: name,
18426                                index: Box::new(idx_expr),
18427                            },
18428                            line,
18429                        }));
18430                    } else {
18431                        self.no_interop_check_scalar_var_name(&name, line)?;
18432                        parts.push(StringPart::ScalarVar(name));
18433                    }
18434                } else if chars[i].is_alphabetic() || chars[i] == '_' {
18435                    let mut name = String::new();
18436                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
18437                        name.push(chars[i]);
18438                        i += 1;
18439                    }
18440                    // Package-qualified names: `$Foo::x`, `$Foo::Bar::baz`. Mirror
18441                    // the `$#Foo::a` continuation logic. Without this, `"$Foo::x"`
18442                    // captures only `Foo` and leaves `::x` as literal text — the
18443                    // interpolation reads bare `$Foo`, which is undef.
18444                    while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
18445                        name.push_str("::");
18446                        i += 2;
18447                        while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
18448                            name.push(chars[i]);
18449                            i += 1;
18450                        }
18451                    }
18452                    // `$_<`, `$_<<`, … — outer topic (stryke extension). Also
18453                    // `$_N<`, `$_N<<` for positional aliases. And the indexed
18454                    // shortcut `$_<N` ≡ `$_<<<...<` (N chevrons), so `"$_<3"`
18455                    // and `"$_<<<"` interpolate identically.
18456                    let is_topic_slot = name == "_"
18457                        || (name.len() > 1
18458                            && name.starts_with('_')
18459                            && name[1..].bytes().all(|b| b.is_ascii_digit()));
18460                    if is_topic_slot {
18461                        // Try indexed-ascent first: `<` immediately followed by digits.
18462                        let try_indexed = chars.get(i) == Some(&'<')
18463                            && chars.get(i + 1).is_some_and(|c| c.is_ascii_digit());
18464                        let mut handled_indexed = false;
18465                        if try_indexed {
18466                            let mut j = i + 1;
18467                            while j < chars.len() && chars[j].is_ascii_digit() {
18468                                j += 1;
18469                            }
18470                            let digits: String = chars[i + 1..j].iter().collect();
18471                            if let Ok(n) = digits.parse::<usize>() {
18472                                if n >= 1 {
18473                                    for _ in 0..n {
18474                                        name.push('<');
18475                                    }
18476                                    i = j;
18477                                    handled_indexed = true;
18478                                }
18479                            }
18480                        }
18481                        if !handled_indexed {
18482                            while i < chars.len() && chars[i] == '<' {
18483                                name.push('<');
18484                                i += 1;
18485                            }
18486                        }
18487                    }
18488                    // `--no-interop`: `$a` / `$b` are Perl-isms; reject inside
18489                    // string interpolation too. Catches both `"$a"` and `"$a[0]"`
18490                    // / `"$a{k}"` / `"$a->[0]"` because every branch below uses
18491                    // `name` to build the expression.
18492                    self.no_interop_check_scalar_var_name(&name, line)?;
18493                    // Build the base expression, then thread arrow-deref chains
18494                    // (`->[…]` / `->{…}`) onto it so things like `$ar->[2]`,
18495                    // `$href->{k}`, and chained `$x->{a}[1]->{b}` interpolate
18496                    // correctly inside double-quoted strings (Perl convention).
18497                    let mut base = if i < chars.len() && chars[i] == '{' {
18498                        // $hash{key}
18499                        i += 1; // skip {
18500                        let mut key = String::new();
18501                        let mut depth = 1;
18502                        while i < chars.len() && depth > 0 {
18503                            if chars[i] == '{' {
18504                                depth += 1;
18505                            } else if chars[i] == '}' {
18506                                depth -= 1;
18507                                if depth == 0 {
18508                                    break;
18509                                }
18510                            }
18511                            key.push(chars[i]);
18512                            i += 1;
18513                        }
18514                        if i < chars.len() {
18515                            i += 1;
18516                        } // skip }
18517                        let key_expr = if let Some(rest) = key.strip_prefix('$') {
18518                            Expr {
18519                                kind: ExprKind::ScalarVar(rest.to_string()),
18520                                line,
18521                            }
18522                        } else {
18523                            Expr {
18524                                kind: ExprKind::String(key),
18525                                line,
18526                            }
18527                        };
18528                        Expr {
18529                            kind: ExprKind::HashElement {
18530                                hash: name,
18531                                key: Box::new(key_expr),
18532                            },
18533                            line,
18534                        }
18535                    } else if i < chars.len() && chars[i] == '[' {
18536                        // $array[idx]
18537                        i += 1;
18538                        let mut idx_str = String::new();
18539                        while i < chars.len() && chars[i] != ']' {
18540                            idx_str.push(chars[i]);
18541                            i += 1;
18542                        }
18543                        if i < chars.len() {
18544                            i += 1;
18545                        }
18546                        let idx_expr = if let Some(rest) = idx_str.strip_prefix('$') {
18547                            Expr {
18548                                kind: ExprKind::ScalarVar(rest.to_string()),
18549                                line,
18550                            }
18551                        } else if let Ok(n) = idx_str.parse::<i64>() {
18552                            Expr {
18553                                kind: ExprKind::Integer(n),
18554                                line,
18555                            }
18556                        } else {
18557                            Expr {
18558                                kind: ExprKind::String(idx_str),
18559                                line,
18560                            }
18561                        };
18562                        Expr {
18563                            kind: ExprKind::ArrayElement {
18564                                array: name,
18565                                index: Box::new(idx_expr),
18566                            },
18567                            line,
18568                        }
18569                    } else {
18570                        // Bare $name — defer to the chain-extension loop below.
18571                        Expr {
18572                            kind: ExprKind::ScalarVar(name),
18573                            line,
18574                        }
18575                    };
18576
18577                    // Chain `->[…]` / `->{…}` AND adjacent `[…]` / `{…}` — Perl
18578                    // implies `->` between consecutive subscripts (`$m[1][2]`
18579                    // ≡ `$m[1]->[2]`).  See `interp_chain_subscripts`.
18580                    base = self.interp_chain_subscripts(&chars, &mut i, base, line);
18581                    parts.push(StringPart::Expr(base));
18582                } else if chars[i].is_ascii_digit() {
18583                    // $0 (program name), $1…$n (regexp captures). Perl disallows $01, $02, …
18584                    if chars[i] == '0' {
18585                        i += 1;
18586                        if i < chars.len() && chars[i].is_ascii_digit() {
18587                            return Err(self.syntax_err(
18588                                "Numeric variables with more than one digit may not start with '0'",
18589                                line,
18590                            ));
18591                        }
18592                        parts.push(StringPart::ScalarVar("0".into()));
18593                    } else {
18594                        let start = i;
18595                        while i < chars.len() && chars[i].is_ascii_digit() {
18596                            i += 1;
18597                        }
18598                        parts.push(StringPart::ScalarVar(chars[start..i].iter().collect()));
18599                    }
18600                } else {
18601                    let c = chars[i];
18602                    let probe = c.to_string();
18603                    // `&` is the regex-match special var — semantically symmetric with
18604                    // backtick (`$``) prematch and apostrophe (`$'`) postmatch which
18605                    // are already handled here. `is_special_scalar_name_for_get` doesn't
18606                    // currently list `&`/`'`/`` ` `` (those have separate runtime paths
18607                    // for set/clear under regex updates), so we add them inline.
18608                    if VMHelper::is_special_scalar_name_for_get(&probe)
18609                        || matches!(c, '\'' | '`' | '&')
18610                    {
18611                        i += 1;
18612                        // Check for hash element access: `$+{key}`, `$-{key}`, etc.
18613                        if i < chars.len() && chars[i] == '{' {
18614                            i += 1; // skip {
18615                            let mut key = String::new();
18616                            let mut depth = 1;
18617                            while i < chars.len() && depth > 0 {
18618                                if chars[i] == '{' {
18619                                    depth += 1;
18620                                } else if chars[i] == '}' {
18621                                    depth -= 1;
18622                                    if depth == 0 {
18623                                        break;
18624                                    }
18625                                }
18626                                key.push(chars[i]);
18627                                i += 1;
18628                            }
18629                            if i < chars.len() {
18630                                i += 1;
18631                            } // skip }
18632                            let key_expr = if let Some(rest) = key.strip_prefix('$') {
18633                                Expr {
18634                                    kind: ExprKind::ScalarVar(rest.to_string()),
18635                                    line,
18636                                }
18637                            } else {
18638                                Expr {
18639                                    kind: ExprKind::String(key),
18640                                    line,
18641                                }
18642                            };
18643                            let mut base = Expr {
18644                                kind: ExprKind::HashElement {
18645                                    hash: probe,
18646                                    key: Box::new(key_expr),
18647                                },
18648                                line,
18649                            };
18650                            base = self.interp_chain_subscripts(&chars, &mut i, base, line);
18651                            parts.push(StringPart::Expr(base));
18652                        } else {
18653                            // Check for arrow deref chain: `$@->{key}`, etc.
18654                            let mut base = Expr {
18655                                kind: ExprKind::ScalarVar(probe),
18656                                line,
18657                            };
18658                            base = self.interp_chain_subscripts(&chars, &mut i, base, line);
18659                            if matches!(base.kind, ExprKind::ScalarVar(_)) {
18660                                // No chain extension — use the simpler ScalarVar part
18661                                if let ExprKind::ScalarVar(name) = base.kind {
18662                                    self.no_interop_check_scalar_var_name(&name, line)?;
18663                                    parts.push(StringPart::ScalarVar(name));
18664                                }
18665                            } else {
18666                                parts.push(StringPart::Expr(base));
18667                            }
18668                        }
18669                    } else {
18670                        literal.push('$');
18671                        literal.push(c);
18672                        i += 1;
18673                    }
18674                }
18675            } else if chars[i] == '@' && i + 1 < chars.len() {
18676                let next = chars[i + 1];
18677                // `@$aref` / `@${expr}` — array dereference in interpolation (Perl `"@$r"` → elements of @$r).
18678                if next == '$' {
18679                    if !literal.is_empty() {
18680                        parts.push(StringPart::Literal(std::mem::take(&mut literal)));
18681                    }
18682                    i += 1; // past `@`
18683                    debug_assert_eq!(chars[i], '$');
18684                    i += 1; // past `$`
18685                    while i < chars.len() && chars[i].is_whitespace() {
18686                        i += 1;
18687                    }
18688                    if i >= chars.len() {
18689                        return Err(self.syntax_err(
18690                            "Expected variable or block after `@$` in double-quoted string",
18691                            line,
18692                        ));
18693                    }
18694                    let inner_expr = if chars[i] == '{' {
18695                        i += 1;
18696                        let start = i;
18697                        let mut depth = 1usize;
18698                        while i < chars.len() && depth > 0 {
18699                            match chars[i] {
18700                                '{' => depth += 1,
18701                                '}' => {
18702                                    depth -= 1;
18703                                    if depth == 0 {
18704                                        break;
18705                                    }
18706                                }
18707                                _ => {}
18708                            }
18709                            i += 1;
18710                        }
18711                        if depth != 0 {
18712                            return Err(self.syntax_err(
18713                                "Unterminated `${ ... }` after `@` in double-quoted string",
18714                                line,
18715                            ));
18716                        }
18717                        let inner: String = chars[start..i].iter().collect();
18718                        i += 1; // closing `}`
18719                        parse_expression_from_str(inner.trim(), "-e")?
18720                    } else {
18721                        let mut name = String::new();
18722                        if chars[i] == '^' {
18723                            name.push('^');
18724                            i += 1;
18725                            while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_')
18726                            {
18727                                name.push(chars[i]);
18728                                i += 1;
18729                            }
18730                        } else {
18731                            while i < chars.len()
18732                                && (chars[i].is_alphanumeric()
18733                                    || chars[i] == '_'
18734                                    || chars[i] == ':')
18735                            {
18736                                name.push(chars[i]);
18737                                i += 1;
18738                            }
18739                            while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
18740                                name.push_str("::");
18741                                i += 2;
18742                                while i < chars.len()
18743                                    && (chars[i].is_alphanumeric() || chars[i] == '_')
18744                                {
18745                                    name.push(chars[i]);
18746                                    i += 1;
18747                                }
18748                            }
18749                        }
18750                        if name.is_empty() {
18751                            return Err(self.syntax_err(
18752                                "Expected identifier after `@$` in double-quoted string",
18753                                line,
18754                            ));
18755                        }
18756                        Expr {
18757                            kind: ExprKind::ScalarVar(name),
18758                            line,
18759                        }
18760                    };
18761                    parts.push(StringPart::Expr(Expr {
18762                        kind: ExprKind::Deref {
18763                            expr: Box::new(inner_expr),
18764                            kind: Sigil::Array,
18765                        },
18766                        line,
18767                    }));
18768                    continue 'istr;
18769                }
18770                if next == '{' {
18771                    if !literal.is_empty() {
18772                        parts.push(StringPart::Literal(std::mem::take(&mut literal)));
18773                    }
18774                    i += 2; // `@{`
18775                    let start = i;
18776                    let mut depth = 1usize;
18777                    while i < chars.len() && depth > 0 {
18778                        match chars[i] {
18779                            '{' => depth += 1,
18780                            '}' => {
18781                                depth -= 1;
18782                                if depth == 0 {
18783                                    break;
18784                                }
18785                            }
18786                            _ => {}
18787                        }
18788                        i += 1;
18789                    }
18790                    if depth != 0 {
18791                        return Err(
18792                            self.syntax_err("Unterminated @{ ... } in double-quoted string", line)
18793                        );
18794                    }
18795                    let inner: String = chars[start..i].iter().collect();
18796                    i += 1; // closing `}`
18797                    let inner_expr = parse_expression_from_str(inner.trim(), "-e")?;
18798                    parts.push(StringPart::Expr(Expr {
18799                        kind: ExprKind::Deref {
18800                            expr: Box::new(inner_expr),
18801                            kind: Sigil::Array,
18802                        },
18803                        line,
18804                    }));
18805                    continue 'istr;
18806                }
18807                if !(next.is_alphabetic() || next == '_' || next == '+' || next == '-') {
18808                    literal.push(chars[i]);
18809                    i += 1;
18810                } else {
18811                    if !literal.is_empty() {
18812                        parts.push(StringPart::Literal(std::mem::take(&mut literal)));
18813                    }
18814                    i += 1;
18815                    let mut name = String::new();
18816                    if i < chars.len() && (chars[i] == '+' || chars[i] == '-') {
18817                        name.push(chars[i]);
18818                        i += 1;
18819                    } else {
18820                        while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
18821                            name.push(chars[i]);
18822                            i += 1;
18823                        }
18824                        while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
18825                            name.push_str("::");
18826                            i += 2;
18827                            while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_')
18828                            {
18829                                name.push(chars[i]);
18830                                i += 1;
18831                            }
18832                        }
18833                    }
18834                    if i < chars.len() && chars[i] == '[' {
18835                        i += 1;
18836                        let start_inner = i;
18837                        let mut depth = 1usize;
18838                        while i < chars.len() && depth > 0 {
18839                            match chars[i] {
18840                                '[' => depth += 1,
18841                                ']' => depth -= 1,
18842                                _ => {}
18843                            }
18844                            if depth == 0 {
18845                                let inner: String = chars[start_inner..i].iter().collect();
18846                                i += 1; // closing ]
18847                                let indices = parse_slice_indices_from_str(inner.trim(), "-e")?;
18848                                parts.push(StringPart::Expr(Expr {
18849                                    kind: ExprKind::ArraySlice {
18850                                        array: name.clone(),
18851                                        indices,
18852                                    },
18853                                    line,
18854                                }));
18855                                continue 'istr;
18856                            }
18857                            i += 1;
18858                        }
18859                        return Err(self.syntax_err(
18860                            "Unterminated [ in array slice inside quoted string",
18861                            line,
18862                        ));
18863                    }
18864                    parts.push(StringPart::ArrayVar(name));
18865                }
18866            } else if chars[i] == '#'
18867                && i + 1 < chars.len()
18868                && chars[i + 1] == '{'
18869                && !crate::compat_mode()
18870            {
18871                // #{expr} — Ruby-style expression interpolation (stryke extension).
18872                if !literal.is_empty() {
18873                    parts.push(StringPart::Literal(std::mem::take(&mut literal)));
18874                }
18875                i += 2; // skip `#{`
18876                let mut inner = String::new();
18877                let mut depth = 1usize;
18878                while i < chars.len() && depth > 0 {
18879                    match chars[i] {
18880                        '{' => depth += 1,
18881                        '}' => {
18882                            depth -= 1;
18883                            if depth == 0 {
18884                                break;
18885                            }
18886                        }
18887                        _ => {}
18888                    }
18889                    inner.push(chars[i]);
18890                    i += 1;
18891                }
18892                if i < chars.len() {
18893                    i += 1; // skip closing `}`
18894                }
18895                let expr = parse_block_from_str(inner.trim(), "-e", line)?;
18896                parts.push(StringPart::Expr(expr));
18897            } else {
18898                literal.push(chars[i]);
18899                i += 1;
18900            }
18901        }
18902        if !literal.is_empty() {
18903            parts.push(StringPart::Literal(literal));
18904        }
18905
18906        if parts.len() == 1 {
18907            if let StringPart::Literal(s) = &parts[0] {
18908                return Ok(Expr {
18909                    kind: ExprKind::String(s.clone()),
18910                    line,
18911                });
18912            }
18913        }
18914        if parts.is_empty() {
18915            return Ok(Expr {
18916                kind: ExprKind::String(String::new()),
18917                line,
18918            });
18919        }
18920
18921        Ok(Expr {
18922            kind: ExprKind::InterpolatedString(parts),
18923            line,
18924        })
18925    }
18926
18927    fn expr_to_overload_key(&self, e: &Expr) -> PerlResult<String> {
18928        match &e.kind {
18929            ExprKind::String(s) => Ok(s.clone()),
18930            _ => Err(self.syntax_err(
18931                "overload key must be a string literal (e.g. '\"\"' or '+')",
18932                e.line,
18933            )),
18934        }
18935    }
18936
18937    fn expr_to_overload_sub(&mut self, e: &Expr) -> PerlResult<String> {
18938        match &e.kind {
18939            ExprKind::String(s) => Ok(s.clone()),
18940            ExprKind::Integer(n) => Ok(n.to_string()),
18941            ExprKind::SubroutineRef(s) | ExprKind::SubroutineCodeRef(s) => Ok(s.clone()),
18942            // Anonymous sub: `use overload "+" => sub { ... };` — promote the
18943            // anon body into a synthetic top-level SubDecl so the overload
18944            // table can hold the name like the named-sub case. (PARITY-012)
18945            ExprKind::CodeRef { params, body } => {
18946                let id = self.next_overload_anon_id;
18947                self.next_overload_anon_id = self.next_overload_anon_id.saturating_add(1);
18948                let name = format!("__overload_anon_{}", id);
18949                self.pending_synthetic_subs.push(Statement {
18950                    label: None,
18951                    kind: StmtKind::SubDecl {
18952                        name: name.clone(),
18953                        params: params.clone(),
18954                        body: body.clone(),
18955                        prototype: None,
18956                    },
18957                    line: e.line,
18958                });
18959                Ok(name)
18960            }
18961            _ => Err(self.syntax_err(
18962                "overload handler must be a string literal, number (e.g. fallback => 1), or \\&subname (method in current package)",
18963                e.line,
18964            )),
18965        }
18966    }
18967}
18968
18969fn merge_expr_list(parts: Vec<Expr>) -> Expr {
18970    if parts.len() == 1 {
18971        parts.into_iter().next().unwrap()
18972    } else {
18973        let line = parts.first().map(|e| e.line).unwrap_or(0);
18974        Expr {
18975            kind: ExprKind::List(parts),
18976            line,
18977        }
18978    }
18979}
18980
18981/// Parse a single expression from `s` (e.g. contents of `@{ ... }` inside a double-quoted string).
18982pub fn parse_expression_from_str(s: &str, file: &str) -> PerlResult<Expr> {
18983    let mut lexer = Lexer::new_with_file(s, file);
18984    let tokens = lexer.tokenize()?;
18985    let mut parser = Parser::new_with_file(tokens, file);
18986    let e = parser.parse_expression()?;
18987    if !parser.at_eof() {
18988        return Err(parser.syntax_err(
18989            "Extra tokens in embedded string expression",
18990            parser.peek_line(),
18991        ));
18992    }
18993    Ok(e)
18994}
18995
18996/// Parse a statement list from `s` and wrap as `do { ... }` (for `#{...}` interpolation).
18997pub fn parse_block_from_str(s: &str, file: &str, line: usize) -> PerlResult<Expr> {
18998    let mut lexer = Lexer::new_with_file(s, file);
18999    let tokens = lexer.tokenize()?;
19000    let mut parser = Parser::new_with_file(tokens, file);
19001    let stmts = parser.parse_statements()?;
19002    let inner_line = stmts.first().map(|st| st.line).unwrap_or(line);
19003    let inner = Expr {
19004        kind: ExprKind::CodeRef {
19005            params: vec![],
19006            body: stmts,
19007        },
19008        line: inner_line,
19009    };
19010    Ok(Expr {
19011        kind: ExprKind::Do(Box::new(inner)),
19012        line,
19013    })
19014}
19015
19016/// Comma-separated expressions on a `format` value line (below a picture line).
19017/// Parse `[ ... ]` contents for `@a[...]` (same rules as `parse_arg_list` / comma-separated indices).
19018pub fn parse_slice_indices_from_str(s: &str, file: &str) -> PerlResult<Vec<Expr>> {
19019    let mut lexer = Lexer::new_with_file(s, file);
19020    let tokens = lexer.tokenize()?;
19021    let mut parser = Parser::new_with_file(tokens, file);
19022    parser.parse_arg_list()
19023}
19024
19025pub fn parse_format_value_line(line: &str) -> PerlResult<Vec<Expr>> {
19026    let trimmed = line.trim();
19027    if trimmed.is_empty() {
19028        return Ok(vec![]);
19029    }
19030    let mut lexer = Lexer::new(trimmed);
19031    let tokens = lexer.tokenize()?;
19032    let mut parser = Parser::new(tokens);
19033    let mut exprs = Vec::new();
19034    loop {
19035        if parser.at_eof() {
19036            break;
19037        }
19038        // Assignment-level expressions so `a, b` yields two fields (not one comma list).
19039        exprs.push(parser.parse_assign_expr()?);
19040        if parser.eat(&Token::Comma) {
19041            continue;
19042        }
19043        if !parser.at_eof() {
19044            return Err(parser.syntax_err("Extra tokens in format value line", parser.peek_line()));
19045        }
19046        break;
19047    }
19048    Ok(exprs)
19049}
19050
19051#[cfg(test)]
19052mod tests {
19053    use super::*;
19054
19055    fn parse_ok(code: &str) -> Program {
19056        let mut lexer = Lexer::new(code);
19057        let tokens = lexer.tokenize().expect("tokenize");
19058        let mut parser = Parser::new(tokens);
19059        parser.parse_program().expect("parse")
19060    }
19061
19062    fn parse_err(code: &str) -> String {
19063        let mut lexer = Lexer::new(code);
19064        let tokens = match lexer.tokenize() {
19065            Ok(t) => t,
19066            Err(e) => return e.message,
19067        };
19068        let mut parser = Parser::new(tokens);
19069        parser.parse_program().unwrap_err().message
19070    }
19071
19072    #[test]
19073    fn parse_empty_program() {
19074        let p = parse_ok("");
19075        assert!(p.statements.is_empty());
19076    }
19077
19078    #[test]
19079    fn parse_semicolons_only() {
19080        let p = parse_ok(";;");
19081        assert!(p.statements.len() <= 3);
19082    }
19083
19084    #[test]
19085    fn parse_simple_scalar_assignment() {
19086        let p = parse_ok("$x = 1");
19087        assert_eq!(p.statements.len(), 1);
19088    }
19089
19090    #[test]
19091    fn parse_simple_array_assignment() {
19092        let p = parse_ok("@arr = (1, 2, 3)");
19093        assert_eq!(p.statements.len(), 1);
19094    }
19095
19096    #[test]
19097    fn parse_simple_hash_assignment() {
19098        let p = parse_ok("%h = (a => 1, b => 2)");
19099        assert_eq!(p.statements.len(), 1);
19100    }
19101
19102    #[test]
19103    fn parse_subroutine_decl() {
19104        let p = parse_ok("fn foo { 1 }");
19105        assert_eq!(p.statements.len(), 1);
19106        match &p.statements[0].kind {
19107            StmtKind::SubDecl { name, .. } => assert_eq!(name, "foo"),
19108            _ => panic!("expected SubDecl"),
19109        }
19110    }
19111
19112    #[test]
19113    fn parse_subroutine_with_prototype() {
19114        let p = parse_ok("fn foo ($$) { 1 }");
19115        assert_eq!(p.statements.len(), 1);
19116        match &p.statements[0].kind {
19117            StmtKind::SubDecl { prototype, .. } => {
19118                assert!(prototype.is_some());
19119            }
19120            _ => panic!("expected SubDecl"),
19121        }
19122    }
19123
19124    #[test]
19125    fn parse_anonymous_fn() {
19126        let p = parse_ok("my $f = fn { 1 }");
19127        assert_eq!(p.statements.len(), 1);
19128    }
19129
19130    #[test]
19131    fn parse_if_statement() {
19132        let p = parse_ok("if (1) { 2 }");
19133        assert_eq!(p.statements.len(), 1);
19134        matches!(&p.statements[0].kind, StmtKind::If { .. });
19135    }
19136
19137    #[test]
19138    fn parse_if_elsif_else() {
19139        let p = parse_ok("if (0) { 1 } elsif (1) { 2 } else { 3 }");
19140        assert_eq!(p.statements.len(), 1);
19141    }
19142
19143    #[test]
19144    fn parse_unless_statement() {
19145        let p = parse_ok("unless (0) { 1 }");
19146        assert_eq!(p.statements.len(), 1);
19147    }
19148
19149    #[test]
19150    fn parse_while_loop() {
19151        let p = parse_ok("while ($x) { $x-- }");
19152        assert_eq!(p.statements.len(), 1);
19153    }
19154
19155    #[test]
19156    fn parse_until_loop() {
19157        let p = parse_ok("until ($x) { $x++ }");
19158        assert_eq!(p.statements.len(), 1);
19159    }
19160
19161    #[test]
19162    fn parse_for_c_style() {
19163        let p = parse_ok("for (my $i=0; $i<10; $i++) { 1 }");
19164        assert_eq!(p.statements.len(), 1);
19165    }
19166
19167    #[test]
19168    fn parse_foreach_loop() {
19169        let p = parse_ok("foreach my $x (@arr) { 1 }");
19170        assert_eq!(p.statements.len(), 1);
19171    }
19172
19173    #[test]
19174    fn parse_loop_with_label() {
19175        let p = parse_ok("OUTER: for my $i (1..10) { last OUTER }");
19176        assert_eq!(p.statements.len(), 1);
19177        assert_eq!(p.statements[0].label.as_deref(), Some("OUTER"));
19178    }
19179
19180    #[test]
19181    fn parse_begin_block() {
19182        let p = parse_ok("BEGIN { 1 }");
19183        assert_eq!(p.statements.len(), 1);
19184        matches!(&p.statements[0].kind, StmtKind::Begin(_));
19185    }
19186
19187    #[test]
19188    fn parse_end_block() {
19189        let p = parse_ok("END { 1 }");
19190        assert_eq!(p.statements.len(), 1);
19191        matches!(&p.statements[0].kind, StmtKind::End(_));
19192    }
19193
19194    #[test]
19195    fn parse_package_statement() {
19196        let p = parse_ok("package Foo::Bar");
19197        assert_eq!(p.statements.len(), 1);
19198        match &p.statements[0].kind {
19199            StmtKind::Package { name } => assert_eq!(name, "Foo::Bar"),
19200            _ => panic!("expected Package"),
19201        }
19202    }
19203
19204    #[test]
19205    fn parse_use_statement() {
19206        let p = parse_ok("use strict");
19207        assert_eq!(p.statements.len(), 1);
19208    }
19209
19210    #[test]
19211    fn parse_no_statement() {
19212        let p = parse_ok("no warnings");
19213        assert_eq!(p.statements.len(), 1);
19214    }
19215
19216    #[test]
19217    fn parse_require_bareword() {
19218        let p = parse_ok("require Foo::Bar");
19219        assert_eq!(p.statements.len(), 1);
19220    }
19221
19222    #[test]
19223    fn parse_require_string() {
19224        let p = parse_ok(r#"require "foo.pl""#);
19225        assert_eq!(p.statements.len(), 1);
19226    }
19227
19228    #[test]
19229    fn parse_eval_block() {
19230        let p = parse_ok("eval { 1 }");
19231        assert_eq!(p.statements.len(), 1);
19232    }
19233
19234    #[test]
19235    fn parse_eval_string() {
19236        let p = parse_ok(r#"eval "1 + 2""#);
19237        assert_eq!(p.statements.len(), 1);
19238    }
19239
19240    #[test]
19241    fn parse_qw_word_list() {
19242        let p = parse_ok("my @a = qw(foo bar baz)");
19243        assert_eq!(p.statements.len(), 1);
19244    }
19245
19246    #[test]
19247    fn parse_q_string() {
19248        let p = parse_ok("my $s = q{hello}");
19249        assert_eq!(p.statements.len(), 1);
19250    }
19251
19252    #[test]
19253    fn parse_qq_string() {
19254        let p = parse_ok(r#"my $s = qq(hello $x)"#);
19255        assert_eq!(p.statements.len(), 1);
19256    }
19257
19258    #[test]
19259    fn parse_regex_match() {
19260        let p = parse_ok(r#"$x =~ /foo/"#);
19261        assert_eq!(p.statements.len(), 1);
19262    }
19263
19264    #[test]
19265    fn parse_regex_substitution() {
19266        let p = parse_ok(r#"$x =~ s/foo/bar/g"#);
19267        assert_eq!(p.statements.len(), 1);
19268    }
19269
19270    #[test]
19271    fn parse_transliterate() {
19272        let p = parse_ok(r#"$x =~ tr/a-z/A-Z/"#);
19273        assert_eq!(p.statements.len(), 1);
19274    }
19275
19276    #[test]
19277    fn parse_ternary_operator() {
19278        let p = parse_ok("my $x = $a ? 1 : 2");
19279        assert_eq!(p.statements.len(), 1);
19280    }
19281
19282    #[test]
19283    fn parse_arrow_method_call() {
19284        let p = parse_ok("$obj->method()");
19285        assert_eq!(p.statements.len(), 1);
19286    }
19287
19288    #[test]
19289    fn parse_arrow_deref_hash() {
19290        let p = parse_ok("$r->{key}");
19291        assert_eq!(p.statements.len(), 1);
19292    }
19293
19294    #[test]
19295    fn parse_arrow_deref_array() {
19296        let p = parse_ok("$r->[0]");
19297        assert_eq!(p.statements.len(), 1);
19298    }
19299
19300    #[test]
19301    fn parse_chained_arrow_deref() {
19302        let p = parse_ok("$r->{a}[0]{b}");
19303        assert_eq!(p.statements.len(), 1);
19304    }
19305
19306    #[test]
19307    fn parse_my_multiple_vars() {
19308        let p = parse_ok("my ($a, $b, $c) = (1, 2, 3)");
19309        assert_eq!(p.statements.len(), 1);
19310    }
19311
19312    #[test]
19313    fn parse_our_scalar() {
19314        let p = parse_ok("our $VERSION = '1.0'");
19315        assert_eq!(p.statements.len(), 1);
19316    }
19317
19318    #[test]
19319    fn parse_local_scalar() {
19320        let p = parse_ok("local $/ = undef");
19321        assert_eq!(p.statements.len(), 1);
19322    }
19323
19324    #[test]
19325    fn parse_state_variable() {
19326        let p = parse_ok("fn Test::counter { state $n = 0; $n++ }");
19327        assert_eq!(p.statements.len(), 1);
19328    }
19329
19330    #[test]
19331    fn parse_postfix_if() {
19332        let p = parse_ok("print 1 if $x");
19333        assert_eq!(p.statements.len(), 1);
19334    }
19335
19336    #[test]
19337    fn parse_postfix_unless() {
19338        let p = parse_ok("die 'error' unless $ok");
19339        assert_eq!(p.statements.len(), 1);
19340    }
19341
19342    #[test]
19343    fn parse_postfix_while() {
19344        let p = parse_ok("$x++ while $x < 10");
19345        assert_eq!(p.statements.len(), 1);
19346    }
19347
19348    #[test]
19349    fn parse_postfix_for() {
19350        let p = parse_ok("print for @arr");
19351        assert_eq!(p.statements.len(), 1);
19352    }
19353
19354    #[test]
19355    fn parse_last_next_redo() {
19356        let p = parse_ok("for (@a) { next if $_ < 0; last if $_ > 10 }");
19357        assert_eq!(p.statements.len(), 1);
19358    }
19359
19360    #[test]
19361    fn parse_return_statement() {
19362        let p = parse_ok("fn foo { return 42 }");
19363        assert_eq!(p.statements.len(), 1);
19364    }
19365
19366    #[test]
19367    fn parse_wantarray() {
19368        let p = parse_ok("fn foo { wantarray ? @a : $a }");
19369        assert_eq!(p.statements.len(), 1);
19370    }
19371
19372    #[test]
19373    fn parse_caller_builtin() {
19374        let p = parse_ok("my @c = caller");
19375        assert_eq!(p.statements.len(), 1);
19376    }
19377
19378    #[test]
19379    fn parse_ref_to_array() {
19380        let p = parse_ok("my $r = \\@arr");
19381        assert_eq!(p.statements.len(), 1);
19382    }
19383
19384    #[test]
19385    fn parse_ref_to_hash() {
19386        let p = parse_ok("my $r = \\%hash");
19387        assert_eq!(p.statements.len(), 1);
19388    }
19389
19390    #[test]
19391    fn parse_ref_to_scalar() {
19392        let p = parse_ok("my $r = \\$x");
19393        assert_eq!(p.statements.len(), 1);
19394    }
19395
19396    #[test]
19397    fn parse_deref_scalar() {
19398        let p = parse_ok("my $v = $$r");
19399        assert_eq!(p.statements.len(), 1);
19400    }
19401
19402    #[test]
19403    fn parse_deref_array() {
19404        let p = parse_ok("my @a = @$r");
19405        assert_eq!(p.statements.len(), 1);
19406    }
19407
19408    #[test]
19409    fn parse_deref_hash() {
19410        let p = parse_ok("my %h = %$r");
19411        assert_eq!(p.statements.len(), 1);
19412    }
19413
19414    #[test]
19415    fn parse_blessed_ref() {
19416        let p = parse_ok("bless $r, 'Foo'");
19417        assert_eq!(p.statements.len(), 1);
19418    }
19419
19420    #[test]
19421    fn parse_heredoc_basic() {
19422        let p = parse_ok("my $s = <<END;\nfoo\nEND");
19423        assert_eq!(p.statements.len(), 1);
19424    }
19425
19426    #[test]
19427    fn parse_heredoc_quoted() {
19428        let p = parse_ok("my $s = <<'END';\nfoo\nEND");
19429        assert_eq!(p.statements.len(), 1);
19430    }
19431
19432    #[test]
19433    fn parse_do_block() {
19434        let p = parse_ok("my $x = do { 1 + 2 }");
19435        assert_eq!(p.statements.len(), 1);
19436    }
19437
19438    #[test]
19439    fn parse_do_file() {
19440        let p = parse_ok(r#"do "foo.pl""#);
19441        assert_eq!(p.statements.len(), 1);
19442    }
19443
19444    #[test]
19445    fn parse_map_expression() {
19446        let p = parse_ok("my @b = map { $_ * 2 } @a");
19447        assert_eq!(p.statements.len(), 1);
19448    }
19449
19450    #[test]
19451    fn parse_grep_expression() {
19452        let p = parse_ok("my @b = grep { $_ > 0 } @a");
19453        assert_eq!(p.statements.len(), 1);
19454    }
19455
19456    #[test]
19457    fn parse_sort_expression() {
19458        let p = parse_ok("my @b = sort { $a <=> $b } @a");
19459        assert_eq!(p.statements.len(), 1);
19460    }
19461
19462    #[test]
19463    fn parse_pipe_forward() {
19464        let p = parse_ok("@a |> map { $_ * 2 }");
19465        assert_eq!(p.statements.len(), 1);
19466    }
19467
19468    #[test]
19469    fn parse_expression_from_str_simple() {
19470        let e = parse_expression_from_str("$x + 1", "-e").unwrap();
19471        assert!(matches!(e.kind, ExprKind::BinOp { .. }));
19472    }
19473
19474    #[test]
19475    fn parse_expression_from_str_extra_tokens_error() {
19476        let err = parse_expression_from_str("$x; $y", "-e").unwrap_err();
19477        assert!(err.message.contains("Extra tokens"));
19478    }
19479
19480    #[test]
19481    fn parse_slice_indices_from_str_basic() {
19482        let indices = parse_slice_indices_from_str("0, 1, 2", "-e").unwrap();
19483        assert_eq!(indices.len(), 3);
19484    }
19485
19486    #[test]
19487    fn parse_format_value_line_empty() {
19488        let exprs = parse_format_value_line("").unwrap();
19489        assert!(exprs.is_empty());
19490    }
19491
19492    #[test]
19493    fn parse_format_value_line_single() {
19494        let exprs = parse_format_value_line("$x").unwrap();
19495        assert_eq!(exprs.len(), 1);
19496    }
19497
19498    #[test]
19499    fn parse_format_value_line_multiple() {
19500        let exprs = parse_format_value_line("$a, $b, $c").unwrap();
19501        assert_eq!(exprs.len(), 3);
19502    }
19503
19504    #[test]
19505    fn parse_unclosed_brace_error() {
19506        let err = parse_err("fn foo {");
19507        assert!(!err.is_empty());
19508    }
19509
19510    #[test]
19511    fn parse_unclosed_paren_error() {
19512        let err = parse_err("print (1, 2");
19513        assert!(!err.is_empty());
19514    }
19515
19516    #[test]
19517    fn parse_invalid_statement_error() {
19518        let err = parse_err("???");
19519        assert!(!err.is_empty());
19520    }
19521
19522    #[test]
19523    fn merge_expr_list_single() {
19524        let e = Expr {
19525            kind: ExprKind::Integer(1),
19526            line: 1,
19527        };
19528        let merged = merge_expr_list(vec![e.clone()]);
19529        matches!(merged.kind, ExprKind::Integer(1));
19530    }
19531
19532    #[test]
19533    fn merge_expr_list_multiple() {
19534        let e1 = Expr {
19535            kind: ExprKind::Integer(1),
19536            line: 1,
19537        };
19538        let e2 = Expr {
19539            kind: ExprKind::Integer(2),
19540            line: 1,
19541        };
19542        let merged = merge_expr_list(vec![e1, e2]);
19543        matches!(merged.kind, ExprKind::List(_));
19544    }
19545}