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    /// When > 0, `parse_multiplication` will not consume `Token::Slash` as division.
125    /// Used by thread macro so `/pattern/` is left for the stage parser to handle.
126    suppress_slash_as_div: u32,
127    /// When > 0, the lexer should not interpret `m/`, `s/`, etc. as regex-starters.
128    /// Used by thread macro to prevent `/m/` from being misparsed.
129    pub suppress_m_regex: u32,
130    /// When > 0, `parse_range` will not consume `:` as the short-form range operator.
131    /// Bumped while parsing the then-branch of a ternary `? :` so `a ? b : c` doesn't
132    /// misparse `b : c` as a range.
133    suppress_colon_range: u32,
134    /// Counter (depth-tracked like [`Self::suppress_colon_range`]) that
135    /// disables `~` as a range separator. Used inside paired `~...~` char-
136    /// index/slice subscripts so the closing `~` doesn't get eaten as a
137    /// range op. `:` range is still allowed inside (e.g. `$_~1:3~` is a
138    /// slice with a `:` range as the index).
139    suppress_tilde_range: u32,
140    /// When true, `pipe_forward_apply` uses thread-last semantics (append to args)
141    /// instead of thread-first (prepend). Set by `->>` thread macro.
142    thread_last_mode: bool,
143    /// When true, we're parsing a module (via `use`/`require`), not user code.
144    /// Modules are allowed to shadow builtins; user code is not (unless `--compat`).
145    pub parsing_module: bool,
146    /// `self.pos` immediately after consuming a paren-list close (`(EXPR)`,
147    /// `(EXPR, …)`, `()`) or `qw(…)` in `parse_primary`. The `x` operator
148    /// reads this at parse time to distinguish `(LIST) x N` (list repetition)
149    /// from `EXPR x N` (scalar string repetition). The compare is exact: any
150    /// postfix consumption (`->method()`, `[idx]`, …) advances `self.pos`
151    /// past this checkpoint, so list-repeat fires only when `x` is the very
152    /// next token after the closing paren.
153    list_construct_close_pos: Option<usize>,
154    /// Synthetic SubDecl statements queued by anonymous-sub overload handlers
155    /// (`use overload "+" => sub { ... }`) — drained at the end of
156    /// [`Self::parse_program`] and prepended to the top-level statements so
157    /// the package-qualified synthetic name resolves at runtime. (PARITY-012)
158    pending_synthetic_subs: Vec<Statement>,
159    /// Counter for unique anonymous-overload-handler names.
160    next_overload_anon_id: u32,
161    /// Token-vector indices where the lexer emitted a *bare* positional alias
162    /// (`_`, `_0`, `_1`, …) — i.e. without a leading `$` sigil. Populated by
163    /// [`crate::lexer::Lexer::tokenize`]. Consulted by [`Self::parse_my_our_local`]
164    /// to auto-wrap an RHS expression that contains free positional aliases
165    /// into an implicit zero-arg coderef, so `my $f = _ * 2` ≡
166    /// `my $f = fn { _ * 2 }`.
167    pub bare_positional_indices: std::collections::HashSet<usize>,
168}
169
170impl Parser {
171    pub fn new(tokens: Vec<(Token, usize)>) -> Self {
172        Self::new_with_file(tokens, "-e")
173    }
174
175    pub fn new_with_file(tokens: Vec<(Token, usize)>, file: impl Into<String>) -> Self {
176        Self {
177            tokens,
178            pos: 0,
179            next_rate_limit_slot: 0,
180            suppress_indirect_paren_call: 0,
181            pipe_rhs_depth: 0,
182            no_pipe_forward_depth: 0,
183            suppress_scalar_hash_brace: 0,
184            next_desugar_tmp: 0,
185            error_file: file.into(),
186            declared_subs: std::collections::HashSet::new(),
187            suppress_parenless_call: 0,
188            suppress_slash_as_div: 0,
189            suppress_m_regex: 0,
190            suppress_colon_range: 0,
191            suppress_tilde_range: 0,
192            thread_last_mode: false,
193            pending_synthetic_subs: Vec::new(),
194            next_overload_anon_id: 0,
195            parsing_module: false,
196            list_construct_close_pos: None,
197            bare_positional_indices: std::collections::HashSet::new(),
198            block_depth: 0,
199        }
200    }
201
202    fn alloc_desugar_tmp(&mut self) -> u32 {
203        let n = self.next_desugar_tmp;
204        self.next_desugar_tmp = self.next_desugar_tmp.saturating_add(1);
205        n
206    }
207
208    /// True when we are currently parsing the RHS of a `|>` pipe-forward.
209    /// Used by builtins (`map`, `grep`, `sort`, `join`, …) to supply a
210    /// placeholder list instead of erroring on a missing operand.
211    #[inline]
212    fn in_pipe_rhs(&self) -> bool {
213        self.pipe_rhs_depth > 0
214    }
215
216    /// List-slurping builtin: the operand is entirely the LHS of `|>` (no following list tokens).
217    /// A newline after the builtin name also terminates the pipe stage (implicit semicolon).
218    fn pipe_supplies_slurped_list_operand(&self) -> bool {
219        self.in_pipe_rhs()
220            && (matches!(
221                self.peek(),
222                Token::Semicolon
223                    | Token::RBrace
224                    | Token::RParen
225                    | Token::Eof
226                    | Token::Comma
227                    | Token::PipeForward
228            ) || self.peek_line() > self.prev_line())
229    }
230
231    /// Empty placeholder list used as a stand-in for the list operand of
232    /// list-taking builtins when they appear on the RHS of `|>`.
233    /// [`Self::pipe_forward_apply`] rewrites this slot with the actual piped
234    /// value at desugar time, so the placeholder is never evaluated.
235    #[inline]
236    fn pipe_placeholder_list(&self, line: usize) -> Expr {
237        Expr {
238            kind: ExprKind::List(vec![]),
239            line,
240        }
241    }
242
243    /// List builtins that take `{ BLOCK }, LIST` and accept the threaded list at
244    /// `args[1]` via [`Self::pipe_forward_apply`]. Used by both the pipe-forward
245    /// dispatcher and `parse_thread_stage_with_block` so `~> @a NAME { ... }` and
246    /// `@a |> NAME { ... }` route through the same substitution.
247    fn is_block_then_list_pipe_builtin(name: &str) -> bool {
248        matches!(
249            name,
250            "pfirst"
251                | "pany"
252                | "any"
253                | "all"
254                | "none"
255                | "first"
256                | "take_while"
257                | "drop_while"
258                | "skip_while"
259                | "reject"
260                | "grepv"
261                | "tap"
262                | "peek"
263                | "group_by"
264                | "chunk_by"
265                | "partition"
266                | "min_by"
267                | "max_by"
268                | "zip_with"
269                | "count_by"
270        )
271    }
272
273    /// Lift a `Bareword("f")` to `FuncCall { f, [$_] }`.
274    ///
275    /// stryke extension contexts (map/grep/fore expression forms, pipe-forward)
276    /// call this so that `map sha512, @list` invokes `sha512($_)` for each
277    /// element instead of stringifying the bareword.  Non-bareword expressions
278    /// pass through unchanged.
279    ///
280    /// Also injects `$_` into known builtins that were parsed with zero
281    /// arguments (e.g. `fore unlink`, `map stat`) so they operate on the
282    /// topic variable instead of being no-ops.
283    fn lift_bareword_to_topic_call(expr: Expr) -> Expr {
284        let line = expr.line;
285        let topic = || Expr {
286            kind: ExprKind::ScalarVar("_".into()),
287            line,
288        };
289        match expr.kind {
290            ExprKind::Bareword(ref name) => Expr {
291                kind: ExprKind::FuncCall {
292                    name: name.clone(),
293                    args: vec![topic()],
294                },
295                line,
296            },
297            // Builtins that take Vec<Expr> args — inject $_ when empty.
298            ExprKind::Unlink(ref args) if args.is_empty() => Expr {
299                kind: ExprKind::Unlink(vec![topic()]),
300                line,
301            },
302            ExprKind::Chmod(ref args) if args.is_empty() => Expr {
303                kind: ExprKind::Chmod(vec![topic()]),
304                line,
305            },
306            // Builtins that take Box<Expr> — inject $_ when arg is implicit.
307            ExprKind::Stat(_) => expr,
308            ExprKind::Lstat(_) => expr,
309            ExprKind::Readlink(_) => expr,
310            // rev with empty list should use $_
311            ExprKind::Rev(ref inner) => {
312                if matches!(inner.kind, ExprKind::List(ref v) if v.is_empty()) {
313                    Expr {
314                        kind: ExprKind::Rev(Box::new(topic())),
315                        line,
316                    }
317                } else {
318                    expr
319                }
320            }
321            _ => expr,
322        }
323    }
324
325    /// `parse_assign_expr` with `no_pipe_forward_depth` bumped for the
326    /// duration, so any trailing `|>` is left to the enclosing parser instead
327    /// of being absorbed into this sub-expression. Used by paren-less arg
328    /// parsers (`parse_list_until_terminator`, `chunked`/`windowed` paren-less,
329    /// paren-less method args, …) so `@a |> head 2 |> join "-"` chains
330    /// left-associatively instead of letting `head`'s first arg swallow the
331    /// outer `|>`. The counter is restored on both success and error paths.
332    fn parse_assign_expr_stop_at_pipe(&mut self) -> PerlResult<Expr> {
333        self.no_pipe_forward_depth = self.no_pipe_forward_depth.saturating_add(1);
334        let r = self.parse_assign_expr();
335        self.no_pipe_forward_depth = self.no_pipe_forward_depth.saturating_sub(1);
336        r
337    }
338
339    fn syntax_err(&self, message: impl Into<String>, line: usize) -> PerlError {
340        PerlError::new(ErrorKind::Syntax, message, line, self.error_file.clone())
341    }
342
343    /// Coderef-in-block-position helper for tier-2 list builtins (`any`,
344    /// `all`, `none`, `first`, `take_while`, …). Returns `Some([f, list])`
345    /// when the next tokens look like `$f [,] LIST` (or `$f` alone in
346    /// pipe-RHS); `None` when the caller should fall through to the block
347    /// form. The first arg is any coderef-shaped expression — runtime
348    /// checks `as_code_ref()` and dispatches.
349    fn try_parse_coderef_listop_args(&mut self, line: usize) -> PerlResult<Option<Vec<Expr>>> {
350        if !matches!(self.peek(), Token::ScalarVar(_) | Token::Backslash) {
351            return Ok(None);
352        }
353        let f = self.parse_assign_expr_stop_at_pipe()?;
354        let _ = self.eat(&Token::Comma);
355        let list = if self.in_pipe_rhs()
356            && matches!(
357                self.peek(),
358                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
359            ) {
360            self.pipe_placeholder_list(line)
361        } else {
362            self.parse_expression()?
363        };
364        Ok(Some(vec![f, list]))
365    }
366
367    fn alloc_rate_limit_slot(&mut self) -> u32 {
368        let s = self.next_rate_limit_slot;
369        self.next_rate_limit_slot = self.next_rate_limit_slot.saturating_add(1);
370        s
371    }
372
373    fn peek(&self) -> &Token {
374        self.tokens
375            .get(self.pos)
376            .map(|(t, _)| t)
377            .unwrap_or(&Token::Eof)
378    }
379
380    fn peek_line(&self) -> usize {
381        self.tokens.get(self.pos).map(|(_, l)| *l).unwrap_or(0)
382    }
383
384    fn peek_at(&self, offset: usize) -> &Token {
385        self.tokens
386            .get(self.pos + offset)
387            .map(|(t, _)| t)
388            .unwrap_or(&Token::Eof)
389    }
390
391    fn advance(&mut self) -> (Token, usize) {
392        let tok = self
393            .tokens
394            .get(self.pos)
395            .cloned()
396            .unwrap_or((Token::Eof, 0));
397        self.pos += 1;
398        tok
399    }
400
401    /// Line number of the most recently consumed token (the token at `pos - 1`).
402    fn prev_line(&self) -> usize {
403        if self.pos > 0 {
404            self.tokens.get(self.pos - 1).map(|(_, l)| *l).unwrap_or(0)
405        } else {
406            0
407        }
408    }
409
410    /// Check if `{ ... }` starting at current position looks like a hashref rather than a block.
411    /// Heuristics (assuming current token is `{`):
412    /// - `{ bareword =>` → hashref
413    /// - `{ "string" =>` → hashref
414    /// - `{ $var =>` → hashref
415    /// - `{ 0 =>` → hashref (numeric key)
416    /// - `{ %hash }` or `{ %hash, ...}` → hashref (spread)
417    /// - `{ }` (empty) → hashref
418    fn looks_like_hashref(&self) -> bool {
419        debug_assert!(matches!(self.peek(), Token::LBrace));
420        let tok1 = self.peek_at(1);
421        let tok2 = self.peek_at(2);
422        match tok1 {
423            Token::RBrace => true,
424            Token::Ident(_)
425            | Token::SingleString(_)
426            | Token::DoubleString(_)
427            | Token::ScalarVar(_)
428            | Token::Integer(_) => matches!(tok2, Token::FatArrow),
429            Token::HashVar(_) => matches!(tok2, Token::RBrace | Token::Comma),
430            _ => false,
431        }
432    }
433
434    fn expect(&mut self, expected: &Token) -> PerlResult<usize> {
435        let (tok, line) = self.advance();
436        if std::mem::discriminant(&tok) == std::mem::discriminant(expected) {
437            Ok(line)
438        } else {
439            Err(self.syntax_err(format!("Expected {:?}, got {:?}", expected, tok), line))
440        }
441    }
442
443    fn eat(&mut self, expected: &Token) -> bool {
444        if std::mem::discriminant(self.peek()) == std::mem::discriminant(expected) {
445            self.advance();
446            true
447        } else {
448            false
449        }
450    }
451
452    fn at_eof(&self) -> bool {
453        matches!(self.peek(), Token::Eof)
454    }
455
456    /// True when a file test (`-d`, `-f`, …) may omit its operand and use `$_` (Perl filetest default).
457    fn filetest_allows_implicit_topic(tok: &Token) -> bool {
458        matches!(
459            tok,
460            Token::RParen
461                | Token::Semicolon
462                | Token::Comma
463                | Token::RBrace
464                | Token::Eof
465                | Token::LogAnd
466                | Token::LogOr
467                | Token::LogAndWord
468                | Token::LogOrWord
469                | Token::PipeForward
470        )
471    }
472
473    /// True when the next token is a statement-starting keyword on a *different*
474    /// line from `stmt_line`.  Used by `parse_use` / `parse_no` to stop parsing
475    /// import lists when semicolons are omitted (stryke extension).
476    fn next_is_new_stmt_keyword(&self, stmt_line: usize) -> bool {
477        // Semicolons-optional is a stryke extension; in compat mode, require them.
478        if crate::compat_mode() {
479            return false;
480        }
481        if self.peek_line() == stmt_line {
482            return false;
483        }
484        matches!(
485            self.peek(),
486            Token::Ident(ref kw) if matches!(kw.as_str(),
487                "use" | "no" | "my" | "our" | "local" | "sub" | "struct" | "enum"
488                | "if" | "unless" | "while" | "until" | "for" | "foreach"
489                | "return" | "last" | "next" | "redo" | "package" | "require"
490                | "BEGIN" | "END" | "UNITCHECK" | "frozen" | "const" | "typed"
491            )
492        )
493    }
494
495    /// True when the next token is on a different line from `stmt_line` and could
496    /// start a new statement. More permissive than `next_is_new_stmt_keyword` —
497    /// includes sigil-prefixed variables like `$var`, `@arr`, `%hash`.
498    fn next_is_new_statement_start(&self, stmt_line: usize) -> bool {
499        if crate::compat_mode() {
500            return false;
501        }
502        if self.peek_line() == stmt_line {
503            return false;
504        }
505        matches!(
506            self.peek(),
507            Token::ScalarVar(_)
508                | Token::DerefScalarVar(_)
509                | Token::ArrayVar(_)
510                | Token::HashVar(_)
511                | Token::LBrace
512        ) || self.next_is_new_stmt_keyword(stmt_line)
513    }
514
515    // ── Top level ──
516
517    pub fn parse_program(&mut self) -> PerlResult<Program> {
518        let mut statements = self.parse_statements()?;
519        // Prepend any synthetic SubDecl stubs queued by anonymous overload
520        // handlers so the package-qualified synthetic names resolve when the
521        // overload table is consulted at runtime. (PARITY-012)
522        if !self.pending_synthetic_subs.is_empty() {
523            let synthetics = std::mem::take(&mut self.pending_synthetic_subs);
524            let mut combined = Vec::with_capacity(synthetics.len() + statements.len());
525            combined.extend(synthetics);
526            combined.append(&mut statements);
527            statements = combined;
528        }
529        Ok(Program { statements })
530    }
531
532    /// Parse statements until EOF. Used by parse_program and parse_block_from_str.
533    pub fn parse_statements(&mut self) -> PerlResult<Vec<Statement>> {
534        let mut statements = Vec::new();
535        while !self.at_eof() {
536            if matches!(self.peek(), Token::Semicolon) {
537                let line = self.peek_line();
538                self.advance();
539                statements.push(Statement {
540                    label: None,
541                    kind: StmtKind::Empty,
542                    line,
543                });
544                continue;
545            }
546            statements.push(self.parse_statement()?);
547        }
548        Ok(statements)
549    }
550
551    // ── Statements ──
552
553    fn parse_statement(&mut self) -> PerlResult<Statement> {
554        let line = self.peek_line();
555
556        // Statement label `FOO:` / `boot:` / `BAR_BAZ:` (not `Foo::` — that is `Ident` + `::`).
557        // Uppercase-only was too strict: XSLoader.pm uses `boot:` before `my $xs = ...`.
558        let label = match self.peek().clone() {
559            Token::Ident(_) => {
560                if matches!(self.peek_at(1), Token::Colon)
561                    && !matches!(self.peek_at(2), Token::Colon)
562                {
563                    let (tok, _) = self.advance();
564                    let l = match tok {
565                        Token::Ident(l) => l,
566                        _ => unreachable!(),
567                    };
568                    self.advance(); // ':'
569                    Some(l)
570                } else {
571                    None
572                }
573            }
574            _ => None,
575        };
576
577        let mut stmt = match self.peek().clone() {
578            Token::FormatDecl { .. } => {
579                let tok_line = self.peek_line();
580                let (tok, _) = self.advance();
581                match tok {
582                    Token::FormatDecl { name, lines } => Statement {
583                        label: label.clone(),
584                        kind: StmtKind::FormatDecl { name, lines },
585                        line: tok_line,
586                    },
587                    _ => unreachable!(),
588                }
589            }
590            Token::Ident(ref kw) => match kw.as_str() {
591                "if" => self.parse_if()?,
592                "unless" => self.parse_unless()?,
593                "while" => {
594                    let mut s = self.parse_while()?;
595                    if let StmtKind::While {
596                        label: ref mut lbl, ..
597                    } = s.kind
598                    {
599                        *lbl = label.clone();
600                    }
601                    s
602                }
603                "until" => {
604                    let mut s = self.parse_until()?;
605                    if let StmtKind::Until {
606                        label: ref mut lbl, ..
607                    } = s.kind
608                    {
609                        *lbl = label.clone();
610                    }
611                    s
612                }
613                "for" => {
614                    let mut s = self.parse_for_or_foreach()?;
615                    match s.kind {
616                        StmtKind::For {
617                            label: ref mut lbl, ..
618                        }
619                        | StmtKind::Foreach {
620                            label: ref mut lbl, ..
621                        } => *lbl = label.clone(),
622                        _ => {}
623                    }
624                    s
625                }
626                "foreach" => {
627                    let mut s = self.parse_foreach()?;
628                    if let StmtKind::Foreach {
629                        label: ref mut lbl, ..
630                    } = s.kind
631                    {
632                        *lbl = label.clone();
633                    }
634                    s
635                }
636                "sub" => {
637                    if crate::no_interop_mode() {
638                        return Err(self.syntax_err(
639                            "stryke uses `fn` instead of `sub` (--no-interop is active)",
640                            self.peek_line(),
641                        ));
642                    }
643                    self.parse_sub_decl(true)?
644                }
645                "fn" => self.parse_sub_decl(false)?,
646                "struct" => {
647                    if crate::compat_mode() {
648                        return Err(self.syntax_err(
649                            "`struct` is a stryke extension (disabled by --compat)",
650                            self.peek_line(),
651                        ));
652                    }
653                    self.parse_struct_decl()?
654                }
655                "enum" => {
656                    if crate::compat_mode() {
657                        return Err(self.syntax_err(
658                            "`enum` is a stryke extension (disabled by --compat)",
659                            self.peek_line(),
660                        ));
661                    }
662                    self.parse_enum_decl()?
663                }
664                "class" => {
665                    if crate::compat_mode() {
666                        // TODO: parse Perl 5.38 class syntax with :isa()
667                        return Err(self.syntax_err(
668                            "Perl 5.38 `class` syntax not yet implemented in --compat mode",
669                            self.peek_line(),
670                        ));
671                    }
672                    self.parse_class_decl(false, false)?
673                }
674                "abstract" => {
675                    self.advance(); // abstract
676                    if !matches!(self.peek(), Token::Ident(ref s) if s == "class") {
677                        return Err(self.syntax_err(
678                            "`abstract` must be followed by `class`",
679                            self.peek_line(),
680                        ));
681                    }
682                    self.parse_class_decl(true, false)?
683                }
684                "final" => {
685                    self.advance(); // final
686                    if !matches!(self.peek(), Token::Ident(ref s) if s == "class") {
687                        return Err(self
688                            .syntax_err("`final` must be followed by `class`", self.peek_line()));
689                    }
690                    self.parse_class_decl(false, true)?
691                }
692                "trait" => {
693                    if crate::compat_mode() {
694                        return Err(self.syntax_err(
695                            "`trait` is a stryke extension (disabled by --compat)",
696                            self.peek_line(),
697                        ));
698                    }
699                    self.parse_trait_decl()?
700                }
701                "my" => self.parse_my_our_local("my", false)?,
702                "state" => self.parse_my_our_local("state", false)?,
703                "mysync" => {
704                    if crate::compat_mode() {
705                        return Err(self.syntax_err(
706                            "`mysync` is a stryke extension (disabled by --compat)",
707                            self.peek_line(),
708                        ));
709                    }
710                    self.parse_my_our_local("mysync", false)?
711                }
712                "oursync" => {
713                    if crate::compat_mode() {
714                        return Err(self.syntax_err(
715                            "`oursync` is a stryke extension (disabled by --compat)",
716                            self.peek_line(),
717                        ));
718                    }
719                    self.parse_my_our_local("oursync", false)?
720                }
721                "frozen" | "const" => {
722                    let leading = kw.as_str().to_string();
723                    if crate::compat_mode() {
724                        return Err(self.syntax_err(
725                            format!("`{leading}` is a stryke extension (disabled by --compat)"),
726                            self.peek_line(),
727                        ));
728                    }
729                    // `frozen my $x = val;` / `const my $x = val;` — the
730                    // two spellings are interchangeable (`const` is the
731                    // more-familiar name for new users). Expects `my`
732                    // to follow.
733                    self.advance(); // consume "frozen"/"const"
734                    if let Token::Ident(ref kw) = self.peek().clone() {
735                        if kw == "my" {
736                            let mut stmt = self.parse_my_our_local("my", false)?;
737                            if let StmtKind::My(ref mut decls) = stmt.kind {
738                                for decl in decls.iter_mut() {
739                                    decl.frozen = true;
740                                }
741                            }
742                            stmt
743                        } else {
744                            return Err(self.syntax_err(
745                                format!("Expected 'my' after '{leading}'"),
746                                self.peek_line(),
747                            ));
748                        }
749                    } else {
750                        return Err(self.syntax_err(
751                            format!("Expected 'my' after '{leading}'"),
752                            self.peek_line(),
753                        ));
754                    }
755                }
756                "typed" => {
757                    if crate::compat_mode() {
758                        return Err(self.syntax_err(
759                            "`typed` is a stryke extension (disabled by --compat)",
760                            self.peek_line(),
761                        ));
762                    }
763                    self.advance();
764                    if let Token::Ident(ref kw) = self.peek().clone() {
765                        if kw == "my" {
766                            self.parse_my_our_local("my", true)?
767                        } else {
768                            return Err(
769                                self.syntax_err("Expected 'my' after 'typed'", self.peek_line())
770                            );
771                        }
772                    } else {
773                        return Err(
774                            self.syntax_err("Expected 'my' after 'typed'", self.peek_line())
775                        );
776                    }
777                }
778                "our" => self.parse_my_our_local("our", false)?,
779                "local" => self.parse_my_our_local("local", false)?,
780                "package" => self.parse_package()?,
781                "use" => self.parse_use()?,
782                "no" => self.parse_no()?,
783                "return" => self.parse_return()?,
784                "last" => {
785                    self.advance();
786                    let lbl = if let Token::Ident(ref s) = self.peek() {
787                        if s.chars().all(|c| c.is_uppercase() || c == '_') {
788                            let (Token::Ident(l), _) = self.advance() else {
789                                unreachable!()
790                            };
791                            Some(l)
792                        } else {
793                            None
794                        }
795                    } else {
796                        None
797                    };
798                    let stmt = Statement {
799                        label: None,
800                        kind: StmtKind::Last(lbl.or(label.clone())),
801                        line,
802                    };
803                    self.parse_stmt_postfix_modifier(stmt)?
804                }
805                "next" => {
806                    self.advance();
807                    let lbl = if let Token::Ident(ref s) = self.peek() {
808                        if s.chars().all(|c| c.is_uppercase() || c == '_') {
809                            let (Token::Ident(l), _) = self.advance() else {
810                                unreachable!()
811                            };
812                            Some(l)
813                        } else {
814                            None
815                        }
816                    } else {
817                        None
818                    };
819                    let stmt = Statement {
820                        label: None,
821                        kind: StmtKind::Next(lbl.or(label.clone())),
822                        line,
823                    };
824                    self.parse_stmt_postfix_modifier(stmt)?
825                }
826                "redo" => {
827                    self.advance();
828                    self.eat(&Token::Semicolon);
829                    Statement {
830                        label: None,
831                        kind: StmtKind::Redo(label.clone()),
832                        line,
833                    }
834                }
835                "BEGIN" => {
836                    self.advance();
837                    let block = self.parse_block()?;
838                    Statement {
839                        label: None,
840                        kind: StmtKind::Begin(block),
841                        line,
842                    }
843                }
844                "END" => {
845                    self.advance();
846                    let block = self.parse_block()?;
847                    Statement {
848                        label: None,
849                        kind: StmtKind::End(block),
850                        line,
851                    }
852                }
853                "UNITCHECK" => {
854                    self.advance();
855                    let block = self.parse_block()?;
856                    Statement {
857                        label: None,
858                        kind: StmtKind::UnitCheck(block),
859                        line,
860                    }
861                }
862                "CHECK" => {
863                    self.advance();
864                    let block = self.parse_block()?;
865                    Statement {
866                        label: None,
867                        kind: StmtKind::Check(block),
868                        line,
869                    }
870                }
871                "INIT" => {
872                    self.advance();
873                    let block = self.parse_block()?;
874                    Statement {
875                        label: None,
876                        kind: StmtKind::Init(block),
877                        line,
878                    }
879                }
880                "goto" => {
881                    self.advance();
882                    let target = self.parse_expression()?;
883                    let stmt = Statement {
884                        label: None,
885                        kind: StmtKind::Goto {
886                            target: Box::new(target),
887                        },
888                        line,
889                    };
890                    // `goto $l if COND;` / `goto &$cr if defined &$cr;` (XSLoader.pm)
891                    self.parse_stmt_postfix_modifier(stmt)?
892                }
893                "continue" => {
894                    self.advance();
895                    let block = self.parse_block()?;
896                    Statement {
897                        label: None,
898                        kind: StmtKind::Continue(block),
899                        line,
900                    }
901                }
902                "before"
903                    if matches!(
904                        self.peek_at(1),
905                        Token::SingleString(_) | Token::DoubleString(_)
906                    ) =>
907                {
908                    self.parse_advice_decl(crate::ast::AdviceKind::Before)?
909                }
910                "after"
911                    if matches!(
912                        self.peek_at(1),
913                        Token::SingleString(_) | Token::DoubleString(_)
914                    ) =>
915                {
916                    self.parse_advice_decl(crate::ast::AdviceKind::After)?
917                }
918                "around"
919                    if matches!(
920                        self.peek_at(1),
921                        Token::SingleString(_) | Token::DoubleString(_)
922                    ) =>
923                {
924                    self.parse_advice_decl(crate::ast::AdviceKind::Around)?
925                }
926                "try" => self.parse_try_catch()?,
927                "defer" => self.parse_defer_stmt()?,
928                "tie" => self.parse_tie_stmt()?,
929                "given" => self.parse_given()?,
930                "when" => self.parse_when_stmt()?,
931                "default" => self.parse_default_stmt()?,
932                "eval_timeout" => self.parse_eval_timeout()?,
933                "do" => {
934                    if matches!(self.peek_at(1), Token::LBrace) {
935                        self.advance();
936                        let body = self.parse_block()?;
937                        if let Token::Ident(ref w) = self.peek().clone() {
938                            if w == "while" {
939                                self.advance();
940                                self.expect(&Token::LParen)?;
941                                let mut condition = self.parse_expression()?;
942                                Self::mark_match_scalar_g_for_boolean_condition(&mut condition);
943                                self.expect(&Token::RParen)?;
944                                self.eat(&Token::Semicolon);
945                                Statement {
946                                    label: label.clone(),
947                                    kind: StmtKind::DoWhile { body, condition },
948                                    line,
949                                }
950                            } else {
951                                let inner_line = body.first().map(|s| s.line).unwrap_or(line);
952                                let inner = Expr {
953                                    kind: ExprKind::CodeRef {
954                                        params: vec![],
955                                        body,
956                                    },
957                                    line: inner_line,
958                                };
959                                let expr = Expr {
960                                    kind: ExprKind::Do(Box::new(inner)),
961                                    line,
962                                };
963                                let stmt = Statement {
964                                    label: label.clone(),
965                                    kind: StmtKind::Expression(expr),
966                                    line,
967                                };
968                                // `do { } if EXPR` / `do { } unless EXPR` — postfix modifier, not a new `if (` statement.
969                                self.parse_stmt_postfix_modifier(stmt)?
970                            }
971                        } else {
972                            let inner_line = body.first().map(|s| s.line).unwrap_or(line);
973                            let inner = Expr {
974                                kind: ExprKind::CodeRef {
975                                    params: vec![],
976                                    body,
977                                },
978                                line: inner_line,
979                            };
980                            let expr = Expr {
981                                kind: ExprKind::Do(Box::new(inner)),
982                                line,
983                            };
984                            let stmt = Statement {
985                                label: label.clone(),
986                                kind: StmtKind::Expression(expr),
987                                line,
988                            };
989                            self.parse_stmt_postfix_modifier(stmt)?
990                        }
991                    } else {
992                        if let Some(expr) = self.try_parse_bareword_stmt_call() {
993                            let stmt = self.maybe_postfix_modifier(expr)?;
994                            self.parse_stmt_postfix_modifier(stmt)?
995                        } else {
996                            let expr = self.parse_expression()?;
997                            let stmt = self.maybe_postfix_modifier(expr)?;
998                            self.parse_stmt_postfix_modifier(stmt)?
999                        }
1000                    }
1001                }
1002                _ => {
1003                    // `foo;` or `{ foo }` — bareword statement is a zero-arg call (topic `$_` at runtime).
1004                    if let Some(expr) = self.try_parse_bareword_stmt_call() {
1005                        let stmt = self.maybe_postfix_modifier(expr)?;
1006                        self.parse_stmt_postfix_modifier(stmt)?
1007                    } else {
1008                        let expr = self.parse_expression()?;
1009                        let stmt = self.maybe_postfix_modifier(expr)?;
1010                        self.parse_stmt_postfix_modifier(stmt)?
1011                    }
1012                }
1013            },
1014            Token::LBrace => {
1015                // Disambiguate hashref `{ k => v }` from block `{ stmt; stmt }`.
1016                // If it looks like a hashref, parse as expression; otherwise parse as block.
1017                if self.looks_like_hashref() {
1018                    let expr = self.parse_expression()?;
1019                    let stmt = self.maybe_postfix_modifier(expr)?;
1020                    self.parse_stmt_postfix_modifier(stmt)?
1021                } else {
1022                    let block = self.parse_block()?;
1023                    let stmt = Statement {
1024                        label: None,
1025                        kind: StmtKind::Block(block),
1026                        line,
1027                    };
1028                    // `{ … } if EXPR` / `{ … } unless EXPR` — same postfix rule as `do { } if …` (not `if (`).
1029                    self.parse_stmt_postfix_modifier(stmt)?
1030                }
1031            }
1032            _ => {
1033                let expr = self.parse_expression()?;
1034                let stmt = self.maybe_postfix_modifier(expr)?;
1035                self.parse_stmt_postfix_modifier(stmt)?
1036            }
1037        };
1038
1039        stmt.label = label;
1040        Ok(stmt)
1041    }
1042
1043    /// Handle postfix if/unless on statement-level keywords like last/next.
1044    fn parse_stmt_postfix_modifier(&mut self, stmt: Statement) -> PerlResult<Statement> {
1045        let line = stmt.line;
1046        // Implicit semicolon: a modifier keyword on a new line is a new
1047        // statement, not a postfix modifier.  This prevents semicolon-less
1048        // code like `my $x = "val"\nif ($x) { ... }` from being mis-parsed
1049        // as `my $x = "val" if ($x) { ... }`.
1050        if self.peek_line() > self.prev_line() {
1051            self.eat(&Token::Semicolon);
1052            return Ok(stmt);
1053        }
1054        if let Token::Ident(ref kw) = self.peek().clone() {
1055            match kw.as_str() {
1056                "if" => {
1057                    self.advance();
1058                    let mut cond = self.parse_expression()?;
1059                    Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
1060                    self.eat(&Token::Semicolon);
1061                    return Ok(Statement {
1062                        label: None,
1063                        kind: StmtKind::If {
1064                            condition: cond,
1065                            body: vec![stmt],
1066                            elsifs: vec![],
1067                            else_block: None,
1068                        },
1069                        line,
1070                    });
1071                }
1072                "unless" => {
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::Unless {
1080                            condition: cond,
1081                            body: vec![stmt],
1082                            else_block: None,
1083                        },
1084                        line,
1085                    });
1086                }
1087                "while" | "until" | "for" | "foreach" => {
1088                    // `do { } for @a` / `{ } while COND` — same postfix forms as [`maybe_postfix_modifier`],
1089                    // not a new `for (` / `while (` statement (which would require `(` after `for`).
1090                    if let Some(expr) = Self::stmt_into_postfix_body_expr(stmt) {
1091                        let out = self.maybe_postfix_modifier(expr)?;
1092                        self.eat(&Token::Semicolon);
1093                        return Ok(out);
1094                    }
1095                    return Err(self.syntax_err(
1096                        format!("postfix `{}` is not supported on this statement form", kw),
1097                        self.peek_line(),
1098                    ));
1099                }
1100                // `{ } pmap @a` / `{ } pflat_map @a` / `{ } pfor @a` / `do { } …` — same shapes as prefix forms.
1101                "pmap" | "pflat_map" | "pgrep" | "pfor" | "preduce" | "pcache" => {
1102                    let line = stmt.line;
1103                    let block = self.stmt_into_parallel_block(stmt)?;
1104                    let which = kw.as_str();
1105                    self.advance();
1106                    self.eat(&Token::Comma);
1107                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
1108                    self.eat(&Token::Semicolon);
1109                    let list = Box::new(list);
1110                    let progress = progress.map(Box::new);
1111                    let kind = match which {
1112                        "pmap" => ExprKind::PMapExpr {
1113                            block,
1114                            list,
1115                            progress,
1116                            flat_outputs: false,
1117                            on_cluster: None,
1118                            stream: false,
1119                        },
1120                        "pflat_map" => ExprKind::PMapExpr {
1121                            block,
1122                            list,
1123                            progress,
1124                            flat_outputs: true,
1125                            on_cluster: None,
1126                            stream: false,
1127                        },
1128                        "pgrep" => ExprKind::PGrepExpr {
1129                            block,
1130                            list,
1131                            progress,
1132                            stream: false,
1133                        },
1134                        "pfor" => ExprKind::PForExpr {
1135                            block,
1136                            list,
1137                            progress,
1138                        },
1139                        "preduce" => ExprKind::PReduceExpr {
1140                            block,
1141                            list,
1142                            progress,
1143                        },
1144                        "pcache" => ExprKind::PcacheExpr {
1145                            block,
1146                            list,
1147                            progress,
1148                        },
1149                        _ => unreachable!(),
1150                    };
1151                    return Ok(Statement {
1152                        label: None,
1153                        kind: StmtKind::Expression(Expr { kind, line }),
1154                        line,
1155                    });
1156                }
1157                _ => {}
1158            }
1159        }
1160        self.eat(&Token::Semicolon);
1161        Ok(stmt)
1162    }
1163
1164    /// Block body for postfix `pmap` / `pfor` / … — bare `{ }`, `do { }`, or any expression
1165    /// statement (wrapped as a one-line block, e.g. `` `cmd` pfor @a ``).
1166    fn stmt_into_parallel_block(&self, stmt: Statement) -> PerlResult<Block> {
1167        let line = stmt.line;
1168        match stmt.kind {
1169            StmtKind::Block(block) => Ok(block),
1170            StmtKind::Expression(expr) => {
1171                if let ExprKind::Do(ref inner) = expr.kind {
1172                    if let ExprKind::CodeRef { ref body, .. } = inner.kind {
1173                        return Ok(body.clone());
1174                    }
1175                }
1176                Ok(vec![Statement {
1177                    label: None,
1178                    kind: StmtKind::Expression(expr),
1179                    line,
1180                }])
1181            }
1182            _ => Err(self.syntax_err(
1183                "postfix parallel op expects `do { }`, a bare `{ }` block, or an expression statement",
1184                line,
1185            )),
1186        }
1187    }
1188
1189    /// `StmtKind::Expression` or a bare block (`StmtKind::Block`) as an [`Expr`] for postfix
1190    /// `while` / `until` / `for` / `foreach` (mirrors `do { }` → [`ExprKind::Do`](ExprKind::Do)([`CodeRef`](ExprKind::CodeRef))).
1191    fn stmt_into_postfix_body_expr(stmt: Statement) -> Option<Expr> {
1192        match stmt.kind {
1193            StmtKind::Expression(expr) => Some(expr),
1194            StmtKind::Block(block) => {
1195                let line = stmt.line;
1196                let inner = Expr {
1197                    kind: ExprKind::CodeRef {
1198                        params: vec![],
1199                        body: block,
1200                    },
1201                    line,
1202                };
1203                Some(Expr {
1204                    kind: ExprKind::Do(Box::new(inner)),
1205                    line,
1206                })
1207            }
1208            _ => None,
1209        }
1210    }
1211
1212    /// Statement-modifier keywords that must not be consumed as part of a comma-separated list
1213    /// (same set as [`parse_list_until_terminator`]).
1214    fn peek_is_postfix_stmt_modifier_keyword(&self) -> bool {
1215        matches!(
1216            self.peek(),
1217            Token::Ident(ref kw)
1218                if matches!(
1219                    kw.as_str(),
1220                    "if" | "unless" | "while" | "until" | "for" | "foreach"
1221                )
1222        )
1223    }
1224
1225    /// Token classes whose precedence sits below a Perl-style named unary
1226    /// operator. When one of these is the next token after a unary keyword
1227    /// (`length`, `len`, `cnt`, …), the keyword takes no explicit argument
1228    /// and the surrounding expression continues. Mirrors the `parse_one_arg_or_default`
1229    /// boundary set; kept as a separate predicate so other parse paths can
1230    /// reuse it without committing to default-to-`$_` semantics.
1231    fn peek_is_named_unary_terminator(&self) -> bool {
1232        matches!(
1233            self.peek(),
1234            Token::Semicolon
1235                | Token::RBrace
1236                | Token::RParen
1237                | Token::RBracket
1238                | Token::Eof
1239                | Token::Comma
1240                | Token::FatArrow
1241                | Token::PipeForward
1242                | Token::Question
1243                | Token::Colon
1244                | Token::NumEq
1245                | Token::NumNe
1246                | Token::NumLt
1247                | Token::NumGt
1248                | Token::NumLe
1249                | Token::NumGe
1250                | Token::Spaceship
1251                | Token::StrEq
1252                | Token::StrNe
1253                | Token::StrLt
1254                | Token::StrGt
1255                | Token::StrLe
1256                | Token::StrGe
1257                | Token::StrCmp
1258                | Token::LogAnd
1259                | Token::LogOr
1260                | Token::LogAndWord
1261                | Token::LogOrWord
1262                | Token::DefinedOr
1263                | Token::Range
1264                | Token::RangeExclusive
1265                | Token::Assign
1266                | Token::PlusAssign
1267                | Token::MinusAssign
1268                | Token::MulAssign
1269                | Token::DivAssign
1270                | Token::ModAssign
1271                | Token::PowAssign
1272                | Token::DotAssign
1273                | Token::AndAssign
1274                | Token::OrAssign
1275                | Token::XorAssign
1276                | Token::DefinedOrAssign
1277                | Token::ShiftLeftAssign
1278                | Token::ShiftRightAssign
1279                | Token::BitAndAssign
1280                | Token::BitOrAssign
1281        )
1282    }
1283
1284    fn maybe_postfix_modifier(&mut self, expr: Expr) -> PerlResult<Statement> {
1285        let line = expr.line;
1286        // Implicit semicolon: modifier keyword on a new line starts a new statement.
1287        if self.peek_line() > self.prev_line() {
1288            return Ok(Statement {
1289                label: None,
1290                kind: StmtKind::Expression(expr),
1291                line,
1292            });
1293        }
1294        match self.peek() {
1295            Token::Ident(ref kw) => match kw.as_str() {
1296                "if" => {
1297                    self.advance();
1298                    let cond = self.parse_expression()?;
1299                    Ok(Statement {
1300                        label: None,
1301                        kind: StmtKind::Expression(Expr {
1302                            kind: ExprKind::PostfixIf {
1303                                expr: Box::new(expr),
1304                                condition: Box::new(cond),
1305                            },
1306                            line,
1307                        }),
1308                        line,
1309                    })
1310                }
1311                "unless" => {
1312                    self.advance();
1313                    let cond = self.parse_expression()?;
1314                    Ok(Statement {
1315                        label: None,
1316                        kind: StmtKind::Expression(Expr {
1317                            kind: ExprKind::PostfixUnless {
1318                                expr: Box::new(expr),
1319                                condition: Box::new(cond),
1320                            },
1321                            line,
1322                        }),
1323                        line,
1324                    })
1325                }
1326                "while" => {
1327                    self.advance();
1328                    let cond = self.parse_expression()?;
1329                    Ok(Statement {
1330                        label: None,
1331                        kind: StmtKind::Expression(Expr {
1332                            kind: ExprKind::PostfixWhile {
1333                                expr: Box::new(expr),
1334                                condition: Box::new(cond),
1335                            },
1336                            line,
1337                        }),
1338                        line,
1339                    })
1340                }
1341                "until" => {
1342                    self.advance();
1343                    let cond = self.parse_expression()?;
1344                    Ok(Statement {
1345                        label: None,
1346                        kind: StmtKind::Expression(Expr {
1347                            kind: ExprKind::PostfixUntil {
1348                                expr: Box::new(expr),
1349                                condition: Box::new(cond),
1350                            },
1351                            line,
1352                        }),
1353                        line,
1354                    })
1355                }
1356                "for" | "foreach" => {
1357                    self.advance();
1358                    let list = self.parse_expression()?;
1359                    Ok(Statement {
1360                        label: None,
1361                        kind: StmtKind::Expression(Expr {
1362                            kind: ExprKind::PostfixForeach {
1363                                expr: Box::new(expr),
1364                                list: Box::new(list),
1365                            },
1366                            line,
1367                        }),
1368                        line,
1369                    })
1370                }
1371                _ => Ok(Statement {
1372                    label: None,
1373                    kind: StmtKind::Expression(expr),
1374                    line,
1375                }),
1376            },
1377            _ => Ok(Statement {
1378                label: None,
1379                kind: StmtKind::Expression(expr),
1380                line,
1381            }),
1382        }
1383    }
1384
1385    /// `name;` or `name}` — a bare identifier statement is a sub call with no explicit args (`$_` implied).
1386    fn try_parse_bareword_stmt_call(&mut self) -> Option<Expr> {
1387        let saved = self.pos;
1388        let line = self.peek_line();
1389        let mut name = match self.peek() {
1390            Token::Ident(n) => n.clone(),
1391            _ => return None,
1392        };
1393        // Names that begin `parse_named_expr` (builtins / `undef` / …) must use that path, not a sub call.
1394        if name.starts_with('\x00') || !Self::bareword_stmt_may_be_sub(&name) {
1395            return None;
1396        }
1397        self.advance();
1398        while self.eat(&Token::PackageSep) {
1399            match self.advance() {
1400                (Token::Ident(part), _) => {
1401                    name = format!("{}::{}", name, part);
1402                }
1403                _ => {
1404                    self.pos = saved;
1405                    return None;
1406                }
1407            }
1408        }
1409        match self.peek() {
1410            Token::Semicolon | Token::RBrace => Some(Expr {
1411                kind: ExprKind::FuncCall { name, args: vec![] },
1412                line,
1413            }),
1414            _ => {
1415                self.pos = saved;
1416                None
1417            }
1418        }
1419    }
1420
1421    /// Map an operator-keyword token (the lexer converts `eq`, `ne`, …, `and`,
1422    /// `or`, `not`, `x` to dedicated tokens) back to its identifier spelling.
1423    /// Used in hash-key contexts where the bareword form is the user's intent.
1424    pub(crate) fn operator_keyword_to_ident_str(tok: &Token) -> Option<&'static str> {
1425        Some(match tok {
1426            Token::StrEq => "eq",
1427            Token::StrNe => "ne",
1428            Token::StrLt => "lt",
1429            Token::StrGt => "gt",
1430            Token::StrLe => "le",
1431            Token::StrGe => "ge",
1432            Token::StrCmp => "cmp",
1433            Token::LogAndWord => "and",
1434            Token::LogOrWord => "or",
1435            Token::LogNotWord => "not",
1436            Token::X => "x",
1437            _ => return None,
1438        })
1439    }
1440
1441    /// Bare names that resolve to the topic-slot scalar matrix:
1442    /// `_`, `_0`, `_1`, …, `_N`, plus `_<+`, `_N<+` for the 4-deep outer chain.
1443    /// These must NOT be treated as zero-arg sub calls — they're scalar var refs.
1444    pub(crate) fn is_underscore_topic_slot(name: &str) -> bool {
1445        if name == "_" {
1446            return true;
1447        }
1448        if !name.starts_with('_') || name.len() < 2 {
1449            return false;
1450        }
1451        let bytes = name.as_bytes();
1452        let mut i = 1;
1453        // Optional digit run (positional slot index).
1454        while i < bytes.len() && bytes[i].is_ascii_digit() {
1455            i += 1;
1456        }
1457        // Then any number of `<` chevrons (runtime cap at 5; lexer accepts more).
1458        let chevrons_start = i;
1459        while i < bytes.len() && bytes[i] == b'<' {
1460            i += 1;
1461        }
1462        // Must be one of: `_`, `_N`, `_<+`, `_N<+`. No other trailing chars.
1463        i == bytes.len() && (i > 1 || chevrons_start > 1)
1464    }
1465
1466    /// Bareword names that map to Perl special variables / filehandles /
1467    /// compile-time tokens. A user-defined sub with any of these names
1468    /// would shadow the special variable's expression-position usage and
1469    /// produce silently-broken code. Reject at parse time with a
1470    /// foot-gun error message.
1471    ///
1472    /// Sigil-form spellings (`$@`, `$!`, `@ARGV`, `%ENV`, etc.) are caught
1473    /// separately via the `parse_sub_decl` catch-all branch — those don't
1474    /// even lex as `Token::Ident` so they hit a different code path.
1475    pub(crate) fn is_reserved_special_var_name(name: &str) -> bool {
1476        matches!(
1477            name,
1478            // Standard filehandles (Perl: STDIN, STDOUT, STDERR, ARGV, …)
1479            "STDIN" | "STDOUT" | "STDERR" | "ARGV" | "ARGVOUT" | "DATA"
1480            // Package globals, normally accessed via sigils (@ARGV, %ENV,
1481            // @INC, %SIG, @ISA, %ENV, etc.) — bareword shadow is a foot-gun.
1482            // NOTE: `AUTOLOAD` is intentionally NOT in this list — `fn
1483            // AUTOLOAD { ... }` is the legitimate Perl idiom for handling
1484            // missing-method dispatch. The runtime sets `$AUTOLOAD` to the
1485            // missing sub's qualified name before invoking the user's
1486            // AUTOLOAD sub. Adding it here would break that mechanism.
1487            | "ENV" | "INC" | "SIG" | "ISA"
1488            | "EXPORT" | "EXPORT_OK" | "EXPORT_TAGS"
1489            | "VERSION"
1490            // Compile-time tokens (resolve to constants at parse time).
1491            | "__FILE__" | "__LINE__" | "__PACKAGE__" | "__SUB__"
1492            | "__DATA__" | "__END__"
1493        )
1494    }
1495
1496    /// Identifiers that start a [`parse_named_expr`] arm (builtins / special forms), not a bare sub call.
1497    fn bareword_stmt_may_be_sub(name: &str) -> bool {
1498        // Topic-slot scalar names (`_`, `_N`, `_<+`, `_N<+`) are scalar
1499        // variables, not zero-arg sub calls. Without this guard, the
1500        // statement-position parser would emit `Op::Call("_0", 0)` and fail
1501        // at runtime with "Undefined subroutine &_0".
1502        if Self::is_underscore_topic_slot(name) {
1503            return false;
1504        }
1505        !matches!(
1506            name,
1507            "__FILE__"
1508                | "__LINE__"
1509                | "abs"
1510                | "async"
1511                | "spawn"
1512                | "atan2"
1513                | "await"
1514                | "barrier"
1515                | "bless"
1516                | "caller"
1517                | "capture"
1518                | "cat"
1519                | "chdir"
1520                | "chmod"
1521                | "chomp"
1522                | "chop"
1523                | "chr"
1524                | "chown"
1525                | "closedir"
1526                | "close"
1527                | "collect"
1528                | "cos"
1529                | "crypt"
1530                | "defined"
1531                | "dec"
1532                | "delete"
1533                | "die"
1534                | "deque"
1535                | "do"
1536                | "each"
1537                | "eof"
1538                | "fore"
1539                | "eval"
1540                | "exec"
1541                | "exists"
1542                | "exit"
1543                | "exp"
1544                | "fan"
1545                | "fan_cap"
1546                | "fc"
1547                | "fetch_url"
1548                | "d"
1549                | "dirs"
1550                | "dr"
1551                | "f"
1552                | "fi"
1553                | "files"
1554                | "filesf"
1555                | "filter"
1556                | "fr"
1557                | "getcwd"
1558                | "glob_par"
1559                | "par_sed"
1560                | "glob"
1561                | "grep"
1562                | "greps"
1563                | "heap"
1564                | "hex"
1565                | "inc"
1566                | "index"
1567                | "int"
1568                | "join"
1569                | "keys"
1570                | "lcfirst"
1571                | "lc"
1572                | "length"
1573                | "link"
1574                | "log"
1575                | "lstat"
1576                | "map"
1577                | "flat_map"
1578                | "maps"
1579                | "flat_maps"
1580                | "flatten"
1581                | "frequencies"
1582                | "freq"
1583                | "interleave"
1584                | "ddump"
1585                | "stringify"
1586                | "str"
1587                | "s"
1588                | "input"
1589                | "lines"
1590                | "words"
1591                | "chars"
1592                | "digits"
1593                | "letters"
1594                | "letters_uc"
1595                | "letters_lc"
1596                | "punctuation"
1597                | "sentences"
1598                | "paragraphs"
1599                | "sections"
1600                | "numbers"
1601                | "graphemes"
1602                | "columns"
1603                | "trim"
1604                | "avg"
1605                | "top"
1606                | "pager"
1607                | "pg"
1608                | "less"
1609                | "count_by"
1610                | "to_file"
1611                | "to_json"
1612                | "to_csv"
1613                | "grep_v"
1614                | "select_keys"
1615                | "pluck"
1616                | "clamp"
1617                | "normalize"
1618                | "stddev"
1619                | "squared"
1620                | "square"
1621                | "cubed"
1622                | "cube"
1623                | "expt"
1624                | "pow"
1625                | "pw"
1626                | "snake_case"
1627                | "camel_case"
1628                | "kebab_case"
1629                | "to_toml"
1630                | "to_yaml"
1631                | "to_xml"
1632                | "to_html"
1633                | "to_markdown"
1634                | "xopen"
1635                | "clip"
1636                | "paste"
1637                | "to_table"
1638                | "sparkline"
1639                | "bar_chart"
1640                | "flame"
1641                | "set"
1642                | "list_count"
1643                | "list_size"
1644                | "count"
1645                | "size"
1646                | "cnt"
1647                | "len"
1648                | "all"
1649                | "any"
1650                | "none"
1651                | "take_while"
1652                | "drop_while"
1653                | "skip_while"
1654                | "skip"
1655                | "first_or"
1656                | "tap"
1657                | "peek"
1658                | "partition"
1659                | "min_by"
1660                | "max_by"
1661                | "zip_with"
1662                | "group_by"
1663                | "chunk_by"
1664                | "with_index"
1665                | "puniq"
1666                | "pfirst"
1667                | "pany"
1668                | "uniq"
1669                | "distinct"
1670                | "shuffle"
1671                | "shuffled"
1672                | "chunked"
1673                | "windowed"
1674                | "match"
1675                | "mkdir"
1676                | "every"
1677                | "gen"
1678                | "oct"
1679                | "open"
1680                | "p"
1681                | "opendir"
1682                | "ord"
1683                | "par_lines"
1684                | "par_walk"
1685                | "pipe"
1686                | "pipes"
1687                | "block_devices"
1688                | "char_devices"
1689                | "exe"
1690                | "executables"
1691                | "rate_limit"
1692                | "retry"
1693                | "pcache"
1694                | "pchannel"
1695                | "pfor"
1696                | "pgrep"
1697                | "pgreps"
1698                | "pipeline"
1699                | "pmap_chunked"
1700                | "pmap_reduce"
1701                | "pmap_on"
1702                | "pflat_map_on"
1703                | "pmap"
1704                | "pmaps"
1705                | "pflat_map"
1706                | "pflat_maps"
1707                | "pop"
1708                | "pos"
1709                | "ppool"
1710                | "preduce_init"
1711                | "preduce"
1712                | "pselect"
1713                | "printf"
1714                | "print"
1715                | "pr"
1716                | "psort"
1717                | "push"
1718                | "pwatch"
1719                | "rand"
1720                | "readdir"
1721                | "readlink"
1722                | "reduce"
1723                | "fold"
1724                | "inject"
1725                | "first"
1726                | "detect"
1727                | "find"
1728                | "find_all"
1729                | "ref"
1730                | "rename"
1731                | "require"
1732                | "rev"
1733                | "reverse"
1734                | "reversed"
1735                | "rewinddir"
1736                | "rindex"
1737                | "rmdir"
1738                | "rm"
1739                | "say"
1740                | "scalar"
1741                | "seekdir"
1742                | "shift"
1743                | "sin"
1744                | "slurp"
1745                | "sockets"
1746                | "sort"
1747                | "splice"
1748                | "splice_last"
1749                | "splice1"
1750                | "spl_last"
1751                | "split"
1752                | "sprintf"
1753                | "sqrt"
1754                | "srand"
1755                | "stat"
1756                | "study"
1757                | "substr"
1758                | "symlink"
1759                | "sym_links"
1760                | "system"
1761                | "telldir"
1762                | "timer"
1763                | "trace"
1764                | "ucfirst"
1765                | "uc"
1766                | "undef"
1767                | "umask"
1768                | "unlink"
1769                | "unshift"
1770                | "utime"
1771                | "values"
1772                | "wantarray"
1773                | "warn"
1774                | "watch"
1775                | "yield"
1776                | "sub"
1777        )
1778    }
1779
1780    fn parse_block(&mut self) -> PerlResult<Block> {
1781        self.expect(&Token::LBrace)?;
1782        // Statements inside a block are NOT pipe RHS - reset depth so nested `~>`
1783        // parses its own input instead of using `$_[0]` placeholder.
1784        let saved_pipe_rhs_depth = self.pipe_rhs_depth;
1785        self.pipe_rhs_depth = 0;
1786        self.block_depth += 1;
1787        let mut stmts = Vec::new();
1788        // `{ |$a, $b| body }` — Ruby-style block params.
1789        // Desugars to `my $a = $_` (1 param), `my $a = $a; my $b = $b` (2 — sort/reduce),
1790        // or `my $p = $_N` for positional N≥3.
1791        if let Some(param_stmts) = self.try_parse_block_params()? {
1792            stmts.extend(param_stmts);
1793        }
1794        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
1795            if self.eat(&Token::Semicolon) {
1796                continue;
1797            }
1798            stmts.push(self.parse_statement()?);
1799        }
1800        self.expect(&Token::RBrace)?;
1801        self.pipe_rhs_depth = saved_pipe_rhs_depth;
1802        self.block_depth -= 1;
1803        Self::default_topic_for_sole_bareword(&mut stmts);
1804        Ok(stmts)
1805    }
1806
1807    /// Try to parse `|$var1, $var2, ...|` at the start of a block.
1808    /// Returns `None` if the leading `|` is not block-param syntax.
1809    /// When successful, returns `my $var = <implicit>` assignment statements
1810    /// that alias the block's positional arguments.
1811    fn try_parse_block_params(&mut self) -> PerlResult<Option<Vec<Statement>>> {
1812        if !matches!(self.peek(), Token::BitOr) {
1813            return Ok(None);
1814        }
1815        // Lookahead: `| $scalar [, $scalar]* |` — verify before consuming.
1816        let mut i = 1; // skip the opening `|`
1817        loop {
1818            match self.peek_at(i) {
1819                Token::ScalarVar(_) => i += 1,
1820                _ => return Ok(None), // not `|$var...|`
1821            }
1822            match self.peek_at(i) {
1823                Token::BitOr => break,  // closing `|`
1824                Token::Comma => i += 1, // more params
1825                _ => return Ok(None),   // not block params
1826            }
1827        }
1828        // Confirmed — consume and build assignments.
1829        let line = self.peek_line();
1830        self.advance(); // eat opening `|`
1831        let mut names = Vec::new();
1832        loop {
1833            if let Token::ScalarVar(ref name) = self.peek().clone() {
1834                names.push(name.clone());
1835                self.advance();
1836            }
1837            if self.eat(&Token::BitOr) {
1838                break;
1839            }
1840            self.expect(&Token::Comma)?;
1841        }
1842        // Generate `my $name = <source>` for each param.
1843        // 1 param  → source is `$_` (map/grep/each/for topic)
1844        // 2 params → sources are `$a`, `$b` (sort/reduce)
1845        // N params → sources are `$_`, `$_1`, `$_2`, … (positional)
1846        let sources: Vec<&str> = match names.len() {
1847            1 => vec!["_"],
1848            2 => vec!["a", "b"],
1849            n => {
1850                // Can't return borrowed from a generated vec, handle below.
1851                let _ = n;
1852                vec![] // sentinel — handled in the else branch
1853            }
1854        };
1855        let mut stmts = Vec::with_capacity(names.len());
1856        if !sources.is_empty() {
1857            for (name, src) in names.iter().zip(sources.iter()) {
1858                stmts.push(Statement {
1859                    label: None,
1860                    kind: StmtKind::My(vec![VarDecl {
1861                        sigil: Sigil::Scalar,
1862                        name: name.clone(),
1863                        initializer: Some(Expr {
1864                            kind: ExprKind::ScalarVar(src.to_string()),
1865                            line,
1866                        }),
1867                        frozen: false,
1868                        type_annotation: None,
1869                    }]),
1870                    line,
1871                });
1872            }
1873        } else {
1874            // N≥3: positional `$_`, `$_1`, `$_2`, …
1875            for (idx, name) in names.iter().enumerate() {
1876                let src = if idx == 0 {
1877                    "_".to_string()
1878                } else {
1879                    format!("_{idx}")
1880                };
1881                stmts.push(Statement {
1882                    label: None,
1883                    kind: StmtKind::My(vec![VarDecl {
1884                        sigil: Sigil::Scalar,
1885                        name: name.clone(),
1886                        initializer: Some(Expr {
1887                            kind: ExprKind::ScalarVar(src),
1888                            line,
1889                        }),
1890                        frozen: false,
1891                        type_annotation: None,
1892                    }]),
1893                    line,
1894                });
1895            }
1896        }
1897        Ok(Some(stmts))
1898    }
1899
1900    /// Block shorthand: when the body is literally one bare builtin call
1901    /// (`{ uc }`, `{ basename }`, `{ to_json }`), inject `$_` as its first
1902    /// argument so `map { basename }` == `map { basename($_) }` uniformly.
1903    ///
1904    /// Without this, the ExprKind-modeled core names (`uc`/`lc`/`length`/…)
1905    /// default to `$_` via their own parse arms, but generic `FuncCall`-
1906    /// dispatched builtins (`basename`/`to_json`/`tj`/`bn`) are called with
1907    /// empty args and return the wrong value. This rewrite levels the
1908    /// playing field at parse time — no per-builtin handling needed.
1909    ///
1910    /// Narrow by design: fires only when the block has *exactly one*
1911    /// expression statement whose sole content is a known-bareword call
1912    /// with zero args. Multi-statement blocks and blocks with any other
1913    /// content are untouched.
1914    fn default_topic_for_sole_bareword(stmts: &mut [Statement]) {
1915        let [only] = stmts else { return };
1916        let StmtKind::Expression(ref mut expr) = only.kind else {
1917            return;
1918        };
1919        let topic_line = expr.line;
1920        let topic_arg = || Expr {
1921            kind: ExprKind::ScalarVar("_".to_string()),
1922            line: topic_line,
1923        };
1924        match expr.kind {
1925            // Zero-arg FuncCall whose name is a known builtin → inject `$_`.
1926            ExprKind::FuncCall {
1927                ref name,
1928                ref mut args,
1929            } if args.is_empty()
1930                && (Self::is_known_bareword(name) || Self::is_try_builtin_name(name)) =>
1931            {
1932                args.push(topic_arg());
1933            }
1934            // Lone bareword (the parser sometimes keeps a bareword as a
1935            // `Bareword` node instead of a zero-arg `FuncCall` —
1936            // e.g. `{ to_json }`, `{ ddump }`). Promote to a call.
1937            ExprKind::Bareword(ref name)
1938                if (Self::is_known_bareword(name) || Self::is_try_builtin_name(name)) =>
1939            {
1940                let n = name.clone();
1941                expr.kind = ExprKind::FuncCall {
1942                    name: n,
1943                    args: vec![topic_arg()],
1944                };
1945            }
1946            _ => {}
1947        }
1948    }
1949
1950    /// `defer { BLOCK }` — register a block to run when the current scope exits.
1951    /// Desugars to a `defer__internal(fn { BLOCK })` function call that the compiler
1952    /// handles specially by emitting Op::DeferBlock.
1953    fn parse_defer_stmt(&mut self) -> PerlResult<Statement> {
1954        let line = self.peek_line();
1955        self.advance(); // defer
1956        let body = self.parse_block()?;
1957        self.eat(&Token::Semicolon);
1958        // Desugar: defer { BLOCK } → defer__internal(fn { BLOCK })
1959        let coderef = Expr {
1960            kind: ExprKind::CodeRef {
1961                params: vec![],
1962                body,
1963            },
1964            line,
1965        };
1966        Ok(Statement {
1967            label: None,
1968            kind: StmtKind::Expression(Expr {
1969                kind: ExprKind::FuncCall {
1970                    name: "defer__internal".to_string(),
1971                    args: vec![coderef],
1972                },
1973                line,
1974            }),
1975            line,
1976        })
1977    }
1978
1979    /// `try { } catch ($err) { }` with optional `finally { }`
1980    fn parse_try_catch(&mut self) -> PerlResult<Statement> {
1981        let line = self.peek_line();
1982        self.advance(); // try
1983        let try_block = self.parse_block()?;
1984        match self.peek() {
1985            Token::Ident(ref k) if k == "catch" => {
1986                self.advance();
1987            }
1988            _ => {
1989                return Err(self.syntax_err("expected 'catch' after try block", self.peek_line()));
1990            }
1991        }
1992        self.expect(&Token::LParen)?;
1993        let catch_var = self.parse_scalar_var_name()?;
1994        self.expect(&Token::RParen)?;
1995        let catch_block = self.parse_block()?;
1996        let finally_block = match self.peek() {
1997            Token::Ident(ref k) if k == "finally" => {
1998                self.advance();
1999                Some(self.parse_block()?)
2000            }
2001            _ => None,
2002        };
2003        self.eat(&Token::Semicolon);
2004        Ok(Statement {
2005            label: None,
2006            kind: StmtKind::TryCatch {
2007                try_block,
2008                catch_var,
2009                catch_block,
2010                finally_block,
2011            },
2012            line,
2013        })
2014    }
2015
2016    /// `thread EXPR stage1 stage2 ...` — Clojure-style threading macro.
2017    /// Desugars to `EXPR |> stage1 |> stage2 |> ...`
2018    ///
2019    /// When `thread_last` is true (`->>` syntax), injects as last arg instead of first.
2020    ///
2021    /// When invoked as the RHS of `|>` (e.g. `LHS |> t s1 s2 ...`), the init
2022    /// is not parsed from tokens — using `parse_unary()` there lets the first
2023    /// bareword greedily consume the next token as its arg, which misparses
2024    /// `t inc pow($_, 2) p` as init=`inc(pow(…))` + stage=`p` instead of three
2025    /// separate stages. Instead, seed init with `$_[0]`, run every remaining
2026    /// token through the stage loop, and wrap the resulting chain in a
2027    /// `CodeRef`. The outer `pipe_forward_apply` then calls it with `lhs` as
2028    /// `$_[0]`, giving `LHS |> t s1 s2 s3` == `LHS |> s1 |> s2 |> s3`.
2029    fn parse_thread_macro(&mut self, _line: usize, thread_last: bool) -> PerlResult<Expr> {
2030        // Set thread-last mode for pipe_forward_apply calls within this macro
2031        let saved_thread_last = self.thread_last_mode;
2032        self.thread_last_mode = thread_last;
2033
2034        let pipe_rhs_wrap = self.in_pipe_rhs();
2035        let mut result = if pipe_rhs_wrap {
2036            Expr {
2037                kind: ExprKind::ArrayElement {
2038                    array: "_".to_string(),
2039                    index: Box::new(Expr {
2040                        kind: ExprKind::Integer(0),
2041                        line: _line,
2042                    }),
2043                },
2044                line: _line,
2045            }
2046        } else {
2047            // Suppress paren-less function calls so `t Color::Red p` parses
2048            // the enum variant without consuming `p` as an argument.
2049            self.suppress_parenless_call = self.suppress_parenless_call.saturating_add(1);
2050            let expr = self.parse_thread_input();
2051            self.suppress_parenless_call = self.suppress_parenless_call.saturating_sub(1);
2052            expr?
2053        };
2054
2055        // Track line where the last stage ended (initially the input expression's line).
2056        let mut last_stage_end_line = self.prev_line();
2057
2058        // Parse stages until we hit a statement terminator
2059        loop {
2060            // Newline termination: if the next token is on a different line than where
2061            // the previous stage ended, the thread macro terminates. This allows
2062            // `~> @arr map { $_ * 2 }` on one line followed by `my @b = ...` on the next
2063            // without requiring a semicolon.
2064            if self.peek_line() > last_stage_end_line {
2065                break;
2066            }
2067
2068            // Check for terminators - |> ends thread and allows piping the result.
2069            // Variables ($x, @x, %x) and declaration keywords (my, our, local, state)
2070            // cannot be stages, so they implicitly terminate the thread macro.
2071            match self.peek() {
2072                Token::Semicolon
2073                | Token::RBrace
2074                | Token::RParen
2075                | Token::RBracket
2076                | Token::PipeForward
2077                | Token::Eof
2078                | Token::ScalarVar(_)
2079                | Token::ArrayVar(_)
2080                | Token::HashVar(_)
2081                | Token::Comma => break,
2082                Token::Ident(ref kw)
2083                    if matches!(
2084                        kw.as_str(),
2085                        "my" | "our"
2086                            | "local"
2087                            | "state"
2088                            | "if"
2089                            | "unless"
2090                            | "while"
2091                            | "until"
2092                            | "for"
2093                            | "foreach"
2094                            | "return"
2095                            | "last"
2096                            | "next"
2097                            | "redo"
2098                    ) =>
2099                {
2100                    break
2101                }
2102                _ => {}
2103            }
2104
2105            let stage_line = self.peek_line();
2106
2107            // Parse a stage and apply it to result via pipe
2108            match self.peek().clone() {
2109                // `>{ block }` — standalone anonymous block (sugar for fn { })
2110                Token::ArrowBrace => {
2111                    self.advance(); // consume `>{`
2112                    let mut stmts = Vec::new();
2113                    while !matches!(self.peek(), Token::RBrace | Token::Eof) {
2114                        if self.eat(&Token::Semicolon) {
2115                            continue;
2116                        }
2117                        stmts.push(self.parse_statement()?);
2118                    }
2119                    self.expect(&Token::RBrace)?;
2120                    let code_ref = Expr {
2121                        kind: ExprKind::CodeRef {
2122                            params: vec![],
2123                            body: stmts,
2124                        },
2125                        line: stage_line,
2126                    };
2127                    result = self.pipe_forward_apply(result, code_ref, stage_line)?;
2128                }
2129                // `sub { block }` — blocked in no-interop mode
2130                Token::Ident(ref name) if name == "sub" => {
2131                    if crate::no_interop_mode() {
2132                        return Err(self.syntax_err(
2133                            "stryke uses `fn {}` instead of `sub {}` (--no-interop)",
2134                            stage_line,
2135                        ));
2136                    }
2137                    self.advance(); // consume `sub`
2138                    let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
2139                    let body = self.parse_block()?;
2140                    let code_ref = Expr {
2141                        kind: ExprKind::CodeRef { params, body },
2142                        line: stage_line,
2143                    };
2144                    result = self.pipe_forward_apply(result, code_ref, stage_line)?;
2145                }
2146                // `fn { block }` — stryke anonymous function
2147                Token::Ident(ref name) if name == "fn" => {
2148                    self.advance(); // consume `fn`
2149                    let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
2150                    self.parse_sub_attributes()?;
2151                    let body = self.parse_fn_eq_body_or_block(false)?;
2152                    let code_ref = Expr {
2153                        kind: ExprKind::CodeRef { params, body },
2154                        line: stage_line,
2155                    };
2156                    result = self.pipe_forward_apply(result, code_ref, stage_line)?;
2157                }
2158                // `ident` possibly followed by block (or namespaced like `Foo::Bar::func`)
2159                Token::Ident(ref name) => {
2160                    let mut func_name = name.clone();
2161                    self.advance();
2162
2163                    // Collect namespaced function name (e.g., Rosetta::Stack::push)
2164                    while matches!(self.peek(), Token::PackageSep) {
2165                        self.advance(); // consume `::`
2166                        if let Token::Ident(ref part) = self.peek().clone() {
2167                            func_name.push_str("::");
2168                            func_name.push_str(part);
2169                            self.advance();
2170                        } else {
2171                            return Err(self.syntax_err(
2172                                format!(
2173                                    "Expected identifier after `::` in thread stage, got {:?}",
2174                                    self.peek()
2175                                ),
2176                                stage_line,
2177                            ));
2178                        }
2179                    }
2180
2181                    // Handle s/// and tr/// encoded tokens
2182                    if func_name.starts_with('\x00') {
2183                        let parts: Vec<&str> = func_name.split('\x00').collect();
2184                        if parts.len() >= 4 && parts[1] == "s" {
2185                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
2186                            let stage = Expr {
2187                                kind: ExprKind::Substitution {
2188                                    expr: Box::new(result.clone()),
2189                                    pattern: parts[2].to_string(),
2190                                    replacement: parts[3].to_string(),
2191                                    flags: format!("{}r", parts.get(4).unwrap_or(&"")),
2192                                    delim,
2193                                },
2194                                line: stage_line,
2195                            };
2196                            result = stage;
2197                            last_stage_end_line = self.prev_line();
2198                            continue;
2199                        }
2200                        if parts.len() >= 4 && parts[1] == "tr" {
2201                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
2202                            let stage = Expr {
2203                                kind: ExprKind::Transliterate {
2204                                    expr: Box::new(result.clone()),
2205                                    from: parts[2].to_string(),
2206                                    to: parts[3].to_string(),
2207                                    flags: format!("{}r", parts.get(4).unwrap_or(&"")),
2208                                    delim,
2209                                },
2210                                line: stage_line,
2211                            };
2212                            result = stage;
2213                            last_stage_end_line = self.prev_line();
2214                            continue;
2215                        }
2216                        return Err(
2217                            self.syntax_err("Unexpected encoded token in thread", stage_line)
2218                        );
2219                    }
2220
2221                    // `map +{ ... }` — hashref expression form (not a code block).
2222                    // The `+` disambiguates: `+{` is always a hashref constructor.
2223                    // Desugars to `MapExprComma` so pipe_forward_apply threads the
2224                    // list correctly: `t LIST map +{k => $_}` → `map +{k => $_}, LIST`.
2225                    if matches!(self.peek(), Token::Plus)
2226                        && matches!(self.peek_at(1), Token::LBrace)
2227                    {
2228                        self.advance(); // consume `+`
2229                        self.expect(&Token::LBrace)?;
2230                        // try_parse_hash_ref consumes the closing `}`
2231                        let pairs = self.try_parse_hash_ref()?;
2232                        let hashref_expr = Expr {
2233                            kind: ExprKind::HashRef(pairs),
2234                            line: stage_line,
2235                        };
2236                        let flatten_array_refs =
2237                            matches!(func_name.as_str(), "flat_map" | "flat_maps");
2238                        let stream = matches!(func_name.as_str(), "maps" | "flat_maps");
2239                        // Placeholder list — pipe_forward_apply replaces it with `result`.
2240                        let placeholder = Expr {
2241                            kind: ExprKind::Undef,
2242                            line: stage_line,
2243                        };
2244                        let map_node = Expr {
2245                            kind: ExprKind::MapExprComma {
2246                                expr: Box::new(hashref_expr),
2247                                list: Box::new(placeholder),
2248                                flatten_array_refs,
2249                                stream,
2250                            },
2251                            line: stage_line,
2252                        };
2253                        result = self.pipe_forward_apply(result, map_node, stage_line)?;
2254                    // `pmap_chunked CHUNK_SIZE { BLOCK }` — parallel chunked map
2255                    } else if func_name == "pmap_chunked" {
2256                        let chunk_size = self.parse_assign_expr()?;
2257                        let block = self.parse_block_or_bareword_block()?;
2258                        let placeholder = self.pipe_placeholder_list(stage_line);
2259                        let stage = Expr {
2260                            kind: ExprKind::PMapChunkedExpr {
2261                                chunk_size: Box::new(chunk_size),
2262                                block,
2263                                list: Box::new(placeholder),
2264                                progress: None,
2265                            },
2266                            line: stage_line,
2267                        };
2268                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2269                    // `preduce_init INIT { BLOCK }` — parallel reduce with init value
2270                    } else if func_name == "preduce_init" {
2271                        let init = self.parse_assign_expr()?;
2272                        let block = self.parse_block_or_bareword_block()?;
2273                        let placeholder = self.pipe_placeholder_list(stage_line);
2274                        let stage = Expr {
2275                            kind: ExprKind::PReduceInitExpr {
2276                                init: Box::new(init),
2277                                block,
2278                                list: Box::new(placeholder),
2279                                progress: None,
2280                            },
2281                            line: stage_line,
2282                        };
2283                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2284                    // `pmap_reduce { MAP } { REDUCE }` — parallel map-reduce
2285                    } else if func_name == "pmap_reduce" {
2286                        let map_block = self.parse_block_or_bareword_block()?;
2287                        let reduce_block = if matches!(self.peek(), Token::LBrace) {
2288                            self.parse_block()?
2289                        } else {
2290                            self.expect(&Token::Comma)?;
2291                            self.parse_block_or_bareword_cmp_block()?
2292                        };
2293                        let placeholder = self.pipe_placeholder_list(stage_line);
2294                        let stage = Expr {
2295                            kind: ExprKind::PMapReduceExpr {
2296                                map_block,
2297                                reduce_block,
2298                                list: Box::new(placeholder),
2299                                progress: None,
2300                            },
2301                            line: stage_line,
2302                        };
2303                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2304                    // `pmap_on $cluster { BLOCK }` — parallel map dispatched to a remote
2305                    // cluster. Mirrors the `pmap_chunked` thread-stage shape; the cluster
2306                    // expression is parsed before the block, the threaded list slots in
2307                    // as the placeholder.
2308                    } else if func_name == "pmap_on" || func_name == "pflat_map_on" {
2309                        // Suppress `$cluster { ... }` auto-arrow (`$h->{...}`) so the
2310                        // brace opens the block, not a hash subscript.
2311                        self.suppress_scalar_hash_brace =
2312                            self.suppress_scalar_hash_brace.saturating_add(1);
2313                        let cluster = self.parse_assign_expr();
2314                        self.suppress_scalar_hash_brace =
2315                            self.suppress_scalar_hash_brace.saturating_sub(1);
2316                        let cluster = cluster?;
2317                        // Optional comma between cluster and block (matches the
2318                        // canonical `pmap_on $c, { BLOCK } @list` form in the LSP docs).
2319                        self.eat(&Token::Comma);
2320                        let block = self.parse_block_or_bareword_block()?;
2321                        let placeholder = self.pipe_placeholder_list(stage_line);
2322                        let stage = Expr {
2323                            kind: ExprKind::PMapExpr {
2324                                block,
2325                                list: Box::new(placeholder),
2326                                progress: None,
2327                                flat_outputs: func_name == "pflat_map_on",
2328                                on_cluster: Some(Box::new(cluster)),
2329                                stream: false,
2330                            },
2331                            line: stage_line,
2332                        };
2333                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2334                    // Check if followed by a block (like `filter { }`, `sort { }`, `map { }`)
2335                    } else if matches!(self.peek(), Token::LBrace) {
2336                        // Parse as a block-taking builtin
2337                        self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_add(1);
2338                        let stage = self.parse_thread_stage_with_block(&func_name, stage_line)?;
2339                        self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_sub(1);
2340                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2341                    } else if matches!(self.peek(), Token::LParen) {
2342                        // Special handling for join(sep) and split(pattern) in thread context.
2343                        // These take the threaded list/string as their data argument, not as $_.
2344                        if func_name == "join" {
2345                            self.advance(); // consume `(`
2346                            let separator = self.parse_assign_expr()?;
2347                            self.expect(&Token::RParen)?;
2348                            let placeholder = self.pipe_placeholder_list(stage_line);
2349                            let stage = Expr {
2350                                kind: ExprKind::JoinExpr {
2351                                    separator: Box::new(separator),
2352                                    list: Box::new(placeholder),
2353                                },
2354                                line: stage_line,
2355                            };
2356                            result = self.pipe_forward_apply(result, stage, stage_line)?;
2357                        } else if func_name == "split" {
2358                            self.advance(); // consume `(`
2359                            let pattern = self.parse_assign_expr()?;
2360                            let limit = if self.eat(&Token::Comma) {
2361                                Some(Box::new(self.parse_assign_expr()?))
2362                            } else {
2363                                None
2364                            };
2365                            self.expect(&Token::RParen)?;
2366                            let placeholder = Expr {
2367                                kind: ExprKind::ScalarVar("_".to_string()),
2368                                line: stage_line,
2369                            };
2370                            let stage = Expr {
2371                                kind: ExprKind::SplitExpr {
2372                                    pattern: Box::new(pattern),
2373                                    string: Box::new(placeholder),
2374                                    limit,
2375                                },
2376                                line: stage_line,
2377                            };
2378                            result = self.pipe_forward_apply(result, stage, stage_line)?;
2379                        } else {
2380                            // `name($_-bearing-args)` — parse explicit args, require at
2381                            // least one `$_` placeholder, then wrap as a `>{...}` block
2382                            // so the threaded value binds to `$_` at any position.
2383                            // Examples:
2384                            //   t 10 add2($_, 5) p      → add2(10, 5)
2385                            //   t 10 sub2(20, $_) p     → sub2(20, 10)
2386                            //   t 10 add3($_, 5, 10) p  → add3(10, 5, 10)
2387                            // To pass the threaded value as a sole arg, use bare form:
2388                            //   t 10 add2 p   (not `add2()`)
2389                            self.advance(); // consume `(`
2390                            let mut call_args = Vec::new();
2391                            while !matches!(self.peek(), Token::RParen | Token::Eof) {
2392                                call_args.push(self.parse_assign_expr()?);
2393                                if !self.eat(&Token::Comma) {
2394                                    break;
2395                                }
2396                            }
2397                            self.expect(&Token::RParen)?;
2398                            // If no `$_` placeholder, auto-inject threaded value.
2399                            // Thread-first: `t data to_file("/tmp/o.html")` → `to_file($_, "/tmp/o.html")`
2400                            // Thread-last: `->> data to_file("/tmp/o.html")` → `to_file("/tmp/o.html", $_)`
2401                            if !call_args.iter().any(Self::expr_contains_topic_var) {
2402                                let topic = Expr {
2403                                    kind: ExprKind::ScalarVar("_".to_string()),
2404                                    line: stage_line,
2405                                };
2406                                if self.thread_last_mode {
2407                                    call_args.push(topic);
2408                                } else {
2409                                    call_args.insert(0, topic);
2410                                }
2411                            }
2412                            let call_expr = Expr {
2413                                kind: ExprKind::FuncCall {
2414                                    name: func_name.clone(),
2415                                    args: call_args,
2416                                },
2417                                line: stage_line,
2418                            };
2419                            let code_ref = Expr {
2420                                kind: ExprKind::CodeRef {
2421                                    params: vec![],
2422                                    body: vec![Statement {
2423                                        label: None,
2424                                        kind: StmtKind::Expression(call_expr),
2425                                        line: stage_line,
2426                                    }],
2427                                },
2428                                line: stage_line,
2429                            };
2430                            result = self.pipe_forward_apply(result, code_ref, stage_line)?;
2431                        }
2432                    } else {
2433                        // Bare function name — handle unary builtins specially
2434                        result = self.thread_apply_bare_func(&func_name, result, stage_line)?;
2435                    }
2436                }
2437                // `/pattern/flags` — grep filter (desugar to `grep { /pattern/flags }`)
2438                Token::Regex(ref pattern, ref flags, delim) => {
2439                    let pattern = pattern.clone();
2440                    let flags = flags.clone();
2441                    self.advance();
2442                    result =
2443                        self.thread_regex_grep_stage(result, pattern, flags, delim, stage_line);
2444                }
2445                // Handle `/` that was lexed as Slash (division) because it followed a term.
2446                // In thread stage context, `/pattern/` should be a regex filter.
2447                Token::Slash => {
2448                    self.advance(); // consume opening /
2449
2450                    // Special case: if next token is Ident("m") or similar followed by Regex,
2451                    // the lexer interpreted `/m/` as `/ m/pattern/` where `m/` started a new regex.
2452                    // We need to handle this: the pattern is just "m" (or whatever the ident is).
2453                    if let Token::Ident(ref ident_s) = self.peek().clone() {
2454                        if matches!(ident_s.as_str(), "m" | "s" | "tr" | "y" | "qr")
2455                            && matches!(self.peek_at(1), Token::Regex(..))
2456                        {
2457                            // The `m` (or s/tr/y/qr) is our pattern, the Regex token was misparsed
2458                            self.advance(); // consume the ident
2459                                            // The Token::Regex after it was a misparsed `m/...` - we need to
2460                                            // extract what would have been the closing `/` situation.
2461                                            // Actually, the lexer consumed everything. Let's just use the ident
2462                                            // as the pattern and expect a closing slash.
2463                            if let Token::Regex(ref misparsed_pattern, ref misparsed_flags, _) =
2464                                self.peek().clone()
2465                            {
2466                                // The misparsed regex ate our closing `/`.
2467                                // For `/m/`, lexer saw `m/` and parsed until next `/`, finding nothing or wrong content.
2468                                // Actually for `/m/ less`, after Slash, lexer sees `m`, then `/`,
2469                                // interprets as m// regex start, reads until next `/` (none) -> error.
2470                                // So we shouldn't reach here if there was an error.
2471                                // But if lexer succeeded parsing `m/ less/` as regex, we'd have wrong pattern.
2472                                // This is getting complicated. Let me try a different approach.
2473                                // Just consume the Regex token and issue a warning? No, let's reconstruct.
2474                                // Skip for now and fall through to manual parsing.
2475                                let _ = (misparsed_pattern, misparsed_flags);
2476                            }
2477                        }
2478                    }
2479
2480                    // Manually parse the regex pattern from tokens until we hit another Slash
2481                    let mut pattern = String::new();
2482                    loop {
2483                        match self.peek().clone() {
2484                            Token::Slash => {
2485                                self.advance(); // consume closing /
2486                                break;
2487                            }
2488                            Token::Eof | Token::Semicolon | Token::Newline => {
2489                                return Err(self
2490                                    .syntax_err("Unterminated regex in thread stage", stage_line));
2491                            }
2492                            // Handle case where lexer misparsed m/pattern/ as Ident("m") + Regex
2493                            Token::Regex(ref inner_pattern, ref inner_flags, delim) => {
2494                                // This means `/m/` was lexed as Slash, then `m/` started a regex.
2495                                // The Regex token contains whatever was between the inner `m/` and closing `/`.
2496                                // For `/m/ less`, lexer would fail earlier. For `/m/i`, it might work weirdly.
2497                                // The safest: if we see a Regex token here and pattern is empty or just "m"/"s"/etc,
2498                                // treat the previous ident as the whole pattern and this Regex as misparsed.
2499                                // Actually, let's just prepend the ident we may have seen and use empty pattern.
2500                                // This is a lexer bug workaround.
2501                                if pattern.is_empty()
2502                                    || matches!(pattern.as_str(), "m" | "s" | "tr" | "y" | "qr")
2503                                {
2504                                    // The whole thing was probably `/X/` where X is m/s/tr/y/qr
2505                                    // and lexer misparsed. The Regex token is garbage.
2506                                    // Just use the ident as pattern and ignore this Regex.
2507                                    // But we already advanced past the ident...
2508                                    // This is messy. Let me try a cleaner approach.
2509                                    let _ = (inner_pattern, inner_flags, delim);
2510                                }
2511                                // For now, error out - this case is too complex
2512                                return Err(self.syntax_err(
2513                                    "Complex regex in thread stage - use m/pattern/ syntax instead",
2514                                    stage_line,
2515                                ));
2516                            }
2517                            Token::Ident(ref s) => {
2518                                pattern.push_str(s);
2519                                self.advance();
2520                            }
2521                            Token::Integer(n) => {
2522                                pattern.push_str(&n.to_string());
2523                                self.advance();
2524                            }
2525                            Token::ScalarVar(ref v) => {
2526                                pattern.push('$');
2527                                pattern.push_str(v);
2528                                self.advance();
2529                            }
2530                            Token::Dot => {
2531                                pattern.push('.');
2532                                self.advance();
2533                            }
2534                            Token::Star => {
2535                                pattern.push('*');
2536                                self.advance();
2537                            }
2538                            Token::Plus => {
2539                                pattern.push('+');
2540                                self.advance();
2541                            }
2542                            Token::Question => {
2543                                pattern.push('?');
2544                                self.advance();
2545                            }
2546                            Token::LParen => {
2547                                pattern.push('(');
2548                                self.advance();
2549                            }
2550                            Token::RParen => {
2551                                pattern.push(')');
2552                                self.advance();
2553                            }
2554                            Token::LBracket => {
2555                                pattern.push('[');
2556                                self.advance();
2557                            }
2558                            Token::RBracket => {
2559                                pattern.push(']');
2560                                self.advance();
2561                            }
2562                            Token::Backslash => {
2563                                pattern.push('\\');
2564                                self.advance();
2565                            }
2566                            Token::BitOr => {
2567                                pattern.push('|');
2568                                self.advance();
2569                            }
2570                            Token::Power => {
2571                                pattern.push_str("**");
2572                                self.advance();
2573                            }
2574                            Token::BitXor => {
2575                                pattern.push('^');
2576                                self.advance();
2577                            }
2578                            Token::Minus => {
2579                                pattern.push('-');
2580                                self.advance();
2581                            }
2582                            _ => {
2583                                return Err(self.syntax_err(
2584                                    format!("Unexpected token in regex pattern: {:?}", self.peek()),
2585                                    stage_line,
2586                                ));
2587                            }
2588                        }
2589                    }
2590                    // Parse optional flags (sequence of letters after closing /)
2591                    // Be careful: single letters like 'e' could be regex flags OR thread
2592                    // stages like `fore`/`e`. If followed by `{`, it's a stage, not a flag.
2593                    let mut flags = String::new();
2594                    if let Token::Ident(ref s) = self.peek().clone() {
2595                        let is_flag_only =
2596                            s.chars().all(|c| "gimsxecor".contains(c)) && s.len() <= 6;
2597                        let followed_by_brace = matches!(self.peek_at(1), Token::LBrace);
2598                        if is_flag_only && !followed_by_brace {
2599                            flags.push_str(s);
2600                            self.advance();
2601                        }
2602                    }
2603                    result = self.thread_regex_grep_stage(result, pattern, flags, '/', stage_line);
2604                }
2605                tok => {
2606                    return Err(self.syntax_err(
2607                        format!(
2608                            "thread: expected stage (ident, fn {{}}, s///, tr///, or /re/), got {:?}",
2609                            tok
2610                        ),
2611                        stage_line,
2612                    ));
2613                }
2614            };
2615            last_stage_end_line = self.prev_line();
2616        }
2617
2618        // Restore thread-last mode
2619        self.thread_last_mode = saved_thread_last;
2620
2621        if pipe_rhs_wrap {
2622            // Wrap as `fn { …stages threaded from $_[0]… }` so the outer
2623            // `pipe_forward_apply` can invoke it with `lhs` as the arg.
2624            let body_line = result.line;
2625            return Ok(Expr {
2626                kind: ExprKind::CodeRef {
2627                    params: vec![],
2628                    body: vec![Statement {
2629                        label: None,
2630                        kind: StmtKind::Expression(result),
2631                        line: body_line,
2632                    }],
2633                },
2634                line: _line,
2635            });
2636        }
2637        Ok(result)
2638    }
2639
2640    /// Build a grep filter stage from a regex pattern for the thread macro.
2641    fn thread_regex_grep_stage(
2642        &self,
2643        list: Expr,
2644        pattern: String,
2645        flags: String,
2646        delim: char,
2647        line: usize,
2648    ) -> Expr {
2649        let topic = Expr {
2650            kind: ExprKind::ScalarVar("_".to_string()),
2651            line,
2652        };
2653        let match_expr = Expr {
2654            kind: ExprKind::Match {
2655                expr: Box::new(topic),
2656                pattern,
2657                flags,
2658                scalar_g: false,
2659                delim,
2660            },
2661            line,
2662        };
2663        let block = vec![Statement {
2664            label: None,
2665            kind: StmtKind::Expression(match_expr),
2666            line,
2667        }];
2668        Expr {
2669            kind: ExprKind::GrepExpr {
2670                block,
2671                list: Box::new(list),
2672                keyword: crate::ast::GrepBuiltinKeyword::Grep,
2673            },
2674            line,
2675        }
2676    }
2677
2678    /// Check whether an expression contains a `$_` reference anywhere in its sub-tree.
2679    /// Used by the thread macro to validate `name(args)` call-stages: the threaded
2680    /// value is bound to `$_` via a wrapping CodeRef, so at least one `$_` placeholder
2681    /// must appear in the args, otherwise the threaded value is silently dropped.
2682    ///
2683    /// Implementation uses Rust's `Debug` to serialize the entire sub-tree once and
2684    /// scan for the canonical `ScalarVar("_")` representation. This avoids a
2685    /// per-variant walker that would need to be updated whenever new `ExprKind`
2686    /// variants are added (and would silently miss any it forgot to handle).
2687    /// Parse-time perf is non-critical and the AST is small at this scope.
2688    fn expr_contains_topic_var(e: &Expr) -> bool {
2689        format!("{:?}", e).contains("ScalarVar(\"_\")")
2690    }
2691
2692    /// Apply a bare function name in thread context, handling unary builtins specially.
2693    fn thread_apply_bare_func(&self, name: &str, arg: Expr, line: usize) -> PerlResult<Expr> {
2694        let kind = match name {
2695            // String functions
2696            "uc" => ExprKind::Uc(Box::new(arg)),
2697            "lc" => ExprKind::Lc(Box::new(arg)),
2698            "ucfirst" | "ufc" => ExprKind::Ucfirst(Box::new(arg)),
2699            "lcfirst" | "lfc" => ExprKind::Lcfirst(Box::new(arg)),
2700            "fc" => ExprKind::Fc(Box::new(arg)),
2701            "chomp" => ExprKind::Chomp(Box::new(arg)),
2702            "chop" => ExprKind::Chop(Box::new(arg)),
2703            "length" => ExprKind::Length(Box::new(arg)),
2704            "len" | "cnt" => ExprKind::FuncCall {
2705                name: "count".to_string(),
2706                args: vec![arg],
2707            },
2708            "quotemeta" | "qm" => ExprKind::FuncCall {
2709                name: "quotemeta".to_string(),
2710                args: vec![arg],
2711            },
2712            // Numeric functions
2713            "abs" => ExprKind::Abs(Box::new(arg)),
2714            "int" => ExprKind::Int(Box::new(arg)),
2715            "sqrt" | "sq" => ExprKind::Sqrt(Box::new(arg)),
2716            "sin" => ExprKind::Sin(Box::new(arg)),
2717            "cos" => ExprKind::Cos(Box::new(arg)),
2718            "exp" => ExprKind::Exp(Box::new(arg)),
2719            "log" => ExprKind::Log(Box::new(arg)),
2720            "hex" => ExprKind::Hex(Box::new(arg)),
2721            "oct" => ExprKind::Oct(Box::new(arg)),
2722            "chr" => ExprKind::Chr(Box::new(arg)),
2723            "ord" => ExprKind::Ord(Box::new(arg)),
2724            // Type/ref functions
2725            "defined" | "def" => ExprKind::Defined(Box::new(arg)),
2726            "ref" => ExprKind::Ref(Box::new(arg)),
2727            "scalar" => {
2728                if crate::no_interop_mode() {
2729                    return Err(self.syntax_err(
2730                        "stryke uses `len` (also `cnt` / `count`) instead of `scalar` (--no-interop)",
2731                        line,
2732                    ));
2733                }
2734                ExprKind::ScalarContext(Box::new(arg))
2735            }
2736            // Array/hash functions
2737            "keys" => ExprKind::Keys(Box::new(arg)),
2738            "values" => ExprKind::Values(Box::new(arg)),
2739            "each" => ExprKind::Each(Box::new(arg)),
2740            "pop" => ExprKind::Pop(Box::new(arg)),
2741            "shift" => ExprKind::Shift(Box::new(arg)),
2742            "reverse" => {
2743                if crate::no_interop_mode() {
2744                    return Err(self.syntax_err(
2745                        "stryke uses `rev` instead of `reverse` (--no-interop)",
2746                        line,
2747                    ));
2748                }
2749                ExprKind::ReverseExpr(Box::new(arg))
2750            }
2751            "reversed" | "rv" | "rev" => ExprKind::Rev(Box::new(arg)),
2752            "sort" | "so" => ExprKind::SortExpr {
2753                cmp: None,
2754                list: Box::new(arg),
2755            },
2756            "psort" => ExprKind::PSortExpr {
2757                cmp: None,
2758                list: Box::new(arg),
2759                progress: None,
2760            },
2761            "uniq" | "distinct" | "uq" => ExprKind::FuncCall {
2762                name: "uniq".to_string(),
2763                args: vec![arg],
2764            },
2765            "trim" | "tm" => ExprKind::FuncCall {
2766                name: "trim".to_string(),
2767                args: vec![arg],
2768            },
2769            "flatten" | "fl" => ExprKind::FuncCall {
2770                name: "flatten".to_string(),
2771                args: vec![arg],
2772            },
2773            "compact" | "cpt" => ExprKind::FuncCall {
2774                name: "compact".to_string(),
2775                args: vec![arg],
2776            },
2777            "shuffle" | "shuf" => ExprKind::FuncCall {
2778                name: "shuffle".to_string(),
2779                args: vec![arg],
2780            },
2781            "frequencies" | "freq" | "frq" => ExprKind::FuncCall {
2782                name: "frequencies".to_string(),
2783                args: vec![arg],
2784            },
2785            "dedup" | "dup" => ExprKind::FuncCall {
2786                name: "dedup".to_string(),
2787                args: vec![arg],
2788            },
2789            "enumerate" | "en" => ExprKind::FuncCall {
2790                name: "enumerate".to_string(),
2791                args: vec![arg],
2792            },
2793            "lines" | "ln" => ExprKind::FuncCall {
2794                name: "lines".to_string(),
2795                args: vec![arg],
2796            },
2797            "words" | "wd" => ExprKind::FuncCall {
2798                name: "words".to_string(),
2799                args: vec![arg],
2800            },
2801            "chars" | "ch" => ExprKind::FuncCall {
2802                name: "chars".to_string(),
2803                args: vec![arg],
2804            },
2805            "digits" | "dg" => ExprKind::FuncCall {
2806                name: "digits".to_string(),
2807                args: vec![arg],
2808            },
2809            "letters" | "lts" => ExprKind::FuncCall {
2810                name: "letters".to_string(),
2811                args: vec![arg],
2812            },
2813            "letters_uc" => ExprKind::FuncCall {
2814                name: "letters_uc".to_string(),
2815                args: vec![arg],
2816            },
2817            "letters_lc" => ExprKind::FuncCall {
2818                name: "letters_lc".to_string(),
2819                args: vec![arg],
2820            },
2821            "punctuation" | "punct" => ExprKind::FuncCall {
2822                name: "punctuation".to_string(),
2823                args: vec![arg],
2824            },
2825            "sentences" | "sents" => ExprKind::FuncCall {
2826                name: "sentences".to_string(),
2827                args: vec![arg],
2828            },
2829            "paragraphs" | "paras" => ExprKind::FuncCall {
2830                name: "paragraphs".to_string(),
2831                args: vec![arg],
2832            },
2833            "sections" | "sects" => ExprKind::FuncCall {
2834                name: "sections".to_string(),
2835                args: vec![arg],
2836            },
2837            "numbers" | "nums" => ExprKind::FuncCall {
2838                name: "numbers".to_string(),
2839                args: vec![arg],
2840            },
2841            "graphemes" | "grs" => ExprKind::FuncCall {
2842                name: "graphemes".to_string(),
2843                args: vec![arg],
2844            },
2845            "columns" | "cols" => ExprKind::FuncCall {
2846                name: "columns".to_string(),
2847                args: vec![arg],
2848            },
2849            // File functions
2850            "slurp" | "sl" => ExprKind::Slurp(Box::new(arg)),
2851            "chdir" => ExprKind::Chdir(Box::new(arg)),
2852            "stat" => ExprKind::Stat(Box::new(arg)),
2853            "lstat" => ExprKind::Lstat(Box::new(arg)),
2854            "readlink" => ExprKind::Readlink(Box::new(arg)),
2855            "readdir" => ExprKind::Readdir(Box::new(arg)),
2856            "close" => ExprKind::Close(Box::new(arg)),
2857            "basename" | "bn" => ExprKind::FuncCall {
2858                name: "basename".to_string(),
2859                args: vec![arg],
2860            },
2861            "dirname" | "dn" => ExprKind::FuncCall {
2862                name: "dirname".to_string(),
2863                args: vec![arg],
2864            },
2865            "realpath" | "rp" => ExprKind::FuncCall {
2866                name: "realpath".to_string(),
2867                args: vec![arg],
2868            },
2869            "which" | "wh" => ExprKind::FuncCall {
2870                name: "which".to_string(),
2871                args: vec![arg],
2872            },
2873            // Other
2874            "eval" => ExprKind::Eval(Box::new(arg)),
2875            "require" => ExprKind::Require(Box::new(arg)),
2876            "study" => ExprKind::Study(Box::new(arg)),
2877            // Case conversion
2878            "snake_case" | "sc" => ExprKind::FuncCall {
2879                name: "snake_case".to_string(),
2880                args: vec![arg],
2881            },
2882            "camel_case" | "cc" => ExprKind::FuncCall {
2883                name: "camel_case".to_string(),
2884                args: vec![arg],
2885            },
2886            "kebab_case" | "kc" => ExprKind::FuncCall {
2887                name: "kebab_case".to_string(),
2888                args: vec![arg],
2889            },
2890            // Serialization
2891            "to_json" | "tj" => ExprKind::FuncCall {
2892                name: "to_json".to_string(),
2893                args: vec![arg],
2894            },
2895            "to_yaml" | "ty" => ExprKind::FuncCall {
2896                name: "to_yaml".to_string(),
2897                args: vec![arg],
2898            },
2899            "to_toml" | "tt" => ExprKind::FuncCall {
2900                name: "to_toml".to_string(),
2901                args: vec![arg],
2902            },
2903            "to_csv" | "tc" => ExprKind::FuncCall {
2904                name: "to_csv".to_string(),
2905                args: vec![arg],
2906            },
2907            "to_xml" | "tx" => ExprKind::FuncCall {
2908                name: "to_xml".to_string(),
2909                args: vec![arg],
2910            },
2911            "to_html" | "th" => ExprKind::FuncCall {
2912                name: "to_html".to_string(),
2913                args: vec![arg],
2914            },
2915            "to_markdown" | "to_md" | "tmd" => ExprKind::FuncCall {
2916                name: "to_markdown".to_string(),
2917                args: vec![arg],
2918            },
2919            "xopen" | "xo" => ExprKind::FuncCall {
2920                name: "xopen".to_string(),
2921                args: vec![arg],
2922            },
2923            "clip" | "clipboard" | "pbcopy" => ExprKind::FuncCall {
2924                name: "clip".to_string(),
2925                args: vec![arg],
2926            },
2927            "to_table" | "table" | "tbl" => ExprKind::FuncCall {
2928                name: "to_table".to_string(),
2929                args: vec![arg],
2930            },
2931            "sparkline" | "spark" => ExprKind::FuncCall {
2932                name: "sparkline".to_string(),
2933                args: vec![arg],
2934            },
2935            "bar_chart" | "bars" => ExprKind::FuncCall {
2936                name: "bar_chart".to_string(),
2937                args: vec![arg],
2938            },
2939            "flame" | "flamechart" => ExprKind::FuncCall {
2940                name: "flame".to_string(),
2941                args: vec![arg],
2942            },
2943            "ddump" | "dd" => ExprKind::FuncCall {
2944                name: "ddump".to_string(),
2945                args: vec![arg],
2946            },
2947            "say" => {
2948                if crate::no_interop_mode() {
2949                    return Err(
2950                        self.syntax_err("stryke uses `p` instead of `say` (--no-interop)", line)
2951                    );
2952                }
2953                ExprKind::Say {
2954                    handle: None,
2955                    args: vec![arg],
2956                }
2957            }
2958            "p" => ExprKind::Say {
2959                handle: None,
2960                args: vec![arg],
2961            },
2962            "print" => ExprKind::Print {
2963                handle: None,
2964                args: vec![arg],
2965            },
2966            "warn" => ExprKind::Warn(vec![arg]),
2967            "die" => ExprKind::Die(vec![arg]),
2968            "stringify" | "str" => ExprKind::FuncCall {
2969                name: "stringify".to_string(),
2970                args: vec![arg],
2971            },
2972            "json_decode" | "jd" => ExprKind::FuncCall {
2973                name: "json_decode".to_string(),
2974                args: vec![arg],
2975            },
2976            "yaml_decode" | "yd" => ExprKind::FuncCall {
2977                name: "yaml_decode".to_string(),
2978                args: vec![arg],
2979            },
2980            "toml_decode" | "td" => ExprKind::FuncCall {
2981                name: "toml_decode".to_string(),
2982                args: vec![arg],
2983            },
2984            "xml_decode" | "xd" => ExprKind::FuncCall {
2985                name: "xml_decode".to_string(),
2986                args: vec![arg],
2987            },
2988            "json_encode" | "je" => ExprKind::FuncCall {
2989                name: "json_encode".to_string(),
2990                args: vec![arg],
2991            },
2992            "yaml_encode" | "ye" => ExprKind::FuncCall {
2993                name: "yaml_encode".to_string(),
2994                args: vec![arg],
2995            },
2996            "toml_encode" | "te" => ExprKind::FuncCall {
2997                name: "toml_encode".to_string(),
2998                args: vec![arg],
2999            },
3000            "xml_encode" | "xe" => ExprKind::FuncCall {
3001                name: "xml_encode".to_string(),
3002                args: vec![arg],
3003            },
3004            // Encoding
3005            "base64_encode" | "b64e" => ExprKind::FuncCall {
3006                name: "base64_encode".to_string(),
3007                args: vec![arg],
3008            },
3009            "base64_decode" | "b64d" => ExprKind::FuncCall {
3010                name: "base64_decode".to_string(),
3011                args: vec![arg],
3012            },
3013            "hex_encode" | "hxe" => ExprKind::FuncCall {
3014                name: "hex_encode".to_string(),
3015                args: vec![arg],
3016            },
3017            "hex_decode" | "hxd" => ExprKind::FuncCall {
3018                name: "hex_decode".to_string(),
3019                args: vec![arg],
3020            },
3021            "url_encode" | "uri_escape" | "ue" => ExprKind::FuncCall {
3022                name: "url_encode".to_string(),
3023                args: vec![arg],
3024            },
3025            "url_decode" | "uri_unescape" | "ud" => ExprKind::FuncCall {
3026                name: "url_decode".to_string(),
3027                args: vec![arg],
3028            },
3029            "gzip" | "gz" => ExprKind::FuncCall {
3030                name: "gzip".to_string(),
3031                args: vec![arg],
3032            },
3033            "gunzip" | "ugz" => ExprKind::FuncCall {
3034                name: "gunzip".to_string(),
3035                args: vec![arg],
3036            },
3037            "zstd" | "zst" => ExprKind::FuncCall {
3038                name: "zstd".to_string(),
3039                args: vec![arg],
3040            },
3041            "zstd_decode" | "uzst" => ExprKind::FuncCall {
3042                name: "zstd_decode".to_string(),
3043                args: vec![arg],
3044            },
3045            // Crypto
3046            "sha256" | "s256" => ExprKind::FuncCall {
3047                name: "sha256".to_string(),
3048                args: vec![arg],
3049            },
3050            "sha1" | "s1" => ExprKind::FuncCall {
3051                name: "sha1".to_string(),
3052                args: vec![arg],
3053            },
3054            "md5" | "m5" => ExprKind::FuncCall {
3055                name: "md5".to_string(),
3056                args: vec![arg],
3057            },
3058            "uuid" | "uid" => ExprKind::FuncCall {
3059                name: "uuid".to_string(),
3060                args: vec![arg],
3061            },
3062            // Datetime
3063            "datetime_utc" | "utc" => ExprKind::FuncCall {
3064                name: "datetime_utc".to_string(),
3065                args: vec![arg],
3066            },
3067            // Bare `e` / `fore` / `ep` in thread context: foreach element, say it.
3068            // `t @list e` == `@list |> e p` == `@list |> ep` == foreach (@list) { say }
3069            "e" | "fore" | "ep" => ExprKind::ForEachExpr {
3070                block: vec![Statement {
3071                    label: None,
3072                    kind: StmtKind::Expression(Expr {
3073                        kind: ExprKind::Say {
3074                            handle: None,
3075                            args: vec![Expr {
3076                                kind: ExprKind::ScalarVar("_".into()),
3077                                line,
3078                            }],
3079                        },
3080                        line,
3081                    }),
3082                    line,
3083                }],
3084                list: Box::new(arg),
3085            },
3086            // Default: generic function call
3087            _ => ExprKind::FuncCall {
3088                name: name.to_string(),
3089                args: vec![arg],
3090            },
3091        };
3092        Ok(Expr { kind, line })
3093    }
3094
3095    /// Parse a thread stage that has a block: `map { }`, `filter { }`, `sort { }`, etc.
3096    /// In thread context, we only parse the block - the list comes from the piped result.
3097    fn parse_thread_stage_with_block(&mut self, name: &str, line: usize) -> PerlResult<Expr> {
3098        let block = self.parse_block()?;
3099        // Use a placeholder for the list - pipe_forward_apply will replace it
3100        let placeholder = self.pipe_placeholder_list(line);
3101
3102        match name {
3103            "map" | "flat_map" | "maps" | "flat_maps" => {
3104                let flatten_array_refs = matches!(name, "flat_map" | "flat_maps");
3105                let stream = matches!(name, "maps" | "flat_maps");
3106                Ok(Expr {
3107                    kind: ExprKind::MapExpr {
3108                        block,
3109                        list: Box::new(placeholder),
3110                        flatten_array_refs,
3111                        stream,
3112                    },
3113                    line,
3114                })
3115            }
3116            "grep" | "greps" | "filter" | "fi" | "find_all" | "gr" => {
3117                let keyword = match name {
3118                    "grep" | "gr" => crate::ast::GrepBuiltinKeyword::Grep,
3119                    "greps" => crate::ast::GrepBuiltinKeyword::Greps,
3120                    "filter" | "fi" => crate::ast::GrepBuiltinKeyword::Filter,
3121                    "find_all" => crate::ast::GrepBuiltinKeyword::FindAll,
3122                    _ => unreachable!(),
3123                };
3124                Ok(Expr {
3125                    kind: ExprKind::GrepExpr {
3126                        block,
3127                        list: Box::new(placeholder),
3128                        keyword,
3129                    },
3130                    line,
3131                })
3132            }
3133            "sort" | "so" => Ok(Expr {
3134                kind: ExprKind::SortExpr {
3135                    cmp: Some(SortComparator::Block(block)),
3136                    list: Box::new(placeholder),
3137                },
3138                line,
3139            }),
3140            "reduce" | "rd" => Ok(Expr {
3141                kind: ExprKind::ReduceExpr {
3142                    block,
3143                    list: Box::new(placeholder),
3144                },
3145                line,
3146            }),
3147            "fore" | "e" | "ep" => Ok(Expr {
3148                kind: ExprKind::ForEachExpr {
3149                    block,
3150                    list: Box::new(placeholder),
3151                },
3152                line,
3153            }),
3154            "pmap" | "pflat_map" | "pmaps" | "pflat_maps" => Ok(Expr {
3155                kind: ExprKind::PMapExpr {
3156                    block,
3157                    list: Box::new(placeholder),
3158                    progress: None,
3159                    flat_outputs: name == "pflat_map" || name == "pflat_maps",
3160                    on_cluster: None,
3161                    stream: name == "pmaps" || name == "pflat_maps",
3162                },
3163                line,
3164            }),
3165            "pgrep" | "pgreps" => Ok(Expr {
3166                kind: ExprKind::PGrepExpr {
3167                    block,
3168                    list: Box::new(placeholder),
3169                    progress: None,
3170                    stream: name == "pgreps",
3171                },
3172                line,
3173            }),
3174            "pfor" => Ok(Expr {
3175                kind: ExprKind::PForExpr {
3176                    block,
3177                    list: Box::new(placeholder),
3178                    progress: None,
3179                },
3180                line,
3181            }),
3182            "preduce" => Ok(Expr {
3183                kind: ExprKind::PReduceExpr {
3184                    block,
3185                    list: Box::new(placeholder),
3186                    progress: None,
3187                },
3188                line,
3189            }),
3190            "pcache" => Ok(Expr {
3191                kind: ExprKind::PcacheExpr {
3192                    block,
3193                    list: Box::new(placeholder),
3194                    progress: None,
3195                },
3196                line,
3197            }),
3198            "psort" => Ok(Expr {
3199                kind: ExprKind::PSortExpr {
3200                    cmp: Some(block),
3201                    list: Box::new(placeholder),
3202                    progress: None,
3203                },
3204                line,
3205            }),
3206            _ => {
3207                // Generic: parse block and treat as FuncCall with code ref arg.
3208                // Block-then-list pipe builtins (`pfirst`, `any`, `take_while`, etc.)
3209                // need the threaded list slot pre-allocated at args[1] so
3210                // `pipe_forward_apply` can substitute the lhs there (parser.rs:5823).
3211                // For everything else, the generic pipe-forward arm prepends or
3212                // appends the lhs based on `thread_last_mode`.
3213                let code_ref = Expr {
3214                    kind: ExprKind::CodeRef {
3215                        params: vec![],
3216                        body: block,
3217                    },
3218                    line,
3219                };
3220                let args = if Self::is_block_then_list_pipe_builtin(name) {
3221                    vec![code_ref, placeholder]
3222                } else {
3223                    vec![code_ref]
3224                };
3225                Ok(Expr {
3226                    kind: ExprKind::FuncCall {
3227                        name: name.to_string(),
3228                        args,
3229                    },
3230                    line,
3231                })
3232            }
3233        }
3234    }
3235
3236    /// `tie %hash | tie @arr | tie $x , 'Class', ...args`
3237    fn parse_tie_stmt(&mut self) -> PerlResult<Statement> {
3238        let line = self.peek_line();
3239        self.advance(); // tie
3240                        // `tie my $x, Class` and `tie our $x, Class` — common Perl idiom.
3241                        // Desugar by emitting an implicit `my $x` (or `our $x`) declaration
3242                        // before the tie. The tie target then references the just-declared
3243                        // variable. Without this, `tie my $x, Class, ARGS` errors with
3244                        // "tie expects $scalar, @array, or %hash, got Ident(\"my\")".
3245        let mut implicit_decl: Option<Statement> = None;
3246        if let Token::Ident(kw) = self.peek().clone() {
3247            if matches!(kw.as_str(), "my" | "our") {
3248                let kw_line = self.peek_line();
3249                self.advance(); // my / our
3250                                // Read the variable being declared (must be Scalar/Array/Hash).
3251                let (decl_sigil, decl_name) = match self.peek().clone() {
3252                    Token::ScalarVar(s) => (Sigil::Scalar, s),
3253                    Token::ArrayVar(a) => (Sigil::Array, a),
3254                    Token::HashVar(h) => (Sigil::Hash, h),
3255                    tok => {
3256                        return Err(self.syntax_err(
3257                            format!("expected variable after `tie {}`, got {:?}", kw, tok),
3258                            self.peek_line(),
3259                        ));
3260                    }
3261                };
3262                let decls = vec![VarDecl {
3263                    sigil: decl_sigil,
3264                    name: decl_name.clone(),
3265                    initializer: None,
3266                    frozen: false,
3267                    type_annotation: None,
3268                }];
3269                implicit_decl = Some(Statement {
3270                    label: None,
3271                    kind: if kw == "my" {
3272                        StmtKind::My(decls)
3273                    } else {
3274                        StmtKind::Our(decls)
3275                    },
3276                    line: kw_line,
3277                });
3278                // Don't advance past the variable token here — fall through
3279                // to the existing match below so `target` is built from the
3280                // same token (the ScalarVar/ArrayVar/HashVar path will
3281                // advance and capture the name).
3282            }
3283        }
3284        let target = match self.peek().clone() {
3285            Token::HashVar(h) => {
3286                self.advance();
3287                TieTarget::Hash(h)
3288            }
3289            Token::ArrayVar(a) => {
3290                self.advance();
3291                TieTarget::Array(a)
3292            }
3293            Token::ScalarVar(s) => {
3294                self.advance();
3295                TieTarget::Scalar(s)
3296            }
3297            tok => {
3298                return Err(self.syntax_err(
3299                    format!("tie expects $scalar, @array, or %hash, got {:?}", tok),
3300                    self.peek_line(),
3301                ));
3302            }
3303        };
3304        self.expect(&Token::Comma)?;
3305        let class = self.parse_assign_expr()?;
3306        let mut args = Vec::new();
3307        while self.eat(&Token::Comma) {
3308            if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof) {
3309                break;
3310            }
3311            args.push(self.parse_assign_expr()?);
3312        }
3313        self.eat(&Token::Semicolon);
3314        let tie_stmt = Statement {
3315            label: None,
3316            kind: StmtKind::Tie {
3317                target,
3318                class,
3319                args,
3320            },
3321            line,
3322        };
3323        if let Some(decl) = implicit_decl {
3324            // Wrap the implicit `my $x` + tie in a `StmtGroup` so they live
3325            // in the same lexical block (the parser desugar is invisible to
3326            // callers; `StmtGroup` runs statements in order without a frame
3327            // push).
3328            Ok(Statement {
3329                label: None,
3330                kind: StmtKind::StmtGroup(vec![decl, tie_stmt]),
3331                line,
3332            })
3333        } else {
3334            Ok(tie_stmt)
3335        }
3336    }
3337
3338    /// `given (EXPR) { ... }`
3339    fn parse_given(&mut self) -> PerlResult<Statement> {
3340        let line = self.peek_line();
3341        self.advance();
3342        self.expect(&Token::LParen)?;
3343        let topic = self.parse_expression()?;
3344        self.expect(&Token::RParen)?;
3345        let body = self.parse_block()?;
3346        self.eat(&Token::Semicolon);
3347        Ok(Statement {
3348            label: None,
3349            kind: StmtKind::Given { topic, body },
3350            line,
3351        })
3352    }
3353
3354    /// `when (COND) { ... }` — only meaningful inside `given`
3355    fn parse_when_stmt(&mut self) -> PerlResult<Statement> {
3356        let line = self.peek_line();
3357        self.advance();
3358        self.expect(&Token::LParen)?;
3359        let cond = self.parse_expression()?;
3360        self.expect(&Token::RParen)?;
3361        let body = self.parse_block()?;
3362        self.eat(&Token::Semicolon);
3363        Ok(Statement {
3364            label: None,
3365            kind: StmtKind::When { cond, body },
3366            line,
3367        })
3368    }
3369
3370    /// `default { ... }` — only meaningful inside `given`
3371    fn parse_default_stmt(&mut self) -> PerlResult<Statement> {
3372        let line = self.peek_line();
3373        self.advance();
3374        let body = self.parse_block()?;
3375        self.eat(&Token::Semicolon);
3376        Ok(Statement {
3377            label: None,
3378            kind: StmtKind::DefaultCase { body },
3379            line,
3380        })
3381    }
3382
3383    /// `cond { EXPR => RESULT, ..., default => RESULT }`
3384    ///
3385    /// Desugars to an if/elsif/else chain at parse time.
3386    /// Each arm is `condition => { body }` or `condition => expr`.
3387    /// `default => ...` becomes the else branch.
3388    fn parse_cond_expr(&mut self, line: usize) -> PerlResult<Expr> {
3389        self.expect(&Token::LBrace)?;
3390
3391        let mut arms: Vec<(Expr, Block)> = Vec::new();
3392        let mut else_block: Option<Block> = None;
3393
3394        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
3395            let arm_line = self.peek_line();
3396
3397            // Check for `default =>`
3398            let is_default = matches!(self.peek(), Token::Ident(ref s) if s == "default")
3399                && matches!(self.peek_at(1), Token::FatArrow);
3400
3401            if is_default {
3402                self.advance(); // consume `default`
3403                self.advance(); // consume `=>`
3404                let body = if matches!(self.peek(), Token::LBrace) {
3405                    self.parse_block()?
3406                } else {
3407                    let expr = self.parse_assign_expr()?;
3408                    vec![Statement {
3409                        label: None,
3410                        kind: StmtKind::Expression(expr),
3411                        line: arm_line,
3412                    }]
3413                };
3414                else_block = Some(body);
3415                self.eat(&Token::Comma);
3416                break; // default must be last
3417            }
3418
3419            // Parse condition expression (stop before `=>`)
3420            let condition = self.parse_assign_expr()?;
3421            self.expect(&Token::FatArrow)?;
3422
3423            let body = if matches!(self.peek(), Token::LBrace) {
3424                self.parse_block()?
3425            } else {
3426                let expr = self.parse_assign_expr()?;
3427                vec![Statement {
3428                    label: None,
3429                    kind: StmtKind::Expression(expr),
3430                    line: arm_line,
3431                }]
3432            };
3433
3434            arms.push((condition, body));
3435            self.eat(&Token::Comma);
3436        }
3437
3438        self.expect(&Token::RBrace)?;
3439
3440        if arms.is_empty() {
3441            return Err(self.syntax_err("cond requires at least one condition arm", line));
3442        }
3443
3444        // Build if/elsif/else chain from the arms.
3445        let (first_cond, first_body) = arms.remove(0);
3446        let elsifs: Vec<(Expr, Block)> = arms;
3447
3448        // Wrap in a do-block so `cond { ... }` is an expression.
3449        let if_stmt = Statement {
3450            label: None,
3451            kind: StmtKind::If {
3452                condition: first_cond,
3453                body: first_body,
3454                elsifs,
3455                else_block,
3456            },
3457            line,
3458        };
3459        let inner = Expr {
3460            kind: ExprKind::CodeRef {
3461                params: vec![],
3462                body: vec![if_stmt],
3463            },
3464            line,
3465        };
3466        Ok(Expr {
3467            kind: ExprKind::Do(Box::new(inner)),
3468            line,
3469        })
3470    }
3471
3472    /// `match (EXPR) { PATTERN => EXPR, ... }`
3473    fn parse_algebraic_match_expr(&mut self, line: usize) -> PerlResult<Expr> {
3474        self.expect(&Token::LParen)?;
3475        let subject = self.parse_expression()?;
3476        self.expect(&Token::RParen)?;
3477        self.expect(&Token::LBrace)?;
3478        let mut arms = Vec::new();
3479        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
3480            if self.eat(&Token::Semicolon) {
3481                continue;
3482            }
3483            let pattern = self.parse_match_pattern()?;
3484            let guard = if matches!(self.peek(), Token::Ident(ref s) if s == "if") {
3485                self.advance();
3486                // Use assign-level parsing so `=>` after the guard is not consumed as a comma/fat-comma
3487                // separator (see [`Self::parse_comma_expr`]).
3488                Some(Box::new(self.parse_assign_expr()?))
3489            } else {
3490                None
3491            };
3492            self.expect(&Token::FatArrow)?;
3493            // Use assign-level parsing so commas separate arms, not `List` elements.
3494            let body = self.parse_assign_expr()?;
3495            arms.push(MatchArm {
3496                pattern,
3497                guard,
3498                body,
3499            });
3500            self.eat(&Token::Comma);
3501        }
3502        self.expect(&Token::RBrace)?;
3503        Ok(Expr {
3504            kind: ExprKind::AlgebraicMatch {
3505                subject: Box::new(subject),
3506                arms,
3507            },
3508            line,
3509        })
3510    }
3511
3512    fn parse_match_pattern(&mut self) -> PerlResult<MatchPattern> {
3513        match self.peek().clone() {
3514            Token::Regex(pattern, flags, _delim) => {
3515                self.advance();
3516                Ok(MatchPattern::Regex { pattern, flags })
3517            }
3518            Token::Ident(ref s) if s == "_" => {
3519                self.advance();
3520                Ok(MatchPattern::Any)
3521            }
3522            Token::Ident(ref s) if s == "Some" => {
3523                self.advance();
3524                self.expect(&Token::LParen)?;
3525                let name = self.parse_scalar_var_name()?;
3526                self.expect(&Token::RParen)?;
3527                Ok(MatchPattern::OptionSome(name))
3528            }
3529            Token::LBracket => self.parse_match_array_pattern(),
3530            Token::LBrace => self.parse_match_hash_pattern(),
3531            Token::LParen => {
3532                self.advance();
3533                let e = self.parse_expression()?;
3534                self.expect(&Token::RParen)?;
3535                Ok(MatchPattern::Value(Box::new(e)))
3536            }
3537            _ => {
3538                let e = self.parse_assign_expr()?;
3539                Ok(MatchPattern::Value(Box::new(e)))
3540            }
3541        }
3542    }
3543
3544    /// Contents of `[ ... ]` for algebraic array patterns and `sub ($a, [ ... ])` signatures.
3545    fn parse_match_array_elems_until_rbracket(&mut self) -> PerlResult<Vec<MatchArrayElem>> {
3546        let mut elems = Vec::new();
3547        if self.eat(&Token::RBracket) {
3548            return Ok(vec![]);
3549        }
3550        loop {
3551            if matches!(self.peek(), Token::Star) {
3552                self.advance();
3553                elems.push(MatchArrayElem::Rest);
3554                self.eat(&Token::Comma);
3555                if !matches!(self.peek(), Token::RBracket) {
3556                    return Err(self.syntax_err(
3557                        "`*` must be the last element in an array match pattern",
3558                        self.peek_line(),
3559                    ));
3560                }
3561                self.expect(&Token::RBracket)?;
3562                return Ok(elems);
3563            }
3564            if let Token::ArrayVar(name) = self.peek().clone() {
3565                self.advance();
3566                elems.push(MatchArrayElem::RestBind(name));
3567                self.eat(&Token::Comma);
3568                if !matches!(self.peek(), Token::RBracket) {
3569                    return Err(self.syntax_err(
3570                        "`@name` rest bind must be the last element in an array match pattern",
3571                        self.peek_line(),
3572                    ));
3573                }
3574                self.expect(&Token::RBracket)?;
3575                return Ok(elems);
3576            }
3577            if let Token::ScalarVar(name) = self.peek().clone() {
3578                self.advance();
3579                elems.push(MatchArrayElem::CaptureScalar(name));
3580                if self.eat(&Token::Comma) {
3581                    if matches!(self.peek(), Token::RBracket) {
3582                        break;
3583                    }
3584                    continue;
3585                }
3586                break;
3587            }
3588            let e = self.parse_assign_expr()?;
3589            elems.push(MatchArrayElem::Expr(e));
3590            if self.eat(&Token::Comma) {
3591                if matches!(self.peek(), Token::RBracket) {
3592                    break;
3593                }
3594                continue;
3595            }
3596            break;
3597        }
3598        self.expect(&Token::RBracket)?;
3599        Ok(elems)
3600    }
3601
3602    fn parse_match_array_pattern(&mut self) -> PerlResult<MatchPattern> {
3603        self.expect(&Token::LBracket)?;
3604        let elems = self.parse_match_array_elems_until_rbracket()?;
3605        Ok(MatchPattern::Array(elems))
3606    }
3607
3608    fn parse_match_hash_pattern(&mut self) -> PerlResult<MatchPattern> {
3609        self.expect(&Token::LBrace)?;
3610        let mut pairs = Vec::new();
3611        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
3612            if self.eat(&Token::Semicolon) {
3613                continue;
3614            }
3615            let key = self.parse_assign_expr()?;
3616            self.expect(&Token::FatArrow)?;
3617            match self.advance().0 {
3618                Token::Ident(ref s) if s == "_" => {
3619                    pairs.push(MatchHashPair::KeyOnly { key });
3620                }
3621                Token::ScalarVar(name) => {
3622                    pairs.push(MatchHashPair::Capture { key, name });
3623                }
3624                tok => {
3625                    return Err(self.syntax_err(
3626                        format!(
3627                            "hash match pattern must bind with `=> $name` or `=> _`, got {:?}",
3628                            tok
3629                        ),
3630                        self.peek_line(),
3631                    ));
3632                }
3633            }
3634            self.eat(&Token::Comma);
3635        }
3636        self.expect(&Token::RBrace)?;
3637        Ok(MatchPattern::Hash(pairs))
3638    }
3639
3640    /// `eval_timeout SECS { ... }`
3641    fn parse_eval_timeout(&mut self) -> PerlResult<Statement> {
3642        let line = self.peek_line();
3643        self.advance();
3644        let timeout = self.parse_postfix()?;
3645        let body = self.parse_block_or_bareword_block_no_args()?;
3646        self.eat(&Token::Semicolon);
3647        Ok(Statement {
3648            label: None,
3649            kind: StmtKind::EvalTimeout { timeout, body },
3650            line,
3651        })
3652    }
3653
3654    fn mark_match_scalar_g_for_boolean_condition(cond: &mut Expr) {
3655        match &mut cond.kind {
3656            ExprKind::Match {
3657                flags, scalar_g, ..
3658            } if flags.contains('g') => {
3659                *scalar_g = true;
3660            }
3661            ExprKind::UnaryOp {
3662                op: UnaryOp::LogNot,
3663                expr,
3664            } => {
3665                if let ExprKind::Match {
3666                    flags, scalar_g, ..
3667                } = &mut expr.kind
3668                {
3669                    if flags.contains('g') {
3670                        *scalar_g = true;
3671                    }
3672                }
3673            }
3674            _ => {}
3675        }
3676    }
3677
3678    fn parse_if(&mut self) -> PerlResult<Statement> {
3679        let line = self.peek_line();
3680        self.advance(); // 'if'
3681        if matches!(self.peek(), Token::Ident(ref s) if s == "let") {
3682            if crate::compat_mode() {
3683                return Err(self.syntax_err(
3684                    "`if let` is a stryke extension (disabled by --compat)",
3685                    line,
3686                ));
3687            }
3688            return self.parse_if_let(line);
3689        }
3690        self.expect(&Token::LParen)?;
3691        let mut cond = self.parse_expression()?;
3692        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
3693        self.expect(&Token::RParen)?;
3694        let body = self.parse_block()?;
3695
3696        let mut elsifs = Vec::new();
3697        let mut else_block = None;
3698
3699        loop {
3700            if let Token::Ident(ref kw) = self.peek().clone() {
3701                if kw == "elsif" {
3702                    self.advance();
3703                    self.expect(&Token::LParen)?;
3704                    let mut c = self.parse_expression()?;
3705                    Self::mark_match_scalar_g_for_boolean_condition(&mut c);
3706                    self.expect(&Token::RParen)?;
3707                    let b = self.parse_block()?;
3708                    elsifs.push((c, b));
3709                    continue;
3710                }
3711                if kw == "else" {
3712                    self.advance();
3713                    else_block = Some(self.parse_block()?);
3714                }
3715            }
3716            break;
3717        }
3718
3719        Ok(Statement {
3720            label: None,
3721            kind: StmtKind::If {
3722                condition: cond,
3723                body,
3724                elsifs,
3725                else_block,
3726            },
3727            line,
3728        })
3729    }
3730
3731    /// `if let PAT = EXPR { ... } [ else { ... } ]` — desugars to [`ExprKind::AlgebraicMatch`].
3732    fn parse_if_let(&mut self, line: usize) -> PerlResult<Statement> {
3733        self.advance(); // `let`
3734        let pattern = self.parse_match_pattern()?;
3735        self.expect(&Token::Assign)?;
3736        // Use assign-level parsing so a following `{ ... }` is the `if let` body, not an anon hash.
3737        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_add(1);
3738        let rhs = self.parse_assign_expr();
3739        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_sub(1);
3740        let rhs = rhs?;
3741        let then_block = self.parse_block()?;
3742        let else_block_opt = match self.peek().clone() {
3743            Token::Ident(ref kw) if kw == "else" => {
3744                self.advance();
3745                Some(self.parse_block()?)
3746            }
3747            Token::Ident(ref kw) if kw == "elsif" => {
3748                return Err(self.syntax_err(
3749                    "`if let` does not support `elsif`; use `else { }` or a full `match`",
3750                    self.peek_line(),
3751                ));
3752            }
3753            _ => None,
3754        };
3755        let then_expr = Self::expr_do_anon_block(then_block, line);
3756        let else_expr = if let Some(eb) = else_block_opt {
3757            Self::expr_do_anon_block(eb, line)
3758        } else {
3759            Expr {
3760                kind: ExprKind::Undef,
3761                line,
3762            }
3763        };
3764        let arms = vec![
3765            MatchArm {
3766                pattern,
3767                guard: None,
3768                body: then_expr,
3769            },
3770            MatchArm {
3771                pattern: MatchPattern::Any,
3772                guard: None,
3773                body: else_expr,
3774            },
3775        ];
3776        Ok(Statement {
3777            label: None,
3778            kind: StmtKind::Expression(Expr {
3779                kind: ExprKind::AlgebraicMatch {
3780                    subject: Box::new(rhs),
3781                    arms,
3782                },
3783                line,
3784            }),
3785            line,
3786        })
3787    }
3788
3789    fn expr_do_anon_block(block: Block, outer_line: usize) -> Expr {
3790        let inner_line = block.first().map(|s| s.line).unwrap_or(outer_line);
3791        Expr {
3792            kind: ExprKind::Do(Box::new(Expr {
3793                kind: ExprKind::CodeRef {
3794                    params: vec![],
3795                    body: block,
3796                },
3797                line: inner_line,
3798            })),
3799            line: outer_line,
3800        }
3801    }
3802
3803    fn parse_unless(&mut self) -> PerlResult<Statement> {
3804        let line = self.peek_line();
3805        self.advance(); // 'unless'
3806        self.expect(&Token::LParen)?;
3807        let mut cond = self.parse_expression()?;
3808        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
3809        self.expect(&Token::RParen)?;
3810        let body = self.parse_block()?;
3811        let else_block = if let Token::Ident(ref kw) = self.peek().clone() {
3812            if kw == "else" {
3813                self.advance();
3814                Some(self.parse_block()?)
3815            } else {
3816                None
3817            }
3818        } else {
3819            None
3820        };
3821        Ok(Statement {
3822            label: None,
3823            kind: StmtKind::Unless {
3824                condition: cond,
3825                body,
3826                else_block,
3827            },
3828            line,
3829        })
3830    }
3831
3832    fn parse_while(&mut self) -> PerlResult<Statement> {
3833        let line = self.peek_line();
3834        self.advance(); // 'while'
3835        if matches!(self.peek(), Token::Ident(ref s) if s == "let") {
3836            if crate::compat_mode() {
3837                return Err(self.syntax_err(
3838                    "`while let` is a stryke extension (disabled by --compat)",
3839                    line,
3840                ));
3841            }
3842            return self.parse_while_let(line);
3843        }
3844        self.expect(&Token::LParen)?;
3845        let mut cond = self.parse_expression()?;
3846        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
3847        self.expect(&Token::RParen)?;
3848        let body = self.parse_block()?;
3849        let continue_block = self.parse_optional_continue_block()?;
3850        Ok(Statement {
3851            label: None,
3852            kind: StmtKind::While {
3853                condition: cond,
3854                body,
3855                label: None,
3856                continue_block,
3857            },
3858            line,
3859        })
3860    }
3861
3862    /// `while let PAT = EXPR { ... }` — desugars to a `match` that returns 0/1 plus `unless ($tmp) { last }`
3863    /// so bytecode does not run `last` inside a tree-assisted [`Op::AlgebraicMatch`] arm.
3864    fn parse_while_let(&mut self, line: usize) -> PerlResult<Statement> {
3865        self.advance(); // `let`
3866        let pattern = self.parse_match_pattern()?;
3867        self.expect(&Token::Assign)?;
3868        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_add(1);
3869        let rhs = self.parse_assign_expr();
3870        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_sub(1);
3871        let rhs = rhs?;
3872        let mut user_body = self.parse_block()?;
3873        let continue_block = self.parse_optional_continue_block()?;
3874        user_body.push(Statement::new(
3875            StmtKind::Expression(Expr {
3876                kind: ExprKind::Integer(1),
3877                line,
3878            }),
3879            line,
3880        ));
3881        let tmp = format!("__while_let_{}", self.alloc_desugar_tmp());
3882        let match_expr = Expr {
3883            kind: ExprKind::AlgebraicMatch {
3884                subject: Box::new(rhs),
3885                arms: vec![
3886                    MatchArm {
3887                        pattern,
3888                        guard: None,
3889                        body: Self::expr_do_anon_block(user_body, line),
3890                    },
3891                    MatchArm {
3892                        pattern: MatchPattern::Any,
3893                        guard: None,
3894                        body: Expr {
3895                            kind: ExprKind::Integer(0),
3896                            line,
3897                        },
3898                    },
3899                ],
3900            },
3901            line,
3902        };
3903        let my_stmt = Statement::new(
3904            StmtKind::My(vec![VarDecl {
3905                sigil: Sigil::Scalar,
3906                name: tmp.clone(),
3907                initializer: Some(match_expr),
3908                frozen: false,
3909                type_annotation: None,
3910            }]),
3911            line,
3912        );
3913        let unless_last = Statement::new(
3914            StmtKind::Unless {
3915                condition: Expr {
3916                    kind: ExprKind::ScalarVar(tmp),
3917                    line,
3918                },
3919                body: vec![Statement::new(StmtKind::Last(None), line)],
3920                else_block: None,
3921            },
3922            line,
3923        );
3924        Ok(Statement::new(
3925            StmtKind::While {
3926                condition: Expr {
3927                    kind: ExprKind::Integer(1),
3928                    line,
3929                },
3930                body: vec![my_stmt, unless_last],
3931                label: None,
3932                continue_block,
3933            },
3934            line,
3935        ))
3936    }
3937
3938    fn parse_until(&mut self) -> PerlResult<Statement> {
3939        let line = self.peek_line();
3940        self.advance(); // 'until'
3941        self.expect(&Token::LParen)?;
3942        let mut cond = self.parse_expression()?;
3943        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
3944        self.expect(&Token::RParen)?;
3945        let body = self.parse_block()?;
3946        let continue_block = self.parse_optional_continue_block()?;
3947        Ok(Statement {
3948            label: None,
3949            kind: StmtKind::Until {
3950                condition: cond,
3951                body,
3952                label: None,
3953                continue_block,
3954            },
3955            line,
3956        })
3957    }
3958
3959    /// `continue { ... }` after a loop body (optional).
3960    fn parse_optional_continue_block(&mut self) -> PerlResult<Option<Block>> {
3961        if let Token::Ident(ref kw) = self.peek().clone() {
3962            if kw == "continue" {
3963                self.advance();
3964                return Ok(Some(self.parse_block()?));
3965            }
3966        }
3967        Ok(None)
3968    }
3969
3970    fn parse_for_or_foreach(&mut self) -> PerlResult<Statement> {
3971        let line = self.peek_line();
3972        self.advance(); // 'for'
3973
3974        // Peek to determine if C-style for or foreach
3975        // C-style: for (init; cond; step)
3976        // foreach-style: for $var (list) or for (list)
3977        match self.peek() {
3978            Token::LParen => {
3979                // Check if next after ( is a semicolon or an assignment — C-style
3980                // Or if it's a list — foreach-style
3981                // Heuristic: if the token after ( is 'my' or '$' followed by
3982                // content that contains ';', it's C-style.
3983                let saved = self.pos;
3984                self.advance(); // consume (
3985                                // Look for semicolon at paren depth 0
3986                let mut depth = 1;
3987                let mut has_semi = false;
3988                let mut scan = self.pos;
3989                while scan < self.tokens.len() {
3990                    match &self.tokens[scan].0 {
3991                        Token::LParen => depth += 1,
3992                        Token::RParen => {
3993                            depth -= 1;
3994                            if depth == 0 {
3995                                break;
3996                            }
3997                        }
3998                        Token::Semicolon if depth == 1 => {
3999                            has_semi = true;
4000                            break;
4001                        }
4002                        _ => {}
4003                    }
4004                    scan += 1;
4005                }
4006                self.pos = saved;
4007
4008                if has_semi {
4009                    self.parse_c_style_for(line)
4010                } else {
4011                    // foreach without explicit var — uses $_
4012                    self.expect(&Token::LParen)?;
4013                    let list = self.parse_expression()?;
4014                    self.expect(&Token::RParen)?;
4015                    let body = self.parse_block()?;
4016                    let continue_block = self.parse_optional_continue_block()?;
4017                    Ok(Statement {
4018                        label: None,
4019                        kind: StmtKind::Foreach {
4020                            var: "_".to_string(),
4021                            list,
4022                            body,
4023                            label: None,
4024                            continue_block,
4025                        },
4026                        line,
4027                    })
4028                }
4029            }
4030            Token::Ident(ref kw) if kw == "my" => {
4031                self.advance(); // 'my'
4032                let var = self.parse_scalar_var_name()?;
4033                self.expect(&Token::LParen)?;
4034                let list = self.parse_expression()?;
4035                self.expect(&Token::RParen)?;
4036                let body = self.parse_block()?;
4037                let continue_block = self.parse_optional_continue_block()?;
4038                Ok(Statement {
4039                    label: None,
4040                    kind: StmtKind::Foreach {
4041                        var,
4042                        list,
4043                        body,
4044                        label: None,
4045                        continue_block,
4046                    },
4047                    line,
4048                })
4049            }
4050            Token::ScalarVar(_) => {
4051                let var = self.parse_scalar_var_name()?;
4052                self.expect(&Token::LParen)?;
4053                let list = self.parse_expression()?;
4054                self.expect(&Token::RParen)?;
4055                let body = self.parse_block()?;
4056                let continue_block = self.parse_optional_continue_block()?;
4057                Ok(Statement {
4058                    label: None,
4059                    kind: StmtKind::Foreach {
4060                        var,
4061                        list,
4062                        body,
4063                        label: None,
4064                        continue_block,
4065                    },
4066                    line,
4067                })
4068            }
4069            _ => self.parse_c_style_for(line),
4070        }
4071    }
4072
4073    fn parse_c_style_for(&mut self, line: usize) -> PerlResult<Statement> {
4074        self.expect(&Token::LParen)?;
4075        let init = if self.eat(&Token::Semicolon) {
4076            None
4077        } else {
4078            let s = self.parse_statement()?;
4079            self.eat(&Token::Semicolon);
4080            Some(Box::new(s))
4081        };
4082        let mut condition = if matches!(self.peek(), Token::Semicolon) {
4083            None
4084        } else {
4085            Some(self.parse_expression()?)
4086        };
4087        if let Some(ref mut c) = condition {
4088            Self::mark_match_scalar_g_for_boolean_condition(c);
4089        }
4090        self.expect(&Token::Semicolon)?;
4091        let step = if matches!(self.peek(), Token::RParen) {
4092            None
4093        } else {
4094            Some(self.parse_expression()?)
4095        };
4096        self.expect(&Token::RParen)?;
4097        let body = self.parse_block()?;
4098        let continue_block = self.parse_optional_continue_block()?;
4099        Ok(Statement {
4100            label: None,
4101            kind: StmtKind::For {
4102                init,
4103                condition,
4104                step,
4105                body,
4106                label: None,
4107                continue_block,
4108            },
4109            line,
4110        })
4111    }
4112
4113    fn parse_foreach(&mut self) -> PerlResult<Statement> {
4114        let line = self.peek_line();
4115        self.advance(); // 'foreach'
4116        let var = match self.peek() {
4117            Token::Ident(ref kw) if kw == "my" => {
4118                self.advance();
4119                self.parse_scalar_var_name()?
4120            }
4121            Token::ScalarVar(_) => self.parse_scalar_var_name()?,
4122            _ => "_".to_string(),
4123        };
4124        self.expect(&Token::LParen)?;
4125        let list = self.parse_expression()?;
4126        self.expect(&Token::RParen)?;
4127        let body = self.parse_block()?;
4128        let continue_block = self.parse_optional_continue_block()?;
4129        Ok(Statement {
4130            label: None,
4131            kind: StmtKind::Foreach {
4132                var,
4133                list,
4134                body,
4135                label: None,
4136                continue_block,
4137            },
4138            line,
4139        })
4140    }
4141
4142    fn parse_scalar_var_name(&mut self) -> PerlResult<String> {
4143        match self.advance() {
4144            (Token::ScalarVar(name), _) => Ok(name),
4145            (tok, line) => {
4146                Err(self.syntax_err(format!("Expected scalar variable, got {:?}", tok), line))
4147            }
4148        }
4149    }
4150
4151    /// After `(` was consumed: Perl5 prototype characters until `)` (or `$)` + `{`).
4152    fn parse_legacy_sub_prototype_tail(&mut self) -> PerlResult<String> {
4153        let mut s = String::new();
4154        loop {
4155            match self.peek().clone() {
4156                Token::RParen => {
4157                    self.advance();
4158                    break;
4159                }
4160                Token::Eof => {
4161                    return Err(self.syntax_err(
4162                        "Unterminated sub prototype (expected ')' before end of input)",
4163                        self.peek_line(),
4164                    ));
4165                }
4166                Token::ScalarVar(v) if v == ")" => {
4167                    // Lexer merges `$` + `)` into one token (`$)`). In `sub name ($) {`, the
4168                    // closing `)` of the prototype is not a separate `RParen` — next is `{`.
4169                    self.advance();
4170                    s.push('$');
4171                    if matches!(self.peek(), Token::LBrace) {
4172                        break;
4173                    }
4174                }
4175                Token::Ident(i) => {
4176                    let i = i.clone();
4177                    self.advance();
4178                    s.push_str(&i);
4179                }
4180                Token::Semicolon => {
4181                    self.advance();
4182                    s.push(';');
4183                }
4184                Token::LParen => {
4185                    self.advance();
4186                    s.push('(');
4187                }
4188                Token::LBracket => {
4189                    self.advance();
4190                    s.push('[');
4191                }
4192                Token::RBracket => {
4193                    self.advance();
4194                    s.push(']');
4195                }
4196                Token::Backslash => {
4197                    self.advance();
4198                    s.push('\\');
4199                }
4200                Token::Comma => {
4201                    self.advance();
4202                    s.push(',');
4203                }
4204                Token::ScalarVar(v) => {
4205                    let v = v.clone();
4206                    self.advance();
4207                    s.push('$');
4208                    s.push_str(&v);
4209                }
4210                Token::ArrayVar(v) => {
4211                    let v = v.clone();
4212                    self.advance();
4213                    s.push('@');
4214                    s.push_str(&v);
4215                }
4216                // Bare `@` / `%` in prototypes (e.g. Try::Tiny's `sub try (&;@)`).
4217                Token::ArrayAt => {
4218                    self.advance();
4219                    s.push('@');
4220                }
4221                Token::HashVar(v) => {
4222                    let v = v.clone();
4223                    self.advance();
4224                    s.push('%');
4225                    s.push_str(&v);
4226                }
4227                Token::HashPercent => {
4228                    self.advance();
4229                    s.push('%');
4230                }
4231                Token::Plus => {
4232                    self.advance();
4233                    s.push('+');
4234                }
4235                Token::Minus => {
4236                    self.advance();
4237                    s.push('-');
4238                }
4239                Token::BitAnd => {
4240                    self.advance();
4241                    s.push('&');
4242                }
4243                tok => {
4244                    return Err(self.syntax_err(
4245                        format!("Unexpected token in sub prototype: {:?}", tok),
4246                        self.peek_line(),
4247                    ));
4248                }
4249            }
4250        }
4251        Ok(s)
4252    }
4253
4254    fn sub_signature_list_starts_here(&self) -> bool {
4255        match self.peek() {
4256            Token::LBrace | Token::LBracket => true,
4257            Token::ScalarVar(name) if name != "$$" && name != ")" => true,
4258            Token::ArrayVar(_) | Token::HashVar(_) => true,
4259            _ => false,
4260        }
4261    }
4262
4263    fn parse_sub_signature_hash_key(&mut self) -> PerlResult<String> {
4264        let (tok, line) = self.advance();
4265        match tok {
4266            Token::Ident(i) => Ok(i),
4267            Token::SingleString(s) | Token::DoubleString(s) => Ok(s),
4268            tok => Err(self.syntax_err(
4269                format!(
4270                    "sub signature: expected hash key (identifier or string), got {:?}",
4271                    tok
4272                ),
4273                line,
4274            )),
4275        }
4276    }
4277
4278    fn parse_sub_signature_param_list(&mut self) -> PerlResult<Vec<SubSigParam>> {
4279        let mut params = Vec::new();
4280        loop {
4281            if matches!(self.peek(), Token::RParen) {
4282                break;
4283            }
4284            match self.peek().clone() {
4285                Token::ScalarVar(name) => {
4286                    if name == "$$" || name == ")" {
4287                        return Err(self.syntax_err(
4288                            format!(
4289                                "`{name}` cannot start a stryke sub signature (use legacy prototype `($$)` etc.)"
4290                            ),
4291                            self.peek_line(),
4292                        ));
4293                    }
4294                    self.advance();
4295                    let ty = if self.eat(&Token::Colon) {
4296                        match self.peek() {
4297                            Token::Ident(ref tname) => {
4298                                let tname = tname.clone();
4299                                self.advance();
4300                                Some(match tname.as_str() {
4301                                    "Int" => PerlTypeName::Int,
4302                                    "Str" => PerlTypeName::Str,
4303                                    "Float" => PerlTypeName::Float,
4304                                    "Bool" => PerlTypeName::Bool,
4305                                    "Array" => PerlTypeName::Array,
4306                                    "Hash" => PerlTypeName::Hash,
4307                                    "Ref" => PerlTypeName::Ref,
4308                                    "Any" => PerlTypeName::Any,
4309                                    _ => PerlTypeName::Struct(tname),
4310                                })
4311                            }
4312                            _ => {
4313                                return Err(self.syntax_err(
4314                                    "expected type name after `:` in sub signature",
4315                                    self.peek_line(),
4316                                ));
4317                            }
4318                        }
4319                    } else {
4320                        None
4321                    };
4322                    // Check for default value: `$x = expr`
4323                    let default = if self.eat(&Token::Assign) {
4324                        Some(Box::new(self.parse_ternary()?))
4325                    } else {
4326                        None
4327                    };
4328                    params.push(SubSigParam::Scalar(name, ty, default));
4329                }
4330                Token::ArrayVar(name) => {
4331                    self.advance();
4332                    let default = if self.eat(&Token::Assign) {
4333                        Some(Box::new(self.parse_ternary()?))
4334                    } else {
4335                        None
4336                    };
4337                    params.push(SubSigParam::Array(name, default));
4338                }
4339                Token::HashVar(name) => {
4340                    self.advance();
4341                    let default = if self.eat(&Token::Assign) {
4342                        Some(Box::new(self.parse_ternary()?))
4343                    } else {
4344                        None
4345                    };
4346                    params.push(SubSigParam::Hash(name, default));
4347                }
4348                Token::LBracket => {
4349                    self.advance();
4350                    let elems = self.parse_match_array_elems_until_rbracket()?;
4351                    params.push(SubSigParam::ArrayDestruct(elems));
4352                }
4353                Token::LBrace => {
4354                    self.advance();
4355                    let mut pairs = Vec::new();
4356                    loop {
4357                        if matches!(self.peek(), Token::RBrace | Token::Eof) {
4358                            break;
4359                        }
4360                        if self.eat(&Token::Comma) {
4361                            continue;
4362                        }
4363                        let key = self.parse_sub_signature_hash_key()?;
4364                        self.expect(&Token::FatArrow)?;
4365                        let bind = self.parse_scalar_var_name()?;
4366                        pairs.push((key, bind));
4367                        self.eat(&Token::Comma);
4368                    }
4369                    self.expect(&Token::RBrace)?;
4370                    params.push(SubSigParam::HashDestruct(pairs));
4371                }
4372                tok => {
4373                    return Err(self.syntax_err(
4374                        format!(
4375                            "expected `$name`, `[ ... ]`, or `{{ ... }}` in sub signature, got {:?}",
4376                            tok
4377                        ),
4378                        self.peek_line(),
4379                    ));
4380                }
4381            }
4382            match self.peek() {
4383                Token::Comma => {
4384                    self.advance();
4385                    if matches!(self.peek(), Token::RParen) {
4386                        return Err(self.syntax_err(
4387                            "trailing `,` before `)` in sub signature",
4388                            self.peek_line(),
4389                        ));
4390                    }
4391                }
4392                Token::RParen => break,
4393                _ => {
4394                    return Err(self.syntax_err(
4395                        format!(
4396                            "expected `,` or `)` after sub signature parameter, got {:?}",
4397                            self.peek()
4398                        ),
4399                        self.peek_line(),
4400                    ));
4401                }
4402            }
4403        }
4404        Ok(params)
4405    }
4406
4407    /// Optional `sub` parens: either a Perl 5 prototype string or a stryke **`$name` / `{ k => $v }`** signature.
4408    fn parse_sub_sig_or_prototype_opt(&mut self) -> PerlResult<(Vec<SubSigParam>, Option<String>)> {
4409        if !matches!(self.peek(), Token::LParen) {
4410            return Ok((vec![], None));
4411        }
4412        self.advance();
4413        if matches!(self.peek(), Token::RParen) {
4414            self.advance();
4415            return Ok((vec![], Some(String::new())));
4416        }
4417        if self.sub_signature_list_starts_here() {
4418            let params = self.parse_sub_signature_param_list()?;
4419            self.expect(&Token::RParen)?;
4420            return Ok((params, None));
4421        }
4422        let proto = self.parse_legacy_sub_prototype_tail()?;
4423        Ok((vec![], Some(proto)))
4424    }
4425
4426    /// Optional subroutine attributes after name/prototype: `sub foo : lvalue { }`, `sub : ATTR(ARGS) { }`.
4427    fn parse_sub_attributes(&mut self) -> PerlResult<()> {
4428        while self.eat(&Token::Colon) {
4429            match self.advance() {
4430                (Token::Ident(_), _) => {}
4431                (tok, line) => {
4432                    return Err(self.syntax_err(
4433                        format!("Expected attribute name after `:`, got {:?}", tok),
4434                        line,
4435                    ));
4436                }
4437            }
4438            if self.eat(&Token::LParen) {
4439                let mut depth = 1usize;
4440                while depth > 0 {
4441                    match self.advance().0 {
4442                        Token::LParen => depth += 1,
4443                        Token::RParen => {
4444                            depth -= 1;
4445                        }
4446                        Token::Eof => {
4447                            return Err(self.syntax_err(
4448                                "Unterminated sub attribute argument list",
4449                                self.peek_line(),
4450                            ));
4451                        }
4452                        _ => {}
4453                    }
4454                }
4455            }
4456        }
4457        Ok(())
4458    }
4459
4460    /// After `fn` + optional `(SIG)` + attrs: `{ ... }` or stryke-only `= EXPR` (one assign-level
4461    /// expression; no top-level `,`). `sub` always requires `{ ... }`.
4462    fn parse_fn_eq_body_or_block(&mut self, is_sub_keyword: bool) -> PerlResult<Block> {
4463        if !is_sub_keyword && self.eat(&Token::Assign) {
4464            let expr = self.parse_assign_expr()?;
4465            if matches!(self.peek(), Token::Comma) {
4466                return Err(self.syntax_err(
4467                    "`fn ... =` allows only a single expression; use `fn ... { ... }` for multiple statements",
4468                    self.peek_line(),
4469                ));
4470            }
4471            let eline = expr.line;
4472            self.eat(&Token::Semicolon);
4473            let mut body = vec![Statement {
4474                label: None,
4475                kind: StmtKind::Expression(expr),
4476                line: eline,
4477            }];
4478            Self::default_topic_for_sole_bareword(&mut body);
4479            Ok(body)
4480        } else {
4481            self.parse_block()
4482        }
4483    }
4484
4485    fn parse_sub_decl(&mut self, is_sub_keyword: bool) -> PerlResult<Statement> {
4486        let line = self.peek_line();
4487        self.advance(); // 'sub' or 'fn'
4488        match self.peek().clone() {
4489            Token::Ident(_) => {
4490                let name = self.parse_package_qualified_identifier()?;
4491                // Topic-slot barewords (`_`, `_<`, `_<<`, `_<<<`, `_<<<<`,
4492                // `_0`, `_1`, …, `_N`, plus `_N<+` chain forms) are scalar
4493                // refs to the current/positional/outer topic. A user-defined
4494                // sub with any of these names — bare or package-qualified —
4495                // would shadow the topic in expression position and silently
4496                // break every `_`-aware builtin (`map { _ }`, `say _`,
4497                // `lc _`, …). Reject ALL forms at parse time, including
4498                // `Foo::_`, `Pkg::_0`, `My::Module::_<<<<`.
4499                let bare = name.rsplit("::").next().unwrap_or(&name);
4500                if Self::is_underscore_topic_slot(bare) {
4501                    return Err(self.syntax_err(
4502                        format!(
4503                            "`fn {}` would shadow the topic-slot scalar; pick a different name",
4504                            name
4505                        ),
4506                        line,
4507                    ));
4508                }
4509                if Self::is_reserved_special_var_name(bare) {
4510                    return Err(self.syntax_err(
4511                        format!(
4512                            "`fn {}` would shadow a Perl special variable / filehandle / compile-time token; pick a different name",
4513                            name
4514                        ),
4515                        line,
4516                    ));
4517                }
4518                // Allow shadowing builtins:
4519                // - In compat mode (full Perl 5)
4520                // - When parsing a module (imports should work)
4521                // Block shadowing:
4522                // - In user code (default mode, not parsing module)
4523                // - Always in no-interop mode
4524                let allow_shadow =
4525                    crate::compat_mode() || (self.parsing_module && !crate::no_interop_mode());
4526                if !allow_shadow {
4527                    self.check_udf_shadows_builtin(&name, line)?;
4528                }
4529                self.declared_subs.insert(name.clone());
4530                let (params, prototype) = self.parse_sub_sig_or_prototype_opt()?;
4531                self.parse_sub_attributes()?;
4532                let body = self.parse_fn_eq_body_or_block(is_sub_keyword)?;
4533                Ok(Statement {
4534                    label: None,
4535                    kind: StmtKind::SubDecl {
4536                        name,
4537                        params,
4538                        body,
4539                        prototype,
4540                    },
4541                    line,
4542                })
4543            }
4544            Token::LParen | Token::LBrace | Token::Colon => {
4545                // In no-interop mode, `sub {}` anonymous is not allowed — must use `fn {}`
4546                if is_sub_keyword && crate::no_interop_mode() {
4547                    return Err(self.syntax_err(
4548                        "stryke uses `fn {}` instead of `sub {}` (--no-interop)",
4549                        line,
4550                    ));
4551                }
4552                // Statement-level anonymous sub: `fn { }`, `sub () { }`, `sub :lvalue { }`
4553                let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
4554                self.parse_sub_attributes()?;
4555                let body = self.parse_fn_eq_body_or_block(is_sub_keyword)?;
4556                Ok(Statement {
4557                    label: None,
4558                    kind: StmtKind::Expression(Expr {
4559                        kind: ExprKind::CodeRef { params, body },
4560                        line,
4561                    }),
4562                    line,
4563                })
4564            }
4565            tok => {
4566                // Sigil-form topic-slot names (`fn $_`, `fn $_<`, `fn $_0`,
4567                // `fn @_`, `fn %_`, …) are also rejected with the same
4568                // foot-gun message as the bareword form. Without this branch
4569                // the user gets a confusing generic "Expected sub name" error.
4570                let topic_name = match &tok {
4571                    Token::ScalarVar(n) | Token::ArrayVar(n) | Token::HashVar(n)
4572                        if Self::is_underscore_topic_slot(n) =>
4573                    {
4574                        Some((
4575                            match &tok {
4576                                Token::ScalarVar(_) => '$',
4577                                Token::ArrayVar(_) => '@',
4578                                Token::HashVar(_) => '%',
4579                                _ => unreachable!(),
4580                            },
4581                            n.clone(),
4582                        ))
4583                    }
4584                    _ => None,
4585                };
4586                if let Some((sigil, n)) = topic_name {
4587                    return Err(self.syntax_err(
4588                        format!(
4589                            "`fn {}{}` would shadow the topic-slot scalar; pick a different name",
4590                            sigil, n
4591                        ),
4592                        self.peek_line(),
4593                    ));
4594                }
4595                // Sigil-form Perl special variables / globals — same foot-gun.
4596                // Catches `fn $@`, `fn $!`, `fn $/`, `fn $\\`, `fn $,`, `fn $;`,
4597                // `fn $"`, `fn $.`, `fn $0`, `fn $$`, `fn $?`, `fn $1`-`$9`,
4598                // `fn $^I`, `fn @ARGV`, `fn @INC`, `fn %ENV`, `fn %SIG`, etc.
4599                let special_var = match &tok {
4600                    Token::ScalarVar(n) | Token::ArrayVar(n) | Token::HashVar(n) => Some((
4601                        match &tok {
4602                            Token::ScalarVar(_) => '$',
4603                            Token::ArrayVar(_) => '@',
4604                            Token::HashVar(_) => '%',
4605                            _ => unreachable!(),
4606                        },
4607                        n.clone(),
4608                    )),
4609                    _ => None,
4610                };
4611                if let Some((sigil, n)) = special_var {
4612                    return Err(self.syntax_err(
4613                        format!(
4614                            "`fn {}{}` would shadow a Perl special variable / global; pick a different name",
4615                            sigil, n
4616                        ),
4617                        self.peek_line(),
4618                    ));
4619                }
4620                // After `fn`, `%` lexes as `Token::Percent` (modulo) rather
4621                // than a hash sigil — but `fn %ENV { }`, `fn %SIG { }`,
4622                // `fn %_ { }`, etc. all reach here. Emit the same foot-gun
4623                // message as the sigil-form catch above.
4624                if matches!(tok, Token::Percent) {
4625                    return Err(self.syntax_err(
4626                        "`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 { ... }`",
4627                        self.peek_line(),
4628                    ));
4629                }
4630                Err(self.syntax_err(
4631                    format!("Expected sub name, `(`, `{{`, or `:`, got {:?}", tok),
4632                    self.peek_line(),
4633                ))
4634            }
4635        }
4636    }
4637
4638    /// `before|after|around "<glob>" { ... }` — register AOP advice.
4639    /// The pattern is a glob (`*`, `?`) matched against the called sub's bare name.
4640    fn parse_advice_decl(&mut self, kind: crate::ast::AdviceKind) -> PerlResult<Statement> {
4641        let line = self.peek_line();
4642        self.advance(); // before/after/around
4643        let pattern = match self.advance() {
4644            (Token::SingleString(s), _) | (Token::DoubleString(s), _) => s,
4645            (tok, err_line) => {
4646                return Err(self.syntax_err(
4647                    format!(
4648                        "Expected string-literal pattern after `{}`, got {:?}",
4649                        match kind {
4650                            crate::ast::AdviceKind::Before => "before",
4651                            crate::ast::AdviceKind::After => "after",
4652                            crate::ast::AdviceKind::Around => "around",
4653                        },
4654                        tok
4655                    ),
4656                    err_line,
4657                ));
4658            }
4659        };
4660        let body = self.parse_block()?;
4661        Ok(Statement {
4662            label: None,
4663            kind: StmtKind::AdviceDecl {
4664                kind,
4665                pattern,
4666                body,
4667            },
4668            line,
4669        })
4670    }
4671
4672    /// `struct Name { field => Type, ... ; fn method { } }`
4673    fn parse_struct_decl(&mut self) -> PerlResult<Statement> {
4674        let line = self.peek_line();
4675        self.advance(); // struct
4676        let name = self.parse_package_qualified_identifier().map_err(|_| {
4677            self.syntax_err(
4678                format!("Expected struct name, got {:?}", self.peek()),
4679                self.peek_line(),
4680            )
4681        })?;
4682        self.expect(&Token::LBrace)?;
4683        let mut fields = Vec::new();
4684        let mut methods = Vec::new();
4685        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
4686            // Check for method definition: `fn name { }` or `fn name { }`
4687            let is_method = match self.peek() {
4688                Token::Ident(s) => s == "fn" || s == "sub",
4689                _ => false,
4690            };
4691            if is_method {
4692                self.advance(); // fn/sub
4693                let method_name = match self.advance() {
4694                    (Token::Ident(n), _) => n,
4695                    (tok, err_line) => {
4696                        return Err(self
4697                            .syntax_err(format!("Expected method name, got {:?}", tok), err_line))
4698                    }
4699                };
4700                // Parse optional signature: `($self, $arg: Type, ...)`
4701                let params = if self.eat(&Token::LParen) {
4702                    let p = self.parse_sub_signature_param_list()?;
4703                    self.expect(&Token::RParen)?;
4704                    p
4705                } else {
4706                    Vec::new()
4707                };
4708                // parse_block handles its own { } delimiters
4709                let body = self.parse_block()?;
4710                methods.push(crate::ast::StructMethod {
4711                    name: method_name,
4712                    params,
4713                    body,
4714                });
4715                // Optional trailing comma/semicolon after method
4716                self.eat(&Token::Comma);
4717                self.eat(&Token::Semicolon);
4718                continue;
4719            }
4720
4721            let field_name = match self.advance() {
4722                (Token::Ident(n), _) => n,
4723                (tok, err_line) => {
4724                    return Err(
4725                        self.syntax_err(format!("Expected field name, got {:?}", tok), err_line)
4726                    )
4727                }
4728            };
4729            // Support both `field => Type` and bare `field` (implies Any type)
4730            let ty = if self.eat(&Token::FatArrow) {
4731                self.parse_type_name()?
4732            } else {
4733                crate::ast::PerlTypeName::Any
4734            };
4735            let default = if self.eat(&Token::Assign) {
4736                // Use parse_ternary to avoid consuming commas (next field separator)
4737                Some(self.parse_ternary()?)
4738            } else {
4739                None
4740            };
4741            fields.push(StructField {
4742                name: field_name,
4743                ty,
4744                default,
4745            });
4746            if !self.eat(&Token::Comma) {
4747                // Also allow semicolons as field separators
4748                self.eat(&Token::Semicolon);
4749            }
4750        }
4751        self.expect(&Token::RBrace)?;
4752        self.eat(&Token::Semicolon);
4753        Ok(Statement {
4754            label: None,
4755            kind: StmtKind::StructDecl {
4756                def: StructDef {
4757                    name,
4758                    fields,
4759                    methods,
4760                },
4761            },
4762            line,
4763        })
4764    }
4765
4766    /// `enum Name { Variant1, Variant2 => Type, ... }`
4767    fn parse_enum_decl(&mut self) -> PerlResult<Statement> {
4768        let line = self.peek_line();
4769        self.advance(); // enum
4770        let name = self.parse_package_qualified_identifier().map_err(|_| {
4771            self.syntax_err(
4772                format!("Expected enum name, got {:?}", self.peek()),
4773                self.peek_line(),
4774            )
4775        })?;
4776        self.expect(&Token::LBrace)?;
4777        let mut variants = Vec::new();
4778        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
4779            let variant_name = match self.advance() {
4780                (Token::Ident(n), _) => n,
4781                (tok, err_line) => {
4782                    return Err(
4783                        self.syntax_err(format!("Expected variant name, got {:?}", tok), err_line)
4784                    )
4785                }
4786            };
4787            let ty = if self.eat(&Token::FatArrow) {
4788                Some(self.parse_type_name()?)
4789            } else {
4790                None
4791            };
4792            variants.push(EnumVariant {
4793                name: variant_name,
4794                ty,
4795            });
4796            if !self.eat(&Token::Comma) {
4797                self.eat(&Token::Semicolon);
4798            }
4799        }
4800        self.expect(&Token::RBrace)?;
4801        self.eat(&Token::Semicolon);
4802        Ok(Statement {
4803            label: None,
4804            kind: StmtKind::EnumDecl {
4805                def: EnumDef { name, variants },
4806            },
4807            line,
4808        })
4809    }
4810
4811    /// `[abstract|final] class Name extends Parent impl Trait { fields; methods }`
4812    fn parse_class_decl(&mut self, is_abstract: bool, is_final: bool) -> PerlResult<Statement> {
4813        use crate::ast::{ClassDef, ClassField, ClassMethod, ClassStaticField, Visibility};
4814        let line = self.peek_line();
4815        self.advance(); // class
4816        let name = self.parse_package_qualified_identifier().map_err(|_| {
4817            self.syntax_err(
4818                format!("Expected class name, got {:?}", self.peek()),
4819                self.peek_line(),
4820            )
4821        })?;
4822
4823        // Parse `extends Parent1, Parent2` (each may be namespaced: `Foo::Base`)
4824        let mut extends = Vec::new();
4825        if matches!(self.peek(), Token::Ident(ref s) if s == "extends") {
4826            self.advance(); // extends
4827            loop {
4828                let parent = self.parse_package_qualified_identifier().map_err(|_| {
4829                    self.syntax_err(
4830                        format!(
4831                            "Expected parent class name after `extends`, got {:?}",
4832                            self.peek()
4833                        ),
4834                        self.peek_line(),
4835                    )
4836                })?;
4837                extends.push(parent);
4838                if !self.eat(&Token::Comma) {
4839                    break;
4840                }
4841            }
4842        }
4843
4844        // Parse `impl Trait1, Trait2` (each may be namespaced: `Foo::Trait`)
4845        let mut implements = Vec::new();
4846        if matches!(self.peek(), Token::Ident(ref s) if s == "impl") {
4847            self.advance(); // impl
4848            loop {
4849                let trait_name = self.parse_package_qualified_identifier().map_err(|_| {
4850                    self.syntax_err(
4851                        format!("Expected trait name after `impl`, got {:?}", self.peek()),
4852                        self.peek_line(),
4853                    )
4854                })?;
4855                implements.push(trait_name);
4856                if !self.eat(&Token::Comma) {
4857                    break;
4858                }
4859            }
4860        }
4861
4862        self.expect(&Token::LBrace)?;
4863        let mut fields = Vec::new();
4864        let mut methods = Vec::new();
4865        let mut static_fields = Vec::new();
4866
4867        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
4868            // Check for visibility modifier
4869            let visibility = match self.peek() {
4870                Token::Ident(ref s) if s == "pub" => {
4871                    self.advance();
4872                    Visibility::Public
4873                }
4874                Token::Ident(ref s) if s == "priv" => {
4875                    self.advance();
4876                    Visibility::Private
4877                }
4878                Token::Ident(ref s) if s == "prot" => {
4879                    self.advance();
4880                    Visibility::Protected
4881                }
4882                _ => Visibility::Public, // default public
4883            };
4884
4885            // Check for static field: `static name: Type = default`
4886            if matches!(self.peek(), Token::Ident(ref s) if s == "static") {
4887                self.advance(); // static
4888
4889                // Could be a static method (`static fn`) or static field
4890                if matches!(self.peek(), Token::Ident(ref s) if s == "fn" || s == "sub") {
4891                    // static fn is same as fn Self.name — handled below but not here
4892                    return Err(self.syntax_err(
4893                        "use `fn Self.name` for static methods, not `static fn`",
4894                        self.peek_line(),
4895                    ));
4896                }
4897
4898                let field_name = match self.advance() {
4899                    (Token::Ident(n), _) => n,
4900                    (tok, err_line) => {
4901                        return Err(self.syntax_err(
4902                            format!("Expected static field name, got {:?}", tok),
4903                            err_line,
4904                        ))
4905                    }
4906                };
4907
4908                let ty = if self.eat(&Token::Colon) {
4909                    self.parse_type_name()?
4910                } else {
4911                    crate::ast::PerlTypeName::Any
4912                };
4913
4914                let default = if self.eat(&Token::Assign) {
4915                    Some(self.parse_ternary()?)
4916                } else {
4917                    None
4918                };
4919
4920                static_fields.push(ClassStaticField {
4921                    name: field_name,
4922                    ty,
4923                    visibility,
4924                    default,
4925                });
4926
4927                if !self.eat(&Token::Comma) {
4928                    self.eat(&Token::Semicolon);
4929                }
4930                continue;
4931            }
4932
4933            // Check for `final` modifier before fn
4934            let method_is_final = matches!(self.peek(), Token::Ident(ref s) if s == "final");
4935            if method_is_final {
4936                self.advance(); // final
4937            }
4938
4939            // Check for method: `fn name` or `fn Self.name` (static)
4940            let is_method = matches!(self.peek(), Token::Ident(ref s) if s == "fn" || s == "sub");
4941            if is_method {
4942                self.advance(); // fn/sub
4943
4944                // Check for static method: `fn Self.name`
4945                let is_static = matches!(self.peek(), Token::Ident(ref s) if s == "Self");
4946                if is_static {
4947                    self.advance(); // Self
4948                    self.expect(&Token::Dot)?;
4949                }
4950
4951                let method_name = match self.advance() {
4952                    (Token::Ident(n), _) => n,
4953                    (tok, err_line) => {
4954                        return Err(self
4955                            .syntax_err(format!("Expected method name, got {:?}", tok), err_line))
4956                    }
4957                };
4958
4959                // Parse optional signature
4960                let params = if self.eat(&Token::LParen) {
4961                    let p = self.parse_sub_signature_param_list()?;
4962                    self.expect(&Token::RParen)?;
4963                    p
4964                } else {
4965                    Vec::new()
4966                };
4967
4968                // Body is optional (abstract method in trait has no body)
4969                let body = if matches!(self.peek(), Token::LBrace) {
4970                    Some(self.parse_block()?)
4971                } else {
4972                    None
4973                };
4974
4975                methods.push(ClassMethod {
4976                    name: method_name,
4977                    params,
4978                    body,
4979                    visibility,
4980                    is_static,
4981                    is_final: method_is_final,
4982                });
4983                self.eat(&Token::Comma);
4984                self.eat(&Token::Semicolon);
4985                continue;
4986            } else if method_is_final {
4987                return Err(self.syntax_err("`final` must be followed by `fn`", self.peek_line()));
4988            }
4989
4990            // Parse field: `name: Type = default`
4991            let field_name = match self.advance() {
4992                (Token::Ident(n), _) => n,
4993                (tok, err_line) => {
4994                    return Err(
4995                        self.syntax_err(format!("Expected field name, got {:?}", tok), err_line)
4996                    )
4997                }
4998            };
4999
5000            // Type after colon: `name: Type`
5001            let ty = if self.eat(&Token::Colon) {
5002                self.parse_type_name()?
5003            } else {
5004                crate::ast::PerlTypeName::Any
5005            };
5006
5007            // Default value after `=`
5008            let default = if self.eat(&Token::Assign) {
5009                Some(self.parse_ternary()?)
5010            } else {
5011                None
5012            };
5013
5014            fields.push(ClassField {
5015                name: field_name,
5016                ty,
5017                visibility,
5018                default,
5019            });
5020
5021            if !self.eat(&Token::Comma) {
5022                self.eat(&Token::Semicolon);
5023            }
5024        }
5025
5026        self.expect(&Token::RBrace)?;
5027        self.eat(&Token::Semicolon);
5028
5029        Ok(Statement {
5030            label: None,
5031            kind: StmtKind::ClassDecl {
5032                def: ClassDef {
5033                    name,
5034                    is_abstract,
5035                    is_final,
5036                    extends,
5037                    implements,
5038                    fields,
5039                    methods,
5040                    static_fields,
5041                },
5042            },
5043            line,
5044        })
5045    }
5046
5047    /// `trait Name { fn required; fn with_default { } }`
5048    fn parse_trait_decl(&mut self) -> PerlResult<Statement> {
5049        use crate::ast::{ClassMethod, TraitDef, Visibility};
5050        let line = self.peek_line();
5051        self.advance(); // trait
5052        let name = self.parse_package_qualified_identifier().map_err(|_| {
5053            self.syntax_err(
5054                format!("Expected trait name, got {:?}", self.peek()),
5055                self.peek_line(),
5056            )
5057        })?;
5058
5059        self.expect(&Token::LBrace)?;
5060        let mut methods = Vec::new();
5061
5062        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
5063            // Optional visibility
5064            let visibility = match self.peek() {
5065                Token::Ident(ref s) if s == "pub" => {
5066                    self.advance();
5067                    Visibility::Public
5068                }
5069                Token::Ident(ref s) if s == "priv" => {
5070                    self.advance();
5071                    Visibility::Private
5072                }
5073                Token::Ident(ref s) if s == "prot" => {
5074                    self.advance();
5075                    Visibility::Protected
5076                }
5077                _ => Visibility::Public,
5078            };
5079
5080            // Expect `fn` or `sub`
5081            if !matches!(self.peek(), Token::Ident(ref s) if s == "fn" || s == "sub") {
5082                return Err(self.syntax_err("Expected `fn` in trait definition", self.peek_line()));
5083            }
5084            self.advance(); // fn/sub
5085
5086            let method_name = match self.advance() {
5087                (Token::Ident(n), _) => n,
5088                (tok, err_line) => {
5089                    return Err(
5090                        self.syntax_err(format!("Expected method name, got {:?}", tok), err_line)
5091                    )
5092                }
5093            };
5094
5095            // Optional signature
5096            let params = if self.eat(&Token::LParen) {
5097                let p = self.parse_sub_signature_param_list()?;
5098                self.expect(&Token::RParen)?;
5099                p
5100            } else {
5101                Vec::new()
5102            };
5103
5104            // Body is optional (no body = abstract/required method)
5105            let body = if matches!(self.peek(), Token::LBrace) {
5106                Some(self.parse_block()?)
5107            } else {
5108                None
5109            };
5110
5111            methods.push(ClassMethod {
5112                name: method_name,
5113                params,
5114                body,
5115                visibility,
5116                is_static: false,
5117                is_final: false,
5118            });
5119
5120            self.eat(&Token::Comma);
5121            self.eat(&Token::Semicolon);
5122        }
5123
5124        self.expect(&Token::RBrace)?;
5125        self.eat(&Token::Semicolon);
5126
5127        Ok(Statement {
5128            label: None,
5129            kind: StmtKind::TraitDecl {
5130                def: TraitDef { name, methods },
5131            },
5132            line,
5133        })
5134    }
5135
5136    fn local_simple_target_to_var_decl(target: &Expr) -> Option<VarDecl> {
5137        match &target.kind {
5138            ExprKind::ScalarVar(name) => Some(VarDecl {
5139                sigil: Sigil::Scalar,
5140                name: name.clone(),
5141                initializer: None,
5142                frozen: false,
5143                type_annotation: None,
5144            }),
5145            ExprKind::ArrayVar(name) => Some(VarDecl {
5146                sigil: Sigil::Array,
5147                name: name.clone(),
5148                initializer: None,
5149                frozen: false,
5150                type_annotation: None,
5151            }),
5152            ExprKind::HashVar(name) => Some(VarDecl {
5153                sigil: Sigil::Hash,
5154                name: name.clone(),
5155                initializer: None,
5156                frozen: false,
5157                type_annotation: None,
5158            }),
5159            ExprKind::Typeglob(name) => Some(VarDecl {
5160                sigil: Sigil::Typeglob,
5161                name: name.clone(),
5162                initializer: None,
5163                frozen: false,
5164                type_annotation: None,
5165            }),
5166            _ => None,
5167        }
5168    }
5169
5170    fn parse_decl_array_destructure(
5171        &mut self,
5172        keyword: &str,
5173        line: usize,
5174    ) -> PerlResult<Statement> {
5175        self.expect(&Token::LBracket)?;
5176        let elems = self.parse_match_array_elems_until_rbracket()?;
5177        self.expect(&Token::Assign)?;
5178        self.suppress_scalar_hash_brace += 1;
5179        let rhs = self.parse_expression()?;
5180        self.suppress_scalar_hash_brace -= 1;
5181        let stmt = self.desugar_array_destructure(keyword, line, elems, rhs)?;
5182        self.parse_stmt_postfix_modifier(stmt)
5183    }
5184
5185    fn parse_decl_hash_destructure(&mut self, keyword: &str, line: usize) -> PerlResult<Statement> {
5186        let MatchPattern::Hash(pairs) = self.parse_match_hash_pattern()? else {
5187            unreachable!("parse_match_hash_pattern returns Hash");
5188        };
5189        self.expect(&Token::Assign)?;
5190        self.suppress_scalar_hash_brace += 1;
5191        let rhs = self.parse_expression()?;
5192        self.suppress_scalar_hash_brace -= 1;
5193        let stmt = self.desugar_hash_destructure(keyword, line, pairs, rhs)?;
5194        self.parse_stmt_postfix_modifier(stmt)
5195    }
5196
5197    fn desugar_array_destructure(
5198        &mut self,
5199        keyword: &str,
5200        line: usize,
5201        elems: Vec<MatchArrayElem>,
5202        rhs: Expr,
5203    ) -> PerlResult<Statement> {
5204        let tmp = format!("__stryke_ds_{}", self.alloc_desugar_tmp());
5205        let mut stmts: Vec<Statement> = Vec::new();
5206        stmts.push(destructure_stmt_from_var_decls(
5207            keyword,
5208            vec![VarDecl {
5209                sigil: Sigil::Scalar,
5210                name: tmp.clone(),
5211                initializer: Some(rhs),
5212                frozen: false,
5213                type_annotation: None,
5214            }],
5215            line,
5216        ));
5217
5218        let has_rest = elems
5219            .iter()
5220            .any(|e| matches!(e, MatchArrayElem::Rest | MatchArrayElem::RestBind(_)));
5221        let fixed_slots = elems
5222            .iter()
5223            .filter(|e| {
5224                matches!(
5225                    e,
5226                    MatchArrayElem::CaptureScalar(_) | MatchArrayElem::Expr(_)
5227                )
5228            })
5229            .count();
5230        if !has_rest {
5231            let cond = Expr {
5232                kind: ExprKind::BinOp {
5233                    left: Box::new(destructure_expr_array_len(&tmp, line)),
5234                    op: BinOp::NumEq,
5235                    right: Box::new(Expr {
5236                        kind: ExprKind::Integer(fixed_slots as i64),
5237                        line,
5238                    }),
5239                },
5240                line,
5241            };
5242            stmts.push(destructure_stmt_unless_die(
5243                line,
5244                cond,
5245                "array destructure: length mismatch",
5246            ));
5247        }
5248
5249        let mut idx: i64 = 0;
5250        for elem in elems {
5251            match elem {
5252                MatchArrayElem::Rest => break,
5253                MatchArrayElem::RestBind(name) => {
5254                    let list_source = Expr {
5255                        kind: ExprKind::Deref {
5256                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
5257                            kind: Sigil::Array,
5258                        },
5259                        line,
5260                    };
5261                    let last_ix = Expr {
5262                        kind: ExprKind::BinOp {
5263                            left: Box::new(destructure_expr_array_len(&tmp, line)),
5264                            op: BinOp::Sub,
5265                            right: Box::new(Expr {
5266                                kind: ExprKind::Integer(1),
5267                                line,
5268                            }),
5269                        },
5270                        line,
5271                    };
5272                    let range = Expr {
5273                        kind: ExprKind::Range {
5274                            from: Box::new(Expr {
5275                                kind: ExprKind::Integer(idx),
5276                                line,
5277                            }),
5278                            to: Box::new(last_ix),
5279                            exclusive: false,
5280                            step: None,
5281                        },
5282                        line,
5283                    };
5284                    let slice = Expr {
5285                        kind: ExprKind::AnonymousListSlice {
5286                            source: Box::new(list_source),
5287                            indices: vec![range],
5288                        },
5289                        line,
5290                    };
5291                    stmts.push(destructure_stmt_from_var_decls(
5292                        keyword,
5293                        vec![VarDecl {
5294                            sigil: Sigil::Array,
5295                            name,
5296                            initializer: Some(slice),
5297                            frozen: false,
5298                            type_annotation: None,
5299                        }],
5300                        line,
5301                    ));
5302                    break;
5303                }
5304                MatchArrayElem::CaptureScalar(name) => {
5305                    let arrow = Expr {
5306                        kind: ExprKind::ArrowDeref {
5307                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
5308                            index: Box::new(Expr {
5309                                kind: ExprKind::Integer(idx),
5310                                line,
5311                            }),
5312                            kind: DerefKind::Array,
5313                        },
5314                        line,
5315                    };
5316                    stmts.push(destructure_stmt_from_var_decls(
5317                        keyword,
5318                        vec![VarDecl {
5319                            sigil: Sigil::Scalar,
5320                            name,
5321                            initializer: Some(arrow),
5322                            frozen: false,
5323                            type_annotation: None,
5324                        }],
5325                        line,
5326                    ));
5327                    idx += 1;
5328                }
5329                MatchArrayElem::Expr(e) => {
5330                    let elem_subj = Expr {
5331                        kind: ExprKind::ArrowDeref {
5332                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
5333                            index: Box::new(Expr {
5334                                kind: ExprKind::Integer(idx),
5335                                line,
5336                            }),
5337                            kind: DerefKind::Array,
5338                        },
5339                        line,
5340                    };
5341                    let match_expr = Expr {
5342                        kind: ExprKind::AlgebraicMatch {
5343                            subject: Box::new(elem_subj),
5344                            arms: vec![
5345                                MatchArm {
5346                                    pattern: MatchPattern::Value(Box::new(e.clone())),
5347                                    guard: None,
5348                                    body: Expr {
5349                                        kind: ExprKind::Integer(0),
5350                                        line,
5351                                    },
5352                                },
5353                                MatchArm {
5354                                    pattern: MatchPattern::Any,
5355                                    guard: None,
5356                                    body: Expr {
5357                                        kind: ExprKind::Die(vec![Expr {
5358                                            kind: ExprKind::String(
5359                                                "array destructure: element pattern mismatch"
5360                                                    .to_string(),
5361                                            ),
5362                                            line,
5363                                        }]),
5364                                        line,
5365                                    },
5366                                },
5367                            ],
5368                        },
5369                        line,
5370                    };
5371                    stmts.push(Statement {
5372                        label: None,
5373                        kind: StmtKind::Expression(match_expr),
5374                        line,
5375                    });
5376                    idx += 1;
5377                }
5378            }
5379        }
5380
5381        Ok(Statement {
5382            label: None,
5383            kind: StmtKind::StmtGroup(stmts),
5384            line,
5385        })
5386    }
5387
5388    fn desugar_hash_destructure(
5389        &mut self,
5390        keyword: &str,
5391        line: usize,
5392        pairs: Vec<MatchHashPair>,
5393        rhs: Expr,
5394    ) -> PerlResult<Statement> {
5395        let tmp = format!("__stryke_ds_{}", self.alloc_desugar_tmp());
5396        let mut stmts: Vec<Statement> = Vec::new();
5397        stmts.push(destructure_stmt_from_var_decls(
5398            keyword,
5399            vec![VarDecl {
5400                sigil: Sigil::Scalar,
5401                name: tmp.clone(),
5402                initializer: Some(rhs),
5403                frozen: false,
5404                type_annotation: None,
5405            }],
5406            line,
5407        ));
5408
5409        for pair in pairs {
5410            match pair {
5411                MatchHashPair::KeyOnly { key } => {
5412                    let exists_op = Expr {
5413                        kind: ExprKind::Exists(Box::new(Expr {
5414                            kind: ExprKind::ArrowDeref {
5415                                expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
5416                                index: Box::new(key),
5417                                kind: DerefKind::Hash,
5418                            },
5419                            line,
5420                        })),
5421                        line,
5422                    };
5423                    stmts.push(destructure_stmt_unless_die(
5424                        line,
5425                        exists_op,
5426                        "hash destructure: missing required key",
5427                    ));
5428                }
5429                MatchHashPair::Capture { key, name } => {
5430                    let init = Expr {
5431                        kind: ExprKind::ArrowDeref {
5432                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
5433                            index: Box::new(key),
5434                            kind: DerefKind::Hash,
5435                        },
5436                        line,
5437                    };
5438                    stmts.push(destructure_stmt_from_var_decls(
5439                        keyword,
5440                        vec![VarDecl {
5441                            sigil: Sigil::Scalar,
5442                            name,
5443                            initializer: Some(init),
5444                            frozen: false,
5445                            type_annotation: None,
5446                        }],
5447                        line,
5448                    ));
5449                }
5450            }
5451        }
5452
5453        Ok(Statement {
5454            label: None,
5455            kind: StmtKind::StmtGroup(stmts),
5456            line,
5457        })
5458    }
5459
5460    fn parse_my_our_local(
5461        &mut self,
5462        keyword: &str,
5463        allow_type_annotation: bool,
5464    ) -> PerlResult<Statement> {
5465        let line = self.peek_line();
5466        self.advance(); // 'my'/'our'/'local'
5467
5468        if keyword == "local"
5469            && !matches!(self.peek(), Token::LParen | Token::LBracket | Token::LBrace)
5470        {
5471            let target = self.parse_postfix()?;
5472            let mut initializer: Option<Expr> = None;
5473            if self.eat(&Token::Assign) {
5474                initializer = Some(self.parse_expression()?);
5475            } else if matches!(
5476                self.peek(),
5477                Token::OrAssign | Token::DefinedOrAssign | Token::AndAssign
5478            ) {
5479                if matches!(&target.kind, ExprKind::Typeglob(_)) {
5480                    return Err(self.syntax_err(
5481                        "compound assignment on typeglob declaration is not supported",
5482                        self.peek_line(),
5483                    ));
5484                }
5485                let op = match self.peek().clone() {
5486                    Token::OrAssign => BinOp::LogOr,
5487                    Token::DefinedOrAssign => BinOp::DefinedOr,
5488                    Token::AndAssign => BinOp::LogAnd,
5489                    _ => unreachable!(),
5490                };
5491                self.advance();
5492                let rhs = self.parse_assign_expr()?;
5493                let tgt_line = target.line;
5494                initializer = Some(Expr {
5495                    kind: ExprKind::CompoundAssign {
5496                        target: Box::new(target.clone()),
5497                        op,
5498                        value: Box::new(rhs),
5499                    },
5500                    line: tgt_line,
5501                });
5502            }
5503
5504            let kind = if let Some(mut decl) = Self::local_simple_target_to_var_decl(&target) {
5505                decl.initializer = initializer;
5506                StmtKind::Local(vec![decl])
5507            } else {
5508                StmtKind::LocalExpr {
5509                    target,
5510                    initializer,
5511                }
5512            };
5513            let stmt = Statement {
5514                label: None,
5515                kind,
5516                line,
5517            };
5518            return self.parse_stmt_postfix_modifier(stmt);
5519        }
5520
5521        if matches!(self.peek(), Token::LBracket) {
5522            return self.parse_decl_array_destructure(keyword, line);
5523        }
5524        if matches!(self.peek(), Token::LBrace) {
5525            return self.parse_decl_hash_destructure(keyword, line);
5526        }
5527
5528        let mut decls = Vec::new();
5529
5530        if self.eat(&Token::LParen) {
5531            // my ($a, @b, %c)
5532            while !matches!(self.peek(), Token::RParen | Token::Eof) {
5533                let decl = self.parse_var_decl(allow_type_annotation)?;
5534                decls.push(decl);
5535                if !self.eat(&Token::Comma) {
5536                    break;
5537                }
5538            }
5539            self.expect(&Token::RParen)?;
5540        } else {
5541            decls.push(self.parse_var_decl(allow_type_annotation)?);
5542        }
5543
5544        // Optional initializer: my $x = expr — plus `our @EXPORT = our @EXPORT_OK = qw(...)` (Try::Tiny).
5545        if self.eat(&Token::Assign) {
5546            if keyword == "our" && decls.len() == 1 {
5547                while matches!(self.peek(), Token::Ident(ref i) if i == "our") {
5548                    self.advance();
5549                    decls.push(self.parse_var_decl(allow_type_annotation)?);
5550                    if !self.eat(&Token::Assign) {
5551                        return Err(self.syntax_err(
5552                            "expected `=` after `our` in chained our-declaration",
5553                            self.peek_line(),
5554                        ));
5555                    }
5556                }
5557            }
5558            let rhs_start_pos = self.pos;
5559            let mut val = self.parse_expression()?;
5560            // Stryke implicit-coderef sugar: `my $f = _ * 2;` ≡
5561            // `my $f = fn { _ * 2 };`. Triggers only when (a) the LHS is a
5562            // single scalar declaration, (b) the RHS contains at least one
5563            // *bare* positional alias (`_`, `_0`, `_1`, …; no `$` sigil), and
5564            // (c) the RHS isn't already a coderef-shaped value. Bare-positional
5565            // tracking comes from the lexer (see `Lexer::bare_positional_indices`)
5566            // so legitimate uses of `$_` inside e.g. `fn { my $x = $_; … }`
5567            // closures keep their Perl semantics.
5568            if !crate::compat_mode()
5569                && self.block_depth == 0
5570                && decls.len() == 1
5571                && matches!(decls[0].sigil, Sigil::Scalar)
5572                && !matches!(
5573                    val.kind,
5574                    ExprKind::CodeRef { .. }
5575                        | ExprKind::SubroutineRef(_)
5576                        | ExprKind::SubroutineCodeRef(_)
5577                        | ExprKind::DynamicSubCodeRef(_)
5578                )
5579            {
5580                let rhs_end_pos = self.pos;
5581                // Trigger only when the RHS *begins* with a bare positional
5582                // alias (e.g. `_ * 2`, `_1 + _2`). Restricting to the leading
5583                // token avoids false positives when bare `_` appears deeper in
5584                // unrelated grammar (most notably `match { _ => ... }` arm
5585                // patterns, which are wildcard patterns rather than topic
5586                // references).
5587                let rhs_has_bare_positional = self.bare_positional_indices.contains(&rhs_start_pos)
5588                    && rhs_start_pos < rhs_end_pos;
5589                if rhs_has_bare_positional {
5590                    let val_line = val.line;
5591                    val = Expr {
5592                        kind: ExprKind::CodeRef {
5593                            params: Vec::new(),
5594                            body: vec![Statement {
5595                                label: None,
5596                                kind: StmtKind::Expression(val),
5597                                line: val_line,
5598                            }],
5599                        },
5600                        line: val_line,
5601                    };
5602                }
5603            }
5604            // Validate assignment for single variable declarations (not destructuring)
5605            // `my ($a, $b) = (1, 2)` is destructuring, not scalar-from-list
5606            if !crate::compat_mode() && decls.len() == 1 {
5607                let decl = &decls[0];
5608                let target_kind = match decl.sigil {
5609                    Sigil::Scalar => ExprKind::ScalarVar(decl.name.clone()),
5610                    Sigil::Array => ExprKind::ArrayVar(decl.name.clone()),
5611                    Sigil::Hash => ExprKind::HashVar(decl.name.clone()),
5612                    Sigil::Typeglob => {
5613                        // Skip validation for typeglob
5614                        if decls.len() == 1 {
5615                            decls[0].initializer = Some(val);
5616                        } else {
5617                            for d in &mut decls {
5618                                d.initializer = Some(val.clone());
5619                            }
5620                        }
5621                        return Ok(Statement {
5622                            label: None,
5623                            kind: match keyword {
5624                                "my" => StmtKind::My(decls),
5625                                "mysync" => StmtKind::MySync(decls),
5626                                "our" => StmtKind::Our(decls),
5627                                "oursync" => StmtKind::OurSync(decls),
5628                                "local" => StmtKind::Local(decls),
5629                                "state" => StmtKind::State(decls),
5630                                _ => unreachable!(),
5631                            },
5632                            line,
5633                        });
5634                    }
5635                };
5636                let target = Expr {
5637                    kind: target_kind,
5638                    line,
5639                };
5640                self.validate_assignment(&target, &val, line)?;
5641            }
5642            if decls.len() == 1 {
5643                decls[0].initializer = Some(val);
5644            } else {
5645                for decl in &mut decls {
5646                    decl.initializer = Some(val.clone());
5647                }
5648            }
5649        } else if decls.len() == 1 {
5650            // `our $Verbose ||= 0` (Exporter.pm) — compound assign on a single decl
5651            let op = match self.peek().clone() {
5652                Token::OrAssign => Some(BinOp::LogOr),
5653                Token::DefinedOrAssign => Some(BinOp::DefinedOr),
5654                Token::AndAssign => Some(BinOp::LogAnd),
5655                _ => None,
5656            };
5657            if let Some(op) = op {
5658                let d = &decls[0];
5659                if matches!(d.sigil, Sigil::Typeglob) {
5660                    return Err(self.syntax_err(
5661                        "compound assignment on typeglob declaration is not supported",
5662                        self.peek_line(),
5663                    ));
5664                }
5665                self.advance();
5666                let rhs = self.parse_assign_expr()?;
5667                let target = Expr {
5668                    kind: match d.sigil {
5669                        Sigil::Scalar => ExprKind::ScalarVar(d.name.clone()),
5670                        Sigil::Array => ExprKind::ArrayVar(d.name.clone()),
5671                        Sigil::Hash => ExprKind::HashVar(d.name.clone()),
5672                        Sigil::Typeglob => unreachable!(),
5673                    },
5674                    line,
5675                };
5676                decls[0].initializer = Some(Expr {
5677                    kind: ExprKind::CompoundAssign {
5678                        target: Box::new(target),
5679                        op,
5680                        value: Box::new(rhs),
5681                    },
5682                    line,
5683                });
5684            }
5685        }
5686
5687        let kind = match keyword {
5688            "my" => StmtKind::My(decls),
5689            "mysync" => StmtKind::MySync(decls),
5690            "our" => StmtKind::Our(decls),
5691            "oursync" => StmtKind::OurSync(decls),
5692            "local" => StmtKind::Local(decls),
5693            "state" => StmtKind::State(decls),
5694            _ => unreachable!(),
5695        };
5696        let stmt = Statement {
5697            label: None,
5698            kind,
5699            line,
5700        };
5701        // `my $x = 1 if $y;` — statement modifier applies to the whole declaration (Perl).
5702        self.parse_stmt_postfix_modifier(stmt)
5703    }
5704
5705    fn parse_var_decl(&mut self, allow_type_annotation: bool) -> PerlResult<VarDecl> {
5706        let mut decl = match self.advance() {
5707            (Token::ScalarVar(name), _) => VarDecl {
5708                sigil: Sigil::Scalar,
5709                name,
5710                initializer: None,
5711                frozen: false,
5712                type_annotation: None,
5713            },
5714            (Token::ArrayVar(name), _) => VarDecl {
5715                sigil: Sigil::Array,
5716                name,
5717                initializer: None,
5718                frozen: false,
5719                type_annotation: None,
5720            },
5721            (Token::HashVar(name), line) => {
5722                if !crate::compat_mode() {
5723                    self.check_hash_shadows_reserved(&name, line)?;
5724                }
5725                VarDecl {
5726                    sigil: Sigil::Hash,
5727                    name,
5728                    initializer: None,
5729                    frozen: false,
5730                    type_annotation: None,
5731                }
5732            }
5733            (Token::Star, _line) => {
5734                let name = match self.advance() {
5735                    (Token::Ident(n), _) => n,
5736                    (tok, l) => {
5737                        return Err(self
5738                            .syntax_err(format!("Expected identifier after *, got {:?}", tok), l));
5739                    }
5740                };
5741                VarDecl {
5742                    sigil: Sigil::Typeglob,
5743                    name,
5744                    initializer: None,
5745                    frozen: false,
5746                    type_annotation: None,
5747                }
5748            }
5749            // `my ($a, undef, $c) = (1, 2, 3)` — Perl idiom for discarding a
5750            // slot in a list assignment. The interpreter treats `undef`-named
5751            // scalar decls as throwaway: declared into a unique sink so the
5752            // distribute-to-decls loop advances past the slot.
5753            (Token::Ident(ref kw), _) if kw == "undef" => VarDecl {
5754                sigil: Sigil::Scalar,
5755                // Synthesize a name that user code cannot reference. Each
5756                // sink slot in a list-assign gets its own unique name so the
5757                // declarations don't collide.
5758                name: format!("__undef_sink_{}", self.pos),
5759                initializer: None,
5760                frozen: false,
5761                type_annotation: None,
5762            },
5763            (tok, line) => {
5764                return Err(self.syntax_err(
5765                    format!("Expected variable in declaration, got {:?}", tok),
5766                    line,
5767                ));
5768            }
5769        };
5770        if allow_type_annotation && self.eat(&Token::Colon) {
5771            let ty = self.parse_type_name()?;
5772            if decl.sigil != Sigil::Scalar {
5773                return Err(self.syntax_err(
5774                    "`: Type` is only valid for scalar declarations (typed my $name : Int)",
5775                    self.peek_line(),
5776                ));
5777            }
5778            decl.type_annotation = Some(ty);
5779        }
5780        Ok(decl)
5781    }
5782
5783    fn parse_type_name(&mut self) -> PerlResult<PerlTypeName> {
5784        match self.advance() {
5785            (Token::Ident(name), _) => match name.as_str() {
5786                "Int" => Ok(PerlTypeName::Int),
5787                "Str" => Ok(PerlTypeName::Str),
5788                "Float" => Ok(PerlTypeName::Float),
5789                "Bool" => Ok(PerlTypeName::Bool),
5790                "Array" => Ok(PerlTypeName::Array),
5791                "Hash" => Ok(PerlTypeName::Hash),
5792                "Ref" => Ok(PerlTypeName::Ref),
5793                "Any" => Ok(PerlTypeName::Any),
5794                _ => Ok(PerlTypeName::Struct(name)),
5795            },
5796            (tok, err_line) => Err(self.syntax_err(
5797                format!("Expected type name after `:`, got {:?}", tok),
5798                err_line,
5799            )),
5800        }
5801    }
5802
5803    fn parse_package(&mut self) -> PerlResult<Statement> {
5804        let line = self.peek_line();
5805        self.advance(); // 'package'
5806        let name = match self.advance() {
5807            (Token::Ident(n), _) => n,
5808            (tok, line) => {
5809                return Err(self.syntax_err(format!("Expected package name, got {:?}", tok), line))
5810            }
5811        };
5812        // Handle Foo::Bar
5813        let mut full_name = name;
5814        while self.eat(&Token::PackageSep) {
5815            if let (Token::Ident(part), _) = self.advance() {
5816                full_name = format!("{}::{}", full_name, part);
5817            }
5818        }
5819        self.eat(&Token::Semicolon);
5820        Ok(Statement {
5821            label: None,
5822            kind: StmtKind::Package { name: full_name },
5823            line,
5824        })
5825    }
5826
5827    fn parse_use(&mut self) -> PerlResult<Statement> {
5828        let line = self.peek_line();
5829        self.advance(); // 'use'
5830        let (tok, tok_line) = self.advance();
5831        match tok {
5832            Token::Float(v) => {
5833                self.eat(&Token::Semicolon);
5834                Ok(Statement {
5835                    label: None,
5836                    kind: StmtKind::UsePerlVersion { version: v },
5837                    line,
5838                })
5839            }
5840            Token::Integer(n) => {
5841                if matches!(self.peek(), Token::Semicolon | Token::Eof) {
5842                    self.eat(&Token::Semicolon);
5843                    Ok(Statement {
5844                        label: None,
5845                        kind: StmtKind::UsePerlVersion { version: n as f64 },
5846                        line,
5847                    })
5848                } else {
5849                    Err(self.syntax_err(
5850                        format!("Expected ';' after use VERSION (got {:?})", self.peek()),
5851                        line,
5852                    ))
5853                }
5854            }
5855            Token::Ident(n) => {
5856                let mut full_name = n;
5857                while self.eat(&Token::PackageSep) {
5858                    if let (Token::Ident(part), _) = self.advance() {
5859                        full_name = format!("{}::{}", full_name, part);
5860                    }
5861                }
5862                if full_name == "overload" {
5863                    let mut pairs = Vec::new();
5864                    let mut parse_overload_pairs = |this: &mut Self| -> PerlResult<()> {
5865                        loop {
5866                            if matches!(this.peek(), Token::RParen | Token::Semicolon | Token::Eof)
5867                            {
5868                                break;
5869                            }
5870                            let key_e = this.parse_assign_expr()?;
5871                            this.expect(&Token::FatArrow)?;
5872                            let val_e = this.parse_assign_expr()?;
5873                            let key = this.expr_to_overload_key(&key_e)?;
5874                            let val = this.expr_to_overload_sub(&val_e)?;
5875                            pairs.push((key, val));
5876                            if !this.eat(&Token::Comma) {
5877                                break;
5878                            }
5879                        }
5880                        Ok(())
5881                    };
5882                    if self.eat(&Token::LParen) {
5883                        // `use overload ();` — common in JSON::PP and other modules.
5884                        parse_overload_pairs(self)?;
5885                        self.expect(&Token::RParen)?;
5886                    } else if !matches!(self.peek(), Token::Semicolon | Token::Eof) {
5887                        parse_overload_pairs(self)?;
5888                    }
5889                    self.eat(&Token::Semicolon);
5890                    return Ok(Statement {
5891                        label: None,
5892                        kind: StmtKind::UseOverload { pairs },
5893                        line,
5894                    });
5895                }
5896                let mut imports = Vec::new();
5897                if !matches!(self.peek(), Token::Semicolon | Token::Eof)
5898                    && !self.next_is_new_statement_start(tok_line)
5899                {
5900                    loop {
5901                        if matches!(self.peek(), Token::Semicolon | Token::Eof) {
5902                            break;
5903                        }
5904                        imports.push(self.parse_expression()?);
5905                        if !self.eat(&Token::Comma) {
5906                            break;
5907                        }
5908                    }
5909                }
5910                self.eat(&Token::Semicolon);
5911                Ok(Statement {
5912                    label: None,
5913                    kind: StmtKind::Use {
5914                        module: full_name,
5915                        imports,
5916                    },
5917                    line,
5918                })
5919            }
5920            other => Err(self.syntax_err(
5921                format!("Expected module name or version after use, got {:?}", other),
5922                tok_line,
5923            )),
5924        }
5925    }
5926
5927    fn parse_no(&mut self) -> PerlResult<Statement> {
5928        let line = self.peek_line();
5929        self.advance(); // 'no'
5930        let module = match self.advance() {
5931            (Token::Ident(n), tok_line) => (n, tok_line),
5932            (tok, line) => {
5933                return Err(self.syntax_err(
5934                    format!("Expected module name after no, got {:?}", tok),
5935                    line,
5936                ))
5937            }
5938        };
5939        let (module_name, tok_line) = module;
5940        let mut full_name = module_name;
5941        while self.eat(&Token::PackageSep) {
5942            if let (Token::Ident(part), _) = self.advance() {
5943                full_name = format!("{}::{}", full_name, part);
5944            }
5945        }
5946        let mut imports = Vec::new();
5947        if !matches!(self.peek(), Token::Semicolon | Token::Eof)
5948            && !self.next_is_new_statement_start(tok_line)
5949        {
5950            loop {
5951                if matches!(self.peek(), Token::Semicolon | Token::Eof) {
5952                    break;
5953                }
5954                imports.push(self.parse_expression()?);
5955                if !self.eat(&Token::Comma) {
5956                    break;
5957                }
5958            }
5959        }
5960        self.eat(&Token::Semicolon);
5961        Ok(Statement {
5962            label: None,
5963            kind: StmtKind::No {
5964                module: full_name,
5965                imports,
5966            },
5967            line,
5968        })
5969    }
5970
5971    fn parse_return(&mut self) -> PerlResult<Statement> {
5972        let line = self.peek_line();
5973        self.advance(); // 'return'
5974                        // No-value return: terminator tokens AND any postfix statement-modifier
5975                        // keyword (`if`/`unless`/`while`/`until`/`for`/`foreach`). Without this
5976                        // the postfix-modifier check below never fires for valueless returns —
5977                        // `parse_assign_expr` would see `if` and look it up as a sub call,
5978                        // producing the misleading "Undefined subroutine &if" error.
5979        let val = if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof)
5980            || self.peek_is_postfix_stmt_modifier_keyword()
5981        {
5982            None
5983        } else {
5984            // Parse the operand as a comma-list — Perl's `return` is a
5985            // list-operator, so `return 1, 2, 3` returns the list (1, 2, 3).
5986            // (BUG-010) Stay below pipe-forward and stop at postfix
5987            // statement-modifier keywords like `if` / `unless`.
5988            let first = self.parse_assign_expr()?;
5989            if matches!(self.peek(), Token::Comma | Token::FatArrow) {
5990                let mut items = vec![first];
5991                while self.eat(&Token::Comma) || self.eat(&Token::FatArrow) {
5992                    if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof)
5993                        || self.peek_is_postfix_stmt_modifier_keyword()
5994                    {
5995                        break;
5996                    }
5997                    items.push(self.parse_assign_expr()?);
5998                }
5999                let line = items.first().map(|e| e.line).unwrap_or(line);
6000                Some(Expr {
6001                    kind: ExprKind::List(items),
6002                    line,
6003                })
6004            } else {
6005                Some(first)
6006            }
6007        };
6008        // Check for postfix modifiers on return
6009        let stmt = Statement {
6010            label: None,
6011            kind: StmtKind::Return(val),
6012            line,
6013        };
6014        if let Token::Ident(ref kw) = self.peek().clone() {
6015            match kw.as_str() {
6016                "if" => {
6017                    self.advance();
6018                    let cond = self.parse_expression()?;
6019                    self.eat(&Token::Semicolon);
6020                    return Ok(Statement {
6021                        label: None,
6022                        kind: StmtKind::If {
6023                            condition: cond,
6024                            body: vec![stmt],
6025                            elsifs: vec![],
6026                            else_block: None,
6027                        },
6028                        line,
6029                    });
6030                }
6031                "unless" => {
6032                    self.advance();
6033                    let cond = self.parse_expression()?;
6034                    self.eat(&Token::Semicolon);
6035                    return Ok(Statement {
6036                        label: None,
6037                        kind: StmtKind::Unless {
6038                            condition: cond,
6039                            body: vec![stmt],
6040                            else_block: None,
6041                        },
6042                        line,
6043                    });
6044                }
6045                _ => {}
6046            }
6047        }
6048        self.eat(&Token::Semicolon);
6049        Ok(stmt)
6050    }
6051
6052    // ── Expressions (Pratt / precedence climbing) ──
6053
6054    fn parse_expression(&mut self) -> PerlResult<Expr> {
6055        self.parse_comma_expr()
6056    }
6057
6058    fn parse_comma_expr(&mut self) -> PerlResult<Expr> {
6059        // Word-op precedence (or/and/not) sits ABOVE assignment in Perl —
6060        // `EXPR or $err = $@` parses as `EXPR or ($err = $@)`, NOT
6061        // `(EXPR or $err) = $@`. Entering through `parse_or_word` here
6062        // (instead of `parse_assign_expr` directly) gives `or`/`and`/`not`
6063        // looser binding than `=`, matching `perlop`. The deeper chain
6064        // (`parse_not_word → parse_assign_expr → parse_ternary → … →
6065        // parse_log_or → …`) handles tighter operators normally.
6066        let expr = self.parse_or_word()?;
6067        let mut exprs = vec![expr];
6068        while self.eat(&Token::Comma) || self.eat(&Token::FatArrow) {
6069            if matches!(
6070                self.peek(),
6071                Token::RParen | Token::RBracket | Token::RBrace | Token::Semicolon | Token::Eof
6072            ) {
6073                break; // trailing comma
6074            }
6075            exprs.push(self.parse_or_word()?);
6076        }
6077        if exprs.len() == 1 {
6078            return Ok(exprs.pop().unwrap());
6079        }
6080        let line = exprs[0].line;
6081        Ok(Expr {
6082            kind: ExprKind::List(exprs),
6083            line,
6084        })
6085    }
6086
6087    fn parse_assign_expr(&mut self) -> PerlResult<Expr> {
6088        let expr = self.parse_ternary()?;
6089        let line = expr.line;
6090
6091        match self.peek().clone() {
6092            Token::Assign => {
6093                self.advance();
6094                let right = self.parse_assign_expr()?;
6095                // Desugar `$obj->field = value` into `$obj->field(value)` (setter call)
6096                if let ExprKind::MethodCall { ref args, .. } = expr.kind {
6097                    if args.is_empty() {
6098                        // Destructure again to take ownership
6099                        let ExprKind::MethodCall {
6100                            object,
6101                            method,
6102                            super_call,
6103                            ..
6104                        } = expr.kind
6105                        else {
6106                            unreachable!()
6107                        };
6108                        return Ok(Expr {
6109                            kind: ExprKind::MethodCall {
6110                                object,
6111                                method,
6112                                args: vec![right],
6113                                super_call,
6114                            },
6115                            line,
6116                        });
6117                    }
6118                }
6119                self.validate_assignment(&expr, &right, line)?;
6120                Ok(Expr {
6121                    kind: ExprKind::Assign {
6122                        target: Box::new(expr),
6123                        value: Box::new(right),
6124                    },
6125                    line,
6126                })
6127            }
6128            Token::PlusAssign => {
6129                self.advance();
6130                let r = self.parse_assign_expr()?;
6131                Ok(Expr {
6132                    kind: ExprKind::CompoundAssign {
6133                        target: Box::new(expr),
6134                        op: BinOp::Add,
6135                        value: Box::new(r),
6136                    },
6137                    line,
6138                })
6139            }
6140            Token::MinusAssign => {
6141                self.advance();
6142                let r = self.parse_assign_expr()?;
6143                Ok(Expr {
6144                    kind: ExprKind::CompoundAssign {
6145                        target: Box::new(expr),
6146                        op: BinOp::Sub,
6147                        value: Box::new(r),
6148                    },
6149                    line,
6150                })
6151            }
6152            Token::MulAssign => {
6153                self.advance();
6154                let r = self.parse_assign_expr()?;
6155                Ok(Expr {
6156                    kind: ExprKind::CompoundAssign {
6157                        target: Box::new(expr),
6158                        op: BinOp::Mul,
6159                        value: Box::new(r),
6160                    },
6161                    line,
6162                })
6163            }
6164            Token::DivAssign => {
6165                self.advance();
6166                let r = self.parse_assign_expr()?;
6167                Ok(Expr {
6168                    kind: ExprKind::CompoundAssign {
6169                        target: Box::new(expr),
6170                        op: BinOp::Div,
6171                        value: Box::new(r),
6172                    },
6173                    line,
6174                })
6175            }
6176            Token::ModAssign => {
6177                self.advance();
6178                let r = self.parse_assign_expr()?;
6179                Ok(Expr {
6180                    kind: ExprKind::CompoundAssign {
6181                        target: Box::new(expr),
6182                        op: BinOp::Mod,
6183                        value: Box::new(r),
6184                    },
6185                    line,
6186                })
6187            }
6188            Token::PowAssign => {
6189                self.advance();
6190                let r = self.parse_assign_expr()?;
6191                Ok(Expr {
6192                    kind: ExprKind::CompoundAssign {
6193                        target: Box::new(expr),
6194                        op: BinOp::Pow,
6195                        value: Box::new(r),
6196                    },
6197                    line,
6198                })
6199            }
6200            Token::DotAssign => {
6201                self.advance();
6202                let r = self.parse_assign_expr()?;
6203                Ok(Expr {
6204                    kind: ExprKind::CompoundAssign {
6205                        target: Box::new(expr),
6206                        op: BinOp::Concat,
6207                        value: Box::new(r),
6208                    },
6209                    line,
6210                })
6211            }
6212            Token::BitAndAssign => {
6213                self.advance();
6214                let r = self.parse_assign_expr()?;
6215                Ok(Expr {
6216                    kind: ExprKind::CompoundAssign {
6217                        target: Box::new(expr),
6218                        op: BinOp::BitAnd,
6219                        value: Box::new(r),
6220                    },
6221                    line,
6222                })
6223            }
6224            Token::BitOrAssign => {
6225                self.advance();
6226                let r = self.parse_assign_expr()?;
6227                Ok(Expr {
6228                    kind: ExprKind::CompoundAssign {
6229                        target: Box::new(expr),
6230                        op: BinOp::BitOr,
6231                        value: Box::new(r),
6232                    },
6233                    line,
6234                })
6235            }
6236            Token::XorAssign => {
6237                self.advance();
6238                let r = self.parse_assign_expr()?;
6239                Ok(Expr {
6240                    kind: ExprKind::CompoundAssign {
6241                        target: Box::new(expr),
6242                        op: BinOp::BitXor,
6243                        value: Box::new(r),
6244                    },
6245                    line,
6246                })
6247            }
6248            Token::ShiftLeftAssign => {
6249                self.advance();
6250                let r = self.parse_assign_expr()?;
6251                Ok(Expr {
6252                    kind: ExprKind::CompoundAssign {
6253                        target: Box::new(expr),
6254                        op: BinOp::ShiftLeft,
6255                        value: Box::new(r),
6256                    },
6257                    line,
6258                })
6259            }
6260            Token::ShiftRightAssign => {
6261                self.advance();
6262                let r = self.parse_assign_expr()?;
6263                Ok(Expr {
6264                    kind: ExprKind::CompoundAssign {
6265                        target: Box::new(expr),
6266                        op: BinOp::ShiftRight,
6267                        value: Box::new(r),
6268                    },
6269                    line,
6270                })
6271            }
6272            Token::OrAssign => {
6273                self.advance();
6274                let r = self.parse_assign_expr()?;
6275                Ok(Expr {
6276                    kind: ExprKind::CompoundAssign {
6277                        target: Box::new(expr),
6278                        op: BinOp::LogOr,
6279                        value: Box::new(r),
6280                    },
6281                    line,
6282                })
6283            }
6284            Token::DefinedOrAssign => {
6285                self.advance();
6286                let r = self.parse_assign_expr()?;
6287                Ok(Expr {
6288                    kind: ExprKind::CompoundAssign {
6289                        target: Box::new(expr),
6290                        op: BinOp::DefinedOr,
6291                        value: Box::new(r),
6292                    },
6293                    line,
6294                })
6295            }
6296            Token::AndAssign => {
6297                self.advance();
6298                let r = self.parse_assign_expr()?;
6299                Ok(Expr {
6300                    kind: ExprKind::CompoundAssign {
6301                        target: Box::new(expr),
6302                        op: BinOp::LogAnd,
6303                        value: Box::new(r),
6304                    },
6305                    line,
6306                })
6307            }
6308            _ => Ok(expr),
6309        }
6310    }
6311
6312    fn parse_ternary(&mut self) -> PerlResult<Expr> {
6313        let expr = self.parse_pipe_forward()?;
6314        if self.eat(&Token::Question) {
6315            let line = expr.line;
6316            self.suppress_colon_range = self.suppress_colon_range.saturating_add(1);
6317            let then_expr = self.parse_assign_expr();
6318            self.suppress_colon_range = self.suppress_colon_range.saturating_sub(1);
6319            let then_expr = then_expr?;
6320            self.expect(&Token::Colon)?;
6321            let else_expr = self.parse_assign_expr()?;
6322            return Ok(Expr {
6323                kind: ExprKind::Ternary {
6324                    condition: Box::new(expr),
6325                    then_expr: Box::new(then_expr),
6326                    else_expr: Box::new(else_expr),
6327                },
6328                line,
6329            });
6330        }
6331        Ok(expr)
6332    }
6333
6334    /// `EXPR |> CALL` — pipe-forward (F#/Elixir). Left-associative; the LHS is threaded
6335    /// in as the **first argument** of the RHS call at parse time (pure AST rewrite,
6336    /// no runtime cost). `x |> f(a, b)` → `f(x, a, b)`; `x |> f` → `f(x)`; chain
6337    /// `x |> f |> g(2)` → `g(f(x), 2)`. Precedence sits between `?:` and `||`, so
6338    /// `x + 1 |> f || y` parses as `f(x + 1) || y`.
6339    fn parse_pipe_forward(&mut self) -> PerlResult<Expr> {
6340        // After moving word-ops (or/and/not) above the assignment level,
6341        // pipe_forward must descend into `parse_range` (which itself
6342        // descends into `parse_log_or`) — calling `parse_or_word` here
6343        // would re-introduce `or` at a wrong place in the precedence chain
6344        // (it now sits above `parse_comma_expr`). We skip past `parse_range`
6345        // rather than `parse_log_or` so `..` stays reachable.
6346        let mut left = self.parse_range()?;
6347        // Inside a paren-less arg list, `|>` is a hard terminator for the
6348        // enclosing call — leave it for the outer `parse_pipe_forward` loop
6349        // so `qw(…) |> head 2 |> join "-"` chains left-to-right as
6350        // `(qw(…) |> head 2) |> join "-"` instead of `head` swallowing the
6351        // outer `|>` via its first-arg `parse_assign_expr`.
6352        if self.no_pipe_forward_depth > 0 {
6353            return Ok(left);
6354        }
6355        while matches!(self.peek(), Token::PipeForward) {
6356            if crate::compat_mode() {
6357                return Err(self.syntax_err(
6358                    "pipe-forward operator `|>` is a stryke extension (disabled by --compat)",
6359                    left.line,
6360                ));
6361            }
6362            let line = left.line;
6363            self.advance();
6364            // Set pipe-RHS context so list-taking builtins (`map`, `grep`,
6365            // `join`, …) accept a placeholder in place of their list operand.
6366            self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_add(1);
6367            // RHS of `|>` parses at the same precedence as the LHS — see
6368            // the comment at the top of `parse_pipe_forward` for why this
6369            // descends into `parse_range` instead of `parse_or_word`.
6370            let right_result = self.parse_range();
6371            self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_sub(1);
6372            let right = right_result?;
6373            left = self.pipe_forward_apply(left, right, line)?;
6374        }
6375        Ok(left)
6376    }
6377
6378    /// Desugar `lhs |> rhs`: thread `lhs` into the call that `rhs` represents as
6379    /// its **first** argument (Elixir / R / proposed-JS convention).
6380    ///
6381    /// The strategy depends on the shape of `rhs`:
6382    /// - Generic calls (`FuncCall`, `MethodCall`, `IndirectCall`) and variadic
6383    ///   builtins (`Print`, `Say`, `Printf`, `Die`, `Warn`, `Sprintf`, `System`,
6384    ///   `Exec`, `Unlink`, `Chmod`, `Chown`, `Glob`, …) — **prepend** `lhs` to
6385    ///   the args list. So `URL |> json_jq ".[]"` → `json_jq(URL, ".[]")`,
6386    ///   matching the `(data, filter)` signature the builtin expects.
6387    /// - Unary-style builtins (`Length`, `Abs`, `Lc`, `Uc`, `Defined`, `Ref`,
6388    ///   `Keys`, `Values`, `Pop`, `Shift`, …) — **replace** the sole operand with
6389    ///   `lhs` (these parse a single default `$_` when called without an arg, so
6390    ///   piping overrides that default; first-arg and last-arg are identical).
6391    /// - List-taking higher-order forms (`map`, `flat_map`, `grep`, `sort`, `join`, `reduce`, `fold`,
6392    ///   `pmap`, `pflat_map`, `pgrep`, `pfor`, …) — **replace** the `list` field with `lhs`, so
6393    ///   `@arr |> map { $_ * 2 }` becomes `map { $_ * 2 } @arr`.
6394    /// - `Bareword("f")` — lift to `FuncCall { f, [lhs] }`.
6395    /// - Scalar / deref / coderef expressions — wrap in `IndirectCall` with `lhs`
6396    ///   as the sole argument.
6397    /// - Ambiguous forms (binary ops, ternaries, literals, lists) — parse error,
6398    ///   since silently calling a non-callable at runtime would be worse.
6399    fn pipe_forward_apply(&self, lhs: Expr, rhs: Expr, line: usize) -> PerlResult<Expr> {
6400        let Expr { kind, line: rline } = rhs;
6401        let new_kind = match kind {
6402            // ── Generic / user-defined calls ───────────────────────────────────
6403            ExprKind::FuncCall { name, mut args } => {
6404                // Stryke builtins are unprefixed; `CORE::` callers route back to the
6405                // bare-name pipe-forward dispatch below.
6406                let dispatch_name: &str = name.strip_prefix("CORE::").unwrap_or(name.as_str());
6407                match dispatch_name {
6408                    "puniq" | "uniq" | "distinct" | "flatten" | "set" | "list_count"
6409                    | "list_size" | "count" | "size" | "cnt" | "len" | "with_index" | "shuffle"
6410                    | "shuffled" | "frequencies" | "freq" | "interleave" | "ddump"
6411                    | "stringify" | "str" | "lines" | "words" | "chars" | "digits" | "letters"
6412                    | "letters_uc" | "letters_lc" | "punctuation" | "numbers" | "graphemes"
6413                    | "columns" | "sentences" | "paragraphs" | "sections" | "trim" | "avg"
6414                    | "to_json" | "to_csv" | "to_toml" | "to_yaml" | "to_xml" | "to_html"
6415                    | "from_json" | "from_csv" | "from_toml" | "from_yaml" | "from_xml"
6416                    | "to_markdown" | "to_table" | "xopen" | "clip" | "sparkline" | "bar_chart"
6417                    | "flame" | "stddev" | "squared" | "sq" | "square" | "cubed" | "cb"
6418                    | "cube" | "normalize" | "snake_case" | "camel_case" | "kebab_case" => {
6419                        if args.is_empty() {
6420                            args.push(lhs);
6421                        } else {
6422                            args[0] = lhs;
6423                        }
6424                    }
6425                    "chunked" | "windowed" => {
6426                        if args.is_empty() {
6427                            return Err(self.syntax_err(
6428                                "|>: chunked(N) / windowed(N) needs size — e.g. `@a |> windowed(2)`",
6429                                line,
6430                            ));
6431                        }
6432                        args.insert(0, lhs);
6433                    }
6434                    "reduce" | "fold" => {
6435                        args.push(lhs);
6436                    }
6437                    "grep_v" | "pluck" | "tee" | "nth" | "chunk" => {
6438                        // data |> grep_v "pattern" → grep_v("pattern", data...)
6439                        // data |> pluck "key" → pluck("key", data...)
6440                        // data |> tee "file" → tee("file", data...)
6441                        // data |> nth N → nth(N, data...)
6442                        // data |> chunk N → chunk(N, data...)
6443                        args.push(lhs);
6444                    }
6445                    "enumerate" | "dedup" => {
6446                        // data |> enumerate → enumerate(data)
6447                        // data |> dedup → dedup(data)
6448                        args.insert(0, lhs);
6449                    }
6450                    "clamp" => {
6451                        // data |> clamp MIN, MAX → clamp(MIN, MAX, data...)
6452                        args.push(lhs);
6453                    }
6454                    n if Self::is_block_then_list_pipe_builtin(n) => {
6455                        if args.len() < 2 {
6456                            return Err(self.syntax_err(
6457                                format!(
6458                                    "|>: `{name}` needs {{ BLOCK }}, LIST so the list can receive the pipe"
6459                                ),
6460                                line,
6461                            ));
6462                        }
6463                        args[1] = lhs;
6464                    }
6465                    "take" | "head" | "tail" | "drop" => {
6466                        if args.is_empty() {
6467                            return Err(self.syntax_err(
6468                                "|>: `{name}` needs N last — e.g. `@a |> take(3)` for `take(@a, 3)`",
6469                                line,
6470                            ));
6471                        }
6472                        // `LIST |> take N` → `take(LIST, N)` (prepend piped list before trailing count)
6473                        args.insert(0, lhs);
6474                    }
6475                    _ => {
6476                        if self.thread_last_mode {
6477                            args.push(lhs);
6478                        } else {
6479                            args.insert(0, lhs);
6480                        }
6481                    }
6482                }
6483                ExprKind::FuncCall { name, args }
6484            }
6485            ExprKind::MethodCall {
6486                object,
6487                method,
6488                mut args,
6489                super_call,
6490            } => {
6491                if self.thread_last_mode {
6492                    args.push(lhs);
6493                } else {
6494                    args.insert(0, lhs);
6495                }
6496                ExprKind::MethodCall {
6497                    object,
6498                    method,
6499                    args,
6500                    super_call,
6501                }
6502            }
6503            ExprKind::IndirectCall {
6504                target,
6505                mut args,
6506                ampersand,
6507                pass_caller_arglist: _,
6508            } => {
6509                if self.thread_last_mode {
6510                    args.push(lhs);
6511                } else {
6512                    args.insert(0, lhs);
6513                }
6514                ExprKind::IndirectCall {
6515                    target,
6516                    args,
6517                    ampersand,
6518                    // Prepending an explicit first arg means this is no longer
6519                    // "pass the caller's @_" — that form is only bare `&$cr`.
6520                    pass_caller_arglist: false,
6521                }
6522            }
6523
6524            // ── Print-like / diagnostic ops (variadic) ─────────────────────────
6525            ExprKind::Print { handle, mut args } => {
6526                if self.thread_last_mode {
6527                    args.push(lhs);
6528                } else {
6529                    args.insert(0, lhs);
6530                }
6531                ExprKind::Print { handle, args }
6532            }
6533            ExprKind::Say { handle, mut args } => {
6534                if self.thread_last_mode {
6535                    args.push(lhs);
6536                } else {
6537                    args.insert(0, lhs);
6538                }
6539                ExprKind::Say { handle, args }
6540            }
6541            ExprKind::Printf { handle, mut args } => {
6542                if self.thread_last_mode {
6543                    args.push(lhs);
6544                } else {
6545                    args.insert(0, lhs);
6546                }
6547                ExprKind::Printf { handle, args }
6548            }
6549            ExprKind::Die(mut args) => {
6550                if self.thread_last_mode {
6551                    args.push(lhs);
6552                } else {
6553                    args.insert(0, lhs);
6554                }
6555                ExprKind::Die(args)
6556            }
6557            ExprKind::Warn(mut args) => {
6558                if self.thread_last_mode {
6559                    args.push(lhs);
6560                } else {
6561                    args.insert(0, lhs);
6562                }
6563                ExprKind::Warn(args)
6564            }
6565
6566            // ── Sprintf: first-arg pipe threads lhs into the `format` slot ─────
6567            //   `"n=%d" |> sprintf(42)` → `sprintf("n=%d", 42)` is awkward,
6568            //   but piping the format string is the rarer case. Prepending
6569            //   to the values list gives `sprintf(format, lhs, ...args)` for
6570            //   the common `$n |> sprintf "count=%d"` case.
6571            ExprKind::Sprintf { format, mut args } => {
6572                if self.thread_last_mode {
6573                    args.push(lhs);
6574                } else {
6575                    args.insert(0, lhs);
6576                }
6577                ExprKind::Sprintf { format, args }
6578            }
6579
6580            // ── System / exec / globbing / filesystem variadics ────────────────
6581            ExprKind::System(mut args) => {
6582                if self.thread_last_mode {
6583                    args.push(lhs);
6584                } else {
6585                    args.insert(0, lhs);
6586                }
6587                ExprKind::System(args)
6588            }
6589            ExprKind::Exec(mut args) => {
6590                if self.thread_last_mode {
6591                    args.push(lhs);
6592                } else {
6593                    args.insert(0, lhs);
6594                }
6595                ExprKind::Exec(args)
6596            }
6597            ExprKind::Unlink(mut args) => {
6598                if self.thread_last_mode {
6599                    args.push(lhs);
6600                } else {
6601                    args.insert(0, lhs);
6602                }
6603                ExprKind::Unlink(args)
6604            }
6605            ExprKind::Chmod(mut args) => {
6606                if self.thread_last_mode {
6607                    args.push(lhs);
6608                } else {
6609                    args.insert(0, lhs);
6610                }
6611                ExprKind::Chmod(args)
6612            }
6613            ExprKind::Chown(mut args) => {
6614                if self.thread_last_mode {
6615                    args.push(lhs);
6616                } else {
6617                    args.insert(0, lhs);
6618                }
6619                ExprKind::Chown(args)
6620            }
6621            ExprKind::Glob(mut args) => {
6622                if self.thread_last_mode {
6623                    args.push(lhs);
6624                } else {
6625                    args.insert(0, lhs);
6626                }
6627                ExprKind::Glob(args)
6628            }
6629            ExprKind::Files(mut args) => {
6630                if self.thread_last_mode {
6631                    args.push(lhs);
6632                } else {
6633                    args.insert(0, lhs);
6634                }
6635                ExprKind::Files(args)
6636            }
6637            ExprKind::Filesf(mut args) => {
6638                if self.thread_last_mode {
6639                    args.push(lhs);
6640                } else {
6641                    args.insert(0, lhs);
6642                }
6643                ExprKind::Filesf(args)
6644            }
6645            ExprKind::FilesfRecursive(mut args) => {
6646                if self.thread_last_mode {
6647                    args.push(lhs);
6648                } else {
6649                    args.insert(0, lhs);
6650                }
6651                ExprKind::FilesfRecursive(args)
6652            }
6653            ExprKind::Dirs(mut args) => {
6654                if self.thread_last_mode {
6655                    args.push(lhs);
6656                } else {
6657                    args.insert(0, lhs);
6658                }
6659                ExprKind::Dirs(args)
6660            }
6661            ExprKind::DirsRecursive(mut args) => {
6662                if self.thread_last_mode {
6663                    args.push(lhs);
6664                } else {
6665                    args.insert(0, lhs);
6666                }
6667                ExprKind::DirsRecursive(args)
6668            }
6669            ExprKind::SymLinks(mut args) => {
6670                if self.thread_last_mode {
6671                    args.push(lhs);
6672                } else {
6673                    args.insert(0, lhs);
6674                }
6675                ExprKind::SymLinks(args)
6676            }
6677            ExprKind::Sockets(mut args) => {
6678                if self.thread_last_mode {
6679                    args.push(lhs);
6680                } else {
6681                    args.insert(0, lhs);
6682                }
6683                ExprKind::Sockets(args)
6684            }
6685            ExprKind::Pipes(mut args) => {
6686                if self.thread_last_mode {
6687                    args.push(lhs);
6688                } else {
6689                    args.insert(0, lhs);
6690                }
6691                ExprKind::Pipes(args)
6692            }
6693            ExprKind::BlockDevices(mut args) => {
6694                if self.thread_last_mode {
6695                    args.push(lhs);
6696                } else {
6697                    args.insert(0, lhs);
6698                }
6699                ExprKind::BlockDevices(args)
6700            }
6701            ExprKind::CharDevices(mut args) => {
6702                if self.thread_last_mode {
6703                    args.push(lhs);
6704                } else {
6705                    args.insert(0, lhs);
6706                }
6707                ExprKind::CharDevices(args)
6708            }
6709            ExprKind::GlobPar { mut args, progress } => {
6710                if self.thread_last_mode {
6711                    args.push(lhs);
6712                } else {
6713                    args.insert(0, lhs);
6714                }
6715                ExprKind::GlobPar { args, progress }
6716            }
6717            ExprKind::ParSed { mut args, progress } => {
6718                if self.thread_last_mode {
6719                    args.push(lhs);
6720                } else {
6721                    args.insert(0, lhs);
6722                }
6723                ExprKind::ParSed { args, progress }
6724            }
6725
6726            // ── Unary-style builtins: replace the lone operand with `lhs` ──────
6727            ExprKind::Length(_) => ExprKind::Length(Box::new(lhs)),
6728            ExprKind::Abs(_) => ExprKind::Abs(Box::new(lhs)),
6729            ExprKind::Int(_) => ExprKind::Int(Box::new(lhs)),
6730            ExprKind::Sqrt(_) => ExprKind::Sqrt(Box::new(lhs)),
6731            ExprKind::Sin(_) => ExprKind::Sin(Box::new(lhs)),
6732            ExprKind::Cos(_) => ExprKind::Cos(Box::new(lhs)),
6733            ExprKind::Exp(_) => ExprKind::Exp(Box::new(lhs)),
6734            ExprKind::Log(_) => ExprKind::Log(Box::new(lhs)),
6735            ExprKind::Hex(_) => ExprKind::Hex(Box::new(lhs)),
6736            ExprKind::Oct(_) => ExprKind::Oct(Box::new(lhs)),
6737            ExprKind::Lc(_) => ExprKind::Lc(Box::new(lhs)),
6738            ExprKind::Uc(_) => ExprKind::Uc(Box::new(lhs)),
6739            ExprKind::Lcfirst(_) => ExprKind::Lcfirst(Box::new(lhs)),
6740            ExprKind::Ucfirst(_) => ExprKind::Ucfirst(Box::new(lhs)),
6741            ExprKind::Fc(_) => ExprKind::Fc(Box::new(lhs)),
6742            ExprKind::Chr(_) => ExprKind::Chr(Box::new(lhs)),
6743            ExprKind::Ord(_) => ExprKind::Ord(Box::new(lhs)),
6744            ExprKind::Chomp(_) => ExprKind::Chomp(Box::new(lhs)),
6745            ExprKind::Chop(_) => ExprKind::Chop(Box::new(lhs)),
6746            ExprKind::Defined(_) => ExprKind::Defined(Box::new(lhs)),
6747            ExprKind::Ref(_) => ExprKind::Ref(Box::new(lhs)),
6748            ExprKind::ScalarContext(_) => ExprKind::ScalarContext(Box::new(lhs)),
6749            ExprKind::Keys(_) => ExprKind::Keys(Box::new(lhs)),
6750            ExprKind::Values(_) => ExprKind::Values(Box::new(lhs)),
6751            ExprKind::Each(_) => ExprKind::Each(Box::new(lhs)),
6752            ExprKind::Pop(_) => ExprKind::Pop(Box::new(lhs)),
6753            ExprKind::Shift(_) => ExprKind::Shift(Box::new(lhs)),
6754            ExprKind::Delete(_) => ExprKind::Delete(Box::new(lhs)),
6755            ExprKind::Exists(_) => ExprKind::Exists(Box::new(lhs)),
6756            ExprKind::ReverseExpr(_) => ExprKind::ReverseExpr(Box::new(lhs)),
6757            ExprKind::Rev(_) => ExprKind::Rev(Box::new(lhs)),
6758            ExprKind::Slurp(_) => ExprKind::Slurp(Box::new(lhs)),
6759            ExprKind::Capture(_) => ExprKind::Capture(Box::new(lhs)),
6760            ExprKind::Qx(_) => ExprKind::Qx(Box::new(lhs)),
6761            ExprKind::FetchUrl(_) => ExprKind::FetchUrl(Box::new(lhs)),
6762            ExprKind::Close(_) => ExprKind::Close(Box::new(lhs)),
6763            ExprKind::Chdir(_) => ExprKind::Chdir(Box::new(lhs)),
6764            ExprKind::Readdir(_) => ExprKind::Readdir(Box::new(lhs)),
6765            ExprKind::Closedir(_) => ExprKind::Closedir(Box::new(lhs)),
6766            ExprKind::Rewinddir(_) => ExprKind::Rewinddir(Box::new(lhs)),
6767            ExprKind::Telldir(_) => ExprKind::Telldir(Box::new(lhs)),
6768            ExprKind::Stat(_) => ExprKind::Stat(Box::new(lhs)),
6769            ExprKind::Lstat(_) => ExprKind::Lstat(Box::new(lhs)),
6770            ExprKind::Readlink(_) => ExprKind::Readlink(Box::new(lhs)),
6771            ExprKind::Study(_) => ExprKind::Study(Box::new(lhs)),
6772            ExprKind::Await(_) => ExprKind::Await(Box::new(lhs)),
6773            ExprKind::Eval(_) => ExprKind::Eval(Box::new(lhs)),
6774            ExprKind::Rand(_) => ExprKind::Rand(Some(Box::new(lhs))),
6775            ExprKind::Srand(_) => ExprKind::Srand(Some(Box::new(lhs))),
6776            ExprKind::Pos(_) => ExprKind::Pos(Some(Box::new(lhs))),
6777            ExprKind::Exit(_) => ExprKind::Exit(Some(Box::new(lhs))),
6778
6779            // ── Higher-order / list-taking forms: replace the `list` slot ──────
6780            ExprKind::MapExpr {
6781                block,
6782                list: _,
6783                flatten_array_refs,
6784                stream,
6785            } => ExprKind::MapExpr {
6786                block,
6787                list: Box::new(lhs),
6788                flatten_array_refs,
6789                stream,
6790            },
6791            ExprKind::MapExprComma {
6792                expr,
6793                list: _,
6794                flatten_array_refs,
6795                stream,
6796            } => ExprKind::MapExprComma {
6797                expr,
6798                list: Box::new(lhs),
6799                flatten_array_refs,
6800                stream,
6801            },
6802            ExprKind::GrepExpr {
6803                block,
6804                list: _,
6805                keyword,
6806            } => ExprKind::GrepExpr {
6807                block,
6808                list: Box::new(lhs),
6809                keyword,
6810            },
6811            ExprKind::GrepExprComma {
6812                expr,
6813                list: _,
6814                keyword,
6815            } => ExprKind::GrepExprComma {
6816                expr,
6817                list: Box::new(lhs),
6818                keyword,
6819            },
6820            ExprKind::ForEachExpr { block, list: _ } => ExprKind::ForEachExpr {
6821                block,
6822                list: Box::new(lhs),
6823            },
6824            ExprKind::SortExpr { cmp, list: _ } => ExprKind::SortExpr {
6825                cmp,
6826                list: Box::new(lhs),
6827            },
6828            ExprKind::JoinExpr { separator, list: _ } => ExprKind::JoinExpr {
6829                separator,
6830                list: Box::new(lhs),
6831            },
6832            ExprKind::ReduceExpr { block, list: _ } => ExprKind::ReduceExpr {
6833                block,
6834                list: Box::new(lhs),
6835            },
6836            ExprKind::PMapExpr {
6837                block,
6838                list: _,
6839                progress,
6840                flat_outputs,
6841                on_cluster,
6842                stream,
6843            } => ExprKind::PMapExpr {
6844                block,
6845                list: Box::new(lhs),
6846                progress,
6847                flat_outputs,
6848                on_cluster,
6849                stream,
6850            },
6851            ExprKind::PMapChunkedExpr {
6852                chunk_size,
6853                block,
6854                list: _,
6855                progress,
6856            } => ExprKind::PMapChunkedExpr {
6857                chunk_size,
6858                block,
6859                list: Box::new(lhs),
6860                progress,
6861            },
6862            ExprKind::PGrepExpr {
6863                block,
6864                list: _,
6865                progress,
6866                stream,
6867            } => ExprKind::PGrepExpr {
6868                block,
6869                list: Box::new(lhs),
6870                progress,
6871                stream,
6872            },
6873            ExprKind::PForExpr {
6874                block,
6875                list: _,
6876                progress,
6877            } => ExprKind::PForExpr {
6878                block,
6879                list: Box::new(lhs),
6880                progress,
6881            },
6882            ExprKind::PSortExpr {
6883                cmp,
6884                list: _,
6885                progress,
6886            } => ExprKind::PSortExpr {
6887                cmp,
6888                list: Box::new(lhs),
6889                progress,
6890            },
6891            ExprKind::PReduceExpr {
6892                block,
6893                list: _,
6894                progress,
6895            } => ExprKind::PReduceExpr {
6896                block,
6897                list: Box::new(lhs),
6898                progress,
6899            },
6900            ExprKind::PcacheExpr {
6901                block,
6902                list: _,
6903                progress,
6904            } => ExprKind::PcacheExpr {
6905                block,
6906                list: Box::new(lhs),
6907                progress,
6908            },
6909            ExprKind::PReduceInitExpr {
6910                init,
6911                block,
6912                list: _,
6913                progress,
6914            } => ExprKind::PReduceInitExpr {
6915                init,
6916                block,
6917                list: Box::new(lhs),
6918                progress,
6919            },
6920            ExprKind::PMapReduceExpr {
6921                map_block,
6922                reduce_block,
6923                list: _,
6924                progress,
6925            } => ExprKind::PMapReduceExpr {
6926                map_block,
6927                reduce_block,
6928                list: Box::new(lhs),
6929                progress,
6930            },
6931
6932            // ── Push / unshift: first arg is the array, so pipe the LHS
6933            //     into the **values** list — `"x" |> push(@arr)` → `push @arr, "x"`
6934            //     is unchanged, but `@arr |> push "x"` is unnatural; use push
6935            //     directly for that.
6936            ExprKind::Push { array, mut values } => {
6937                values.insert(0, lhs);
6938                ExprKind::Push { array, values }
6939            }
6940            ExprKind::Unshift { array, mut values } => {
6941                values.insert(0, lhs);
6942                ExprKind::Unshift { array, values }
6943            }
6944
6945            // ── Split: pipe the subject string — `$line |> split /,/` ─────────
6946            ExprKind::SplitExpr {
6947                pattern,
6948                string: _,
6949                limit,
6950            } => ExprKind::SplitExpr {
6951                pattern,
6952                string: Box::new(lhs),
6953                limit,
6954            },
6955
6956            // ── Regex ops: pipe the subject — `$str |> s/\n//g` ────────────────
6957            //    Auto-inject `r` flag so the substitution returns the modified
6958            //    string instead of the match count (non-destructive / Perl /r).
6959            ExprKind::Substitution {
6960                pattern,
6961                replacement,
6962                mut flags,
6963                expr: _,
6964                delim,
6965            } => {
6966                if !flags.contains('r') {
6967                    flags.push('r');
6968                }
6969                ExprKind::Substitution {
6970                    expr: Box::new(lhs),
6971                    pattern,
6972                    replacement,
6973                    flags,
6974                    delim,
6975                }
6976            }
6977            ExprKind::Transliterate {
6978                from,
6979                to,
6980                mut flags,
6981                expr: _,
6982                delim,
6983            } => {
6984                if !flags.contains('r') {
6985                    flags.push('r');
6986                }
6987                ExprKind::Transliterate {
6988                    expr: Box::new(lhs),
6989                    from,
6990                    to,
6991                    flags,
6992                    delim,
6993                }
6994            }
6995            ExprKind::Match {
6996                pattern,
6997                flags,
6998                scalar_g,
6999                expr: _,
7000                delim,
7001            } => ExprKind::Match {
7002                expr: Box::new(lhs),
7003                pattern,
7004                flags,
7005                scalar_g,
7006                delim,
7007            },
7008            // Bare `/regex/` (no explicit `m`): promote to Match on piped LHS
7009            ExprKind::Regex(pattern, flags) => ExprKind::Match {
7010                expr: Box::new(lhs),
7011                pattern,
7012                flags,
7013                scalar_g: false,
7014                delim: '/',
7015            },
7016
7017            // ── Bareword function name → plain unary call ──────────────────────
7018            ExprKind::Bareword(name) => match name.as_str() {
7019                "reverse" => {
7020                    if crate::no_interop_mode() {
7021                        return Err(self.syntax_err(
7022                            "stryke uses `rev` instead of `reverse` (--no-interop)",
7023                            line,
7024                        ));
7025                    }
7026                    ExprKind::ReverseExpr(Box::new(lhs))
7027                }
7028                "rv" | "reversed" | "rev" => ExprKind::Rev(Box::new(lhs)),
7029                "uq" | "uniq" | "distinct" => ExprKind::FuncCall {
7030                    name: "uniq".to_string(),
7031                    args: vec![lhs],
7032                },
7033                "fl" | "flatten" => ExprKind::FuncCall {
7034                    name: "flatten".to_string(),
7035                    args: vec![lhs],
7036                },
7037                _ => ExprKind::FuncCall {
7038                    name,
7039                    args: vec![lhs],
7040                },
7041            },
7042
7043            // ── Callable scalars / coderefs / derefs → IndirectCall ────────────
7044            kind @ (ExprKind::ScalarVar(_)
7045            | ExprKind::ArrayElement { .. }
7046            | ExprKind::HashElement { .. }
7047            | ExprKind::Deref { .. }
7048            | ExprKind::ArrowDeref { .. }
7049            | ExprKind::CodeRef { .. }
7050            | ExprKind::SubroutineRef(_)
7051            | ExprKind::SubroutineCodeRef(_)
7052            | ExprKind::DynamicSubCodeRef(_)) => ExprKind::IndirectCall {
7053                target: Box::new(Expr { kind, line: rline }),
7054                args: vec![lhs],
7055                ampersand: false,
7056                pass_caller_arglist: false,
7057            },
7058
7059            // `LHS |> >{ BLOCK }` — the `>{}` form is parsed everywhere as `Do(CodeRef)` (IIFE).
7060            // On the RHS of `|>` we want pipe-apply semantics instead: unwrap the Do and invoke
7061            // the inner coderef with `lhs` as `$_[0]`, matching `LHS |> fn { ... }`.
7062            ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. }) => {
7063                ExprKind::IndirectCall {
7064                    target: inner,
7065                    args: vec![lhs],
7066                    ampersand: false,
7067                    pass_caller_arglist: false,
7068                }
7069            }
7070
7071            other => {
7072                return Err(self.syntax_err(
7073                    format!(
7074                        "right-hand side of `|>` must be a call, builtin, or coderef \
7075                         expression (got {})",
7076                        Self::expr_kind_name(&other)
7077                    ),
7078                    line,
7079                ));
7080            }
7081        };
7082        Ok(Expr {
7083            kind: new_kind,
7084            line,
7085        })
7086    }
7087
7088    /// Short label for an `ExprKind` (used in `|>` error messages).
7089    fn expr_kind_name(kind: &ExprKind) -> &'static str {
7090        match kind {
7091            ExprKind::Integer(_) | ExprKind::Float(_) => "numeric literal",
7092            ExprKind::String(_) | ExprKind::InterpolatedString(_) => "string literal",
7093            ExprKind::BinOp { .. } => "binary expression",
7094            ExprKind::UnaryOp { .. } => "unary expression",
7095            ExprKind::Ternary { .. } => "ternary expression",
7096            ExprKind::Assign { .. } | ExprKind::CompoundAssign { .. } => "assignment",
7097            ExprKind::List(_) => "list expression",
7098            ExprKind::Range { .. } => "range expression",
7099            _ => "expression",
7100        }
7101    }
7102
7103    // or / not (lowest precedence word operators)
7104    fn parse_or_word(&mut self) -> PerlResult<Expr> {
7105        let mut left = self.parse_and_word()?;
7106        while matches!(self.peek(), Token::LogOrWord) {
7107            let line = left.line;
7108            self.advance();
7109            let right = self.parse_and_word()?;
7110            left = Expr {
7111                kind: ExprKind::BinOp {
7112                    left: Box::new(left),
7113                    op: BinOp::LogOrWord,
7114                    right: Box::new(right),
7115                },
7116                line,
7117            };
7118        }
7119        Ok(left)
7120    }
7121
7122    fn parse_and_word(&mut self) -> PerlResult<Expr> {
7123        let mut left = self.parse_not_word()?;
7124        while matches!(self.peek(), Token::LogAndWord) {
7125            let line = left.line;
7126            self.advance();
7127            let right = self.parse_not_word()?;
7128            left = Expr {
7129                kind: ExprKind::BinOp {
7130                    left: Box::new(left),
7131                    op: BinOp::LogAndWord,
7132                    right: Box::new(right),
7133                },
7134                line,
7135            };
7136        }
7137        Ok(left)
7138    }
7139
7140    fn parse_not_word(&mut self) -> PerlResult<Expr> {
7141        if matches!(self.peek(), Token::LogNotWord) {
7142            let line = self.peek_line();
7143            self.advance();
7144            let expr = self.parse_not_word()?;
7145            return Ok(Expr {
7146                kind: ExprKind::UnaryOp {
7147                    op: UnaryOp::LogNotWord,
7148                    expr: Box::new(expr),
7149                },
7150                line,
7151            });
7152        }
7153        // Descend into assignment level — `not` sits ABOVE `=` in Perl
7154        // precedence, so `not $x = 5` parses as `not ($x = 5)`.
7155        self.parse_assign_expr()
7156    }
7157
7158    fn parse_log_or(&mut self) -> PerlResult<Expr> {
7159        let mut left = self.parse_log_and()?;
7160        loop {
7161            let op = match self.peek() {
7162                Token::LogOr => BinOp::LogOr,
7163                Token::DefinedOr => BinOp::DefinedOr,
7164                _ => break,
7165            };
7166            let line = left.line;
7167            self.advance();
7168            let right = self.parse_log_and()?;
7169            left = Expr {
7170                kind: ExprKind::BinOp {
7171                    left: Box::new(left),
7172                    op,
7173                    right: Box::new(right),
7174                },
7175                line,
7176            };
7177        }
7178        Ok(left)
7179    }
7180
7181    fn parse_log_and(&mut self) -> PerlResult<Expr> {
7182        let mut left = self.parse_bit_or()?;
7183        while matches!(self.peek(), Token::LogAnd) {
7184            let line = left.line;
7185            self.advance();
7186            let right = self.parse_bit_or()?;
7187            left = Expr {
7188                kind: ExprKind::BinOp {
7189                    left: Box::new(left),
7190                    op: BinOp::LogAnd,
7191                    right: Box::new(right),
7192                },
7193                line,
7194            };
7195        }
7196        Ok(left)
7197    }
7198
7199    fn parse_bit_or(&mut self) -> PerlResult<Expr> {
7200        let mut left = self.parse_bit_xor()?;
7201        while matches!(self.peek(), Token::BitOr) {
7202            let line = left.line;
7203            self.advance();
7204            let right = self.parse_bit_xor()?;
7205            left = Expr {
7206                kind: ExprKind::BinOp {
7207                    left: Box::new(left),
7208                    op: BinOp::BitOr,
7209                    right: Box::new(right),
7210                },
7211                line,
7212            };
7213        }
7214        Ok(left)
7215    }
7216
7217    fn parse_bit_xor(&mut self) -> PerlResult<Expr> {
7218        let mut left = self.parse_bit_and()?;
7219        while matches!(self.peek(), Token::BitXor) {
7220            let line = left.line;
7221            self.advance();
7222            let right = self.parse_bit_and()?;
7223            left = Expr {
7224                kind: ExprKind::BinOp {
7225                    left: Box::new(left),
7226                    op: BinOp::BitXor,
7227                    right: Box::new(right),
7228                },
7229                line,
7230            };
7231        }
7232        Ok(left)
7233    }
7234
7235    fn parse_bit_and(&mut self) -> PerlResult<Expr> {
7236        let mut left = self.parse_equality()?;
7237        while matches!(self.peek(), Token::BitAnd) {
7238            let line = left.line;
7239            self.advance();
7240            let right = self.parse_equality()?;
7241            left = Expr {
7242                kind: ExprKind::BinOp {
7243                    left: Box::new(left),
7244                    op: BinOp::BitAnd,
7245                    right: Box::new(right),
7246                },
7247                line,
7248            };
7249        }
7250        Ok(left)
7251    }
7252
7253    fn parse_equality(&mut self) -> PerlResult<Expr> {
7254        let mut left = self.parse_comparison()?;
7255        loop {
7256            let op = match self.peek() {
7257                Token::NumEq => BinOp::NumEq,
7258                Token::NumNe => BinOp::NumNe,
7259                Token::StrEq => BinOp::StrEq,
7260                Token::StrNe => BinOp::StrNe,
7261                Token::Spaceship => BinOp::Spaceship,
7262                Token::StrCmp => BinOp::StrCmp,
7263                _ => break,
7264            };
7265            let line = left.line;
7266            self.advance();
7267            let right = self.parse_comparison()?;
7268            left = Expr {
7269                kind: ExprKind::BinOp {
7270                    left: Box::new(left),
7271                    op,
7272                    right: Box::new(right),
7273                },
7274                line,
7275            };
7276        }
7277        Ok(left)
7278    }
7279
7280    fn parse_comparison(&mut self) -> PerlResult<Expr> {
7281        let left = self.parse_shift()?;
7282        let first_op = match self.peek() {
7283            Token::NumLt => BinOp::NumLt,
7284            Token::NumGt => BinOp::NumGt,
7285            Token::NumLe => BinOp::NumLe,
7286            Token::NumGe => BinOp::NumGe,
7287            Token::StrLt => BinOp::StrLt,
7288            Token::StrGt => BinOp::StrGt,
7289            Token::StrLe => BinOp::StrLe,
7290            Token::StrGe => BinOp::StrGe,
7291            _ => return Ok(left),
7292        };
7293        let line = left.line;
7294        self.advance();
7295        let middle = self.parse_shift()?;
7296
7297        let second_op = match self.peek() {
7298            Token::NumLt => Some(BinOp::NumLt),
7299            Token::NumGt => Some(BinOp::NumGt),
7300            Token::NumLe => Some(BinOp::NumLe),
7301            Token::NumGe => Some(BinOp::NumGe),
7302            Token::StrLt => Some(BinOp::StrLt),
7303            Token::StrGt => Some(BinOp::StrGt),
7304            Token::StrLe => Some(BinOp::StrLe),
7305            Token::StrGe => Some(BinOp::StrGe),
7306            _ => None,
7307        };
7308
7309        if second_op.is_none() {
7310            return Ok(Expr {
7311                kind: ExprKind::BinOp {
7312                    left: Box::new(left),
7313                    op: first_op,
7314                    right: Box::new(middle),
7315                },
7316                line,
7317            });
7318        }
7319
7320        // Chained comparison: `a < b < c` → `(a < b) && (b < c)`
7321        // Collect all operands and operators for chains like `1 < x < 10 < y`
7322        let mut operands = vec![left, middle];
7323        let mut ops = vec![first_op];
7324
7325        loop {
7326            let op = match self.peek() {
7327                Token::NumLt => BinOp::NumLt,
7328                Token::NumGt => BinOp::NumGt,
7329                Token::NumLe => BinOp::NumLe,
7330                Token::NumGe => BinOp::NumGe,
7331                Token::StrLt => BinOp::StrLt,
7332                Token::StrGt => BinOp::StrGt,
7333                Token::StrLe => BinOp::StrLe,
7334                Token::StrGe => BinOp::StrGe,
7335                _ => break,
7336            };
7337            self.advance();
7338            ops.push(op);
7339            operands.push(self.parse_shift()?);
7340        }
7341
7342        // Build `(a op0 b) && (b op1 c) && (c op2 d) && ...`
7343        let mut result = Expr {
7344            kind: ExprKind::BinOp {
7345                left: Box::new(operands[0].clone()),
7346                op: ops[0],
7347                right: Box::new(operands[1].clone()),
7348            },
7349            line,
7350        };
7351
7352        for i in 1..ops.len() {
7353            let cmp = Expr {
7354                kind: ExprKind::BinOp {
7355                    left: Box::new(operands[i].clone()),
7356                    op: ops[i],
7357                    right: Box::new(operands[i + 1].clone()),
7358                },
7359                line,
7360            };
7361            result = Expr {
7362                kind: ExprKind::BinOp {
7363                    left: Box::new(result),
7364                    op: BinOp::LogAnd,
7365                    right: Box::new(cmp),
7366                },
7367                line,
7368            };
7369        }
7370
7371        Ok(result)
7372    }
7373
7374    fn parse_shift(&mut self) -> PerlResult<Expr> {
7375        let mut left = self.parse_addition()?;
7376        loop {
7377            let op = match self.peek() {
7378                Token::ShiftLeft => BinOp::ShiftLeft,
7379                Token::ShiftRight => BinOp::ShiftRight,
7380                _ => break,
7381            };
7382            let line = left.line;
7383            self.advance();
7384            let right = self.parse_addition()?;
7385            left = Expr {
7386                kind: ExprKind::BinOp {
7387                    left: Box::new(left),
7388                    op,
7389                    right: Box::new(right),
7390                },
7391                line,
7392            };
7393        }
7394        Ok(left)
7395    }
7396
7397    fn parse_addition(&mut self) -> PerlResult<Expr> {
7398        let mut left = self.parse_multiplication()?;
7399        loop {
7400            // Implicit semicolon: `-` or `+` on a new line is a unary operator on
7401            // the next statement, not a binary operator continuing this expression.
7402            let op = match self.peek() {
7403                Token::Plus if self.peek_line() == self.prev_line() => BinOp::Add,
7404                Token::Minus if self.peek_line() == self.prev_line() => BinOp::Sub,
7405                Token::Dot => BinOp::Concat,
7406                _ => break,
7407            };
7408            let line = left.line;
7409            self.advance();
7410            let right = self.parse_multiplication()?;
7411            left = Expr {
7412                kind: ExprKind::BinOp {
7413                    left: Box::new(left),
7414                    op,
7415                    right: Box::new(right),
7416                },
7417                line,
7418            };
7419        }
7420        Ok(left)
7421    }
7422
7423    fn parse_multiplication(&mut self) -> PerlResult<Expr> {
7424        let mut left = self.parse_regex_bind()?;
7425        loop {
7426            let op = match self.peek() {
7427                Token::Star => BinOp::Mul,
7428                Token::Slash if self.suppress_slash_as_div == 0 => BinOp::Div,
7429                // Implicit semicolon: `%` on a new line is a hash dereference or hash
7430                // sigil for the next statement, not modulo operator on this expression.
7431                Token::Percent if self.peek_line() == self.prev_line() => BinOp::Mod,
7432                Token::X => {
7433                    let line = left.line;
7434                    // List-repeat fires when the LHS was just closed by a
7435                    // list-constructor paren (`(EXPR)`, `(LIST)`, `()`) or
7436                    // `qw(...)`. `parse_primary` records the post-close
7437                    // position; an exact match against `self.pos` here means
7438                    // no postfix consumed any tokens between the close and
7439                    // the `x`, so the LHS is intrinsically a list construct.
7440                    let list_repeat = self.list_construct_close_pos == Some(self.pos);
7441                    self.advance();
7442                    let right = self.parse_regex_bind()?;
7443                    left = Expr {
7444                        kind: ExprKind::Repeat {
7445                            expr: Box::new(left),
7446                            count: Box::new(right),
7447                            list_repeat,
7448                        },
7449                        line,
7450                    };
7451                    continue;
7452                }
7453                _ => break,
7454            };
7455            let line = left.line;
7456            self.advance();
7457            let right = self.parse_regex_bind()?;
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_regex_bind(&mut self) -> PerlResult<Expr> {
7471        let left = self.parse_unary()?;
7472        match self.peek() {
7473            Token::BindMatch => {
7474                let line = left.line;
7475                self.advance();
7476                match self.peek().clone() {
7477                    Token::Regex(pattern, flags, delim) => {
7478                        self.advance();
7479                        Ok(Expr {
7480                            kind: ExprKind::Match {
7481                                expr: Box::new(left),
7482                                pattern,
7483                                flags,
7484                                scalar_g: false,
7485                                delim,
7486                            },
7487                            line,
7488                        })
7489                    }
7490                    Token::Ident(ref s) if s.starts_with('\x00') => {
7491                        let (Token::Ident(encoded), _) = self.advance() else {
7492                            unreachable!()
7493                        };
7494                        let parts: Vec<&str> = encoded.split('\x00').collect();
7495                        if parts.len() >= 4 && parts[1] == "s" {
7496                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
7497                            Ok(Expr {
7498                                kind: ExprKind::Substitution {
7499                                    expr: Box::new(left),
7500                                    pattern: parts[2].to_string(),
7501                                    replacement: parts[3].to_string(),
7502                                    flags: parts.get(4).unwrap_or(&"").to_string(),
7503                                    delim,
7504                                },
7505                                line,
7506                            })
7507                        } else if parts.len() >= 4 && parts[1] == "tr" {
7508                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
7509                            Ok(Expr {
7510                                kind: ExprKind::Transliterate {
7511                                    expr: Box::new(left),
7512                                    from: parts[2].to_string(),
7513                                    to: parts[3].to_string(),
7514                                    flags: parts.get(4).unwrap_or(&"").to_string(),
7515                                    delim,
7516                                },
7517                                line,
7518                            })
7519                        } else {
7520                            Err(self.syntax_err("Invalid regex binding", line))
7521                        }
7522                    }
7523                    _ => {
7524                        let rhs = self.parse_unary()?;
7525                        Ok(Expr {
7526                            kind: ExprKind::BinOp {
7527                                left: Box::new(left),
7528                                op: BinOp::BindMatch,
7529                                right: Box::new(rhs),
7530                            },
7531                            line,
7532                        })
7533                    }
7534                }
7535            }
7536            Token::BindNotMatch => {
7537                let line = left.line;
7538                self.advance();
7539                match self.peek().clone() {
7540                    Token::Regex(pattern, flags, delim) => {
7541                        self.advance();
7542                        Ok(Expr {
7543                            kind: ExprKind::UnaryOp {
7544                                op: UnaryOp::LogNot,
7545                                expr: Box::new(Expr {
7546                                    kind: ExprKind::Match {
7547                                        expr: Box::new(left),
7548                                        pattern,
7549                                        flags,
7550                                        scalar_g: false,
7551                                        delim,
7552                                    },
7553                                    line,
7554                                }),
7555                            },
7556                            line,
7557                        })
7558                    }
7559                    Token::Ident(ref s) if s.starts_with('\x00') => {
7560                        let (Token::Ident(encoded), _) = self.advance() else {
7561                            unreachable!()
7562                        };
7563                        let parts: Vec<&str> = encoded.split('\x00').collect();
7564                        if parts.len() >= 4 && parts[1] == "s" {
7565                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
7566                            Ok(Expr {
7567                                kind: ExprKind::UnaryOp {
7568                                    op: UnaryOp::LogNot,
7569                                    expr: Box::new(Expr {
7570                                        kind: ExprKind::Substitution {
7571                                            expr: Box::new(left),
7572                                            pattern: parts[2].to_string(),
7573                                            replacement: parts[3].to_string(),
7574                                            flags: parts.get(4).unwrap_or(&"").to_string(),
7575                                            delim,
7576                                        },
7577                                        line,
7578                                    }),
7579                                },
7580                                line,
7581                            })
7582                        } else if parts.len() >= 4 && parts[1] == "tr" {
7583                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
7584                            Ok(Expr {
7585                                kind: ExprKind::UnaryOp {
7586                                    op: UnaryOp::LogNot,
7587                                    expr: Box::new(Expr {
7588                                        kind: ExprKind::Transliterate {
7589                                            expr: Box::new(left),
7590                                            from: parts[2].to_string(),
7591                                            to: parts[3].to_string(),
7592                                            flags: parts.get(4).unwrap_or(&"").to_string(),
7593                                            delim,
7594                                        },
7595                                        line,
7596                                    }),
7597                                },
7598                                line,
7599                            })
7600                        } else {
7601                            Err(self.syntax_err("Invalid regex binding after !~", line))
7602                        }
7603                    }
7604                    _ => {
7605                        let rhs = self.parse_unary()?;
7606                        Ok(Expr {
7607                            kind: ExprKind::BinOp {
7608                                left: Box::new(left),
7609                                op: BinOp::BindNotMatch,
7610                                right: Box::new(rhs),
7611                            },
7612                            line,
7613                        })
7614                    }
7615                }
7616            }
7617            _ => Ok(left),
7618        }
7619    }
7620
7621    /// Parse thread macro input. Like `parse_range` but suppresses `/` as division
7622    /// so that `/pattern/` is left for the thread stage parser to handle as regex filter.
7623    fn parse_thread_input(&mut self) -> PerlResult<Expr> {
7624        self.suppress_slash_as_div = self.suppress_slash_as_div.saturating_add(1);
7625        let result = self.parse_range();
7626        self.suppress_slash_as_div = self.suppress_slash_as_div.saturating_sub(1);
7627        result
7628    }
7629
7630    /// Perl `..` / `...` operator — precedence sits between `?:` and `||` (`perlop`), so
7631    /// `$x .. $x + 3` parses as `$x .. ($x + 3)` and `1..$n||5` parses as `1..($n||5)`. Both
7632    /// operands recurse through `parse_log_or`, which in turn walks down through all tighter
7633    /// operators (additive, multiplicative, regex bind, unary). Non-associative: the right
7634    /// operand is a single `parse_log_or` so `1..5..10` is a parse error in Perl, but we accept
7635    /// it greedily (left-associated) because the lexer already forbids `..` after a range RHS.
7636    fn parse_range(&mut self) -> PerlResult<Expr> {
7637        let left = self.parse_log_or()?;
7638        let line = left.line;
7639        // `1..10` (traditional inclusive) / `1...10` (exclusive) / `1:10`
7640        // (short form) / `1~10` (universal short form). The `~` separator
7641        // works for every range type and is the only viable separator for
7642        // IPv6 since IPv6 already uses `:` internally; `:` would collide.
7643        // It also dodges `!`'s collision with the `_!N!` paired char-index
7644        // syntax. Single-`~` (vs `!!!` triple) keeps the surface simple.
7645        let (exclusive, _colon_style) = if self.eat(&Token::RangeExclusive) {
7646            (true, false)
7647        } else if self.eat(&Token::Range) {
7648            (false, false)
7649        } else if self.suppress_colon_range == 0 && self.eat(&Token::Colon) {
7650            // `1:10` short form — only valid for numeric ranges, not ternary
7651            // Lookahead: must be followed by something that looks like a range endpoint
7652            (false, true)
7653        } else if self.suppress_tilde_range == 0 && self.eat(&Token::BitNot) {
7654            (false, true)
7655        } else {
7656            return Ok(left);
7657        };
7658        let right = self.parse_log_or()?;
7659        // Optional step: `1..100:2` / `1:100:2` / `IPV6~IPV6~STEP`. `~` is
7660        // gated by `suppress_tilde_range` so paired char-index (`$x~5~`)
7661        // doesn't get its closing delimiter eaten as a range op.
7662        let step = if self.eat(&Token::Colon)
7663            || (self.suppress_tilde_range == 0 && self.eat(&Token::BitNot))
7664        {
7665            Some(Box::new(self.parse_unary()?))
7666        } else {
7667            None
7668        };
7669        Ok(Expr {
7670            kind: ExprKind::Range {
7671                from: Box::new(left),
7672                to: Box::new(right),
7673                exclusive,
7674                step,
7675            },
7676            line,
7677        })
7678    }
7679
7680    /// `name` or `Foo::Bar::baz` — used after `sub`, unary `&`, etc.
7681    fn parse_package_qualified_identifier(&mut self) -> PerlResult<String> {
7682        let mut name = match self.advance() {
7683            (Token::Ident(n), _) => n,
7684            (tok, l) => {
7685                return Err(self.syntax_err(format!("Expected identifier, got {:?}", tok), l));
7686            }
7687        };
7688        while self.eat(&Token::PackageSep) {
7689            match self.advance() {
7690                (Token::Ident(part), _) => {
7691                    name.push_str("::");
7692                    name.push_str(&part);
7693                }
7694                // Topic-slot scalars (`_`, `_<<<<`, `_3`, etc.) lex as
7695                // `Token::ScalarVar` per the lexer's reservation. Accept
7696                // them as the trailing segment of a package-qualified
7697                // name so callers (e.g. `parse_sub_decl`) can reject the
7698                // full name with a friendly "would shadow topic-slot"
7699                // message rather than a generic "Expected identifier
7700                // after `::`" lexer-level error.
7701                (Token::ScalarVar(part), _) if Self::is_underscore_topic_slot(&part) => {
7702                    name.push_str("::");
7703                    name.push_str(&part);
7704                }
7705                (tok, l) => {
7706                    return Err(self
7707                        .syntax_err(format!("Expected identifier after `::`, got {:?}", tok), l));
7708                }
7709            }
7710        }
7711        Ok(name)
7712    }
7713
7714    /// After consuming unary `&`: `name` or `Foo::Bar::baz` (Perl `&foo` / `&Foo::bar`).
7715    fn parse_qualified_subroutine_name(&mut self) -> PerlResult<String> {
7716        self.parse_package_qualified_identifier()
7717    }
7718
7719    fn parse_unary(&mut self) -> PerlResult<Expr> {
7720        let line = self.peek_line();
7721        match self.peek().clone() {
7722            Token::Minus => {
7723                self.advance();
7724                let expr = self.parse_power()?;
7725                Ok(Expr {
7726                    kind: ExprKind::UnaryOp {
7727                        op: UnaryOp::Negate,
7728                        expr: Box::new(expr),
7729                    },
7730                    line,
7731                })
7732            }
7733            // Unary `+EXPR` — Perl uses this to disambiguate barewords in hash subscripts (`$h{+Foo}`)
7734            // and for scalar context; treat as a no-op on the parsed operand.
7735            // Special case: `+{ ... }` forces hashref interpretation (Perl idiom),
7736            // even when the body is a list-yielding expression like `+{ map { ... } @arr }`.
7737            // Without this, `{ map { ... } @arr }` falls back to block/CodeRef parsing
7738            // because the body doesn't fit `KEY => VAL` shape.
7739            Token::Plus => {
7740                self.advance();
7741                if matches!(self.peek(), Token::LBrace) {
7742                    let line = self.peek_line();
7743                    self.advance(); // consume {
7744                    return self.parse_forced_hashref_body(line);
7745                }
7746                self.parse_unary()
7747            }
7748            Token::LogNot => {
7749                self.advance();
7750                let expr = self.parse_unary()?;
7751                Ok(Expr {
7752                    kind: ExprKind::UnaryOp {
7753                        op: UnaryOp::LogNot,
7754                        expr: Box::new(expr),
7755                    },
7756                    line,
7757                })
7758            }
7759            Token::BitNot => {
7760                self.advance();
7761                let expr = self.parse_unary()?;
7762                Ok(Expr {
7763                    kind: ExprKind::UnaryOp {
7764                        op: UnaryOp::BitNot,
7765                        expr: Box::new(expr),
7766                    },
7767                    line,
7768                })
7769            }
7770            Token::Increment => {
7771                self.advance();
7772                let expr = self.parse_postfix()?;
7773                Ok(Expr {
7774                    kind: ExprKind::UnaryOp {
7775                        op: UnaryOp::PreIncrement,
7776                        expr: Box::new(expr),
7777                    },
7778                    line,
7779                })
7780            }
7781            Token::Decrement => {
7782                self.advance();
7783                let expr = self.parse_postfix()?;
7784                Ok(Expr {
7785                    kind: ExprKind::UnaryOp {
7786                        op: UnaryOp::PreDecrement,
7787                        expr: Box::new(expr),
7788                    },
7789                    line,
7790                })
7791            }
7792            Token::BitAnd => {
7793                // Unary `&name` / `&Pkg::name` (call / coderef); binary `&` is in `parse_bit_and`.
7794                // `&$coderef(...)` — call sub whose ref is in a scalar (core `B.pm` / `&$recurse($sym)`).
7795                self.advance();
7796                if matches!(self.peek(), Token::LBrace) {
7797                    self.advance();
7798                    let inner = self.parse_expression()?;
7799                    self.expect(&Token::RBrace)?;
7800                    return Ok(Expr {
7801                        kind: ExprKind::DynamicSubCodeRef(Box::new(inner)),
7802                        line,
7803                    });
7804                }
7805                if matches!(self.peek(), Token::Ident(_)) {
7806                    let name = self.parse_qualified_subroutine_name()?;
7807                    return Ok(Expr {
7808                        kind: ExprKind::SubroutineRef(name),
7809                        line,
7810                    });
7811                }
7812                let target = self.parse_primary()?;
7813                if matches!(self.peek(), Token::LParen) {
7814                    self.advance();
7815                    let args = self.parse_arg_list()?;
7816                    self.expect(&Token::RParen)?;
7817                    return Ok(Expr {
7818                        kind: ExprKind::IndirectCall {
7819                            target: Box::new(target),
7820                            args,
7821                            ampersand: true,
7822                            pass_caller_arglist: false,
7823                        },
7824                        line,
7825                    });
7826                }
7827                // `&$coderef` / `&{expr}` with no `(...)` — call with caller's @_ (Perl `&$sub`).
7828                Ok(Expr {
7829                    kind: ExprKind::IndirectCall {
7830                        target: Box::new(target),
7831                        args: vec![],
7832                        ampersand: true,
7833                        pass_caller_arglist: true,
7834                    },
7835                    line,
7836                })
7837            }
7838            Token::Backslash => {
7839                self.advance();
7840                let expr = self.parse_unary()?;
7841                if let ExprKind::SubroutineRef(name) = expr.kind {
7842                    return Ok(Expr {
7843                        kind: ExprKind::SubroutineCodeRef(name),
7844                        line,
7845                    });
7846                }
7847                if matches!(expr.kind, ExprKind::DynamicSubCodeRef(_)) {
7848                    return Ok(expr);
7849                }
7850                // `\` uses `ScalarRef`; array/hash vars and `\@{...}` lower to binding or alias refs.
7851                Ok(Expr {
7852                    kind: ExprKind::ScalarRef(Box::new(expr)),
7853                    line,
7854                })
7855            }
7856            Token::FileTest(op) => {
7857                self.advance();
7858                // Perl: `-d` with no operand uses `$_` (e.g. `if (-d)` inside `for` / `while read`).
7859                let expr = if Self::filetest_allows_implicit_topic(self.peek()) {
7860                    Expr {
7861                        kind: ExprKind::ScalarVar("_".into()),
7862                        line: self.peek_line(),
7863                    }
7864                } else {
7865                    self.parse_unary()?
7866                };
7867                Ok(Expr {
7868                    kind: ExprKind::FileTest {
7869                        op,
7870                        expr: Box::new(expr),
7871                    },
7872                    line,
7873                })
7874            }
7875            _ => self.parse_power(),
7876        }
7877    }
7878
7879    fn parse_power(&mut self) -> PerlResult<Expr> {
7880        let left = self.parse_postfix()?;
7881        if matches!(self.peek(), Token::Power) {
7882            let line = left.line;
7883            self.advance();
7884            let right = self.parse_unary()?; // right-associative
7885            return Ok(Expr {
7886                kind: ExprKind::BinOp {
7887                    left: Box::new(left),
7888                    op: BinOp::Pow,
7889                    right: Box::new(right),
7890                },
7891                line,
7892            });
7893        }
7894        Ok(left)
7895    }
7896
7897    fn parse_postfix(&mut self) -> PerlResult<Expr> {
7898        let mut expr = self.parse_primary()?;
7899        loop {
7900            match self.peek().clone() {
7901                Token::Increment => {
7902                    // Implicit semicolon: `++` on a new line is a prefix operator
7903                    // on the next statement, not postfix on the previous expression.
7904                    if self.peek_line() > self.prev_line() {
7905                        break;
7906                    }
7907                    let line = expr.line;
7908                    self.advance();
7909                    expr = Expr {
7910                        kind: ExprKind::PostfixOp {
7911                            expr: Box::new(expr),
7912                            op: PostfixOp::Increment,
7913                        },
7914                        line,
7915                    };
7916                }
7917                Token::Decrement => {
7918                    // Implicit semicolon: `--` on a new line is a prefix operator
7919                    // on the next statement, not postfix on the previous expression.
7920                    if self.peek_line() > self.prev_line() {
7921                        break;
7922                    }
7923                    let line = expr.line;
7924                    self.advance();
7925                    expr = Expr {
7926                        kind: ExprKind::PostfixOp {
7927                            expr: Box::new(expr),
7928                            op: PostfixOp::Decrement,
7929                        },
7930                        line,
7931                    };
7932                }
7933                Token::LParen => {
7934                    if self.suppress_indirect_paren_call > 0 {
7935                        break;
7936                    }
7937                    // Implicit semicolon: `(` on a new line after an expression
7938                    // is a new statement, not a postfix code-ref call.
7939                    // e.g.  `my $x = $ENV{"KEY"}\n($y =~ s/.../.../)`
7940                    if self.peek_line() > self.prev_line() {
7941                        break;
7942                    }
7943                    let line = expr.line;
7944                    self.advance();
7945                    let args = self.parse_arg_list()?;
7946                    self.expect(&Token::RParen)?;
7947                    expr = Expr {
7948                        kind: ExprKind::IndirectCall {
7949                            target: Box::new(expr),
7950                            args,
7951                            ampersand: false,
7952                            pass_caller_arglist: false,
7953                        },
7954                        line,
7955                    };
7956                }
7957                Token::Arrow => {
7958                    let line = expr.line;
7959                    self.advance();
7960                    match self.peek().clone() {
7961                        Token::LBracket => {
7962                            self.advance();
7963                            let index = self.parse_expression()?;
7964                            self.expect(&Token::RBracket)?;
7965                            expr = Expr {
7966                                kind: ExprKind::ArrowDeref {
7967                                    expr: Box::new(expr),
7968                                    index: Box::new(index),
7969                                    kind: DerefKind::Array,
7970                                },
7971                                line,
7972                            };
7973                        }
7974                        Token::LBrace => {
7975                            self.advance();
7976                            let key = self.parse_hash_subscript_key()?;
7977                            self.expect(&Token::RBrace)?;
7978                            expr = Expr {
7979                                kind: ExprKind::ArrowDeref {
7980                                    expr: Box::new(expr),
7981                                    index: Box::new(key),
7982                                    kind: DerefKind::Hash,
7983                                },
7984                                line,
7985                            };
7986                        }
7987                        Token::LParen => {
7988                            self.advance();
7989                            let args = self.parse_arg_list()?;
7990                            self.expect(&Token::RParen)?;
7991                            expr = Expr {
7992                                kind: ExprKind::ArrowDeref {
7993                                    expr: Box::new(expr),
7994                                    index: Box::new(Expr {
7995                                        kind: ExprKind::List(args),
7996                                        line,
7997                                    }),
7998                                    kind: DerefKind::Call,
7999                                },
8000                                line,
8001                            };
8002                        }
8003                        Token::Ident(method) => {
8004                            self.advance();
8005                            if method == "SUPER" {
8006                                self.expect(&Token::PackageSep)?;
8007                                let real_method = match self.advance() {
8008                                    (Token::Ident(n), _) => n,
8009                                    (tok, l) => {
8010                                        return Err(self.syntax_err(
8011                                            format!(
8012                                                "Expected method name after SUPER::, got {:?}",
8013                                                tok
8014                                            ),
8015                                            l,
8016                                        ));
8017                                    }
8018                                };
8019                                let args = if self.eat(&Token::LParen) {
8020                                    let a = self.parse_arg_list()?;
8021                                    self.expect(&Token::RParen)?;
8022                                    a
8023                                } else {
8024                                    self.parse_method_arg_list_no_paren()?
8025                                };
8026                                expr = Expr {
8027                                    kind: ExprKind::MethodCall {
8028                                        object: Box::new(expr),
8029                                        method: real_method,
8030                                        args,
8031                                        super_call: true,
8032                                    },
8033                                    line,
8034                                };
8035                            } else {
8036                                let mut method_name = method;
8037                                while self.eat(&Token::PackageSep) {
8038                                    match self.advance() {
8039                                        (Token::Ident(part), _) => {
8040                                            method_name.push_str("::");
8041                                            method_name.push_str(&part);
8042                                        }
8043                                        (tok, l) => {
8044                                            return Err(self.syntax_err(
8045                                                format!(
8046                                                    "Expected identifier after :: in method name, got {:?}",
8047                                                    tok
8048                                                ),
8049                                                l,
8050                                            ));
8051                                        }
8052                                    }
8053                                }
8054                                let args = if self.eat(&Token::LParen) {
8055                                    let a = self.parse_arg_list()?;
8056                                    self.expect(&Token::RParen)?;
8057                                    a
8058                                } else {
8059                                    self.parse_method_arg_list_no_paren()?
8060                                };
8061                                expr = Expr {
8062                                    kind: ExprKind::MethodCall {
8063                                        object: Box::new(expr),
8064                                        method: method_name,
8065                                        args,
8066                                        super_call: false,
8067                                    },
8068                                    line,
8069                                };
8070                            }
8071                        }
8072                        // Postfix dereference (Perl 5.20+, default 5.24+):
8073                        //   `$ref->@*`         — full array      ≡ `@{$ref}`
8074                        //   `$ref->@[i,j]`     — array slice     ≡ `@{$ref}[i,j]`
8075                        //   `$ref->@{k,l}`     — hash slice (vals) ≡ `@{$ref}{k,l}`
8076                        //   `$ref->%*`         — full hash       ≡ `%{$ref}`
8077                        Token::ArrayAt => {
8078                            self.advance(); // consume `@`
8079                            match self.peek().clone() {
8080                                Token::Star => {
8081                                    self.advance();
8082                                    expr = Expr {
8083                                        kind: ExprKind::Deref {
8084                                            expr: Box::new(expr),
8085                                            kind: Sigil::Array,
8086                                        },
8087                                        line,
8088                                    };
8089                                }
8090                                Token::LBracket => {
8091                                    self.advance();
8092                                    let indices = self.parse_slice_arg_list(false)?;
8093                                    self.expect(&Token::RBracket)?;
8094                                    let source = Expr {
8095                                        kind: ExprKind::Deref {
8096                                            expr: Box::new(expr),
8097                                            kind: Sigil::Array,
8098                                        },
8099                                        line,
8100                                    };
8101                                    expr = Expr {
8102                                        kind: ExprKind::AnonymousListSlice {
8103                                            source: Box::new(source),
8104                                            indices,
8105                                        },
8106                                        line,
8107                                    };
8108                                }
8109                                Token::LBrace => {
8110                                    self.advance();
8111                                    let keys = self.parse_slice_arg_list(true)?;
8112                                    self.expect(&Token::RBrace)?;
8113                                    expr = Expr {
8114                                        kind: ExprKind::HashSliceDeref {
8115                                            container: Box::new(expr),
8116                                            keys,
8117                                        },
8118                                        line,
8119                                    };
8120                                }
8121                                tok => {
8122                                    return Err(self.syntax_err(
8123                                        format!(
8124                                            "Expected `*`, `[…]`, or `{{…}}` after `->@`, got {:?}",
8125                                            tok
8126                                        ),
8127                                        line,
8128                                    ));
8129                                }
8130                            }
8131                        }
8132                        Token::HashPercent => {
8133                            self.advance(); // consume `%`
8134                            match self.peek().clone() {
8135                                Token::Star => {
8136                                    self.advance();
8137                                    expr = Expr {
8138                                        kind: ExprKind::Deref {
8139                                            expr: Box::new(expr),
8140                                            kind: Sigil::Hash,
8141                                        },
8142                                        line,
8143                                    };
8144                                }
8145                                tok => {
8146                                    return Err(self.syntax_err(
8147                                        format!("Expected `*` after `->%`, got {:?}", tok),
8148                                        line,
8149                                    ));
8150                                }
8151                            }
8152                        }
8153                        // `x` is lexed as `Token::X` (repeat op); after `->` it is a method name.
8154                        Token::X => {
8155                            self.advance();
8156                            let args = if self.eat(&Token::LParen) {
8157                                let a = self.parse_arg_list()?;
8158                                self.expect(&Token::RParen)?;
8159                                a
8160                            } else {
8161                                self.parse_method_arg_list_no_paren()?
8162                            };
8163                            expr = Expr {
8164                                kind: ExprKind::MethodCall {
8165                                    object: Box::new(expr),
8166                                    method: "x".to_string(),
8167                                    args,
8168                                    super_call: false,
8169                                },
8170                                line,
8171                            };
8172                        }
8173                        _ => break,
8174                    }
8175                }
8176                Token::LBracket => {
8177                    // Implicit semicolon: `[` on a new line is a new statement (array literal),
8178                    // not an array subscript on the preceding expression.
8179                    if self.peek_line() > self.prev_line() {
8180                        break;
8181                    }
8182                    // `$a[i]` — or chained `$r->{k}[i]` / `$a[1][2]` — or list slice `(sort ...)[0]`.
8183                    let line = expr.line;
8184                    if matches!(expr.kind, ExprKind::ScalarVar(_)) {
8185                        if let ExprKind::ScalarVar(ref name) = expr.kind {
8186                            let name = name.clone();
8187                            self.advance();
8188                            let index = self.parse_expression()?;
8189                            self.expect(&Token::RBracket)?;
8190                            expr = Expr {
8191                                kind: ExprKind::ArrayElement {
8192                                    array: name,
8193                                    index: Box::new(index),
8194                                },
8195                                line,
8196                            };
8197                        }
8198                    } else if postfix_lbracket_is_arrow_container(&expr) {
8199                        self.advance();
8200                        let indices = self.parse_arg_list()?;
8201                        self.expect(&Token::RBracket)?;
8202                        expr = Expr {
8203                            kind: ExprKind::ArrowDeref {
8204                                expr: Box::new(expr),
8205                                index: Box::new(Expr {
8206                                    kind: ExprKind::List(indices),
8207                                    line,
8208                                }),
8209                                kind: DerefKind::Array,
8210                            },
8211                            line,
8212                        };
8213                    } else {
8214                        self.advance();
8215                        let indices = self.parse_arg_list()?;
8216                        self.expect(&Token::RBracket)?;
8217                        expr = Expr {
8218                            kind: ExprKind::AnonymousListSlice {
8219                                source: Box::new(expr),
8220                                indices,
8221                            },
8222                            line,
8223                        };
8224                    }
8225                }
8226                Token::LBrace => {
8227                    if self.suppress_scalar_hash_brace > 0 {
8228                        break;
8229                    }
8230                    // Implicit semicolon: `{` on a new line is a new statement (block/hashref),
8231                    // not a hash subscript on the preceding expression.
8232                    if self.peek_line() > self.prev_line() {
8233                        break;
8234                    }
8235                    // `$h{k}`, or chained `$h{k2}{k3}` / `$r->{a}{b}` / `$a[0]{k}` — second+ `{…}` is
8236                    // hash subscript on the scalar value (same as `-> { … }` without extra `->`).
8237                    let line = expr.line;
8238                    let is_scalar_named_hash = matches!(expr.kind, ExprKind::ScalarVar(_));
8239                    let is_chainable_hash_subscript = is_scalar_named_hash
8240                        || matches!(
8241                            expr.kind,
8242                            ExprKind::HashElement { .. }
8243                                | ExprKind::ArrayElement { .. }
8244                                | ExprKind::ArrowDeref { .. }
8245                                | ExprKind::Deref {
8246                                    kind: Sigil::Scalar,
8247                                    ..
8248                                }
8249                        );
8250                    if !is_chainable_hash_subscript {
8251                        break;
8252                    }
8253                    self.advance();
8254                    let key = self.parse_hash_subscript_key()?;
8255                    self.expect(&Token::RBrace)?;
8256                    expr = if is_scalar_named_hash {
8257                        if let ExprKind::ScalarVar(ref name) = expr.kind {
8258                            let name = name.clone();
8259                            // Perl: `$_ { k }` means `$_->{k}` (implicit arrow), not the `%_` stash hash.
8260                            if name == "_" {
8261                                Expr {
8262                                    kind: ExprKind::ArrowDeref {
8263                                        expr: Box::new(Expr {
8264                                            kind: ExprKind::ScalarVar("_".into()),
8265                                            line,
8266                                        }),
8267                                        index: Box::new(key),
8268                                        kind: DerefKind::Hash,
8269                                    },
8270                                    line,
8271                                }
8272                            } else {
8273                                Expr {
8274                                    kind: ExprKind::HashElement {
8275                                        hash: name,
8276                                        key: Box::new(key),
8277                                    },
8278                                    line,
8279                                }
8280                            }
8281                        } else {
8282                            unreachable!("is_scalar_named_hash implies ScalarVar");
8283                        }
8284                    } else {
8285                        Expr {
8286                            kind: ExprKind::ArrowDeref {
8287                                expr: Box::new(expr),
8288                                index: Box::new(key),
8289                                kind: DerefKind::Hash,
8290                            },
8291                            line,
8292                        }
8293                    };
8294                }
8295                Token::LogNot | Token::BitNot => {
8296                    // Stryke universal string-subscript sugar — paired `!…!`
8297                    // OR paired `~…~`: `$VAR!N!`, `$VAR~N~`, `$VAR!1:5:2!`,
8298                    // `_!N!`, `_~from:to:step~`. Returns substring of the
8299                    // scalar (Unicode chars).  Distinct from `[N]` which has
8300                    // Perl's `@VAR[N]` / `$_[N]` semantics. Both forms work on
8301                    // any scalar (named or topic) without colliding: `!` and
8302                    // `~` after a value have no current postfix meaning (`!=`
8303                    // / `!~` are pre-merged binary tokens; `~` is prefix-only
8304                    // bit-not). The opening and closing delimiter must match.
8305                    //
8306                    // Implementation: rewrite to ArrayElement with a
8307                    // synthetic name `__topicstr__$NAME`. The interpreter
8308                    // and VM strip the prefix and dispatch to char-of-string
8309                    // (and slice-of-string for Range indices).
8310                    if !matches!(expr.kind, ExprKind::ScalarVar(_)) {
8311                        break;
8312                    }
8313                    if self.peek_line() > self.prev_line() {
8314                        break;
8315                    }
8316                    let opener = self.peek().clone();
8317                    let line = expr.line;
8318                    let name = if let ExprKind::ScalarVar(ref n) = expr.kind {
8319                        n.clone()
8320                    } else {
8321                        unreachable!()
8322                    };
8323                    self.advance(); // consume opening `!` or `~`
8324                                    // Suppress `~` as a range separator while parsing the
8325                                    // paired index — `$_~5~` would otherwise consume the
8326                                    // closing `~` as a range op. `:` is still allowed so
8327                                    // `$_~1:3~` (slice with `:` range index) keeps working.
8328                    self.suppress_tilde_range = self.suppress_tilde_range.saturating_add(1);
8329                    let index_result = self.parse_expression();
8330                    self.suppress_tilde_range = self.suppress_tilde_range.saturating_sub(1);
8331                    let index = index_result?;
8332                    let close_match = matches!(
8333                        (&opener, self.peek()),
8334                        (Token::LogNot, Token::LogNot) | (Token::BitNot, Token::BitNot)
8335                    );
8336                    if !close_match {
8337                        let want = if matches!(opener, Token::LogNot) {
8338                            "!"
8339                        } else {
8340                            "~"
8341                        };
8342                        return Err(self.syntax_err(
8343                            format!("expected closing `{}` for string subscript", want),
8344                            self.peek_line(),
8345                        ));
8346                    }
8347                    self.advance(); // consume closing delimiter
8348                    expr = Expr {
8349                        kind: ExprKind::ArrayElement {
8350                            array: format!("__topicstr__{}", name),
8351                            index: Box::new(index),
8352                        },
8353                        line,
8354                    };
8355                }
8356                _ => break,
8357            }
8358        }
8359        Ok(expr)
8360    }
8361
8362    fn parse_primary(&mut self) -> PerlResult<Expr> {
8363        let line = self.peek_line();
8364        // `my $x = …` (or `our` / `state` / `local`) used inside an expression —
8365        // typically `if (my $x = …)` / `while (my $line = <FH>)`.  Returns the
8366        // assigned value(s); has the side effect of declaring the variable in
8367        // the current scope.  See `ExprKind::MyExpr`.
8368        if let Token::Ident(ref kw) = self.peek().clone() {
8369            if matches!(kw.as_str(), "my" | "our" | "state" | "local") {
8370                let kw_owned = kw.clone();
8371                // Parse exactly like the statement form via `parse_my_our_local`,
8372                // then unwrap the resulting `StmtKind::*` back into a list of
8373                // `VarDecl`s for the expression node.  This re-uses the full
8374                // syntax (typed sigs, list destructuring, type annotations).
8375                let saved_pos = self.pos;
8376                let stmt = self.parse_my_our_local(&kw_owned, false)?;
8377                let decls = match stmt.kind {
8378                    StmtKind::My(d)
8379                    | StmtKind::Our(d)
8380                    | StmtKind::State(d)
8381                    | StmtKind::Local(d) => d,
8382                    _ => {
8383                        // `local *FOO = …` / non-decl forms — fall back to the
8384                        // statement parser (already advanced); restore position
8385                        // and let the surrounding code handle it as a statement
8386                        // by erroring loudly here.
8387                        self.pos = saved_pos;
8388                        return Err(self.syntax_err(
8389                            "`my`/`our`/`local` in expression must declare variables",
8390                            line,
8391                        ));
8392                    }
8393                };
8394                return Ok(Expr {
8395                    kind: ExprKind::MyExpr {
8396                        keyword: kw_owned,
8397                        decls,
8398                    },
8399                    line,
8400                });
8401            }
8402        }
8403        match self.peek().clone() {
8404            Token::Integer(n) => {
8405                self.advance();
8406                Ok(Expr {
8407                    kind: ExprKind::Integer(n),
8408                    line,
8409                })
8410            }
8411            Token::Float(f) => {
8412                self.advance();
8413                Ok(Expr {
8414                    kind: ExprKind::Float(f),
8415                    line,
8416                })
8417            }
8418            // `>{ BLOCK }` — IIFE block expression (immediately-invoked anonymous sub).
8419            // Valid in any expression position; evaluates the block and yields its last value.
8420            // In thread-macro stage position (`EXPR |>` already consumed by the stage loop in
8421            // `parse_thread_macro`), the explicit branch at ~1417 wins and the block is
8422            // instead pipe-applied as a coderef — that path is never reached from here.
8423            Token::ArrowBrace => {
8424                self.advance();
8425                let mut stmts = Vec::new();
8426                while !matches!(self.peek(), Token::RBrace | Token::Eof) {
8427                    if self.eat(&Token::Semicolon) {
8428                        continue;
8429                    }
8430                    stmts.push(self.parse_statement()?);
8431                }
8432                self.expect(&Token::RBrace)?;
8433                let inner_line = stmts.first().map(|s| s.line).unwrap_or(line);
8434                let inner = Expr {
8435                    kind: ExprKind::CodeRef {
8436                        params: vec![],
8437                        body: stmts,
8438                    },
8439                    line: inner_line,
8440                };
8441                Ok(Expr {
8442                    kind: ExprKind::Do(Box::new(inner)),
8443                    line,
8444                })
8445            }
8446            Token::Star => {
8447                self.advance();
8448                if matches!(self.peek(), Token::LBrace) {
8449                    self.advance();
8450                    let inner = self.parse_expression()?;
8451                    self.expect(&Token::RBrace)?;
8452                    return Ok(Expr {
8453                        kind: ExprKind::Deref {
8454                            expr: Box::new(inner),
8455                            kind: Sigil::Typeglob,
8456                        },
8457                        line,
8458                    });
8459                }
8460                // `*$_{$k}`, `*${expr}`, `*$foo` — typeglob from a sigil expression (Perl 5 `*$globref`).
8461                if matches!(
8462                    self.peek(),
8463                    Token::ScalarVar(_)
8464                        | Token::ArrayVar(_)
8465                        | Token::HashVar(_)
8466                        | Token::DerefScalarVar(_)
8467                        | Token::HashPercent
8468                ) {
8469                    let inner = self.parse_postfix()?;
8470                    return Ok(Expr {
8471                        kind: ExprKind::TypeglobExpr(Box::new(inner)),
8472                        line,
8473                    });
8474                }
8475                // `x` tokenizes as `Token::X` (repeat op) — still a valid package/typeglob name.
8476                let mut full_name = match self.advance() {
8477                    (Token::Ident(n), _) => n,
8478                    (Token::X, _) => "x".to_string(),
8479                    (tok, l) => {
8480                        return Err(self
8481                            .syntax_err(format!("Expected identifier after *, got {:?}", tok), l));
8482                    }
8483                };
8484                while self.eat(&Token::PackageSep) {
8485                    match self.advance() {
8486                        (Token::Ident(part), _) => {
8487                            full_name = format!("{}::{}", full_name, part);
8488                        }
8489                        (Token::X, _) => {
8490                            full_name = format!("{}::x", full_name);
8491                        }
8492                        (tok, l) => {
8493                            return Err(self.syntax_err(
8494                                format!("Expected identifier after :: in typeglob, got {:?}", tok),
8495                                l,
8496                            ));
8497                        }
8498                    }
8499                }
8500                Ok(Expr {
8501                    kind: ExprKind::Typeglob(full_name),
8502                    line,
8503                })
8504            }
8505            Token::SingleString(s) => {
8506                self.advance();
8507                Ok(Expr {
8508                    kind: ExprKind::String(s),
8509                    line,
8510                })
8511            }
8512            Token::DoubleString(s) => {
8513                self.advance();
8514                self.parse_interpolated_string(&s, line)
8515            }
8516            Token::BacktickString(s) => {
8517                self.advance();
8518                let inner = self.parse_interpolated_string(&s, line)?;
8519                Ok(Expr {
8520                    kind: ExprKind::Qx(Box::new(inner)),
8521                    line,
8522                })
8523            }
8524            Token::HereDoc(_, body, interpolate) => {
8525                self.advance();
8526                if interpolate {
8527                    self.parse_interpolated_string(&body, line)
8528                } else {
8529                    Ok(Expr {
8530                        kind: ExprKind::String(body),
8531                        line,
8532                    })
8533                }
8534            }
8535            Token::Regex(pattern, flags, _delim) => {
8536                self.advance();
8537                Ok(Expr {
8538                    kind: ExprKind::Regex(pattern, flags),
8539                    line,
8540                })
8541            }
8542            Token::QW(words) => {
8543                self.advance();
8544                // `qw(a b c) x N` is list-repeat in Perl even without explicit
8545                // outer parens — `qw(...)` is itself a list constructor.
8546                self.list_construct_close_pos = Some(self.pos);
8547                Ok(Expr {
8548                    kind: ExprKind::QW(words),
8549                    line,
8550                })
8551            }
8552            Token::DerefScalarVar(name) => {
8553                self.advance();
8554                Ok(Expr {
8555                    kind: ExprKind::Deref {
8556                        expr: Box::new(Expr {
8557                            kind: ExprKind::ScalarVar(name),
8558                            line,
8559                        }),
8560                        kind: Sigil::Scalar,
8561                    },
8562                    line,
8563                })
8564            }
8565            Token::ScalarVar(name) => {
8566                self.advance();
8567                Ok(Expr {
8568                    kind: ExprKind::ScalarVar(name),
8569                    line,
8570                })
8571            }
8572            Token::ArrayVar(name) => {
8573                self.advance();
8574                // Check for slice: @arr[...] (array slice) or @hash{...} (hash slice)
8575                match self.peek() {
8576                    Token::LBracket => {
8577                        self.advance();
8578                        let indices = self.parse_slice_arg_list(false)?;
8579                        self.expect(&Token::RBracket)?;
8580                        Ok(Expr {
8581                            kind: ExprKind::ArraySlice {
8582                                array: name,
8583                                indices,
8584                            },
8585                            line,
8586                        })
8587                    }
8588                    Token::LBrace if self.suppress_scalar_hash_brace == 0 => {
8589                        self.advance();
8590                        let keys = self.parse_slice_arg_list(true)?;
8591                        self.expect(&Token::RBrace)?;
8592                        Ok(Expr {
8593                            kind: ExprKind::HashSlice { hash: name, keys },
8594                            line,
8595                        })
8596                    }
8597                    _ => Ok(Expr {
8598                        kind: ExprKind::ArrayVar(name),
8599                        line,
8600                    }),
8601                }
8602            }
8603            Token::HashVar(name) => {
8604                self.advance();
8605                // `%h{KEYS}` — Perl 5.20+ key-value slice. Parser-level
8606                // disambiguation: `%h` immediately followed by `{` is a kv-
8607                // slice; `%h` alone (or followed by `=`, list ops, etc.) is
8608                // the bare hash. (BUG-008)
8609                if matches!(self.peek(), Token::LBrace) && self.suppress_scalar_hash_brace == 0 {
8610                    self.advance(); // {
8611                    let keys = self.parse_slice_arg_list(true)?;
8612                    self.expect(&Token::RBrace)?;
8613                    return Ok(Expr {
8614                        kind: ExprKind::HashKvSlice { hash: name, keys },
8615                        line,
8616                    });
8617                }
8618                Ok(Expr {
8619                    kind: ExprKind::HashVar(name),
8620                    line,
8621                })
8622            }
8623            Token::HashPercent => {
8624                // `%$href` — hash ref deref; `%{ $expr }` — symbolic / braced form
8625                self.advance();
8626                if matches!(self.peek(), Token::ScalarVar(_)) {
8627                    let n = match self.advance() {
8628                        (Token::ScalarVar(n), _) => n,
8629                        (tok, l) => {
8630                            return Err(self.syntax_err(
8631                                format!("Expected scalar variable after %%, got {:?}", tok),
8632                                l,
8633                            ));
8634                        }
8635                    };
8636                    return Ok(Expr {
8637                        kind: ExprKind::Deref {
8638                            expr: Box::new(Expr {
8639                                kind: ExprKind::ScalarVar(n),
8640                                line,
8641                            }),
8642                            kind: Sigil::Hash,
8643                        },
8644                        line,
8645                    });
8646                }
8647                // `%[a => 1, b => 2]` — sugar for `%{+{a=>1,b=>2}}`: dereference an
8648                // anonymous hashref inline, using `[...]` as the delimiter to avoid
8649                // the block-vs-hashref ambiguity that `%{a=>1}` has in real Perl.
8650                // Real Perl errors on `%[...]` syntactically, so no compat risk.
8651                if matches!(self.peek(), Token::LBracket) {
8652                    self.advance();
8653                    let pairs = self.parse_hashref_pairs_until(&Token::RBracket)?;
8654                    self.expect(&Token::RBracket)?;
8655                    let href = Expr {
8656                        kind: ExprKind::HashRef(pairs),
8657                        line,
8658                    };
8659                    return Ok(Expr {
8660                        kind: ExprKind::Deref {
8661                            expr: Box::new(href),
8662                            kind: Sigil::Hash,
8663                        },
8664                        line,
8665                    });
8666                }
8667                self.expect(&Token::LBrace)?;
8668                // Peek to disambiguate `%{ $ref }` (deref a hashref expression) from
8669                // `%{ k => v }` (inline hash literal). Real Perl's block-vs-hashref
8670                // heuristic is famously unreliable — when the first non-whitespace
8671                // token is an ident/string followed by `=>`, treat the whole thing
8672                // as a hashref literal to make `%{a=>1,b=>2}` work reliably.
8673                let looks_like_pair = matches!(
8674                    self.peek(),
8675                    Token::Ident(_) | Token::SingleString(_) | Token::DoubleString(_)
8676                ) && matches!(self.peek_at(1), Token::FatArrow);
8677                let inner = if looks_like_pair {
8678                    let pairs = self.parse_hashref_pairs_until(&Token::RBrace)?;
8679                    Expr {
8680                        kind: ExprKind::HashRef(pairs),
8681                        line,
8682                    }
8683                } else {
8684                    self.parse_expression()?
8685                };
8686                self.expect(&Token::RBrace)?;
8687                Ok(Expr {
8688                    kind: ExprKind::Deref {
8689                        expr: Box::new(inner),
8690                        kind: Sigil::Hash,
8691                    },
8692                    line,
8693                })
8694            }
8695            Token::ArrayAt => {
8696                self.advance();
8697                // `@{ $expr }` / `@{ "Pkg::NAME" }` — symbolic array (e.g. `@{"$pkg\::EXPORT"}` in Exporter.pm)
8698                if matches!(self.peek(), Token::LBrace) {
8699                    self.advance();
8700                    let inner = self.parse_expression()?;
8701                    self.expect(&Token::RBrace)?;
8702                    return Ok(Expr {
8703                        kind: ExprKind::Deref {
8704                            expr: Box::new(inner),
8705                            kind: Sigil::Array,
8706                        },
8707                        line,
8708                    });
8709                }
8710                // `@[a, b, c]` — sugar for `@{[a, b, c]}`: dereference an
8711                // anonymous arrayref inline. Real Perl rejects `@[...]` at
8712                // the parser level, so this extension has no compat risk.
8713                if matches!(self.peek(), Token::LBracket) {
8714                    self.advance();
8715                    let mut elems = Vec::new();
8716                    if !matches!(self.peek(), Token::RBracket) {
8717                        elems.push(self.parse_assign_expr()?);
8718                        while self.eat(&Token::Comma) {
8719                            if matches!(self.peek(), Token::RBracket) {
8720                                break;
8721                            }
8722                            elems.push(self.parse_assign_expr()?);
8723                        }
8724                    }
8725                    self.expect(&Token::RBracket)?;
8726                    let aref = Expr {
8727                        kind: ExprKind::ArrayRef(elems),
8728                        line,
8729                    };
8730                    return Ok(Expr {
8731                        kind: ExprKind::Deref {
8732                            expr: Box::new(aref),
8733                            kind: Sigil::Array,
8734                        },
8735                        line,
8736                    });
8737                }
8738                // `@$arr` — array dereference; `@$h{k1,k2}` — hash slice via hashref
8739                let container = match self.peek().clone() {
8740                    Token::ScalarVar(n) => {
8741                        self.advance();
8742                        Expr {
8743                            kind: ExprKind::ScalarVar(n),
8744                            line,
8745                        }
8746                    }
8747                    _ => {
8748                        return Err(self.syntax_err(
8749                            "Expected `$name`, `{`, or `[` after `@` (e.g. `@$aref`, `@{expr}`, `@[1,2,3]`, or `@$href{keys}`)",
8750                            line,
8751                        ));
8752                    }
8753                };
8754                if matches!(self.peek(), Token::LBrace) {
8755                    self.advance();
8756                    let keys = self.parse_slice_arg_list(true)?;
8757                    self.expect(&Token::RBrace)?;
8758                    return Ok(Expr {
8759                        kind: ExprKind::HashSliceDeref {
8760                            container: Box::new(container),
8761                            keys,
8762                        },
8763                        line,
8764                    });
8765                }
8766                Ok(Expr {
8767                    kind: ExprKind::Deref {
8768                        expr: Box::new(container),
8769                        kind: Sigil::Array,
8770                    },
8771                    line,
8772                })
8773            }
8774            Token::LParen => {
8775                self.advance();
8776                if matches!(self.peek(), Token::RParen) {
8777                    self.advance();
8778                    // Empty `() x 3` is a no-op list repeat — record the close
8779                    // position so `Token::X` knows the LHS was a list literal.
8780                    self.list_construct_close_pos = Some(self.pos);
8781                    return Ok(Expr {
8782                        kind: ExprKind::List(vec![]),
8783                        line,
8784                    });
8785                }
8786                // Inside parens, pipe-forward is allowed even if we're in a
8787                // paren-less arg context. Save and restore no_pipe_forward_depth.
8788                let saved_no_pipe = self.no_pipe_forward_depth;
8789                self.no_pipe_forward_depth = 0;
8790                let expr = self.parse_expression();
8791                self.no_pipe_forward_depth = saved_no_pipe;
8792                let expr = expr?;
8793                self.expect(&Token::RParen)?;
8794                // Mark this paren as a list-constructor for the `x` operator
8795                // (parse_multiplication compares `self.pos` at the X token to
8796                // this checkpoint). Function-call parens (`f(args)`) don't
8797                // reach this branch; they're parsed by the call machinery.
8798                self.list_construct_close_pos = Some(self.pos);
8799                Ok(expr)
8800            }
8801            Token::LBracket => {
8802                self.advance();
8803                let elems = self.parse_arg_list()?;
8804                self.expect(&Token::RBracket)?;
8805                Ok(Expr {
8806                    kind: ExprKind::ArrayRef(elems),
8807                    line,
8808                })
8809            }
8810            Token::LBrace => {
8811                // Could be hash ref or block — disambiguate
8812                self.advance();
8813                // Try to parse as hash ref: { key => val, ... }
8814                let saved = self.pos;
8815                match self.try_parse_hash_ref() {
8816                    Ok(pairs) => Ok(Expr {
8817                        kind: ExprKind::HashRef(pairs),
8818                        line,
8819                    }),
8820                    Err(_) => {
8821                        self.pos = saved;
8822                        // Parse as block, wrap in code ref
8823                        let mut stmts = Vec::new();
8824                        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
8825                            if self.eat(&Token::Semicolon) {
8826                                continue;
8827                            }
8828                            stmts.push(self.parse_statement()?);
8829                        }
8830                        self.expect(&Token::RBrace)?;
8831                        Ok(Expr {
8832                            kind: ExprKind::CodeRef {
8833                                params: vec![],
8834                                body: stmts,
8835                            },
8836                            line,
8837                        })
8838                    }
8839                }
8840            }
8841            Token::Diamond => {
8842                self.advance();
8843                Ok(Expr {
8844                    kind: ExprKind::ReadLine(None),
8845                    line,
8846                })
8847            }
8848            Token::ReadLine(handle) => {
8849                self.advance();
8850                Ok(Expr {
8851                    kind: ExprKind::ReadLine(Some(handle)),
8852                    line,
8853                })
8854            }
8855
8856            // Named functions / builtins
8857            Token::ThreadArrow => {
8858                self.advance();
8859                self.parse_thread_macro(line, false)
8860            }
8861            Token::ThreadArrowLast => {
8862                self.advance();
8863                self.parse_thread_macro(line, true)
8864            }
8865            Token::Ident(ref name) => {
8866                let name = name.clone();
8867                // Handle s///
8868                if name.starts_with('\x00') {
8869                    self.advance();
8870                    let parts: Vec<&str> = name.split('\x00').collect();
8871                    if parts.len() >= 4 && parts[1] == "s" {
8872                        let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
8873                        return Ok(Expr {
8874                            kind: ExprKind::Substitution {
8875                                expr: Box::new(Expr {
8876                                    kind: ExprKind::ScalarVar("_".into()),
8877                                    line,
8878                                }),
8879                                pattern: parts[2].to_string(),
8880                                replacement: parts[3].to_string(),
8881                                flags: parts.get(4).unwrap_or(&"").to_string(),
8882                                delim,
8883                            },
8884                            line,
8885                        });
8886                    }
8887                    if parts.len() >= 4 && parts[1] == "tr" {
8888                        let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
8889                        return Ok(Expr {
8890                            kind: ExprKind::Transliterate {
8891                                expr: Box::new(Expr {
8892                                    kind: ExprKind::ScalarVar("_".into()),
8893                                    line,
8894                                }),
8895                                from: parts[2].to_string(),
8896                                to: parts[3].to_string(),
8897                                flags: parts.get(4).unwrap_or(&"").to_string(),
8898                                delim,
8899                            },
8900                            line,
8901                        });
8902                    }
8903                    return Err(self.syntax_err("Unexpected encoded token", line));
8904                }
8905                self.parse_named_expr(name)
8906            }
8907
8908            // `%name` when lexer emitted `Token::Percent` (due to preceding term context)
8909            // instead of `Token::HashVar`. This happens after `t` (thread macro) etc.
8910            Token::Percent => {
8911                self.advance();
8912                match self.peek().clone() {
8913                    Token::Ident(name) => {
8914                        self.advance();
8915                        Ok(Expr {
8916                            kind: ExprKind::HashVar(name),
8917                            line,
8918                        })
8919                    }
8920                    Token::ScalarVar(n) => {
8921                        self.advance();
8922                        Ok(Expr {
8923                            kind: ExprKind::Deref {
8924                                expr: Box::new(Expr {
8925                                    kind: ExprKind::ScalarVar(n),
8926                                    line,
8927                                }),
8928                                kind: Sigil::Hash,
8929                            },
8930                            line,
8931                        })
8932                    }
8933                    Token::LBrace => {
8934                        self.advance();
8935                        let looks_like_pair = matches!(
8936                            self.peek(),
8937                            Token::Ident(_) | Token::SingleString(_) | Token::DoubleString(_)
8938                        ) && matches!(self.peek_at(1), Token::FatArrow);
8939                        let inner = if looks_like_pair {
8940                            let pairs = self.parse_hashref_pairs_until(&Token::RBrace)?;
8941                            Expr {
8942                                kind: ExprKind::HashRef(pairs),
8943                                line,
8944                            }
8945                        } else {
8946                            self.parse_expression()?
8947                        };
8948                        self.expect(&Token::RBrace)?;
8949                        Ok(Expr {
8950                            kind: ExprKind::Deref {
8951                                expr: Box::new(inner),
8952                                kind: Sigil::Hash,
8953                            },
8954                            line,
8955                        })
8956                    }
8957                    Token::LBracket => {
8958                        self.advance();
8959                        let pairs = self.parse_hashref_pairs_until(&Token::RBracket)?;
8960                        self.expect(&Token::RBracket)?;
8961                        let href = Expr {
8962                            kind: ExprKind::HashRef(pairs),
8963                            line,
8964                        };
8965                        Ok(Expr {
8966                            kind: ExprKind::Deref {
8967                                expr: Box::new(href),
8968                                kind: Sigil::Hash,
8969                            },
8970                            line,
8971                        })
8972                    }
8973                    tok => Err(self.syntax_err(
8974                        format!(
8975                            "Expected identifier, `$`, `{{`, or `[` after `%`, got {:?}",
8976                            tok
8977                        ),
8978                        line,
8979                    )),
8980                }
8981            }
8982
8983            tok => Err(self.syntax_err(format!("Unexpected token {:?}", tok), line)),
8984        }
8985    }
8986
8987    fn parse_named_expr(&mut self, mut name: String) -> PerlResult<Expr> {
8988        let line = self.peek_line();
8989        self.advance(); // consume the ident
8990        while self.eat(&Token::PackageSep) {
8991            match self.advance() {
8992                (Token::Ident(part), _) => {
8993                    name = format!("{}::{}", name, part);
8994                }
8995                (tok, err_line) => {
8996                    return Err(self.syntax_err(
8997                        format!("Expected identifier after `::`, got {:?}", tok),
8998                        err_line,
8999                    ));
9000                }
9001            }
9002        }
9003
9004        // Fat-arrow auto-quoting: ANY bareword (including keywords/builtins)
9005        // before `=>` is treated as a string key, matching Perl 5 semantics.
9006        // e.g. `(print => 1, pr => "x", sort => 3)` are all valid hash pairs.
9007        // Stryke exception: topic-slot barewords (`_`, `_<`, `_0`, `_0<`, …) are
9008        // scalar references to the topic / positional / outer-topic chain — they
9009        // must evaluate as the topic value, not the literal name.
9010        if matches!(self.peek(), Token::FatArrow) && !Self::is_underscore_topic_slot(&name) {
9011            return Ok(Expr {
9012                kind: ExprKind::String(name),
9013                line,
9014            });
9015        }
9016
9017        if crate::compat_mode() {
9018            if let Some(ext) = Self::stryke_extension_name(&name) {
9019                if !self.declared_subs.contains(&name) {
9020                    return Err(self.syntax_err(
9021                        format!("`{ext}` is a stryke extension (disabled by --compat)"),
9022                        line,
9023                    ));
9024                }
9025            }
9026        }
9027
9028        // `CORE::length(...)` etc. — strip the explicit core-dispatch prefix so
9029        // the keyword arms below match the bare name and produce the same
9030        // `ExprKind::Length` / `ExprKind::Print` / etc. as the unprefixed form.
9031        // Matches Perl 5's `CORE::` namespace, which routes back to the
9032        // built-in implementation regardless of any same-named user sub.
9033        // (PARITY-011)
9034        if let Some(rest) = name.strip_prefix("CORE::") {
9035            name = rest.to_string();
9036        }
9037
9038        match name.as_str() {
9039            "__FILE__" => Ok(Expr {
9040                kind: ExprKind::MagicConst(MagicConstKind::File),
9041                line,
9042            }),
9043            "__LINE__" => Ok(Expr {
9044                kind: ExprKind::MagicConst(MagicConstKind::Line),
9045                line,
9046            }),
9047            "__SUB__" => Ok(Expr {
9048                kind: ExprKind::MagicConst(MagicConstKind::Sub),
9049                line,
9050            }),
9051            "stdin" => Ok(Expr {
9052                kind: ExprKind::FuncCall {
9053                    name: "stdin".into(),
9054                    args: vec![],
9055                },
9056                line,
9057            }),
9058            "range" => {
9059                let args = self.parse_builtin_args()?;
9060                Ok(Expr {
9061                    kind: ExprKind::FuncCall {
9062                        name: "range".into(),
9063                        args,
9064                    },
9065                    line,
9066                })
9067            }
9068            "print" | "pr" => self.parse_print_like(|h, a| ExprKind::Print { handle: h, args: a }),
9069            "say" => {
9070                if crate::no_interop_mode() {
9071                    return Err(
9072                        self.syntax_err("stryke uses `p` instead of `say` (--no-interop)", line)
9073                    );
9074                }
9075                self.parse_print_like(|h, a| ExprKind::Say { handle: h, args: a })
9076            }
9077            "p" => self.parse_print_like(|h, a| ExprKind::Say { handle: h, args: a }),
9078            "printf" => self.parse_print_like(|h, a| ExprKind::Printf { handle: h, args: a }),
9079            "die" => {
9080                let args = self.parse_list_until_terminator()?;
9081                Ok(Expr {
9082                    kind: ExprKind::Die(args),
9083                    line,
9084                })
9085            }
9086            "warn" => {
9087                let args = self.parse_list_until_terminator()?;
9088                Ok(Expr {
9089                    kind: ExprKind::Warn(args),
9090                    line,
9091                })
9092            }
9093            // `croak` / `confess` — `Carp` builtins available without `use Carp`
9094            // (matches the doc claim in `lsp.rs:1243`). For now both desugar to
9095            // `die` — TODO: croak should report caller's file/line, confess
9096            // should append a full stack trace.
9097            "croak" | "confess" => {
9098                let args = self.parse_list_until_terminator()?;
9099                Ok(Expr {
9100                    kind: ExprKind::Die(args),
9101                    line,
9102                })
9103            }
9104            // `carp` / `cluck` — `Carp` warning siblings of `croak`/`confess`.
9105            "carp" | "cluck" => {
9106                let args = self.parse_list_until_terminator()?;
9107                Ok(Expr {
9108                    kind: ExprKind::Warn(args),
9109                    line,
9110                })
9111            }
9112            "chomp" => {
9113                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9114                    return Ok(e);
9115                }
9116                let a = self.parse_one_arg_or_default()?;
9117                Ok(Expr {
9118                    kind: ExprKind::Chomp(Box::new(a)),
9119                    line,
9120                })
9121            }
9122            "chop" => {
9123                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9124                    return Ok(e);
9125                }
9126                let a = self.parse_one_arg_or_default()?;
9127                Ok(Expr {
9128                    kind: ExprKind::Chop(Box::new(a)),
9129                    line,
9130                })
9131            }
9132            "length" => {
9133                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9134                    return Ok(e);
9135                }
9136                let a = self.parse_one_arg_or_default()?;
9137                Ok(Expr {
9138                    kind: ExprKind::Length(Box::new(a)),
9139                    line,
9140                })
9141            }
9142            "defined" => {
9143                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9144                    return Ok(e);
9145                }
9146                // Named-unary precedence: `defined X && Y` is `(defined X) && Y`,
9147                // not `defined(X && Y)`. The default `parse_one_arg_or_default`
9148                // path is greedy (calls `parse_assign_expr_stop_at_pipe`), which
9149                // would let `&&` bind into the argument and silently make
9150                // `defined $h{k} && $h{k} > 0`-style guards always-true when the
9151                // hash element existed. `parse_named_unary_arg` stops at shift
9152                // level so logical operators stay outside.
9153                let a = if matches!(
9154                    self.peek(),
9155                    Token::Semicolon
9156                        | Token::RBrace
9157                        | Token::RParen
9158                        | Token::RBracket
9159                        | Token::Eof
9160                        | Token::Comma
9161                        | Token::FatArrow
9162                        | Token::PipeForward
9163                        | Token::Question
9164                        | Token::Colon
9165                        | Token::NumEq
9166                        | Token::NumNe
9167                        | Token::NumLt
9168                        | Token::NumGt
9169                        | Token::NumLe
9170                        | Token::NumGe
9171                        | Token::Spaceship
9172                        | Token::StrEq
9173                        | Token::StrNe
9174                        | Token::StrLt
9175                        | Token::StrGt
9176                        | Token::StrLe
9177                        | Token::StrGe
9178                        | Token::StrCmp
9179                        | Token::LogAnd
9180                        | Token::LogOr
9181                        | Token::LogNot
9182                        | Token::LogAndWord
9183                        | Token::LogOrWord
9184                        | Token::LogNotWord
9185                        | Token::DefinedOr
9186                        | Token::Range
9187                        | Token::RangeExclusive
9188                        | Token::Assign
9189                        | Token::PlusAssign
9190                        | Token::MinusAssign
9191                        | Token::MulAssign
9192                        | Token::DivAssign
9193                        | Token::ModAssign
9194                        | Token::PowAssign
9195                        | Token::DotAssign
9196                        | Token::AndAssign
9197                        | Token::OrAssign
9198                        | Token::XorAssign
9199                        | Token::DefinedOrAssign
9200                        | Token::ShiftLeftAssign
9201                        | Token::ShiftRightAssign
9202                        | Token::BitAndAssign
9203                        | Token::BitOrAssign
9204                ) {
9205                    Expr {
9206                        kind: ExprKind::ScalarVar("_".into()),
9207                        line: self.peek_line(),
9208                    }
9209                } else if matches!(self.peek(), Token::LParen)
9210                    && matches!(self.peek_at(1), Token::RParen)
9211                {
9212                    let pl = self.peek_line();
9213                    self.advance();
9214                    self.advance();
9215                    Expr {
9216                        kind: ExprKind::ScalarVar("_".into()),
9217                        line: pl,
9218                    }
9219                } else {
9220                    self.parse_named_unary_arg()?
9221                };
9222                Ok(Expr {
9223                    kind: ExprKind::Defined(Box::new(a)),
9224                    line,
9225                })
9226            }
9227            "ref" => {
9228                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9229                    return Ok(e);
9230                }
9231                let a = self.parse_one_arg_or_default()?;
9232                Ok(Expr {
9233                    kind: ExprKind::Ref(Box::new(a)),
9234                    line,
9235                })
9236            }
9237            "undef" => {
9238                // `undef $var` sets `$var` to undef — but a variable on a new line
9239                // is a separate statement (implicit semicolon), not an argument.
9240                if self.peek_line() == self.prev_line()
9241                    && matches!(
9242                        self.peek(),
9243                        Token::ScalarVar(_) | Token::ArrayVar(_) | Token::HashVar(_)
9244                    )
9245                {
9246                    let target = self.parse_primary()?;
9247                    return Ok(Expr {
9248                        kind: ExprKind::Assign {
9249                            target: Box::new(target),
9250                            value: Box::new(Expr {
9251                                kind: ExprKind::Undef,
9252                                line,
9253                            }),
9254                        },
9255                        line,
9256                    });
9257                }
9258                Ok(Expr {
9259                    kind: ExprKind::Undef,
9260                    line,
9261                })
9262            }
9263            "scalar" => {
9264                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9265                    return Ok(e);
9266                }
9267                if crate::no_interop_mode() {
9268                    return Err(self.syntax_err(
9269                        "stryke uses `len` (also `cnt` / `count`) instead of `scalar` (--no-interop)",
9270                        line,
9271                    ));
9272                }
9273                let a = self.parse_one_arg_or_default()?;
9274                Ok(Expr {
9275                    kind: ExprKind::ScalarContext(Box::new(a)),
9276                    line,
9277                })
9278            }
9279            "abs" => {
9280                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9281                    return Ok(e);
9282                }
9283                let a = self.parse_one_arg_or_default()?;
9284                Ok(Expr {
9285                    kind: ExprKind::Abs(Box::new(a)),
9286                    line,
9287                })
9288            }
9289            // stryke unary numeric extensions — treat like `abs` so a bare
9290            // identifier in `map { inc }` / `for (…) { p inc }` becomes a
9291            // call with implicit `$_` rather than falling through to the
9292            // generic `Bareword` arm (which stringifies to `"inc"`).
9293            "inc" | "dec" => {
9294                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9295                    return Ok(e);
9296                }
9297                let a = self.parse_one_arg_or_default()?;
9298                Ok(Expr {
9299                    kind: ExprKind::FuncCall {
9300                        name,
9301                        args: vec![a],
9302                    },
9303                    line,
9304                })
9305            }
9306            "int" => {
9307                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9308                    return Ok(e);
9309                }
9310                let a = self.parse_one_arg_or_default()?;
9311                Ok(Expr {
9312                    kind: ExprKind::Int(Box::new(a)),
9313                    line,
9314                })
9315            }
9316            "sqrt" => {
9317                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9318                    return Ok(e);
9319                }
9320                let a = self.parse_one_arg_or_default()?;
9321                Ok(Expr {
9322                    kind: ExprKind::Sqrt(Box::new(a)),
9323                    line,
9324                })
9325            }
9326            "sin" => {
9327                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9328                    return Ok(e);
9329                }
9330                let a = self.parse_one_arg_or_default()?;
9331                Ok(Expr {
9332                    kind: ExprKind::Sin(Box::new(a)),
9333                    line,
9334                })
9335            }
9336            "cos" => {
9337                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9338                    return Ok(e);
9339                }
9340                let a = self.parse_one_arg_or_default()?;
9341                Ok(Expr {
9342                    kind: ExprKind::Cos(Box::new(a)),
9343                    line,
9344                })
9345            }
9346            "atan2" => {
9347                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9348                    return Ok(e);
9349                }
9350                let args = self.parse_builtin_args()?;
9351                if args.len() != 2 {
9352                    return Err(self.syntax_err("atan2 requires two arguments", line));
9353                }
9354                Ok(Expr {
9355                    kind: ExprKind::Atan2 {
9356                        y: Box::new(args[0].clone()),
9357                        x: Box::new(args[1].clone()),
9358                    },
9359                    line,
9360                })
9361            }
9362            "exp" => {
9363                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9364                    return Ok(e);
9365                }
9366                let a = self.parse_one_arg_or_default()?;
9367                Ok(Expr {
9368                    kind: ExprKind::Exp(Box::new(a)),
9369                    line,
9370                })
9371            }
9372            "log" => {
9373                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9374                    return Ok(e);
9375                }
9376                let a = self.parse_one_arg_or_default()?;
9377                Ok(Expr {
9378                    kind: ExprKind::Log(Box::new(a)),
9379                    line,
9380                })
9381            }
9382            "input" => {
9383                let args = if matches!(
9384                    self.peek(),
9385                    Token::Semicolon
9386                        | Token::RBrace
9387                        | Token::RParen
9388                        | Token::Eof
9389                        | Token::Comma
9390                        | Token::PipeForward
9391                ) {
9392                    vec![]
9393                } else if matches!(self.peek(), Token::LParen) {
9394                    self.advance();
9395                    if matches!(self.peek(), Token::RParen) {
9396                        self.advance();
9397                        vec![]
9398                    } else {
9399                        let a = self.parse_expression()?;
9400                        self.expect(&Token::RParen)?;
9401                        vec![a]
9402                    }
9403                } else {
9404                    let a = self.parse_one_arg()?;
9405                    vec![a]
9406                };
9407                Ok(Expr {
9408                    kind: ExprKind::FuncCall {
9409                        name: "input".to_string(),
9410                        args,
9411                    },
9412                    line,
9413                })
9414            }
9415            "rand" => {
9416                if matches!(
9417                    self.peek(),
9418                    Token::Semicolon
9419                        | Token::RBrace
9420                        | Token::RParen
9421                        | Token::Eof
9422                        | Token::Comma
9423                        | Token::PipeForward
9424                ) {
9425                    Ok(Expr {
9426                        kind: ExprKind::Rand(None),
9427                        line,
9428                    })
9429                } else if matches!(self.peek(), Token::LParen) {
9430                    self.advance();
9431                    if matches!(self.peek(), Token::RParen) {
9432                        self.advance();
9433                        Ok(Expr {
9434                            kind: ExprKind::Rand(None),
9435                            line,
9436                        })
9437                    } else {
9438                        let a = self.parse_expression()?;
9439                        self.expect(&Token::RParen)?;
9440                        Ok(Expr {
9441                            kind: ExprKind::Rand(Some(Box::new(a))),
9442                            line,
9443                        })
9444                    }
9445                } else {
9446                    let a = self.parse_one_arg()?;
9447                    Ok(Expr {
9448                        kind: ExprKind::Rand(Some(Box::new(a))),
9449                        line,
9450                    })
9451                }
9452            }
9453            "srand" => {
9454                if matches!(
9455                    self.peek(),
9456                    Token::Semicolon
9457                        | Token::RBrace
9458                        | Token::RParen
9459                        | Token::Eof
9460                        | Token::Comma
9461                        | Token::PipeForward
9462                ) {
9463                    Ok(Expr {
9464                        kind: ExprKind::Srand(None),
9465                        line,
9466                    })
9467                } else if matches!(self.peek(), Token::LParen) {
9468                    self.advance();
9469                    if matches!(self.peek(), Token::RParen) {
9470                        self.advance();
9471                        Ok(Expr {
9472                            kind: ExprKind::Srand(None),
9473                            line,
9474                        })
9475                    } else {
9476                        let a = self.parse_expression()?;
9477                        self.expect(&Token::RParen)?;
9478                        Ok(Expr {
9479                            kind: ExprKind::Srand(Some(Box::new(a))),
9480                            line,
9481                        })
9482                    }
9483                } else {
9484                    let a = self.parse_one_arg()?;
9485                    Ok(Expr {
9486                        kind: ExprKind::Srand(Some(Box::new(a))),
9487                        line,
9488                    })
9489                }
9490            }
9491            "hex" => {
9492                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9493                    return Ok(e);
9494                }
9495                let a = self.parse_one_arg_or_default()?;
9496                Ok(Expr {
9497                    kind: ExprKind::Hex(Box::new(a)),
9498                    line,
9499                })
9500            }
9501            "oct" => {
9502                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9503                    return Ok(e);
9504                }
9505                let a = self.parse_one_arg_or_default()?;
9506                Ok(Expr {
9507                    kind: ExprKind::Oct(Box::new(a)),
9508                    line,
9509                })
9510            }
9511            "chr" => {
9512                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9513                    return Ok(e);
9514                }
9515                let a = self.parse_one_arg_or_default()?;
9516                Ok(Expr {
9517                    kind: ExprKind::Chr(Box::new(a)),
9518                    line,
9519                })
9520            }
9521            "ord" => {
9522                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9523                    return Ok(e);
9524                }
9525                let a = self.parse_one_arg_or_default()?;
9526                Ok(Expr {
9527                    kind: ExprKind::Ord(Box::new(a)),
9528                    line,
9529                })
9530            }
9531            "lc" => {
9532                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9533                    return Ok(e);
9534                }
9535                let a = self.parse_one_arg_or_default()?;
9536                Ok(Expr {
9537                    kind: ExprKind::Lc(Box::new(a)),
9538                    line,
9539                })
9540            }
9541            "uc" => {
9542                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9543                    return Ok(e);
9544                }
9545                let a = self.parse_one_arg_or_default()?;
9546                Ok(Expr {
9547                    kind: ExprKind::Uc(Box::new(a)),
9548                    line,
9549                })
9550            }
9551            "lcfirst" => {
9552                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9553                    return Ok(e);
9554                }
9555                let a = self.parse_one_arg_or_default()?;
9556                Ok(Expr {
9557                    kind: ExprKind::Lcfirst(Box::new(a)),
9558                    line,
9559                })
9560            }
9561            "ucfirst" => {
9562                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9563                    return Ok(e);
9564                }
9565                let a = self.parse_one_arg_or_default()?;
9566                Ok(Expr {
9567                    kind: ExprKind::Ucfirst(Box::new(a)),
9568                    line,
9569                })
9570            }
9571            "fc" => {
9572                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9573                    return Ok(e);
9574                }
9575                let a = self.parse_one_arg_or_default()?;
9576                Ok(Expr {
9577                    kind: ExprKind::Fc(Box::new(a)),
9578                    line,
9579                })
9580            }
9581            "crypt" => {
9582                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9583                    return Ok(e);
9584                }
9585                let args = self.parse_builtin_args()?;
9586                if args.len() != 2 {
9587                    return Err(self.syntax_err("crypt requires two arguments", line));
9588                }
9589                Ok(Expr {
9590                    kind: ExprKind::Crypt {
9591                        plaintext: Box::new(args[0].clone()),
9592                        salt: Box::new(args[1].clone()),
9593                    },
9594                    line,
9595                })
9596            }
9597            "pos" => {
9598                if matches!(
9599                    self.peek(),
9600                    Token::Semicolon
9601                        | Token::RBrace
9602                        | Token::RParen
9603                        | Token::Eof
9604                        | Token::Comma
9605                        | Token::PipeForward
9606                ) {
9607                    Ok(Expr {
9608                        kind: ExprKind::Pos(None),
9609                        line,
9610                    })
9611                } else if matches!(self.peek(), Token::Assign) {
9612                    // Perl: `pos = EXPR` is `pos($_) = EXPR` (Text::Balanced `_eb_delims`).
9613                    self.advance();
9614                    let rhs = self.parse_assign_expr()?;
9615                    Ok(Expr {
9616                        kind: ExprKind::Assign {
9617                            target: Box::new(Expr {
9618                                kind: ExprKind::Pos(Some(Box::new(Expr {
9619                                    kind: ExprKind::ScalarVar("_".into()),
9620                                    line,
9621                                }))),
9622                                line,
9623                            }),
9624                            value: Box::new(rhs),
9625                        },
9626                        line,
9627                    })
9628                } else if matches!(self.peek(), Token::LParen) {
9629                    self.advance();
9630                    if matches!(self.peek(), Token::RParen) {
9631                        self.advance();
9632                        Ok(Expr {
9633                            kind: ExprKind::Pos(None),
9634                            line,
9635                        })
9636                    } else {
9637                        let a = self.parse_expression()?;
9638                        self.expect(&Token::RParen)?;
9639                        Ok(Expr {
9640                            kind: ExprKind::Pos(Some(Box::new(a))),
9641                            line,
9642                        })
9643                    }
9644                } else {
9645                    let saved = self.pos;
9646                    let subj = self.parse_unary()?;
9647                    if matches!(self.peek(), Token::Assign) {
9648                        self.advance();
9649                        let rhs = self.parse_assign_expr()?;
9650                        Ok(Expr {
9651                            kind: ExprKind::Assign {
9652                                target: Box::new(Expr {
9653                                    kind: ExprKind::Pos(Some(Box::new(subj))),
9654                                    line,
9655                                }),
9656                                value: Box::new(rhs),
9657                            },
9658                            line,
9659                        })
9660                    } else {
9661                        self.pos = saved;
9662                        let a = self.parse_one_arg()?;
9663                        Ok(Expr {
9664                            kind: ExprKind::Pos(Some(Box::new(a))),
9665                            line,
9666                        })
9667                    }
9668                }
9669            }
9670            "study" => {
9671                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9672                    return Ok(e);
9673                }
9674                let a = self.parse_one_arg_or_default()?;
9675                Ok(Expr {
9676                    kind: ExprKind::Study(Box::new(a)),
9677                    line,
9678                })
9679            }
9680            "push" => {
9681                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9682                    return Ok(e);
9683                }
9684                let args = self.parse_builtin_args()?;
9685                let (first, rest) = args
9686                    .split_first()
9687                    .ok_or_else(|| self.syntax_err("push requires arguments", line))?;
9688                Ok(Expr {
9689                    kind: ExprKind::Push {
9690                        array: Box::new(first.clone()),
9691                        values: rest.to_vec(),
9692                    },
9693                    line,
9694                })
9695            }
9696            "pop" => {
9697                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9698                    return Ok(e);
9699                }
9700                let a = self.parse_one_arg_or_argv()?;
9701                Ok(Expr {
9702                    kind: ExprKind::Pop(Box::new(a)),
9703                    line,
9704                })
9705            }
9706            "shift" => {
9707                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9708                    return Ok(e);
9709                }
9710                let a = self.parse_one_arg_or_argv()?;
9711                Ok(Expr {
9712                    kind: ExprKind::Shift(Box::new(a)),
9713                    line,
9714                })
9715            }
9716            "unshift" => {
9717                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9718                    return Ok(e);
9719                }
9720                let args = self.parse_builtin_args()?;
9721                let (first, rest) = args
9722                    .split_first()
9723                    .ok_or_else(|| self.syntax_err("unshift requires arguments", line))?;
9724                Ok(Expr {
9725                    kind: ExprKind::Unshift {
9726                        array: Box::new(first.clone()),
9727                        values: rest.to_vec(),
9728                    },
9729                    line,
9730                })
9731            }
9732            "splice" => {
9733                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9734                    return Ok(e);
9735                }
9736                let args = self.parse_builtin_args()?;
9737                let mut iter = args.into_iter();
9738                let array = Box::new(
9739                    iter.next()
9740                        .ok_or_else(|| self.syntax_err("splice requires arguments", line))?,
9741                );
9742                let offset = iter.next().map(Box::new);
9743                let length = iter.next().map(Box::new);
9744                let replacement: Vec<Expr> = iter.collect();
9745                Ok(Expr {
9746                    kind: ExprKind::Splice {
9747                        array,
9748                        offset,
9749                        length,
9750                        replacement,
9751                    },
9752                    line,
9753                })
9754            }
9755            // `splice_last(@a, off[, n])` is the stryke spelling of Perl's
9756            // `scalar splice(@a, off, n)` — returns the LAST removed element
9757            // (or undef if nothing was removed). Desugars to `tail(splice(...))`
9758            // so the array is still mutated in place.
9759            "splice_last" | "splice1" | "spl_last" => {
9760                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9761                    return Ok(e);
9762                }
9763                let args = self.parse_builtin_args()?;
9764                let mut iter = args.into_iter();
9765                let array = Box::new(
9766                    iter.next()
9767                        .ok_or_else(|| self.syntax_err("splice_last requires arguments", line))?,
9768                );
9769                let offset = iter.next().map(Box::new);
9770                let length = iter.next().map(Box::new);
9771                let replacement: Vec<Expr> = iter.collect();
9772                let splice_expr = Expr {
9773                    kind: ExprKind::Splice {
9774                        array,
9775                        offset,
9776                        length,
9777                        replacement,
9778                    },
9779                    line,
9780                };
9781                Ok(Expr {
9782                    kind: ExprKind::FuncCall {
9783                        name: "tail".to_string(),
9784                        args: vec![splice_expr],
9785                    },
9786                    line,
9787                })
9788            }
9789            "delete" => {
9790                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9791                    return Ok(e);
9792                }
9793                let a = self.parse_postfix()?;
9794                Ok(Expr {
9795                    kind: ExprKind::Delete(Box::new(a)),
9796                    line,
9797                })
9798            }
9799            "exists" => {
9800                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9801                    return Ok(e);
9802                }
9803                let a = self.parse_postfix()?;
9804                Ok(Expr {
9805                    kind: ExprKind::Exists(Box::new(a)),
9806                    line,
9807                })
9808            }
9809            "keys" => {
9810                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9811                    return Ok(e);
9812                }
9813                let a = self.parse_one_arg_or_default()?;
9814                Ok(Expr {
9815                    kind: ExprKind::Keys(Box::new(a)),
9816                    line,
9817                })
9818            }
9819            "values" => {
9820                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9821                    return Ok(e);
9822                }
9823                let a = self.parse_one_arg_or_default()?;
9824                Ok(Expr {
9825                    kind: ExprKind::Values(Box::new(a)),
9826                    line,
9827                })
9828            }
9829            "each" => {
9830                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9831                    return Ok(e);
9832                }
9833                let a = self.parse_one_arg_or_default()?;
9834                Ok(Expr {
9835                    kind: ExprKind::Each(Box::new(a)),
9836                    line,
9837                })
9838            }
9839            "fore" | "e" | "ep" => {
9840                // `fore { BLOCK } LIST` / `ep` — forEach expression (pipe-forward friendly)
9841                if matches!(self.peek(), Token::LBrace) {
9842                    let (block, list) = self.parse_block_list()?;
9843                    Ok(Expr {
9844                        kind: ExprKind::ForEachExpr {
9845                            block,
9846                            list: Box::new(list),
9847                        },
9848                        line,
9849                    })
9850                } else if self.in_pipe_rhs() {
9851                    // `|> ep` — bare ep at end of pipe: default to `say $_`
9852                    // `|> fore say` / `|> e say` — blockless pipe form: wrap EXPR into a synthetic block
9853                    let is_terminal = matches!(
9854                        self.peek(),
9855                        Token::Semicolon
9856                            | Token::RParen
9857                            | Token::Eof
9858                            | Token::PipeForward
9859                            | Token::RBrace
9860                    );
9861                    let block = if name == "ep" && is_terminal {
9862                        vec![Statement {
9863                            label: None,
9864                            kind: StmtKind::Expression(Expr {
9865                                kind: ExprKind::Say {
9866                                    handle: None,
9867                                    args: vec![Expr {
9868                                        kind: ExprKind::ScalarVar("_".into()),
9869                                        line,
9870                                    }],
9871                                },
9872                                line,
9873                            }),
9874                            line,
9875                        }]
9876                    } else {
9877                        let expr = self.parse_assign_expr_stop_at_pipe()?;
9878                        let expr = Self::lift_bareword_to_topic_call(expr);
9879                        vec![Statement {
9880                            label: None,
9881                            kind: StmtKind::Expression(expr),
9882                            line,
9883                        }]
9884                    };
9885                    let list = self.pipe_placeholder_list(line);
9886                    Ok(Expr {
9887                        kind: ExprKind::ForEachExpr {
9888                            block,
9889                            list: Box::new(list),
9890                        },
9891                        line,
9892                    })
9893                } else {
9894                    // Two surface forms share this branch:
9895                    //   `fore EXPR, LIST` — comma form (explicit per-item EXPR + list)
9896                    //   `ep LIST`         — list-only form: print each item with `say $_`
9897                    // We disambiguate by peeking after the first parsed expression:
9898                    // if the next token is a comma we're in the EXPR-then-LIST form;
9899                    // otherwise the first parse *was* the LIST and we default the
9900                    // block to `say $_` (only for `ep` — `fore`/`e` keep their
9901                    // explicit-expression contract).
9902                    let expr = self.parse_assign_expr()?;
9903                    let expr = Self::lift_bareword_to_topic_call(expr);
9904                    if !matches!(self.peek(), Token::Comma) && name == "ep" {
9905                        let block = vec![Statement {
9906                            label: None,
9907                            kind: StmtKind::Expression(Expr {
9908                                kind: ExprKind::Say {
9909                                    handle: None,
9910                                    args: vec![Expr {
9911                                        kind: ExprKind::ScalarVar("_".into()),
9912                                        line,
9913                                    }],
9914                                },
9915                                line,
9916                            }),
9917                            line,
9918                        }];
9919                        return Ok(Expr {
9920                            kind: ExprKind::ForEachExpr {
9921                                block,
9922                                list: Box::new(expr),
9923                            },
9924                            line,
9925                        });
9926                    }
9927                    self.expect(&Token::Comma)?;
9928                    let list_parts = self.parse_list_until_terminator()?;
9929                    let list_expr = if list_parts.len() == 1 {
9930                        list_parts.into_iter().next().unwrap()
9931                    } else {
9932                        Expr {
9933                            kind: ExprKind::List(list_parts),
9934                            line,
9935                        }
9936                    };
9937                    let block = vec![Statement {
9938                        label: None,
9939                        kind: StmtKind::Expression(expr),
9940                        line,
9941                    }];
9942                    Ok(Expr {
9943                        kind: ExprKind::ForEachExpr {
9944                            block,
9945                            list: Box::new(list_expr),
9946                        },
9947                        line,
9948                    })
9949                }
9950            }
9951            "rev" => {
9952                // `rev` — context-aware reverse: string in scalar, list in list context.
9953                // List-operator precedence (so `rev 1..3` parses as `rev(1..3)`, not
9954                // `(rev 1)..3`). Defaults to $_ when no argument given.
9955                // Only use pipe placeholder when directly in pipe RHS (not inside a block).
9956                // RBrace means we're inside a block like `map { rev }` - use $_ default.
9957                let a = if self.in_pipe_rhs()
9958                    && matches!(
9959                        self.peek(),
9960                        Token::Semicolon | Token::RParen | Token::Eof | Token::PipeForward
9961                    ) {
9962                    self.pipe_placeholder_list(line)
9963                } else if matches!(
9964                    self.peek(),
9965                    Token::Semicolon
9966                        | Token::RBrace
9967                        | Token::RParen
9968                        | Token::RBracket
9969                        | Token::Eof
9970                        | Token::Comma
9971                        | Token::FatArrow
9972                        | Token::PipeForward
9973                ) {
9974                    Expr {
9975                        kind: ExprKind::ScalarVar("_".into()),
9976                        line: self.peek_line(),
9977                    }
9978                } else if matches!(self.peek(), Token::LParen)
9979                    && matches!(self.peek_at(1), Token::RParen)
9980                {
9981                    // `rev()` — empty parens default to `$_` (matches Perl's
9982                    // `length()` / `uc()` etc. and the `|> rev()` pipe form).
9983                    let pl = self.peek_line();
9984                    self.advance(); // (
9985                    self.advance(); // )
9986                    Expr {
9987                        kind: ExprKind::ScalarVar("_".into()),
9988                        line: pl,
9989                    }
9990                } else {
9991                    self.parse_one_arg()?
9992                };
9993                Ok(Expr {
9994                    kind: ExprKind::Rev(Box::new(a)),
9995                    line,
9996                })
9997            }
9998            "reverse" => {
9999                if crate::no_interop_mode() {
10000                    return Err(self.syntax_err(
10001                        "stryke uses `rev` instead of `reverse` (--no-interop)",
10002                        line,
10003                    ));
10004                }
10005                // On the RHS of `|>`, the operand is supplied by the piped LHS.
10006                let a = if self.in_pipe_rhs()
10007                    && matches!(
10008                        self.peek(),
10009                        Token::Semicolon
10010                            | Token::RBrace
10011                            | Token::RParen
10012                            | Token::Eof
10013                            | Token::PipeForward
10014                    ) {
10015                    self.pipe_placeholder_list(line)
10016                } else {
10017                    self.parse_one_arg()?
10018                };
10019                Ok(Expr {
10020                    kind: ExprKind::ReverseExpr(Box::new(a)),
10021                    line,
10022                })
10023            }
10024            "reversed" | "rv" => {
10025                // On the RHS of `|>`, the operand is supplied by the piped LHS.
10026                let a = if self.in_pipe_rhs()
10027                    && matches!(
10028                        self.peek(),
10029                        Token::Semicolon
10030                            | Token::RBrace
10031                            | Token::RParen
10032                            | Token::Eof
10033                            | Token::PipeForward
10034                    ) {
10035                    self.pipe_placeholder_list(line)
10036                } else {
10037                    self.parse_one_arg()?
10038                };
10039                Ok(Expr {
10040                    kind: ExprKind::Rev(Box::new(a)),
10041                    line,
10042                })
10043            }
10044            "join" => {
10045                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10046                    return Ok(e);
10047                }
10048                let args = self.parse_builtin_args()?;
10049                if args.is_empty() {
10050                    return Err(self.syntax_err("join requires separator and list", line));
10051                }
10052                // `@list |> join(",")` — list slot is filled by the piped LHS.
10053                if args.len() < 2 && !self.in_pipe_rhs() {
10054                    return Err(self.syntax_err("join requires separator and list", line));
10055                }
10056                Ok(Expr {
10057                    kind: ExprKind::JoinExpr {
10058                        separator: Box::new(args[0].clone()),
10059                        list: Box::new(Expr {
10060                            kind: ExprKind::List(args[1..].to_vec()),
10061                            line,
10062                        }),
10063                    },
10064                    line,
10065                })
10066            }
10067            "split" => {
10068                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10069                    return Ok(e);
10070                }
10071                let args = self.parse_builtin_args()?;
10072                let pattern = args.first().cloned().unwrap_or(Expr {
10073                    kind: ExprKind::String(" ".into()),
10074                    line,
10075                });
10076                let string = args.get(1).cloned().unwrap_or(Expr {
10077                    kind: ExprKind::ScalarVar("_".into()),
10078                    line,
10079                });
10080                let limit = args.get(2).cloned().map(Box::new);
10081                Ok(Expr {
10082                    kind: ExprKind::SplitExpr {
10083                        pattern: Box::new(pattern),
10084                        string: Box::new(string),
10085                        limit,
10086                    },
10087                    line,
10088                })
10089            }
10090            "substr" => {
10091                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10092                    return Ok(e);
10093                }
10094                let args = self.parse_builtin_args()?;
10095                Ok(Expr {
10096                    kind: ExprKind::Substr {
10097                        string: Box::new(args[0].clone()),
10098                        offset: Box::new(args[1].clone()),
10099                        length: args.get(2).cloned().map(Box::new),
10100                        replacement: args.get(3).cloned().map(Box::new),
10101                    },
10102                    line,
10103                })
10104            }
10105            "index" => {
10106                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10107                    return Ok(e);
10108                }
10109                let args = self.parse_builtin_args()?;
10110                Ok(Expr {
10111                    kind: ExprKind::Index {
10112                        string: Box::new(args[0].clone()),
10113                        substr: Box::new(args[1].clone()),
10114                        position: args.get(2).cloned().map(Box::new),
10115                    },
10116                    line,
10117                })
10118            }
10119            "rindex" => {
10120                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10121                    return Ok(e);
10122                }
10123                let args = self.parse_builtin_args()?;
10124                Ok(Expr {
10125                    kind: ExprKind::Rindex {
10126                        string: Box::new(args[0].clone()),
10127                        substr: Box::new(args[1].clone()),
10128                        position: args.get(2).cloned().map(Box::new),
10129                    },
10130                    line,
10131                })
10132            }
10133            "sprintf" => {
10134                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10135                    return Ok(e);
10136                }
10137                let args = self.parse_builtin_args()?;
10138                let (first, rest) = args
10139                    .split_first()
10140                    .ok_or_else(|| self.syntax_err("sprintf requires format", line))?;
10141                Ok(Expr {
10142                    kind: ExprKind::Sprintf {
10143                        format: Box::new(first.clone()),
10144                        args: rest.to_vec(),
10145                    },
10146                    line,
10147                })
10148            }
10149            "map" | "flat_map" | "maps" | "flat_maps" => {
10150                let flatten_array_refs = matches!(name.as_str(), "flat_map" | "flat_maps");
10151                let stream = matches!(name.as_str(), "maps" | "flat_maps");
10152                if matches!(self.peek(), Token::LBrace) {
10153                    let (block, list) = self.parse_block_list()?;
10154                    Ok(Expr {
10155                        kind: ExprKind::MapExpr {
10156                            block,
10157                            list: Box::new(list),
10158                            flatten_array_refs,
10159                            stream,
10160                        },
10161                        line,
10162                    })
10163                } else {
10164                    let expr = self.parse_assign_expr_stop_at_pipe()?;
10165                    // Lift bareword to FuncCall($_) so `map sha512, @list`
10166                    // calls sha512($_) for each element instead of stringifying.
10167                    let expr = Self::lift_bareword_to_topic_call(expr);
10168                    let list_expr = if self.pipe_supplies_slurped_list_operand() {
10169                        self.pipe_placeholder_list(line)
10170                    } else {
10171                        self.expect(&Token::Comma)?;
10172                        let list_parts = self.parse_list_until_terminator()?;
10173                        if list_parts.len() == 1 {
10174                            list_parts.into_iter().next().unwrap()
10175                        } else {
10176                            Expr {
10177                                kind: ExprKind::List(list_parts),
10178                                line,
10179                            }
10180                        }
10181                    };
10182                    Ok(Expr {
10183                        kind: ExprKind::MapExprComma {
10184                            expr: Box::new(expr),
10185                            list: Box::new(list_expr),
10186                            flatten_array_refs,
10187                            stream,
10188                        },
10189                        line,
10190                    })
10191                }
10192            }
10193            "cond" => {
10194                if crate::compat_mode() {
10195                    return Err(self
10196                        .syntax_err("`cond` is a stryke extension (disabled by --compat)", line));
10197                }
10198                self.parse_cond_expr(line)
10199            }
10200            "match" => {
10201                if crate::compat_mode() {
10202                    return Err(self.syntax_err(
10203                        "algebraic `match` is a stryke extension (disabled by --compat)",
10204                        line,
10205                    ));
10206                }
10207                self.parse_algebraic_match_expr(line)
10208            }
10209            "grep" | "greps" | "filter" | "fi" | "find_all" => {
10210                let keyword = match name.as_str() {
10211                    "grep" => crate::ast::GrepBuiltinKeyword::Grep,
10212                    "greps" => crate::ast::GrepBuiltinKeyword::Greps,
10213                    "filter" | "fi" => crate::ast::GrepBuiltinKeyword::Filter,
10214                    "find_all" => crate::ast::GrepBuiltinKeyword::FindAll,
10215                    _ => unreachable!(),
10216                };
10217                if matches!(self.peek(), Token::LBrace) {
10218                    let (block, list) = self.parse_block_list()?;
10219                    Ok(Expr {
10220                        kind: ExprKind::GrepExpr {
10221                            block,
10222                            list: Box::new(list),
10223                            keyword,
10224                        },
10225                        line,
10226                    })
10227                } else {
10228                    let expr = self.parse_assign_expr_stop_at_pipe()?;
10229                    if self.pipe_supplies_slurped_list_operand() {
10230                        // Pipe-RHS blockless form: `|> grep EXPR`
10231                        // For literals, desugar to `$_ eq/== EXPR` so
10232                        // `|> filter 't'` keeps only elements equal to 't'.
10233                        // For regexes, desugar to `$_ =~ EXPR`.
10234                        let list = self.pipe_placeholder_list(line);
10235                        let topic = Expr {
10236                            kind: ExprKind::ScalarVar("_".into()),
10237                            line,
10238                        };
10239                        let test = match &expr.kind {
10240                            ExprKind::Integer(_) | ExprKind::Float(_) => Expr {
10241                                kind: ExprKind::BinOp {
10242                                    op: BinOp::NumEq,
10243                                    left: Box::new(topic),
10244                                    right: Box::new(expr),
10245                                },
10246                                line,
10247                            },
10248                            ExprKind::String(_) | ExprKind::InterpolatedString(_) => Expr {
10249                                kind: ExprKind::BinOp {
10250                                    op: BinOp::StrEq,
10251                                    left: Box::new(topic),
10252                                    right: Box::new(expr),
10253                                },
10254                                line,
10255                            },
10256                            ExprKind::Regex { .. } => Expr {
10257                                kind: ExprKind::BinOp {
10258                                    op: BinOp::BindMatch,
10259                                    left: Box::new(topic),
10260                                    right: Box::new(expr),
10261                                },
10262                                line,
10263                            },
10264                            _ => {
10265                                // Non-literal (e.g. `defined`, scalar coderef var,
10266                                // hash slot): lift barewords to topic-call, then
10267                                // route through GrepExprComma so the runtime
10268                                // coderef-dispatch in Op::GrepWithExpr handles
10269                                // both truthiness AND coderef-call uniformly.
10270                                let expr = Self::lift_bareword_to_topic_call(expr);
10271                                return Ok(Expr {
10272                                    kind: ExprKind::GrepExprComma {
10273                                        expr: Box::new(expr),
10274                                        list: Box::new(list),
10275                                        keyword,
10276                                    },
10277                                    line,
10278                                });
10279                            }
10280                        };
10281                        let block = vec![Statement {
10282                            label: None,
10283                            kind: StmtKind::Expression(test),
10284                            line,
10285                        }];
10286                        Ok(Expr {
10287                            kind: ExprKind::GrepExpr {
10288                                block,
10289                                list: Box::new(list),
10290                                keyword,
10291                            },
10292                            line,
10293                        })
10294                    } else {
10295                        let expr = Self::lift_bareword_to_topic_call(expr);
10296                        self.expect(&Token::Comma)?;
10297                        let list_parts = self.parse_list_until_terminator()?;
10298                        let list_expr = if list_parts.len() == 1 {
10299                            list_parts.into_iter().next().unwrap()
10300                        } else {
10301                            Expr {
10302                                kind: ExprKind::List(list_parts),
10303                                line,
10304                            }
10305                        };
10306                        Ok(Expr {
10307                            kind: ExprKind::GrepExprComma {
10308                                expr: Box::new(expr),
10309                                list: Box::new(list_expr),
10310                                keyword,
10311                            },
10312                            line,
10313                        })
10314                    }
10315                }
10316            }
10317            "sort" => {
10318                use crate::ast::SortComparator;
10319                if matches!(self.peek(), Token::LBrace) {
10320                    let block = self.parse_block()?;
10321                    let block_end_line = self.prev_line();
10322                    let _ = self.eat(&Token::Comma);
10323                    let list = if self.in_pipe_rhs()
10324                        && (matches!(
10325                            self.peek(),
10326                            Token::Semicolon
10327                                | Token::RBrace
10328                                | Token::RParen
10329                                | Token::Eof
10330                                | Token::PipeForward
10331                        ) || self.peek_line() > block_end_line)
10332                    {
10333                        self.pipe_placeholder_list(line)
10334                    } else {
10335                        self.parse_expression()?
10336                    };
10337                    Ok(Expr {
10338                        kind: ExprKind::SortExpr {
10339                            cmp: Some(SortComparator::Block(block)),
10340                            list: Box::new(list),
10341                        },
10342                        line,
10343                    })
10344                } else if matches!(self.peek(), Token::ScalarVar(ref v) if v == "a" || v == "b") {
10345                    // Blockless comparator: `sort $a <=> $b, @list`
10346                    let block = self.parse_block_or_bareword_cmp_block()?;
10347                    let _ = self.eat(&Token::Comma);
10348                    let list = if self.in_pipe_rhs()
10349                        && matches!(
10350                            self.peek(),
10351                            Token::Semicolon
10352                                | Token::RBrace
10353                                | Token::RParen
10354                                | Token::Eof
10355                                | Token::PipeForward
10356                        ) {
10357                        self.pipe_placeholder_list(line)
10358                    } else {
10359                        self.parse_expression()?
10360                    };
10361                    Ok(Expr {
10362                        kind: ExprKind::SortExpr {
10363                            cmp: Some(SortComparator::Block(block)),
10364                            list: Box::new(list),
10365                        },
10366                        line,
10367                    })
10368                } else if matches!(self.peek(), Token::ScalarVar(_)) {
10369                    // `sort $coderef (LIST)` — comparator is first; list often parenthesized.
10370                    // Pipe-RHS form `|> sort $coderef` uses placeholder LHS as the list.
10371                    self.suppress_indirect_paren_call =
10372                        self.suppress_indirect_paren_call.saturating_add(1);
10373                    let code = self.parse_assign_expr()?;
10374                    self.suppress_indirect_paren_call =
10375                        self.suppress_indirect_paren_call.saturating_sub(1);
10376                    let _ = self.eat(&Token::Comma);
10377                    let list = if self.in_pipe_rhs()
10378                        && matches!(
10379                            self.peek(),
10380                            Token::Semicolon
10381                                | Token::RBrace
10382                                | Token::RParen
10383                                | Token::Eof
10384                                | Token::PipeForward
10385                        ) {
10386                        self.pipe_placeholder_list(line)
10387                    } else if matches!(self.peek(), Token::LParen) {
10388                        self.advance();
10389                        let e = self.parse_expression()?;
10390                        self.expect(&Token::RParen)?;
10391                        e
10392                    } else {
10393                        self.parse_expression()?
10394                    };
10395                    Ok(Expr {
10396                        kind: ExprKind::SortExpr {
10397                            cmp: Some(SortComparator::Code(Box::new(code))),
10398                            list: Box::new(list),
10399                        },
10400                        line,
10401                    })
10402                } else if matches!(self.peek(), Token::Ident(ref name) if !Self::is_known_bareword(name))
10403                {
10404                    // Blockless comparator via bare sub name: `sort my_cmp @list`
10405                    let block = self.parse_block_or_bareword_cmp_block()?;
10406                    let _ = self.eat(&Token::Comma);
10407                    let list = if self.in_pipe_rhs()
10408                        && matches!(
10409                            self.peek(),
10410                            Token::Semicolon
10411                                | Token::RBrace
10412                                | Token::RParen
10413                                | Token::Eof
10414                                | Token::PipeForward
10415                        ) {
10416                        self.pipe_placeholder_list(line)
10417                    } else {
10418                        self.parse_expression()?
10419                    };
10420                    Ok(Expr {
10421                        kind: ExprKind::SortExpr {
10422                            cmp: Some(SortComparator::Block(block)),
10423                            list: Box::new(list),
10424                        },
10425                        line,
10426                    })
10427                } else {
10428                    // Bare `sort` with no comparator and no list: only allowed
10429                    // as the RHS of `|>`, where the list comes from the LHS.
10430                    let list = if self.in_pipe_rhs()
10431                        && matches!(
10432                            self.peek(),
10433                            Token::Semicolon
10434                                | Token::RBrace
10435                                | Token::RParen
10436                                | Token::Eof
10437                                | Token::PipeForward
10438                        ) {
10439                        self.pipe_placeholder_list(line)
10440                    } else {
10441                        self.parse_expression()?
10442                    };
10443                    Ok(Expr {
10444                        kind: ExprKind::SortExpr {
10445                            cmp: None,
10446                            list: Box::new(list),
10447                        },
10448                        line,
10449                    })
10450                }
10451            }
10452            "reduce" | "fold" | "inject" => {
10453                let (block, list) = self.parse_block_list()?;
10454                Ok(Expr {
10455                    kind: ExprKind::ReduceExpr {
10456                        block,
10457                        list: Box::new(list),
10458                    },
10459                    line,
10460                })
10461            }
10462            // Parallel extensions
10463            "pmap" => {
10464                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10465                Ok(Expr {
10466                    kind: ExprKind::PMapExpr {
10467                        block,
10468                        list: Box::new(list),
10469                        progress: progress.map(Box::new),
10470                        flat_outputs: false,
10471                        on_cluster: None,
10472                        stream: false,
10473                    },
10474                    line,
10475                })
10476            }
10477            "pmap_on" => {
10478                let (cluster, block, list, progress) =
10479                    self.parse_cluster_block_then_list_optional_progress()?;
10480                Ok(Expr {
10481                    kind: ExprKind::PMapExpr {
10482                        block,
10483                        list: Box::new(list),
10484                        progress: progress.map(Box::new),
10485                        flat_outputs: false,
10486                        on_cluster: Some(Box::new(cluster)),
10487                        stream: false,
10488                    },
10489                    line,
10490                })
10491            }
10492            "pflat_map" => {
10493                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10494                Ok(Expr {
10495                    kind: ExprKind::PMapExpr {
10496                        block,
10497                        list: Box::new(list),
10498                        progress: progress.map(Box::new),
10499                        flat_outputs: true,
10500                        on_cluster: None,
10501                        stream: false,
10502                    },
10503                    line,
10504                })
10505            }
10506            "pflat_map_on" => {
10507                let (cluster, block, list, progress) =
10508                    self.parse_cluster_block_then_list_optional_progress()?;
10509                Ok(Expr {
10510                    kind: ExprKind::PMapExpr {
10511                        block,
10512                        list: Box::new(list),
10513                        progress: progress.map(Box::new),
10514                        flat_outputs: true,
10515                        on_cluster: Some(Box::new(cluster)),
10516                        stream: false,
10517                    },
10518                    line,
10519                })
10520            }
10521            "pmaps" => {
10522                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10523                Ok(Expr {
10524                    kind: ExprKind::PMapExpr {
10525                        block,
10526                        list: Box::new(list),
10527                        progress: progress.map(Box::new),
10528                        flat_outputs: false,
10529                        on_cluster: None,
10530                        stream: true,
10531                    },
10532                    line,
10533                })
10534            }
10535            "pflat_maps" => {
10536                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10537                Ok(Expr {
10538                    kind: ExprKind::PMapExpr {
10539                        block,
10540                        list: Box::new(list),
10541                        progress: progress.map(Box::new),
10542                        flat_outputs: true,
10543                        on_cluster: None,
10544                        stream: true,
10545                    },
10546                    line,
10547                })
10548            }
10549            "pgreps" => {
10550                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10551                Ok(Expr {
10552                    kind: ExprKind::PGrepExpr {
10553                        block,
10554                        list: Box::new(list),
10555                        progress: progress.map(Box::new),
10556                        stream: true,
10557                    },
10558                    line,
10559                })
10560            }
10561            "pmap_chunked" => {
10562                let chunk_size = self.parse_assign_expr()?;
10563                let block = self.parse_block_or_bareword_block()?;
10564                self.eat(&Token::Comma);
10565                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10566                Ok(Expr {
10567                    kind: ExprKind::PMapChunkedExpr {
10568                        chunk_size: Box::new(chunk_size),
10569                        block,
10570                        list: Box::new(list),
10571                        progress: progress.map(Box::new),
10572                    },
10573                    line,
10574                })
10575            }
10576            "pgrep" => {
10577                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10578                Ok(Expr {
10579                    kind: ExprKind::PGrepExpr {
10580                        block,
10581                        list: Box::new(list),
10582                        progress: progress.map(Box::new),
10583                        stream: false,
10584                    },
10585                    line,
10586                })
10587            }
10588            "pfor" => {
10589                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10590                Ok(Expr {
10591                    kind: ExprKind::PForExpr {
10592                        block,
10593                        list: Box::new(list),
10594                        progress: progress.map(Box::new),
10595                    },
10596                    line,
10597                })
10598            }
10599            "par_lines" | "par_walk" => {
10600                let args = self.parse_builtin_args()?;
10601                if args.len() < 2 {
10602                    return Err(
10603                        self.syntax_err(format!("{} requires at least two arguments", name), line)
10604                    );
10605                }
10606
10607                if name == "par_lines" {
10608                    Ok(Expr {
10609                        kind: ExprKind::ParLinesExpr {
10610                            path: Box::new(args[0].clone()),
10611                            callback: Box::new(args[1].clone()),
10612                            progress: None,
10613                        },
10614                        line,
10615                    })
10616                } else {
10617                    Ok(Expr {
10618                        kind: ExprKind::ParWalkExpr {
10619                            path: Box::new(args[0].clone()),
10620                            callback: Box::new(args[1].clone()),
10621                            progress: None,
10622                        },
10623                        line,
10624                    })
10625                }
10626            }
10627            "pwatch" | "watch" => {
10628                let args = self.parse_builtin_args()?;
10629                if args.len() < 2 {
10630                    return Err(
10631                        self.syntax_err(format!("{} requires at least two arguments", name), line)
10632                    );
10633                }
10634                Ok(Expr {
10635                    kind: ExprKind::PwatchExpr {
10636                        path: Box::new(args[0].clone()),
10637                        callback: Box::new(args[1].clone()),
10638                    },
10639                    line,
10640                })
10641            }
10642            "fan" => {
10643                // fan { BLOCK }            — no count, block body
10644                // fan COUNT { BLOCK }      — count + block body
10645                // fan EXPR;                — no count, blockless body (wrap EXPR as block)
10646                // fan COUNT EXPR;          — count + blockless body
10647                // Optional: `, progress => EXPR` or `progress => EXPR` (no comma before progress)
10648                let (count, block) = self.parse_fan_count_and_block(line)?;
10649                let progress = self.parse_fan_optional_progress("fan")?;
10650                Ok(Expr {
10651                    kind: ExprKind::FanExpr {
10652                        count,
10653                        block,
10654                        progress,
10655                        capture: false,
10656                    },
10657                    line,
10658                })
10659            }
10660            "fan_cap" => {
10661                let (count, block) = self.parse_fan_count_and_block(line)?;
10662                let progress = self.parse_fan_optional_progress("fan_cap")?;
10663                Ok(Expr {
10664                    kind: ExprKind::FanExpr {
10665                        count,
10666                        block,
10667                        progress,
10668                        capture: true,
10669                    },
10670                    line,
10671                })
10672            }
10673            "async" => {
10674                if !matches!(self.peek(), Token::LBrace) {
10675                    return Err(self.syntax_err("async must be followed by { BLOCK }", line));
10676                }
10677                let block = self.parse_block()?;
10678                Ok(Expr {
10679                    kind: ExprKind::AsyncBlock { body: block },
10680                    line,
10681                })
10682            }
10683            "spawn" => {
10684                if !matches!(self.peek(), Token::LBrace) {
10685                    return Err(self.syntax_err("spawn must be followed by { BLOCK }", line));
10686                }
10687                let block = self.parse_block()?;
10688                Ok(Expr {
10689                    kind: ExprKind::SpawnBlock { body: block },
10690                    line,
10691                })
10692            }
10693            "trace" => {
10694                if !matches!(self.peek(), Token::LBrace) {
10695                    return Err(self.syntax_err("trace must be followed by { BLOCK }", line));
10696                }
10697                let block = self.parse_block()?;
10698                Ok(Expr {
10699                    kind: ExprKind::Trace { body: block },
10700                    line,
10701                })
10702            }
10703            "timer" => {
10704                let block = self.parse_block_or_bareword_block_no_args()?;
10705                Ok(Expr {
10706                    kind: ExprKind::Timer { body: block },
10707                    line,
10708                })
10709            }
10710            "bench" => {
10711                let block = self.parse_block_or_bareword_block_no_args()?;
10712                let times = Box::new(self.parse_expression()?);
10713                Ok(Expr {
10714                    kind: ExprKind::Bench { body: block, times },
10715                    line,
10716                })
10717            }
10718            "spinner" => {
10719                // `spinner "msg" { BLOCK }` or `spinner { BLOCK }`
10720                let (message, body) = if matches!(self.peek(), Token::LBrace) {
10721                    let body = self.parse_block()?;
10722                    (
10723                        Box::new(Expr {
10724                            kind: ExprKind::String("working".to_string()),
10725                            line,
10726                        }),
10727                        body,
10728                    )
10729                } else {
10730                    let msg = self.parse_assign_expr()?;
10731                    let body = self.parse_block()?;
10732                    (Box::new(msg), body)
10733                };
10734                Ok(Expr {
10735                    kind: ExprKind::Spinner { message, body },
10736                    line,
10737                })
10738            }
10739            "thread" | "t" => {
10740                // `thread EXPR stage1 stage2 ...` — threading macro (thread-first)
10741                // `t` is a short alias for `thread`
10742                // Each stage is either:
10743                //   - `ident` — bare function call
10744                //   - `ident { block }` — function with block arg
10745                //   - `ident arg1 arg2 { block }` — function with args and optional block
10746                //   - `fn { block }` — standalone anonymous block
10747                //   - `>{ block }` — shorthand for standalone anonymous block
10748                // Desugars to: EXPR |> stage1 |> stage2 |> ...
10749                self.parse_thread_macro(line, false)
10750            }
10751            "retry" => {
10752                // `retry { BLOCK }` or `retry BAREWORD` — bareword becomes zero-arg call.
10753                // An optional comma before `times` is allowed in both forms.
10754                let body = if matches!(self.peek(), Token::LBrace) {
10755                    self.parse_block()?
10756                } else {
10757                    let bw_line = self.peek_line();
10758                    let Token::Ident(ref name) = self.peek().clone() else {
10759                        return Err(self
10760                            .syntax_err("retry: expected block or bareword function name", line));
10761                    };
10762                    let name = name.clone();
10763                    self.advance();
10764                    vec![Statement::new(
10765                        StmtKind::Expression(Expr {
10766                            kind: ExprKind::FuncCall { name, args: vec![] },
10767                            line: bw_line,
10768                        }),
10769                        bw_line,
10770                    )]
10771                };
10772                self.eat(&Token::Comma);
10773                match self.peek() {
10774                    Token::Ident(ref s) if s == "times" => {
10775                        self.advance();
10776                    }
10777                    _ => {
10778                        return Err(self.syntax_err("retry: expected `times =>` after block", line));
10779                    }
10780                }
10781                self.expect(&Token::FatArrow)?;
10782                let times = Box::new(self.parse_assign_expr()?);
10783                let mut backoff = RetryBackoff::None;
10784                if self.eat(&Token::Comma) {
10785                    match self.peek() {
10786                        Token::Ident(ref s) if s == "backoff" => {
10787                            self.advance();
10788                        }
10789                        _ => {
10790                            return Err(
10791                                self.syntax_err("retry: expected `backoff =>` after comma", line)
10792                            );
10793                        }
10794                    }
10795                    self.expect(&Token::FatArrow)?;
10796                    let Token::Ident(mode) = self.peek().clone() else {
10797                        return Err(self.syntax_err(
10798                            "retry: expected backoff mode (none, linear, exponential)",
10799                            line,
10800                        ));
10801                    };
10802                    backoff = match mode.as_str() {
10803                        "none" => RetryBackoff::None,
10804                        "linear" => RetryBackoff::Linear,
10805                        "exponential" => RetryBackoff::Exponential,
10806                        _ => {
10807                            return Err(
10808                                self.syntax_err(format!("retry: invalid backoff `{mode}`"), line)
10809                            );
10810                        }
10811                    };
10812                    self.advance();
10813                }
10814                Ok(Expr {
10815                    kind: ExprKind::RetryBlock {
10816                        body,
10817                        times,
10818                        backoff,
10819                    },
10820                    line,
10821                })
10822            }
10823            "rate_limit" => {
10824                self.expect(&Token::LParen)?;
10825                let max = Box::new(self.parse_assign_expr()?);
10826                self.expect(&Token::Comma)?;
10827                let window = Box::new(self.parse_assign_expr()?);
10828                self.expect(&Token::RParen)?;
10829                let body = self.parse_block_or_bareword_block_no_args()?;
10830                let slot = self.alloc_rate_limit_slot();
10831                Ok(Expr {
10832                    kind: ExprKind::RateLimitBlock {
10833                        slot,
10834                        max,
10835                        window,
10836                        body,
10837                    },
10838                    line,
10839                })
10840            }
10841            "every" => {
10842                // `every("500ms") { BLOCK }` or `every "500ms" BODY` — parens optional.
10843                // Body consumes `|>` (every is an infinite loop, not a pipeable source).
10844                let has_paren = self.eat(&Token::LParen);
10845                let interval = Box::new(self.parse_assign_expr()?);
10846                if has_paren {
10847                    self.expect(&Token::RParen)?;
10848                }
10849                let body = if matches!(self.peek(), Token::LBrace) {
10850                    self.parse_block()?
10851                } else {
10852                    let bline = self.peek_line();
10853                    let expr = self.parse_assign_expr()?;
10854                    vec![Statement::new(StmtKind::Expression(expr), bline)]
10855                };
10856                Ok(Expr {
10857                    kind: ExprKind::EveryBlock { interval, body },
10858                    line,
10859                })
10860            }
10861            "gen" => {
10862                if !matches!(self.peek(), Token::LBrace) {
10863                    return Err(self.syntax_err("gen must be followed by { BLOCK }", line));
10864                }
10865                let body = self.parse_block()?;
10866                Ok(Expr {
10867                    kind: ExprKind::GenBlock { body },
10868                    line,
10869                })
10870            }
10871            "yield" => {
10872                let e = self.parse_assign_expr()?;
10873                Ok(Expr {
10874                    kind: ExprKind::Yield(Box::new(e)),
10875                    line,
10876                })
10877            }
10878            "await" => {
10879                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10880                    return Ok(e);
10881                }
10882                // `await` defaults to `$_` so `map { await } @tasks` works
10883                // (Perl-style topic-defaulting unary).
10884                let a = self.parse_one_arg_or_default()?;
10885                Ok(Expr {
10886                    kind: ExprKind::Await(Box::new(a)),
10887                    line,
10888                })
10889            }
10890            "slurp" | "cat" | "c" => {
10891                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10892                    return Ok(e);
10893                }
10894                let a = self.parse_one_arg_or_default()?;
10895                Ok(Expr {
10896                    kind: ExprKind::Slurp(Box::new(a)),
10897                    line,
10898                })
10899            }
10900            "capture" => {
10901                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10902                    return Ok(e);
10903                }
10904                let a = self.parse_one_arg()?;
10905                Ok(Expr {
10906                    kind: ExprKind::Capture(Box::new(a)),
10907                    line,
10908                })
10909            }
10910            "fetch_url" => {
10911                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10912                    return Ok(e);
10913                }
10914                let a = self.parse_one_arg()?;
10915                Ok(Expr {
10916                    kind: ExprKind::FetchUrl(Box::new(a)),
10917                    line,
10918                })
10919            }
10920            "pchannel" => {
10921                let capacity = if self.eat(&Token::LParen) {
10922                    if matches!(self.peek(), Token::RParen) {
10923                        self.advance();
10924                        None
10925                    } else {
10926                        let e = self.parse_expression()?;
10927                        self.expect(&Token::RParen)?;
10928                        Some(Box::new(e))
10929                    }
10930                } else {
10931                    None
10932                };
10933                Ok(Expr {
10934                    kind: ExprKind::Pchannel { capacity },
10935                    line,
10936                })
10937            }
10938            "psort" => {
10939                if matches!(self.peek(), Token::LBrace)
10940                    || matches!(self.peek(), Token::ScalarVar(ref v) if v == "a" || v == "b")
10941                    || matches!(self.peek(), Token::Ident(ref name) if !Self::is_known_bareword(name))
10942                {
10943                    let block = self.parse_block_or_bareword_cmp_block()?;
10944                    self.eat(&Token::Comma);
10945                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10946                    Ok(Expr {
10947                        kind: ExprKind::PSortExpr {
10948                            cmp: Some(block),
10949                            list: Box::new(list),
10950                            progress: progress.map(Box::new),
10951                        },
10952                        line,
10953                    })
10954                } else {
10955                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
10956                    Ok(Expr {
10957                        kind: ExprKind::PSortExpr {
10958                            cmp: None,
10959                            list: Box::new(list),
10960                            progress: progress.map(Box::new),
10961                        },
10962                        line,
10963                    })
10964                }
10965            }
10966            "preduce" => {
10967                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
10968                Ok(Expr {
10969                    kind: ExprKind::PReduceExpr {
10970                        block,
10971                        list: Box::new(list),
10972                        progress: progress.map(Box::new),
10973                    },
10974                    line,
10975                })
10976            }
10977            "preduce_init" => {
10978                let (init, block, list, progress) =
10979                    self.parse_init_block_then_list_optional_progress()?;
10980                Ok(Expr {
10981                    kind: ExprKind::PReduceInitExpr {
10982                        init: Box::new(init),
10983                        block,
10984                        list: Box::new(list),
10985                        progress: progress.map(Box::new),
10986                    },
10987                    line,
10988                })
10989            }
10990            "pmap_reduce" => {
10991                let map_block = self.parse_block_or_bareword_block()?;
10992                // After the map block, expect either a `{ REDUCE }` block, or
10993                // after an eaten comma, a blockless reduce expr (`$a + $b`).
10994                let reduce_block = if matches!(self.peek(), Token::LBrace) {
10995                    self.parse_block()?
10996                } else {
10997                    // comma separates blockless map from blockless reduce
10998                    self.expect(&Token::Comma)?;
10999                    self.parse_block_or_bareword_cmp_block()?
11000                };
11001                self.eat(&Token::Comma);
11002                let line = self.peek_line();
11003                if let Token::Ident(ref kw) = self.peek().clone() {
11004                    if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
11005                        self.advance();
11006                        self.expect(&Token::FatArrow)?;
11007                        let prog = self.parse_assign_expr()?;
11008                        return Ok(Expr {
11009                            kind: ExprKind::PMapReduceExpr {
11010                                map_block,
11011                                reduce_block,
11012                                list: Box::new(Expr {
11013                                    kind: ExprKind::List(vec![]),
11014                                    line,
11015                                }),
11016                                progress: Some(Box::new(prog)),
11017                            },
11018                            line,
11019                        });
11020                    }
11021                }
11022                if matches!(
11023                    self.peek(),
11024                    Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
11025                ) {
11026                    return Ok(Expr {
11027                        kind: ExprKind::PMapReduceExpr {
11028                            map_block,
11029                            reduce_block,
11030                            list: Box::new(Expr {
11031                                kind: ExprKind::List(vec![]),
11032                                line,
11033                            }),
11034                            progress: None,
11035                        },
11036                        line,
11037                    });
11038                }
11039                let mut parts = vec![self.parse_assign_expr()?];
11040                loop {
11041                    if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
11042                        break;
11043                    }
11044                    if matches!(
11045                        self.peek(),
11046                        Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
11047                    ) {
11048                        break;
11049                    }
11050                    if let Token::Ident(ref kw) = self.peek().clone() {
11051                        if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
11052                            self.advance();
11053                            self.expect(&Token::FatArrow)?;
11054                            let prog = self.parse_assign_expr()?;
11055                            return Ok(Expr {
11056                                kind: ExprKind::PMapReduceExpr {
11057                                    map_block,
11058                                    reduce_block,
11059                                    list: Box::new(merge_expr_list(parts)),
11060                                    progress: Some(Box::new(prog)),
11061                                },
11062                                line,
11063                            });
11064                        }
11065                    }
11066                    parts.push(self.parse_assign_expr()?);
11067                }
11068                Ok(Expr {
11069                    kind: ExprKind::PMapReduceExpr {
11070                        map_block,
11071                        reduce_block,
11072                        list: Box::new(merge_expr_list(parts)),
11073                        progress: None,
11074                    },
11075                    line,
11076                })
11077            }
11078            "puniq" => {
11079                if self.pipe_supplies_slurped_list_operand() {
11080                    return Ok(Expr {
11081                        kind: ExprKind::FuncCall {
11082                            name: "puniq".to_string(),
11083                            args: vec![],
11084                        },
11085                        line,
11086                    });
11087                }
11088                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
11089                let mut args = vec![list];
11090                if let Some(p) = progress {
11091                    args.push(p);
11092                }
11093                Ok(Expr {
11094                    kind: ExprKind::FuncCall {
11095                        name: "puniq".to_string(),
11096                        args,
11097                    },
11098                    line,
11099                })
11100            }
11101            "pfirst" => {
11102                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11103                let cr = Expr {
11104                    kind: ExprKind::CodeRef {
11105                        params: vec![],
11106                        body: block,
11107                    },
11108                    line,
11109                };
11110                let mut args = vec![cr, list];
11111                if let Some(p) = progress {
11112                    args.push(p);
11113                }
11114                Ok(Expr {
11115                    kind: ExprKind::FuncCall {
11116                        name: "pfirst".to_string(),
11117                        args,
11118                    },
11119                    line,
11120                })
11121            }
11122            "pany" => {
11123                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11124                let cr = Expr {
11125                    kind: ExprKind::CodeRef {
11126                        params: vec![],
11127                        body: block,
11128                    },
11129                    line,
11130                };
11131                let mut args = vec![cr, list];
11132                if let Some(p) = progress {
11133                    args.push(p);
11134                }
11135                Ok(Expr {
11136                    kind: ExprKind::FuncCall {
11137                        name: "pany".to_string(),
11138                        args,
11139                    },
11140                    line,
11141                })
11142            }
11143            "uniq" | "distinct" => {
11144                if self.pipe_supplies_slurped_list_operand() {
11145                    return Ok(Expr {
11146                        kind: ExprKind::FuncCall {
11147                            name: name.clone(),
11148                            args: vec![],
11149                        },
11150                        line,
11151                    });
11152                }
11153                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
11154                if progress.is_some() {
11155                    return Err(self.syntax_err(
11156                        "`progress =>` is not supported for uniq (use puniq for parallel + progress)",
11157                        line,
11158                    ));
11159                }
11160                Ok(Expr {
11161                    kind: ExprKind::FuncCall {
11162                        name: name.clone(),
11163                        args: vec![list],
11164                    },
11165                    line,
11166                })
11167            }
11168            "flatten" => {
11169                if self.pipe_supplies_slurped_list_operand() {
11170                    return Ok(Expr {
11171                        kind: ExprKind::FuncCall {
11172                            name: "flatten".to_string(),
11173                            args: vec![],
11174                        },
11175                        line,
11176                    });
11177                }
11178                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
11179                if progress.is_some() {
11180                    return Err(self.syntax_err("`progress =>` is not supported for flatten", line));
11181                }
11182                Ok(Expr {
11183                    kind: ExprKind::FuncCall {
11184                        name: "flatten".to_string(),
11185                        args: vec![list],
11186                    },
11187                    line,
11188                })
11189            }
11190            "set" => {
11191                if self.pipe_supplies_slurped_list_operand() {
11192                    return Ok(Expr {
11193                        kind: ExprKind::FuncCall {
11194                            name: "set".to_string(),
11195                            args: vec![],
11196                        },
11197                        line,
11198                    });
11199                }
11200                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
11201                if progress.is_some() {
11202                    return Err(self.syntax_err("`progress =>` is not supported for set", line));
11203                }
11204                Ok(Expr {
11205                    kind: ExprKind::FuncCall {
11206                        name: "set".to_string(),
11207                        args: vec![list],
11208                    },
11209                    line,
11210                })
11211            }
11212            // `size` is the file-size builtin (Perl `-s`), not a list-count alias.
11213            // Defaults to `$_` when no arg is given, like `length`. See
11214            // `builtin_file_size` in builtins.rs for the runtime behavior.
11215            "size" => {
11216                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11217                    return Ok(e);
11218                }
11219                if self.pipe_supplies_slurped_list_operand() {
11220                    return Ok(Expr {
11221                        kind: ExprKind::FuncCall {
11222                            name: "size".to_string(),
11223                            args: vec![],
11224                        },
11225                        line,
11226                    });
11227                }
11228                let a = self.parse_one_arg_or_default()?;
11229                Ok(Expr {
11230                    kind: ExprKind::FuncCall {
11231                        name: "size".to_string(),
11232                        args: vec![a],
11233                    },
11234                    line,
11235                })
11236            }
11237            "list_count" | "list_size" | "count" | "len" | "cnt" => {
11238                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11239                    return Ok(e);
11240                }
11241                if self.pipe_supplies_slurped_list_operand() {
11242                    return Ok(Expr {
11243                        kind: ExprKind::FuncCall {
11244                            name: name.clone(),
11245                            args: vec![],
11246                        },
11247                        line,
11248                    });
11249                }
11250                // `len(EXPR)` / `cnt(EXPR)` / `count(EXPR)` with a tight `(` —
11251                // the parens are function-call syntax, not a parenthesized
11252                // list: stop the argument at `)` so `len(@a) % 2 == 1` is
11253                // `(len(@a)) % 2 == 1`, not `len(@a % 2 == 1)`. Empty parens
11254                // `len()` collapse to a zero-arg call (use the piped operand
11255                // or `$_`). Bare `len` followed by a low-precedence operator
11256                // (`==`, `&&`, `?`, …) also defaults to a zero-arg call so
11257                // `{ len == 0 }` works as a block predicate on the topic.
11258                // Bare `len EXPR` (no parens, e.g. `len @arr`) goes through
11259                // the greedy list-arg parser; this means `len @a + len @b`
11260                // is `len(@a + len(@b))` (returning the length of the sum
11261                // string), not `(len @a) + (len @b)`. Use explicit parens
11262                // when combining `len` with `+`, `-`, comparisons, etc.
11263                let args = if matches!(self.peek(), Token::LParen) {
11264                    self.advance();
11265                    if matches!(self.peek(), Token::RParen) {
11266                        self.advance();
11267                        Vec::new()
11268                    } else {
11269                        let inner = self.parse_expression()?;
11270                        self.expect(&Token::RParen)?;
11271                        vec![inner]
11272                    }
11273                } else if self.peek_is_named_unary_terminator() {
11274                    Vec::new()
11275                } else {
11276                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
11277                    if progress.is_some() {
11278                        return Err(self.syntax_err(
11279                            "`progress =>` is not supported for list_count / list_size / count / cnt",
11280                            line,
11281                        ));
11282                    }
11283                    vec![list]
11284                };
11285                Ok(Expr {
11286                    kind: ExprKind::FuncCall {
11287                        name: name.clone(),
11288                        args,
11289                    },
11290                    line,
11291                })
11292            }
11293            "shuffle" | "shuffled" => {
11294                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11295                    return Ok(e);
11296                }
11297                if self.pipe_supplies_slurped_list_operand() {
11298                    return Ok(Expr {
11299                        kind: ExprKind::FuncCall {
11300                            name: "shuffle".to_string(),
11301                            args: vec![],
11302                        },
11303                        line,
11304                    });
11305                }
11306                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
11307                if progress.is_some() {
11308                    return Err(self.syntax_err("`progress =>` is not supported for shuffle", line));
11309                }
11310                Ok(Expr {
11311                    kind: ExprKind::FuncCall {
11312                        name: "shuffle".to_string(),
11313                        args: vec![list],
11314                    },
11315                    line,
11316                })
11317            }
11318            "chunked" => {
11319                let mut parts = Vec::new();
11320                if self.eat(&Token::LParen) {
11321                    if !matches!(self.peek(), Token::RParen) {
11322                        parts.push(self.parse_assign_expr()?);
11323                        while self.eat(&Token::Comma) {
11324                            if matches!(self.peek(), Token::RParen) {
11325                                break;
11326                            }
11327                            parts.push(self.parse_assign_expr()?);
11328                        }
11329                    }
11330                    self.expect(&Token::RParen)?;
11331                } else {
11332                    // Paren-less `chunked N`: `|>` is a hard terminator, not
11333                    // an operator inside the arg (see
11334                    // `parse_assign_expr_stop_at_pipe`).
11335                    parts.push(self.parse_assign_expr_stop_at_pipe()?);
11336                    loop {
11337                        if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
11338                            break;
11339                        }
11340                        if matches!(
11341                            self.peek(),
11342                            Token::Semicolon
11343                                | Token::RBrace
11344                                | Token::RParen
11345                                | Token::Eof
11346                                | Token::PipeForward
11347                        ) {
11348                            break;
11349                        }
11350                        if self.peek_is_postfix_stmt_modifier_keyword() {
11351                            break;
11352                        }
11353                        parts.push(self.parse_assign_expr_stop_at_pipe()?);
11354                    }
11355                }
11356                if parts.len() == 1 {
11357                    let n = parts.pop().unwrap();
11358                    return Ok(Expr {
11359                        kind: ExprKind::FuncCall {
11360                            name: "chunked".to_string(),
11361                            args: vec![n],
11362                        },
11363                        line,
11364                    });
11365                }
11366                if parts.is_empty() {
11367                    return Ok(Expr {
11368                        kind: ExprKind::FuncCall {
11369                            name: "chunked".to_string(),
11370                            args: parts,
11371                        },
11372                        line,
11373                    });
11374                }
11375                if parts.len() == 2 {
11376                    let n = parts.pop().unwrap();
11377                    let list = parts.pop().unwrap();
11378                    return Ok(Expr {
11379                        kind: ExprKind::FuncCall {
11380                            name: "chunked".to_string(),
11381                            args: vec![list, n],
11382                        },
11383                        line,
11384                    });
11385                }
11386                Err(self.syntax_err(
11387                    "chunked: use LIST |> chunked(N) or chunked((1,2,3), 2)",
11388                    line,
11389                ))
11390            }
11391            "windowed" => {
11392                let mut parts = Vec::new();
11393                if self.eat(&Token::LParen) {
11394                    if !matches!(self.peek(), Token::RParen) {
11395                        parts.push(self.parse_assign_expr()?);
11396                        while self.eat(&Token::Comma) {
11397                            if matches!(self.peek(), Token::RParen) {
11398                                break;
11399                            }
11400                            parts.push(self.parse_assign_expr()?);
11401                        }
11402                    }
11403                    self.expect(&Token::RParen)?;
11404                } else {
11405                    // Paren-less `windowed N`: same `|>`-terminator rule as
11406                    // `chunked` above.
11407                    parts.push(self.parse_assign_expr_stop_at_pipe()?);
11408                    loop {
11409                        if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
11410                            break;
11411                        }
11412                        if matches!(
11413                            self.peek(),
11414                            Token::Semicolon
11415                                | Token::RBrace
11416                                | Token::RParen
11417                                | Token::Eof
11418                                | Token::PipeForward
11419                        ) {
11420                            break;
11421                        }
11422                        if self.peek_is_postfix_stmt_modifier_keyword() {
11423                            break;
11424                        }
11425                        parts.push(self.parse_assign_expr_stop_at_pipe()?);
11426                    }
11427                }
11428                if parts.len() == 1 {
11429                    let n = parts.pop().unwrap();
11430                    return Ok(Expr {
11431                        kind: ExprKind::FuncCall {
11432                            name: "windowed".to_string(),
11433                            args: vec![n],
11434                        },
11435                        line,
11436                    });
11437                }
11438                if parts.is_empty() {
11439                    return Ok(Expr {
11440                        kind: ExprKind::FuncCall {
11441                            name: "windowed".to_string(),
11442                            args: parts,
11443                        },
11444                        line,
11445                    });
11446                }
11447                if parts.len() == 2 {
11448                    let n = parts.pop().unwrap();
11449                    let list = parts.pop().unwrap();
11450                    return Ok(Expr {
11451                        kind: ExprKind::FuncCall {
11452                            name: "windowed".to_string(),
11453                            args: vec![list, n],
11454                        },
11455                        line,
11456                    });
11457                }
11458                Err(self.syntax_err(
11459                    "windowed: use LIST |> windowed(N) or windowed((1,2,3), 2)",
11460                    line,
11461                ))
11462            }
11463            "any" | "all" | "none" => {
11464                // `any(CODEREF, LIST)` with parens — parse as normal call.
11465                if matches!(self.peek(), Token::LParen) {
11466                    self.advance();
11467                    let args = self.parse_arg_list()?;
11468                    self.expect(&Token::RParen)?;
11469                    return Ok(Expr {
11470                        kind: ExprKind::FuncCall {
11471                            name: name.clone(),
11472                            args,
11473                        },
11474                        line,
11475                    });
11476                }
11477                // Coderef-in-block-position: `any $f LIST` / `any $f, LIST` /
11478                // `LIST |> any $f`. Same shape as the block form but uses a
11479                // value expression where `{ BLOCK }` would go.
11480                if let Some(args) = self.try_parse_coderef_listop_args(line)? {
11481                    return Ok(Expr {
11482                        kind: ExprKind::FuncCall {
11483                            name: name.clone(),
11484                            args,
11485                        },
11486                        line,
11487                    });
11488                }
11489                // `any BLOCK LIST` without parens.
11490                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11491                if progress.is_some() {
11492                    return Err(self.syntax_err(
11493                        "`progress =>` is not supported for any/all/none (use pany for parallel + progress)",
11494                        line,
11495                    ));
11496                }
11497                let cr = Expr {
11498                    kind: ExprKind::CodeRef {
11499                        params: vec![],
11500                        body: block,
11501                    },
11502                    line,
11503                };
11504                Ok(Expr {
11505                    kind: ExprKind::FuncCall {
11506                        name: name.clone(),
11507                        args: vec![cr, list],
11508                    },
11509                    line,
11510                })
11511            }
11512            // Ruby `detect` / `find` — same as `first` (first element matching block).
11513            "first" | "detect" | "find" => {
11514                // `first(CODEREF, LIST)` with parens — parse as normal call.
11515                if matches!(self.peek(), Token::LParen) {
11516                    self.advance();
11517                    let args = self.parse_arg_list()?;
11518                    self.expect(&Token::RParen)?;
11519                    return Ok(Expr {
11520                        kind: ExprKind::FuncCall {
11521                            name: "first".to_string(),
11522                            args,
11523                        },
11524                        line,
11525                    });
11526                }
11527                // Coderef-in-block-position: `first $f LIST` / `LIST |> first $f`.
11528                if let Some(args) = self.try_parse_coderef_listop_args(line)? {
11529                    return Ok(Expr {
11530                        kind: ExprKind::FuncCall {
11531                            name: "first".to_string(),
11532                            args,
11533                        },
11534                        line,
11535                    });
11536                }
11537                // `first BLOCK LIST` without parens.
11538                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11539                if progress.is_some() {
11540                    return Err(self.syntax_err(
11541                        "`progress =>` is not supported for first/detect/find (use pfirst for parallel + progress)",
11542                        line,
11543                    ));
11544                }
11545                let cr = Expr {
11546                    kind: ExprKind::CodeRef {
11547                        params: vec![],
11548                        body: block,
11549                    },
11550                    line,
11551                };
11552                Ok(Expr {
11553                    kind: ExprKind::FuncCall {
11554                        name: "first".to_string(),
11555                        args: vec![cr, list],
11556                    },
11557                    line,
11558                })
11559            }
11560            "take_while" | "drop_while" | "skip_while" | "reject" | "grepv" | "tap" | "peek"
11561            | "partition" | "min_by" | "max_by" | "zip_with" | "count_by" => {
11562                // Coderef-in-block-position: `take_while $f LIST` etc.
11563                if let Some(args) = self.try_parse_coderef_listop_args(line)? {
11564                    return Ok(Expr {
11565                        kind: ExprKind::FuncCall {
11566                            name: name.to_string(),
11567                            args,
11568                        },
11569                        line,
11570                    });
11571                }
11572                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11573                if progress.is_some() {
11574                    return Err(
11575                        self.syntax_err(format!("`progress =>` is not supported for {name}"), line)
11576                    );
11577                }
11578                let cr = Expr {
11579                    kind: ExprKind::CodeRef {
11580                        params: vec![],
11581                        body: block,
11582                    },
11583                    line,
11584                };
11585                Ok(Expr {
11586                    kind: ExprKind::FuncCall {
11587                        name: name.to_string(),
11588                        args: vec![cr, list],
11589                    },
11590                    line,
11591                })
11592            }
11593            "group_by" | "chunk_by" => {
11594                if matches!(self.peek(), Token::LBrace) {
11595                    let (block, list) = self.parse_block_list()?;
11596                    let cr = Expr {
11597                        kind: ExprKind::CodeRef {
11598                            params: vec![],
11599                            body: block,
11600                        },
11601                        line,
11602                    };
11603                    Ok(Expr {
11604                        kind: ExprKind::FuncCall {
11605                            name: name.to_string(),
11606                            args: vec![cr, list],
11607                        },
11608                        line,
11609                    })
11610                } else {
11611                    let key_expr = self.parse_assign_expr()?;
11612                    self.expect(&Token::Comma)?;
11613                    let list_parts = self.parse_list_until_terminator()?;
11614                    let list_expr = if list_parts.len() == 1 {
11615                        list_parts.into_iter().next().unwrap()
11616                    } else {
11617                        Expr {
11618                            kind: ExprKind::List(list_parts),
11619                            line,
11620                        }
11621                    };
11622                    Ok(Expr {
11623                        kind: ExprKind::FuncCall {
11624                            name: name.to_string(),
11625                            args: vec![key_expr, list_expr],
11626                        },
11627                        line,
11628                    })
11629                }
11630            }
11631            "with_index" => {
11632                if self.pipe_supplies_slurped_list_operand() {
11633                    return Ok(Expr {
11634                        kind: ExprKind::FuncCall {
11635                            name: "with_index".to_string(),
11636                            args: vec![],
11637                        },
11638                        line,
11639                    });
11640                }
11641                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
11642                if progress.is_some() {
11643                    return Err(
11644                        self.syntax_err("`progress =>` is not supported for with_index", line)
11645                    );
11646                }
11647                Ok(Expr {
11648                    kind: ExprKind::FuncCall {
11649                        name: "with_index".to_string(),
11650                        args: vec![list],
11651                    },
11652                    line,
11653                })
11654            }
11655            "pcache" => {
11656                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11657                Ok(Expr {
11658                    kind: ExprKind::PcacheExpr {
11659                        block,
11660                        list: Box::new(list),
11661                        progress: progress.map(Box::new),
11662                    },
11663                    line,
11664                })
11665            }
11666            "pselect" => {
11667                let paren = self.eat(&Token::LParen);
11668                let (receivers, timeout) = self.parse_comma_expr_list_with_timeout_tail(paren)?;
11669                if paren {
11670                    self.expect(&Token::RParen)?;
11671                }
11672                if receivers.is_empty() {
11673                    return Err(self.syntax_err("pselect needs at least one receiver", line));
11674                }
11675                Ok(Expr {
11676                    kind: ExprKind::PselectExpr {
11677                        receivers,
11678                        timeout: timeout.map(Box::new),
11679                    },
11680                    line,
11681                })
11682            }
11683            "open" => {
11684                let paren = matches!(self.peek(), Token::LParen);
11685                if paren {
11686                    self.advance();
11687                }
11688                if matches!(self.peek(), Token::Ident(ref s) if s == "my") {
11689                    self.advance();
11690                    let name = self.parse_scalar_var_name()?;
11691                    self.expect(&Token::Comma)?;
11692                    let mode = self.parse_assign_expr()?;
11693                    let file = if self.eat(&Token::Comma) {
11694                        Some(self.parse_assign_expr()?)
11695                    } else {
11696                        None
11697                    };
11698                    if paren {
11699                        self.expect(&Token::RParen)?;
11700                    }
11701                    Ok(Expr {
11702                        kind: ExprKind::Open {
11703                            handle: Box::new(Expr {
11704                                kind: ExprKind::OpenMyHandle { name },
11705                                line,
11706                            }),
11707                            mode: Box::new(mode),
11708                            file: file.map(Box::new),
11709                        },
11710                        line,
11711                    })
11712                } else {
11713                    let args = if paren {
11714                        self.parse_arg_list()?
11715                    } else {
11716                        self.parse_list_until_terminator()?
11717                    };
11718                    if paren {
11719                        self.expect(&Token::RParen)?;
11720                    }
11721                    if args.len() < 2 {
11722                        return Err(self.syntax_err("open requires at least 2 arguments", line));
11723                    }
11724                    Ok(Expr {
11725                        kind: ExprKind::Open {
11726                            handle: Box::new(args[0].clone()),
11727                            mode: Box::new(args[1].clone()),
11728                            file: args.get(2).cloned().map(Box::new),
11729                        },
11730                        line,
11731                    })
11732                }
11733            }
11734            "close" => {
11735                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11736                    return Ok(e);
11737                }
11738                let a = self.parse_one_arg_or_default()?;
11739                Ok(Expr {
11740                    kind: ExprKind::Close(Box::new(a)),
11741                    line,
11742                })
11743            }
11744            "opendir" => {
11745                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11746                    return Ok(e);
11747                }
11748                let args = self.parse_builtin_args()?;
11749                if args.len() != 2 {
11750                    return Err(self.syntax_err("opendir requires two arguments", line));
11751                }
11752                Ok(Expr {
11753                    kind: ExprKind::Opendir {
11754                        handle: Box::new(args[0].clone()),
11755                        path: Box::new(args[1].clone()),
11756                    },
11757                    line,
11758                })
11759            }
11760            "readdir" => {
11761                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11762                    return Ok(e);
11763                }
11764                let a = self.parse_one_arg()?;
11765                Ok(Expr {
11766                    kind: ExprKind::Readdir(Box::new(a)),
11767                    line,
11768                })
11769            }
11770            "closedir" => {
11771                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11772                    return Ok(e);
11773                }
11774                let a = self.parse_one_arg()?;
11775                Ok(Expr {
11776                    kind: ExprKind::Closedir(Box::new(a)),
11777                    line,
11778                })
11779            }
11780            "rewinddir" => {
11781                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11782                    return Ok(e);
11783                }
11784                let a = self.parse_one_arg()?;
11785                Ok(Expr {
11786                    kind: ExprKind::Rewinddir(Box::new(a)),
11787                    line,
11788                })
11789            }
11790            "telldir" => {
11791                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11792                    return Ok(e);
11793                }
11794                let a = self.parse_one_arg()?;
11795                Ok(Expr {
11796                    kind: ExprKind::Telldir(Box::new(a)),
11797                    line,
11798                })
11799            }
11800            "seekdir" => {
11801                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11802                    return Ok(e);
11803                }
11804                let args = self.parse_builtin_args()?;
11805                if args.len() != 2 {
11806                    return Err(self.syntax_err("seekdir requires two arguments", line));
11807                }
11808                Ok(Expr {
11809                    kind: ExprKind::Seekdir {
11810                        handle: Box::new(args[0].clone()),
11811                        position: Box::new(args[1].clone()),
11812                    },
11813                    line,
11814                })
11815            }
11816            "eof" => {
11817                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11818                    return Ok(e);
11819                }
11820                if matches!(self.peek(), Token::LParen) {
11821                    self.advance();
11822                    if matches!(self.peek(), Token::RParen) {
11823                        self.advance();
11824                        Ok(Expr {
11825                            kind: ExprKind::Eof(None),
11826                            line,
11827                        })
11828                    } else {
11829                        let a = self.parse_expression()?;
11830                        self.expect(&Token::RParen)?;
11831                        Ok(Expr {
11832                            kind: ExprKind::Eof(Some(Box::new(a))),
11833                            line,
11834                        })
11835                    }
11836                } else {
11837                    Ok(Expr {
11838                        kind: ExprKind::Eof(None),
11839                        line,
11840                    })
11841                }
11842            }
11843            "system" => {
11844                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11845                    return Ok(e);
11846                }
11847                let args = self.parse_builtin_args()?;
11848                Ok(Expr {
11849                    kind: ExprKind::System(args),
11850                    line,
11851                })
11852            }
11853            "exec" => {
11854                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11855                    return Ok(e);
11856                }
11857                let args = self.parse_builtin_args()?;
11858                Ok(Expr {
11859                    kind: ExprKind::Exec(args),
11860                    line,
11861                })
11862            }
11863            "eval" => {
11864                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11865                    return Ok(e);
11866                }
11867                let a = if matches!(self.peek(), Token::LBrace) {
11868                    let block = self.parse_block()?;
11869                    Expr {
11870                        kind: ExprKind::CodeRef {
11871                            params: vec![],
11872                            body: block,
11873                        },
11874                        line,
11875                    }
11876                } else {
11877                    self.parse_one_arg_or_default()?
11878                };
11879                Ok(Expr {
11880                    kind: ExprKind::Eval(Box::new(a)),
11881                    line,
11882                })
11883            }
11884            "do" => {
11885                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11886                    return Ok(e);
11887                }
11888                let a = self.parse_one_arg()?;
11889                Ok(Expr {
11890                    kind: ExprKind::Do(Box::new(a)),
11891                    line,
11892                })
11893            }
11894            "require" => {
11895                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11896                    return Ok(e);
11897                }
11898                let a = self.parse_one_arg()?;
11899                Ok(Expr {
11900                    kind: ExprKind::Require(Box::new(a)),
11901                    line,
11902                })
11903            }
11904            "exit" => {
11905                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11906                    return Ok(e);
11907                }
11908                if matches!(
11909                    self.peek(),
11910                    Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
11911                ) {
11912                    Ok(Expr {
11913                        kind: ExprKind::Exit(None),
11914                        line,
11915                    })
11916                } else {
11917                    let a = self.parse_one_arg()?;
11918                    Ok(Expr {
11919                        kind: ExprKind::Exit(Some(Box::new(a))),
11920                        line,
11921                    })
11922                }
11923            }
11924            "chdir" => {
11925                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11926                    return Ok(e);
11927                }
11928                let a = self.parse_one_arg_or_default()?;
11929                Ok(Expr {
11930                    kind: ExprKind::Chdir(Box::new(a)),
11931                    line,
11932                })
11933            }
11934            "mkdir" => {
11935                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11936                    return Ok(e);
11937                }
11938                let args = self.parse_builtin_args()?;
11939                Ok(Expr {
11940                    kind: ExprKind::Mkdir {
11941                        path: Box::new(args[0].clone()),
11942                        mode: args.get(1).cloned().map(Box::new),
11943                    },
11944                    line,
11945                })
11946            }
11947            "unlink" | "rm" => {
11948                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11949                    return Ok(e);
11950                }
11951                let args = self.parse_builtin_args()?;
11952                Ok(Expr {
11953                    kind: ExprKind::Unlink(args),
11954                    line,
11955                })
11956            }
11957            "rename" => {
11958                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11959                    return Ok(e);
11960                }
11961                let args = self.parse_builtin_args()?;
11962                if args.len() != 2 {
11963                    return Err(self.syntax_err("rename requires two arguments", line));
11964                }
11965                Ok(Expr {
11966                    kind: ExprKind::Rename {
11967                        old: Box::new(args[0].clone()),
11968                        new: Box::new(args[1].clone()),
11969                    },
11970                    line,
11971                })
11972            }
11973            "chmod" => {
11974                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11975                    return Ok(e);
11976                }
11977                let args = self.parse_builtin_args()?;
11978                if args.len() < 2 {
11979                    return Err(self.syntax_err("chmod requires mode and at least one file", line));
11980                }
11981                Ok(Expr {
11982                    kind: ExprKind::Chmod(args),
11983                    line,
11984                })
11985            }
11986            "chown" => {
11987                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11988                    return Ok(e);
11989                }
11990                let args = self.parse_builtin_args()?;
11991                if args.len() < 3 {
11992                    return Err(
11993                        self.syntax_err("chown requires uid, gid, and at least one file", line)
11994                    );
11995                }
11996                Ok(Expr {
11997                    kind: ExprKind::Chown(args),
11998                    line,
11999                })
12000            }
12001            "stat" => {
12002                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12003                    return Ok(e);
12004                }
12005                let args = self.parse_builtin_args()?;
12006                let arg = if args.len() == 1 {
12007                    args[0].clone()
12008                } else if args.is_empty() {
12009                    Expr {
12010                        kind: ExprKind::ScalarVar("_".into()),
12011                        line,
12012                    }
12013                } else {
12014                    return Err(self.syntax_err("stat requires zero or one argument", line));
12015                };
12016                Ok(Expr {
12017                    kind: ExprKind::Stat(Box::new(arg)),
12018                    line,
12019                })
12020            }
12021            "lstat" => {
12022                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12023                    return Ok(e);
12024                }
12025                let args = self.parse_builtin_args()?;
12026                let arg = if args.len() == 1 {
12027                    args[0].clone()
12028                } else if args.is_empty() {
12029                    Expr {
12030                        kind: ExprKind::ScalarVar("_".into()),
12031                        line,
12032                    }
12033                } else {
12034                    return Err(self.syntax_err("lstat requires zero or one argument", line));
12035                };
12036                Ok(Expr {
12037                    kind: ExprKind::Lstat(Box::new(arg)),
12038                    line,
12039                })
12040            }
12041            "link" => {
12042                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12043                    return Ok(e);
12044                }
12045                let args = self.parse_builtin_args()?;
12046                if args.len() != 2 {
12047                    return Err(self.syntax_err("link requires two arguments", line));
12048                }
12049                Ok(Expr {
12050                    kind: ExprKind::Link {
12051                        old: Box::new(args[0].clone()),
12052                        new: Box::new(args[1].clone()),
12053                    },
12054                    line,
12055                })
12056            }
12057            "symlink" => {
12058                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12059                    return Ok(e);
12060                }
12061                let args = self.parse_builtin_args()?;
12062                if args.len() != 2 {
12063                    return Err(self.syntax_err("symlink requires two arguments", line));
12064                }
12065                Ok(Expr {
12066                    kind: ExprKind::Symlink {
12067                        old: Box::new(args[0].clone()),
12068                        new: Box::new(args[1].clone()),
12069                    },
12070                    line,
12071                })
12072            }
12073            "readlink" => {
12074                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12075                    return Ok(e);
12076                }
12077                let args = self.parse_builtin_args()?;
12078                let arg = if args.len() == 1 {
12079                    args[0].clone()
12080                } else if args.is_empty() {
12081                    Expr {
12082                        kind: ExprKind::ScalarVar("_".into()),
12083                        line,
12084                    }
12085                } else {
12086                    return Err(self.syntax_err("readlink requires zero or one argument", line));
12087                };
12088                Ok(Expr {
12089                    kind: ExprKind::Readlink(Box::new(arg)),
12090                    line,
12091                })
12092            }
12093            "files" => {
12094                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12095                    return Ok(e);
12096                }
12097                let args = self.parse_builtin_args()?;
12098                Ok(Expr {
12099                    kind: ExprKind::Files(args),
12100                    line,
12101                })
12102            }
12103            "filesf" | "f" => {
12104                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12105                    return Ok(e);
12106                }
12107                let args = self.parse_builtin_args()?;
12108                Ok(Expr {
12109                    kind: ExprKind::Filesf(args),
12110                    line,
12111                })
12112            }
12113            "fr" => {
12114                let args = self.parse_builtin_args()?;
12115                Ok(Expr {
12116                    kind: ExprKind::FilesfRecursive(args),
12117                    line,
12118                })
12119            }
12120            "dirs" | "d" => {
12121                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12122                    return Ok(e);
12123                }
12124                let args = self.parse_builtin_args()?;
12125                Ok(Expr {
12126                    kind: ExprKind::Dirs(args),
12127                    line,
12128                })
12129            }
12130            "dr" => {
12131                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12132                    return Ok(e);
12133                }
12134                let args = self.parse_builtin_args()?;
12135                Ok(Expr {
12136                    kind: ExprKind::DirsRecursive(args),
12137                    line,
12138                })
12139            }
12140            "sym_links" => {
12141                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12142                    return Ok(e);
12143                }
12144                let args = self.parse_builtin_args()?;
12145                Ok(Expr {
12146                    kind: ExprKind::SymLinks(args),
12147                    line,
12148                })
12149            }
12150            "sockets" => {
12151                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12152                    return Ok(e);
12153                }
12154                let args = self.parse_builtin_args()?;
12155                Ok(Expr {
12156                    kind: ExprKind::Sockets(args),
12157                    line,
12158                })
12159            }
12160            "pipes" => {
12161                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12162                    return Ok(e);
12163                }
12164                let args = self.parse_builtin_args()?;
12165                Ok(Expr {
12166                    kind: ExprKind::Pipes(args),
12167                    line,
12168                })
12169            }
12170            "block_devices" => {
12171                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12172                    return Ok(e);
12173                }
12174                let args = self.parse_builtin_args()?;
12175                Ok(Expr {
12176                    kind: ExprKind::BlockDevices(args),
12177                    line,
12178                })
12179            }
12180            "char_devices" => {
12181                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12182                    return Ok(e);
12183                }
12184                let args = self.parse_builtin_args()?;
12185                Ok(Expr {
12186                    kind: ExprKind::CharDevices(args),
12187                    line,
12188                })
12189            }
12190            "exe" | "executables" => {
12191                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12192                    return Ok(e);
12193                }
12194                let args = self.parse_builtin_args()?;
12195                Ok(Expr {
12196                    kind: ExprKind::Executables(args),
12197                    line,
12198                })
12199            }
12200            "glob" => {
12201                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12202                    return Ok(e);
12203                }
12204                let args = self.parse_builtin_args()?;
12205                Ok(Expr {
12206                    kind: ExprKind::Glob(args),
12207                    line,
12208                })
12209            }
12210            "glob_par" => {
12211                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12212                    return Ok(e);
12213                }
12214                let (args, progress) = self.parse_glob_par_or_par_sed_args()?;
12215                Ok(Expr {
12216                    kind: ExprKind::GlobPar { args, progress },
12217                    line,
12218                })
12219            }
12220            "par_sed" => {
12221                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12222                    return Ok(e);
12223                }
12224                let (args, progress) = self.parse_glob_par_or_par_sed_args()?;
12225                Ok(Expr {
12226                    kind: ExprKind::ParSed { args, progress },
12227                    line,
12228                })
12229            }
12230            "bless" => {
12231                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12232                    return Ok(e);
12233                }
12234                let args = self.parse_builtin_args()?;
12235                Ok(Expr {
12236                    kind: ExprKind::Bless {
12237                        ref_expr: Box::new(args[0].clone()),
12238                        class: args.get(1).cloned().map(Box::new),
12239                    },
12240                    line,
12241                })
12242            }
12243            "caller" => {
12244                if matches!(self.peek(), Token::LParen) {
12245                    self.advance();
12246                    if matches!(self.peek(), Token::RParen) {
12247                        self.advance();
12248                        Ok(Expr {
12249                            kind: ExprKind::Caller(None),
12250                            line,
12251                        })
12252                    } else {
12253                        let a = self.parse_expression()?;
12254                        self.expect(&Token::RParen)?;
12255                        Ok(Expr {
12256                            kind: ExprKind::Caller(Some(Box::new(a))),
12257                            line,
12258                        })
12259                    }
12260                } else {
12261                    Ok(Expr {
12262                        kind: ExprKind::Caller(None),
12263                        line,
12264                    })
12265                }
12266            }
12267            "wantarray" => {
12268                if matches!(self.peek(), Token::LParen) {
12269                    self.advance();
12270                    self.expect(&Token::RParen)?;
12271                }
12272                Ok(Expr {
12273                    kind: ExprKind::Wantarray,
12274                    line,
12275                })
12276            }
12277            "sub" => {
12278                // In no-interop mode, `sub {}` is not valid — must use `fn {}`
12279                if crate::no_interop_mode() {
12280                    return Err(self.syntax_err(
12281                        "stryke uses `fn {}` instead of `sub {}` (--no-interop)",
12282                        line,
12283                    ));
12284                }
12285                // Anonymous sub — optional prototype `sub () { }` (e.g. Carp.pm `*X = sub () { 1 }`)
12286                let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
12287                let body = self.parse_block()?;
12288                Ok(Expr {
12289                    kind: ExprKind::CodeRef { params, body },
12290                    line,
12291                })
12292            }
12293            "fn" => {
12294                // Anonymous fn — stryke syntax for anonymous subroutines
12295                let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
12296                self.parse_sub_attributes()?;
12297                let body = self.parse_fn_eq_body_or_block(false)?;
12298                Ok(Expr {
12299                    kind: ExprKind::CodeRef { params, body },
12300                    line,
12301                })
12302            }
12303            _ => {
12304                // Generic function call
12305                // Check for fat arrow (bareword string in hash) — except for
12306                // topic-slot barewords (`_`, `_<`, `_0`, `_0<`, …), which must
12307                // resolve to the topic value, not the literal name.
12308                if matches!(self.peek(), Token::FatArrow) && !Self::is_underscore_topic_slot(&name)
12309                {
12310                    return Ok(Expr {
12311                        kind: ExprKind::String(name),
12312                        line,
12313                    });
12314                }
12315                // Bare `_` in expression position → topic variable `$_`.
12316                // Allows concise blocks: `map { _ * 2 }`, `fi { _ > 5 }`.
12317                // Also handles the outer-topic chain: `_<`, `_<<`, `_<<<`,
12318                // `_<<<<` for 1..4 frames up — and the positional matrix:
12319                // `_0<<<<`, `_1<<<<`, `_N<<<<` (N positionals × 5 levels).
12320                // `_0` is canonically aliased to `_` at every level (see
12321                // `Scope::set_closure_args`).
12322                //
12323                // Stryke string-index sugar: `_[N]` (bareword, no sigil) is
12324                // an alias for `_!N!` — char-of-topic substring. The sigil
12325                // form `$_[N]` keeps Perl's `@_`-access semantics (first
12326                // positional arg). We dispatch here, before the generic
12327                // ArrayElement path, so the AST for `_[N]` carries the
12328                // synthetic `__topicstr__$NAME` flag the interpreter / VM
12329                // strip and route to char-of-string.
12330                if Self::is_underscore_topic_slot(&name) {
12331                    if matches!(self.peek(), Token::LBracket) && self.peek_line() == line {
12332                        self.advance(); // [
12333                        let index = self.parse_expression()?;
12334                        self.expect(&Token::RBracket)?;
12335                        return Ok(Expr {
12336                            kind: ExprKind::ArrayElement {
12337                                array: format!("__topicstr__{}", name),
12338                                index: Box::new(index),
12339                            },
12340                            line,
12341                        });
12342                    }
12343                    return Ok(Expr {
12344                        kind: ExprKind::ScalarVar(name.clone()),
12345                        line,
12346                    });
12347                }
12348                // Function call with optional parens
12349                if matches!(self.peek(), Token::LParen) {
12350                    self.advance();
12351                    let args = self.parse_arg_list()?;
12352                    self.expect(&Token::RParen)?;
12353                    Ok(Expr {
12354                        kind: ExprKind::FuncCall { name, args },
12355                        line,
12356                    })
12357                } else if self.peek().is_term_start()
12358                    && !(matches!(self.peek(), Token::Ident(ref kw) if kw == "sub")
12359                        && matches!(self.peek_at(1), Token::Ident(_)))
12360                    && !(self.suppress_parenless_call > 0 && matches!(self.peek(), Token::Ident(_)))
12361                    && !(matches!(self.peek(), Token::LBrace)
12362                        && self.peek_line() > self.prev_line())
12363                    && !(matches!(self.peek(), Token::BitNot)
12364                        && self.suppress_tilde_range == 0
12365                        && matches!(
12366                            self.peek_at(1),
12367                            Token::Ident(_) | Token::Integer(_) | Token::Float(_)
12368                        ))
12369                {
12370                    // Perl allows func arg without parens
12371                    // Guard: `sub <name> { }` is a named sub declaration (new
12372                    // statement), not an argument to the preceding call.
12373                    // Guard: suppress_parenless_call > 0 with Ident prevents consuming
12374                    // barewords (used by thread macro so `t Color::Red p` treats
12375                    // `p` as a stage, not an argument to the enum variant), but
12376                    // still allows `{` for struct/hash literals like `t Foo { x => 1 } p`.
12377                    // Guard: `{` on a new line is a new statement (hashref/block),
12378                    // not an argument to the preceding bareword call.
12379                    // Guard: `~Ident` / `~Integer` / `~Float` after a bareword is
12380                    // the universal-tilde range separator (`I~M~5`, `Mon~Fri`,
12381                    // `Jan~Dec~2`), not unary BitNot of an arg. Bail to Bareword
12382                    // so the outer `parse_range` consumes `~` as the range op.
12383                    let args = self.parse_list_until_terminator()?;
12384                    Ok(Expr {
12385                        kind: ExprKind::FuncCall { name, args },
12386                        line,
12387                    })
12388                } else {
12389                    // No parens, no visible arguments — emit a Bareword.
12390                    // At runtime, Bareword tries sub resolution first (zero-arg
12391                    // call) and falls back to a string value.  stryke extension
12392                    // contexts (pipe-forward, map/fore) lift Bareword → FuncCall
12393                    // with `$_` injection separately.
12394                    Ok(Expr {
12395                        kind: ExprKind::Bareword(name),
12396                        line,
12397                    })
12398                }
12399            }
12400        }
12401    }
12402
12403    fn parse_print_like(
12404        &mut self,
12405        make: impl FnOnce(Option<String>, Vec<Expr>) -> ExprKind,
12406    ) -> PerlResult<Expr> {
12407        let line = self.peek_line();
12408        // Check for filehandle: print STDERR "msg"  /  print $fh "msg"
12409        let handle = if let Token::Ident(ref h) = self.peek().clone() {
12410            if h.chars().all(|c| c.is_uppercase() || c == '_')
12411                && !matches!(self.peek(), Token::LParen)
12412            {
12413                let h = h.clone();
12414                let saved = self.pos;
12415                self.advance();
12416                // Verify next token is a term start (not operator).
12417                // Guard: `~Ident` / `~Integer` / `~Float` is a universal-tilde
12418                // range separator (`p I~M~5`, `p Mon~Fri`), not unary BitNot of
12419                // an arg. Bail filehandle detection so the bareword `I` flows
12420                // into the regular expression path where `parse_range` consumes
12421                // `~` as the range op.
12422                let is_tilde_range_after = matches!(self.peek(), Token::BitNot)
12423                    && self.suppress_tilde_range == 0
12424                    && matches!(
12425                        self.peek_at(1),
12426                        Token::Ident(_) | Token::Integer(_) | Token::Float(_)
12427                    );
12428                if !is_tilde_range_after
12429                    && (self.peek().is_term_start()
12430                        || matches!(
12431                            self.peek(),
12432                            Token::DoubleString(_)
12433                                | Token::BacktickString(_)
12434                                | Token::SingleString(_)
12435                        ))
12436                {
12437                    Some(h)
12438                } else {
12439                    self.pos = saved;
12440                    None
12441                }
12442            } else {
12443                None
12444            }
12445        } else if let Token::ScalarVar(ref v) = self.peek().clone() {
12446            // `print $fh "msg"` — scalar variable as indirect filehandle.
12447            // Treat as handle when the next token (after $var) is a term-start or
12448            // string literal *without* a preceding comma/operator, matching Perl's
12449            // indirect-object heuristic.
12450            // Exclude `$_` — it's virtually always the topic variable, not a handle.
12451            // Exclude `[` and `{` — those are array/hash subscripts on the variable
12452            // itself (`print $F[0]`, `print $h{k}`), not separate print arguments.
12453            // Exclude statement modifiers (`if`/`unless`/`while`/`until`/`for`/`foreach`)
12454            // — `print $_ if COND` prints `$_` to STDOUT, not to a handle named `$_`.
12455            let v = v.clone();
12456            if v == "_" {
12457                None
12458            } else {
12459                let saved = self.pos;
12460                self.advance();
12461                let next = self.peek().clone();
12462                let is_stmt_modifier = matches!(&next, Token::Ident(kw)
12463                    if matches!(kw.as_str(), "if" | "unless" | "while" | "until" | "for" | "foreach"));
12464                if !is_stmt_modifier
12465                    && !matches!(next, Token::LBracket | Token::LBrace)
12466                    && (next.is_term_start()
12467                        || matches!(
12468                            next,
12469                            Token::DoubleString(_)
12470                                | Token::BacktickString(_)
12471                                | Token::SingleString(_)
12472                        ))
12473                {
12474                    // Next token looks like a print argument — $var is the handle.
12475                    Some(format!("${v}"))
12476                } else {
12477                    self.pos = saved;
12478                    None
12479                }
12480            }
12481        } else {
12482            None
12483        };
12484        // `print()` / `say()` / `printf()` — empty parens default to `$_`,
12485        // matching Perl 5: `perldoc -f print` / `-f say` say "If no arguments
12486        // are given, prints $_." (Same convention as the topic-default unary
12487        // builtins handled in `parse_one_arg_or_default`.)
12488        let args =
12489            if matches!(self.peek(), Token::LParen) && matches!(self.peek_at(1), Token::RParen) {
12490                let line_topic = self.peek_line();
12491                self.advance(); // (
12492                self.advance(); // )
12493                vec![Expr {
12494                    kind: ExprKind::ScalarVar("_".into()),
12495                    line: line_topic,
12496                }]
12497            } else {
12498                self.parse_list_until_terminator()?
12499            };
12500        Ok(Expr {
12501            kind: make(handle, args),
12502            line,
12503        })
12504    }
12505
12506    fn parse_block_list(&mut self) -> PerlResult<(Block, Expr)> {
12507        let block = self.parse_block()?;
12508        let block_end_line = self.prev_line();
12509        self.eat(&Token::Comma);
12510        // On the RHS of `|>`, the list operand is supplied by the piped LHS
12511        // and will be substituted at desugar time — accept a placeholder when
12512        // we're at a terminator here or on a new line (implicit semicolon).
12513        if self.in_pipe_rhs()
12514            && (matches!(
12515                self.peek(),
12516                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
12517            ) || self.peek_line() > block_end_line)
12518        {
12519            let line = self.peek_line();
12520            return Ok((block, self.pipe_placeholder_list(line)));
12521        }
12522        let list = self.parse_expression()?;
12523        Ok((block, list))
12524    }
12525
12526    /// Comma-separated expressions with optional trailing `timeout => SECS` (for `pselect`).
12527    /// When `paren` is true, stops at `)` as well as normal terminators.
12528    fn parse_comma_expr_list_with_timeout_tail(
12529        &mut self,
12530        paren: bool,
12531    ) -> PerlResult<(Vec<Expr>, Option<Expr>)> {
12532        let mut parts = vec![self.parse_assign_expr()?];
12533        loop {
12534            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
12535                break;
12536            }
12537            if paren && matches!(self.peek(), Token::RParen) {
12538                break;
12539            }
12540            if matches!(
12541                self.peek(),
12542                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
12543            ) {
12544                break;
12545            }
12546            if self.peek_is_postfix_stmt_modifier_keyword() {
12547                break;
12548            }
12549            if let Token::Ident(ref kw) = self.peek().clone() {
12550                if kw == "timeout" && matches!(self.peek_at(1), Token::FatArrow) {
12551                    self.advance();
12552                    self.expect(&Token::FatArrow)?;
12553                    let t = self.parse_assign_expr()?;
12554                    return Ok((parts, Some(t)));
12555                }
12556            }
12557            parts.push(self.parse_assign_expr()?);
12558        }
12559        Ok((parts, None))
12560    }
12561
12562    /// `preduce_init EXPR, BLOCK, LIST` with optional `, progress => EXPR`.
12563    fn parse_init_block_then_list_optional_progress(
12564        &mut self,
12565    ) -> PerlResult<(Expr, Block, Expr, Option<Expr>)> {
12566        let init = self.parse_assign_expr()?;
12567        self.expect(&Token::Comma)?;
12568        let block = self.parse_block_or_bareword_block()?;
12569        self.eat(&Token::Comma);
12570        let line = self.peek_line();
12571        if let Token::Ident(ref kw) = self.peek().clone() {
12572            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
12573                self.advance();
12574                self.expect(&Token::FatArrow)?;
12575                let prog = self.parse_assign_expr()?;
12576                return Ok((
12577                    init,
12578                    block,
12579                    Expr {
12580                        kind: ExprKind::List(vec![]),
12581                        line,
12582                    },
12583                    Some(prog),
12584                ));
12585            }
12586        }
12587        if matches!(
12588            self.peek(),
12589            Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
12590        ) {
12591            return Ok((
12592                init,
12593                block,
12594                Expr {
12595                    kind: ExprKind::List(vec![]),
12596                    line,
12597                },
12598                None,
12599            ));
12600        }
12601        let mut parts = vec![self.parse_assign_expr()?];
12602        loop {
12603            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
12604                break;
12605            }
12606            if matches!(
12607                self.peek(),
12608                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
12609            ) {
12610                break;
12611            }
12612            if self.peek_is_postfix_stmt_modifier_keyword() {
12613                break;
12614            }
12615            if let Token::Ident(ref kw) = self.peek().clone() {
12616                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
12617                    self.advance();
12618                    self.expect(&Token::FatArrow)?;
12619                    let prog = self.parse_assign_expr()?;
12620                    return Ok((init, block, merge_expr_list(parts), Some(prog)));
12621                }
12622            }
12623            parts.push(self.parse_assign_expr()?);
12624        }
12625        Ok((init, block, merge_expr_list(parts), None))
12626    }
12627
12628    /// `pmap_on CLUSTER { BLOCK } LIST [, progress => EXPR]` — cluster expr, then same tail as [`Self::parse_block_then_list_optional_progress`].
12629    fn parse_cluster_block_then_list_optional_progress(
12630        &mut self,
12631    ) -> PerlResult<(Expr, Block, Expr, Option<Expr>)> {
12632        // `pmap_on $c { BLOCK } @list` — suppress `$c { ... }` hash-subscript
12633        // auto-arrow so the brace opens the BLOCK, not a `$c->{...}` deref.
12634        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_add(1);
12635        let cluster = self.parse_assign_expr();
12636        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_sub(1);
12637        let cluster = cluster?;
12638        // Accept the canonical `pmap_on $c, { BLOCK } @list` LSP-doc form too.
12639        self.eat(&Token::Comma);
12640        let block = self.parse_block_or_bareword_block()?;
12641        let block_end_line = self.prev_line();
12642        self.eat(&Token::Comma);
12643        let line = self.peek_line();
12644        if let Token::Ident(ref kw) = self.peek().clone() {
12645            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
12646                self.advance();
12647                self.expect(&Token::FatArrow)?;
12648                let prog = self.parse_assign_expr_stop_at_pipe()?;
12649                return Ok((
12650                    cluster,
12651                    block,
12652                    Expr {
12653                        kind: ExprKind::List(vec![]),
12654                        line,
12655                    },
12656                    Some(prog),
12657                ));
12658            }
12659        }
12660        let empty_list_ok = matches!(
12661            self.peek(),
12662            Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
12663        ) || (self.in_pipe_rhs()
12664            && (matches!(self.peek(), Token::Comma) || self.peek_line() > block_end_line));
12665        if empty_list_ok {
12666            return Ok((
12667                cluster,
12668                block,
12669                Expr {
12670                    kind: ExprKind::List(vec![]),
12671                    line,
12672                },
12673                None,
12674            ));
12675        }
12676        let mut parts = vec![self.parse_assign_expr_stop_at_pipe()?];
12677        loop {
12678            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
12679                break;
12680            }
12681            if matches!(
12682                self.peek(),
12683                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
12684            ) {
12685                break;
12686            }
12687            if self.peek_is_postfix_stmt_modifier_keyword() {
12688                break;
12689            }
12690            if let Token::Ident(ref kw) = self.peek().clone() {
12691                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
12692                    self.advance();
12693                    self.expect(&Token::FatArrow)?;
12694                    let prog = self.parse_assign_expr_stop_at_pipe()?;
12695                    return Ok((cluster, block, merge_expr_list(parts), Some(prog)));
12696                }
12697            }
12698            parts.push(self.parse_assign_expr_stop_at_pipe()?);
12699        }
12700        Ok((cluster, block, merge_expr_list(parts), None))
12701    }
12702
12703    /// Like [`parse_block_list`] but supports a trailing `, progress => EXPR`
12704    /// (`pmap`, `pgrep`, `preduce`, `pfor`, `pcache`, `psort`, …).
12705    ///
12706    /// Always invoked for paren-less trailing forms (`pmap { … } LIST`,
12707    /// `pmap { … } LIST, progress => EXPR`), so `|>` must terminate the whole
12708    /// stage — individual list parts and the progress value parse through
12709    /// [`Self::parse_assign_expr_stop_at_pipe`] to keep pipe-forward
12710    /// left-associative in `@a |> pmap { $_ * 2 }, progress => 0 |> join ','`.
12711    fn parse_block_then_list_optional_progress(
12712        &mut self,
12713    ) -> PerlResult<(Block, Expr, Option<Expr>)> {
12714        let block = self.parse_block_or_bareword_block()?;
12715        let block_end_line = self.prev_line();
12716        self.eat(&Token::Comma);
12717        let line = self.peek_line();
12718        if let Token::Ident(ref kw) = self.peek().clone() {
12719            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
12720                self.advance();
12721                self.expect(&Token::FatArrow)?;
12722                let prog = self.parse_assign_expr_stop_at_pipe()?;
12723                return Ok((
12724                    block,
12725                    Expr {
12726                        kind: ExprKind::List(vec![]),
12727                        line,
12728                    },
12729                    Some(prog),
12730                ));
12731            }
12732        }
12733        // An empty list operand is allowed when the next token terminates the
12734        // enclosing context. Inside a pipe-forward RHS, a trailing `,` also
12735        // counts — `foo(bar, @a |> pmap { $_ * 2 }, baz)`. `|>` is also a
12736        // terminator — left-associative chaining leaves the outer `|>` for
12737        // the enclosing pipe-forward loop. A newline after the block also
12738        // terminates in pipe-RHS — the LHS supplies the list, so we must NOT
12739        // greedily eat the next statement (matches `parse_block_list`).
12740        let empty_list_ok = matches!(
12741            self.peek(),
12742            Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
12743        ) || (self.in_pipe_rhs()
12744            && (matches!(self.peek(), Token::Comma) || self.peek_line() > block_end_line));
12745        if empty_list_ok {
12746            return Ok((
12747                block,
12748                Expr {
12749                    kind: ExprKind::List(vec![]),
12750                    line,
12751                },
12752                None,
12753            ));
12754        }
12755        let mut parts = vec![self.parse_assign_expr_stop_at_pipe()?];
12756        loop {
12757            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
12758                break;
12759            }
12760            if matches!(
12761                self.peek(),
12762                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
12763            ) {
12764                break;
12765            }
12766            if self.peek_is_postfix_stmt_modifier_keyword() {
12767                break;
12768            }
12769            if let Token::Ident(ref kw) = self.peek().clone() {
12770                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
12771                    self.advance();
12772                    self.expect(&Token::FatArrow)?;
12773                    let prog = self.parse_assign_expr_stop_at_pipe()?;
12774                    return Ok((block, merge_expr_list(parts), Some(prog)));
12775                }
12776            }
12777            parts.push(self.parse_assign_expr_stop_at_pipe()?);
12778        }
12779        Ok((block, merge_expr_list(parts), None))
12780    }
12781
12782    /// Parse fan/fan_cap arguments: optional count + block or blockless expression.
12783    fn parse_fan_count_and_block(&mut self, line: usize) -> PerlResult<(Option<Box<Expr>>, Block)> {
12784        // `fan { BLOCK }` — no count
12785        if matches!(self.peek(), Token::LBrace) {
12786            let block = self.parse_block()?;
12787            return Ok((None, block));
12788        }
12789        let saved = self.pos;
12790        // Not a brace — first expr could be count or body
12791        let first = self.parse_postfix()?;
12792        if matches!(self.peek(), Token::LBrace) {
12793            // `fan COUNT { BLOCK }`
12794            let block = self.parse_block()?;
12795            Ok((Some(Box::new(first)), block))
12796        } else if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof)
12797            || (matches!(self.peek(), Token::Comma)
12798                && matches!(self.peek_at(1), Token::Ident(ref kw) if kw == "progress"))
12799        {
12800            // `fan EXPR;` — no count, first is the body
12801            let block = self.bareword_to_no_arg_block(first);
12802            Ok((None, block))
12803        } else if matches!(first.kind, ExprKind::Integer(_)) {
12804            // `fan COUNT EXPR` or `fan COUNT, EXPR` — integer count + body
12805            self.eat(&Token::Comma);
12806            let body = self.parse_fan_blockless_body(line)?;
12807            Ok((Some(Box::new(first)), body))
12808        } else {
12809            // Non-integer first (e.g. `$_`) followed by binary op (e.g. `* $_`)
12810            // — backtrack and re-parse as a full body expression.
12811            self.pos = saved;
12812            let body = self.parse_fan_blockless_body(line)?;
12813            Ok((None, body))
12814        }
12815    }
12816
12817    /// Parse a blockless fan/fan_cap body as a full expression (not just postfix).
12818    fn parse_fan_blockless_body(&mut self, line: usize) -> PerlResult<Block> {
12819        if matches!(self.peek(), Token::LBrace) {
12820            return self.parse_block();
12821        }
12822        // Check for bareword (zero-arg sub call) terminated by ; } EOF , or pipe
12823        if let Token::Ident(ref name) = self.peek().clone() {
12824            if matches!(
12825                self.peek_at(1),
12826                Token::Comma | Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
12827            ) {
12828                let name = name.clone();
12829                self.advance();
12830                let body = Expr {
12831                    kind: ExprKind::FuncCall { name, args: vec![] },
12832                    line,
12833                };
12834                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
12835            }
12836        }
12837        // Full expression (handles `$_ * $_`, `$_ + 1`, etc.)
12838        let expr = self.parse_assign_expr_stop_at_pipe()?;
12839        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
12840    }
12841
12842    /// Wrap a parsed expression as a single-statement block, converting bare
12843    /// identifiers to zero-arg calls (`work` → `work()`).
12844    fn bareword_to_no_arg_block(&self, expr: Expr) -> Block {
12845        let line = expr.line;
12846        let body = match &expr.kind {
12847            ExprKind::Bareword(name) => Expr {
12848                kind: ExprKind::FuncCall {
12849                    name: name.clone(),
12850                    args: vec![],
12851                },
12852                line,
12853            },
12854            _ => expr,
12855        };
12856        vec![Statement::new(StmtKind::Expression(body), line)]
12857    }
12858
12859    /// Parse either a `{ BLOCK }` or a bare expression and wrap it as a synthetic block.
12860    ///
12861    /// When the next token is `{`, delegates to [`Self::parse_block`].
12862    /// Otherwise parses a single postfix expression and wraps it as a call
12863    /// with `$_` as argument (for barewords) or a plain expression statement:
12864    ///
12865    /// - Bareword `foo` → `{ foo($_) }`
12866    /// - Other expr     → `{ EXPR }`
12867    fn parse_block_or_bareword_block(&mut self) -> PerlResult<Block> {
12868        if matches!(self.peek(), Token::LBrace) {
12869            return self.parse_block();
12870        }
12871        let line = self.peek_line();
12872        // A lone identifier followed by a list-terminator is a bare sub name:
12873        // `pmap double, @list` → block is `{ double($_) }`, rest is list.
12874        if let Token::Ident(ref name) = self.peek().clone() {
12875            if matches!(
12876                self.peek_at(1),
12877                Token::Comma | Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
12878            ) {
12879                let name = name.clone();
12880                self.advance();
12881                let body = Expr {
12882                    kind: ExprKind::FuncCall {
12883                        name,
12884                        args: vec![Expr {
12885                            kind: ExprKind::ScalarVar("_".to_string()),
12886                            line,
12887                        }],
12888                    },
12889                    line,
12890                };
12891                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
12892            }
12893        }
12894        // Not a simple bareword — parse as expression (e.g. `$_ * 2`, `uc $_`)
12895        let expr = self.parse_assign_expr_stop_at_pipe()?;
12896        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
12897    }
12898
12899    /// Like [`parse_block_or_bareword_block`] but for fan/timer/bench where the
12900    /// bare function takes no args (body runs stand-alone, not per-element).
12901    /// Only consumes a single bareword identifier — does NOT let `parse_primary`
12902    /// greedily swallow subsequent tokens as function arguments.
12903    fn parse_block_or_bareword_block_no_args(&mut self) -> PerlResult<Block> {
12904        if matches!(self.peek(), Token::LBrace) {
12905            return self.parse_block();
12906        }
12907        let line = self.peek_line();
12908        if let Token::Ident(ref name) = self.peek().clone() {
12909            if matches!(
12910                self.peek_at(1),
12911                Token::Comma
12912                    | Token::Semicolon
12913                    | Token::RBrace
12914                    | Token::Eof
12915                    | Token::PipeForward
12916                    | Token::Integer(_)
12917            ) {
12918                let name = name.clone();
12919                self.advance();
12920                let body = Expr {
12921                    kind: ExprKind::FuncCall { name, args: vec![] },
12922                    line,
12923                };
12924                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
12925            }
12926        }
12927        let expr = self.parse_postfix()?;
12928        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
12929    }
12930
12931    /// Returns true if `name` is a Perl keyword/builtin that should NOT be
12932    /// treated as a bare sub name (e.g. inside `sort`).
12933    /// True for any bareword the parser treats as a known builtin / keyword —
12934    /// Perl 5 core *or* a stryke extension. Used to suppress "call as user
12935    /// sub" interpretations (e.g. `sort my_cmp @list` only treats `my_cmp`
12936    /// as a comparator name if it *isn't* a known bareword). Previously named
12937    /// `is_perl_keyword`, which was misleading.
12938    fn is_known_bareword(name: &str) -> bool {
12939        Self::is_perl5_core(name) || Self::stryke_extension_name(name).is_some()
12940    }
12941
12942    /// True iff `name` appears as any spelling (primary *or* alias) in a
12943    /// `try_builtin` match arm. Picks up the ~300 aliases that don't show
12944    /// up in the parser-level keyword lists but are still callable at
12945    /// runtime — so `map { tj }` can default to `tj($_)` the same way
12946    /// `map { to_json }` does.
12947    fn is_try_builtin_name(name: &str) -> bool {
12948        crate::builtins::BUILTIN_ARMS
12949            .iter()
12950            .any(|arm| arm.contains(&name))
12951    }
12952
12953    /// True iff `name` is a Perl 5 core keyword/builtin (as shipped in stock
12954    /// `perl`). Extensions (`pmap`, `fan`, `timer`, …) are *not* included
12955    /// here — those live in `stryke_extension_name`. `%stryke::perl_compats`
12956    /// is derived from this list by `build.rs`.
12957    fn is_perl5_core(name: &str) -> bool {
12958        matches!(
12959            name,
12960            // ── array / list ────────────────────────────────────────────
12961            "map" | "grep" | "sort" | "reverse" | "join" | "split"
12962            | "push" | "pop" | "shift" | "unshift" | "splice"
12963            | "splice_last" | "splice1" | "spl_last"
12964            | "pack" | "unpack"
12965            | "unpack_first" | "unpack1" | "up1"
12966            // ── hash ────────────────────────────────────────────────────
12967            | "keys" | "values" | "each"
12968            // ── string ──────────────────────────────────────────────────
12969            | "chomp" | "chop" | "chr" | "ord" | "hex" | "oct"
12970            | "lc" | "uc" | "lcfirst" | "ucfirst"
12971            | "length" | "substr" | "index" | "rindex"
12972            | "sprintf" | "printf" | "print" | "say"
12973            | "pos" | "quotemeta" | "study"
12974            // ── numeric ─────────────────────────────────────────────────
12975            | "abs" | "int" | "sqrt" | "sin" | "cos" | "atan2"
12976            | "exp" | "log" | "rand" | "srand"
12977            // ── time ────────────────────────────────────────────────────
12978            | "time" | "localtime" | "gmtime"
12979            // ── type / reflection ───────────────────────────────────────
12980            | "defined" | "undef" | "ref" | "scalar" | "wantarray"
12981            | "caller" | "delete" | "exists" | "bless" | "prototype"
12982            | "tie" | "untie" | "tied"
12983            // ── io ──────────────────────────────────────────────────────
12984            | "open" | "close" | "read" | "readline" | "write" | "seek" | "tell"
12985            | "eof" | "binmode" | "getc" | "fileno" | "truncate"
12986            | "format" | "formline" | "select" | "vec"
12987            | "sysopen" | "sysread" | "sysseek" | "syswrite"
12988            // ── filesystem ──────────────────────────────────────────────
12989            | "stat" | "lstat" | "rename" | "unlink" | "utime"
12990            | "mkdir" | "rmdir" | "chdir" | "chmod" | "chown"
12991            | "glob" | "opendir" | "readdir" | "closedir"
12992            | "link" | "readlink" | "symlink"
12993            // ── ipc ─────────────────────────────────────────────────────
12994            | "fcntl" | "flock" | "ioctl" | "pipe" | "dbmopen" | "dbmclose"
12995            // ── sysv ipc ────────────────────────────────────────────────
12996            | "msgctl" | "msgget" | "msgrcv" | "msgsnd"
12997            | "semctl" | "semget" | "semop"
12998            | "shmctl" | "shmget" | "shmread" | "shmwrite"
12999            // ── process / system ────────────────────────────────────────
13000            | "system" | "exec" | "exit" | "die" | "warn" | "dump"
13001            | "fork" | "wait" | "waitpid" | "kill" | "alarm" | "sleep"
13002            | "chroot" | "times" | "umask" | "reset"
13003            | "getpgrp" | "setpgrp" | "getppid"
13004            | "getpriority" | "setpriority"
13005            // ── socket ──────────────────────────────────────────────────
13006            | "socket" | "socketpair" | "connect" | "listen" | "accept" | "shutdown"
13007            | "send" | "recv" | "bind" | "setsockopt" | "getsockopt"
13008            | "getpeername" | "getsockname"
13009            // ── posix metadata ──────────────────────────────────────────
13010            | "getpwnam" | "getpwuid" | "getpwent" | "setpwent"
13011            | "getgrnam" | "getgrgid" | "getgrent" | "setgrent"
13012            | "getlogin"
13013            | "gethostbyname" | "gethostbyaddr" | "gethostent"
13014            | "getnetbyname" | "getnetent"
13015            | "getprotobyname" | "getprotoent"
13016            | "getservbyname" | "getservent"
13017            | "sethostent" | "setnetent" | "setprotoent" | "setservent"
13018            | "endpwent" | "endgrent"
13019            | "endhostent" | "endnetent" | "endprotoent" | "endservent"
13020            // ── control flow ────────────────────────────────────────────
13021            | "return" | "do" | "eval" | "require"
13022            | "my" | "our" | "local" | "use" | "no"
13023            | "sub" | "if" | "unless" | "while" | "until"
13024            | "for" | "foreach" | "last" | "next" | "redo" | "goto"
13025            | "not" | "and" | "or"
13026            // ── quoting ─────────────────────────────────────────────────
13027            | "qw" | "qq" | "q"
13028            // ── phase blocks ────────────────────────────────────────────
13029            | "BEGIN" | "END"
13030        )
13031    }
13032
13033    /// If `name` is a stryke-only extension keyword/builtin, return it; else `None`.
13034    /// Used by `--compat` to reject extensions at parse time.
13035    fn stryke_extension_name(name: &str) -> Option<&str> {
13036        match name {
13037            // ── aop ────────────────────────────────────────────────────────
13038            | "proceed" | "intercept_list" | "intercept_remove" | "intercept_clear"
13039            // ── parallel ────────────────────────────────────────────────────
13040            | "pmap" | "pmap_on" | "pflat_map" | "pflat_map_on" | "pmap_chunked"
13041            | "pgrep" | "pfor" | "psort" | "preduce" | "preduce_init" | "pmap_reduce"
13042            | "pcache" | "pchannel" | "pselect" | "puniq" | "pfirst" | "pany"
13043            | "fan" | "fan_cap" | "par_lines" | "par_walk" | "par_sed"
13044            | "par_find_files" | "par_line_count" | "pwatch" | "par_pipeline_stream"
13045            | "glob_par" | "ppool" | "barrier" | "pipeline" | "cluster"
13046            | "pmaps" | "pflat_maps" | "pgreps"
13047            // ── functional / iterator ───────────────────────────────────────
13048            | "fore" | "e" | "ep" | "flat_map" | "flat_maps" | "maps" | "filter" | "fi" | "find_all" | "reduce" | "fold"
13049            | "inject" | "collect" | "uniq" | "distinct" | "any" | "all" | "none"
13050            | "first" | "detect" | "find" | "compact" | "concat" | "chain" | "reject" | "grepv" | "flatten" | "set"
13051            | "min_by" | "max_by" | "sort_by" | "tally" | "find_index"
13052            | "each_with_index" | "count" | "cnt" |"len" | "group_by" | "chunk_by"
13053            | "zip" | "chunk" | "chunked" | "sliding_window" | "windowed"
13054            | "enumerate" | "with_index" | "shuffle" | "shuffled"| "heap"
13055            | "take_while" | "drop_while" | "skip_while" | "tap" | "peek" | "partition"
13056            | "zip_with" | "count_by" | "skip" | "first_or"
13057            // ── pipeline / string helpers ───────────────────────────────────
13058            | "input" | "lines" | "words" | "chars" | "digits" | "letters" | "letters_uc" | "letters_lc"
13059            | "punctuation" | "punct"
13060            | "sentences" | "sents"
13061            | "paragraphs" | "paras" | "sections" | "sects"
13062            | "numbers" | "nums" | "graphemes" | "grs" | "columns" | "cols"
13063            | "trim" | "avg" | "stddev"
13064            | "squared" | "sq" | "square" | "cubed" | "cb" | "cube" | "expt" | "pow" | "pw"
13065            | "normalize" | "snake_case" | "camel_case" | "kebab_case"
13066            | "frequencies" | "freq" | "interleave" | "ddump" | "stringify" | "str" | "top"
13067            | "to_json" | "to_csv" | "to_toml" | "to_yaml" | "to_xml"
13068            | "to_html" | "to_markdown" | "to_table" | "xopen"
13069            | "from_json" | "from_csv" | "from_toml" | "from_yaml" | "from_xml"
13070            | "clip" | "clipboard" | "paste" | "pbcopy" | "pbpaste" | "preview"
13071            | "sparkline" | "spark" | "bar_chart" | "bars" | "flame" | "flamechart"
13072            | "histo" | "gauge" | "spinner" | "spinner_start" | "spinner_stop"
13073            | "to_hash" | "to_set"
13074            | "to_file" | "read_lines" | "append_file" | "write_json" | "read_json"
13075            | "tempfile" | "tempdir" | "list_count" | "list_size" | "size"
13076            | "clamp" | "grep_v" | "select_keys" | "pluck" | "glob_match" | "which_all"
13077            | "dedup" | "nth" | "tail" | "take" | "drop" | "tee" | "range"
13078            | "inc" | "dec" | "elapsed"
13079            // ── filesystem extensions ───────────────────────────────────────
13080            | "files" | "filesf" | "f" | "fr" | "dirs" | "d" | "dr" | "sym_links"
13081            | "sockets" | "pipes" | "block_devices" | "char_devices" | "exe" | "executables"
13082            | "basename" | "dirname" | "fileparse" | "realpath" | "canonpath"
13083            | "copy" | "move" | "spurt" | "spit" | "read_bytes" | "which"
13084            | "getcwd" | "touch" | "gethostname" | "uname"
13085            // ── data / network ──────────────────────────────────────────────
13086            | "csv_read" | "csv_write" | "dataframe" | "sqlite"
13087            | "fetch" | "fetch_json" | "fetch_async" | "fetch_async_json"
13088            | "par_fetch" | "par_csv_read" | "par_pipeline"
13089            | "json_encode" | "json_decode" | "json_jq"
13090            | "http_request" | "serve" | "ssh"
13091            | "html_parse" | "css_select" | "xml_parse" | "xpath"
13092            | "smtp_send"
13093            | "net_interfaces" | "net_ipv4" | "net_ipv6" | "net_mac"
13094            | "net_public_ip" | "net_dns" | "net_reverse_dns"
13095            | "net_ping" | "net_port_open" | "net_ports_scan"
13096            | "net_latency" | "net_download" | "net_headers"
13097            | "net_dns_servers" | "net_gateway" | "net_whois" | "net_hostname"
13098            // ── git ─────────────────────────────────────────────────────────
13099            | "git_log" | "git_status" | "git_diff" | "git_branches"
13100            | "git_tags" | "git_blame" | "git_authors" | "git_files"
13101            | "git_show" | "git_root"
13102            // ── audio / media ───────────────────────────────────────────────
13103            | "audio_convert" | "audio_info" | "id3_read" | "id3_write"
13104            // ── pdf ─────────────────────────────────────────────────────────
13105            | "to_pdf" | "pdf_text" | "pdf_pages"
13106            // ── serialization (stryke-only encoders) ────────────────────────
13107            | "toml_encode" | "toml_decode"
13108            | "yaml_encode" | "yaml_decode"
13109            | "xml_encode" | "xml_decode"
13110            // ── crypto / encoding ───────────────────────────────────────────
13111            | "md5" | "sha1" | "sha224" | "sha256" | "sha384" | "sha512"
13112            | "sha3_256" | "s3_256" | "sha3_512" | "s3_512"
13113            | "shake128" | "shake256"
13114            | "hmac_sha256" | "hmac_sha1" | "hmac_sha384" | "hmac_sha512" | "hmac_md5"
13115            | "uuid" | "crc32"
13116            | "blake2b" | "b2b" | "blake2s" | "b2s" | "blake3" | "b3"
13117            | "ripemd160" | "rmd160" | "md4"
13118            | "xxh32" | "xxhash32" | "xxh64" | "xxhash64" | "xxh3" | "xxhash3" | "xxh3_128" | "xxhash3_128"
13119            | "murmur3" | "murmur3_32" | "murmur3_128"
13120            | "siphash" | "siphash_keyed"
13121            | "hkdf_sha256" | "hkdf" | "hkdf_sha512"
13122            | "poly1305" | "poly1305_mac"
13123            | "base32_encode" | "b32e" | "base32_decode" | "b32d"
13124            | "base58_encode" | "b58e" | "base58_decode" | "b58d"
13125            | "totp" | "totp_generate" | "totp_verify" | "hotp" | "hotp_generate"
13126            | "aes_cbc_encrypt" | "aes_cbc_enc" | "aes_cbc_decrypt" | "aes_cbc_dec"
13127            | "blowfish_encrypt" | "bf_enc" | "blowfish_decrypt" | "bf_dec"
13128            | "des3_encrypt" | "3des_enc" | "tdes_enc" | "des3_decrypt" | "3des_dec" | "tdes_dec"
13129            | "twofish_encrypt" | "tf_enc" | "twofish_decrypt" | "tf_dec"
13130            | "camellia_encrypt" | "cam_enc" | "camellia_decrypt" | "cam_dec"
13131            | "cast5_encrypt" | "cast5_enc" | "cast5_decrypt" | "cast5_dec"
13132            | "salsa20" | "salsa20_encrypt" | "salsa20_decrypt"
13133            | "xsalsa20" | "xsalsa20_encrypt" | "xsalsa20_decrypt"
13134            | "secretbox" | "secretbox_seal" | "secretbox_open"
13135            | "nacl_box_keygen" | "box_keygen" | "nacl_box" | "nacl_box_seal" | "box_seal"
13136            | "nacl_box_open" | "box_open"
13137            | "qr_ascii" | "qr" | "qr_png" | "qr_svg"
13138            | "barcode_code128" | "code128" | "barcode_code39" | "code39"
13139            | "barcode_ean13" | "ean13" | "barcode_svg"
13140            | "argon2_hash" | "argon2" | "argon2_verify"
13141            | "bcrypt_hash" | "bcrypt" | "bcrypt_verify"
13142            | "scrypt_hash" | "scrypt" | "scrypt_verify"
13143            | "pbkdf2" | "pbkdf2_derive"
13144            | "random_bytes" | "randbytes" | "random_bytes_hex" | "randhex"
13145            | "aes_encrypt" | "aes_enc" | "aes_decrypt" | "aes_dec"
13146            | "chacha_encrypt" | "chacha_enc" | "chacha_decrypt" | "chacha_dec"
13147            | "rsa_keygen" | "rsa_encrypt" | "rsa_enc" | "rsa_decrypt" | "rsa_dec"
13148            | "rsa_encrypt_pkcs1" | "rsa_decrypt_pkcs1" | "rsa_sign" | "rsa_verify"
13149            | "ecdsa_p256_keygen" | "p256_keygen" | "ecdsa_p256_sign" | "p256_sign"
13150            | "ecdsa_p256_verify" | "p256_verify"
13151            | "ecdsa_p384_keygen" | "p384_keygen" | "ecdsa_p384_sign" | "p384_sign"
13152            | "ecdsa_p384_verify" | "p384_verify"
13153            | "ecdsa_secp256k1_keygen" | "secp256k1_keygen"
13154            | "ecdsa_secp256k1_sign" | "secp256k1_sign"
13155            | "ecdsa_secp256k1_verify" | "secp256k1_verify"
13156            | "ecdh_p256" | "p256_dh" | "ecdh_p384" | "p384_dh"
13157            | "ed25519_keygen" | "ed_keygen" | "ed25519_sign" | "ed_sign"
13158            | "ed25519_verify" | "ed_verify"
13159            | "x25519_keygen" | "x_keygen" | "x25519_dh" | "x_dh"
13160            | "base64_encode" | "base64_decode"
13161            | "hex_encode" | "hex_decode"
13162            | "url_encode" | "url_decode"
13163            | "gzip" | "gunzip" | "gz" | "ugz" | "zstd" | "zstd_decode" | "zst" | "uzst"
13164            | "brotli" | "br" | "brotli_decode" | "ubr"
13165            | "xz" | "lzma" | "xz_decode" | "unxz" | "unlzma"
13166            | "bzip2" | "bz2" | "bzip2_decode" | "bunzip2" | "ubz2"
13167            | "lz4" | "lz4_decode" | "unlz4"
13168            | "snappy" | "snp" | "snappy_decode" | "unsnappy"
13169            | "lzw" | "lzw_decode" | "unlzw"
13170            | "tar_create" | "tar" | "tar_extract" | "untar" | "tar_list"
13171            | "tar_gz_create" | "tgz" | "tar_gz_extract" | "untgz"
13172            | "zip_create" | "zip_archive" | "zip_extract" | "unzip_archive" | "zip_list"
13173            // ── special math functions ────────────────────────────────────────
13174            | "erf" | "erfc" | "gamma" | "tgamma" | "lgamma" | "ln_gamma"
13175            | "digamma" | "psi" | "beta_fn" | "lbeta" | "ln_beta"
13176            | "betainc" | "beta_reg" | "gammainc" | "gamma_li"
13177            | "gammaincc" | "gamma_ui" | "gammainc_reg" | "gamma_lr"
13178            | "gammaincc_reg" | "gamma_ur"
13179            // ── date / time ─────────────────────────────────────────────────
13180            | "datetime_utc" | "datetime_now_tz"
13181            | "datetime_format_tz" | "datetime_add_seconds"
13182            | "datetime_from_epoch"
13183            | "datetime_parse_rfc3339" | "datetime_parse_local"
13184            | "datetime_strftime"
13185            | "dateseq" | "dategrep" | "dateround" | "datesort"
13186            // ── jwt ─────────────────────────────────────────────────────────
13187            | "jwt_encode" | "jwt_decode" | "jwt_decode_unsafe"
13188            // ── logging ─────────────────────────────────────────────────────
13189            | "log_info" | "log_warn" | "log_error"
13190            | "log_debug" | "log_trace" | "log_json" | "log_level"
13191            // ── concurrency / timing ────────────────────────────────────────
13192            | "async" | "spawn" | "trace" | "timer" | "bench"
13193            | "eval_timeout" | "retry" | "rate_limit" | "every"
13194            | "gen" | "watch"
13195            // ── caching ────────────────────────────────────────────────────────
13196            | "cache_clear" | "cache_exists" | "cache_stats" | "cacheview"
13197            // ── testing framework ────────────────────────────────────────────
13198            | "assert_eq" | "assert_ne" | "assert_ok" | "assert_err"
13199            | "assert_true" | "assert_false"
13200            | "assert_gt" | "assert_lt" | "assert_ge" | "assert_le"
13201            | "assert_match" | "assert_contains" | "assert_near" | "assert_dies"
13202            | "test_run" | "run_tests" | "test_skip" | "skip_test" | "skip_assert"
13203            // ── system info ─────────────────────────────────────────────────
13204            | "mounts" | "du" | "du_tree" | "process_list"
13205            | "thread_count" | "pool_info" | "par_bench"
13206            // ── stress testing ──────────────────────────────────────────────
13207            | "stress_cpu" | "scpu" | "stress_mem" | "smem"
13208            | "stress_io" | "sio" | "stress_test" | "st"
13209            | "heat" | "fire" | "fire_and_forget" | "pin"
13210            // ── I/O extensions ──────────────────────────────────────────────
13211            | "slurp" | "cat" | "c" | "capture" | "pager" | "pg" | "less"
13212            | "stdin"
13213            // ── internal ────────────────────────────────────────────────────
13214            | "__stryke_rust_compile"
13215            | "vec_set_value"
13216            // ── short aliases ───────────────────────────────────────────────
13217            | "p" | "rev"
13218            // ── trivial numeric / predicate builtins ────────────────────────
13219            | "even" | "odd" | "zero" | "nonzero"
13220            | "positive" | "pos_n" | "negative" | "neg_n"
13221            | "sign" | "negate" | "double" | "triple" | "half"
13222            | "identity" | "id"
13223            | "round" | "floor" | "ceil" | "ceiling" | "trunc" | "truncn"
13224            | "gcd" | "lcm" | "min2" | "max2"
13225            | "log2" | "log10" | "hypot"
13226            | "rad_to_deg" | "r2d" | "deg_to_rad" | "d2r"
13227            | "pow2" | "abs_diff"
13228            | "factorial" | "fact" | "fibonacci" | "fib"
13229            | "is_prime" | "is_square" | "is_power_of_two" | "is_pow2"
13230            | "cbrt" | "exp2" | "percent" | "pct" | "inverse"
13231            | "median" | "mode_val" | "variance"
13232            // ── trivial string ops ──────────────────────────────────────────
13233            | "is_empty" | "is_blank" | "is_numeric"
13234            | "is_upper" | "is_lower" | "is_alpha" | "is_digit" | "is_alnum"
13235            | "is_space" | "is_whitespace"
13236            | "starts_with" | "sw" | "ends_with" | "ew" | "contains"
13237            | "capitalize" | "cap" | "swap_case" | "repeat"
13238            | "title_case" | "title" | "squish"
13239            | "pad_left" | "lpad" | "pad_right" | "rpad" | "center"
13240            | "truncate_at" | "shorten" | "reverse_str" | "rev_str"
13241            | "char_count" | "word_count" | "wc" | "line_count" | "lc_lines"
13242            // ── trivial type predicates ─────────────────────────────────────
13243            | "is_array" | "is_arrayref" | "is_hash" | "is_hashref"
13244            | "is_code" | "is_coderef" | "is_ref"
13245            | "is_undef" | "is_defined" | "is_def"
13246            | "is_string" | "is_str" | "is_int" | "is_integer" | "is_float"
13247            // ── hash helpers ────────────────────────────────────────────────
13248            | "invert" | "merge_hash"
13249            | "has_key" | "hk" | "has_any_key" | "has_all_keys"
13250            // ── boolean combinators ─────────────────────────────────────────
13251            | "both" | "either" | "neither" | "xor_bool" | "bool_to_int" | "b2i"
13252            // ── collection helpers (trivial) ────────────────────────────────
13253            | "riffle" | "intersperse" | "every_nth"
13254            | "drop_n" | "take_n" | "rotate" | "swap_pairs"
13255            // ── base conversion ─────────────────────────────────────────────
13256            | "to_bin" | "bin_of" | "to_hex" | "hex_of" | "to_oct" | "oct_of"
13257            | "from_bin" | "from_hex" | "from_oct" | "to_base" | "from_base"
13258            | "bits_count" | "popcount" | "leading_zeros" | "lz"
13259            | "trailing_zeros" | "tz" | "bit_length" | "bitlen"
13260            // ── bit ops ─────────────────────────────────────────────────────
13261            | "bit_and" | "bit_or" | "bit_xor" | "bit_not"
13262            | "shift_left" | "shl" | "shift_right" | "shr"
13263            | "bit_set" | "bit_clear" | "bit_toggle" | "bit_test"
13264            // ── unit conversions: temperature ───────────────────────────────
13265            | "c_to_f" | "f_to_c" | "c_to_k" | "k_to_c" | "f_to_k" | "k_to_f"
13266            // ── unit conversions: distance ──────────────────────────────────
13267            | "miles_to_km" | "km_to_miles" | "miles_to_m" | "m_to_miles"
13268            | "feet_to_m" | "m_to_feet" | "inches_to_cm" | "cm_to_inches"
13269            | "yards_to_m" | "m_to_yards"
13270            // ── unit conversions: mass ──────────────────────────────────────
13271            | "kg_to_lbs" | "lbs_to_kg" | "g_to_oz" | "oz_to_g"
13272            | "stone_to_kg" | "kg_to_stone"
13273            // ── unit conversions: digital ───────────────────────────────────
13274            | "bytes_to_kb" | "b_to_kb" | "kb_to_bytes" | "kb_to_b"
13275            | "bytes_to_mb" | "mb_to_bytes" | "bytes_to_gb" | "gb_to_bytes"
13276            | "kb_to_mb" | "mb_to_gb"
13277            | "bits_to_bytes" | "bytes_to_bits"
13278            // ── unit conversions: time ──────────────────────────────────────
13279            | "seconds_to_minutes" | "s_to_m" | "minutes_to_seconds" | "m_to_s"
13280            | "seconds_to_hours" | "hours_to_seconds"
13281            | "seconds_to_days" | "days_to_seconds"
13282            | "minutes_to_hours" | "hours_to_minutes"
13283            | "hours_to_days" | "days_to_hours"
13284            // ── date helpers ────────────────────────────────────────────────
13285            | "is_leap_year" | "is_leap" | "days_in_month"
13286            | "month_name" | "month_short"
13287            | "weekday_name" | "weekday_short" | "quarter_of"
13288            // ── now / timestamp ─────────────────────────────────────────────
13289            | "now_ms" | "now_us" | "now_ns"
13290            | "unix_epoch" | "epoch" | "unix_epoch_ms" | "epoch_ms"
13291            // ── color / ANSI ────────────────────────────────────────────────
13292            | "rgb_to_hex" | "hex_to_rgb"
13293            | "ansi_red" | "ansi_green" | "ansi_yellow" | "ansi_blue"
13294            | "ansi_magenta" | "ansi_cyan" | "ansi_white" | "ansi_black"
13295            | "ansi_bold" | "ansi_dim" | "ansi_underline" | "ansi_reverse"
13296            | "strip_ansi"
13297            | "red" | "green" | "yellow" | "blue" | "magenta" | "purple" | "cyan"
13298            | "white" | "black" | "bold" | "dim" | "italic" | "underline"
13299            | "strikethrough" | "ansi_off" | "off" | "gray" | "grey"
13300            | "bright_red" | "bright_green" | "bright_yellow" | "bright_blue"
13301            | "bright_magenta" | "bright_cyan" | "bright_white"
13302            | "bg_red" | "bg_green" | "bg_yellow" | "bg_blue"
13303            | "bg_magenta" | "bg_cyan" | "bg_white" | "bg_black"
13304            | "red_bold" | "bold_red" | "green_bold" | "bold_green"
13305            | "yellow_bold" | "bold_yellow" | "blue_bold" | "bold_blue"
13306            | "magenta_bold" | "bold_magenta" | "cyan_bold" | "bold_cyan"
13307            | "white_bold" | "bold_white"
13308            | "blink" | "rapid_blink" | "hidden" | "overline"
13309            | "bg_bright_red" | "bg_bright_green" | "bg_bright_yellow" | "bg_bright_blue"
13310            | "bg_bright_magenta" | "bg_bright_cyan" | "bg_bright_white"
13311            | "rgb" | "bg_rgb" | "color256" | "c256" | "bg_color256" | "bg_c256"
13312            // ── network / validation ────────────────────────────────────────
13313            | "ipv4_to_int" | "int_to_ipv4"
13314            | "is_valid_ipv4" | "is_valid_ipv6" | "is_valid_email" | "is_valid_url"
13315            // ── path helpers ────────────────────────────────────────────────
13316            | "path_ext" | "path_stem" | "path_parent" | "path_join" | "path_split"
13317            | "strip_prefix" | "strip_suffix" | "ensure_prefix" | "ensure_suffix"
13318            // ── functional primitives ───────────────────────────────────────
13319            | "const_fn" | "always_true" | "always_false"
13320            | "flip_args" | "first_arg" | "second_arg" | "last_arg"
13321            // ── more list helpers ───────────────────────────────────────────
13322            | "count_eq" | "count_ne" | "all_eq"
13323            | "all_distinct" | "all_unique" | "has_duplicates"
13324            | "sum_of" | "product_of" | "max_of" | "min_of" | "range_of"
13325            // ── string quote / escape ───────────────────────────────────────
13326            | "quote" | "single_quote" | "unquote"
13327            | "extract_between" | "ellipsis"
13328            // ── random ──────────────────────────────────────────────────────
13329            | "coin_flip" | "dice_roll"
13330            | "random_int" | "random_float" | "random_bool"
13331            | "random_choice" | "random_between"
13332            | "random_string" | "random_alpha" | "random_digit"
13333            // ── symbol table ────────────────────────────────────────────────
13334            | "refresh_stashes"
13335            // ── system introspection ────────────────────────────────────────
13336            | "os_name" | "os_arch" | "num_cpus"
13337            | "pid" | "ppid" | "uid" | "gid"
13338            | "username" | "home_dir" | "temp_dir"
13339            | "mem_total" | "mem_free" | "mem_used"
13340            | "swap_total" | "swap_free" | "swap_used"
13341            | "disk_total" | "disk_free" | "disk_avail" | "disk_used"
13342            | "load_avg" | "sys_uptime" | "page_size"
13343            | "os_version" | "os_family" | "endianness" | "pointer_width"
13344            | "proc_mem" | "rss"
13345            // ── collection more ─────────────────────────────────────────────
13346            | "transpose" | "unzip"
13347            | "run_length_encode" | "rle" | "run_length_decode" | "rld"
13348            | "sliding_pairs" | "consecutive_eq" | "flatten_deep"
13349            // ── trig / math (batch 2) ───────────────────────────────────────
13350            | "tan" | "asin" | "acos" | "atan"
13351            | "sinh" | "cosh" | "tanh" | "asinh" | "acosh" | "atanh"
13352            | "sqr" | "cube_fn"
13353            | "mod_op" | "ceil_div" | "floor_div"
13354            | "is_finite" | "is_infinite" | "is_inf" | "is_nan"
13355            | "degrees" | "radians"
13356            | "min_abs" | "max_abs"
13357            | "saturate" | "sat01" | "wrap_around"
13358            // ── string (batch 2) ────────────────────────────────────────────
13359            | "rot13" | "rot47" | "caesar_shift" | "reverse_words"
13360            | "count_vowels" | "count_consonants" | "is_vowel" | "is_consonant"
13361            | "first_word" | "last_word"
13362            | "left_str" | "head_str" | "right_str" | "tail_str" | "mid_str"
13363            | "lowercase" | "uppercase"
13364            | "pascal_case" | "pc_case"
13365            | "constant_case" | "upper_snake" | "dot_case" | "path_case"
13366            | "is_palindrome" | "hamming_distance"
13367            | "longest_common_prefix" | "lcp"
13368            | "ascii_ord" | "ascii_chr" | "count_char" | "indexes_of"
13369            | "replace_first" | "replace_all_str"
13370            | "contains_any" | "contains_all"
13371            | "starts_with_any" | "ends_with_any"
13372            // ── predicates (batch 2) ────────────────────────────────────────
13373            | "is_pair" | "is_triple"
13374            | "is_sorted" | "is_asc" | "is_sorted_desc" | "is_desc"
13375            | "is_empty_arr" | "is_empty_hash"
13376            | "is_subset" | "is_superset" | "is_permutation"
13377            // ── collection (batch 2) ────────────────────────────────────────
13378            | "first_eq" | "last_eq"
13379            | "index_of" | "last_index_of" | "positions_of"
13380            | "batch" | "binary_search" | "bsearch" | "linear_search" | "lsearch"
13381            | "distinct_count" | "longest" | "shortest"
13382            | "array_union" | "list_union"
13383            | "array_intersection" | "list_intersection"
13384            | "array_difference" | "list_difference"
13385            | "symmetric_diff" | "group_of_n" | "chunk_n"
13386            | "repeat_list" | "cycle_n" | "random_sample" | "sample_n"
13387            // ── hash ops (batch 2) ──────────────────────────────────────────
13388            | "pick_keys" | "pick" | "omit_keys" | "omit"
13389            | "map_keys_fn" | "map_values_fn"
13390            | "hash_size" | "hash_from_pairs" | "pairs_from_hash"
13391            | "hash_eq" | "keys_sorted" | "values_sorted" | "remove_keys"
13392            // ── date (batch 2) ──────────────────────────────────────────────
13393            | "today" | "yesterday" | "tomorrow" | "is_weekend" | "is_weekday"
13394            // ── json helpers ────────────────────────────────────────────────
13395            | "json_pretty" | "json_minify" | "escape_json" | "json_escape"
13396            // ── process / env ───────────────────────────────────────────────
13397            | "cmd_exists" | "env_get" | "env_has" | "env_keys"
13398            | "argc" | "script_name"
13399            | "has_stdin_tty" | "has_stdout_tty" | "has_stderr_tty"
13400            // ── id helpers ──────────────────────────────────────────────────
13401            | "uuid_v4" | "nanoid" | "short_id" | "is_uuid" | "token"
13402            // ── url / email parts ───────────────────────────────────────────
13403            | "email_domain" | "email_local"
13404            | "url_host" | "url_path" | "url_query" | "url_scheme"
13405            // ── file stat / path ────────────────────────────────────────────
13406            | "file_size" | "fsize" | "file_mtime" | "mtime"
13407            | "file_atime" | "atime" | "file_ctime" | "ctime"
13408            | "is_symlink" | "is_readable" | "is_writable" | "is_executable"
13409            | "path_is_abs" | "path_is_rel"
13410            // ── stats / sort / array / format / cmp / regex / time conv / volume / force ──
13411            | "min_max" | "percentile" | "harmonic_mean" | "geometric_mean" | "zscore"
13412            | "sorted" | "sorted_desc" | "sorted_nums" | "sorted_by_length"
13413            | "reverse_list" | "list_reverse"
13414            | "without" | "without_nth" | "take_last" | "drop_last"
13415            | "pairwise" | "zipmap"
13416            | "format_bytes" | "human_bytes"
13417            | "format_duration" | "human_duration"
13418            | "format_number" | "group_number"
13419            | "format_percent" | "pad_number"
13420            | "spaceship" | "cmp_num" | "cmp_str"
13421            | "compare_versions" | "version_cmp"
13422            | "hash_insert" | "hash_update" | "hash_delete"
13423            | "matches_regex" | "re_match"
13424            | "count_regex_matches" | "regex_extract"
13425            | "regex_split_str" | "regex_replace_str"
13426            | "shuffle_chars" | "random_char" | "nth_word"
13427            | "head_lines" | "tail_lines" | "count_substring"
13428            | "is_valid_hex" | "hex_upper" | "hex_lower"
13429            | "ms_to_s" | "s_to_ms" | "ms_to_ns" | "ns_to_ms"
13430            | "us_to_ns" | "ns_to_us"
13431            | "liters_to_gallons" | "gallons_to_liters"
13432            | "liters_to_ml" | "ml_to_liters"
13433            | "cups_to_ml" | "ml_to_cups"
13434            | "newtons_to_lbf" | "lbf_to_newtons"
13435            | "joules_to_cal" | "cal_to_joules"
13436            | "watts_to_hp" | "hp_to_watts"
13437            | "pascals_to_psi" | "psi_to_pascals"
13438            | "bar_to_pascals" | "pascals_to_bar"
13439            // ── algebraic match ─────────────────────────────────────────────
13440            | "match"
13441            // ── clojure stdlib (only names not matched above) ─────────────────
13442            | "fst" | "rest" | "rst" | "second" | "snd"
13443            | "last_clj" | "lastc" | "butlast" | "bl"
13444            | "ffirst" | "ffs" | "fnext" | "fne" | "nfirst" | "nfs" | "nnext" | "nne"
13445            | "cons" | "conj"
13446            | "peek_clj" | "pkc" | "pop_clj" | "popc"
13447            | "some" | "not_any" | "not_every"
13448            | "comp" | "compose" | "partial" | "constantly" | "complement" | "compl"
13449            | "fnil" | "juxt"
13450            | "memoize" | "memo" | "curry" | "once"
13451            | "deep_clone" | "dclone" | "deep_merge" | "dmerge" | "deep_equal" | "deq"
13452            | "iterate" | "iter" | "repeatedly" | "rptd" | "cycle" | "cyc"
13453            | "mapcat" | "mcat" | "keep" | "kp" | "remove_clj" | "remc"
13454            | "reductions" | "rdcs"
13455            | "partition_by" | "pby" | "partition_all" | "pall"
13456            | "split_at" | "spat" | "split_with" | "spw"
13457            | "assoc" | "dissoc" | "get_in" | "gin" | "assoc_in" | "ain" | "update_in" | "uin"
13458            | "into" | "empty_clj" | "empc" | "seq" | "vec_clj" | "vecc"
13459            | "apply" | "appl"
13460            // ── python/ruby stdlib ───────────────────────────────────────────
13461            | "divmod" | "dm" | "accumulate" | "accum" | "starmap" | "smap"
13462            | "zip_longest" | "zipl" | "zip_fill" | "zipf" | "combinations" | "comb" | "permutations" | "perm"
13463            | "cartesian_product" | "cprod" | "compress" | "cmpr" | "filterfalse" | "falf"
13464            | "islice" | "isl" | "chain_from" | "chfr" | "pairwise_iter" | "pwi"
13465            | "tee_iter" | "teei" | "groupby_iter" | "gbi"
13466            | "each_slice" | "eslice" | "each_cons" | "econs"
13467            | "one" | "none_match" | "nonem"
13468            | "find_index_fn" | "fidx" | "rindex_fn" | "ridx"
13469            | "minmax" | "mmx" | "minmax_by" | "mmxb"
13470            | "dig" | "values_at" | "vat" | "fetch_val" | "fv" | "slice_arr" | "sla"
13471            | "transform_keys" | "tkeys" | "transform_values" | "tvals"
13472            | "sum_by" | "sumb" | "uniq_by" | "uqb"
13473            | "flat_map_fn" | "fmf" | "then_fn" | "thfn" | "times_fn" | "timf"
13474            | "step" | "upto" | "downto"
13475            // ── javascript array/object methods ─────────────────────────────
13476            | "find_last" | "fndl" | "find_last_index" | "fndli"
13477            | "at_index" | "ati" | "replace_at" | "repa"
13478            | "to_sorted" | "tsrt" | "to_reversed" | "trev" | "to_spliced" | "tspl"
13479            | "flat_depth" | "fltd" | "fill_arr" | "filla" | "includes_val" | "incv"
13480            | "object_keys" | "okeys" | "object_values" | "ovals"
13481            | "object_entries" | "oents" | "object_from_entries" | "ofents"
13482            // ── haskell list functions ──────────────────────────────────────
13483            | "span_fn" | "spanf" | "break_fn" | "brkf" | "group_runs" | "gruns"
13484            | "nub" | "sort_on" | "srton"
13485            | "intersperse_val" | "isp" | "intercalate" | "ical"
13486            | "replicate_val" | "repv" | "elem_of" | "elof" | "not_elem" | "ntelm"
13487            | "lookup_assoc" | "lkpa" | "scanl" | "scanr" | "unfoldr" | "unfr"
13488            // ── rust iterator methods ───────────────────────────────────────
13489            | "find_map" | "fndm" | "filter_map" | "fltm" | "fold_right" | "fldr"
13490            | "partition_either" | "peith" | "try_fold" | "tfld"
13491            | "map_while" | "mapw" | "inspect" | "insp"
13492            // ── ruby enumerable extras ──────────────────────────────────────
13493            | "tally_by" | "talb" | "sole" | "chunk_while" | "chkw" | "count_while" | "cntw"
13494            // ── go/general functional utilities ─────────────────────────────
13495            | "insert_at" | "insa" | "delete_at" | "dela" | "update_at" | "upda"
13496            | "split_on" | "spon" | "words_from" | "wfrm" | "unwords" | "unwds"
13497            | "lines_from" | "lfrm" | "unlines" | "unlns"
13498            | "window_n" | "winn" | "adjacent_pairs" | "adjp"
13499            | "zip_all" | "zall" | "unzip_pairs" | "uzp"
13500            | "interpose" | "ipos" | "partition_n" | "partn"
13501            | "map_indexed" | "mapi" | "reduce_indexed" | "redi" | "filter_indexed" | "flti"
13502            | "group_by_fn" | "gbf" | "index_by" | "idxb" | "associate" | "assoc_fn"
13503            // ── additional missing stdlib functions ─────────────────────────
13504            | "combinations_rep" | "combrep" | "inits" | "tails" | "subsequences" | "subseqs"
13505            | "nub_by" | "nubb" | "slice_when" | "slcw" | "slice_before" | "slcb" | "slice_after" | "slca"
13506            | "each_with_object" | "ewo" | "reduce_right" | "redr"
13507            | "is_sorted_by" | "issrtb" | "intersperse_with" | "ispw"
13508            | "running_reduce" | "runred" | "windowed_circular" | "wincirc"
13509            | "distinct_by" | "distb" | "average" | "mean" | "copy_within" | "cpyw"
13510            | "and_list" | "andl" | "or_list" | "orl" | "concat_map" | "cmap"
13511            | "elem_index" | "elidx" | "elem_indices" | "elidxs" | "find_indices" | "fndidxs"
13512            | "delete_first" | "delfst" | "delete_by" | "delby" | "insert_sorted" | "inssrt"
13513            | "union_list" | "unionl" | "intersect_list" | "intl"
13514            | "maximum_by" | "maxby" | "minimum_by" | "minby" | "batched" | "btch"
13515            // ── Extended stdlib: Text Processing ─────────────────────────────
13516            | "match_all" | "mall" | "capture_groups" | "capg" | "is_match" | "ism"
13517            | "split_regex" | "splre" | "replace_regex" | "replre"
13518            | "is_ascii" | "isasc" | "to_ascii" | "toasc"
13519            | "char_at" | "chat" | "code_point_at" | "cpat" | "from_code_point" | "fcp"
13520            | "normalize_spaces" | "nrmsp" | "remove_whitespace" | "rmws"
13521            | "pluralize" | "plur" | "ordinalize" | "ordn"
13522            | "parse_int" | "pint" | "parse_float" | "pflt" | "parse_bool" | "pbool"
13523            | "levenshtein" | "lev" | "soundex" | "sdx" | "similarity" | "sim"
13524            | "common_prefix" | "cpfx" | "common_suffix" | "csfx"
13525            | "wrap_text" | "wrpt" | "dedent" | "ddt" | "indent" | "idt"
13526            // ── Extended stdlib: Advanced Numeric ────────────────────────────
13527            | "lerp" | "inv_lerp" | "ilerp" | "smoothstep" | "smst" | "remap"
13528            | "dot_product" | "dotp" | "cross_product" | "crossp"
13529            | "matrix_mul" | "matmul" | "mm"
13530            | "magnitude" | "mag" | "normalize_vec" | "nrmv"
13531            | "distance" | "dist" | "manhattan_distance" | "mdist"
13532            | "covariance" | "cov" | "correlation" | "corr"
13533            | "iqr" | "quantile" | "qntl" | "clamp_int" | "clpi"
13534            | "in_range" | "inrng" | "wrap_range" | "wrprng"
13535            | "sum_squares" | "sumsq" | "rms" | "cumsum" | "csum" | "cumprod" | "cprod_acc" | "diff"
13536            // ── Extended stdlib: Date/Time ───────────────────────────────────
13537            | "add_days" | "addd" | "add_hours" | "addh" | "add_minutes" | "addm"
13538            | "diff_days" | "diffd" | "diff_hours" | "diffh"
13539            | "start_of_day" | "sod" | "end_of_day" | "eod"
13540            | "start_of_hour" | "soh" | "start_of_minute" | "som"
13541            // ── Extended stdlib: Encoding/Hashing ────────────────────────────
13542            | "urle" | "urld"
13543            | "html_encode" | "htmle" | "html_decode" | "htmld"
13544            | "adler32" | "adl32" | "fnv1a" | "djb2"
13545            // ── Extended stdlib: Validation ──────────────────────────────────
13546            | "is_credit_card" | "iscc" | "is_isbn10" | "isbn10" | "is_isbn13" | "isbn13"
13547            | "is_iban" | "isiban" | "is_hex_str" | "ishex" | "is_binary_str" | "isbin"
13548            | "is_octal_str" | "isoct" | "is_json" | "isjson" | "is_base64" | "isb64"
13549            | "is_semver" | "issv" | "is_slug" | "isslug" | "slugify" | "slug"
13550            // ── Extended stdlib: Collection Advanced ─────────────────────────
13551            | "mode_stat" | "mstat" | "sampn" | "weighted_sample" | "wsamp"
13552            | "shuffle_arr" | "shuf" | "argmax" | "amax" | "argmin" | "amin"
13553            | "argsort" | "asrt" | "rank" | "rnk" | "dense_rank" | "drnk"
13554            | "partition_point" | "ppt" | "lower_bound" | "lbound"
13555            | "upper_bound" | "ubound" | "equal_range" | "eqrng"
13556            // ── Extended stdlib: Matrix Operations ───────────────────────────
13557            | "matrix_add" | "madd" | "matrix_sub" | "msub" | "matrix_mult" | "mmult"
13558            | "matrix_scalar" | "mscal" | "matrix_identity" | "mident"
13559            | "matrix_zeros" | "mzeros" | "matrix_ones" | "mones"
13560            | "matrix_diag" | "mdiag" | "matrix_trace" | "mtrace"
13561            | "matrix_row" | "mrow" | "matrix_col" | "mcol"
13562            | "matrix_shape" | "mshape" | "matrix_det" | "mdet"
13563            | "matrix_scale" | "mat_scale" | "diagonal" | "diag"
13564            // ── Extended stdlib: Graph Algorithms ────────────────────────────
13565            | "topological_sort" | "toposort" | "bfs_traverse" | "bfs"
13566            | "dfs_traverse" | "dfs" | "shortest_path_bfs" | "spbfs"
13567            | "connected_components_graph" | "ccgraph"
13568            | "has_cycle_graph" | "hascyc" | "is_bipartite_graph" | "isbip"
13569            // ── Extended stdlib: Data Validation ─────────────────────────────
13570            | "is_ipv4_addr" | "isip4" | "is_ipv6_addr" | "isip6"
13571            | "is_mac_addr" | "ismac" | "is_port_num" | "isport"
13572            | "is_hostname_valid" | "ishost"
13573            | "is_iso_date" | "isisodt" | "is_iso_time" | "isisotm"
13574            | "is_iso_datetime" | "isisodtm"
13575            | "is_phone_num" | "isphone" | "is_us_zip" | "iszip"
13576            // ── Extended stdlib: String Utilities Novel ──────────────────────
13577            | "word_wrap_text" | "wwrap" | "center_text" | "ctxt"
13578            | "ljust_text" | "ljt" | "rjust_text" | "rjt" | "zfill_num" | "zfill"
13579            | "remove_all_str" | "rmall" | "replace_n_times" | "repln"
13580            | "find_all_indices" | "fndalli"
13581            | "text_between" | "txbtwn" | "text_before" | "txbef" | "text_after" | "txaft"
13582            | "text_before_last" | "txbefl" | "text_after_last" | "txaftl"
13583            // ── Extended stdlib: Math Novel ──────────────────────────────────
13584            | "is_even_num" | "iseven" | "is_odd_num" | "isodd"
13585            | "is_positive_num" | "ispos" | "is_negative_num" | "isneg"
13586            | "is_zero_num" | "iszero" | "is_whole_num" | "iswhole"
13587            | "log_with_base" | "logb" | "nth_root_of" | "nroot"
13588            | "frac_part" | "fracp" | "reciprocal_of" | "recip"
13589            | "copy_sign" | "cpsgn" | "fused_mul_add" | "fmadd"
13590            | "floor_mod" | "fmod" | "floor_div_op" | "fdivop"
13591            | "signum_of" | "sgnum" | "midpoint_of" | "midpt"
13592            // ── Extended stdlib batch 3: Array Analysis ──────────────────────
13593            | "longest_run" | "lrun" | "longest_increasing" | "linc"
13594            | "longest_decreasing" | "ldec" | "max_sum_subarray" | "maxsub"
13595            | "majority_element" | "majority" | "kth_largest" | "kthl"
13596            | "kth_smallest" | "kths" | "count_inversions" | "cinv"
13597            | "is_monotonic" | "ismono" | "equilibrium_index" | "eqidx"
13598            // ── Extended stdlib batch 3: Set Operations ──────────────────────
13599            | "jaccard_index" | "jaccard" | "dice_coefficient" | "dicecoef"
13600            | "overlap_coefficient" | "overlapcoef"
13601            | "power_set" | "powerset" | "cartesian_power" | "cartpow"
13602            // ── Extended stdlib batch 3: Advanced String ─────────────────────
13603            | "is_isogram" | "isiso" | "is_heterogram" | "ishet"
13604            | "hamdist" | "jaro_similarity" | "jarosim"
13605            | "longest_common_substring" | "lcsub"
13606            | "longest_common_subsequence" | "lcseq"
13607            | "count_words" | "wcount" | "count_lines" | "lcount"
13608            | "count_chars" | "ccount" | "count_bytes" | "bcount"
13609            // ── Extended stdlib batch 3: More Math ───────────────────────────
13610            | "binomial" | "binom" | "catalan" | "catn" | "pascal_row" | "pascrow"
13611            | "is_coprime" | "iscopr" | "euler_totient" | "etot"
13612            | "mobius" | "mob" | "is_squarefree" | "issqfr"
13613            | "digital_root" | "digroot" | "is_narcissistic" | "isnarc"
13614            | "is_harshad" | "isharsh" | "is_kaprekar" | "iskap"
13615            // ── Extended stdlib batch 3: Date/Time Additional ────────────────
13616            | "day_of_year" | "doy" | "week_of_year" | "woy"
13617            | "days_in_month_fn" | "daysinmo" | "is_valid_date" | "isvdate"
13618            | "age_in_years" | "ageyrs"
13619            // ── functional combinators ──────────────────────────────────────
13620
13621            | "when_true" | "when_false" | "if_else" | "clamp_fn"
13622            | "attempt" | "try_fn" | "safe_div" | "safe_mod" | "safe_sqrt" | "safe_log"
13623            | "juxt2" | "juxt3" | "tap_val" | "debug_val" | "converge"
13624            | "iterate_n" | "unfold" | "arity_of" | "is_callable"
13625            | "coalesce" | "default_to" | "fallback"
13626            | "apply_list" | "zip_apply" | "scan"
13627            | "keep_if" | "reject_if" | "group_consecutive"
13628            | "after_n" | "before_n" | "clamp_list" | "normalize_list" | "softmax"
13629
13630            // ── matrix / linear algebra ─────────────────────────────────────
13631
13632
13633            | "matrix_multiply" | "mat_mul"
13634            | "identity_matrix" | "eye" | "zeros_matrix" | "zeros" | "ones_matrix" | "ones"
13635
13636
13637
13638            | "vec_normalize" | "unit_vec" | "vec_add" | "vec_sub" | "vec_scale"
13639            | "linspace" | "arange"
13640            // ── more regex ──────────────────────────────────────────────────
13641            | "re_test" | "re_find_all" | "re_groups" | "re_escape"
13642            | "re_split_limit" | "glob_to_regex" | "is_regex_valid"
13643            // ── more process / system ───────────────────────────────────────
13644            | "cwd" | "pwd_str" | "cpu_count" | "is_root" | "uptime_secs"
13645            | "env_pairs" | "env_set" | "env_remove" | "hostname_str" | "is_tty" | "signal_name"
13646            // ── data structure helpers ───────────────────────────────────────
13647            | "stack_new" | "queue_new" | "lru_new"
13648            | "counter" | "counter_most_common" | "defaultdict" | "ordered_set"
13649            | "bitset_new" | "bitset_set" | "bitset_test" | "bitset_clear"
13650            // ── trivial numeric helpers (batch 4) ─────────────────────────────
13651            | "abs_ceil" | "abs_each" | "abs_floor" | "ceil_each" | "dec_each"
13652            | "double_each" | "floor_each" | "half_each" | "inc_each" | "length_each"
13653            | "negate_each" | "not_each" | "offset_each" | "reverse_each" | "round_each"
13654            | "scale_each" | "sqrt_each" | "square_each" | "to_float_each" | "to_int_each"
13655            | "trim_each" | "type_each" | "upcase_each" | "downcase_each" | "bool_each"
13656            // ── math / physics constants ──────────────────────────────────────
13657            | "avogadro" | "boltzmann" | "golden_ratio" | "gravity" | "ln10" | "ln2"
13658            | "planck" | "speed_of_light" | "sqrt2"
13659            // ── physics formulas ──────────────────────────────────────────────
13660            | "bmi_calc" | "compound_interest" | "dew_point" | "discount_amount"
13661            | "force_mass_acc" | "freq_wavelength" | "future_value" | "haversine"
13662            | "heat_index" | "kinetic_energy" | "margin_price" | "markup_price"
13663            | "mortgage_payment" | "ohms_law_i" | "ohms_law_r" | "ohms_law_v"
13664            | "potential_energy" | "present_value" | "simple_interest" | "speed_distance_time"
13665            | "tax_amount" | "tip_amount" | "wavelength_freq" | "wind_chill"
13666            // ── math functions ────────────────────────────────────────────────
13667            | "angle_between_deg" | "approx_eq" | "chebyshev_distance" | "copysign"
13668            | "cosine_similarity" | "cube_root" | "entropy" | "float_bits" | "fma"
13669            | "int_bits" | "jaccard_similarity" | "log_base" | "mae" | "mse" | "nth_root"
13670            | "r_squared" | "reciprocal" | "relu" | "rmse" | "rotate_point" | "round_to"
13671            | "sigmoid" | "signum" | "square_root"
13672            // ── sequences ─────────────────────────────────────────────────────
13673            | "cubes_seq" | "fibonacci_seq" | "powers_of_seq" | "primes_seq"
13674            | "squares_seq" | "triangular_seq"
13675            // ── string helpers (batch 4) ──────────────────────────────────────
13676            | "alternate_case" | "angle_bracket" | "bracket" | "byte_length"
13677            | "bytes_to_hex_str" | "camel_words" | "char_length" | "chars_to_string"
13678            | "chomp_str" | "chop_str" | "filter_chars" | "from_csv_line" | "hex_to_bytes"
13679            | "insert_str" | "intersperse_char" | "ljust" | "map_chars" | "mirror_string"
13680            | "normalize_whitespace" | "only_alnum" | "only_alpha" | "only_ascii"
13681            | "only_digits" | "parenthesize" | "remove_str" | "repeat_string" | "rjust"
13682            | "sentence_case" | "string_count" | "string_sort" | "string_to_chars"
13683            | "string_unique_chars" | "substring" | "to_csv_line" | "trim_left" | "trim_right"
13684            | "xor_strings"
13685            // ── list helpers (batch 4) ─────────────────────────────────────────
13686            | "adjacent_difference" | "append_elem" | "consecutive_pairs" | "contains_elem"
13687            | "count_elem" | "drop_every" | "duplicate_count" | "elem_at" | "find_first"
13688            | "first_elem" | "flatten_once" | "fold_left" | "from_digits" | "from_pairs"
13689            | "group_by_size" | "hash_filter_keys" | "hash_from_list" | "hash_map_values"
13690            | "hash_merge_deep" | "hash_to_list" | "hash_zip" | "head_n" | "histogram_bins"
13691            | "index_of_elem" | "init_list" | "interleave_lists" | "last_elem" | "least_common"
13692            | "list_compact" | "list_eq" | "list_flatten_deep" | "max_list" | "mean_list"
13693            | "min_list" | "mode_list" | "most_common" | "partition_two" | "prefix_sums"
13694            | "prepend" | "product_list" | "remove_at" | "remove_elem" | "remove_first_elem"
13695            | "repeat_elem" | "running_max" | "running_min" | "sample_one" | "scan_left"
13696            | "second_elem" | "span" | "suffix_sums" | "sum_list" | "tail_n" | "take_every"
13697            | "third_elem" | "to_array" | "to_pairs" | "trimmed_mean" | "unique_count_of"
13698            | "wrap_index" | "digits_of"
13699            // ── predicates (batch 4) ──────────────────────────────────────────
13700            | "all_match" | "any_match" | "is_between" | "is_blank_or_nil" | "is_divisible_by"
13701            | "is_email" | "is_even" | "is_falsy" | "is_fibonacci" | "is_hex_color"
13702            | "is_in_range" | "is_ipv4" | "is_multiple_of" | "is_negative" | "is_nil"
13703            | "is_nonzero" | "is_odd" | "is_perfect_square" | "is_positive" | "is_power_of"
13704            | "is_prefix" | "is_present" | "is_strictly_decreasing" | "is_strictly_increasing"
13705            | "is_suffix" | "is_triangular" | "is_truthy" | "is_url" | "is_whole" | "is_zero"
13706            // ── counters (batch 4) ────────────────────────────────────────────
13707            | "count_digits" | "count_letters" | "count_lower" | "count_match"
13708            | "count_punctuation" | "count_spaces" | "count_upper" | "defined_count"
13709            | "empty_count" | "falsy_count" | "nonempty_count" | "numeric_count"
13710            | "truthy_count" | "undef_count"
13711            // ── conversion / utility (batch 4) ────────────────────────────────
13712            | "assert_type" | "between" | "clamp_each" | "die_if" | "die_unless"
13713            | "join_colons" | "join_commas" | "join_dashes" | "join_dots" | "join_lines"
13714            | "join_pipes" | "join_slashes" | "join_spaces" | "join_tabs" | "measure"
13715            | "max_float" | "min_float" | "noop_val" | "nop" | "pass" | "pred" | "succ"
13716            | "tap_debug" | "to_bool" | "to_float" | "to_int" | "to_string" | "void"
13717            | "range_exclusive" | "range_inclusive"
13718            // ── math / numeric (uncategorized batch) ────────────────────────────
13719            | "aliquot_sum" | "autocorrelation" | "bell_number" | "cagr" | "coeff_of_variation"
13720            | "collatz_length" | "collatz_sequence" | "convolution" | "cross_entropy"
13721            | "depreciation_double" | "depreciation_linear" | "discount" | "divisors"
13722            | "epsilon" | "euclidean_distance" | "euler_number" | "exponential_moving_average"
13723            | "f64_max" | "f64_min" | "fft_magnitude" | "goldbach" | "i64_max" | "i64_min"
13724            | "kurtosis" | "linear_regression" | "look_and_say" | "lucas" | "luhn_check"
13725            | "mean_absolute_error" | "mean_squared_error" | "median_absolute_deviation"
13726            | "minkowski_distance" | "moving_average" | "multinomial" | "neg_inf" | "npv"
13727            | "num_divisors" | "partition_number" | "pascals_triangle" | "skewness"
13728            | "standard_error" | "subfactorial" | "sum_divisors" | "totient_sum"
13729            | "tribonacci" | "weighted_mean" | "winsorize"
13730            // ── statistics (extended) ─────────────────────────────────────────
13731            | "chi_square_stat" | "describe" | "five_number_summary"
13732            | "gini" | "gini_coefficient" | "lorenz_curve" | "outliers_iqr"
13733            | "percentile_rank" | "quartiles" | "sample_stddev" | "sample_variance"
13734            | "spearman_correlation" | "t_test_one_sample" | "t_test_two_sample"
13735            | "z_score" | "z_scores"
13736            // ── number theory / primes ──────────────────────────────────────────
13737            | "abundant_numbers" | "deficient_numbers" | "is_abundant" | "is_deficient"
13738            | "is_pentagonal" | "is_perfect" | "is_smith" | "next_prime" | "nth_prime"
13739            | "pentagonal_number" | "perfect_numbers" | "prev_prime" | "prime_factors"
13740            | "prime_pi" | "primes_up_to" | "triangular_number" | "twin_primes"
13741            // ── geometry / physics ──────────────────────────────────────────────
13742            | "area_circle" | "area_ellipse" | "area_rectangle" | "area_trapezoid" | "area_triangle"
13743            | "bearing" | "circumference" | "cone_volume" | "cylinder_volume" | "heron_area"
13744            | "midpoint" | "perimeter_rectangle" | "perimeter_triangle" | "point_distance"
13745            | "polygon_area" | "slope" | "sphere_surface" | "sphere_volume" | "triangle_hypotenuse"
13746            // ── geometry (extended) ───────────────────────────────────────────
13747            | "angle_between" | "arc_length" | "bounding_box" | "centroid"
13748            | "circle_from_three_points" | "convex_hull" | "ellipse_perimeter"
13749            | "frustum_volume" | "haversine_distance" | "line_intersection"
13750            | "point_in_polygon" | "polygon_perimeter" | "pyramid_volume"
13751            | "reflect_point" | "scale_point" | "sector_area"
13752            | "torus_surface" | "torus_volume" | "translate_point"
13753            | "vector_angle" | "vector_cross" | "vector_dot" | "vector_magnitude" | "vector_normalize"
13754            // ── constants ───────────────────────────────────────────────────────
13755            | "avogadro_number" | "boltzmann_constant" | "electron_mass" | "elementary_charge"
13756            | "gravitational_constant" | "phi" | "pi" | "planck_constant" | "proton_mass"
13757            | "sol" | "tau"
13758            // ── finance ─────────────────────────────────────────────────────────
13759            | "bac_estimate" | "bmi" | "break_even" | "margin" | "markup" | "roi" | "tax" | "tip"
13760            // ── finance (extended) ────────────────────────────────────────────
13761            | "amortization_schedule" | "black_scholes_call" | "black_scholes_put"
13762            | "bond_price" | "bond_yield" | "capm" | "continuous_compound"
13763            | "discounted_payback" | "duration" | "irr"
13764            | "max_drawdown" | "modified_duration" | "nper" | "num_periods" | "payback_period"
13765            | "pmt" | "pv" | "rule_of_72" | "sharpe_ratio" | "sortino_ratio"
13766            | "wacc" | "xirr"
13767            // ── string processing (uncategorized batch) ─────────────────────────
13768            | "acronym" | "atbash" | "bigrams" | "camel_to_snake" | "char_frequencies"
13769            | "chunk_string" | "collapse_whitespace" | "dedent_text" | "indent_text"
13770            | "initials" | "leetspeak" | "mask_string" | "ngrams" | "pig_latin"
13771            | "remove_consonants" | "remove_vowels" | "reverse_each_word" | "snake_to_camel"
13772            | "sort_words" | "string_distance" | "string_multiply" | "strip_html"
13773            | "trigrams" | "unique_words" | "word_frequencies" | "zalgo"
13774            // ── encoding / phonetics ────────────────────────────────────────────
13775            | "braille_encode" | "double_metaphone" | "metaphone" | "morse_decode"
13776            | "morse_encode" | "nato_phonetic" | "phonetic_digit" | "subscript" | "superscript"
13777            | "to_emoji_num"
13778            // ── roman numerals ──────────────────────────────────────────────────
13779            | "int_to_roman" | "roman_add" | "roman_numeral_list" | "roman_to_int"
13780            // ── base / gray code ────────────────────────────────────────────────
13781            | "base_convert" | "binary_to_gray" | "gray_code_sequence" | "gray_to_binary"
13782            // ── color operations ────────────────────────────────────────────────
13783            | "ansi_256" | "ansi_truecolor" | "color_blend" | "color_complement"
13784            | "color_darken" | "color_distance" | "color_grayscale" | "color_invert"
13785            | "color_lighten" | "hsl_to_rgb" | "hsv_to_rgb" | "random_color"
13786            | "rgb_to_hsl" | "rgb_to_hsv"
13787            // ── matrix operations (uncategorized batch) ─────────────────────────
13788            | "matrix_flatten" | "matrix_from_rows" | "matrix_hadamard" | "matrix_inverse"
13789            | "matrix_map" | "matrix_max" | "matrix_min" | "matrix_power" | "matrix_sum"
13790            | "matrix_transpose"
13791            // ── array / list operations (uncategorized batch) ───────────────────
13792            | "binary_insert" | "bucket" | "clamp_array" | "group_consecutive_by"
13793            | "histogram" | "merge_sorted" | "next_permutation" | "normalize_array"
13794            | "normalize_range" | "peak_detect" | "range_compress" | "range_expand"
13795            | "reservoir_sample" | "run_length_decode_str" | "run_length_encode_str"
13796            | "zero_crossings"
13797            // ── DSP / signal (extended) ───────────────────────────────────────
13798            | "apply_window" | "bandpass_filter" | "cross_correlation" | "dft"
13799            | "downsample" | "energy" | "envelope" | "highpass_filter" | "idft"
13800            | "lowpass_filter" | "median_filter" | "normalize_signal" | "phase_spectrum"
13801            | "power_spectrum" | "resample" | "spectral_centroid" | "spectrogram" | "upsample"
13802            | "window_blackman" | "window_hamming" | "window_hann" | "window_kaiser"
13803            // ── validation predicates (uncategorized batch) ─────────────────────
13804            | "is_anagram" | "is_balanced_parens" | "is_control" | "is_numeric_string"
13805            | "is_pangram" | "is_printable" | "is_valid_cidr" | "is_valid_cron"
13806            | "is_valid_hex_color" | "is_valid_latitude" | "is_valid_longitude" | "is_valid_mime"
13807            // ── algorithms / puzzles ────────────────────────────────────────────
13808            | "eval_rpn" | "fizzbuzz" | "game_of_life_step" | "mandelbrot_char"
13809            | "sierpinski" | "tower_of_hanoi" | "truth_table"
13810            // ── misc / utility ──────────────────────────────────────────────────
13811            | "byte_size" | "degrees_to_compass" | "to_string_val" | "type_of"
13812            // ── math formulas ───────────────────────────────────────────────────
13813            | "quadratic_roots" | "quadratic_discriminant" | "arithmetic_series"
13814            | "geometric_series" | "stirling_approx"
13815            | "double_factorial" | "rising_factorial" | "falling_factorial"
13816            | "gamma_approx" | "erf_approx" | "normal_pdf" | "normal_cdf"
13817            | "poisson_pmf" | "exponential_pdf" | "inverse_lerp"
13818            | "map_range"
13819            // ── physics formulas ────────────────────────────────────────────────
13820            | "momentum" | "impulse" | "work" | "power_phys" | "torque" | "angular_velocity"
13821            | "centripetal_force" | "escape_velocity" | "orbital_velocity" | "orbital_period"
13822            | "gravitational_force" | "coulomb_force" | "electric_field" | "capacitance"
13823            | "capacitor_energy" | "inductor_energy" | "resonant_frequency"
13824            | "rc_time_constant" | "rl_time_constant" | "impedance_rlc"
13825            | "relativistic_mass" | "lorentz_factor" | "time_dilation" | "length_contraction"
13826            | "relativistic_energy" | "rest_energy" | "de_broglie_wavelength"
13827            | "photon_energy" | "photon_energy_wavelength" | "schwarzschild_radius"
13828            | "stefan_boltzmann" | "wien_displacement" | "ideal_gas_pressure" | "ideal_gas_volume"
13829            | "projectile_range" | "projectile_max_height" | "projectile_time"
13830            | "spring_force" | "spring_energy" | "pendulum_period" | "doppler_frequency"
13831            | "decibel_ratio" | "snells_law" | "brewster_angle" | "critical_angle"
13832            | "lens_power" | "thin_lens" | "magnification_lens"
13833            // ── math constants ──────────────────────────────────────────────────
13834            | "euler_mascheroni" | "apery_constant" | "feigenbaum_delta" | "feigenbaum_alpha"
13835            | "catalan_constant" | "khinchin_constant" | "glaisher_constant"
13836            | "plastic_number" | "silver_ratio" | "supergolden_ratio"
13837            // ── physics constants ───────────────────────────────────────────────
13838            | "vacuum_permittivity" | "vacuum_permeability" | "coulomb_constant"
13839            | "fine_structure_constant" | "rydberg_constant" | "bohr_radius"
13840            | "bohr_magneton" | "nuclear_magneton" | "stefan_boltzmann_constant"
13841            | "wien_constant" | "gas_constant" | "faraday_constant" | "neutron_mass"
13842            | "atomic_mass_unit" | "earth_mass" | "earth_radius" | "sun_mass" | "sun_radius"
13843            | "astronomical_unit" | "light_year" | "parsec" | "hubble_constant"
13844            | "planck_length" | "planck_time" | "planck_mass" | "planck_temperature"
13845            // ── linear algebra (extended) ──────────────────────────────────
13846            | "matrix_solve" | "msolve" | "solve"
13847            | "matrix_lu" | "mlu" | "matrix_qr" | "mqr"
13848            | "matrix_eigenvalues" | "meig" | "eigenvalues" | "eig"
13849            | "matrix_norm" | "mnorm" | "matrix_cond" | "mcond" | "cond"
13850            | "matrix_pinv" | "mpinv" | "pinv"
13851            | "matrix_cholesky" | "mchol" | "cholesky"
13852            | "matrix_det_general" | "mdetg" | "det"
13853            // ── statistics tests (extended) ────────────────────────────────
13854            | "welch_ttest" | "welcht" | "paired_ttest" | "pairedt"
13855            | "cohen_d" | "cohend" | "anova_oneway" | "anova" | "anova1"
13856            | "spearman_corr" | "rho" | "kendall_tau" | "kendall" | "ktau"
13857            | "confidence_interval" | "ci"
13858            // ── distributions (extended) ──────────────────────────────────
13859            | "beta_pdf" | "betapdf" | "gamma_pdf" | "gammapdf"
13860            | "chi2_pdf" | "chi2pdf" | "chi_squared_pdf"
13861            | "t_pdf" | "tpdf" | "student_pdf"
13862            | "f_pdf" | "fpdf" | "fisher_pdf"
13863            | "lognormal_pdf" | "lnormpdf" | "weibull_pdf" | "weibpdf"
13864            | "cauchy_pdf" | "cauchypdf" | "laplace_pdf" | "laplacepdf"
13865            | "pareto_pdf" | "paretopdf"
13866            // ── interpolation & curve fitting ─────────────────────────────
13867            | "lagrange_interp" | "lagrange" | "linterp"
13868            | "cubic_spline" | "cspline" | "spline"
13869            | "poly_eval" | "polyval" | "polynomial_fit" | "polyfit"
13870            // ── numerical integration & differentiation ───────────────────
13871            | "trapz" | "trapezoid" | "simpson" | "simps"
13872            | "numerical_diff" | "numdiff" | "diff_array"
13873            | "cumtrapz" | "cumulative_trapz"
13874            // ── optimization / root finding ────────────────────────────────
13875            | "bisection" | "bisect" | "newton_method" | "newton" | "newton_raphson"
13876            | "golden_section" | "golden" | "gss"
13877            // ── ODE solvers ───────────────────────────────────────────────
13878            | "rk4" | "runge_kutta" | "rk4_ode" | "euler_ode" | "euler_method"
13879            // ── graph algorithms (extended) ────────────────────────────────
13880            | "dijkstra" | "shortest_path" | "bellman_ford" | "bellmanford"
13881            | "floyd_warshall" | "floydwarshall" | "apsp"
13882            | "prim_mst" | "mst" | "prim"
13883            // ── trig extensions ───────────────────────────────────────────
13884            | "cot" | "sec" | "csc" | "acot" | "asec" | "acsc" | "sinc" | "versin" | "versine"
13885            // ── ML activation functions ───────────────────────────────────
13886            | "leaky_relu" | "lrelu" | "elu" | "selu" | "gelu"
13887            | "silu" | "swish" | "mish" | "softplus"
13888            | "hard_sigmoid" | "hardsigmoid" | "hard_swish" | "hardswish"
13889            // ── special functions ─────────────────────────────────────────
13890            | "bessel_j0" | "j0" | "bessel_j1" | "j1"
13891            | "lambert_w" | "lambertw" | "productlog"
13892            // ── Wolfram-Math parity: Bessel/Airy/Hankel/Struve/Kelvin ─────
13893            | "bessel_j" | "bessel_y" | "bessel_i" | "bessel_k"
13894            | "hankel_h1" | "hankel_h2" | "bessel_j_zero"
13895            | "airy_ai" | "airy_bi" | "airy_ai_prime" | "airy_bi_prime"
13896            | "spherical_bessel_j" | "spherical_bessel_y"
13897            | "struve_h" | "struve_l" | "kelvin_ber" | "kelvin_bei"
13898            // ── orthogonal polynomials ────────────────────────────────────
13899            | "legendre_p" | "legendre_q" | "assoc_legendre_p"
13900            | "hermite_h" | "hermite_he" | "laguerre_l" | "assoc_laguerre_l"
13901            | "jacobi_p" | "gegenbauer_c" | "chebyshev_t" | "chebyshev_u"
13902            | "spherical_harmonic_y" | "zernike_r"
13903            // ── elliptic integrals + Jacobi/Weierstrass/theta ─────────────
13904            | "elliptic_k" | "elliptic_e" | "elliptic_pi" | "elliptic_f"
13905            | "elliptic_e_inc" | "elliptic_pi_inc"
13906            | "carlson_rf" | "carlson_rd" | "carlson_rj"
13907            | "jacobi_sn" | "jacobi_cn" | "jacobi_dn" | "jacobi_am"
13908            | "elliptic_theta"
13909            | "weierstrass_p" | "weierstrass_zeta" | "weierstrass_sigma"
13910            // ── zeta / polylog / Lerch ────────────────────────────────────
13911            | "zeta" | "riemann_zeta" | "hurwitz_zeta"
13912            | "polylog" | "dilog" | "lerch_phi"
13913            | "riemann_siegel_z" | "riemann_siegel_theta"
13914            | "dirichlet_eta" | "dirichlet_beta"
13915            // ── hypergeometric ────────────────────────────────────────────
13916            | "hypergeometric_2f1" | "hyper_2f1"
13917            | "hypergeometric_1f1" | "hyper_1f1" | "kummer_m"
13918            | "hypergeometric_0f1" | "hyper_0f1"
13919            | "hypergeometric_pfq" | "hyper_pfq"
13920            | "hypergeometric_u" | "tricomi_u"
13921            // ── modular forms ─────────────────────────────────────────────
13922            | "dedekind_eta" | "klein_j" | "klein_invariant_j"
13923            | "modular_lambda" | "ramanujan_tau"
13924            // ── integrals: Si / Ci / Ei / Li / Fresnel ────────────────────
13925            | "sin_integral" | "si_int" | "cos_integral" | "ci_int"
13926            | "sinh_integral" | "shi_int" | "cosh_integral" | "chi_int"
13927            | "exp_integral_e" | "ei_n" | "exp_integral_ei" | "ei_int"
13928            | "log_integral" | "li_int" | "fresnel_s" | "fresnel_c"
13929            // ── number-theory gaps ────────────────────────────────────────
13930            | "jacobi_symbol" | "kronecker_symbol"
13931            | "primitive_root" | "multiplicative_order"
13932            | "mangoldt_lambda" | "von_mangoldt" | "carmichael_lambda"
13933            | "squares_r" | "thue_morse" | "rudin_shapiro"
13934            | "farey_sequence" | "farey"
13935            | "frobenius_number" | "frobenius_solve" | "stern_brocot"
13936            // ── combinatorial gaps ────────────────────────────────────────
13937            | "stirling_s1" | "stirling_first" | "bell_polynomial_b" | "bell_y"
13938            | "clebsch_gordan" | "three_j_symbol" | "wigner_3j"
13939            | "six_j_symbol" | "wigner_6j" | "nine_j_symbol" | "wigner_9j"
13940            | "debruijn_sequence" | "debruijn" | "wigner_d"
13941            // ── q-series, Mittag-Leffler, Coulomb wave ────────────────────
13942            | "q_pochhammer" | "q_factorial" | "q_binomial"
13943            | "q_hypergeometric_pfq"
13944            | "mittag_leffler_e" | "mittag_leffler"
13945            | "coulomb_wave_f" | "coulomb_wave_g"
13946            // ── inverse special functions ─────────────────────────────────
13947            | "inverse_erf" | "erfinv" | "inverse_erfc" | "erfcinv"
13948            | "inverse_gamma_regularized" | "gamma_lr_inv"
13949            | "inverse_beta_regularized" | "beta_reg_inv"
13950            | "inverse_jacobi_sn"
13951            // ── piecewise / symbolic primitives ───────────────────────────
13952            | "dirac_delta" | "heaviside_theta" | "heaviside"
13953            | "unit_box" | "unit_triangle"
13954            | "square_wave" | "triangle_wave" | "sawtooth_wave" | "dirac_comb"
13955            // ── Tier A: number theory extensions ──────────────────────────
13956            | "liouville_lambda" | "jordan_totient" | "ramanujan_sum"
13957            | "cyclotomic_polynomial" | "cyclotomic" | "legendre_symbol"
13958            | "pythagorean_triple_q" | "gen_pythagorean_triple"
13959            | "sophie_germain_q" | "mersenne_q"
13960            | "lucas_lehmer_test" | "lucas_lehmer"
13961            | "continued_fraction" | "from_continued_fraction" | "convergents"
13962            | "best_rational_approximation" | "best_rational"
13963            // ── Tier B: combinatorial sequences ───────────────────────────
13964            | "motzkin_number" | "motzkin"
13965            | "narayana_number" | "narayana"
13966            | "delannoy_number" | "delannoy"
13967            | "schroder_number" | "schroder" | "large_schroder"
13968            | "small_schroder_number" | "small_schroder"
13969            | "eulerian_number"
13970            | "bernoulli_polynomial" | "euler_polynomial"
13971            | "pell_number" | "pell" | "pell_lucas_number" | "pell_lucas"
13972            | "perrin_number" | "perrin" | "padovan_number" | "padovan"
13973            // ── Tier C: linear algebra extras ─────────────────────────────
13974            | "kronecker_product" | "tensor_product" | "tensor_contract"
13975            | "matrix_rank" | "mrank"
13976            | "companion_matrix" | "companion"
13977            | "characteristic_polynomial" | "charpoly"
13978            | "singular_values" | "svals"
13979            | "nullspace" | "null_space" | "kernel"
13980            // ── Tier D: polynomial algebra ────────────────────────────────
13981            | "polynomial_gcd" | "polygcd"
13982            | "polynomial_quotient" | "polyquot"
13983            | "polynomial_remainder" | "polyrem"
13984            | "polynomial_resultant" | "resultant"
13985            | "polynomial_discriminant" | "discriminant"
13986            | "polynomial_roots" | "polyroots"
13987            // ── Tier E: more distributions ────────────────────────────────
13988            | "gumbel_pdf" | "gumbel_cdf" | "gumbel_quantile"
13989            | "frechet_pdf" | "frechet_cdf" | "frechet_quantile"
13990            | "logistic_pdf" | "logistic_cdf" | "logistic_quantile"
13991            | "rayleigh_pdf" | "rayleigh_cdf" | "rayleigh_quantile"
13992            | "inverse_gamma_pdf" | "inverse_gamma_cdf" | "inverse_gamma_quantile"
13993            | "kumaraswamy_pdf" | "kumaraswamy_cdf" | "kumaraswamy_quantile"
13994            // ── Tier F: Mathieu ───────────────────────────────────────────
13995            | "mathieu_a" | "mathieu_characteristic_a"
13996            | "mathieu_ce" | "mathieu_se"
13997            // ── Tier G: Heun general ──────────────────────────────────────
13998            | "heun_g"
13999            // ── Tier H: wavelets ──────────────────────────────────────────
14000            | "haar_transform" | "haar" | "haar_inverse" | "ihaar"
14001            | "daubechies_db4" | "db4" | "daubechies_db4_inverse" | "idb4"
14002            // ── Tier I: graph algorithms ──────────────────────────────────
14003            | "topo_sort_adj"
14004            | "scc_tarjan" | "tarjan_scc" | "strongly_connected"
14005            | "bipartite_q" | "is_bipartite"
14006            | "max_flow_edmonds_karp" | "max_flow" | "edmonds_karp"
14007            | "min_cut" | "eccentricity"
14008            | "graph_diameter" | "graph_radius"
14009            // ── Tier J: misc fillers ──────────────────────────────────────
14010            | "stieltjes_constant" | "stieltjes"
14011            | "gauss_sum" | "kloosterman_sum"
14012            | "eta_quotient" | "root_approximant"
14013            // ── Batch 3: vector calculus ──────────────────────────────────
14014            | "numerical_gradient" | "ngrad"
14015            | "numerical_jacobian" | "njac"
14016            | "numerical_hessian" | "nhess"
14017            | "numerical_divergence" | "ndiv"
14018            | "numerical_curl" | "ncurl"
14019            | "numerical_laplacian" | "nlap"
14020            // ── Batch 3: optimization ─────────────────────────────────────
14021            | "nelder_mead" | "simplex_min"
14022            | "gradient_descent" | "gd_min"
14023            | "bfgs_minimize" | "bfgs"
14024            | "levenberg_marquardt" | "lev_marq" | "lm_min"
14025            | "conjugate_gradient" | "cg_solve"
14026            | "least_squares" | "lstsq"
14027            // ── Batch 3: integration extras ───────────────────────────────
14028            | "romberg" | "romberg_int"
14029            | "gauss_legendre_quad" | "glquad" | "gl_quad"
14030            | "monte_carlo_integrate" | "mc_int"
14031            | "adaptive_simpson" | "asimp"
14032            // ── Batch 3: LA extras ────────────────────────────────────────
14033            | "lu_decompose" | "ludec"
14034            | "qr_decompose" | "qrdec"
14035            | "householder_reflector" | "householder"
14036            | "givens_rotation" | "givens"
14037            | "forward_substitute" | "fwdsub"
14038            | "back_substitute" | "backsub"
14039            | "hessenberg_reduce" | "hessen"
14040            // ── Batch 3: polynomial helpers ───────────────────────────────
14041            | "poly_derivative" | "polyder"
14042            | "poly_integrate" | "polyint"
14043            | "poly_compose" | "poly_eval_horner" | "horner"
14044            | "pade_approximant" | "pade"
14045            // ── Batch 3: quaternions ──────────────────────────────────────
14046            | "quat_mul" | "quat_conj" | "quat_norm" | "quat_inv"
14047            | "quat_from_axis_angle" | "axis_angle_to_quat"
14048            | "quat_to_axis_angle"
14049            | "quat_to_matrix" | "quat_from_matrix" | "matrix_to_quat"
14050            | "quat_slerp" | "slerp"
14051            | "euler_zyx_to_matrix" | "matrix_to_euler_zyx"
14052            | "rotate_3d_vec"
14053            // ── Batch 3: information theory ───────────────────────────────
14054            | "kl_divergence" | "kl_div"
14055            | "js_divergence" | "js_div"
14056            | "mutual_information" | "mi"
14057            | "cross_entropy_arr" | "cross_entropy_dist"
14058            | "renyi_entropy" | "tsallis_entropy"
14059            // ── Batch 3: quantum ──────────────────────────────────────────
14060            | "pauli_x" | "pauli_y" | "pauli_z"
14061            | "pauli_id" | "pauli_i" | "pauli_identity"
14062            | "ket_bra" | "density_matrix" | "expectation_value" | "expval"
14063            | "commutator" | "anticommutator"
14064            | "partial_trace" | "ptrace"
14065            | "von_neumann_entropy" | "vn_entropy"
14066            // ── Batch 3: stat mech ────────────────────────────────────────
14067            | "bose_einstein" | "fermi_dirac"
14068            | "maxwell_boltzmann_speed" | "mb_speed"
14069            | "partition_function" | "z_partition"
14070            | "helmholtz_free_energy" | "free_energy_f"
14071            | "boltzmann_factor"
14072            | "einstein_specific_heat" | "einstein_cv"
14073            // ── Batch 3: optics ───────────────────────────────────────────
14074            | "fresnel_reflection_te" | "fresnel_reflection_tm"
14075            | "fresnel_transmission_te" | "fresnel_transmission_tm"
14076            | "abcd_thin_lens" | "abcd_free_space"
14077            | "gaussian_beam_q"
14078            // ── Batch 3: astrodynamics ────────────────────────────────────
14079            | "kepler_solve"
14080            | "true_to_eccentric" | "eccentric_to_mean"
14081            | "julian_date" | "j_date"
14082            | "jd_to_gregorian" | "jd_to_date"
14083            | "sidereal_time_gmst" | "gmst"
14084            | "vis_viva" | "orbital_period_kepler"
14085            | "orbital_elements_to_state" | "elem_to_state"
14086            // ── Batch 3: time series ──────────────────────────────────────
14087            | "kalman_step" | "kalman_filter"
14088            | "exponential_smoothing" | "exp_smooth"
14089            | "holt_winters" | "arma_yw_fit" | "ar_yw"
14090            // ── Batch 3: graph centrality ─────────────────────────────────
14091            | "pagerank" | "betweenness_centrality" | "closeness_centrality"
14092            | "eigenvector_centrality" | "degree_centrality" | "triangle_count"
14093            // ── Batch 3: random samplers ──────────────────────────────────
14094            | "rgumbel" | "rfrechet" | "rrayleigh"
14095            | "rlogistic" | "rkumaraswamy" | "rinverse_gamma" | "rinvgamma"
14096            // ── Batch 3: 2D geometry ──────────────────────────────────────
14097            | "graham_scan" | "convex_hull_2d"
14098            | "line_line_intersect_2d" | "ll_intersect_2d"
14099            | "point_segment_distance" | "p_seg_dist"
14100            // ── Batch 4: auto-diff ────────────────────────────────────────
14101            | "forward_diff" | "fdiff"
14102            | "forward_diff_grad" | "fdiff_grad"
14103            // ── Batch 4: stat tests ───────────────────────────────────────
14104            | "bartlett_test" | "levene_test"
14105            | "fishers_exact_test_2x2" | "fishers_exact"
14106            | "mcnemar_test"
14107            | "runs_test" | "wald_wolfowitz"
14108            | "friedman_test" | "kruskal_wallis_test" | "kruskal"
14109            | "sign_test"
14110            | "anderson_darling_normality" | "ad_normality"
14111            | "jarque_bera_test" | "jb_test"
14112            | "ljung_box_test" | "ljung_box"
14113            | "durbin_watson_stat" | "durbin_watson"
14114            // ── Batch 4: distance metrics ─────────────────────────────────
14115            | "mahalanobis_distance" | "mahalanobis_dist"
14116            | "cosine_distance" | "canberra_distance"
14117            | "bray_curtis_distance" | "bray_curtis"
14118            | "l1_distance"
14119            | "chi_squared_distance"
14120            // ── Batch 4: more distributions ───────────────────────────────
14121            | "multivariate_normal_pdf" | "mvn_pdf"
14122            | "multivariate_normal_sample" | "rmvn"
14123            | "dirichlet_pdf" | "dirichlet_sample" | "rdirichlet"
14124            | "skellam_pmf"
14125            | "inverse_gaussian_pdf" | "wald_pdf"
14126            | "inverse_gaussian_cdf" | "wald_cdf"
14127            | "inverse_gaussian_sample" | "rwald"
14128            | "non_central_chi2_pdf" | "ncchi2_pdf"
14129            // ── Batch 4: matrix functions ─────────────────────────────────
14130            | "matrix_exp" | "expm" | "matrix_log" | "logm"
14131            | "matrix_sqrt" | "sqrtm" | "matrix_sin" | "sinm"
14132            | "matrix_cos" | "cosm"
14133            // ── Batch 4: adaptive ODE ─────────────────────────────────────
14134            | "rk45_dormand_prince" | "rk45" | "dopri5"
14135            | "midpoint_step" | "ode_midpoint"
14136            | "heun_step" | "ode_heun"
14137            | "verlet_step" | "ode_verlet"
14138            // ── Batch 4: GLM ──────────────────────────────────────────────
14139            | "logistic_regression" | "logit_fit"
14140            | "poisson_regression"
14141            | "ridge_regression" | "ridge"
14142            | "lasso_coord" | "lasso"
14143            // ── Batch 4: bootstrap/resampling ─────────────────────────────
14144            | "bootstrap_mean_ci" | "boot_mean_ci"
14145            | "jackknife_estimate" | "jackknife"
14146            | "permutation_test_diff" | "perm_test_diff"
14147            // ── Batch 4: time series extras ───────────────────────────────
14148            | "acf_at_lag" | "diff_op" | "lag_op"
14149            | "decompose_classical" | "decompose_ts"
14150            // ── Batch 4: combinatorial generators ─────────────────────────
14151            | "combinations_list" | "permutations_list"
14152            | "cyclic_permutations" | "subsets_of_size"
14153            // ── Batch 4: DP utilities ─────────────────────────────────────
14154            | "longest_increasing_subseq" | "lis"
14155            | "knapsack_01" | "knapsack"
14156            | "subset_sum_target" | "subset_sum"
14157            | "coin_change_min" | "coin_change_minimum"
14158            | "edit_distance_levenshtein" | "edit_distance"
14159            // ── Batch 4: ML metrics ───────────────────────────────────────
14160            | "one_hot_encode" | "onehot" | "label_encode"
14161            | "categorical_cross_entropy" | "cce"
14162            | "classification_metrics" | "binary_metrics"
14163            | "roc_auc" | "auroc"
14164            // ── Batch 4: DSP / image filters ──────────────────────────────
14165            | "gaussian_blur_kernel" | "sobel_x" | "sobel_y"
14166            | "prewitt_x" | "prewitt_y"
14167            | "laplacian_of_gaussian" | "log_kernel"
14168            // ── Batch 4: stochastic processes ─────────────────────────────
14169            | "brownian_path" | "wiener_path"
14170            | "geometric_brownian_path" | "gbm_path"
14171            | "poisson_process" | "random_walk_1d"
14172            // ── Batch 4: compression / info ───────────────────────────────
14173            | "lempel_ziv_complexity" | "lz_complexity"
14174            | "huffman_code_lengths" | "huffman"
14175            | "shannon_entropy_rate" | "block_entropy_rate"
14176            // ── Batch 4: physics / quantum ────────────────────────────────
14177            | "planck_blackbody" | "blackbody"
14178            | "rayleigh_jeans" | "compton_shift"
14179            | "rydberg_energy"
14180            | "hydrogen_radial_wavefunction" | "h_rad_psi"
14181            // ── Batch 4: number theory / algebra ──────────────────────────
14182            | "integer_log" | "ilog"
14183            | "aks_primality" | "aks"
14184            | "elliptic_curve_add" | "ec_add"
14185            | "berlekamp_massey" | "bm_lfsr"
14186            | "bezout_coefficients" | "bezout" | "extended_euclid"
14187            // ── Batch 5: CAS-lite ─────────────────────────────────────────
14188            | "factor_quadratic" | "complete_square"
14189            | "partial_fraction_simple" | "partial_fraction"
14190            // ── Batch 5: more quadrature ──────────────────────────────────
14191            | "gauss_chebyshev_quad" | "gc_quad"
14192            | "gauss_hermite_quad" | "gh_quad"
14193            | "gauss_laguerre_quad" | "glag_quad"
14194            | "clenshaw_curtis_quad" | "cc_quad"
14195            | "tanh_sinh_quad" | "ts_quad"
14196            | "gauss_legendre_2d" | "gl_2d"
14197            | "monte_carlo_2d" | "mc_2d"
14198            // ── Batch 5: more optimization ────────────────────────────────
14199            | "simulated_annealing" | "sa_min"
14200            | "simplex_lp" | "lp_simplex"
14201            | "particle_swarm" | "pso_min"
14202            // ── Batch 5: distributions ────────────────────────────────────
14203            | "gev_pdf" | "gev_cdf" | "gev_sample" | "rgev"
14204            | "gen_pareto_pdf" | "gen_pareto_cdf"
14205            | "gen_pareto_sample" | "rgenpareto"
14206            | "skew_normal_pdf" | "skew_normal_cdf"
14207            | "mixture_normal_pdf"
14208            | "categorical_sample" | "rcat"
14209            | "multinomial_pmf" | "multinomial_sample" | "rmultinom"
14210            | "truncated_normal_pdf"
14211            | "truncated_normal_sample" | "rtnorm"
14212            // ── Batch 5: clustering ───────────────────────────────────────
14213            | "dbscan" | "gmm_em_1d" | "gmm_1d"
14214            | "silhouette_score"
14215            | "davies_bouldin_index" | "db_index"
14216            | "calinski_harabasz_index" | "ch_index"
14217            | "mds_2d" | "pcoa_2d" | "mean_shift"
14218            // ── Batch 5: NN primitives ────────────────────────────────────
14219            | "batch_norm" | "layer_norm"
14220            | "dropout_mask"
14221            | "max_pool_1d" | "avg_pool_1d"
14222            | "attention_softmax" | "positional_encoding"
14223            | "glorot_init" | "xavier_init"
14224            | "he_init" | "kaiming_init"
14225            | "adam_step" | "rmsprop_step"
14226            // ── Batch 5: time series ──────────────────────────────────────
14227            | "ewma" | "ccf" | "periodogram"
14228            | "welch_psd" | "welch"
14229            | "lag_features"
14230            // ── Batch 5: image processing ─────────────────────────────────
14231            | "median_filter_2d"
14232            | "threshold_otsu" | "otsu"
14233            | "histogram_equalize" | "hist_eq"
14234            | "erode_2d" | "dilate_2d"
14235            // ── Batch 5: losses ───────────────────────────────────────────
14236            | "mse_loss" | "mae_loss" | "huber_loss"
14237            // ── Batch 5: spatial ──────────────────────────────────────────
14238            | "vincenty_distance" | "vincenty"
14239            | "mercator_project"
14240            | "destination_from_bearing" | "dest_bearing"
14241            // ── Batch 5: integer sequences ────────────────────────────────
14242            | "recaman" | "recaman_seq"
14243            | "sylvester" | "sylvester_seq"
14244            | "happy_q" | "is_happy"
14245            | "amicable_pair_q"
14246            | "aliquot_sequence"
14247            | "magic_constant"
14248            // ── Batch 5: graph metrics ────────────────────────────────────
14249            | "clustering_coefficient_local" | "cc_local"
14250            | "clustering_coefficient_global" | "cc_global"
14251            | "assortativity" | "common_neighbors" | "jaccard_neighbors"
14252            | "adamic_adar"
14253            | "preferential_attachment_score" | "pa_score"
14254            // ── Batch 5: 3D geometry ──────────────────────────────────────
14255            | "triangle_3d_normal" | "triangle_3d_area"
14256            | "tetrahedron_volume"
14257            | "plane_from_3_points" | "plane_from_pts"
14258            | "point_to_plane_distance" | "pt_plane_dist"
14259            | "ray_triangle_intersect" | "moller_trumbore"
14260            | "ray_sphere_intersect" | "aabb_overlap"
14261            // ── Batch 5: iterative solvers ────────────────────────────────
14262            | "gauss_seidel"
14263            | "jacobi_iteration" | "jacobi_solve"
14264            | "sor_solve" | "sor"
14265            | "thomas_tridiag_solve" | "thomas"
14266            | "richardson_extrapolation" | "richardson"
14267            | "finite_difference_5pt" | "fd5pt"
14268            // ── Batch 5: crypto / algebra ─────────────────────────────────
14269            | "tonelli_shanks_sqrt" | "tonelli_shanks"
14270            | "baby_step_giant_step" | "bsgs"
14271            | "pollard_rho_factor" | "pollard_rho"
14272            | "modular_lcm" | "mlcm"
14273            | "crt_general" | "crt_arbitrary"
14274            // ── Batch 5: physics / chemistry ──────────────────────────────
14275            | "van_der_waals_p" | "vdw_pressure"
14276            | "nernst_equation" | "nernst"
14277            | "arrhenius_rate" | "arrhenius"
14278            | "reduced_mass"
14279            | "ph_to_concentration" | "ph_to_h"
14280            // ── Batch 6: MCMC / SDE / HMM ─────────────────────────────────
14281            | "metropolis_hastings" | "mh_sampler"
14282            | "gibbs_sampler_step" | "gibbs_step"
14283            | "euler_maruyama" | "em_sde"
14284            | "milstein" | "milstein_sde"
14285            | "ornstein_uhlenbeck_path" | "ou_path"
14286            | "hmm_forward" | "hmm_viterbi" | "hmm_backward"
14287            // ── Batch 6: survival / alignment ─────────────────────────────
14288            | "kaplan_meier" | "km_estimator" | "log_rank_test"
14289            | "needleman_wunsch" | "nw_align"
14290            | "smith_waterman" | "sw_align"
14291            // ── Batch 6: chemistry ────────────────────────────────────────
14292            | "gibbs_free_energy" | "delta_g"
14293            | "henderson_hasselbalch" | "hh_eq"
14294            | "radioactive_decay"
14295            | "half_life_to_constant" | "hl_to_lambda"
14296            // ── Batch 6: control theory ───────────────────────────────────
14297            | "pid_step"
14298            | "transfer_function_eval" | "tf_eval"
14299            | "bode_magnitude_db" | "bode_mag_db"
14300            | "bode_phase_deg"
14301            | "lqr_2x2"
14302            // ── Batch 6: game theory ──────────────────────────────────────
14303            | "nash_eq_2x2" | "nash_2x2"
14304            | "shapley_value" | "expected_utility"
14305            // ── Batch 6: operations research ──────────────────────────────
14306            | "hungarian_assignment" | "hungarian"
14307            | "tsp_nearest_neighbor" | "tsp_nn"
14308            | "vertex_cover_2approx" | "vc_2approx"
14309            // ── Batch 6: PDE ──────────────────────────────────────────────
14310            | "heat_eq_1d" | "wave_eq_1d"
14311            | "laplace_2d_jacobi" | "laplace_jacobi"
14312            // ── Batch 6: Bayesian conjugate ───────────────────────────────
14313            | "beta_binomial_update"
14314            | "normal_normal_update"
14315            | "gamma_poisson_update"
14316            | "dirichlet_multinomial_update"
14317            // ── Batch 6: quantum gates ────────────────────────────────────
14318            | "hadamard_gate" | "h_gate"
14319            | "cnot_gate" | "cx_gate"
14320            | "swap_gate" | "cz_gate"
14321            | "qft_matrix" | "phase_gate"
14322            | "s_gate" | "t_gate"
14323            // ── Batch 6: splines ──────────────────────────────────────────
14324            | "bezier_eval"
14325            | "catmull_rom_eval" | "cmr_eval"
14326            | "cubic_hermite_eval" | "ch_eval"
14327            | "bspline_basis" | "nik_basis"
14328            // ── Batch 6: music ────────────────────────────────────────────
14329            | "freq_to_midi" | "midi_to_freq"
14330            | "equal_temperament_freq"
14331            | "cents_difference" | "cents_diff"
14332            // ── Batch 6: astronomy ────────────────────────────────────────
14333            | "redshift_z" | "hubble_distance" | "luminosity_distance"
14334            // ── Batch 6: fluid dynamics ───────────────────────────────────
14335            | "reynolds_number" | "mach_number"
14336            | "prandtl_number" | "bernoulli_velocity"
14337            // ── Batch 6: distributions ────────────────────────────────────
14338            | "negative_binomial_pmf" | "nb_pmf"
14339            | "hypergeometric_pmf"
14340            | "beta_binomial_pmf" | "bb_pmf"
14341            | "von_mises_pdf" | "vmf_pdf"
14342            // ── Batch 6: random graphs ────────────────────────────────────
14343            | "erdos_renyi_random" | "erdos_renyi"
14344            | "barabasi_albert_random" | "barabasi_albert"
14345            | "watts_strogatz_random" | "watts_strogatz"
14346            // ── Batch 6: color science ────────────────────────────────────
14347            | "rgb_to_lab" | "lab_to_rgb"
14348            | "kelvin_to_rgb" | "color_temp_rgb"
14349            // ── Batch 6: integer sequences ────────────────────────────────
14350            | "bell_triangle" | "surjection_count"
14351            | "distinct_partition_count" | "q_partition"
14352            | "fibonacci_q" | "is_fib_number"
14353            // ── Batch 7: stats / divergences / distribs / physics / astro / chem ──
14354            | "bonferroni_correction" | "bonferroni"
14355            | "benjamini_hochberg" | "bh_fdr"
14356            | "tukey_hsd"
14357            | "hellinger_distance"
14358            | "wasserstein_1d" | "earth_movers_1d"
14359            | "chi_squared_divergence"
14360            | "beta_geometric_pmf"
14361            | "generalized_gamma_pdf" | "gengamma_pdf"
14362            | "zip_pmf" | "zero_inflated_poisson_pmf"
14363            | "stefan_boltzmann_luminosity" | "stellar_luminosity"
14364            | "photon_momentum" | "photon_energy_ev"
14365            | "dipole_radiation_power" | "larmor_power"
14366            | "parallax_to_distance" | "hawking_temperature"
14367            | "roche_limit" | "apparent_magnitude" | "distance_modulus"
14368            | "beer_lambert" | "absorbance"
14369            | "rate_law_n"
14370            | "freezing_point_depression" | "fpd"
14371            | "mixed_nash_2x2" | "minimax_2x2"
14372            // ── Batch 7: graphics / DSP / image / clustering / combinatorics / NT ─
14373            | "barycentric_coords_2d" | "barycentric_2d"
14374            | "bresenham_line" | "bilinear_interp_2d"
14375            | "point_in_polygon_2d"
14376            | "hilbert_transform" | "cepstrum"
14377            | "butterworth_lowpass_coeffs" | "butter_lp"
14378            | "savitzky_golay_coeffs" | "sg_coeffs"
14379            | "savitzky_golay_filter" | "sg_filter"
14380            | "canny_edge_intensity" | "canny_intensity"
14381            | "bilateral_filter_basic" | "bilateral_filter"
14382            | "kmeans_pp_init" | "kpp_init"
14383            | "elbow_score" | "wcss"
14384            | "young_tableaux_count" | "syt_count"
14385            | "euler_alt_permutation" | "euler_zigzag"
14386            | "genocchi_number" | "lattice_paths_count"
14387            | "tetration"
14388            | "ackermann_limited" | "ackermann"
14389            | "perfect_power_q" | "b_smooth_q"
14390            // ── Batch 7: networks / crypto / quantum / geom / TS ──────────
14391            | "k_core"
14392            | "rich_club_coefficient" | "rich_club"
14393            | "rsa_basic_encrypt" | "rsa_enc_int"
14394            | "rsa_basic_decrypt" | "rsa_dec_int"
14395            | "dh_shared_secret"
14396            | "bell_state_phi_plus" | "bell_phi_plus"
14397            | "bell_state_psi_minus" | "bell_psi_minus"
14398            | "density_matrix_purity" | "rho_purity"
14399            | "concurrence_2qubit"
14400            | "point_in_circle"
14401            | "circle_circle_intersect_2d"
14402            | "polygon_centroid"
14403            | "sutherland_hodgman_clip" | "sh_clip"
14404            | "kalman_rts_smoother" | "rts_smoother"
14405            // ── Batch 8: bioinformatics ───────────────────────────────────
14406            | "gc_content" | "codon_to_aa"
14407            | "reverse_complement_dna" | "rev_comp_dna"
14408            | "hamming_dna"
14409            | "blosum62_pair_score" | "blosum62"
14410            | "kmer_count"
14411            // ── Batch 8: geographic ───────────────────────────────────────
14412            | "great_circle_bearing" | "gc_bearing"
14413            | "midpoint_lat_lon" | "mid_geo"
14414            | "utm_zone_for"
14415            | "area_polygon_lat_lon" | "geo_polygon_area"
14416            // ── Batch 8: finance ──────────────────────────────────────────
14417            | "crr_binomial_option" | "crr_option"
14418            | "bond_price_clean"
14419            | "bond_yield_to_maturity" | "bond_ytm"
14420            | "modified_duration_bond"
14421            | "convexity_bond" | "bond_convexity"
14422            // ── Batch 8: image quality ────────────────────────────────────
14423            | "ssim" | "psnr" | "mssim"
14424            // ── Batch 8: acoustics ────────────────────────────────────────
14425            | "db_spl_from_pa" | "db_spl"
14426            | "a_weighting_factor" | "a_weight"
14427            | "octave_band_center" | "octave_center"
14428            | "semitone_ratio"
14429            // ── Batch 8: genetics ─────────────────────────────────────────
14430            | "hardy_weinberg"
14431            | "expected_heterozygosity" | "het_e"
14432            | "fst_simple"
14433            | "allele_frequencies"
14434            // ── Batch 8: epidemiology ─────────────────────────────────────
14435            | "sir_step" | "sir_r0" | "doubling_time"
14436            // ── Batch 8: economics ────────────────────────────────────────
14437            | "theil_index"
14438            | "herfindahl_hirschman" | "hhi"
14439            | "atkinson_index"
14440            | "lorenz_curve_points"
14441            // ── Batch 8: APL/J primitives ─────────────────────────────────
14442            | "iota_range" | "iota"
14443            | "reshape_array" | "reshape"
14444            | "grade_up" | "grade_asc"
14445            | "grade_down" | "grade_desc"
14446            // ── Batch 8: plasma physics ───────────────────────────────────
14447            | "plasma_frequency" | "omega_p"
14448            | "debye_length" | "lambda_d"
14449            | "cyclotron_frequency" | "omega_c"
14450            | "larmor_radius" | "gyroradius"
14451            // ── Batch 8: string similarity ────────────────────────────────
14452            | "jaro_winkler_similarity" | "jaro_winkler"
14453            | "metaphone_simple"
14454            // ── Batch 8: rating systems ───────────────────────────────────
14455            | "elo_rating_update" | "elo"
14456            | "glicko_rating_update" | "glicko"
14457            | "dice_sum_pmf"
14458            // ── Batch 8: effect sizes ─────────────────────────────────────
14459            | "cohens_d" | "effect_size_d"
14460            | "cliff_delta"
14461            | "vargha_delaney_a12" | "a12"
14462            // ── Batch 8: control transient ────────────────────────────────
14463            | "step_response_2nd_order" | "step_2nd"
14464            | "overshoot_2nd_order" | "overshoot_pct"
14465            // ── Batch 8: matrix norms ─────────────────────────────────────
14466            | "frobenius_norm"
14467            | "spectral_norm" | "operator_norm_2"
14468            | "trace_matrix" | "tr_mat"
14469            // ── Batch 8: networks ─────────────────────────────────────────
14470            | "homophily_index" | "homophily"
14471            | "dyad_census" | "triad_census"
14472            // ── Batch 8: misc ─────────────────────────────────────────────
14473            | "sigmoid_inverse" | "logit"
14474            // ── Batch 9: list / string / date / color / music / astro / perm / linguistics / regression / combinatorics / PRNG ──
14475            | "partition_at" | "drop_at" | "insert_at_idx"
14476            | "replace_at_index" | "set_at"
14477            | "swap_indices" | "nth_largest" | "nth_smallest"
14478            | "position_of_all_matching" | "positions_of_all"
14479            | "string_take_first" | "string_take_last"
14480            | "string_drop_first" | "string_drop_last"
14481            | "pluralize_simple"
14482            | "singularize_simple" | "singularize"
14483            | "capitalize_words" | "title_words"
14484            | "format_table_simple" | "ascii_table"
14485            | "days_between" | "weeks_between"
14486            | "months_between" | "years_between"
14487            | "first_of_month" | "last_of_month"
14488            | "day_of_week_iso" | "iso_dow"
14489            | "easter_sunday" | "chinese_zodiac"
14490            | "iso_week_number" | "iso_week"
14491            | "relative_luminance" | "wcag_luminance"
14492            | "contrast_ratio_wcag" | "wcag_contrast"
14493            | "delta_e_76" | "delta_e"
14494            | "color_blend_t" | "lerp_color"
14495            | "chord_to_freqs" | "scale_to_intervals"
14496            | "interval_semitones"
14497            | "transpose_freq_semitones" | "transpose_semi"
14498            | "bpm_to_period" | "midi_to_pitch_class"
14499            | "key_signature_for" | "circle_of_fifths_step"
14500            | "moon_phase" | "equation_of_time"
14501            | "solar_declination" | "sidereal_day_period" | "ecliptic_obliquity"
14502            | "permutation_order"
14503            | "permutation_parity" | "perm_sign"
14504            | "identity_permutation"
14505            | "permutation_compose" | "perm_mul"
14506            | "flesch_reading_ease" | "flesch_kincaid_grade"
14507            | "gunning_fog"
14508            | "automated_readability_index" | "ari"
14509            | "lix"
14510            | "adjusted_r_squared" | "adj_r2"
14511            | "aic" | "bic"
14512            | "residuals_compute" | "compute_residuals"
14513            | "composition_count" | "weak_composition_count"
14514            | "necklace_count" | "bracelet_count"
14515            | "multiset_permutations_count" | "multinomial_count"
14516            | "pearson_hash_byte" | "pearson_hash"
14517            | "xorshift32_step" | "lcg_next_u32"
14518            | "fisher_yates_shuffle"
14519            // ── Batch 10 ──────────────────────────────────────────────────
14520            | "tetrahedral_number" | "square_pyramidal_number"
14521            | "octahedral_number" | "pentagonal_pyramidal_number"
14522            | "cake_number" | "cuban_number" | "centered_hexagonal_number"
14523            | "carmichael_q" | "is_carmichael"
14524            | "sphenic_q" | "is_sphenic"
14525            | "seven_smooth_q" | "is_7_smooth"
14526            | "cartesian_product_n" | "cart_n"
14527            | "multiset_union" | "multiset_intersection" | "multiset_difference"
14528            | "polynomial_roots_dk" | "durand_kerner"
14529            | "lin_bairstow_step" | "bairstow"
14530            | "heap_sift_down"
14531            | "fenwick_build" | "bit_build"
14532            | "fenwick_query" | "bit_query"
14533            | "segment_tree_sum" | "seg_sum"
14534            | "kmp_failure" | "kmp"
14535            | "z_array" | "z_func"
14536            | "suffix_array_naive"
14537            | "manacher_radii" | "manacher"
14538            | "rabin_karp_hash" | "lcp_array"
14539            | "regex_escape_simple"
14540            | "horspool_search" | "bm_horspool"
14541            | "lpt_schedule" | "lpt"
14542            | "johnsons_rule" | "johnson_2m"
14543            | "bit_reverse_32" | "bit_reverse"
14544            | "bin_to_gray" | "gray_to_bin"
14545            | "swap_bits_pos" | "swap_bits"
14546            | "hamming_weight" | "popcnt"
14547            | "hamming_distance_int" | "hamdist_int"
14548            | "internal_rate_of_return"
14549            | "modified_irr" | "mirr"
14550            | "payback_period_simple" | "payback_simple"
14551            | "rfc3339_format" | "rfc3339"
14552            | "rfc3339_parse"
14553            | "iso_ordinal_date" | "ordinal_date"
14554            // ── Batch 11 ──────────────────────────────────────────────────
14555            | "lazy_caterer" | "central_polygonal"
14556            | "centered_square" | "centered_triangular" | "centered_pentagonal"
14557            | "star_number" | "dodecahedral_number" | "icosahedral_number"
14558            | "pronic_number" | "squared_triangular"
14559            | "woodall_number" | "cullen_number"
14560            | "repunit" | "repdigit" | "kaprekar_routine_step"
14561            | "smith_q"
14562            | "keith_q" | "is_keith"
14563            | "armstrong_q" | "is_armstrong"
14564            | "fnv1a_hash" | "djb2_hash"
14565            | "jenkins_one_at_a_time" | "jenkins_oat"
14566            | "murmurhash3_x32"
14567            | "adler32_hash" | "crc16_ccitt"
14568            | "vec_dot"
14569            | "l1_norm" | "l2_norm" | "vec_l2"
14570            | "linf_norm" | "max_norm" | "lp_norm"
14571            | "unit_vector"
14572            | "vector_project" | "proj" | "vector_reject"
14573            | "orthogonalize_vectors" | "gram_schmidt"
14574            | "outer_product" | "vec_outer"
14575            | "matrix_diagonal" | "mdiagvec"
14576            | "matrix_anti_diagonal"
14577            | "matrix_symmetric_q" | "matrix_orthogonal_q"
14578            | "geometric_mean_arr" | "harmonic_mean_arr"
14579            | "quadratic_mean_arr" | "lehmer_mean"
14580            | "running_mean" | "running_variance"
14581            | "outlier_iqr_q" | "z_score_robust"
14582            | "geometric_sequence" | "arithmetic_sequence"
14583            | "log_sum_exp" | "lse"
14584            | "log_sigmoid" | "log1p_exp"
14585            | "string_chars"
14586            | "string_words_count" | "word_count_simple"
14587            | "string_lines_count" | "line_count_simple"
14588            | "string_intersperse" | "string_replicate"
14589            | "string_uniq_chars" | "string_letter_frequency"
14590            | "anagram_q" | "is_anagram_q"
14591            | "string_take_while" | "string_drop_while"
14592            | "string_split_at_first" | "string_partition_at_word"
14593            // ── Batch 12 ──────────────────────────────────────────────────
14594 | "relativistic_kinetic"
14595            | "lorentz_factor_v" | "doppler_relativistic"
14596            | "drag_force_quadratic" | "terminal_velocity"
14597            | "carnot_efficiency" | "otto_efficiency"
14598            | "brayton_efficiency" | "diesel_efficiency"
14599            | "specific_heat_const_v" | "speed_of_sound_ideal"
14600            | "kepler_period_au" | "synodic_period"
14601            | "hill_radius" | "jeans_length"
14602            | "chandrasekhar_mass" | "eddington_luminosity"
14603            | "schwarzschild_radius_m" | "gravity_at_radius"
14604            | "gravitational_pe"
14605            | "freefall_time" | "pendulum_freq" | "spring_period"
14606            | "centripetal_accel" | "lens_focal_length"
14607            | "avogadros_number" | "boltzmann_const"
14608            | "planck_const_h" | "gas_constant_r"
14609            | "concentration_dilute" | "partial_pressure"
14610            | "mole_fraction" | "molarity" | "molality"
14611            | "normality_chem" | "ionic_strength"
14612 | "titration_volume"
14613            | "atomic_radius_pm" | "de_broglie_wavelength_kg"
14614 | "lotka_volterra_step"
14615            | "michaelis_menten" | "hill_equation"
14616            | "lineweaver_burk" | "eadie_hofstee_y"
14617            | "arrhenius_temp_q10"
14618            | "body_surface_area_dubois" | "bsa_dubois"
14619            | "bmr_harris_benedict_male" | "bmr_harris_benedict_female"
14620            | "max_heart_rate" | "target_heart_rate"
14621            | "vo2_max_estimate" | "pulse_pressure"
14622            | "mean_arterial_pressure" | "map_bp"
14623            | "dew_point_magnus" | "heat_index_celsius"
14624            | "wind_chill_celsius" | "pressure_altitude_m"
14625            | "density_altitude_m" | "saturation_vapor_pressure"
14626            | "humidex" | "utci_simple"
14627            | "resistance_parallel" | "r_parallel"
14628            | "resistance_series" | "r_series"
14629            | "capacitance_parallel" | "c_parallel"
14630            | "capacitance_series" | "c_series"
14631            | "inductance_parallel" | "l_parallel"
14632            | "inductance_series" | "l_series"
14633            | "voltage_divider" | "current_divider"
14634            | "lc_resonant" | "q_factor_rlc"
14635            | "skin_depth" | "wire_resistance"
14636            | "motor_torque" | "efficiency_ratio"
14637            | "dB_voltage" | "db_voltage"
14638            | "dB_power" | "db_power"
14639            // ── Batch 13 ──────────────────────────────────────────────────
14640            | "bfs_distances" | "dfs_preorder" | "connected_components"
14641            | "graph_is_tree" | "graph_density"
14642            | "graph_average_degree" | "graph_max_degree" | "graph_min_degree"
14643            | "graph_complement"
14644            | "in_degree_directed" | "out_degree_directed"
14645            | "graph_eccentricity_all" | "is_connected"
14646            | "articulation_points" | "bridges_edges"
14647            | "eulerian_path_q" | "hamiltonian_brute"
14648            | "string_to_charcodes" | "charcodes_to_string"
14649            | "string_xor"
14650            | "string_camel_to_snake" | "string_snake_to_camel"
14651            | "string_kebab_to_snake" | "string_snake_to_kebab"
14652            | "palindromic_q" | "substring_count"
14653            | "string_truncate_ellipsis" | "string_expand_tabs"
14654            | "string_normalize_spaces"
14655 | "days_in_year" | "quarter_of_year"
14656            | "zeller_day_of_week" | "age_from_birthdate"
14657            | "business_days_between" | "unix_epoch_to_iso"
14658            | "loan_payment_pmt" | "loan_balance"
14659            | "amortization_total_interest"
14660            | "apr_to_apy" | "apy_to_apr"
14661            | "compound_interest_periods" | "simple_interest_compute"
14662
14663            | "perpetuity_value" | "growing_perpetuity"
14664            | "annuity_present_value" | "annuity_future_value"
14665            | "capm_expected_return"
14666            | "treynor_ratio"
14667            | "jensens_alpha" | "information_ratio"
14668            | "friction_factor_laminar" | "swamee_jain_factor"
14669            | "pipe_pressure_drop" | "orifice_velocity"
14670            | "chezy_velocity" | "manning_velocity"
14671            | "froude_number" | "weber_number" | "grashof_number"
14672            | "nusselt_dittus_boelter"
14673            // ── Batches 14-16 ─────────────────────────────────────────────
14674            | "mollweide_project" | "robinson_project" | "sinusoidal_project"
14675            | "equirectangular_project" | "lambert_azimuthal_project" | "albers_conic_project"
14676            | "geohash_encode" | "geohash_decode" | "geohash_neighbor" | "geohash_bbox"
14677            | "gabor_kernel" | "unsharp_mask_kernel" | "emboss_kernel"
14678            | "box_blur_kernel" | "motion_blur_kernel" | "sharpen_kernel"
14679            | "edge_detect_kernel" | "sobel_diagonal_kernel" | "haar_2d_step"
14680            | "db4_coeffs" | "db6_coeffs" | "sym4_coeffs" | "coif1_coeffs"
14681            | "aes_sbox_byte" | "aes_inv_sbox_byte"
14682            | "chacha20_qround" | "xtea_round" | "speck_round" | "simon_round"
14683            | "kepler_hyperbolic" | "hohmann_dv1" | "hohmann_dv2" | "hohmann_total"
14684            | "bielliptic_total" | "lambert_simple"
14685            | "horizon_distance" | "solar_zenith_angle" | "air_mass_kasten"
14686            | "solar_constant" | "julian_centuries_j2000"
14687            | "mean_solar_longitude" | "mean_solar_anomaly" | "lst_to_solar"
14688            | "ra_dec_to_az_alt" | "ecliptic_to_equatorial" | "equatorial_to_galactic"
14689            | "orbital_eccentricity" | "semi_major_axis"
14690            | "specific_orbital_energy" | "specific_angular_momentum"
14691            | "toffoli_gate" | "ccx_gate" | "fredkin_gate" | "cswap_gate"
14692            | "iswap_gate" | "sqrt_swap_gate"
14693            | "rx_gate" | "ry_gate" | "rz_gate"
14694            | "ghz_state_n" | "w_state_n"
14695            | "depolarizing_channel" | "dephasing_channel" | "amplitude_damping_channel"
14696            | "quantum_fidelity_pure" | "trace_distance"
14697            | "bell_inequality_chsh" | "pauli_decomposition_2x2"
14698            | "quantum_relative_entropy" | "qft_4_real"
14699            | "bwt_encode" | "bwt_decode" | "mtf_encode" | "mtf_decode"
14700
14701            | "lyndon_factorize" | "christoffel_word" | "sturmian_word"
14702            | "z_function_alt" | "period_of_string" | "borders_of_string"
14703            | "thue_morse_string" | "fibonacci_word"
14704            | "mann_kendall_tau" | "theil_sen_slope" | "hodges_lehmann"
14705            | "huber_m_estimator" | "winsorized_variance_arr"
14706            | "bowley_skewness" | "pearson_skewness_2"
14707            | "concordance_correlation" | "quantile_p"
14708            | "label_propagation_step" | "modularity_q"
14709            | "clique_count_3" | "local_efficiency" | "global_efficiency"
14710            | "diameter_unweighted"
14711            | "aitken_delta_squared" | "wynn_epsilon"
14712            | "shanks_transform" | "levin_t_transform"
14713            | "harmonic_seq_sum" | "alternating_seq_sum"
14714            // ── Batches 17-18 ─────────────────────────────────────────────
14715            | "sparse_csr_build" | "sparse_csr_mul_vec" | "sparse_density"
14716            | "lower_triangular_q" | "upper_triangular_q"
14717            | "diagonal_dominance_q" | "matrix_zero_q" | "matrix_identity_q"
14718            | "matrix_random_uniform" | "matrix_random_normal"
14719            | "andrew_monotone_chain" | "polygon_area_signed"
14720            | "polygon_convex_q" | "iou_2d_axis_aligned" | "hausdorff_distance_2d"
14721            | "minkowski_sum_simple" | "circle_3_points"
14722            | "polygon_winding_number" | "segment_length"
14723            | "segments_parallel_q" | "segments_perpendicular_q"
14724            | "burr_xii_pdf" | "burr_xii_cdf" | "dagum_pdf" | "lomax_pdf"
14725            | "birnbaum_saunders_pdf" | "tukey_lambda_quantile"
14726            | "half_cauchy_pdf" | "half_logistic_pdf" | "reciprocal_pdf"
14727            | "levy_pdf" | "voigt_profile_simple"
14728            | "gompertz_pdf" | "inverse_weibull_pdf"
14729            | "log_gamma_simple" | "inverse_chi2_pdf"
14730            | "poly1305_block_step" | "x25519_field_mul" | "curve25519_mul_simple"
14731            | "secp256k1_y_recover" | "hmac_step_xor"
14732            | "pkcs7_pad" | "pkcs7_unpad" | "xor_byte_string"
14733 | "atbash_cipher"
14734            | "vigenere_encrypt" | "vigenere_decrypt" | "xor_brute_keylen"
14735            | "arima_diff" | "seasonal_diff"
14736            | "garch_step" | "egarch_step"
14737            | "realized_volatility" | "max_drawdown_arr"
14738            | "calmar_ratio" | "omega_ratio" | "kelly_criterion"
14739            | "var_historical" | "cvar_historical"
14740            | "graph_degree_distribution" | "graph_count_edges"
14741            | "graph_bipartite_match_simple" | "graph_count_triangles"
14742            | "graph_avg_clustering" | "graph_transitivity"
14743            | "graph_max_clique_brute" | "graph_independent_set_brute"
14744            | "graph_count_paths_length_k" | "graph_pagerank_simple"
14745            // ── batch 19: integration / ODE / root finding / optimization ─
14746            | "boole_rule" | "boole_int"
14747            | "gauss_legendre_5" | "gl5"
14748            | "gauss_kronrod_15" | "gk15"
14749
14750            | "midpoint_rule"
14751            | "adams_bashforth_4" | "ab4"
14752            | "heun_method" | "rk45_cash_karp" | "rkck"
14753            | "milne_pc" | "milne"
14754            | "modified_midpoint_ode" | "modmidpoint"
14755            | "backward_euler" | "implicit_euler"
14756            | "crank_nicolson_ode" | "cn_ode"
14757            | "brent_root" | "brent" | "ridders_root" | "ridders"
14758            | "steffensen_root" | "steffensen" | "halley_root" | "halley"
14759            | "householder_root" | "muller_root" | "muller"
14760            | "regula_falsi" | "false_position"
14761            | "secant_root" | "secant"
14762            | "anderson_step" | "aberth_step" | "inverse_quad_interp"
14763            | "lm_step" | "gradient_descent_step"
14764 | "nesterov_step" | "adagrad_step"
14765            | "cg_beta_pr" | "cg_beta_fr" | "bfgs_h_update_1d"
14766            | "wolfe_strong_q" | "dogleg_step"
14767            | "nelder_mead_reflect" | "nelder_mead_expand" | "nelder_mead_contract"
14768            | "sa_accept_prob" | "sa_boltzmann_temp" | "sa_cauchy_temp"
14769            | "sa_geometric_temp" | "acceptance_target"
14770            // ── batch 20: financial pricing models ────────────────────────
14771            | "bs_call" | "blackscholes_call" | "bs_put" | "blackscholes_put"
14772 | "bs_theta_call" | "bs_rho_call"
14773 | "bachelier_call" | "black76_call"
14774            | "crr_american_call" | "crr_american_put" | "jr_european_call"
14775            | "trinomial_call" | "heston_price_simple" | "sabr_implied_vol"
14776            | "merton_jump_call" | "asian_call_mc" | "barrier_up_out_call"
14777            | "digital_call" | "lookback_call"
14778            | "macaulay_duration" | "forward_rate"
14779            | "discount_continuous" | "ytm_newton"
14780            | "vasicek_bond" | "cir_bond" | "hull_white_drift"
14781            | "cds_upfront" | "black_karasinski_drift" | "quanto_adjustment"
14782            | "fx_forward" | "garman_kohlhagen_call" | "margrabe" | "stulz_min_call"
14783            | "sharpe_annualized"
14784            | "jensen_alpha" | "modified_sharpe"
14785            // ── batch 21: chemistry ───────────────────────────────────────
14786            | "ph_from_h" | "poh_from_oh" | "pka_from_ka"
14787 | "henderson_base"
14788            | "arrhenius_k" | "eyring_k"
14789            | "first_order_concentration" | "first_order_half_life"
14790            | "second_order_concentration" | "second_order_half_life"
14791            | "zero_order_concentration"
14792
14793            | "ideal_gas_n" | "redlich_kwong_p"
14794            | "compressibility_z"
14795            | "kc_from_rates" | "kp_from_kc" | "reaction_quotient" | "rxn_q"
14796            | "le_chatelier_dir"
14797            | "dg_from_k" | "k_from_dg" | "vant_hoff" | "clausius_clapeyron" | "antoine_p"
14798 | "emf_from_half_cells" | "faraday_mass_deposited"
14799 | "transmittance" | "ksp_from_concs"
14800 | "debye_huckel"
14801            | "cp_monatomic_ideal" | "cv_monatomic_ideal"
14802            | "heat_capacity_q" | "calorimeter_dt" | "enthalpy_reaction"
14803            | "avogadro_count" | "moles_from_mass"
14804            | "dilution_v2" | "raoult_law" | "bp_elevation" | "fp_depression"
14805            | "osmotic_pressure" | "rydberg_lambda" | "bohr_radius_n"
14806            | "bohr_energy_ev" | "photon_energy_freq" | "photon_energy_lambda"
14807            | "de_broglie"
14808            // ── batch 22: biology / ecology ───────────────────────────────
14809 | "logistic_growth_step" | "logistic_growth_analytic"
14810            | "gompertz_growth_step" | "allee_growth_step"
14811 | "growth_rate_from_ratio"
14812 | "seir_step" | "seird_step" | "sis_step"
14813            | "r0_basic" | "rt_effective" | "herd_immunity_threshold" | "generation_time"
14814 | "inverse_simpson"
14815            | "pielou_evenness" | "margalef_richness" | "menhinick_richness"
14816            | "berger_parker" | "sorensen_dice"
14817            | "rao_quadratic_entropy"
14818 | "selection_step" | "nei_genetic_distance"
14819            | "effective_pop_size" | "carrying_capacity_from_data"
14820            | "petersen_estimator" | "chapman_estimator"
14821            | "lv_competition_step"
14822            | "holling_type1" | "holling_type2" | "holling_type3"
14823            | "leslie_step" | "net_reproductive_rate" | "generation_time_demo"
14824            | "finite_rate_lambda" | "kleibers_law" | "bergmann_adjust"
14825            | "q10" | "species_area" | "intrinsic_growth_rate"
14826            | "macarthur_wilson_immigration" | "macarthur_wilson_extinction"
14827            | "island_equilibrium"
14828            // ── batch 23: EM / optics / relativity ────────────────────────
14829 | "efield_point" | "epotential_point"
14830 | "capacitor_charge"
14831            | "ohm_voltage" | "power_vi" | "power_i2r"
14832
14833 | "capacitance_parallel_sum"
14834            | "bfield_wire" | "bfield_solenoid" | "lorentz_force_mag"
14835 | "faraday_emf"
14836 | "lc_frequency" | "lc_omega"
14837            | "rc_tau" | "rl_tau"
14838            | "poynting_magnitude" | "em_intensity" | "radiation_pressure"
14839            | "em_wavelength" | "em_frequency"
14840            | "snell_theta2"
14841            | "index_from_speed" | "fresnel_reflection_normal"
14842            | "fresnel_rs" | "fresnel_rp"
14843            | "lensmaker" | "thin_lens_v" | "mirror_equation_v"
14844            | "lens_magnification" | "diffraction_grating_angle"
14845            | "single_slit_min" | "rayleigh_resolution"
14846            | "lorentz_gamma"
14847            | "rel_momentum" | "rel_ke" | "rel_total_energy" | "rel_energy_pm"
14848            | "relativistic_doppler" | "rel_velocity_add"
14849
14850            | "wave_string_speed" | "sound_solid" | "sound_gas"
14851            | "doppler_classical" | "standing_wave_fundamental"
14852            | "open_pipe_harmonic" | "closed_pipe_harmonic"
14853            | "sound_db"
14854            | "alfven_speed"
14855            | "grav_time_dilation" | "grav_redshift"
14856            // ── batch 24: graph algorithms ────────────────────────────────
14857            | "kosaraju_scc" | "bridges"
14858            | "max_flow_ek" | "min_cut_value" | "hopcroft_karp"
14859
14860 | "katz_centrality" | "hits_simple"
14861            | "pagerank_damped" | "cc_count" | "cc_labels"
14862            | "topological_sort_kahn" | "has_cycle_directed" | "has_cycle_undirected"
14863 | "diameter_bfs" | "radius_bfs"
14864            | "num_edges" | "k_coreness"
14865            | "greedy_coloring" | "chromatic_number_greedy"
14866            | "sum_degrees" | "avg_degree" | "max_degree"
14867            | "is_tree" | "girth"
14868            // ── batch 25: signal processing ───────────────────────────────
14869            | "hamming_window" | "hann_window" | "blackman_window"
14870            | "blackman_harris_window" | "bartlett_window" | "welch_window"
14871            | "kaiser_window" | "tukey_window" | "gaussian_window"
14872            | "hilbert_envelope"
14873            | "biquad_step" | "biquad_lowpass_coeffs" | "biquad_highpass_coeffs"
14874            | "biquad_bandpass_coeffs" | "biquad_notch_coeffs" | "biquad_allpass_coeffs"
14875            | "biquad_peak_coeffs" | "biquad_lowshelf_coeffs" | "biquad_highshelf_coeffs"
14876            | "butterworth_prewarp" | "butterworth_order"
14877            | "fir_moving_average" | "fir_lowpass_design"
14878 | "spectrogram_simple"
14879            | "zero_pad" | "resample_nearest" | "resample_linear" | "quantize"
14880            | "mu_law_encode" | "mu_law_decode" | "a_law_encode" | "a_law_decode"
14881            | "chirp_linear"
14882            // ── batch 26: cryptography deep ───────────────────────────────
14883            | "fnv1a_32" | "fnv1a_64" | "sdbm_hash"
14884            | "siphash24"
14885            | "pbkdf2_hmac_step" | "scrypt_round" | "bcrypt_cost_iters"
14886            | "argon2_block_mix" | "hkdf_expand_step"
14887            | "lfsr_galois_step" | "mt19937_temper" | "xorshift64" | "xorshift32"
14888            | "pcg32_step" | "lcg_numrec_step" | "splitmix64_step" | "wyhash_mix"
14889
14890            | "xor_cipher_byte"
14891            | "railfence_encrypt" | "beaufort" | "affine_encrypt" | "substitution_encrypt"
14892            | "letter_frequency" | "english_chi2" | "index_of_coincidence" | "kasiski_repeats"
14893            | "deterministic_prime" | "dh_shared" | "rsa_encrypt_simple"
14894            | "monobit_test" | "approximate_entropy"
14895            // ── batch 27: ML extensions ───────────────────────────────────
14896            | "gini_impurity" | "entropy_bits" | "information_gain" | "gain_ratio"
14897            | "nb_gaussian_likelihood" | "nb_bernoulli_likelihood" | "nb_multinomial_log_likelihood"
14898            | "adaboost_alpha" | "hinge_loss" | "squared_hinge"
14899            | "logistic_loss"
14900 | "sigmoid_grad" | "tanh_grad"
14901 | "relu_grad"
14902 | "softsign" | "prelu" | "threshold_act"
14903            | "confusion_counts" | "mcc" | "f_beta" | "specificity"
14904            | "balanced_accuracy" | "cohen_kappa" | "brier_score" | "log_loss"
14905            | "tversky" | "mahalanobis_1d"
14906 | "log_softmax" | "one_hot" | "topk_indices"
14907            | "minmax_scale" | "zscore_norm" | "robust_scale"
14908            // ── batch 28: geometry / topology ─────────────────────────────
14909            | "triangle_area_heron" | "triangle_area_pts"
14910            | "triangle_inradius" | "triangle_circumradius"
14911            | "regular_ngon_area" | "regular_ngon_inradius" | "regular_ngon_circumradius"
14912 | "n_ball_volume"
14913 | "cylinder_surface" | "cone_surface"
14914
14915            | "ellipsoid_volume" | "ellipsoid_surface_approx"
14916            | "dist_point_line_2d" | "dist_point_plane_3d" | "closest_pt_segment_2d"
14917            | "bbox_from_points"
14918 | "euclidean_distance_nd"
14919 | "hamming_distance_str"
14920 | "great_circle_law_of_cos"
14921            | "initial_bearing" | "midpoint_great_circle"
14922            | "shoelace_area" | "polygon_is_convex" | "convex_hull_jarvis"
14923            | "euler_characteristic" | "genus_from_euler"
14924            | "spherical_triangle_area" | "polygon_with_holes_area" | "picks_theorem"
14925            | "centroid_nd" | "covariance_matrix_pts" | "simplex_volume_3d"
14926            // ── batch 29: special functions extra ─────────────────────────
14927            | "hyper2f1" | "hyper1f1" | "hyper0f1" | "pochhammer"
14928            | "mathieu_ce0" | "mathieu_se1" | "parabolic_d0" | "parabolic_d1"
14929            | "whittaker_m" | "struve_h0" | "struve_h1"
14930            | "lambert_w0" | "wright_omega"
14931            | "sinhc" | "cosh_minus1_over_x2"
14932            | "sine_integral_si" | "cosine_integral_ci" | "exp_integral_e1"
14933 | "dawson_function" | "owen_t"
14934            | "spherical_bessel_j0" | "spherical_bessel_j1"
14935            | "spherical_bessel_y0" | "spherical_bessel_y1"
14936            | "mod_sph_bessel_i0" | "mod_sph_bessel_i1" | "mod_sph_bessel_k0"
14937            | "coulomb_f0"
14938            | "polylog_li2" | "polylog_n"
14939
14940 | "ti2" | "clausen_cl2"
14941            | "bose_einstein_g" | "fermi_dirac_int"
14942            | "theta3" | "theta2"
14943            | "jacobi_sn_small_q" | "jacobi_cn_small_q" | "jacobi_dn_small_q"
14944            | "riemann_xi" | "bessel_jn_general" | "bessel_in_general"
14945            // ── batch 30: astronomy / music / color / units ───────────────
14946 | "absolute_magnitude"
14947            | "pc_to_ly" | "ly_to_pc" | "pc_to_au" | "au_to_m"
14948            | "solar_mass_to_kg" | "solar_luminosity_to_w"
14949            | "hubble_distance_mpc" | "comoving_distance_approx" | "critical_density"
14950            | "et_freq_ratio" | "midi_to_hz" | "hz_to_midi" | "cents_between"
14951            | "just_intonation_ratio" | "pythagorean_ratio"
14952            | "beat_frequency" | "bpm_to_spb" | "note_name_to_midi"
14953 | "rgb_to_yiq" | "rgb_to_yuv601"
14954            | "srgb_to_xyz" | "xyz_to_lab" | "delta_e_94"
14955
14956
14957            | "feet_to_meters" | "meters_to_feet"
14958            | "lb_to_kg" | "kg_to_lb"
14959            | "mph_to_kmh" | "kmh_to_mph" | "mps_to_kmh" | "kmh_to_mps" | "knots_to_kmh"
14960 | "atm_to_pa" | "pa_to_atm" | "mmhg_to_pa"
14961            | "ev_to_joules" | "joules_to_ev" | "btu_to_joules" | "kwh_to_joules"
14962            | "bpm_to_midi_tick_us" | "iso226_phon_adjustment"
14963            | "db_to_amp" | "amp_to_db"
14964            | "roman_encode" | "roman_decode" | "number_to_english"
14965            // ── batch 31: cosmology / GR / FLRW ───────────────────────────
14966            | "hubble_lcdm" | "hubble_time" | "hubble_distance_si" | "critical_density_si"
14967            | "comoving_distance" | "angular_diameter_distance"
14968            | "lookback_time" | "age_at_z" | "scale_factor" | "redshift_from_a"
14969            | "omega_m_at_z" | "lcdm_eos" | "cpl_w" | "deceleration_q"
14970            | "schwarzschild_radius_kg" | "kerr_ergosphere_eq" | "kerr_horizon"
14971 | "bh_entropy" | "bh_evaporation_time"
14972            | "schwarzschild_isco" | "photon_sphere_radius"
14973            | "tidal_force" | "grav_dilation_factor" | "lense_thirring_omega"
14974            | "gw_strain_amplitude" | "chirp_mass" | "grav_binding_energy"
14975            | "roche_limit_rigid" | "roche_limit_fluid"
14976            | "lagrange_l1" | "sphere_of_influence"
14977            | "freefall_velocity_schwarzschild" | "einstein_ring_radius"
14978            | "microlensing_magnification" | "cosmic_distance_modulus_si"
14979            | "cmb_temperature" | "cmb_temperature_at_z"
14980 | "stefan_boltzmann_si" | "planck_spectral_radiance"
14981            | "schwarzschild_g_tt" | "schwarzschild_g_rr" | "kretschmann_schwarzschild"
14982            | "hill_velocity" | "vacuum_energy_density"
14983            | "sound_horizon_recomb" | "bao_scale_today" | "sigma8_default"
14984            | "lensing_convergence" | "sigma_crit"
14985            | "perihelion_precession" | "shapiro_delay" | "light_deflection_angle"
14986 | "tov_mass_limit"
14987            | "main_sequence_lifetime" | "schwarzschild_freefall_time"
14988            | "friedmann_density_total" | "cosmological_constant"
14989
14990 | "planck_energy"
14991            // ── batch 32: quantum mechanics deep ──────────────────────────
14992            | "pure_state_density" | "purity"
14993            | "linear_entropy" | "quantum_mutual_info"
14994 | "eof_from_concurrence"
14995            | "bell_state_index" | "chsh_expectation" | "tsirelson_bound"
14996            | "pauli_real_part" | "pauli_y_imag"
14997            | "bloch_to_density_real" | "bloch_purity_check"
14998            | "fidelity_pure_real" | "l1_coherence" | "relative_entropy_coherence"
14999            | "kraus_apply" | "bit_flip_prob" | "phase_flip_prob"
15000            | "depolarizing_density_2x2" | "amplitude_damping_excited"
15001            | "quantum_fisher_info" | "cramer_rao_bound" | "squeezing_db" | "heisenberg_min"
15002            | "coherent_mean_photons" | "thermal_mean_photons" | "poisson_photon_pmf"
15003            | "bose_einstein_pmf" | "mandel_q" | "g2_zero"
15004            | "free_particle_energy" | "infinite_well_energy" | "harmonic_oscillator_energy"
15005            | "hydrogen_energy_n" | "stark_shift_linear"
15006            | "zeeman_energy" | "larmor_frequency" | "rabi_frequency"
15007            | "schrodinger_step_real" | "probability_density" | "state_norm" | "state_normalize"
15008 | "quantum_variance" | "spin_casimir"
15009            | "cg_simple" | "wigner_3j_bound" | "qho_ground_state"
15010            | "tunneling_prob" | "gamow_factor" | "compton_wavelength" | "uncertainty_position"
15011            | "berry_phase_spin_half" | "zeno_survival" | "decoherence_time"
15012            | "ramsey_visibility" | "fermi_golden_rule"
15013            // ── batch 33: bioinformatics deep ─────────────────────────────
15014            | "needleman_wunsch_score" | "smith_waterman_score" | "pam250_score"
15015            | "tanimoto_bits" | "translate_dna" | "transcribe_dna_rna" | "reverse_transcribe"
15016            | "at_content" | "tm_wallace" | "tm_marmur" | "codon_adaptation_index"
15017            | "kmer_jaccard" | "sequence_shannon_info" | "pwm_score"
15018            | "msa_column_entropy" | "seq_logo_information"
15019 | "damerau_levenshtein" | "lcs_length"
15020 | "hirschberg_lcs_length" | "common_kmers"
15021            | "jukes_cantor_distance" | "kimura_2p_distance" | "felsenstein_step"
15022            | "branch_length_substitutions" | "num_unrooted_trees" | "bayes_posterior"
15023            | "hw_expected_counts" | "allele_frequency" | "ld_d" | "ld_r_squared"
15024 | "heterozygosity" | "ne_from_variance"
15025            | "expected_coverage" | "lander_waterman_gaps"
15026            | "bh_adjusted_p" | "zscore_count"
15027 | "go_enrichment_p" | "blosum45_score"
15028            | "henikoff_weight" | "hamming_protein" | "codon_usage_variance"
15029            | "dnds_ratio" | "mutation_rate" | "tajimas_d" | "wattersons_theta"
15030            | "coalescent_expected_time" | "coalescent_tree_length" | "nm_from_fst"
15031            // ── batch 34: ODE advanced ────────────────────────────────────
15032            | "bdf1_step" | "bdf2_step" | "bdf3_step" | "bdf4_step" | "bdf5_step" | "bdf6_step"
15033            | "ab1_step" | "ab2_step" | "ab3_step"
15034            | "am2_step" | "am3_step" | "am4_step"
15035            | "ros2_step" | "imex_euler_step" | "symplectic_euler_step"
15036            | "leapfrog_step" | "stormer_verlet_step"
15037            | "rk4_single" | "dopri5_combine" | "rkf45_error"
15038            | "lobatto_iiia_2" | "lobatto_iiic_3" | "gauss_irk_2_stage" | "magnus_1st"
15039            | "euler_lte" | "trapezoidal_lte" | "pi_step_size"
15040            | "stiffness_ratio" | "spectral_radius"
15041            | "heun_euler_step" | "bogacki_shampine_step" | "verner_8_combine"
15042            | "rk_combine" | "ab_coeff_sum"
15043            | "newmark_beta_step" | "wilson_theta_step"
15044            | "strang_split" | "lie_split"
15045            | "exp_euler_step" | "etd_rk2" | "dde_euler_step"
15046            | "em_step" | "milstein_step" | "heun_sde_step" | "stratonovich_correction"
15047            | "predictor_corrector" | "numerical_jacobian_col"
15048            | "cn_coefficient" | "imex_theta_split" | "bulirsch_stoer_step"
15049            | "cfl_number" | "diffusion_stability"
15050            | "lax_friedrichs_flux" | "lax_wendroff_flux"
15051            | "van_leer_limiter" | "minmod_limiter" | "superbee_limiter" | "mc_limiter"
15052            // ── batch 35: cryptanalysis & number theory deep ──────────────
15053            | "pollard_p_minus_1" | "fermat_factor"
15054            | "trial_smallest_factor" | "bsgs_discrete_log"
15055            | "mertens" | "liouville"
15056            | "is_b_smooth" | "primorial_n"
15057            | "pseudoprime_base2" | "strong_pseudoprime"
15058            | "aks_witness_count" | "qs_relation"
15059            | "index_calculus_naive" | "lll_2x2_step" | "coppersmith_bound"
15060            | "shor_period_prob" | "rsa_d_from_e" | "dh_secret"
15061            | "elgamal_encrypt" | "ecc_point_double" | "continued_fraction_sqrt"
15062            | "pell_fundamental" | "sum_two_squares" | "class_number_bound"
15063            | "smith_normal_2x2_step" | "regulator_naive"
15064            | "power_residue_check" | "wieferich_check" | "wilson_test"
15065            | "goldbach_pair" | "english_likeness" | "xor_break_singlebyte"
15066            | "bit_reverse_64"
15067            | "gf256_multiply" | "hash_combine"
15068            // ── batch 36: econometrics ────────────────────────────────────
15069            | "arch_lm_test" | "breusch_pagan_test" | "white_robust_se"
15070            | "newey_west_se" | "hansen_j_test" | "gmm_moment_condition"
15071            | "hausman_test" | "breusch_godfrey_test" | "box_pierce_test"
15072            | "adf_test_stat" | "pp_test_stat" | "kpss_test_stat"
15073            | "dickey_fuller_critical" | "engle_granger_step"
15074            | "johansen_trace_step" | "vecm_alpha_beta"
15075            | "panel_within_estimator" | "panel_between_estimator"
15076            | "panel_random_effects" | "arellano_bond_step"
15077            | "ols_estimator" | "ols_residual_variance" | "ols_r_squared"
15078            | "ols_adjusted_r2" | "akaike_info_crit" | "bayesian_info_crit"
15079            | "hannan_quinn_ic" | "f_statistic_pooled" | "breusch_pagan_lm"
15080            | "ramsey_reset_test" | "chow_test_stat" | "white_test_stat"
15081            | "goldfeld_quandt" | "wald_test_stat" | "score_test_stat"
15082            | "likelihood_ratio_test" | "two_sls_iv" | "iv_estimator"
15083            | "mle_normal_log_lik" | "mle_exponential_log_lik"
15084            | "mle_poisson_log_lik" | "gmm_moment_function"
15085            | "pooling_test_stat" | "heteroskedasticity_test"
15086            | "robust_se_huber_white" | "bootstrap_se_estimate"
15087            | "heckman_correction" | "tobit_log_likelihood"
15088            | "probit_log_likelihood" | "logit_log_likelihood"
15089            | "multinomial_logit_prob" | "ordered_probit_threshold"
15090            | "panel_var_step" | "impulse_response_step"
15091            | "variance_decomposition" | "granger_causality_chi2"
15092            | "cointegration_residual" | "error_correction_step"
15093            | "random_walk_innovation" | "random_walk_drift_step"
15094            | "ar_model_likelihood" | "ma_model_likelihood"
15095            | "arma_model_innovation"
15096            // ── batch 37: algebraic topology, knot theory, lie algebras ───
15097            | "euler_char_complex" | "betti_zero" | "betti_one" | "betti_two"
15098            | "genus_surface" | "chern_first_2d" | "genus_curve_arith"
15099            | "genus_curve_geo" | "hodge_diamond_value" | "poincare_duality"
15100            | "fundamental_group_zn" | "homology_rank" | "cohomology_rank"
15101            | "homotopy_group_sphere_pi" | "mapping_class_torus"
15102            | "linking_number_two" | "writhe_polygon" | "torsion_coefficient"
15103            | "simplex_volume_n" | "simplicial_volume" | "nerve_complex_count"
15104            | "cech_zero_cohomology" | "de_rham_zero"
15105            | "poincare_polynomial_eval" | "chromatic_homology_rank"
15106            | "khovanov_q_grading" | "hochschild_zero" | "cyclic_homology_step"
15107            | "group_cohomology_dim" | "group_homology_dim"
15108            | "abelianization_quotient" | "free_group_rank_lower"
15109            | "nilpotency_class_lower" | "solvable_length_upper"
15110            | "schreier_index" | "todd_genus_eval" | "hirzebruch_signature"
15111            | "chern_simons_action" | "gauss_bonnet_total"
15112            | "seifert_genus_lower" | "alexander_polynomial_at_one"
15113            | "jones_polynomial_at_minus_one" | "jones_polynomial_at_i"
15114            | "homfly_evaluation" | "kauffman_bracket_eval"
15115            | "cabling_pair_signature" | "seifert_form_2x2"
15116            | "turaev_alexander_step" | "v_polynomial_eval"
15117            | "polynomial_jones_skein" | "delta_complex_count"
15118            | "poset_zeta_two" | "mobius_poset_two" | "mobius_function_pair"
15119            | "mobius_inversion_step" | "incidence_algebra_dim"
15120            | "quiver_path_count" | "representation_dim_step"
15121            | "weyl_group_order" | "root_system_count"
15122            | "cartan_determinant_a2" | "cartan_matrix_b2"
15123            | "killing_form_su2" | "casimir_eigenvalue_su2"
15124            | "universal_enveloping_dim" | "verma_character_step"
15125            | "plethystic_substitution_value" | "schur_polynomial_eval"
15126            | "hall_inner_product_two" | "plactic_class_size"
15127            | "robinson_schensted_pair" | "yamanouchi_word_count"
15128            | "rsk_size" | "character_su2" | "character_sun"
15129            | "quantum_dimension_su2" | "quantum_dimension_q"
15130            | "fusion_rule_su2_step" | "modular_data_s_value"
15131            | "modular_data_t_value" | "verlinde_count_step"
15132            | "quantum_invariant_eval" | "operad_count_two"
15133            | "moduli_dimension_curves" | "hodge_polynomial_eval"
15134            | "mirror_symmetry_check" | "gromov_witten_invariant"
15135            | "donaldson_invariant" | "seiberg_witten_value"
15136            | "floer_homology_rank" | "khovanov_rasmussen_s"
15137            | "ozsvath_szabo_tau" | "heegaard_genus_lower"
15138            | "fintushel_stern_step" | "bauer_furuta_step"
15139            | "geometric_intersection_number"
15140            | "algebraic_intersection_number"
15141            // ── batch 38: electrochemistry, batteries, fuel cells ─────────
15142            | "nernst_potential_full" | "electrode_potential_step"
15143            | "exchange_current_density" | "butler_volmer_current"
15144            | "tafel_anodic_current" | "tafel_cathodic_current"
15145            | "mass_transport_overpotential" | "limiting_current_density"
15146            | "diffusion_layer_thickness" | "faradaic_efficiency"
15147            | "coulombic_efficiency_cell" | "energy_efficiency_cell"
15148            | "voltaic_efficiency" | "charge_capacity_battery"
15149            | "energy_density_battery" | "power_density_battery"
15150            | "specific_capacity_active" | "columbic_capacity_lihalfcell"
15151            | "ragone_point" | "peukert_capacity" | "peukert_exponent_fit"
15152            | "shepherd_voltage_step" | "nernst_planck_flux"
15153            | "debye_length_electrolyte" | "debye_huckel_activity"
15154            | "gouy_chapman_potential" | "stern_layer_capacitance"
15155            | "double_layer_capacitance" | "helmholtz_capacitance"
15156            | "zeta_potential_estimate" | "electroosmotic_velocity"
15157            | "hagen_poiseuille_eo" | "diffuse_layer_thickness"
15158            | "poisson_boltzmann_step" | "linearized_pb_step"
15159            | "electrochem_impedance_z" | "randles_circuit_z"
15160            | "warburg_impedance" | "cole_cole_eis" | "nyquist_phase"
15161            | "charge_transfer_resistance" | "solution_resistance_estimate"
15162            | "ionic_conductivity_arrhenius" | "nernst_einstein_diffusivity"
15163            | "walden_product" | "kohlrausch_law"
15164            | "onsager_relation_two_species" | "trasatti_voltammetry_charge"
15165            | "randles_sevcik_peak" | "levich_current_rde"
15166            | "koutecky_levich_intercept" | "mott_schottky_capacitance"
15167            | "flat_band_potential" | "schottky_barrier_height"
15168            | "photocurrent_density" | "quantum_efficiency_photo"
15169            | "overall_efficiency_pec" | "fuel_cell_polarization"
15170            | "electrolyzer_voltage" | "faraday_efficiency_h2"
15171            | "overpotential_oer" | "overpotential_her"
15172            | "electrocrystallization_step" | "nucleation_rate_constant"
15173            | "metal_corrosion_rate" | "pourbaix_line_value"
15174            | "mixed_potential_step" | "electrochemiluminescence_yield"
15175            | "solid_electrolyte_capacity" | "ionic_liquid_viscosity_step"
15176            | "lithium_ion_diffusivity" | "soc_estimate_coulomb"
15177            | "soh_capacity_fade" | "ocv_lithium_ion_step"
15178            | "state_of_charge_kalman" | "thermal_runaway_threshold"
15179            | "joule_heating_battery" | "calorimetric_heat_battery"
15180            | "abuse_test_voltage" | "swelling_strain_step"
15181            | "sei_resistance_growth" | "binder_content_optimal"
15182            | "porosity_active_layer" | "tortuosity_estimate_bruggeman"
15183            | "electrolyte_decomposition_temp" | "gibbs_thomson_undercooling"
15184            | "nernst_diffusion_layer" | "diff_coeff_aqueous_estimate"
15185            | "salt_activity_coefficient" | "mean_activity_coeff_pitzer"
15186            | "osmotic_coefficient_pitzer" | "debye_huckel_screening_factor"
15187            | "ph_at_isoelectric" | "buffer_capacity_acid_base"
15188            | "henderson_hasselbalch_solve" | "titration_endpoint_index"
15189            // ── batch 39: tensor calculus, GR, differential geometry ──────
15190            | "tensor_contract_two" | "tensor_outer_two" | "tensor_trace_index"
15191            | "tensor_symmetrize_two" | "tensor_antisymmetrize_two"
15192            | "levi_civita_three" | "levi_civita_four"
15193            | "kronecker_three" | "kronecker_four"
15194            | "metric_minkowski_eta_step" | "metric_schwarzschild_step"
15195            | "metric_kerr_step_simple" | "metric_frw_lapse"
15196            | "christoffel_first_kind_step" | "christoffel_second_kind_step"
15197            | "riemann_tensor_step_zero" | "riemann_curvature_normal_form"
15198            | "ricci_tensor_step_zero" | "scalar_curvature_step"
15199            | "einstein_tensor_step" | "weyl_tensor_step_zero"
15200            | "schouten_tensor_step" | "geodesic_equation_step_zero"
15201            | "parallel_transport_step" | "covariant_derivative_step"
15202            | "christoffel_symbol_normalize" | "ricci_identity_step"
15203            | "bianchi_first_identity_check" | "bianchi_second_identity_check"
15204            | "killing_vector_lie_step" | "lie_derivative_scalar_step"
15205            | "lie_derivative_vector_step" | "exterior_derivative_one_form"
15206            | "hodge_star_one_form" | "codifferential_step"
15207            | "laplace_de_rham_step" | "volume_form_riemannian"
15208            | "hodge_inner_product_one" | "sectional_curvature_two_plane"
15209            | "gauss_codazzi_step" | "mainardi_codazzi_step"
15210            | "weingarten_map_step" | "shape_operator_eig"
15211            | "mean_curvature_step" | "gaussian_curvature_step"
15212            | "extrinsic_principal_curv" | "intrinsic_principal_curv"
15213            | "geodesic_curvature_step" | "darboux_frame_step"
15214            | "fermi_normal_step" | "synge_world_function"
15215            | "raychaudhuri_step" | "expansion_scalar_step"
15216            | "shear_tensor_step" | "twist_tensor_step"
15217            | "optical_scalars_step" | "peeling_step_psi4"
15218            | "ads_metric_step" | "de_sitter_metric_step"
15219            | "warped_product_step_zero" | "kaluza_klein_step"
15220            | "brans_dicke_step" | "horndeski_step"
15221            | "einstein_dilaton_step" | "gauss_bonnet_term_2d"
15222            | "chern_pontryagin_4d_step" | "adm_mass_step"
15223            | "komar_mass_step" | "bondi_mass_step"
15224            | "brown_york_quasilocal" | "isolated_horizon_charge"
15225            | "trapped_surface_check" | "apparent_horizon_step"
15226            | "event_horizon_check" | "cosmological_constant_term"
15227            | "de_sitter_radius_step" | "anti_de_sitter_radius_step"
15228            | "penrose_diagram_factor" | "conformal_compactification_step"
15229            | "schwarzschild_kruskal_step" | "gullstrand_painleve_step"
15230            | "kerr_newman_charge_term" | "boyer_lindquist_step"
15231            | "hartle_thorne_metric" | "oppenheimer_volkoff_step"
15232            | "post_newtonian_step" | "shapiro_delay_step"
15233            | "mercury_perihelion_advance"
15234            | "gravitational_wave_quadrupole"
15235            | "plus_polarization_amp" | "cross_polarization_amp"
15236            | "chirp_mass_inspiral_step" | "isco_radius_kerr_step"
15237            | "spin_orbit_coupling_term" | "spin_spin_coupling_term"
15238            | "hawking_area_increase" | "unruh_temperature_full"
15239            | "bekenstein_entropy_step" | "holographic_entanglement_step"
15240            | "ryu_takayanagi_step" | "swampland_distance_check"
15241            // ── batch 40: information theory, coding, signal processing ──
15242            | "conditional_entropy_step" | "joint_entropy_step"
15243            | "relative_entropy_kl" | "mutual_information_step"
15244            | "chain_rule_entropy" | "fano_inequality_bound"
15245            | "data_processing_inequality" | "arithmetic_coding_interval"
15246            | "range_coding_step" | "golomb_rice_code"
15247            | "elias_gamma_code" | "elias_delta_code" | "exp_golomb_code"
15248            | "fibonacci_code" | "shannon_fano_elias_code"
15249            | "huffman_balanced_step" | "arithmetic_decode_interval"
15250            | "range_decode_step" | "universal_code_length"
15251            | "ziv_lempel_estimate" | "lz77_match_length"
15252            | "lz78_dictionary_growth" | "lzw_step_dict"
15253            | "ppm_predict_prob" | "deflate_huffman_lit"
15254            | "brotli_distance_code_count" | "zstd_window_size_log"
15255            | "mpeg_quant_value" | "jpeg_zig_zag_index"
15256            | "jpeg_dct_8x8_quant" | "hadamard_walsh_transform_step"
15257            | "karhunen_loeve_step" | "discrete_haar_step"
15258            | "db4_wavelet_step" | "biorthogonal_step"
15259            | "beylkin_wavelet_step" | "coiflet_wavelet_step"
15260            | "mallat_pyramid_step" | "threshold_soft_value"
15261            | "threshold_hard_value" | "median_filter_window"
15262            | "mean_filter_window" | "gaussian_filter_window"
15263            | "unsharp_mask_step" | "sobel_kernel_value"
15264            | "prewitt_kernel_value" | "roberts_kernel_value"
15265            | "laplacian_kernel_value" | "canny_threshold_step"
15266            | "hough_accumulator_step" | "ransac_iteration_count"
15267            | "optical_flow_lk_step" | "horn_schunck_step"
15268            | "kalman_predict_state" | "kalman_update_state"
15269            | "particle_filter_resample" | "unscented_sigma_point"
15270            | "ekf_jacobian_step" | "markov_decision_value"
15271            | "bellman_equation_step" | "q_learning_update"
15272            | "policy_iteration_step" | "value_iteration_step"
15273            | "sarsa_update" | "double_q_learning_step"
15274            | "ucb1_action_value" | "thompson_sample_beta"
15275            | "boltzmann_softmax_action" | "explore_exploit_epsilon"
15276            | "montecarlo_returns_step" | "td_zero_update"
15277            | "td_lambda_update" | "gradient_temporal_diff"
15278            | "deep_q_target" | "ddpg_critic_loss_step"
15279            | "ppo_clip_term" | "trpo_kl_constraint"
15280            | "a3c_advantage_step" | "ppo_advantage_step"
15281            | "gae_advantage_step" | "generalized_advantage"
15282            | "information_bottleneck_step" | "free_energy_principle"
15283            | "fisher_info_metric" | "kullback_jensen_div"
15284            | "hellinger_distance_step" | "total_variation_distance"
15285            | "bhattacharyya_coefficient" | "wasserstein_dist_emp"
15286            | "chisquare_metric" | "hellinger_kernel"
15287            | "jensen_shannon_div" | "renyi_divergence_step"
15288            | "amari_alpha_div" | "csiszar_phi_div"
15289            | "sinkhorn_iteration_step" | "sliced_wasserstein"
15290            | "gromov_wasserstein_step" | "spectral_signature_match"
15291            | "mfcc_coeff_step" | "chroma_feature_step"
15292            // ── batch 41: combinatorial optimization, scheduling ──────────
15293            | "tsp_lower_bound_mst" | "tsp_held_karp_step"
15294            | "christofides_ratio_bound" | "two_opt_swap_delta"
15295            | "or_opt_delta" | "three_opt_delta" | "lin_kernighan_step"
15296            | "nearest_neighbor_tour_step" | "greedy_edge_tour"
15297            | "nearest_insertion_step" | "farthest_insertion_step"
15298            | "cheapest_insertion_step" | "max_flow_ford_fulkerson_step"
15299            | "edmonds_karp_step" | "dinic_blocking_flow"
15300            | "push_relabel_step" | "boykov_kolmogorov_step"
15301            | "mincut_stoer_wagner" | "gomory_hu_step"
15302            | "karger_contract_edge" | "karger_min_cut_count"
15303            | "maximum_bipartite_matching" | "hopcroft_karp_phase"
15304            | "blossom_match_step" | "weighted_match_kuhn_step"
15305            | "hungarian_method_step" | "ap_jonker_volgenant_step"
15306            | "assignment_lower_bound" | "job_shop_makespan_lower"
15307            | "flow_shop_johnson_step" | "parallel_machine_lpt"
15308            | "parallel_machine_spt" | "list_scheduling_step"
15309            | "graham_2approx_bound" | "chc_bound_makespan"
15310            | "bin_packing_first_fit" | "bin_packing_best_fit"
15311            | "bin_packing_next_fit" | "bin_packing_lower_bound_l1"
15312            | "multidim_packing_step" | "knapsack_01_dp_value"
15313            | "knapsack_unbounded_dp" | "knapsack_fractional_step"
15314            | "knapsack_branch_bound" | "knapsack_lp_relaxation"
15315            | "multi_knapsack_step" | "quadratic_assignment_step"
15316            | "qap_lower_bound" | "graph_coloring_dsatur_step"
15317            | "graph_coloring_welsh_powell"
15318            | "graph_coloring_brooks_bound" | "graph_coloring_lp_bound"
15319            | "fractional_chromatic_lower" | "list_coloring_step"
15320            | "edge_coloring_vizing_step" | "clique_number_lower"
15321            | "independence_number_upper" | "vertex_cover_lp_round"
15322            | "dominating_set_greedy_step" | "dominating_set_lp_bound"
15323            | "set_cover_greedy_step" | "set_cover_lp_round"
15324            | "hitting_set_greedy" | "weighted_set_cover_step"
15325            | "matroid_greedy_step" | "matroid_intersection_step"
15326            | "submodular_greedy_step" | "submodular_curvature_bound"
15327            | "nemhauser_wolsey_bound" | "lp_relax_round"
15328            | "branch_and_bound_step" | "cutting_plane_step"
15329            | "gomory_cut_step" | "chvatal_gomory_cut"
15330            | "mixed_integer_round_up" | "mixed_integer_round_down"
15331            | "sos_constraint_check" | "column_generation_step"
15332            | "benders_decomposition_step" | "dantzig_wolfe_step"
15333            | "lagrangian_relax_step" | "lagrangian_dual_step"
15334            | "subgradient_step_size" | "nonlinear_dual_step"
15335            | "augmented_lagrangian_step" | "admm_primal_step"
15336            | "admm_dual_step" | "proximal_gradient_step"
15337            | "nesterov_accelerate_step" | "fista_step" | "ista_step"
15338            | "mirror_descent_step" | "frank_wolfe_step"
15339            | "conditional_gradient_step" | "greedy_set_cover_round"
15340            | "local_search_swap_step" | "tabu_search_move_score"
15341            | "simulated_annealing_step" | "genetic_crossover_one_point"
15342            | "mutation_bit_flip_prob" | "roulette_wheel_select_index"
15343            // ── batch 42: climate, fluids, atmospheric ────────────────────
15344            | "stefan_boltzmann_radiation" | "emissivity_grey_body"
15345            | "albedo_blackbody_balance" | "solar_constant_at_distance"
15346            | "total_solar_irradiance_step" | "absorbed_short_wave"
15347            | "emitted_long_wave" | "clausius_clapeyron_full"
15348            | "relative_humidity_step" | "dewpoint_temperature_full"
15349            | "wet_bulb_potential" | "virtual_temperature_full"
15350            | "density_altitude_full" | "geopotential_height_full"
15351            | "geometric_height_full" | "adiabatic_lapse_rate_dry"
15352            | "adiabatic_lapse_rate_moist" | "brunt_vaisala_full"
15353            | "richardson_number_step" | "gradient_richardson_full"
15354            | "flux_richardson_full" | "turbulent_kinetic_energy_step"
15355            | "mixing_length_prandtl" | "monin_obukhov_length"
15356            | "similarity_function_phi" | "log_law_wind_profile"
15357            | "power_law_wind_profile" | "ekman_layer_depth"
15358            | "ekman_pumping_step" | "geostrophic_wind_step"
15359            | "gradient_wind_step" | "thermal_wind_step"
15360            | "quasi_geostrophic_omega" | "omega_equation_step"
15361            | "potential_temperature_step" | "equivalent_potential_temp"
15362            | "saturation_equivalent_pt" | "ipv_potential_vorticity"
15363            | "ertel_pv_step" | "absolute_vorticity_step"
15364            | "relative_vorticity_step" | "divergence_omega_step"
15365            | "streamfunction_step" | "velocity_potential_step"
15366            | "helmholtz_decomp_step" | "courant_friedrichs_lewy"
15367            | "peclet_number_step" | "prandtl_number_step"
15368            | "reynolds_full_number" | "schmidt_number_step"
15369            | "sherwood_number_step" | "nusselt_full_number"
15370            | "grashof_number_step" | "rayleigh_number_step"
15371            | "weber_number_step" | "froude_number_step"
15372            | "strouhal_full" | "mach_full_step"
15373            | "biot_number_step" | "fourier_number_step"
15374            | "turbulence_intensity_step" | "hurst_exponent_estimate"
15375            | "detrended_fluct_alpha" | "power_spectrum_slope"
15376            | "spectral_kappa_minus53" | "batchelor_scale_step"
15377            | "kolmogorov_microscale" | "taylor_microscale_step"
15378            | "integral_length_scale" | "turbulent_dissipation_eps"
15379            | "isotropic_relation_check" | "sst_anomaly_step"
15380            | "enso_index_step" | "amo_index_step" | "nao_index_step"
15381            | "soi_oscillation_index" | "pdo_index_step" | "mjo_phase_step"
15382            | "walker_circulation_step" | "hadley_cell_max_lat"
15383            | "ferrel_cell_step" | "itcz_position_lat" | "trade_wind_speed"
15384            | "westerlies_jet_speed" | "polar_vortex_radius"
15385            | "arctic_oscillation_step" | "indian_monsoon_index"
15386            | "african_monsoon_index" | "qbo_oscillation_step"
15387            | "solar_cycle_phase" | "sunspot_relative_number"
15388            | "geomagnetic_kp_index" | "ozone_dobson_total"
15389            | "chlorine_radical_decay" | "montreal_protocol_track"
15390            | "co2_growth_rate_step" | "methane_growth_rate"
15391            | "aerosol_optical_depth" | "ice_age_milankovitch"
15392            | "greenhouse_forcing_step"
15393            // ── batch 43: game theory, mechanism design, social choice ────
15394            | "game_two_player_value" | "nash_equilibrium_pair"
15395            | "mixed_strategy_value" | "zero_sum_minmax"
15396            | "saddle_point_check" | "correlated_equilibrium_value"
15397            | "shapley_value_two_step" | "banzhaf_index_two"
15398            | "nucleolus_lp_step" | "core_membership_check"
15399            | "imputation_efficient_check" | "imputation_individual_rational"
15400            | "prisoners_dilemma_payoff" | "matching_pennies_payoff"
15401            | "chicken_game_payoff" | "stag_hunt_payoff"
15402            | "battle_sexes_payoff" | "public_goods_game_payoff"
15403            | "tragedy_commons_metric" | "ultimatum_acceptance_prob"
15404            | "dictator_game_share" | "trust_game_repayment"
15405            | "cooperative_game_value" | "characteristic_function"
15406            | "bargaining_set_check" | "kalai_smorodinsky_step"
15407            | "nash_bargaining_solution" | "egalitarian_solution"
15408            | "utilitarian_solution" | "social_welfare_sum"
15409            | "arrow_impossibility_check" | "gibbard_satterthwaite_check"
15410            | "borda_count_step" | "condorcet_winner_check"
15411            | "plurality_winner_step" | "kemeny_score_step"
15412            | "dodgson_swap_count" | "coombs_runoff_step"
15413            | "single_transferable_vote" | "range_voting_score"
15414            | "approval_voting_max" | "schulze_method_step"
15415            | "copeland_score_step" | "black_method_winner"
15416            | "median_voter_step" | "hotelling_location_step"
15417            | "arrow_pareto_check" | "fair_division_envy_free"
15418            | "proportional_share" | "maximin_share"
15419            | "egalitarian_split" | "nash_social_welfare"
15420            | "divisible_goods_proportional" | "indivisible_envy_free_check"
15421            | "adjusted_winner_pct" | "sealed_bid_first_price"
15422            | "sealed_bid_second_price" | "english_auction_step"
15423            | "dutch_auction_step" | "all_pay_auction_step"
15424            | "vcg_payment_step" | "revenue_equivalence_check"
15425            | "truthful_mechanism_check" | "incentive_compatibility_check"
15426            | "mechanism_design_obj" | "double_auction_step"
15427            | "combinatorial_auction_step" | "posted_price_offer_accept"
15428            | "matching_market_step" | "deferred_acceptance_step"
15429            | "boston_mechanism_step" | "top_trading_cycles_step"
15430            | "school_choice_match" | "roommate_match_step"
15431            | "network_formation_step" | "coordination_game_payoff"
15432            | "evolutionary_stable_strategy" | "replicator_dynamics_step"
15433            | "hawk_dove_payoff" | "fictitious_play_step"
15434            | "best_response_dynamic" | "quantal_response_logit"
15435            | "level_k_step" | "cognitive_hierarchy_step"
15436            | "sequential_eq_check" | "subgame_perfect_eq"
15437            | "stackelberg_step" | "cournot_quantity_step"
15438            | "bertrand_price_step" | "hotelling_price_step"
15439            | "collusion_payoff_step" | "folk_theorem_value"
15440            | "repeated_game_avg_payoff" | "discount_factor_step"
15441            | "trigger_strategy_payoff" | "grim_trigger_step"
15442            | "tit_for_tat_step" | "prisoners_repeated_eq"
15443            | "mertens_zamir_step" | "ex_post_value_check"
15444            | "ex_ante_value_check" | "common_knowledge_iterations"
15445            // ── batch 44: symbolic CAS, decompositions, projections ───────
15446            | "cas_simplify_term" | "cas_expand_two_terms"
15447            | "cas_factor_quadratic" | "cas_partial_fraction_simple"
15448            | "cas_polynomial_gcd_step" | "cas_polynomial_div_step"
15449            | "cas_lagrange_interpolate" | "cas_chebyshev_eval"
15450            | "cas_legendre_eval" | "cas_hermite_eval"
15451            | "cas_laguerre_eval" | "cas_jacobi_eval"
15452            | "cas_gegenbauer_eval" | "cas_taylor_coefficient"
15453            | "cas_padé_diagonal" | "cas_continued_fraction_step"
15454            | "cas_resultant_two" | "cas_subresultant_two"
15455            | "cas_groebner_lt_step" | "cas_buchberger_step"
15456            | "cas_macaulay_matrix_step" | "cas_modular_inverse"
15457            | "cas_extended_euclid_step" | "cas_smith_normal_step"
15458            | "cas_hermite_normal_step" | "cas_radical_simplify"
15459            | "cas_minimal_polynomial" | "cas_gcd_polynomial_step"
15460            | "cas_resultant_x_y" | "cas_solve_linear"
15461            | "cas_solve_quadratic" | "cas_solve_cubic"
15462            | "cas_solve_quartic" | "cas_solve_polynomial_n"
15463            | "cas_root_isolate_step" | "cas_sturm_sequence_step"
15464            | "cas_descartes_rule_count" | "cas_companion_matrix_root"
15465            | "cas_polynomial_roots_kahan"
15466            | "cas_eigenvalue_inverse_iteration" | "cas_qr_iteration_step"
15467            | "cas_jacobi_eigen_step" | "cas_lanczos_iteration_step"
15468            | "cas_arnoldi_iteration_step" | "cas_givens_rotation_apply"
15469            | "cas_householder_reflection" | "cas_modified_gram_schmidt"
15470            | "cas_classical_gram_schmidt" | "cas_rank_revealing_qr"
15471            | "cas_pivoted_lu_step" | "cas_block_lu_step"
15472            | "cas_cholesky_step" | "cas_modified_cholesky"
15473            | "cas_ldlt_step" | "cas_bunch_kaufman_step"
15474            | "cas_woodbury_identity" | "cas_matrix_pencil_step"
15475            | "cas_generalized_eigen" | "cas_singular_value_step"
15476            | "cas_truncated_svd_value" | "cas_pseudoinverse_step"
15477            | "cas_polar_decomposition" | "cas_schur_decomposition_step"
15478            | "cas_quasi_triangular" | "cas_riccati_continuous_step"
15479            | "cas_riccati_discrete_step" | "cas_lyapunov_continuous_step"
15480            | "cas_lyapunov_discrete_step" | "cas_sylvester_equation_step"
15481            | "cas_kronecker_product_step" | "cas_vec_operator_step"
15482            | "cas_matrix_function_step" | "cas_matrix_log_step"
15483            | "cas_matrix_exp_pade" | "cas_matrix_sqrt_step"
15484            | "cas_drazin_inverse_step" | "cas_moore_penrose_step"
15485            | "cas_least_squares_solve" | "cas_total_least_squares"
15486            | "cas_constrained_ls_step" | "cas_truncated_lsq"
15487            | "cas_regularized_lsq_tikhonov" | "cas_basis_pursuit_step"
15488            | "cas_lasso_soft_threshold" | "cas_elastic_net_step"
15489            | "cas_omp_step" | "cas_iht_iteration"
15490            | "cas_cosamp_step" | "cas_admm_lasso_step"
15491            | "cas_proximal_l1_step" | "cas_proximal_l2_step"
15492            | "cas_proximal_l_inf_step" | "cas_indicator_simplex_proj"
15493            | "cas_proj_l1_ball" | "cas_proj_l2_ball"
15494            | "cas_proj_box" | "cas_proj_psd_cone"
15495            | "cas_proj_soc_step" | "cas_proj_exp_cone"
15496            | "cas_dykstra_step" | "cas_alternating_projection"
15497            | "cas_polya_enumeration_step" | "cas_burnside_count_step"
15498            // ── batch 45: ML primitives — activations, losses, optimizers ─
15499            | "ml_relu_step" | "ml_leaky_relu_step" | "ml_elu_step"
15500            | "ml_selu_step" | "ml_gelu_step" | "ml_swish_step"
15501            | "ml_mish_step" | "ml_softplus_step" | "ml_softsign_step"
15502            | "ml_hard_sigmoid" | "ml_hard_tanh" | "ml_prelu_step"
15503            | "ml_celu_step" | "ml_silu_step" | "ml_logsumexp_step"
15504            | "ml_log_softmax_step" | "ml_log_sigmoid"
15505            | "ml_glu_step" | "ml_geglu_step" | "ml_swiglu_step"
15506            | "ml_attention_score_step" | "ml_scaled_dot_product"
15507            | "ml_multihead_avg" | "ml_softmax_temperature"
15508            | "ml_dropout_mask_prob" | "ml_layer_norm_step"
15509            | "ml_batch_norm_step" | "ml_group_norm_step"
15510            | "ml_rms_norm_step" | "ml_instance_norm_step"
15511            | "ml_weight_norm_step" | "ml_spectral_norm_step"
15512            | "ml_l2_normalize_step" | "ml_huber_loss_step"
15513            | "ml_smooth_l1_loss" | "ml_focal_loss_step"
15514            | "ml_dice_loss_step" | "ml_iou_loss_step"
15515            | "ml_giou_loss_step" | "ml_diou_loss_step"
15516            | "ml_ciou_loss_step" | "ml_contrastive_loss"
15517            | "ml_triplet_loss_step" | "ml_arcface_loss_step"
15518            | "ml_center_loss_step" | "ml_kl_divergence_loss"
15519            | "ml_cross_entropy_loss" | "ml_binary_cross_entropy"
15520            | "ml_label_smoothing" | "ml_mixup_lambda"
15521            | "ml_cutmix_box_iou" | "ml_random_erasing_step"
15522            | "ml_cosine_lr_schedule" | "ml_warmup_lr_step"
15523            | "ml_step_lr_schedule" | "ml_exponential_lr"
15524            | "ml_polynomial_lr" | "ml_one_cycle_lr"
15525            | "ml_inverse_sqrt_lr" | "ml_cyclic_lr_step"
15526            | "ml_sgd_step" | "ml_momentum_step"
15527            | "ml_nesterov_momentum" | "ml_adagrad_step"
15528            | "ml_rmsprop_step" | "ml_adam_step"
15529            | "ml_adamw_step" | "ml_adamax_step"
15530            | "ml_nadam_step" | "ml_radam_step"
15531            | "ml_lookahead_step" | "ml_lamb_step"
15532            | "ml_lars_step" | "ml_yogi_step"
15533            | "ml_amsgrad_step" | "ml_adabelief_step"
15534            | "ml_shampoo_step" | "ml_lion_step"
15535            | "ml_sophia_step" | "ml_gradient_clip_norm"
15536            | "ml_gradient_clip_value" | "ml_gradient_accumulate"
15537            | "ml_gradient_centralize" | "ml_weight_decay_step"
15538            | "ml_he_init_value" | "ml_xavier_init_value"
15539            | "ml_glorot_init_value" | "ml_orthogonal_init"
15540            | "ml_truncnormal_init" | "ml_kaiming_init"
15541            | "ml_lecun_init_value" | "ml_zero_init"
15542            | "ml_constant_init" | "ml_uniform_init"
15543            | "ml_one_hot_index" | "ml_label_to_id"
15544            | "ml_id_to_label_step" | "ml_token_logit_top_k"
15545            | "ml_topk_argmax" | "ml_nucleus_sample_p"
15546            | "ml_temperature_decay" | "ml_repetition_penalty"
15547            | "ml_eos_logit_boost"
15548            // ── batch 46: NLP — ranking, similarity, language models ──────
15549            | "nlp_bm25_score" | "nlp_tf_idf_step" | "nlp_okapi_score"
15550            | "nlp_word_freq_value" | "nlp_doc_freq_step"
15551            | "nlp_inverse_doc_freq" | "nlp_cosine_similarity_two"
15552            | "nlp_jaccard_similarity_two" | "nlp_overlap_coefficient"
15553            | "nlp_dice_coefficient_two" | "nlp_simpson_coefficient"
15554            | "nlp_levenshtein_dist" | "nlp_damerau_levenshtein"
15555            | "nlp_jaro_distance" | "nlp_jaro_winkler"
15556            | "nlp_hamming_distance" | "nlp_lcs_length" | "nlp_lcs_ratio"
15557            | "nlp_meteor_score" | "nlp_bleu_score_n"
15558            | "nlp_rouge_score_n" | "nlp_chrf_score" | "nlp_ter_score"
15559            | "nlp_wer_score" | "nlp_cer_score" | "nlp_perplexity_value"
15560            | "nlp_bits_per_character" | "nlp_char_ngram_count"
15561            | "nlp_word_ngram_count" | "nlp_skip_gram_count"
15562            | "nlp_byte_pair_merge_step" | "nlp_wordpiece_score"
15563            | "nlp_unigram_lm_score" | "nlp_kneser_ney_step"
15564            | "nlp_witten_bell_step" | "nlp_good_turing_count"
15565            | "nlp_laplace_smoothing" | "nlp_lidstone_smoothing"
15566            | "nlp_jelinek_mercer" | "nlp_dirichlet_smoothing"
15567            | "nlp_query_likelihood_step" | "nlp_kl_lm_div"
15568            | "nlp_pmi_score" | "nlp_npmi_score"
15569            | "nlp_chi2_collocation" | "nlp_loglikelihood_collocation"
15570            | "nlp_t_score_collocation" | "nlp_dunning_log_likelihood"
15571            | "nlp_lda_alpha_step" | "nlp_lda_beta_step"
15572            | "nlp_lda_topic_dist" | "nlp_plsa_step"
15573            | "nlp_word2vec_skipgram_loss" | "nlp_word2vec_cbow_loss"
15574            | "nlp_glove_loss_step" | "nlp_fasttext_subword_count"
15575            | "nlp_byte_level_bpe_step" | "nlp_sentencepiece_score"
15576            | "nlp_unigram_subword_loss" | "nlp_subword_regularization"
15577            | "nlp_pointwise_attn_score" | "nlp_relative_position_bias"
15578            | "nlp_alibi_position_bias" | "nlp_rope_rotary_angle"
15579            | "nlp_rope_apply_step" | "nlp_position_encoding_sin"
15580            | "nlp_position_encoding_cos" | "nlp_pe_freq_band"
15581            | "nlp_max_seq_len_check" | "nlp_token_drop_rate"
15582            | "nlp_byte_frequency" | "nlp_char_frequency"
15583            | "nlp_punct_ratio" | "nlp_uppercase_ratio"
15584            | "nlp_digit_ratio" | "nlp_emoji_ratio"
15585            | "nlp_url_count" | "nlp_email_count" | "nlp_phone_count"
15586            | "nlp_hashtag_count" | "nlp_mention_count"
15587            | "nlp_token_overlap_two" | "nlp_word_mover_dist"
15588            | "nlp_sif_weight_step" | "nlp_doc_embedding_avg"
15589            | "nlp_attention_pool_step" | "nlp_max_pool_step"
15590            | "nlp_avg_pool_step" | "nlp_sum_pool_step"
15591            | "nlp_self_attn_compute_step" | "nlp_cross_attn_compute_step"
15592            | "nlp_window_attn_step" | "nlp_strided_attn_step"
15593            | "nlp_block_attn_step" | "nlp_sliding_window_step"
15594            | "nlp_local_attn_step" | "nlp_dilated_attn_step"
15595            | "nlp_global_attn_step" | "nlp_sparse_attn_score"
15596            | "nlp_linformer_step" | "nlp_performer_step"
15597            | "nlp_reformer_step" | "nlp_longformer_step"
15598            | "nlp_bigbird_step" | "nlp_routing_attn_step"
15599            // ── batch 47: graphics, geometry, ray tracing, BRDF, color ────
15600            | "gfx_perspective_proj_x" | "gfx_perspective_proj_y"
15601            | "gfx_orthographic_proj" | "gfx_view_matrix_step"
15602            | "gfx_lookat_forward" | "gfx_lookat_right" | "gfx_lookat_up"
15603            | "gfx_quat_to_axis_angle" | "gfx_axis_angle_to_quat"
15604            | "gfx_quat_slerp_step" | "gfx_quat_nlerp_step"
15605            | "gfx_quat_dot_two" | "gfx_quat_inverse_step"
15606            | "gfx_quat_to_euler_pitch" | "gfx_quat_to_euler_yaw"
15607            | "gfx_quat_to_euler_roll" | "gfx_euler_to_quat_x"
15608            | "gfx_euler_to_quat_y" | "gfx_euler_to_quat_z"
15609            | "gfx_euler_to_quat_w" | "gfx_rotation_matrix_xx"
15610            | "gfx_rotation_matrix_yy" | "gfx_rotation_matrix_zz"
15611            | "gfx_translation_matrix_step" | "gfx_scale_matrix_step"
15612            | "gfx_shear_matrix_xy" | "gfx_homogeneous_divide"
15613            | "gfx_screen_space_x" | "gfx_screen_space_y"
15614            | "gfx_ndc_to_screen_x" | "gfx_ndc_to_screen_y"
15615            | "gfx_screen_to_ndc_x" | "gfx_screen_to_ndc_y"
15616            | "gfx_clip_polygon_step" | "gfx_sutherland_hodgman"
15617            | "gfx_cohen_sutherland_code" | "gfx_liang_barsky_t"
15618            | "gfx_bresenham_step_x" | "gfx_bresenham_step_y"
15619            | "gfx_xiaolin_wu_intensity" | "gfx_aabb_intersect_check"
15620            | "gfx_obb_overlap_step" | "gfx_sphere_intersect_t"
15621            | "gfx_ray_triangle_t" | "gfx_ray_plane_t" | "gfx_ray_box_t"
15622            | "gfx_ray_sphere_t" | "gfx_ray_disk_t"
15623            | "gfx_ray_cylinder_t" | "gfx_ray_cone_t"
15624            | "gfx_ray_ellipsoid_t" | "gfx_ray_torus_t_approx"
15625            | "gfx_barycentric_alpha" | "gfx_barycentric_beta"
15626            | "gfx_barycentric_gamma" | "gfx_phong_diffuse_step"
15627            | "gfx_phong_specular_step" | "gfx_phong_ambient_step"
15628            | "gfx_blinn_specular_step" | "gfx_lambert_term"
15629            | "gfx_oren_nayar_term" | "gfx_cook_torrance_d_ggx"
15630            | "gfx_cook_torrance_g_smith" | "gfx_cook_torrance_f_schlick"
15631            | "gfx_disney_principled_d" | "gfx_microfacet_brdf_step"
15632            | "gfx_subsurface_scattering_term" | "gfx_translucent_falloff"
15633            | "gfx_normal_distribution_ggx"
15634            | "gfx_geometric_attenuation_smith"
15635            | "gfx_fresnel_dielectric_step" | "gfx_fresnel_conductor_step"
15636            | "gfx_index_of_refraction" | "gfx_snells_law_angle"
15637            | "gfx_total_internal_reflection" | "gfx_refract_direction_x"
15638            | "gfx_reflect_direction_x" | "gfx_environment_map_uv_u"
15639            | "gfx_environment_map_uv_v" | "gfx_cube_map_face_index"
15640            | "gfx_octahedral_encode_x" | "gfx_octahedral_encode_y"
15641            | "gfx_spherical_harmonic_y00" | "gfx_spherical_harmonic_y10"
15642            | "gfx_spherical_harmonic_y11" | "gfx_spherical_harmonic_y20"
15643            | "gfx_zonal_harmonic_step" | "gfx_irradiance_sh_eval"
15644            | "gfx_radiance_sh_eval" | "gfx_skybox_uv_u" | "gfx_skybox_uv_v"
15645            | "gfx_tonemap_reinhard" | "gfx_tonemap_aces"
15646            | "gfx_tonemap_uncharted2" | "gfx_tonemap_filmic"
15647            | "gfx_gamma_correct_step" | "gfx_srgb_to_linear"
15648            | "gfx_linear_to_srgb" | "gfx_dither_bayer_4x4"
15649            | "gfx_dither_floyd_steinberg" | "gfx_oklab_l_step"
15650            | "gfx_oklab_a_step" | "gfx_oklab_b_step"
15651            | "gfx_oklch_chroma" | "gfx_oklch_hue"
15652            | "gfx_pcg_hash_step" | "gfx_xorshift_step"
15653            | "gfx_halton_step" | "gfx_sobol_step"
15654            | "gfx_van_der_corput" | "gfx_low_discrepancy_step"
15655            | "gfx_blue_noise_value" | "gfx_perlin_noise_step"
15656            | "gfx_simplex_noise_step" | "gfx_fbm_noise_step"
15657            | "gfx_worley_noise_step" | "gfx_voronoi_distance"
15658            | "gfx_curl_noise_step" | "gfx_gradient_noise_step"
15659            | "gfx_value_noise_step" | "gfx_signed_distance_box"
15660            | "gfx_signed_distance_sphere" | "gfx_signed_distance_capsule"
15661            // ── batch 48: database internals, distributed systems ─────────
15662            | "db_b_tree_split" | "db_b_tree_merge"
15663            | "db_lsm_compaction_step" | "db_skiplist_height_pick"
15664            | "db_bloom_filter_bit_index" | "db_cuckoo_filter_fingerprint"
15665            | "db_quotient_filter_canonical" | "db_count_min_sketch_bin"
15666            | "db_hyperloglog_register_max" | "db_min_hash_value"
15667            | "db_simhash_bit" | "db_consistent_hash_index"
15668            | "db_rendezvous_hash_score" | "db_jump_hash_bucket"
15669            | "db_maglev_hash_step" | "db_lru_cache_eviction_age"
15670            | "db_lfu_cache_decay" | "db_arc_cache_score"
15671            | "db_clock_cache_hand" | "db_tinylfu_admit_score"
15672            | "db_w_tinylfu_freq" | "db_buffer_pool_score"
15673            | "db_query_plan_cost_step" | "db_join_selectivity_step"
15674            | "db_index_seek_cost" | "db_seq_scan_cost"
15675            | "db_index_scan_cost" | "db_sort_cost_estimate"
15676            | "db_hash_join_cost" | "db_merge_join_cost"
15677            | "db_nested_loop_cost" | "db_query_cardinality"
15678            | "db_histogram_bucket_index" | "db_quantile_estimate_p99"
15679            | "db_t_digest_centroid" | "db_kll_quantile_step"
15680            | "db_dd_sketch_bin" | "db_reservoir_sample_index"
15681            | "db_chao_estimator_step" | "db_jaccard_minhash_estimate"
15682            | "db_distinct_estimate_lpc" | "db_distinct_estimate_hll"
15683            | "db_throttle_token_step" | "db_leaky_bucket_step"
15684            | "db_token_bucket_step" | "db_circuit_breaker_step"
15685            | "db_two_phase_commit_step" | "db_three_phase_commit_step"
15686            | "db_paxos_propose_id" | "db_raft_term_advance"
15687            | "db_raft_log_match_check" | "db_zab_epoch_step"
15688            | "db_chubby_lease_step" | "db_logical_clock_step"
15689            | "db_lamport_timestamp" | "db_vector_clock_merge"
15690            | "db_hybrid_logical_clock" | "db_crdt_g_counter_merge"
15691            | "db_crdt_pn_counter_merge" | "db_crdt_lww_register_merge"
15692            | "db_crdt_set_or_merge" | "db_consensus_quorum_size"
15693            | "db_replication_lag_step" | "db_partitions_for_n"
15694            | "db_consistent_lookup_id" | "db_chord_finger_index"
15695            | "db_kademlia_xor_distance" | "db_pastry_routing_step"
15696            | "db_dht_replicate_factor" | "db_partition_failure_check"
15697            | "db_byzantine_quorum_size" | "db_pbft_view_change"
15698            | "db_honey_badger_step" | "db_avalanche_query_step"
15699            | "db_quorum_intersection_check" | "db_anti_entropy_step"
15700            | "db_merkle_node_hash" | "db_merkle_path_verify"
15701            | "db_gossip_fanout_step" | "db_anti_entropy_pull_step"
15702            | "db_split_brain_check" | "db_clock_skew_estimate"
15703            | "db_freshness_score" | "db_read_repair_step"
15704            | "db_hinted_handoff_step" | "db_compaction_score"
15705            | "db_levelled_compaction_step" | "db_size_tiered_compaction"
15706            | "db_universal_compaction_step" | "db_write_amplification"
15707            | "db_read_amplification" | "db_space_amplification"
15708            | "db_block_cache_hit_rate" | "db_page_cache_eviction_age"
15709            | "db_wal_fsync_cost" | "db_group_commit_count"
15710            | "db_replica_lag_threshold" | "db_synchronous_commit_check"
15711            | "db_async_commit_check" | "db_eventual_consistency_check"
15712            | "db_strong_consistency_check" | "db_linearizability_check"
15713            | "db_causal_consistency_check"
15714            // ── batch 49: networking — TCP, AQM, MIMO, queueing ───────────
15715            | "net_tcp_cwnd_step" | "net_tcp_ssthresh_update"
15716            | "net_tcp_reno_step" | "net_tcp_cubic_step"
15717            | "net_tcp_bbr_step" | "net_tcp_vegas_step"
15718            | "net_tcp_westwood_step" | "net_tcp_compound_step"
15719            | "net_tcp_dctcp_step" | "net_tcp_yeah_step"
15720            | "net_tcp_htcp_step" | "net_tcp_hybla_step"
15721            | "net_tcp_illinois_step" | "net_tcp_lp_step"
15722            | "net_tcp_scalable_step" | "net_tcp_veno_step"
15723            | "net_aiad_step" | "net_aimd_step"
15724            | "net_miad_step" | "net_mimd_step"
15725            | "net_aqm_red_drop_prob" | "net_aqm_codel_target"
15726            | "net_aqm_pie_drop_rate" | "net_aqm_fq_codel_step"
15727            | "net_aqm_blue_step" | "net_aqm_choke_step"
15728            | "net_aqm_sfq_step" | "net_aqm_drr_step"
15729            | "net_aqm_wrr_step" | "net_token_rate_limit"
15730            | "net_traffic_shaper_step" | "net_priority_queue_index"
15731            | "net_packet_loss_estimate" | "net_jitter_estimate"
15732            | "net_latency_avg" | "net_rtt_smoothed"
15733            | "net_rtt_variation" | "net_rto_compute"
15734            | "net_bandwidth_delay_product" | "net_path_capacity_kleinrock"
15735            | "net_loss_rate_to_throughput" | "net_throughput_padhye"
15736            | "net_throughput_mathis" | "net_throughput_response"
15737            | "net_router_buffer_size" | "net_drop_tail_check"
15738            | "net_burst_size_compute" | "net_packet_pacing_step"
15739            | "net_link_capacity_share" | "net_proportional_fair_share"
15740            | "net_max_min_fair_step" | "net_alpha_fair_step"
15741            | "net_kelly_pricing_step" | "net_network_utility_max"
15742            | "net_lyapunov_drift_plus_penalty" | "net_backpressure_step"
15743            | "net_max_weight_match" | "net_qcsma_propose"
15744            | "net_csma_back_off" | "net_alohanet_throughput"
15745            | "net_slotted_aloha_throughput" | "net_csma_efficiency"
15746            | "net_token_ring_efficiency" | "net_polling_efficiency"
15747            | "net_radio_path_loss" | "net_friis_received_power"
15748            | "net_two_ray_ground_loss" | "net_okumura_hata_loss"
15749            | "net_log_distance_path" | "net_shadowing_normal"
15750            | "net_rician_k_factor" | "net_rayleigh_envelope"
15751            | "net_doppler_shift" | "net_capacity_shannon"
15752            | "net_mimo_capacity_step" | "net_zero_forcing_beam"
15753            | "net_mmse_beam_step" | "net_water_filling_power"
15754            | "net_amc_threshold_index" | "net_harq_combining_gain"
15755            | "net_turbo_decode_iter" | "net_ldpc_iteration_step"
15756            | "net_polar_decode_step" | "net_viterbi_step"
15757            | "net_bcjr_step" | "net_outage_probability"
15758            | "net_diversity_gain" | "net_array_gain"
15759            | "net_multiplexing_gain" | "net_coding_gain"
15760            | "net_pruning_gain" | "net_macro_diversity_step"
15761            | "net_micro_diversity_step" | "net_handoff_threshold"
15762            | "net_call_admission_check" | "net_blocking_probability"
15763            | "net_erlang_b_formula" | "net_erlang_c_formula"
15764            | "net_engset_formula" | "net_little_law_l"
15765            | "net_throughput_law" | "net_response_time_law"
15766            | "net_utilization_law" | "net_forced_flow_law"
15767            // ── batch 50: OS internals — schedulers, I/O, memory ──────────
15768            | "os_priority_aging_step" | "os_mlfq_demote_step"
15769            | "os_mlfq_promote_step" | "os_round_robin_quantum"
15770            | "os_completely_fair_vruntime" | "os_lottery_ticket_count"
15771            | "os_stride_pass_step" | "os_eevdf_eligible"
15772            | "os_cfs_load_balance_step" | "os_eas_energy_estimate"
15773            | "os_smt_threading_share" | "os_numa_node_distance"
15774            | "os_cpu_affinity_score" | "os_thread_migration_cost"
15775            | "os_load_average_decay" | "os_runqueue_depth"
15776            | "os_io_scheduler_deadline" | "os_io_scheduler_cfq_step"
15777            | "os_io_scheduler_noop_step" | "os_io_scheduler_bfq_step"
15778            | "os_io_scheduler_kyber_step" | "os_io_scheduler_mq_deadline"
15779            | "os_anticipation_window" | "os_elevator_step"
15780            | "os_disk_seek_time" | "os_disk_rotational_lat"
15781            | "os_disk_transfer_time" | "os_pre_fetch_window"
15782            | "os_buffer_cache_pages" | "os_dirty_page_threshold"
15783            | "os_writeback_step" | "os_swappiness_factor"
15784            | "os_kswapd_wake_threshold" | "os_oom_score_step"
15785            | "os_page_replacement_lru" | "os_page_replacement_clock"
15786            | "os_page_replacement_2q" | "os_working_set_size"
15787            | "os_thrashing_threshold" | "os_demand_paging_step"
15788            | "os_copy_on_write_check" | "os_zero_page_optimization"
15789            | "os_huge_page_threshold" | "os_transparent_hugepage"
15790            | "os_kasan_shadow_offset" | "os_kfence_check"
15791            | "os_kfence_alloc_index" | "os_slub_object_size_round"
15792            | "os_slab_color_offset" | "os_per_cpu_cache_size"
15793            | "os_buddy_order_pick" | "os_compact_memory_step"
15794            | "os_kvm_vmcs_field_offset" | "os_apic_irq_priority"
15795            | "os_msi_x_vector_count" | "os_iommu_domain_step"
15796            | "os_pci_bus_address" | "os_acpi_state_transition"
15797            | "os_cpufreq_governor_step" | "os_intel_pstate_target"
15798            | "os_amd_pstate_target" | "os_thermal_zone_trip"
15799            | "os_throttle_temperature" | "os_battery_capacity_pct"
15800            | "os_powertop_score" | "os_idle_state_select"
15801            | "os_c_state_residency" | "os_p_state_voltage"
15802            | "os_dvfs_step" | "os_voltage_scaling_step"
15803            | "os_frequency_scaling_step" | "os_inotify_event_count"
15804            | "os_epoll_ctl_count" | "os_io_uring_sqe_count"
15805            | "os_io_uring_cqe_count" | "os_kqueue_event_count"
15806            | "os_systemd_journal_size" | "os_dmesg_severity_level"
15807            | "os_audit_event_priority" | "os_apparmor_profile_active"
15808            | "os_selinux_context_match" | "os_smack_label_compare"
15809            | "os_capability_check" | "os_seccomp_filter_step"
15810            | "os_namespace_isolation" | "os_cgroup_v1_count"
15811            | "os_cgroup_v2_count" | "os_pid_max_value"
15812            | "os_thread_max_value" | "os_file_max_value"
15813            | "os_open_files_count" | "os_socket_max_value"
15814            | "os_inotify_max_watches" | "os_oom_kill_score"
15815            | "os_zswap_compress_ratio" | "os_zram_compress_ratio"
15816            | "os_swap_pressure_score" | "os_pressure_stall_step"
15817            | "os_psi_avg10_step" | "os_psi_avg60_step"
15818            | "os_psi_avg300_step" | "os_load_proc_avg"
15819            | "os_load_user_avg" | "os_load_iowait_avg"
15820            // ── batch 51: security — KDFs, MFA, PKI, web sec, TLS ─────────
15821            | "sec_argon2_memcost" | "sec_argon2_timecost"
15822            | "sec_argon2_parallelism" | "sec_argon2_block_step"
15823            | "sec_pbkdf2_iter" | "sec_scrypt_n_param"
15824            | "sec_scrypt_r_param" | "sec_scrypt_p_param"
15825            | "sec_balloon_hash_step" | "sec_yescrypt_step"
15826            | "sec_bcrypt_cost_factor" | "sec_bcrypt_round_step"
15827            | "sec_password_strength_zxcvbn" | "sec_haveibeenpwned_check"
15828            | "sec_diceware_word_index" | "sec_xkcd_passphrase_score"
15829            | "sec_passphrase_entropy" | "sec_chosen_charset_strength"
15830            | "sec_keystroke_timing_var" | "sec_2fa_totp_window"
15831            | "sec_totp_drift_check" | "sec_hotp_counter_step"
15832            | "sec_yubikey_otp_check" | "sec_webauthn_attestation_check"
15833            | "sec_fido2_assertion_check" | "sec_certificate_chain_depth"
15834            | "sec_revocation_ocsp_check" | "sec_crl_age_seconds"
15835            | "sec_pki_path_validate" | "sec_x509_subject_match"
15836            | "sec_san_match_count" | "sec_basic_constraints_ca"
15837            | "sec_pinning_compare" | "sec_certificate_transparency"
15838            | "sec_dane_tlsa_match" | "sec_hpkp_pin_match"
15839            | "sec_csp_directive_match" | "sec_csrf_token_match"
15840            | "sec_cors_origin_match" | "sec_xss_filter_score"
15841            | "sec_html_escape_check" | "sec_url_safe_encode_check"
15842            | "sec_path_traversal_detect" | "sec_sqli_pattern_score"
15843            | "sec_xxe_pattern_score" | "sec_xxe_dtd_check"
15844            | "sec_command_injection_score" | "sec_idor_check"
15845            | "sec_jwt_alg_safe" | "sec_jwt_kid_match"
15846            | "sec_jwt_signature_verify" | "sec_oauth2_state_validate"
15847            | "sec_oauth2_pkce_step" | "sec_oauth_nonce_check"
15848            | "sec_session_lifetime" | "sec_idle_timeout_step"
15849            | "sec_login_throttle_step" | "sec_account_lockout_step"
15850            | "sec_password_history_check" | "sec_complexity_policy_score"
15851            | "sec_dictionary_attack_check" | "sec_brute_force_attempts"
15852            | "sec_credential_stuffing_score" | "sec_kerberos_ticket_age"
15853            | "sec_kerberos_pac_check" | "sec_kerberos_pre_auth"
15854            | "sec_ldap_bind_step" | "sec_radius_auth_step"
15855            | "sec_diameter_avp_step" | "sec_saml_assertion_age"
15856            | "sec_oidc_id_token_age" | "sec_acme_dns_challenge"
15857            | "sec_dnssec_signature_check" | "sec_spf_pass_check"
15858            | "sec_dkim_signature_check" | "sec_dmarc_policy_check"
15859            | "sec_arc_chain_step" | "sec_smtp_ssl_check"
15860            | "sec_imap_starttls_check" | "sec_pop3_security_step"
15861            | "sec_tls_alert_severity" | "sec_tls13_handshake_step"
15862            | "sec_tls12_handshake_step" | "sec_tls11_deprecation_check"
15863            | "sec_ssl3_disabled_check" | "sec_cipher_suite_strength"
15864            | "sec_cbc_mac_block_count" | "sec_gcm_iv_unique_check"
15865            | "sec_chachapoly_nonce_check" | "sec_x25519_clamping_step"
15866            | "sec_ed25519_signature_step" | "sec_ed448_signature_step"
15867            | "sec_p384_curve_step" | "sec_secp256k1_step"
15868            | "sec_blake3_chunk_step" | "sec_keccak_round_step"
15869            | "sec_sha3_padding_step" | "sec_argon2_state_advance"
15870            | "sec_chacha20_quarterround" | "sec_aes_round_step"
15871            | "sec_aes_keyschedule_step" | "sec_des_round_step"
15872            | "sec_blowfish_round_step" | "sec_serpent_round_step"
15873            | "sec_twofish_round_step"
15874            // ── batch 52: calendrical algorithms ──────────────────────────
15875            | "fixed_from_gregorian" | "gregorian_from_fixed"
15876            | "fixed_from_julian" | "julian_from_fixed"
15877            | "iso_week_date" | "hebrew_leap_year"
15878            | "hebrew_year_length" | "fixed_from_hebrew"
15879            | "islamic_leap_year" | "fixed_from_islamic"
15880            | "persian_arithmetic_leap" | "fixed_from_persian"
15881            | "coptic_from_fixed" | "ethiopic_from_fixed"
15882            | "french_revolutionary_leap" | "fixed_from_french"
15883            | "chinese_year_zodiac" | "chinese_lunation_winter"
15884            | "hindu_solar_year" | "hindu_lunisolar_month"
15885            | "maya_long_count_from_fixed" | "mayan_haab_from_fixed"
15886            | "mayan_tzolkin_from_fixed" | "badi_year_from_fixed"
15887            | "bahai_from_fixed" | "easter_gregorian_year"
15888            | "easter_orthodox_year" | "easter_julian_year"
15889            | "day_of_week_zeller" | "iso_day_number"
15890            | "weekday_name_short" | "leap_year_gregorian"
15891
15892            // ── batch 53: R / SciPy distributions and tests ───────────────
15893            | "dnorm" | "dt" | "df_dist" | "dchisq"
15894            | "glm" | "aov" | "shapiro_wilk" | "anderson_darling"
15895            | "kolmogorov_smirnov" | "spearmanr" | "kendalltau" | "pearsonr"
15896            | "mannwhitneyu" | "wilcoxon" | "kruskal_h"
15897
15898            // ── batch 54: APL/J/K array primitives ────────────────────────
15899            | "iota_n" | "reduce_axis" | "scan_axis" | "fold_axis"
15900            | "rotate_axis" | "transpose_axis" | "reshape_dim"
15901            | "encode_base" | "decode_base" | "nub_list" | "nub_count"
15902            | "membership_idx" | "deal_n_k" | "roll_n"
15903            | "permute_idx" | "invert_perm"
15904
15905            // ── batch 55: astronomy / astrometry ──────────────────────────
15906            | "julian_day" | "jd_to_calendar" | "tt_to_tdb"
15907            | "ra_dec_to_alt_az" | "alt_az_to_ra_dec"
15908            | "precession_iau2006" | "nutation_iau2000a"
15909            | "aberration_annual" | "proper_motion_apply"
15910            | "parallax_correction" | "sun_position_low" | "sun_distance_au"
15911            | "moon_position_low" | "moon_phase_age" | "lunation_index"
15912            | "eclipse_magnitude" | "saros_cycle" | "metonic_cycle"
15913            | "orbit_kepler3" | "orbital_period_au" | "orbit_eccentric_anomaly"
15914            | "escape_velocity_body" | "hill_sphere_radius" | "tisserand_param"
15915            | "tle_mean_motion" | "sgp4_propagate_step" | "airy_disk_radius"
15916            | "rayleigh_criterion" | "strehl_ratio" | "au_to_km"
15917
15918            // ── batch 56: sports analytics — ratings & sabermetric ────────
15919            | "elo_expected" | "elo_update" | "glicko_rating"
15920            | "trueskill_update" | "trueskill_match_quality"
15921            | "pythagorean_expectation" | "war_above_replacement"
15922            | "woba_weight" | "wrc_plus" | "ops_plus" | "era_plus"
15923            | "fip" | "xfip" | "siera" | "babip" | "wpa"
15924            | "win_probability" | "leverage_index" | "clutch_score"
15925            | "shooting_pct" | "save_pct" | "corsi_for" | "fenwick_for"
15926            | "goals_above_avg" | "tackle_efficiency" | "yards_per_attempt"
15927            | "qbr_metric" | "epa_per_play"
15928
15929            // ── batch 57: Excel/Sheets + bond/loan financial ──────────────
15930            | "vlookup" | "hlookup" | "xlookup" | "index_match"
15931            | "indirect" | "choose" | "offset"
15932            | "sumif" | "countif" | "averageif"
15933            | "sumifs" | "countifs" | "averageifs"
15934            | "sumproduct" | "rank_eq" | "rank_avg" | "percentrank"
15935            | "quartile_inc" | "quartile_exc"
15936            | "xnpv" | "ppmt" | "ipmt" | "rate"
15937            | "macauley_duration" | "convexity" | "yield_to_maturity"
15938            | "accrued_interest" | "clean_price" | "dirty_price"
15939            | "coupon_count" | "skill_score" | "reliability_diagram"
15940            | "taylor_diagram_score"
15941
15942            // ── batch 58: GIS — geohash, H3, S2, UTM, projections ─────────
15943            | "geohash_neighbors" | "h3_index" | "h3_geo_to_h3"
15944            | "h3_h3_to_geo" | "h3_k_ring" | "h3_neighbor" | "h3_resolution"
15945            | "s2_cell_id" | "s2_cell_at_lat_lng" | "s2_cell_neighbors"
15946            | "utm_from_lat_lng" | "utm_to_lat_lng"
15947            | "mgrs_encode" | "mgrs_decode"
15948            | "lat_lng_to_xy_mercator" | "lat_lng_to_xy_lambert"
15949            | "haversine_dist" | "vincenty_dist" | "andoyer_dist"
15950            | "rhumb_line_bearing"
15951            | "destination_point" | "tile_xyz_to_lat_lng" | "lat_lng_to_tile_xyz"
15952            | "polygon_winding_order" | "point_in_polygon_ray"
15953            | "point_in_polygon_winding" | "segment_intersection"
15954            | "segment_distance_point" | "convex_hull_chan"
15955
15956            // ── batch 59: robotics & control ──────────────────────────────
15957            | "pid_anti_windup" | "pid_ziegler_nichols"
15958            | "smith_predictor_step" | "lqr_gain_continuous"
15959            | "lqr_gain_discrete" | "lqg_step" | "h_infinity_norm"
15960            | "bode_gain_margin" | "bode_phase_margin"
15961            | "nyquist_encirclement" | "nichols_chart_step"
15962            | "servo_position_velocity" | "servo_torque_step"
15963            | "imu_madgwick_step" | "imu_mahony_step" | "quaternion_from_imu"
15964            | "denavit_hartenberg_h" | "forward_kinematics_dh"
15965            | "inverse_kinematics_2link" | "jacobian_2dof"
15966            | "manipulability_yoshikawa" | "singularity_check_2link"
15967            | "path_dubins_lsl" | "path_dubins_rsr" | "path_reeds_shepp"
15968            | "rrt_extend" | "rrt_star_rewire" | "prm_node_connect"
15969
15970            // ── batch 60: actuarial science ───────────────────────────────
15971            | "life_expectancy_e0" | "force_of_mortality" | "select_ultimate"
15972            | "annuity_due_an" | "annuity_immediate_an"
15973            | "term_life_a_n_t" | "whole_life_a"
15974            | "endowment_pure_e" | "endowment_combined_a"
15975            | "premium_net" | "level_premium"
15976            | "reserve_prospective" | "reserve_retrospective"
15977            | "gross_premium_load" | "experience_factor"
15978            | "mortality_table_q" | "select_period_step"
15979            | "multi_decrement_q" | "multi_state_pij"
15980            | "credibility_buhlmann" | "loss_severity_lognormal"
15981            | "loss_frequency_poisson" | "ruin_probability_lundberg"
15982            | "cramer_lundberg_step" | "bornhuetter_ferguson"
15983            | "chain_ladder_step" | "ibnr_estimate" | "run_off_triangle_step"
15984
15985            // ── batch 61: epidemiology / public health ────────────────────
15986            | "r_naught_basic" | "r_effective_t" | "doubling_time_growth"
15987            | "sirs_step" | "seirs_step" | "susceptible_to_infected"
15988            | "attack_rate" | "vaccination_coverage_required"
15989            | "cfr_case_fatality" | "ifr_infection_fatality"
15990            | "dalys_disability_weight" | "qaly_lifetime" | "ylll_pml"
15991            | "rt_serial_interval" | "generation_time_step"
15992            | "gini_inequality_health" | "standardized_mortality_smr"
15993            | "indirect_age_adjusted" | "direct_age_adjusted"
15994            | "odds_ratio_2x2" | "risk_ratio_2x2" | "number_needed_to_treat"
15995            | "attributable_fraction_pop" | "preventive_fraction"
15996            | "contact_tracing_eff" | "cluster_attack_rate"
15997            | "transmission_pair_index"
15998
15999            // ── batch 62: archive/encoding format primitives ──────────────
16000            | "tar_header_checksum" | "tar_pad_512" | "tar_member_record"
16001            | "zip_local_header" | "zip_central_dir" | "zip_eocd"
16002            | "gzip_member_step" | "gzip_crc32_init" | "gzip_isize"
16003            | "deflate_dynamic_huffman" | "deflate_static_block"
16004            | "lz4_block_step" | "lz4_match_offset"
16005            | "zstd_frame_header" | "brotli_huffman_table"
16006            | "brotli_meta_block" | "lzma_range_step"
16007            | "quoted_printable_encode" | "uuencode_step"
16008            | "modhex_encode" | "percent_encode_full"
16009            | "punycode_encode" | "idn_to_ascii" | "idn_to_unicode"
16010            | "msgpack_pack_int" | "msgpack_pack_str"
16011            | "cbor_encode_uint" | "cbor_encode_str"
16012
16013            // ── batch 63: chemistry & biochemistry ────────────────────────
16014            | "molecular_weight_compound" | "molarity_dilution"
16015            | "gas_constant_value" | "eyring_rate" | "van_t_hoff_kp"
16016            | "henderson_buffer" | "titration_ph_endpoint"
16017            | "isoelectric_point_protein" | "ka_to_pka" | "pkb_to_kb"
16018            | "amphoteric_check" | "oxidation_number"
16019            | "half_reaction_balance" | "redox_potential_cell"
16020            | "electrolysis_mass" | "spectrophotometer_beer_lambert"
16021            | "epsilon_extinction" | "transmittance_to_a"
16022            | "crystal_field_ligand" | "jahn_teller_check"
16023            | "vsepr_geometry" | "lewis_dot_count"
16024            | "formal_charge" | "resonance_count"
16025            | "ramachandran_phi_psi" | "rg_radius_of_gyration"
16026            | "spectroscopic_factor" | "avogadro_constant"
16027
16028            // ── batch 64: music theory ────────────────────────────────────
16029            | "cents_between_freqs" | "note_name_from_midi"
16030            | "interval_quality_size" | "scale_pitches_major"
16031            | "scale_pitches_minor" | "mode_pitches_dorian"
16032            | "mode_pitches_phrygian" | "mode_pitches_lydian"
16033            | "chord_root_inversion" | "chord_quality_classify"
16034            | "chord_voicing_close" | "key_signature_sharps"
16035            | "key_signature_flats" | "tempo_to_ms" | "beat_to_seconds"
16036            | "time_sig_subdivision" | "equal_tempered_freq"
16037            | "just_intonation_freq" | "pythagorean_freq"
16038            | "mean_tone_freq" | "werckmeister_iii" | "kirnberger_iii"
16039            | "dynamics_db_level" | "harmonics_partial"
16040
16041            // ── batch 65: geology, seismology, mineralogy ─────────────────
16042            | "moment_magnitude_mw" | "richter_local_ml"
16043            | "surface_wave_ms" | "body_wave_mb"
16044            | "gutenberg_richter_b" | "omori_aftershock"
16045            | "pga_attenuation" | "arias_intensity" | "shake_map_pga"
16046            | "liquefaction_potential_index" | "spt_n_correction"
16047            | "mineral_mohs_hardness" | "streak_color_index"
16048            | "specific_gravity_water" | "feldspar_classify"
16049            | "silicate_classify" | "igneous_qapf"
16050            | "metamorphic_grade" | "crustal_density_depth"
16051            | "pwave_velocity_depth" | "swave_velocity_depth"
16052            | "gradient_geothermal" | "heat_flow_radiogenic"
16053
16054            // ── batch 66: BLAS / LAPACK ───────────────────────────────────
16055            | "dgemm" | "sgemm" | "zgemm" | "cgemm"
16056            | "dgemv" | "sgemv" | "dtrsm" | "strsm"
16057            | "dgesv" | "dgetrf" | "dgeqrf" | "dgesvd"
16058            | "dsyevd" | "dpotrf" | "daxpy" | "ddot"
16059            | "dnrm2" | "dscal" | "dasum" | "idamax"
16060            | "dsyrk" | "dgerqf" | "dorgqr" | "dorglq"
16061            | "drot" | "drotg" | "dpbsv" | "dgbsv"
16062            | "dtbsv" | "dtrsv" | "ddrot" | "dgemm3m"
16063            | "dgels" | "dgelsd"
16064
16065            // ── batch 67: logic, proof, SAT/SMT, type theory ──────────────
16066            | "cnf_unit_propagate" | "cnf_pure_literal_elim"
16067            | "cnf_dpll_branch" | "dpll_clause_learning"
16068            | "two_watched_literals" | "walksat_step"
16069            | "resolution_step" | "subsumption_check"
16070            | "tableau_branch_close" | "sequent_left_intro"
16071            | "sequent_right_intro" | "nbe_normalize"
16072            | "church_numeral_n" | "encode_pair" | "encode_succ"
16073            | "simply_typed_check" | "hindley_milner_step"
16074            | "unification_robinson" | "bdd_apply" | "bdd_restrict"
16075            | "bdd_quantify" | "aig_simplify_step"
16076            | "smt_qf_lia_solve_step" | "smt_qf_uf_combine"
16077            | "model_checking_ctl" | "model_checking_ltl"
16078            | "bisimulation_step" | "coq_tactic_apply"
16079            | "coq_unify_term" | "refl_check" | "sym_check" | "trans_check"
16080
16081            // ── batch 68: compilers / parsing ─────────────────────────────
16082            | "nfa_to_dfa" | "subset_construction"
16083            | "dfa_minimize_hopcroft" | "regex_to_nfa_thompson"
16084            | "glushkov_construction" | "brzozowski_derivative"
16085            | "ll1_first_set" | "ll1_follow_set" | "ll1_predict_table"
16086            | "lr0_items_step" | "lalr_lookahead_compute"
16087            | "lr1_canonical_collection"
16088            | "earley_scan" | "earley_predict" | "earley_complete"
16089            | "packrat_parse_step" | "ascent_parser_step"
16090            | "pratt_parse_step" | "shunting_yard_step"
16091            | "regex_compile_thompson" | "regex_match_dfa"
16092            | "lex_keyword_classify"
16093            | "peg_seq" | "peg_choice" | "peg_repeat" | "peg_lookahead"
16094            | "dfa_simulate_step" | "bytecode_disasm_step"
16095            | "ssa_phi_insert" | "dom_tree_idom" | "dominance_frontier"
16096
16097            // ── batch 69: computational linguistics ───────────────────────
16098            | "porter_stem_step" | "snowball_stem_english"
16099            | "snowball_stem_french" | "lemmatize_wordnet"
16100            | "lemmatize_lemmy" | "stem_lancaster"
16101            | "soundex_phonetic" | "metaphone_phonetic"
16102            | "caverphone_2" | "nysiis_phonetic"
16103            | "match_rating_codex" | "daitch_mokotoff"
16104            | "viterbi_pos_tag" | "forward_backward_pos"
16105            | "crf_log_likelihood" | "bigram_perplexity"
16106            | "trigram_perplexity" | "ner_bilou_decode"
16107            | "constituency_cyk" | "dependency_parse_eisner"
16108            | "transition_arc_eager" | "transition_arc_standard"
16109            | "word_alignment_ibm1" | "word_alignment_ibm2"
16110            | "lexicalized_parse" | "coreference_singleton"
16111            | "anaphora_distance" | "head_finding_collins"
16112            | "tree_kernel_collins"
16113
16114            // ── batch 70: Postgres SQL strings, JSON, aggregates ─────────
16115            | "btrim" | "translate" | "ascii"
16116            | "regexp_split" | "regexp_matches" | "regexp_replace"
16117            | "json_build_object" | "jsonb_set"
16118            | "json_array_length" | "json_extract_path"
16119            | "json_strip_nulls" | "jsonb_pretty"
16120            | "jsonb_path_query" | "json_each"
16121            | "jsonb_array_length" | "jsonb_object_keys"
16122            | "jsonb_typeof" | "array_to_jsonb"
16123            | "ts_match" | "ts_rank" | "ts_headline"
16124            | "substring_similarity" | "levenshtein_dist"
16125            | "word_similarity" | "strict_word_similarity"
16126            | "hstore_to_array" | "array_to_hstore"
16127            | "string_agg" | "array_agg"
16128            | "corr_agg" | "covar_pop" | "covar_samp"
16129            | "regr_slope" | "regr_intercept" | "regr_r2"
16130            | "percentile_cont" | "percentile_disc" | "mode_agg"
16131            | "array_to_string" | "array_position" | "array_positions"
16132            | "array_remove" | "array_replace"
16133            | "xmlforest" | "xmlagg"
16134
16135            // ── batch 71: Redis-flavour primitives ────────────────────────
16136            | "zadd" | "zrem" | "zrangebyscore"
16137            | "zrank" | "zrevrank" | "zincrby"
16138            | "zcard" | "zcount" | "zlexcount"
16139            | "lpush" | "rpush" | "lrange" | "lrem"
16140            | "hset" | "hget" | "hgetall" | "hlen"
16141            | "hkeys" | "hvals" | "hmset" | "hincrby"
16142            | "sadd" | "srem" | "smembers"
16143            | "sinter" | "sunion" | "sdiff"
16144            | "scard" | "sismember" | "spop"
16145            | "setex" | "setnx" | "expire"
16146            | "ttl" | "pttl" | "persist"
16147            | "incr" | "decr" | "incrby" | "decrby"
16148            | "getset" | "mset" | "mget" | "renamenx"
16149            | "dbsize" | "type_redis" | "exists_key"
16150            | "strlen" | "getrange" | "setrange" | "append_redis"
16151            | "bitcount" | "bitop" | "bitpos"
16152            | "pfadd" | "pfcount"
16153            | "geoadd" | "geodist" | "geohash"
16154            | "xadd" | "xlen" | "xrange"
16155            | "object_encoding" | "debug_object" | "cluster_slots"
16156
16157            // ── batch 72: NumPy + scipy.special ──────────────────────────
16158            | "argpartition" | "bincount" | "nonzero_count"
16159            | "flatnonzero" | "searchsorted" | "digitize"
16160            | "histogram_bin_edges" | "unique_count"
16161            | "polyfit_rmse"
16162            | "ellipk" | "ellipe"
16163            | "hyp1f1" | "hyp2f1" | "mathieu_b"
16164            | "spherical_jn" | "spherical_yn"
16165            | "jv" | "yn" | "iv" | "kv"
16166            | "airyai" | "airybi"
16167            | "polygamma" | "trigamma" | "loggamma"
16168            | "factorial2" | "factorialk"
16169            | "owens_t" | "marcum_q" | "voigt_profile"
16170            | "chebyt" | "chebyu" | "sph_harm"
16171            | "wofz" | "erfcx" | "erfi" | "dawsn"
16172            | "interp1d"
16173            | "convolve_full" | "convolve_valid" | "correlate_full"
16174            | "kron_product"
16175            | "simpson_rule" | "romberg_quad" | "fixed_quad"
16176            | "ode45_step" | "ode_lsoda" | "solve_ivp_step"
16177            | "root_brentq" | "root_newton" | "root_secant"
16178            | "fmin_powell" | "fmin_cobyla"
16179
16180            // ── batch 73: economics + game theory ─────────────────────────
16181            | "cobb_douglas" | "ces_production"
16182            | "leontief_input" | "leontief_output"
16183            | "slutsky_decompose"
16184            | "marshallian_demand" | "hicksian_demand"
16185            | "expenditure_function" | "indirect_utility"
16186            | "gale_shapley_step" | "deferred_acceptance"
16187            | "top_trading_cycle" | "vcg_payment" | "myerson_optimal"
16188            | "gini_market" | "hhi_concentration"
16189            | "cournot_eq" | "stackelberg_eq" | "bertrand_eq"
16190            | "monopoly_lerner"
16191            | "consumer_surplus" | "producer_surplus"
16192            | "deadweight_loss" | "tax_incidence"
16193            | "pareto_efficiency" | "edgeworth_box_alloc"
16194            | "social_welfare_utilitarian"
16195            | "social_welfare_rawls" | "social_welfare_nash"
16196            | "arrow_independence"
16197            | "vickrey_auction" | "first_price_seal"
16198            | "english_auction" | "dutch_auction"
16199            | "core_coalition" | "stable_matching_count"
16200            | "gale_optimal" | "pareto_dominance"
16201            | "lerner_index"
16202            | "price_elasticity" | "supply_elasticity"
16203            | "income_elasticity" | "engel_curve" | "cross_elasticity"
16204            | "diff_in_diff" | "did_estimator" | "rdd_estimate"
16205            // ── batch 74: SciPy.signal — DSP filters, windows, transforms ──
16206            | "hann_w" | "hamming_w" | "blackman_w" | "barthann_w"
16207            | "nuttall_w" | "flattop_w" | "parzen_window" | "tukey_w"
16208            | "taylor_window" | "dpss_window" | "kaiserord_step"
16209            | "butter_lp_re" | "butter_hp_mag"
16210            | "cheby1_lp" | "cheby2_lp" | "ellip_lp" | "bessel_lp"
16211            | "notch_filter"
16212            | "sosfilt_step" | "lfilter_zi_init" | "filtfilt_pad"
16213            | "freqz_eval" | "freqs_eval" | "group_delay_eval"
16214            | "impulse_response_n"
16215            | "tf2zpk_step" | "zpk2tf_step" | "tf2sos_step"
16216            | "zpk2sos_step" | "sos2tf_step"
16217            | "bilinear_xform" | "bilinear_zpk_xform"
16218            | "firwin_lowpass" | "firwin_highpass"
16219            | "firwin_bandpass" | "firwin_bandstop"
16220            | "firwin2_freq" | "remez_design"
16221            | "stft_step" | "istft_step"
16222            | "cwt_morlet" | "ricker_wavelet" | "mexican_hat_wavelet"
16223            | "coherence_xy" | "csd_xy" | "welch_psd_avg"
16224            | "periodogram_basic" | "lombscargle_freq"
16225            | "hilbert_signal" | "envelope_amplitude"
16226            | "deconvolve_step" | "fftconvolve_step" | "oaconvolve_step"
16227            | "upfirdn_step" | "resample_poly_step" | "decimate_step"
16228            | "savgol_coef" | "detrend_linear"
16229            | "wiener_filter" | "medfilt_1d" | "peak_widths_at"
16230            // ── batch 75: NetworkX graph algorithms ───────────────────────
16231            | "dijkstra_relax" | "bellman_ford_relax"
16232            | "floyd_warshall_step" | "johnson_reweight"
16233            | "astar_search" | "bidirectional_dijkstra"
16234            | "yen_k_shortest" | "ida_star"
16235            | "bfs_count" | "dfs_postorder_done" | "topo_kahn_step"
16236            | "tarjan_scc_step" | "kosaraju_step"
16237            | "kruskal_step" | "prim_step" | "boruvka_step"
16238            | "reverse_delete_step"
16239            | "ford_fulkerson_step" | "edmonds_karp_bfs"
16240            | "dinic_step" | "push_relabel_relabel"
16241            | "stoer_wagner_step" | "karger_step"
16242            | "pagerank_iter" | "hits_authority" | "hits_hub"
16243            | "personalized_pagerank"
16244            | "centrality_degree" | "centrality_closeness"
16245            | "centrality_betweenness" | "centrality_eigenvector"
16246            | "centrality_katz" | "harmonic_centrality" | "load_centrality"
16247            | "clustering_coefficient" | "triangles_count" | "transitivity"
16248            | "modularity_score" | "louvain_gain"
16249            | "label_propagation" | "girvan_newman"
16250            | "articulation_point" | "bridge_edge"
16251            | "edge_connectivity" | "vertex_connectivity"
16252            | "biconnected_components"
16253            | "gx_diameter" | "gx_radius" | "gx_eccentricity"
16254            | "warshall_step"
16255            | "tsp_held_karp" | "tsp_nn_step" | "tsp_christofides"
16256            | "graph_coloring_greedy" | "welsh_powell"
16257            | "vf2_consistent" | "subgraph_isomorphism"
16258            | "hungarian_step" | "hopcroft_karp_step"
16259            | "bron_kerbosch"
16260            | "min_vertex_cover" | "max_independent_set"
16261            | "dominating_set_greedy" | "hamiltonian_path"
16262            | "min_steiner_tree" | "k_shortest_spanning"
16263            | "random_walk_hitting" | "simrank"
16264            // ── batch 76: Pandas DataFrame ops ────────────────────────────
16265            | "df_groupby" | "df_aggregate" | "df_apply"
16266            | "df_transform" | "df_pivot" | "df_pivot_table"
16267            | "df_melt" | "df_stack" | "df_unstack"
16268            | "df_explode" | "df_get_dummies" | "df_crosstab"
16269            | "df_merge" | "df_join" | "df_concat"
16270            | "df_resample" | "df_rolling" | "df_expanding"
16271            | "df_ewm" | "df_shift" | "df_diff"
16272            | "df_pct_change" | "df_corr" | "df_cov"
16273            | "df_corrwith" | "df_describe" | "df_kurtosis"
16274            | "df_skew" | "df_sem" | "df_mad"
16275            | "df_dropna" | "df_fillna" | "df_interpolate"
16276            | "df_replace" | "df_isnull" | "df_notnull"
16277            | "df_sort_values" | "df_rank" | "df_quantile"
16278            | "df_value_counts" | "df_sample" | "df_nlargest"
16279            | "df_nsmallest" | "df_idxmax" | "df_idxmin"
16280            | "df_clip" | "df_round" | "df_to_datetime"
16281            | "df_to_timedelta" | "df_to_numeric" | "df_eval"
16282            | "df_query" | "df_filter" | "df_drop_duplicates"
16283            | "df_duplicated" | "df_set_index" | "df_reset_index"
16284            // ── batch 77: PIL/OpenCV image processing ─────────────────────
16285            | "image_resize" | "image_grayscale" | "image_threshold"
16286            | "image_blur_gaussian" | "image_blur_box" | "image_sharpen"
16287            | "image_edge_canny" | "image_edge_sobel" | "image_edge_laplacian"
16288            | "image_dilate" | "image_erode" | "image_morphology_open"
16289            | "image_morphology_close" | "image_histogram" | "image_equalize"
16290            | "image_clahe" | "image_contrast" | "image_brightness"
16291            | "image_gamma" | "image_invert" | "image_sepia"
16292            | "image_posterize" | "image_solarize" | "convolve_2d"
16293            | "filter_median" | "filter_bilateral" | "filter_nlmeans"
16294            | "gabor_filter" | "hog_features" | "harris_corners"
16295            | "shi_tomasi_corners" | "sift_keypoints" | "orb_keypoints"
16296            | "surf_keypoints" | "template_match" | "face_detect_haar"
16297            | "watershed_segment" | "slic_superpixels" | "felzenszwalb_segment"
16298            | "graph_cut_segment" | "hough_lines" | "hough_circles"
16299            | "ransac_homography" | "optical_flow_lk" | "optical_flow_farneback"
16300            | "corner_subpix" | "image_rotate" | "image_flip_h"
16301            | "image_flip_v" | "image_emboss" | "image_motion_blur"
16302            // ── batch 78: statsmodels ─
16303            | "arima_fit" | "arima_forecast" | "arma_order_select"
16304            | "sarimax_fit" | "garch_fit" | "ewma_smooth"
16305            | "holt_winters_additive" | "holt_winters_multiplicative" | "kalman_filter_step"
16306            | "kalman_smoother_step" | "var_fit" | "vecm_fit"
16307            | "johansen_test" | "phillips_perron" | "adfuller"
16308            | "kpss_test" | "breusch_godfrey" | "ljung_box_q"
16309            | "durbin_watson_d" | "granger_causality" | "cointegration_eg"
16310            | "seasonal_decompose" | "stl_decompose" | "acf_basis"
16311            | "pacf_basis" | "moving_average_filter" | "exp_smooth_simple"
16312            | "exp_smooth_double" | "markov_switching_ar" | "markov_switching_mr"
16313            | "arch_lm" | "state_space_kalman" | "ucm_unobserved_components"
16314            | "spectral_density_estimate" | "bayesian_step" | "pivoted_cholesky_var"
16315            // ── batch 79: sklearn ─
16316            | "sk_logistic_predict" | "sk_logistic_fit" | "sk_random_forest_fit"
16317            | "sk_gbt_fit" | "sk_xgb_fit" | "sk_lightgbm_fit"
16318            | "sk_svm_fit" | "sk_kmeans_fit" | "sk_dbscan_fit"
16319            | "sk_agglomerative_fit" | "sk_pca_fit" | "sk_tsne_fit"
16320            | "sk_umap_fit" | "sk_isolation_forest_fit" | "sk_lof_fit"
16321            | "sk_kfold_split" | "sk_stratified_kfold" | "sk_cross_val_score"
16322            | "sk_grid_search" | "sk_random_search" | "sk_bayes_search"
16323            | "sk_pipeline_fit" | "sk_standard_scaler" | "sk_min_max_scaler"
16324            | "sk_robust_scaler" | "sk_quantile_transform" | "sk_power_transform"
16325            | "sk_one_hot" | "sk_ordinal_encode" | "sk_label_encode"
16326            | "sk_tfidf" | "sk_count_vectorize" | "sk_silhouette"
16327            | "sk_calinski_harabasz" | "sk_davies_bouldin" | "sk_adjusted_rand"
16328            | "sk_mutual_info" | "sk_lda_topic" | "sk_nmf_topic"
16329            | "sk_word2vec_train" | "sk_doc2vec_train" | "sk_naive_bayes_predict"
16330            | "sk_knn_predict" | "sk_decision_tree_split"
16331            // ── batch 80: quantum ─
16332            | "qubit_x" | "qubit_y" | "qubit_z"
16333            | "qubit_h" | "qubit_s" | "qubit_t"
16334            | "qubit_rx" | "qubit_ry" | "qubit_rz"
16335            | "qubit_u3" | "qubit_u2" | "qubit_u1"
16336            | "qubit_phase" | "qubit_cnot" | "qubit_cz"
16337            | "qubit_swap" | "qubit_ccx" | "qubit_measure"
16338            | "qubit_reset" | "bell_state" | "ghz_state"
16339            | "w_state" | "qft" | "inverse_qft"
16340            | "grover_iter" | "shor_period" | "vqe_step"
16341            | "qaoa_step" | "qpe_iteration" | "pauli_string_expect"
16342            | "circuit_depth" | "circuit_width" | "gate_decompose"
16343            | "ancilla_alloc" | "bloch_sphere_x" | "bloch_sphere_z"
16344            | "density_matrix_purity_q" | "entanglement_entropy" | "quantum_teleportation"
16345            | "superdense_coding" | "noise_model_depolarize"
16346            // ── batch 81: b81-misc-utility ─
16347            | "mirr_excel" | "accrint" | "cumipmt"
16348            | "cumprinc" | "dollarde" | "dollarfr"
16349            | "received" | "yieldmat" | "yielddisc"
16350            | "duration_macaulay" | "mduration" | "odddyield"
16351            | "disc_excel" | "effect" | "nominal"
16352            | "intrate" | "price_disc" | "cityhash64"
16353            | "farmhash_64" | "metro_hash_64" | "spookyhash_128"
16354            | "t1ha" | "highway_hash" | "fnv0_32"
16355            | "lose_lose"
16356            | "oat_hash" | "lz4_encode_block" | "snappy_encode"
16357            | "zstd_encode_step" | "brotli_encode_meta" | "lzma_encode_step"
16358            | "bz2_encode_step" | "lzo_encode_step" | "deflate_encode_huffman"
16359            | "lzw_encode" | "gzip_encode_step" | "uri_template_expand"
16360            | "uri_resolve" | "uri_normalize" | "percent_decode_url"
16361            | "url_encode_form" | "url_decode_form" | "punycode_decode_step"
16362            | "idn_normalize" | "url_origin" | "etag_validate"
16363            | "cache_control_parse" | "vary_match" | "content_negotiate"
16364            | "accept_lang_pick" | "range_header_parse" | "if_match_check"
16365            | "if_none_match_check" | "digest_auth_quote" | "www_auth_parse"
16366            // ── batch 82: b82-misc-utility ─
16367            | "iso8601_duration_parse" | "iso8601_duration_to_seconds" | "rrule_next_occurrence"
16368            | "cron_next_fire" | "date_round_iso" | "week_number_iso"
16369            | "fiscal_year_us" | "age_at_date" | "easter_western"
16370            | "easter_orthodox_year_2" | "chinese_new_year" | "solstice_winter"
16371            | "equinox_spring" | "rgb_to_oklab" | "oklab_to_rgb"
16372            | "rgb_to_cmyk" | "cmyk_to_rgb" | "rgb_to_xyz"
16373            | "xyz_to_rgb" | "rgb_to_yuv" | "yuv_to_rgb"
16374            | "luminance_relative" | "contrast_ratio" | "wcag_pass"
16375            | "color_temperature_kelvin" | "delta_e76" | "delta_e94"
16376            | "delta_e2000" | "color_blend_alpha" | "isbn10_check"
16377            | "isbn13_check" | "ean13_check" | "upc_check"
16378            | "eth_addr_check" | "btc_addr_check" | "ssn_check"
16379            | "vin_check" | "imei_check" | "iban_check"
16380            | "cusip_check" | "kde_silverman_bw" | "kde_scott_bw"
16381            | "kde_bandwidth_lscv" | "kde_epanechnikov" | "kde_gaussian_2d"
16382            | "kde_uniform" | "kde_triangular" | "kde_biweight"
16383            | "kde_triweight" | "kde_cosine" | "kde_logistic_kernel"
16384            // ── number theory (extended) ──────────────────────────────────
16385            | "mod_exp" | "modexp" | "powmod"
16386            | "mod_inv" | "modinv" | "chinese_remainder" | "crt"
16387            | "miller_rabin" | "millerrabin" | "is_probable_prime"
16388            // ── combinatorics (extended) ──────────────────────────────────
16389            | "derangements" | "stirling2" | "stirling_second"
16390            | "bernoulli_number" | "bernoulli" | "harmonic_number" | "harmonic"
16391            // ── physics (new) ─────────────────────────────────────────────
16392            | "drag_force" | "fdrag" | "ideal_gas" | "pv_nrt"
16393            // ── financial greeks & risk ───────────────────────────────────
16394            | "bs_delta" | "bsdelta" | "option_delta"
16395            | "bs_gamma" | "bsgamma" | "option_gamma"
16396            | "bs_vega" | "bsvega" | "option_vega"
16397            | "bs_theta" | "bstheta" | "option_theta"
16398            | "bs_rho" | "bsrho" | "option_rho"
16399            | "bond_duration" | "mac_duration"
16400            // ── DSP extensions ────────────────────────────────────────────
16401            | "dct" | "idct" | "goertzel" | "chirp" | "chirp_signal"
16402            // ── encoding extensions ───────────────────────────────────────
16403            | "base85_encode" | "b85e" | "ascii85_encode" | "a85e"
16404            | "base85_decode" | "b85d" | "ascii85_decode" | "a85d"
16405            // ── R base: distributions ─────────────────────────────────────
16406            | "pnorm" | "qnorm" | "pbinom" | "dbinom" | "ppois"
16407            | "punif" | "pexp" | "pweibull" | "plnorm" | "pcauchy"
16408            // ── R base: matrix ops ────────────────────────────────────────
16409            | "rbind" | "cbind"
16410            | "row_sums" | "rowSums" | "col_sums" | "colSums"
16411            | "row_means" | "rowMeans" | "col_means" | "colMeans"
16412            | "outer" | "crossprod" | "tcrossprod"
16413            | "nrow" | "ncol" | "prop_table" | "proptable"
16414            // ── R base: vector ops ────────────────────────────────────────
16415            | "cummax" | "cummin" | "scale_vec" | "scale"
16416            | "which_fn" | "tabulate"
16417            | "duplicated" | "duped" | "rev_vec"
16418            | "seq_fn" | "rep_fn" | "rep"
16419            | "cut_bins" | "cut" | "find_interval" | "findInterval"
16420            | "ecdf_fn" | "ecdf" | "density_est" | "density"
16421            | "embed_ts" | "embed"
16422            // ── R base: stats tests ───────────────────────────────────────
16423            | "shapiro_test" | "shapiro" | "ks_test" | "ks"
16424            | "wilcox_test" | "wilcox" | "mann_whitney"
16425            | "prop_test" | "proptest" | "binom_test" | "binomtest"
16426            // ── R base: apply / functional ────────────────────────────────
16427            | "sapply" | "tapply" | "do_call" | "docall"
16428            // ── R base: ML / clustering ───────────────────────────────────
16429            | "kmeans" | "prcomp" | "pca"
16430            // ── R base: random generators ─────────────────────────────────
16431            | "rnorm" | "runif" | "rexp" | "rbinom" | "rpois" | "rgeom"
16432            | "rgamma" | "rbeta" | "rchisq" | "rt" | "rf"
16433            | "rweibull" | "rlnorm" | "rcauchy"
16434            // ── R base: quantile functions ────────────────────────────────
16435            | "qunif" | "qexp" | "qweibull" | "qlnorm" | "qcauchy"
16436            // ── R base: additional CDFs ───────────────────────────────────
16437            | "pgamma" | "pbeta" | "pchisq" | "pt_cdf" | "pt" | "pf_cdf" | "pf"
16438            // ── R base: additional PMFs ───────────────────────────────────
16439            | "dgeom" | "dunif" | "dnbinom" | "dhyper"
16440            // ── R base: smoothing / interpolation ─────────────────────────
16441            | "lowess" | "loess" | "approx_fn" | "approx"
16442            // ── R base: linear models ─────────────────────────────────────
16443            | "lm_fit" | "lm"
16444            // ── R base: remaining quantiles ───────────────────────────────
16445            | "qgamma" | "qbeta" | "qchisq" | "qt_fn" | "qt" | "qf_fn" | "qf"
16446            | "qbinom" | "qpois"
16447            // ── R base: time series ───────────────────────────────────────
16448            | "acf_fn" | "acf" | "pacf_fn" | "pacf"
16449            | "diff_lag" | "diff_ts" | "ts_filter" | "filter_ts"
16450            // ── R base: regression diagnostics ────────────────────────────
16451            | "predict_lm" | "predict" | "confint_lm" | "confint"
16452            // ── R base: multivariate stats ────────────────────────────────
16453            | "cor_matrix" | "cor_mat" | "cov_matrix" | "cov_mat"
16454            | "mahalanobis" | "mahal" | "dist_matrix" | "dist_mat"
16455            | "hclust" | "cutree" | "weighted_var" | "wvar" | "cov2cor"
16456            // ── SVG plotting ──────────────────────────────────────────────
16457            | "scatter_svg" | "scatter_plot" | "line_svg" | "line_plot"
16458            | "plot_svg" | "hist_svg" | "histogram_svg"
16459            | "boxplot_svg" | "box_plot" | "bar_svg" | "barchart_svg"
16460            | "pie_svg" | "pie_chart" | "heatmap_svg" | "heatmap"
16461            | "donut_svg" | "donut" | "area_svg" | "area_chart"
16462            | "hbar_svg" | "hbar" | "radar_svg" | "radar" | "spider"
16463            | "candlestick_svg" | "candlestick" | "ohlc"
16464            | "violin_svg" | "violin" | "cor_heatmap" | "cor_matrix_svg"
16465            | "stacked_bar_svg" | "stacked_bar"
16466            | "wordcloud_svg" | "wordcloud" | "wcloud"
16467            | "treemap_svg" | "treemap"
16468            | "pvw"
16469            // ── Cyberpunk terminal art ────────────────────────────────
16470            | "cyber_city" | "cyber_grid" | "cyber_rain" | "matrix_rain"
16471            | "cyber_glitch" | "glitch_text" | "cyber_banner" | "neon_banner"
16472            | "cyber_circuit" | "cyber_skull" | "cyber_eye"
16473            // ── AI primitives (docs/AI_PRIMITIVES.md) ─────────────────
16474            | "ai" | "ai_agent" | "prompt" | "stream_prompt" | "stream_prompt_cb"
16475            | "tokens_of"
16476            | "ai_estimate" | "ai_cost" | "ai_history" | "ai_history_clear"
16477            | "ai_cache_clear" | "ai_cache_size"
16478            | "ai_mock_install" | "ai_mock_clear"
16479            | "ai_config_get" | "ai_config_set" | "ai_routing_get" | "ai_routing_set"
16480            | "ai_register_tool" | "ai_unregister_tool" | "ai_clear_tools" | "ai_tools_list"
16481            | "ai_filter" | "ai_map" | "ai_classify" | "ai_match" | "ai_sort" | "ai_dedupe"
16482            | "ai_extract" | "ai_summarize" | "ai_translate" | "ai_template"
16483            | "ai_session_new" | "ai_session_send" | "ai_session_history"
16484            | "ai_session_close" | "ai_session_reset"
16485            | "ai_session_export" | "ai_session_import"
16486            | "ai_memory_save" | "ai_memory_recall" | "ai_memory_forget"
16487            | "ai_memory_count" | "ai_memory_clear"
16488            | "ai_vision" | "ai_pdf" | "ai_grounded" | "ai_citations"
16489            | "ai_transcribe" | "ai_speak" | "ai_image" | "ai_image_edit" | "ai_image_variation"
16490            | "ai_models" | "ai_describe" | "ai_pricing" | "ai_dashboard"
16491            | "ai_moderate" | "ai_chunk" | "ai_warm" | "ai_compare"
16492            | "ai_last_thinking" | "ai_budget" | "ai_batch" | "ai_pmap"
16493            | "ai_file_upload" | "ai_file_list" | "ai_file_get" | "ai_file_delete"
16494            | "ai_file_anthropic_upload" | "ai_file_anthropic_list" | "ai_file_anthropic_delete"
16495            | "vec_cosine" | "vec_search" | "vec_topk"
16496            // ── AI tool specs ────────────────────────────────────────
16497            | "web_search_tool" | "fetch_url_tool" | "read_file_tool" | "run_code_tool"
16498            // ── MCP (Model Context Protocol) ─────────────────────────
16499            | "mcp_connect" | "mcp_close" | "mcp_tools" | "mcp_call"
16500            | "mcp_resource" | "mcp_resources" | "mcp_prompt" | "mcp_prompts"
16501            | "mcp_attach_to_ai" | "mcp_detach_from_ai" | "mcp_attached"
16502            | "mcp_server_start" | "mcp_serve_registered_tools"
16503            // ── PTY / expect (docs/expect-feature-idea.md) ────────────
16504            | "pty_spawn" | "pty_send" | "pty_read" | "pty_expect" | "pty_expect_table"
16505            | "pty_buffer" | "pty_alive" | "pty_eof" | "pty_close" | "pty_interact"
16506            | "pty_strip_ansi" | "pty_after_eof" | "pty_pending_events"
16507            // ── Stress / telemetry extensions ─────────────────────────
16508            | "stress_fp" | "stress_int" | "stress_cache" | "stress_branch"
16509            | "stress_sort" | "stress_alloc" | "stress_mmap" | "stress_disk"
16510            | "stress_iops" | "stress_net" | "stress_http" | "stress_dns"
16511            | "stress_fork" | "stress_thread" | "stress_aes" | "stress_compress"
16512            | "stress_regex" | "stress_json" | "stress_burst" | "stress_ramp"
16513            | "stress_oscillate" | "stress_all" | "stress_temp" | "stress_thermal_zones"
16514            | "stress_freq" | "stress_throttled" | "stress_load" | "stress_meminfo"
16515            | "stress_cores" | "stress_arm_kill_switch" | "stress_killed"
16516            | "stress_disarm_kill_switch"
16517            | "stress_metrics_record" | "stress_metrics_clear" | "stress_metrics_count"
16518            | "stress_metrics_export" | "stress_metrics_prometheus"
16519            | "stress_metrics_json" | "stress_metrics_csv" | "stress_metrics_watch"
16520            // ── Compliance / secrets ─────────────────────────────────
16521            | "audit_log" | "audit_log_path"
16522            | "secrets_encrypt" | "secrets_decrypt" | "secrets_random_key" | "secrets_kdf"
16523            // ── Web framework (docs/WEB_FRAMEWORK.md) ─────────────────
16524            | "web_route" | "web_resources" | "web_root" | "web_routes_table"
16525            | "web_application_config" | "web_boot_application"
16526            | "web_render" | "web_render_partial" | "web_redirect"
16527            | "web_json" | "web_text" | "web_csv" | "web_markdown"
16528            | "web_params" | "web_request" | "web_set_header" | "web_status"
16529            | "web_before_action" | "web_after_action"
16530            | "web_session" | "web_session_set" | "web_session_get" | "web_session_clear"
16531            | "web_signed" | "web_unsigned"
16532            | "web_cookies" | "web_set_cookie"
16533            | "web_flash" | "web_flash_set" | "web_flash_get"
16534            | "web_validate" | "web_permit"
16535            | "web_password_hash" | "web_password_verify"
16536            | "web_token_for" | "web_token_consume" | "web_csrf_meta_tag"
16537            | "web_security_headers" | "web_can"
16538            | "web_h" | "web_truncate" | "web_pluralize" | "web_time_ago_in_words"
16539            | "web_image_tag" | "web_link_to" | "web_button_to"
16540            | "web_form_with" | "web_form_close"
16541            | "web_text_field" | "web_text_area" | "web_check_box"
16542            | "web_stylesheet_link_tag" | "web_javascript_link_tag"
16543            | "web_yield_content" | "web_content_for"
16544            | "web_etag" | "web_cache_get" | "web_cache_set"
16545            | "web_cache_delete" | "web_cache_clear"
16546            | "web_db_connect" | "web_db_execute" | "web_db_query"
16547            | "web_db_begin" | "web_db_commit" | "web_db_rollback"
16548            | "web_create_table" | "web_drop_table"
16549            | "web_add_column" | "web_remove_column"
16550            | "web_migrate" | "web_rollback"
16551            | "web_model_all" | "web_model_find" | "web_model_first" | "web_model_last"
16552            | "web_model_where" | "web_model_create" | "web_model_update"
16553            | "web_model_destroy" | "web_model_count" | "web_model_increment"
16554            | "web_model_paginate" | "web_model_search" | "web_model_soft_destroy"
16555            | "web_model_with"
16556            | "web_jobs_init" | "web_job_enqueue" | "web_job_dequeue"
16557            | "web_job_complete" | "web_job_fail"
16558            | "web_jobs_list" | "web_jobs_stats" | "web_job_purge"
16559            | "web_jsonapi_resource" | "web_jsonapi_collection" | "web_jsonapi_error"
16560            | "web_bearer_token" | "web_jwt_encode" | "web_jwt_decode"
16561            | "web_otp_secret" | "web_otp_generate" | "web_otp_verify"
16562            | "web_uuid" | "web_now" | "web_log" | "web_rate_limit"
16563            | "web_t" | "web_load_locale" | "web_openapi"
16564            | "web_faker_int" | "web_faker_email" | "web_faker_name"
16565            | "web_faker_sentence" | "web_faker_paragraph"
16566            => Some(name),
16567            _ => None,
16568        }
16569    }
16570
16571    /// Reserved hash names that cannot be shadowed by user declarations.
16572    /// These are stryke's reflection hashes populated from builtins metadata.
16573    fn is_reserved_hash_name(name: &str) -> bool {
16574        matches!(
16575            name,
16576            "b" | "pc"
16577                | "e"
16578                | "a"
16579                | "d"
16580                | "c"
16581                | "p"
16582                | "all"
16583                | "stryke::builtins"
16584                | "stryke::perl_compats"
16585                | "stryke::extensions"
16586                | "stryke::aliases"
16587                | "stryke::descriptions"
16588                | "stryke::categories"
16589                | "stryke::primaries"
16590                | "stryke::all"
16591        )
16592    }
16593
16594    /// Check if a UDF name shadows a stryke builtin and error if so.
16595    /// Called only in non-compat mode — compat mode allows shadowing for Perl 5 parity.
16596    /// Reserved words that cannot be used as function names because they are
16597    /// lexer-level operators or language keywords that would be mis-tokenized.
16598    const RESERVED_FUNCTION_NAMES: &'static [&'static str] = &[
16599        "y",
16600        "tr",
16601        "s",
16602        "m",
16603        "q",
16604        "qq",
16605        "qw",
16606        "qx",
16607        "qr",
16608        "if",
16609        "unless",
16610        "while",
16611        "until",
16612        "for",
16613        "foreach",
16614        "given",
16615        "when",
16616        "else",
16617        "elsif",
16618        "do",
16619        "eval",
16620        "return",
16621        "last",
16622        "next",
16623        "redo",
16624        "goto",
16625        "my",
16626        "our",
16627        "local",
16628        "state",
16629        "sub",
16630        "fn",
16631        "class",
16632        "struct",
16633        "enum",
16634        "trait",
16635        "use",
16636        "no",
16637        "require",
16638        "package",
16639        "BEGIN",
16640        "END",
16641        "CHECK",
16642        "INIT",
16643        "UNITCHECK",
16644        "and",
16645        "or",
16646        "not",
16647        "x",
16648        "eq",
16649        "ne",
16650        "lt",
16651        "gt",
16652        "le",
16653        "ge",
16654        "cmp",
16655    ];
16656
16657    fn check_udf_shadows_builtin(&self, name: &str, line: usize) -> PerlResult<()> {
16658        // Only check bare names, not namespaced ones (Foo::y is allowed)
16659        if !name.contains("::") {
16660            if Self::RESERVED_FUNCTION_NAMES.contains(&name) {
16661                return Err(self.syntax_err(
16662                    format!("`{name}` is a reserved word and cannot be used as a function name"),
16663                    line,
16664                ));
16665            }
16666            if Self::is_known_bareword(name)
16667                || Self::is_try_builtin_name(name)
16668                || crate::list_builtins::is_list_builtin_name(name)
16669            {
16670                return Err(self.syntax_err(
16671                    format!(
16672"`{name}` is a stryke builtin and cannot be redefined (this is not Perl 5; use `fn` not `sub`, or pass --compat)"
16673                    ),
16674                    line,
16675                ));
16676            }
16677        }
16678        Ok(())
16679    }
16680
16681    /// Check if a hash name shadows a reserved stryke hash and error if so.
16682    /// Called only in non-compat mode.
16683    fn check_hash_shadows_reserved(&self, name: &str, line: usize) -> PerlResult<()> {
16684        if Self::is_reserved_hash_name(name) {
16685            return Err(self.syntax_err(
16686                format!(
16687"`%{name}` is a stryke reserved hash and cannot be redefined (this is not Perl 5; pass --compat for Perl 5 mode)"
16688                ),
16689                line,
16690            ));
16691        }
16692        Ok(())
16693    }
16694
16695    /// Validate assignment to %hash in non-compat mode.
16696    /// Rejects: scalar, string, arrayref, hashref, coderef, undef, odd-length list.
16697    fn validate_hash_assignment(&self, value: &Expr, line: usize) -> PerlResult<()> {
16698        match &value.kind {
16699            ExprKind::Integer(_) | ExprKind::Float(_) => {
16700                return Err(self.syntax_err(
16701                    "cannot assign scalar to hash — use %h = (key => value) or %h = %{$hashref}",
16702                    line,
16703                ));
16704            }
16705            ExprKind::String(_) | ExprKind::InterpolatedString(_) | ExprKind::Bareword(_) => {
16706                return Err(self.syntax_err(
16707                    "cannot assign string to hash — use %h = (key => value) or %h = %{$hashref}",
16708                    line,
16709                ));
16710            }
16711            ExprKind::ArrayRef(_) => {
16712                return Err(self.syntax_err(
16713                    "cannot assign arrayref to hash — use %h = @{$arrayref} for even-length list",
16714                    line,
16715                ));
16716            }
16717            ExprKind::ScalarRef(inner) => {
16718                if matches!(inner.kind, ExprKind::ArrayVar(_)) {
16719                    return Err(self.syntax_err(
16720                        "cannot assign \\@array to hash — use %h = @array for even-length list",
16721                        line,
16722                    ));
16723                }
16724                if matches!(inner.kind, ExprKind::HashVar(_)) {
16725                    return Err(self.syntax_err(
16726                        "cannot assign \\%hash to hash — use %h = %other directly",
16727                        line,
16728                    ));
16729                }
16730            }
16731            ExprKind::HashRef(_) => {
16732                return Err(self.syntax_err(
16733                    "cannot assign hashref to hash — use %h = %{$hashref} to dereference",
16734                    line,
16735                ));
16736            }
16737            ExprKind::CodeRef { .. } => {
16738                return Err(self.syntax_err("cannot assign coderef to hash", line));
16739            }
16740            ExprKind::Undef => {
16741                return Err(
16742                    self.syntax_err("cannot assign undef to hash — use %h = () to empty", line)
16743                );
16744            }
16745            ExprKind::List(items)
16746                if items.len() % 2 != 0
16747                    && !items.iter().any(|e| {
16748                        matches!(
16749                            e.kind,
16750                            ExprKind::ArrayVar(_)
16751                                | ExprKind::HashVar(_)
16752                                | ExprKind::FuncCall { .. }
16753                                | ExprKind::Deref { .. }
16754                                | ExprKind::ScalarVar(_)
16755                        )
16756                    }) =>
16757            {
16758                return Err(self.syntax_err(
16759                        format!(
16760                            "odd-length list ({} elements) in hash assignment — missing value for last key",
16761                            items.len()
16762                        ),
16763                        line,
16764                    ));
16765            }
16766            _ => {}
16767        }
16768        Ok(())
16769    }
16770
16771    /// Validate assignment to @array in non-compat mode.
16772    /// Rejects: undef (likely a mistake — use `@a = ()` to empty).
16773    /// Note: bare scalars like `@a = 2` are allowed since Perl coerces them to single-element lists.
16774    /// Note: `@a = {hashref}` is allowed as a common pattern for single-element arrays.
16775    fn validate_array_assignment(&self, value: &Expr, line: usize) -> PerlResult<()> {
16776        if let ExprKind::Undef = &value.kind {
16777            return Err(
16778                self.syntax_err("cannot assign undef to array — use @a = () to empty", line)
16779            );
16780        }
16781        Ok(())
16782    }
16783
16784    /// Validate assignment to $scalar in non-compat mode.
16785    /// Rejects: list literals (Perl 5 silently returns last element — footgun).
16786    fn validate_scalar_assignment(&self, value: &Expr, line: usize) -> PerlResult<()> {
16787        if let ExprKind::List(items) = &value.kind {
16788            if items.len() > 1 {
16789                return Err(self.syntax_err(
16790                    format!(
16791                        "cannot assign {}-element list to scalar — Perl 5 silently takes last element; use ($x) = (list) or $x = $list[-1]",
16792                        items.len()
16793                    ),
16794                    line,
16795                ));
16796            }
16797        }
16798        Ok(())
16799    }
16800
16801    /// Validate an assignment based on target type (in non-compat mode only).
16802    fn validate_assignment(&self, target: &Expr, value: &Expr, line: usize) -> PerlResult<()> {
16803        if crate::compat_mode() {
16804            return Ok(());
16805        }
16806        match &target.kind {
16807            ExprKind::HashVar(_) => self.validate_hash_assignment(value, line),
16808            ExprKind::ArrayVar(_) => self.validate_array_assignment(value, line),
16809            ExprKind::ScalarVar(_) => self.validate_scalar_assignment(value, line),
16810            _ => Ok(()),
16811        }
16812    }
16813
16814    /// Parse a block OR a blockless comparison expression for sort/psort/heap.
16815    /// Blockless: `$a <=> $b` or `$a cmp $b` or any expression → wrapped as a Block.
16816    /// Also accepts a bare function name: `psort my_cmp, @list`.
16817    fn parse_block_or_bareword_cmp_block(&mut self) -> PerlResult<Block> {
16818        if matches!(self.peek(), Token::LBrace) {
16819            return self.parse_block();
16820        }
16821        let line = self.peek_line();
16822        // Bare sub name: `psort my_cmp, @list`
16823        if let Token::Ident(ref name) = self.peek().clone() {
16824            if matches!(
16825                self.peek_at(1),
16826                Token::Comma | Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
16827            ) {
16828                let name = name.clone();
16829                self.advance();
16830                let body = Expr {
16831                    kind: ExprKind::FuncCall {
16832                        name,
16833                        args: vec![
16834                            Expr {
16835                                kind: ExprKind::ScalarVar("a".to_string()),
16836                                line,
16837                            },
16838                            Expr {
16839                                kind: ExprKind::ScalarVar("b".to_string()),
16840                                line,
16841                            },
16842                        ],
16843                    },
16844                    line,
16845                };
16846                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
16847            }
16848        }
16849        // Blockless expression: `$a <=> $b`, `$b cmp $a`, etc.
16850        let expr = self.parse_assign_expr_stop_at_pipe()?;
16851        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
16852    }
16853
16854    /// After `fan` / `fan_cap` `{ BLOCK }`, optional `, progress => EXPR` or `progress => EXPR` (no comma).
16855    fn parse_fan_optional_progress(
16856        &mut self,
16857        which: &'static str,
16858    ) -> PerlResult<Option<Box<Expr>>> {
16859        let line = self.peek_line();
16860        if self.eat(&Token::Comma) {
16861            match self.peek() {
16862                Token::Ident(ref kw)
16863                    if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) =>
16864                {
16865                    self.advance();
16866                    self.expect(&Token::FatArrow)?;
16867                    return Ok(Some(Box::new(self.parse_assign_expr()?)));
16868                }
16869                _ => {
16870                    return Err(self.syntax_err(
16871                        format!("{which}: expected `progress => EXPR` after comma"),
16872                        line,
16873                    ));
16874                }
16875            }
16876        }
16877        if let Token::Ident(ref kw) = self.peek().clone() {
16878            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
16879                self.advance();
16880                self.expect(&Token::FatArrow)?;
16881                return Ok(Some(Box::new(self.parse_assign_expr()?)));
16882            }
16883        }
16884        Ok(None)
16885    }
16886
16887    /// Comma-separated assign expressions with optional trailing `, progress => EXPR`
16888    /// (for `pmap_chunked`, `psort`, etc.).
16889    ///
16890    /// Paren-less — individual parts parse through
16891    /// [`Self::parse_assign_expr_stop_at_pipe`] so a trailing `|>` is left for
16892    /// the enclosing pipe-forward loop (left-associative chaining).
16893    fn parse_assign_expr_list_optional_progress(&mut self) -> PerlResult<(Expr, Option<Expr>)> {
16894        // On the RHS of `|>`, list-taking builtins may be written bare with no
16895        // operand — `@a |> uniq`, `@a |> flatten`, `foo(bar, @a |> psort)`, etc.
16896        // When the next token is a list-terminator, yield an empty placeholder
16897        // list; [`Self::pipe_forward_apply`] substitutes the piped LHS at
16898        // desugar time, so the placeholder is never evaluated.
16899        if self.in_pipe_rhs()
16900            && matches!(
16901                self.peek(),
16902                Token::Semicolon
16903                    | Token::RBrace
16904                    | Token::RParen
16905                    | Token::Eof
16906                    | Token::PipeForward
16907                    | Token::Comma
16908            )
16909        {
16910            return Ok((self.pipe_placeholder_list(self.peek_line()), None));
16911        }
16912        let mut parts = vec![self.parse_assign_expr_stop_at_pipe()?];
16913        loop {
16914            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
16915                break;
16916            }
16917            if matches!(
16918                self.peek(),
16919                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
16920            ) {
16921                break;
16922            }
16923            if self.peek_is_postfix_stmt_modifier_keyword() {
16924                break;
16925            }
16926            if let Token::Ident(ref kw) = self.peek().clone() {
16927                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
16928                    self.advance();
16929                    self.expect(&Token::FatArrow)?;
16930                    let prog = self.parse_assign_expr_stop_at_pipe()?;
16931                    return Ok((merge_expr_list(parts), Some(prog)));
16932                }
16933            }
16934            parts.push(self.parse_assign_expr_stop_at_pipe()?);
16935        }
16936        Ok((merge_expr_list(parts), None))
16937    }
16938
16939    fn parse_one_arg(&mut self) -> PerlResult<Expr> {
16940        if matches!(self.peek(), Token::LParen) {
16941            self.advance();
16942            let expr = self.parse_expression()?;
16943            self.expect(&Token::RParen)?;
16944            Ok(expr)
16945        } else {
16946            self.parse_assign_expr_stop_at_pipe()
16947        }
16948    }
16949
16950    /// Bare argument for a Perl-5 named unary operator (`defined`, `length`,
16951    /// `abs`, `scalar`, `ref`, `keys`, `values`, etc.). Named unary precedence
16952    /// sits between shift (`<<`/`>>`) and comparison (`<`/`>`), so we parse
16953    /// only down to shift level. The surrounding `&&` / `||` / `==` / `<` /
16954    /// equality / logical / ternary stay outside the unary's argument.
16955    /// Without this `defined $x && Y` mis-parsed as `defined($x && Y)` and
16956    /// silently returned true whenever `$x` was defined — see the skip-list
16957    /// debugging write-up. Same scope rule for `length` etc.
16958    fn parse_named_unary_arg(&mut self) -> PerlResult<Expr> {
16959        if matches!(self.peek(), Token::LParen) {
16960            self.advance();
16961            let expr = self.parse_expression()?;
16962            self.expect(&Token::RParen)?;
16963            Ok(expr)
16964        } else {
16965            self.parse_shift()
16966        }
16967    }
16968
16969    fn parse_one_arg_or_default(&mut self) -> PerlResult<Expr> {
16970        // Default to `$_` when the next token cannot start an argument expression
16971        // because it has lower precedence than a named unary operator. Perl 5
16972        // named unary precedence sits above ternary / comparison / logical / bitwise
16973        // / assignment / list ops; everything below should terminate the implicit
16974        // argument and let the surrounding expression continue.
16975        // See `perldoc perlop` ("Named Unary Operators").
16976        if matches!(
16977            self.peek(),
16978            // Statement / list / call boundaries
16979            Token::Semicolon
16980                | Token::RBrace
16981                | Token::RParen
16982                | Token::RBracket
16983                | Token::Eof
16984                | Token::Comma
16985                | Token::FatArrow
16986                | Token::PipeForward
16987            // Ternary `? :`
16988                | Token::Question
16989                | Token::Colon
16990            // Comparison / equality (numeric + string)
16991                | Token::NumEq | Token::NumNe | Token::NumLt | Token::NumGt
16992                | Token::NumLe | Token::NumGe | Token::Spaceship
16993                | Token::StrEq | Token::StrNe | Token::StrLt | Token::StrGt
16994                | Token::StrLe | Token::StrGe | Token::StrCmp
16995            // Logical (symbolic and word forms) + defined-or
16996                | Token::LogAnd | Token::LogOr | Token::LogNot
16997                | Token::LogAndWord | Token::LogOrWord | Token::LogNotWord
16998                | Token::DefinedOr
16999            // Range (lower precedence than named unary)
17000                | Token::Range | Token::RangeExclusive
17001            // Assignment (any compound form)
17002                | Token::Assign | Token::PlusAssign | Token::MinusAssign
17003                | Token::MulAssign | Token::DivAssign | Token::ModAssign
17004                | Token::PowAssign | Token::DotAssign | Token::AndAssign
17005                | Token::OrAssign | Token::XorAssign | Token::DefinedOrAssign
17006                | Token::ShiftLeftAssign | Token::ShiftRightAssign
17007                | Token::BitAndAssign | Token::BitOrAssign
17008        ) {
17009            return Ok(Expr {
17010                kind: ExprKind::ScalarVar("_".into()),
17011                line: self.peek_line(),
17012            });
17013        }
17014        // `f()` — empty parens default to `$_`, matching Perl 5 semantics.
17015        // `perldoc -f length`: "If EXPR is omitted, returns the length of $_."
17016        // Perl accepts both `length` and `length()` as `length($_)`.
17017        if matches!(self.peek(), Token::LParen) && matches!(self.peek_at(1), Token::RParen) {
17018            let line = self.peek_line();
17019            self.advance(); // (
17020            self.advance(); // )
17021            return Ok(Expr {
17022                kind: ExprKind::ScalarVar("_".into()),
17023                line,
17024            });
17025        }
17026        // Named-unary precedence: parenless arg only goes down to shift level,
17027        // so surrounding `eq` / `==` / `?:` / `&&` / `||` stay outside. Without
17028        // this, `ref $x eq "FOO"` mis-parses as `ref ($x eq "FOO")`.
17029        // (PARITY-016 — also fixes `length $s == 3 ? "Y" : "N"` etc.)
17030        self.parse_named_unary_arg()
17031    }
17032
17033    /// Array operand for `shift` / `pop`: default `@_`, or `shift(@a)` / `shift()` (empty parens = `@_`).
17034    fn parse_one_arg_or_argv(&mut self) -> PerlResult<Expr> {
17035        let line = self.prev_line(); // line where shift/pop keyword was
17036        if matches!(self.peek(), Token::LParen) {
17037            self.advance();
17038            if matches!(self.peek(), Token::RParen) {
17039                self.advance();
17040                return Ok(Expr {
17041                    kind: ExprKind::ArrayVar("_".into()),
17042                    line: self.peek_line(),
17043                });
17044            }
17045            let expr = self.parse_expression()?;
17046            self.expect(&Token::RParen)?;
17047            return Ok(expr);
17048        }
17049        // Implicit semicolon: if next token is on a different line, don't consume it
17050        if matches!(
17051            self.peek(),
17052            Token::Semicolon
17053                | Token::RBrace
17054                | Token::RParen
17055                | Token::Eof
17056                | Token::Comma
17057                | Token::PipeForward
17058        ) || self.peek_line() > line
17059        {
17060            Ok(Expr {
17061                kind: ExprKind::ArrayVar("_".into()),
17062                line,
17063            })
17064        } else {
17065            self.parse_assign_expr()
17066        }
17067    }
17068
17069    fn parse_builtin_args(&mut self) -> PerlResult<Vec<Expr>> {
17070        if matches!(self.peek(), Token::LParen) {
17071            self.advance();
17072            let args = self.parse_arg_list()?;
17073            self.expect(&Token::RParen)?;
17074            Ok(args)
17075        } else if self.suppress_parenless_call > 0 && matches!(self.peek(), Token::Ident(_)) {
17076            // In thread context, don't consume barewords as arguments
17077            // so `t filesf sorted ep` parses `sorted` as a stage, not an arg to filesf
17078            Ok(vec![])
17079        } else {
17080            self.parse_list_until_terminator()
17081        }
17082    }
17083
17084    /// Check if the next token is `=>` (fat arrow). If so, the preceding bareword
17085    /// should be treated as an auto-quoted string (hash key), not a function call.
17086    /// Returns `Some(Expr::String(name))` if fat arrow follows, `None` otherwise.
17087    #[inline]
17088    fn fat_arrow_autoquote(&self, name: &str, line: usize) -> Option<Expr> {
17089        if matches!(self.peek(), Token::FatArrow) {
17090            Some(Expr {
17091                kind: ExprKind::String(name.to_string()),
17092                line,
17093            })
17094        } else {
17095            None
17096        }
17097    }
17098
17099    /// Parse a hash subscript key inside `{…}`.
17100    ///
17101    /// Perl auto-quotes a single bareword before `}`, even for keywords:
17102    /// `$h{print}`, `$r->{f}` etc. all yield the string key. Stryke also
17103    /// auto-quotes the string-comparison and word-logical operator tokens
17104    /// (`eq`, `ne`, `lt`, `gt`, `le`, `ge`, `cmp`, `and`, `or`, `not`, `x`)
17105    /// here — the lexer eagerly converts those identifiers to operator tokens,
17106    /// but inside `{…}` followed by `}` they're plainly hash keys.
17107    /// Stryke exception: topic-slot barewords (`_`, `_<`, `_0`, `_0<`, …)
17108    /// resolve to the topic value, not the literal name — `$h{_<}` ≡ `$h{$_<}`.
17109    fn parse_hash_subscript_key(&mut self) -> PerlResult<Expr> {
17110        let line = self.peek_line();
17111        if let Token::Ident(ref k) = self.peek().clone() {
17112            if matches!(self.peek_at(1), Token::RBrace) && !Self::is_underscore_topic_slot(k) {
17113                let s = k.clone();
17114                self.advance();
17115                return Ok(Expr {
17116                    kind: ExprKind::String(s),
17117                    line,
17118                });
17119            }
17120        }
17121        if matches!(self.peek_at(1), Token::RBrace) {
17122            if let Some(s) = Self::operator_keyword_to_ident_str(self.peek()) {
17123                self.advance();
17124                return Ok(Expr {
17125                    kind: ExprKind::String(s.to_string()),
17126                    line,
17127                });
17128            }
17129        }
17130        self.parse_expression()
17131    }
17132
17133    /// `progress` introducing the optional `progress => EXPR` suffix for `glob_par` / `par_sed`.
17134    #[inline]
17135    fn peek_is_glob_par_progress_kw(&self) -> bool {
17136        matches!(self.peek(), Token::Ident(ref kw) if kw == "progress")
17137            && matches!(self.peek_at(1), Token::FatArrow)
17138    }
17139
17140    /// Pattern list for `glob_par` / `par_sed` inside `(...)`, stopping before `)` or `progress =>`.
17141    fn parse_pattern_list_until_rparen_or_progress(&mut self) -> PerlResult<Vec<Expr>> {
17142        let mut args = Vec::new();
17143        loop {
17144            if matches!(self.peek(), Token::RParen | Token::Eof) {
17145                break;
17146            }
17147            if self.peek_is_glob_par_progress_kw() {
17148                break;
17149            }
17150            args.push(self.parse_assign_expr()?);
17151            match self.peek() {
17152                Token::RParen => break,
17153                Token::Comma => {
17154                    self.advance();
17155                    if matches!(self.peek(), Token::RParen) {
17156                        break;
17157                    }
17158                    if self.peek_is_glob_par_progress_kw() {
17159                        break;
17160                    }
17161                }
17162                _ => {
17163                    return Err(self.syntax_err(
17164                        "expected `,`, `)`, or `progress =>` after argument in `glob_par` / `par_sed`",
17165                        self.peek_line(),
17166                    ));
17167                }
17168            }
17169        }
17170        Ok(args)
17171    }
17172
17173    /// Paren-less pattern list for `glob_par` / `par_sed`, stopping before stmt end or `progress =>`.
17174    fn parse_pattern_list_glob_par_bare(&mut self) -> PerlResult<Vec<Expr>> {
17175        let mut args = Vec::new();
17176        loop {
17177            if matches!(
17178                self.peek(),
17179                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
17180            ) {
17181                break;
17182            }
17183            if self.peek_is_postfix_stmt_modifier_keyword() {
17184                break;
17185            }
17186            if self.peek_is_glob_par_progress_kw() {
17187                break;
17188            }
17189            args.push(self.parse_assign_expr()?);
17190            if !self.eat(&Token::Comma) {
17191                break;
17192            }
17193            if self.peek_is_glob_par_progress_kw() {
17194                break;
17195            }
17196        }
17197        Ok(args)
17198    }
17199
17200    /// `glob_pat EXPR, ...` or `glob_pat(...)` plus optional `, progress => EXPR` / inner `progress =>`.
17201    fn parse_glob_par_or_par_sed_args(&mut self) -> PerlResult<(Vec<Expr>, Option<Box<Expr>>)> {
17202        if matches!(self.peek(), Token::LParen) {
17203            self.advance();
17204            let args = self.parse_pattern_list_until_rparen_or_progress()?;
17205            let progress = if self.peek_is_glob_par_progress_kw() {
17206                self.advance();
17207                self.expect(&Token::FatArrow)?;
17208                Some(Box::new(self.parse_assign_expr()?))
17209            } else {
17210                None
17211            };
17212            self.expect(&Token::RParen)?;
17213            Ok((args, progress))
17214        } else {
17215            let args = self.parse_pattern_list_glob_par_bare()?;
17216            // Comma after the last pattern was consumed inside `parse_pattern_list_glob_par_bare`.
17217            let progress = if self.peek_is_glob_par_progress_kw() {
17218                self.advance();
17219                self.expect(&Token::FatArrow)?;
17220                Some(Box::new(self.parse_assign_expr()?))
17221            } else {
17222                None
17223            };
17224            Ok((args, progress))
17225        }
17226    }
17227
17228    pub(crate) fn parse_arg_list(&mut self) -> PerlResult<Vec<Expr>> {
17229        let mut args = Vec::new();
17230        // Inside `(...)`, `|>` is a normal operator again (e.g. `f(2 |> g, 3)`),
17231        // so shadow any outer paren-less-arg suppression from
17232        // `no_pipe_forward_depth`. Saturating so nested mixes are safe.
17233        let saved_no_pf = self.no_pipe_forward_depth;
17234        self.no_pipe_forward_depth = 0;
17235        while !matches!(
17236            self.peek(),
17237            Token::RParen | Token::RBracket | Token::RBrace | Token::Eof
17238        ) {
17239            let arg = match self.parse_assign_expr() {
17240                Ok(e) => e,
17241                Err(err) => {
17242                    self.no_pipe_forward_depth = saved_no_pf;
17243                    return Err(err);
17244                }
17245            };
17246            args.push(arg);
17247            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
17248                break;
17249            }
17250        }
17251        self.no_pipe_forward_depth = saved_no_pf;
17252        Ok(args)
17253    }
17254
17255    /// Parse a comma-separated list of slice subscript args. Each arg may be a regular
17256    /// expression, a closed range (`1:3`, `1..3:2`), or an open-ended Python-style colon
17257    /// range (`:`, `::`, `:N`, `N:`, `::-1`, `:N:M`, `N::M`, `::M`). Open-ended forms
17258    /// produce `ExprKind::SliceRange`; closed `1:3` produces `ExprKind::Range` (legacy).
17259    ///
17260    /// `is_hash` enables fat-comma-style bareword auto-quoting for endpoints — `{a:c:1}`
17261    /// treats `a` and `c` as string keys without quoting (cannot be a function call;
17262    /// use `func():other` if you actually want to invoke).
17263    pub(crate) fn parse_slice_arg_list(&mut self, is_hash: bool) -> PerlResult<Vec<Expr>> {
17264        let mut args = Vec::new();
17265        let saved_no_pf = self.no_pipe_forward_depth;
17266        self.no_pipe_forward_depth = 0;
17267        while !matches!(
17268            self.peek(),
17269            Token::RParen | Token::RBracket | Token::RBrace | Token::Eof
17270        ) {
17271            let arg = match self.parse_slice_arg(is_hash) {
17272                Ok(e) => e,
17273                Err(err) => {
17274                    self.no_pipe_forward_depth = saved_no_pf;
17275                    return Err(err);
17276                }
17277            };
17278            args.push(arg);
17279            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
17280                break;
17281            }
17282        }
17283        self.no_pipe_forward_depth = saved_no_pf;
17284        Ok(args)
17285    }
17286
17287    /// Parse one slice subscript argument (see [`Self::parse_slice_arg_list`]).
17288    fn parse_slice_arg(&mut self, is_hash: bool) -> PerlResult<Expr> {
17289        let line = self.peek_line();
17290
17291        // Open-start: `:` or `::` immediately
17292        if matches!(self.peek(), Token::Colon) {
17293            self.advance();
17294            return self.finish_slice_range(None, false, is_hash, line);
17295        }
17296        if matches!(self.peek(), Token::PackageSep) {
17297            self.advance();
17298            return self.finish_slice_range(None, true, is_hash, line);
17299        }
17300
17301        // Parse FROM with `:` suppressed inside `parse_range` so it doesn't get
17302        // consumed as a colon-range there — we want to handle the colon ourselves.
17303        self.suppress_colon_range = self.suppress_colon_range.saturating_add(1);
17304        let result = self.parse_slice_endpoint(is_hash);
17305        self.suppress_colon_range = self.suppress_colon_range.saturating_sub(1);
17306        let from_expr = result?;
17307
17308        // Trailing `:` or `::` after the FROM endpoint?
17309        if matches!(self.peek(), Token::Colon) {
17310            self.advance();
17311            return self.finish_slice_range(Some(Box::new(from_expr)), false, is_hash, line);
17312        }
17313        if matches!(self.peek(), Token::PackageSep) {
17314            self.advance();
17315            return self.finish_slice_range(Some(Box::new(from_expr)), true, is_hash, line);
17316        }
17317
17318        Ok(from_expr)
17319    }
17320
17321    /// After consuming the first colon (or `::` pair), parse the rest of the slice range.
17322    /// `double` is true if we just consumed `::` — TO is implicit `None`, the next
17323    /// expression (if any) is STEP.
17324    ///
17325    /// Returns `ExprKind::Range` for fully-closed forms (legacy compatibility) and
17326    /// `ExprKind::SliceRange` whenever any endpoint is omitted (open-ended).
17327    fn finish_slice_range(
17328        &mut self,
17329        from: Option<Box<Expr>>,
17330        double: bool,
17331        is_hash: bool,
17332        line: usize,
17333    ) -> PerlResult<Expr> {
17334        let (to, step) = if double {
17335            // `::` so TO is implicit; STEP is whatever (if anything) follows.
17336            let step_v = self.parse_slice_optional_endpoint(is_hash)?;
17337            (None, step_v)
17338        } else {
17339            // single `:` — parse TO, then optional `:STEP`.
17340            let to_v = self.parse_slice_optional_endpoint(is_hash)?;
17341            let step_v = if matches!(self.peek(), Token::Colon) {
17342                self.advance();
17343                self.parse_slice_optional_endpoint(is_hash)?
17344            } else if matches!(self.peek(), Token::PackageSep) {
17345                return Err(
17346                    self.syntax_err("Unexpected `::` after slice TO endpoint".to_string(), line)
17347                );
17348            } else {
17349                None
17350            };
17351            (to_v, step_v)
17352        };
17353
17354        // Closed form (both endpoints present) — produce a regular `Range` so the
17355        // rest of the compiler/VM keeps reusing existing range-expansion paths.
17356        if let (Some(f), Some(t)) = (from.as_ref(), to.as_ref()) {
17357            return Ok(Expr {
17358                kind: ExprKind::Range {
17359                    from: f.clone(),
17360                    to: t.clone(),
17361                    exclusive: false,
17362                    step,
17363                },
17364                line,
17365            });
17366        }
17367
17368        Ok(Expr {
17369            kind: ExprKind::SliceRange { from, to, step },
17370            line,
17371        })
17372    }
17373
17374    /// Parse an optional slice endpoint: returns `None` if the next token closes the slice
17375    /// arg (`,`, `]`, `}`, or another `:`). Otherwise parses an endpoint expression.
17376    fn parse_slice_optional_endpoint(&mut self, is_hash: bool) -> PerlResult<Option<Box<Expr>>> {
17377        if matches!(
17378            self.peek(),
17379            Token::Colon
17380                | Token::PackageSep
17381                | Token::Comma
17382                | Token::RBracket
17383                | Token::RBrace
17384                | Token::Eof
17385        ) {
17386            return Ok(None);
17387        }
17388        self.suppress_colon_range = self.suppress_colon_range.saturating_add(1);
17389        let r = self.parse_slice_endpoint(is_hash);
17390        self.suppress_colon_range = self.suppress_colon_range.saturating_sub(1);
17391        Ok(Some(Box::new(r?)))
17392    }
17393
17394    /// Parse a single slice endpoint expression. For hash slices, a bareword `Ident`
17395    /// followed by `:`, `::`, `,`, `]`, or `}` auto-quotes (fat-comma style); otherwise
17396    /// fall through to standard expression parsing. For array slices, no auto-quote.
17397    fn parse_slice_endpoint(&mut self, is_hash: bool) -> PerlResult<Expr> {
17398        if is_hash {
17399            if let Token::Ident(name) = self.peek().clone() {
17400                if matches!(
17401                    self.peek_at(1),
17402                    Token::Colon
17403                        | Token::PackageSep
17404                        | Token::Comma
17405                        | Token::RBracket
17406                        | Token::RBrace
17407                ) {
17408                    let line = self.peek_line();
17409                    self.advance();
17410                    return Ok(Expr {
17411                        kind: ExprKind::String(name),
17412                        line,
17413                    });
17414                }
17415            }
17416        }
17417        self.parse_assign_expr()
17418    }
17419
17420    /// Arguments for `->name` / `->SUPER::name` **without** `(...)`. Unlike `die foo + 1`
17421    /// (unary `+` on `1` passed to `foo`), Perl treats `$o->meth + 5` as infix `+` after a
17422    /// no-arg method call; we must not consume that `+` as the start of a first argument.
17423    fn parse_method_arg_list_no_paren(&mut self) -> PerlResult<Vec<Expr>> {
17424        let mut args = Vec::new();
17425        let call_line = self.prev_line();
17426        loop {
17427            // `$g->next { ... }` — `{` starts the enclosing statement's block, not an anonymous
17428            // hash argument to `next` (paren-less method call has no args here).
17429            if args.is_empty() && matches!(self.peek(), Token::LBrace) {
17430                break;
17431            }
17432            if matches!(
17433                self.peek(),
17434                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
17435            ) {
17436                break;
17437            }
17438            if let Token::Ident(ref kw) = self.peek().clone() {
17439                if matches!(
17440                    kw.as_str(),
17441                    "if" | "unless" | "while" | "until" | "for" | "foreach"
17442                ) {
17443                    break;
17444                }
17445            }
17446            // `foo($obj->meth, $x)` — comma separates *outer* args; it is not the start of a
17447            // paren-less method argument (those use spaces: `$obj->meth $a, $b`).
17448            if args.is_empty()
17449                && (self.peek_method_arg_infix_terminator() || matches!(self.peek(), Token::Comma))
17450            {
17451                break;
17452            }
17453            // Implicit semicolon: if no args collected yet and next token is on a different
17454            // line, treat newline as statement boundary. Allows `$p->method\nnext_stmt`.
17455            if args.is_empty() && self.peek_line() > call_line {
17456                break;
17457            }
17458            args.push(self.parse_assign_expr()?);
17459            if !self.eat(&Token::Comma) {
17460                break;
17461            }
17462        }
17463        Ok(args)
17464    }
17465
17466    /// Tokens that end a paren-less method arg list when no comma-separated args yet (infix on
17467    /// the whole `->meth` expression).
17468    fn peek_method_arg_infix_terminator(&self) -> bool {
17469        matches!(
17470            self.peek(),
17471            Token::Plus
17472                | Token::Minus
17473                | Token::Star
17474                | Token::Slash
17475                | Token::Percent
17476                | Token::Power
17477                | Token::Dot
17478                | Token::X
17479                | Token::NumEq
17480                | Token::NumNe
17481                | Token::NumLt
17482                | Token::NumGt
17483                | Token::NumLe
17484                | Token::NumGe
17485                | Token::Spaceship
17486                | Token::StrEq
17487                | Token::StrNe
17488                | Token::StrLt
17489                | Token::StrGt
17490                | Token::StrLe
17491                | Token::StrGe
17492                | Token::StrCmp
17493                | Token::LogAnd
17494                | Token::LogOr
17495                | Token::LogAndWord
17496                | Token::LogOrWord
17497                | Token::DefinedOr
17498                | Token::BitAnd
17499                | Token::BitOr
17500                | Token::BitXor
17501                | Token::ShiftLeft
17502                | Token::ShiftRight
17503                | Token::Range
17504                | Token::RangeExclusive
17505                | Token::BindMatch
17506                | Token::BindNotMatch
17507                | Token::Arrow
17508                // `($a->b) ? $a->c : $a->d` — `->c` must not slurp the ternary `:` / `?`.
17509                | Token::Question
17510                | Token::Colon
17511                // Assignment operators: `$obj->field = val` is setter sugar, not method arg.
17512                | Token::Assign
17513                | Token::PlusAssign
17514                | Token::MinusAssign
17515                | Token::MulAssign
17516                | Token::DivAssign
17517                | Token::ModAssign
17518                | Token::PowAssign
17519                | Token::DotAssign
17520                | Token::AndAssign
17521                | Token::OrAssign
17522                | Token::XorAssign
17523                | Token::DefinedOrAssign
17524                | Token::ShiftLeftAssign
17525                | Token::ShiftRightAssign
17526                | Token::BitAndAssign
17527                | Token::BitOrAssign
17528        )
17529    }
17530
17531    fn parse_list_until_terminator(&mut self) -> PerlResult<Vec<Expr>> {
17532        let mut args = Vec::new();
17533        // Line of the last consumed token (the keyword / function name that
17534        // triggered this arg parse).  Used for implicit-semicolon: if no args
17535        // have been parsed yet and the next token is on a *different* line,
17536        // treat the newline as a statement boundary and stop.
17537        let call_line = self.prev_line();
17538        loop {
17539            if matches!(
17540                self.peek(),
17541                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
17542            ) {
17543                break;
17544            }
17545            // Check for postfix modifiers — stop before `expr for LIST` / `expr if COND` etc.
17546            if let Token::Ident(ref kw) = self.peek().clone() {
17547                if matches!(
17548                    kw.as_str(),
17549                    "if" | "unless" | "while" | "until" | "for" | "foreach"
17550                ) {
17551                    break;
17552                }
17553            }
17554            // Implicit semicolons: if no args have been collected yet and the
17555            // next token is on a different line from the call keyword, treat
17556            // the newline as a statement boundary.  This prevents paren-less
17557            // calls (`say`, `print`, user subs) from greedily swallowing the
17558            // *next* statement when the author omitted a semicolon.
17559            // After a comma continuation, multi-line arg lists still work.
17560            if args.is_empty() && self.peek_line() > call_line {
17561                break;
17562            }
17563            // Paren-less builtin args: `|>` terminates the whole call list, so
17564            // individual args must not absorb a following `|>`.
17565            args.push(self.parse_assign_expr_stop_at_pipe()?);
17566            if !self.eat(&Token::Comma) {
17567                break;
17568            }
17569        }
17570        Ok(args)
17571    }
17572
17573    /// Body of `+{ ... }` — Perl's force-hashref idiom. The opening `+` and `{`
17574    /// have already been consumed. Tries the normal `KEY => VAL, …` shape first
17575    /// (so `+{ a => 1, b => 2 }` is identical to `{ a => 1, b => 2 }`); on
17576    /// failure falls back to "single list-yielding expression treated as a
17577    /// flat key/value spread" so `+{ map { (k, v) } LIST }` works without
17578    /// the user needing a temp `my %h = ...; \%h` shuffle.
17579    fn parse_forced_hashref_body(&mut self, line: usize) -> PerlResult<Expr> {
17580        let saved = self.pos;
17581        if let Ok(pairs) = self.try_parse_hash_ref() {
17582            return Ok(Expr {
17583                kind: ExprKind::HashRef(pairs),
17584                line,
17585            });
17586        }
17587        // Empty `+{}` is the empty hashref.
17588        self.pos = saved;
17589        if matches!(self.peek(), Token::RBrace) {
17590            self.advance();
17591            return Ok(Expr {
17592                kind: ExprKind::HashRef(vec![]),
17593                line,
17594            });
17595        }
17596        // Single expression — eval as list, flatten into key/value pairs via the
17597        // existing __HASH_SPREAD__ sentinel that `ExprKind::HashRef` already
17598        // handles in [`Interpreter::eval_expr`].
17599        let inner = self.parse_expression()?;
17600        self.expect(&Token::RBrace)?;
17601        let sentinel_key = Expr {
17602            kind: ExprKind::String("__HASH_SPREAD__".into()),
17603            line,
17604        };
17605        Ok(Expr {
17606            kind: ExprKind::HashRef(vec![(sentinel_key, inner)]),
17607            line,
17608        })
17609    }
17610
17611    fn try_parse_hash_ref(&mut self) -> PerlResult<Vec<(Expr, Expr)>> {
17612        let mut pairs = Vec::new();
17613        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
17614            // Perl autoquotes a bareword immediately before `=>` (hash key), even for keywords like
17615            // `pos`, `bless`, `return` — see Text::Balanced `_failmsg` (`pos => $pos`).
17616            // Stryke exception: topic-slot barewords (`_`, `_<`, `_0`, `_0<`, `_!N!`, …)
17617            // resolve to the topic value, not the literal name — `{ _ => 1 }` ≡ `{ $_ => 1 }`.
17618            let line = self.peek_line();
17619            let key = if let Token::Ident(ref name) = self.peek().clone() {
17620                if matches!(self.peek_at(1), Token::FatArrow)
17621                    && !Self::is_underscore_topic_slot(name)
17622                {
17623                    self.advance();
17624                    Expr {
17625                        kind: ExprKind::String(name.clone()),
17626                        line,
17627                    }
17628                } else {
17629                    self.parse_assign_expr()?
17630                }
17631            } else {
17632                self.parse_assign_expr()?
17633            };
17634            // If the key expression is a hash/array variable and is followed by `}` or `,`
17635            // with no `=>`, treat the whole thing as a hash-from-expression construction.
17636            // This handles `{ %a }`, `{ %a, key => val }`, etc.
17637            if matches!(self.peek(), Token::RBrace | Token::Comma)
17638                && matches!(
17639                    key.kind,
17640                    ExprKind::HashVar(_)
17641                        | ExprKind::Deref {
17642                            kind: Sigil::Hash,
17643                            ..
17644                        }
17645                )
17646            {
17647                // Synthesize a pair whose key/value is spread from the hash expression.
17648                // Use a sentinel "spread" pair: key=the hash expr, value=undef.
17649                // The evaluator will flatten this.
17650                let sentinel_key = Expr {
17651                    kind: ExprKind::String("__HASH_SPREAD__".into()),
17652                    line,
17653                };
17654                pairs.push((sentinel_key, key));
17655                self.eat(&Token::Comma);
17656                continue;
17657            }
17658            // Expect => or , after key
17659            if self.eat(&Token::FatArrow) || self.eat(&Token::Comma) {
17660                let val = self.parse_assign_expr()?;
17661                pairs.push((key, val));
17662                self.eat(&Token::Comma);
17663            } else {
17664                return Err(self.syntax_err("Expected => or , in hash ref", key.line));
17665            }
17666        }
17667        self.expect(&Token::RBrace)?;
17668        Ok(pairs)
17669    }
17670
17671    /// Parse `key => val, key => val, ...` up to (but not consuming) `term`.
17672    /// Used by the `%[…]` and `%{k=>v,…}` sugar to build an inline hashref
17673    /// AST node, sidestepping the block/hashref ambiguity that `try_parse_hash_ref`
17674    /// navigates. Caller expects and consumes `term` itself.
17675    fn parse_hashref_pairs_until(&mut self, term: &Token) -> PerlResult<Vec<(Expr, Expr)>> {
17676        let mut pairs = Vec::new();
17677        while !matches!(&self.peek(), t if std::mem::discriminant(*t) == std::mem::discriminant(term))
17678            && !matches!(self.peek(), Token::Eof)
17679        {
17680            let line = self.peek_line();
17681            let key = if let Token::Ident(ref name) = self.peek().clone() {
17682                if matches!(self.peek_at(1), Token::FatArrow)
17683                    && !Self::is_underscore_topic_slot(name)
17684                {
17685                    self.advance();
17686                    Expr {
17687                        kind: ExprKind::String(name.clone()),
17688                        line,
17689                    }
17690                } else {
17691                    self.parse_assign_expr()?
17692                }
17693            } else {
17694                self.parse_assign_expr()?
17695            };
17696            if self.eat(&Token::FatArrow) || self.eat(&Token::Comma) {
17697                let val = self.parse_assign_expr()?;
17698                pairs.push((key, val));
17699                self.eat(&Token::Comma);
17700            } else {
17701                return Err(self.syntax_err("Expected => or , in hash ref", key.line));
17702            }
17703        }
17704        Ok(pairs)
17705    }
17706
17707    /// Inside an interpolated string, after a `$name`/`${EXPR}`/`$name[i]`/`$name{k}` base
17708    /// expression, consume any chain of `->[…]`, `->{…}`, **adjacent** `[…]`, or `{…}`
17709    /// subscripts. Perl auto-implies `->` between consecutive subscripts, so
17710    /// `$matrix[1][1]` is `$matrix[1]->[1]` and `$h{a}{b}` is `$h{a}->{b}`.
17711    /// Each step wraps the current expression in an `ArrowDeref`.
17712    fn interp_chain_subscripts(
17713        &self,
17714        chars: &[char],
17715        i: &mut usize,
17716        mut base: Expr,
17717        line: usize,
17718    ) -> Expr {
17719        loop {
17720            // Optional `->` connector
17721            let (after, requires_subscript) =
17722                if *i + 1 < chars.len() && chars[*i] == '-' && chars[*i + 1] == '>' {
17723                    (*i + 2, true)
17724                } else {
17725                    (*i, false)
17726                };
17727            if after >= chars.len() {
17728                break;
17729            }
17730            match chars[after] {
17731                '[' => {
17732                    *i = after + 1;
17733                    let mut idx_str = String::new();
17734                    while *i < chars.len() && chars[*i] != ']' {
17735                        idx_str.push(chars[*i]);
17736                        *i += 1;
17737                    }
17738                    if *i < chars.len() {
17739                        *i += 1;
17740                    }
17741                    let idx_expr = if let Some(rest) = idx_str.strip_prefix('$') {
17742                        Expr {
17743                            kind: ExprKind::ScalarVar(rest.to_string()),
17744                            line,
17745                        }
17746                    } else if let Ok(n) = idx_str.parse::<i64>() {
17747                        Expr {
17748                            kind: ExprKind::Integer(n),
17749                            line,
17750                        }
17751                    } else {
17752                        Expr {
17753                            kind: ExprKind::String(idx_str),
17754                            line,
17755                        }
17756                    };
17757                    base = Expr {
17758                        kind: ExprKind::ArrowDeref {
17759                            expr: Box::new(base),
17760                            index: Box::new(idx_expr),
17761                            kind: DerefKind::Array,
17762                        },
17763                        line,
17764                    };
17765                }
17766                '{' => {
17767                    *i = after + 1;
17768                    let mut key = String::new();
17769                    let mut depth = 1usize;
17770                    while *i < chars.len() && depth > 0 {
17771                        if chars[*i] == '{' {
17772                            depth += 1;
17773                        } else if chars[*i] == '}' {
17774                            depth -= 1;
17775                            if depth == 0 {
17776                                break;
17777                            }
17778                        }
17779                        key.push(chars[*i]);
17780                        *i += 1;
17781                    }
17782                    if *i < chars.len() {
17783                        *i += 1;
17784                    }
17785                    let key_expr = if let Some(rest) = key.strip_prefix('$') {
17786                        Expr {
17787                            kind: ExprKind::ScalarVar(rest.to_string()),
17788                            line,
17789                        }
17790                    } else {
17791                        Expr {
17792                            kind: ExprKind::String(key),
17793                            line,
17794                        }
17795                    };
17796                    base = Expr {
17797                        kind: ExprKind::ArrowDeref {
17798                            expr: Box::new(base),
17799                            index: Box::new(key_expr),
17800                            kind: DerefKind::Hash,
17801                        },
17802                        line,
17803                    };
17804                }
17805                _ => {
17806                    if requires_subscript {
17807                        // `->method()` etc — not interpolated, leave for literal output.
17808                    }
17809                    break;
17810                }
17811            }
17812        }
17813        base
17814    }
17815
17816    /// Reject `$a` / `$b` references in `--no-interop` mode (lexer catches them
17817    /// outside double-quoted strings; this catches the in-string interpolation
17818    /// path which has its own parser bypassing `Token::ScalarVar`).
17819    fn no_interop_check_scalar_var_name(&self, name: &str, line: usize) -> PerlResult<()> {
17820        if crate::no_interop_mode() && (name == "a" || name == "b") {
17821            return Err(self.syntax_err(
17822                format!(
17823                    "stryke uses `$_0` / `$_1` instead of `${}` (--no-interop is active)",
17824                    name
17825                ),
17826                line,
17827            ));
17828        }
17829        Ok(())
17830    }
17831
17832    fn parse_interpolated_string(&self, s: &str, line: usize) -> PerlResult<Expr> {
17833        // Parse $var and @var inside double-quoted strings
17834        let mut parts = Vec::new();
17835        let mut literal = String::new();
17836        let chars: Vec<char> = s.chars().collect();
17837        let mut i = 0;
17838
17839        'istr: while i < chars.len() {
17840            if chars[i] == LITERAL_DOLLAR_IN_DQUOTE {
17841                literal.push('$');
17842                i += 1;
17843                continue;
17844            }
17845            if chars[i] == LITERAL_AT_IN_DQUOTE {
17846                literal.push('@');
17847                i += 1;
17848                continue;
17849            }
17850            // "\\$x" in source: one backslash in the string, then interpolate $x (Perl double-quoted string).
17851            if chars[i] == '\\' && i + 1 < chars.len() && chars[i + 1] == '$' {
17852                literal.push('\\');
17853                i += 1;
17854                // i now points at '$' — fall through to $ handling below
17855            }
17856            if chars[i] == '$' && i + 1 < chars.len() {
17857                if !literal.is_empty() {
17858                    parts.push(StringPart::Literal(std::mem::take(&mut literal)));
17859                }
17860                i += 1; // past `$`
17861                        // Perl allows whitespace between `$` and the variable name (`$ foo` → `$foo`).
17862                while i < chars.len() && chars[i].is_whitespace() {
17863                    i += 1;
17864                }
17865                if i >= chars.len() {
17866                    return Err(self.syntax_err("Final $ should be \\$ or $name", line));
17867                }
17868                // `$#name` — last index of `@name` (Perl `$#array`).
17869                if chars[i] == '#' {
17870                    i += 1;
17871                    let mut sname = String::from("#");
17872                    while i < chars.len()
17873                        && (chars[i].is_alphanumeric() || chars[i] == '_' || chars[i] == ':')
17874                    {
17875                        sname.push(chars[i]);
17876                        i += 1;
17877                    }
17878                    while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
17879                        sname.push_str("::");
17880                        i += 2;
17881                        while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
17882                            sname.push(chars[i]);
17883                            i += 1;
17884                        }
17885                    }
17886                    self.no_interop_check_scalar_var_name(&sname, line)?;
17887                    parts.push(StringPart::ScalarVar(sname));
17888                    continue;
17889                }
17890                // `$$` — process id (Perl `$$`), only when the two `$` are adjacent (no whitespace
17891                // between) and the second `$` is not followed by a word character or digit (`$$x`
17892                // / `$$_` / `$$0` are `$` + `$x` / `$_` / `$0`).
17893                if chars[i] == '$' {
17894                    let next_c = chars.get(i + 1).copied();
17895                    let is_pid = match next_c {
17896                        None => true,
17897                        Some(c)
17898                            if !c.is_ascii_digit() && !matches!(c, 'A'..='Z' | 'a'..='z' | '_') =>
17899                        {
17900                            true
17901                        }
17902                        _ => false,
17903                    };
17904                    if is_pid {
17905                        parts.push(StringPart::ScalarVar("$$".to_string()));
17906                        i += 1; // consume second `$`
17907                        continue;
17908                    }
17909                    i += 1; // skip second `$` — same as a single `$` before the identifier
17910                }
17911                if chars[i] == '{' {
17912                    // `${…}` — braced variable OR expression interpolation.
17913                    //   `${name}`              → ScalarVar(name)        (Perl standard)
17914                    //   `${$ref}` / `${\EXPR}` → deref the expression   (Perl standard)
17915                    //   `${name}[idx]` / `${name}{k}` / `${$r}[i]` …    chain after `}`
17916                    // stryke's prior `#{expr}` form remains supported elsewhere.
17917                    i += 1;
17918                    let mut inner = String::new();
17919                    let mut depth = 1usize;
17920                    while i < chars.len() && depth > 0 {
17921                        match chars[i] {
17922                            '{' => depth += 1,
17923                            '}' => {
17924                                depth -= 1;
17925                                if depth == 0 {
17926                                    break;
17927                                }
17928                            }
17929                            _ => {}
17930                        }
17931                        inner.push(chars[i]);
17932                        i += 1;
17933                    }
17934                    if i < chars.len() {
17935                        i += 1; // skip closing }
17936                    }
17937
17938                    // Distinguish "name" from "expression". If trimmed inner starts with
17939                    // `$`, `\`, or contains operator/punctuation chars, treat as Perl
17940                    // expression and emit a scalar deref. Otherwise, plain variable name.
17941                    let trimmed = inner.trim();
17942                    let is_expr = trimmed.starts_with('$')
17943                        || trimmed.starts_with('\\')
17944                        || trimmed.starts_with('@')   // `${@arr}` rare but valid
17945                        || trimmed.starts_with('%')   // `${%h}`   rare but valid
17946                        || trimmed.contains(['(', '+', '-', '*', '/', '.', '?', '&', '|']);
17947                    let mut base: Expr = if is_expr {
17948                        // Re-parse the inner content as a Perl expression. Wrap in
17949                        // `Deref { kind: Sigil::Scalar }` to dereference the resulting
17950                        // scalar reference (Perl: `${$r}` ≡ `$$r`).
17951                        match parse_expression_from_str(trimmed, "<interp>") {
17952                            Ok(e) => Expr {
17953                                kind: ExprKind::Deref {
17954                                    expr: Box::new(e),
17955                                    kind: Sigil::Scalar,
17956                                },
17957                                line,
17958                            },
17959                            Err(_) => Expr {
17960                                kind: ExprKind::ScalarVar(inner.clone()),
17961                                line,
17962                            },
17963                        }
17964                    } else {
17965                        // Treat as a plain (possibly qualified) variable name.
17966                        self.no_interop_check_scalar_var_name(&inner, line)?;
17967                        Expr {
17968                            kind: ExprKind::ScalarVar(inner),
17969                            line,
17970                        }
17971                    };
17972
17973                    // After `${…}` we may see `[idx]` / `{key}` for indexing into the
17974                    // dereferenced array/hash (`${$ar}[1]`, `${$hr}{k}`), and arrow
17975                    // chains thereafter.
17976                    base = self.interp_chain_subscripts(&chars, &mut i, base, line);
17977                    parts.push(StringPart::Expr(base));
17978                } else if chars[i] == '^' {
17979                    // `$^V`, `$^O`, … — name stored as `^V`, `^O`, … (see [`Interpreter::get_special_var`]).
17980                    let mut name = String::from("^");
17981                    i += 1;
17982                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
17983                        name.push(chars[i]);
17984                        i += 1;
17985                    }
17986                    if i < chars.len() && chars[i] == '{' {
17987                        i += 1; // skip {
17988                        let mut key = String::new();
17989                        let mut depth = 1;
17990                        while i < chars.len() && depth > 0 {
17991                            if chars[i] == '{' {
17992                                depth += 1;
17993                            } else if chars[i] == '}' {
17994                                depth -= 1;
17995                                if depth == 0 {
17996                                    break;
17997                                }
17998                            }
17999                            key.push(chars[i]);
18000                            i += 1;
18001                        }
18002                        if i < chars.len() {
18003                            i += 1;
18004                        }
18005                        let key_expr = if let Some(rest) = key.strip_prefix('$') {
18006                            Expr {
18007                                kind: ExprKind::ScalarVar(rest.to_string()),
18008                                line,
18009                            }
18010                        } else {
18011                            Expr {
18012                                kind: ExprKind::String(key),
18013                                line,
18014                            }
18015                        };
18016                        parts.push(StringPart::Expr(Expr {
18017                            kind: ExprKind::HashElement {
18018                                hash: name,
18019                                key: Box::new(key_expr),
18020                            },
18021                            line,
18022                        }));
18023                    } else if i < chars.len() && chars[i] == '[' {
18024                        i += 1;
18025                        let mut idx_str = String::new();
18026                        while i < chars.len() && chars[i] != ']' {
18027                            idx_str.push(chars[i]);
18028                            i += 1;
18029                        }
18030                        if i < chars.len() {
18031                            i += 1;
18032                        }
18033                        let idx_expr = if let Some(rest) = idx_str.strip_prefix('$') {
18034                            Expr {
18035                                kind: ExprKind::ScalarVar(rest.to_string()),
18036                                line,
18037                            }
18038                        } else if let Ok(n) = idx_str.parse::<i64>() {
18039                            Expr {
18040                                kind: ExprKind::Integer(n),
18041                                line,
18042                            }
18043                        } else {
18044                            Expr {
18045                                kind: ExprKind::String(idx_str),
18046                                line,
18047                            }
18048                        };
18049                        parts.push(StringPart::Expr(Expr {
18050                            kind: ExprKind::ArrayElement {
18051                                array: name,
18052                                index: Box::new(idx_expr),
18053                            },
18054                            line,
18055                        }));
18056                    } else {
18057                        self.no_interop_check_scalar_var_name(&name, line)?;
18058                        parts.push(StringPart::ScalarVar(name));
18059                    }
18060                } else if chars[i].is_alphabetic() || chars[i] == '_' {
18061                    let mut name = String::new();
18062                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
18063                        name.push(chars[i]);
18064                        i += 1;
18065                    }
18066                    // Package-qualified names: `$Foo::x`, `$Foo::Bar::baz`. Mirror
18067                    // the `$#Foo::a` continuation logic. Without this, `"$Foo::x"`
18068                    // captures only `Foo` and leaves `::x` as literal text — the
18069                    // interpolation reads bare `$Foo`, which is undef.
18070                    while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
18071                        name.push_str("::");
18072                        i += 2;
18073                        while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
18074                            name.push(chars[i]);
18075                            i += 1;
18076                        }
18077                    }
18078                    // `$_<`, `$_<<`, … — outer topic (stryke extension). Also
18079                    // `$_N<`, `$_N<<` for positional aliases. And the indexed
18080                    // shortcut `$_<N` ≡ `$_<<<...<` (N chevrons), so `"$_<3"`
18081                    // and `"$_<<<"` interpolate identically.
18082                    let is_topic_slot = name == "_"
18083                        || (name.len() > 1
18084                            && name.starts_with('_')
18085                            && name[1..].bytes().all(|b| b.is_ascii_digit()));
18086                    if is_topic_slot {
18087                        // Try indexed-ascent first: `<` immediately followed by digits.
18088                        let try_indexed = chars.get(i) == Some(&'<')
18089                            && chars.get(i + 1).is_some_and(|c| c.is_ascii_digit());
18090                        let mut handled_indexed = false;
18091                        if try_indexed {
18092                            let mut j = i + 1;
18093                            while j < chars.len() && chars[j].is_ascii_digit() {
18094                                j += 1;
18095                            }
18096                            let digits: String = chars[i + 1..j].iter().collect();
18097                            if let Ok(n) = digits.parse::<usize>() {
18098                                if n >= 1 {
18099                                    for _ in 0..n {
18100                                        name.push('<');
18101                                    }
18102                                    i = j;
18103                                    handled_indexed = true;
18104                                }
18105                            }
18106                        }
18107                        if !handled_indexed {
18108                            while i < chars.len() && chars[i] == '<' {
18109                                name.push('<');
18110                                i += 1;
18111                            }
18112                        }
18113                    }
18114                    // `--no-interop`: `$a` / `$b` are Perl-isms; reject inside
18115                    // string interpolation too. Catches both `"$a"` and `"$a[0]"`
18116                    // / `"$a{k}"` / `"$a->[0]"` because every branch below uses
18117                    // `name` to build the expression.
18118                    self.no_interop_check_scalar_var_name(&name, line)?;
18119                    // Build the base expression, then thread arrow-deref chains
18120                    // (`->[…]` / `->{…}`) onto it so things like `$ar->[2]`,
18121                    // `$href->{k}`, and chained `$x->{a}[1]->{b}` interpolate
18122                    // correctly inside double-quoted strings (Perl convention).
18123                    let mut base = if i < chars.len() && chars[i] == '{' {
18124                        // $hash{key}
18125                        i += 1; // skip {
18126                        let mut key = String::new();
18127                        let mut depth = 1;
18128                        while i < chars.len() && depth > 0 {
18129                            if chars[i] == '{' {
18130                                depth += 1;
18131                            } else if chars[i] == '}' {
18132                                depth -= 1;
18133                                if depth == 0 {
18134                                    break;
18135                                }
18136                            }
18137                            key.push(chars[i]);
18138                            i += 1;
18139                        }
18140                        if i < chars.len() {
18141                            i += 1;
18142                        } // skip }
18143                        let key_expr = if let Some(rest) = key.strip_prefix('$') {
18144                            Expr {
18145                                kind: ExprKind::ScalarVar(rest.to_string()),
18146                                line,
18147                            }
18148                        } else {
18149                            Expr {
18150                                kind: ExprKind::String(key),
18151                                line,
18152                            }
18153                        };
18154                        Expr {
18155                            kind: ExprKind::HashElement {
18156                                hash: name,
18157                                key: Box::new(key_expr),
18158                            },
18159                            line,
18160                        }
18161                    } else if i < chars.len() && chars[i] == '[' {
18162                        // $array[idx]
18163                        i += 1;
18164                        let mut idx_str = String::new();
18165                        while i < chars.len() && chars[i] != ']' {
18166                            idx_str.push(chars[i]);
18167                            i += 1;
18168                        }
18169                        if i < chars.len() {
18170                            i += 1;
18171                        }
18172                        let idx_expr = if let Some(rest) = idx_str.strip_prefix('$') {
18173                            Expr {
18174                                kind: ExprKind::ScalarVar(rest.to_string()),
18175                                line,
18176                            }
18177                        } else if let Ok(n) = idx_str.parse::<i64>() {
18178                            Expr {
18179                                kind: ExprKind::Integer(n),
18180                                line,
18181                            }
18182                        } else {
18183                            Expr {
18184                                kind: ExprKind::String(idx_str),
18185                                line,
18186                            }
18187                        };
18188                        Expr {
18189                            kind: ExprKind::ArrayElement {
18190                                array: name,
18191                                index: Box::new(idx_expr),
18192                            },
18193                            line,
18194                        }
18195                    } else {
18196                        // Bare $name — defer to the chain-extension loop below.
18197                        Expr {
18198                            kind: ExprKind::ScalarVar(name),
18199                            line,
18200                        }
18201                    };
18202
18203                    // Chain `->[…]` / `->{…}` AND adjacent `[…]` / `{…}` — Perl
18204                    // implies `->` between consecutive subscripts (`$m[1][2]`
18205                    // ≡ `$m[1]->[2]`).  See `interp_chain_subscripts`.
18206                    base = self.interp_chain_subscripts(&chars, &mut i, base, line);
18207                    parts.push(StringPart::Expr(base));
18208                } else if chars[i].is_ascii_digit() {
18209                    // $0 (program name), $1…$n (regexp captures). Perl disallows $01, $02, …
18210                    if chars[i] == '0' {
18211                        i += 1;
18212                        if i < chars.len() && chars[i].is_ascii_digit() {
18213                            return Err(self.syntax_err(
18214                                "Numeric variables with more than one digit may not start with '0'",
18215                                line,
18216                            ));
18217                        }
18218                        parts.push(StringPart::ScalarVar("0".into()));
18219                    } else {
18220                        let start = i;
18221                        while i < chars.len() && chars[i].is_ascii_digit() {
18222                            i += 1;
18223                        }
18224                        parts.push(StringPart::ScalarVar(chars[start..i].iter().collect()));
18225                    }
18226                } else {
18227                    let c = chars[i];
18228                    let probe = c.to_string();
18229                    // `&` is the regex-match special var — semantically symmetric with
18230                    // backtick (`$``) prematch and apostrophe (`$'`) postmatch which
18231                    // are already handled here. `is_special_scalar_name_for_get` doesn't
18232                    // currently list `&`/`'`/`` ` `` (those have separate runtime paths
18233                    // for set/clear under regex updates), so we add them inline.
18234                    if VMHelper::is_special_scalar_name_for_get(&probe)
18235                        || matches!(c, '\'' | '`' | '&')
18236                    {
18237                        i += 1;
18238                        // Check for hash element access: `$+{key}`, `$-{key}`, etc.
18239                        if i < chars.len() && chars[i] == '{' {
18240                            i += 1; // skip {
18241                            let mut key = String::new();
18242                            let mut depth = 1;
18243                            while i < chars.len() && depth > 0 {
18244                                if chars[i] == '{' {
18245                                    depth += 1;
18246                                } else if chars[i] == '}' {
18247                                    depth -= 1;
18248                                    if depth == 0 {
18249                                        break;
18250                                    }
18251                                }
18252                                key.push(chars[i]);
18253                                i += 1;
18254                            }
18255                            if i < chars.len() {
18256                                i += 1;
18257                            } // skip }
18258                            let key_expr = if let Some(rest) = key.strip_prefix('$') {
18259                                Expr {
18260                                    kind: ExprKind::ScalarVar(rest.to_string()),
18261                                    line,
18262                                }
18263                            } else {
18264                                Expr {
18265                                    kind: ExprKind::String(key),
18266                                    line,
18267                                }
18268                            };
18269                            let mut base = Expr {
18270                                kind: ExprKind::HashElement {
18271                                    hash: probe,
18272                                    key: Box::new(key_expr),
18273                                },
18274                                line,
18275                            };
18276                            base = self.interp_chain_subscripts(&chars, &mut i, base, line);
18277                            parts.push(StringPart::Expr(base));
18278                        } else {
18279                            // Check for arrow deref chain: `$@->{key}`, etc.
18280                            let mut base = Expr {
18281                                kind: ExprKind::ScalarVar(probe),
18282                                line,
18283                            };
18284                            base = self.interp_chain_subscripts(&chars, &mut i, base, line);
18285                            if matches!(base.kind, ExprKind::ScalarVar(_)) {
18286                                // No chain extension — use the simpler ScalarVar part
18287                                if let ExprKind::ScalarVar(name) = base.kind {
18288                                    self.no_interop_check_scalar_var_name(&name, line)?;
18289                                    parts.push(StringPart::ScalarVar(name));
18290                                }
18291                            } else {
18292                                parts.push(StringPart::Expr(base));
18293                            }
18294                        }
18295                    } else {
18296                        literal.push('$');
18297                        literal.push(c);
18298                        i += 1;
18299                    }
18300                }
18301            } else if chars[i] == '@' && i + 1 < chars.len() {
18302                let next = chars[i + 1];
18303                // `@$aref` / `@${expr}` — array dereference in interpolation (Perl `"@$r"` → elements of @$r).
18304                if next == '$' {
18305                    if !literal.is_empty() {
18306                        parts.push(StringPart::Literal(std::mem::take(&mut literal)));
18307                    }
18308                    i += 1; // past `@`
18309                    debug_assert_eq!(chars[i], '$');
18310                    i += 1; // past `$`
18311                    while i < chars.len() && chars[i].is_whitespace() {
18312                        i += 1;
18313                    }
18314                    if i >= chars.len() {
18315                        return Err(self.syntax_err(
18316                            "Expected variable or block after `@$` in double-quoted string",
18317                            line,
18318                        ));
18319                    }
18320                    let inner_expr = if chars[i] == '{' {
18321                        i += 1;
18322                        let start = i;
18323                        let mut depth = 1usize;
18324                        while i < chars.len() && depth > 0 {
18325                            match chars[i] {
18326                                '{' => depth += 1,
18327                                '}' => {
18328                                    depth -= 1;
18329                                    if depth == 0 {
18330                                        break;
18331                                    }
18332                                }
18333                                _ => {}
18334                            }
18335                            i += 1;
18336                        }
18337                        if depth != 0 {
18338                            return Err(self.syntax_err(
18339                                "Unterminated `${ ... }` after `@` in double-quoted string",
18340                                line,
18341                            ));
18342                        }
18343                        let inner: String = chars[start..i].iter().collect();
18344                        i += 1; // closing `}`
18345                        parse_expression_from_str(inner.trim(), "-e")?
18346                    } else {
18347                        let mut name = String::new();
18348                        if chars[i] == '^' {
18349                            name.push('^');
18350                            i += 1;
18351                            while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_')
18352                            {
18353                                name.push(chars[i]);
18354                                i += 1;
18355                            }
18356                        } else {
18357                            while i < chars.len()
18358                                && (chars[i].is_alphanumeric()
18359                                    || chars[i] == '_'
18360                                    || chars[i] == ':')
18361                            {
18362                                name.push(chars[i]);
18363                                i += 1;
18364                            }
18365                            while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
18366                                name.push_str("::");
18367                                i += 2;
18368                                while i < chars.len()
18369                                    && (chars[i].is_alphanumeric() || chars[i] == '_')
18370                                {
18371                                    name.push(chars[i]);
18372                                    i += 1;
18373                                }
18374                            }
18375                        }
18376                        if name.is_empty() {
18377                            return Err(self.syntax_err(
18378                                "Expected identifier after `@$` in double-quoted string",
18379                                line,
18380                            ));
18381                        }
18382                        Expr {
18383                            kind: ExprKind::ScalarVar(name),
18384                            line,
18385                        }
18386                    };
18387                    parts.push(StringPart::Expr(Expr {
18388                        kind: ExprKind::Deref {
18389                            expr: Box::new(inner_expr),
18390                            kind: Sigil::Array,
18391                        },
18392                        line,
18393                    }));
18394                    continue 'istr;
18395                }
18396                if next == '{' {
18397                    if !literal.is_empty() {
18398                        parts.push(StringPart::Literal(std::mem::take(&mut literal)));
18399                    }
18400                    i += 2; // `@{`
18401                    let start = i;
18402                    let mut depth = 1usize;
18403                    while i < chars.len() && depth > 0 {
18404                        match chars[i] {
18405                            '{' => depth += 1,
18406                            '}' => {
18407                                depth -= 1;
18408                                if depth == 0 {
18409                                    break;
18410                                }
18411                            }
18412                            _ => {}
18413                        }
18414                        i += 1;
18415                    }
18416                    if depth != 0 {
18417                        return Err(
18418                            self.syntax_err("Unterminated @{ ... } in double-quoted string", line)
18419                        );
18420                    }
18421                    let inner: String = chars[start..i].iter().collect();
18422                    i += 1; // closing `}`
18423                    let inner_expr = parse_expression_from_str(inner.trim(), "-e")?;
18424                    parts.push(StringPart::Expr(Expr {
18425                        kind: ExprKind::Deref {
18426                            expr: Box::new(inner_expr),
18427                            kind: Sigil::Array,
18428                        },
18429                        line,
18430                    }));
18431                    continue 'istr;
18432                }
18433                if !(next.is_alphabetic() || next == '_' || next == '+' || next == '-') {
18434                    literal.push(chars[i]);
18435                    i += 1;
18436                } else {
18437                    if !literal.is_empty() {
18438                        parts.push(StringPart::Literal(std::mem::take(&mut literal)));
18439                    }
18440                    i += 1;
18441                    let mut name = String::new();
18442                    if i < chars.len() && (chars[i] == '+' || chars[i] == '-') {
18443                        name.push(chars[i]);
18444                        i += 1;
18445                    } else {
18446                        while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
18447                            name.push(chars[i]);
18448                            i += 1;
18449                        }
18450                        while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
18451                            name.push_str("::");
18452                            i += 2;
18453                            while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_')
18454                            {
18455                                name.push(chars[i]);
18456                                i += 1;
18457                            }
18458                        }
18459                    }
18460                    if i < chars.len() && chars[i] == '[' {
18461                        i += 1;
18462                        let start_inner = i;
18463                        let mut depth = 1usize;
18464                        while i < chars.len() && depth > 0 {
18465                            match chars[i] {
18466                                '[' => depth += 1,
18467                                ']' => depth -= 1,
18468                                _ => {}
18469                            }
18470                            if depth == 0 {
18471                                let inner: String = chars[start_inner..i].iter().collect();
18472                                i += 1; // closing ]
18473                                let indices = parse_slice_indices_from_str(inner.trim(), "-e")?;
18474                                parts.push(StringPart::Expr(Expr {
18475                                    kind: ExprKind::ArraySlice {
18476                                        array: name.clone(),
18477                                        indices,
18478                                    },
18479                                    line,
18480                                }));
18481                                continue 'istr;
18482                            }
18483                            i += 1;
18484                        }
18485                        return Err(self.syntax_err(
18486                            "Unterminated [ in array slice inside quoted string",
18487                            line,
18488                        ));
18489                    }
18490                    parts.push(StringPart::ArrayVar(name));
18491                }
18492            } else if chars[i] == '#'
18493                && i + 1 < chars.len()
18494                && chars[i + 1] == '{'
18495                && !crate::compat_mode()
18496            {
18497                // #{expr} — Ruby-style expression interpolation (stryke extension).
18498                if !literal.is_empty() {
18499                    parts.push(StringPart::Literal(std::mem::take(&mut literal)));
18500                }
18501                i += 2; // skip `#{`
18502                let mut inner = String::new();
18503                let mut depth = 1usize;
18504                while i < chars.len() && depth > 0 {
18505                    match chars[i] {
18506                        '{' => depth += 1,
18507                        '}' => {
18508                            depth -= 1;
18509                            if depth == 0 {
18510                                break;
18511                            }
18512                        }
18513                        _ => {}
18514                    }
18515                    inner.push(chars[i]);
18516                    i += 1;
18517                }
18518                if i < chars.len() {
18519                    i += 1; // skip closing `}`
18520                }
18521                let expr = parse_block_from_str(inner.trim(), "-e", line)?;
18522                parts.push(StringPart::Expr(expr));
18523            } else {
18524                literal.push(chars[i]);
18525                i += 1;
18526            }
18527        }
18528        if !literal.is_empty() {
18529            parts.push(StringPart::Literal(literal));
18530        }
18531
18532        if parts.len() == 1 {
18533            if let StringPart::Literal(s) = &parts[0] {
18534                return Ok(Expr {
18535                    kind: ExprKind::String(s.clone()),
18536                    line,
18537                });
18538            }
18539        }
18540        if parts.is_empty() {
18541            return Ok(Expr {
18542                kind: ExprKind::String(String::new()),
18543                line,
18544            });
18545        }
18546
18547        Ok(Expr {
18548            kind: ExprKind::InterpolatedString(parts),
18549            line,
18550        })
18551    }
18552
18553    fn expr_to_overload_key(&self, e: &Expr) -> PerlResult<String> {
18554        match &e.kind {
18555            ExprKind::String(s) => Ok(s.clone()),
18556            _ => Err(self.syntax_err(
18557                "overload key must be a string literal (e.g. '\"\"' or '+')",
18558                e.line,
18559            )),
18560        }
18561    }
18562
18563    fn expr_to_overload_sub(&mut self, e: &Expr) -> PerlResult<String> {
18564        match &e.kind {
18565            ExprKind::String(s) => Ok(s.clone()),
18566            ExprKind::Integer(n) => Ok(n.to_string()),
18567            ExprKind::SubroutineRef(s) | ExprKind::SubroutineCodeRef(s) => Ok(s.clone()),
18568            // Anonymous sub: `use overload "+" => sub { ... };` — promote the
18569            // anon body into a synthetic top-level SubDecl so the overload
18570            // table can hold the name like the named-sub case. (PARITY-012)
18571            ExprKind::CodeRef { params, body } => {
18572                let id = self.next_overload_anon_id;
18573                self.next_overload_anon_id = self.next_overload_anon_id.saturating_add(1);
18574                let name = format!("__overload_anon_{}", id);
18575                self.pending_synthetic_subs.push(Statement {
18576                    label: None,
18577                    kind: StmtKind::SubDecl {
18578                        name: name.clone(),
18579                        params: params.clone(),
18580                        body: body.clone(),
18581                        prototype: None,
18582                    },
18583                    line: e.line,
18584                });
18585                Ok(name)
18586            }
18587            _ => Err(self.syntax_err(
18588                "overload handler must be a string literal, number (e.g. fallback => 1), or \\&subname (method in current package)",
18589                e.line,
18590            )),
18591        }
18592    }
18593}
18594
18595fn merge_expr_list(parts: Vec<Expr>) -> Expr {
18596    if parts.len() == 1 {
18597        parts.into_iter().next().unwrap()
18598    } else {
18599        let line = parts.first().map(|e| e.line).unwrap_or(0);
18600        Expr {
18601            kind: ExprKind::List(parts),
18602            line,
18603        }
18604    }
18605}
18606
18607/// Parse a single expression from `s` (e.g. contents of `@{ ... }` inside a double-quoted string).
18608pub fn parse_expression_from_str(s: &str, file: &str) -> PerlResult<Expr> {
18609    let mut lexer = Lexer::new_with_file(s, file);
18610    let tokens = lexer.tokenize()?;
18611    let mut parser = Parser::new_with_file(tokens, file);
18612    let e = parser.parse_expression()?;
18613    if !parser.at_eof() {
18614        return Err(parser.syntax_err(
18615            "Extra tokens in embedded string expression",
18616            parser.peek_line(),
18617        ));
18618    }
18619    Ok(e)
18620}
18621
18622/// Parse a statement list from `s` and wrap as `do { ... }` (for `#{...}` interpolation).
18623pub fn parse_block_from_str(s: &str, file: &str, line: usize) -> PerlResult<Expr> {
18624    let mut lexer = Lexer::new_with_file(s, file);
18625    let tokens = lexer.tokenize()?;
18626    let mut parser = Parser::new_with_file(tokens, file);
18627    let stmts = parser.parse_statements()?;
18628    let inner_line = stmts.first().map(|st| st.line).unwrap_or(line);
18629    let inner = Expr {
18630        kind: ExprKind::CodeRef {
18631            params: vec![],
18632            body: stmts,
18633        },
18634        line: inner_line,
18635    };
18636    Ok(Expr {
18637        kind: ExprKind::Do(Box::new(inner)),
18638        line,
18639    })
18640}
18641
18642/// Comma-separated expressions on a `format` value line (below a picture line).
18643/// Parse `[ ... ]` contents for `@a[...]` (same rules as `parse_arg_list` / comma-separated indices).
18644pub fn parse_slice_indices_from_str(s: &str, file: &str) -> PerlResult<Vec<Expr>> {
18645    let mut lexer = Lexer::new_with_file(s, file);
18646    let tokens = lexer.tokenize()?;
18647    let mut parser = Parser::new_with_file(tokens, file);
18648    parser.parse_arg_list()
18649}
18650
18651pub fn parse_format_value_line(line: &str) -> PerlResult<Vec<Expr>> {
18652    let trimmed = line.trim();
18653    if trimmed.is_empty() {
18654        return Ok(vec![]);
18655    }
18656    let mut lexer = Lexer::new(trimmed);
18657    let tokens = lexer.tokenize()?;
18658    let mut parser = Parser::new(tokens);
18659    let mut exprs = Vec::new();
18660    loop {
18661        if parser.at_eof() {
18662            break;
18663        }
18664        // Assignment-level expressions so `a, b` yields two fields (not one comma list).
18665        exprs.push(parser.parse_assign_expr()?);
18666        if parser.eat(&Token::Comma) {
18667            continue;
18668        }
18669        if !parser.at_eof() {
18670            return Err(parser.syntax_err("Extra tokens in format value line", parser.peek_line()));
18671        }
18672        break;
18673    }
18674    Ok(exprs)
18675}
18676
18677#[cfg(test)]
18678mod tests {
18679    use super::*;
18680
18681    fn parse_ok(code: &str) -> Program {
18682        let mut lexer = Lexer::new(code);
18683        let tokens = lexer.tokenize().expect("tokenize");
18684        let mut parser = Parser::new(tokens);
18685        parser.parse_program().expect("parse")
18686    }
18687
18688    fn parse_err(code: &str) -> String {
18689        let mut lexer = Lexer::new(code);
18690        let tokens = match lexer.tokenize() {
18691            Ok(t) => t,
18692            Err(e) => return e.message,
18693        };
18694        let mut parser = Parser::new(tokens);
18695        parser.parse_program().unwrap_err().message
18696    }
18697
18698    #[test]
18699    fn parse_empty_program() {
18700        let p = parse_ok("");
18701        assert!(p.statements.is_empty());
18702    }
18703
18704    #[test]
18705    fn parse_semicolons_only() {
18706        let p = parse_ok(";;");
18707        assert!(p.statements.len() <= 3);
18708    }
18709
18710    #[test]
18711    fn parse_simple_scalar_assignment() {
18712        let p = parse_ok("$x = 1");
18713        assert_eq!(p.statements.len(), 1);
18714    }
18715
18716    #[test]
18717    fn parse_simple_array_assignment() {
18718        let p = parse_ok("@arr = (1, 2, 3)");
18719        assert_eq!(p.statements.len(), 1);
18720    }
18721
18722    #[test]
18723    fn parse_simple_hash_assignment() {
18724        let p = parse_ok("%h = (a => 1, b => 2)");
18725        assert_eq!(p.statements.len(), 1);
18726    }
18727
18728    #[test]
18729    fn parse_subroutine_decl() {
18730        let p = parse_ok("fn foo { 1 }");
18731        assert_eq!(p.statements.len(), 1);
18732        match &p.statements[0].kind {
18733            StmtKind::SubDecl { name, .. } => assert_eq!(name, "foo"),
18734            _ => panic!("expected SubDecl"),
18735        }
18736    }
18737
18738    #[test]
18739    fn parse_subroutine_with_prototype() {
18740        let p = parse_ok("fn foo ($$) { 1 }");
18741        assert_eq!(p.statements.len(), 1);
18742        match &p.statements[0].kind {
18743            StmtKind::SubDecl { prototype, .. } => {
18744                assert!(prototype.is_some());
18745            }
18746            _ => panic!("expected SubDecl"),
18747        }
18748    }
18749
18750    #[test]
18751    fn parse_anonymous_fn() {
18752        let p = parse_ok("my $f = fn { 1 }");
18753        assert_eq!(p.statements.len(), 1);
18754    }
18755
18756    #[test]
18757    fn parse_if_statement() {
18758        let p = parse_ok("if (1) { 2 }");
18759        assert_eq!(p.statements.len(), 1);
18760        matches!(&p.statements[0].kind, StmtKind::If { .. });
18761    }
18762
18763    #[test]
18764    fn parse_if_elsif_else() {
18765        let p = parse_ok("if (0) { 1 } elsif (1) { 2 } else { 3 }");
18766        assert_eq!(p.statements.len(), 1);
18767    }
18768
18769    #[test]
18770    fn parse_unless_statement() {
18771        let p = parse_ok("unless (0) { 1 }");
18772        assert_eq!(p.statements.len(), 1);
18773    }
18774
18775    #[test]
18776    fn parse_while_loop() {
18777        let p = parse_ok("while ($x) { $x-- }");
18778        assert_eq!(p.statements.len(), 1);
18779    }
18780
18781    #[test]
18782    fn parse_until_loop() {
18783        let p = parse_ok("until ($x) { $x++ }");
18784        assert_eq!(p.statements.len(), 1);
18785    }
18786
18787    #[test]
18788    fn parse_for_c_style() {
18789        let p = parse_ok("for (my $i=0; $i<10; $i++) { 1 }");
18790        assert_eq!(p.statements.len(), 1);
18791    }
18792
18793    #[test]
18794    fn parse_foreach_loop() {
18795        let p = parse_ok("foreach my $x (@arr) { 1 }");
18796        assert_eq!(p.statements.len(), 1);
18797    }
18798
18799    #[test]
18800    fn parse_loop_with_label() {
18801        let p = parse_ok("OUTER: for my $i (1..10) { last OUTER }");
18802        assert_eq!(p.statements.len(), 1);
18803        assert_eq!(p.statements[0].label.as_deref(), Some("OUTER"));
18804    }
18805
18806    #[test]
18807    fn parse_begin_block() {
18808        let p = parse_ok("BEGIN { 1 }");
18809        assert_eq!(p.statements.len(), 1);
18810        matches!(&p.statements[0].kind, StmtKind::Begin(_));
18811    }
18812
18813    #[test]
18814    fn parse_end_block() {
18815        let p = parse_ok("END { 1 }");
18816        assert_eq!(p.statements.len(), 1);
18817        matches!(&p.statements[0].kind, StmtKind::End(_));
18818    }
18819
18820    #[test]
18821    fn parse_package_statement() {
18822        let p = parse_ok("package Foo::Bar");
18823        assert_eq!(p.statements.len(), 1);
18824        match &p.statements[0].kind {
18825            StmtKind::Package { name } => assert_eq!(name, "Foo::Bar"),
18826            _ => panic!("expected Package"),
18827        }
18828    }
18829
18830    #[test]
18831    fn parse_use_statement() {
18832        let p = parse_ok("use strict");
18833        assert_eq!(p.statements.len(), 1);
18834    }
18835
18836    #[test]
18837    fn parse_no_statement() {
18838        let p = parse_ok("no warnings");
18839        assert_eq!(p.statements.len(), 1);
18840    }
18841
18842    #[test]
18843    fn parse_require_bareword() {
18844        let p = parse_ok("require Foo::Bar");
18845        assert_eq!(p.statements.len(), 1);
18846    }
18847
18848    #[test]
18849    fn parse_require_string() {
18850        let p = parse_ok(r#"require "foo.pl""#);
18851        assert_eq!(p.statements.len(), 1);
18852    }
18853
18854    #[test]
18855    fn parse_eval_block() {
18856        let p = parse_ok("eval { 1 }");
18857        assert_eq!(p.statements.len(), 1);
18858    }
18859
18860    #[test]
18861    fn parse_eval_string() {
18862        let p = parse_ok(r#"eval "1 + 2""#);
18863        assert_eq!(p.statements.len(), 1);
18864    }
18865
18866    #[test]
18867    fn parse_qw_word_list() {
18868        let p = parse_ok("my @a = qw(foo bar baz)");
18869        assert_eq!(p.statements.len(), 1);
18870    }
18871
18872    #[test]
18873    fn parse_q_string() {
18874        let p = parse_ok("my $s = q{hello}");
18875        assert_eq!(p.statements.len(), 1);
18876    }
18877
18878    #[test]
18879    fn parse_qq_string() {
18880        let p = parse_ok(r#"my $s = qq(hello $x)"#);
18881        assert_eq!(p.statements.len(), 1);
18882    }
18883
18884    #[test]
18885    fn parse_regex_match() {
18886        let p = parse_ok(r#"$x =~ /foo/"#);
18887        assert_eq!(p.statements.len(), 1);
18888    }
18889
18890    #[test]
18891    fn parse_regex_substitution() {
18892        let p = parse_ok(r#"$x =~ s/foo/bar/g"#);
18893        assert_eq!(p.statements.len(), 1);
18894    }
18895
18896    #[test]
18897    fn parse_transliterate() {
18898        let p = parse_ok(r#"$x =~ tr/a-z/A-Z/"#);
18899        assert_eq!(p.statements.len(), 1);
18900    }
18901
18902    #[test]
18903    fn parse_ternary_operator() {
18904        let p = parse_ok("my $x = $a ? 1 : 2");
18905        assert_eq!(p.statements.len(), 1);
18906    }
18907
18908    #[test]
18909    fn parse_arrow_method_call() {
18910        let p = parse_ok("$obj->method()");
18911        assert_eq!(p.statements.len(), 1);
18912    }
18913
18914    #[test]
18915    fn parse_arrow_deref_hash() {
18916        let p = parse_ok("$r->{key}");
18917        assert_eq!(p.statements.len(), 1);
18918    }
18919
18920    #[test]
18921    fn parse_arrow_deref_array() {
18922        let p = parse_ok("$r->[0]");
18923        assert_eq!(p.statements.len(), 1);
18924    }
18925
18926    #[test]
18927    fn parse_chained_arrow_deref() {
18928        let p = parse_ok("$r->{a}[0]{b}");
18929        assert_eq!(p.statements.len(), 1);
18930    }
18931
18932    #[test]
18933    fn parse_my_multiple_vars() {
18934        let p = parse_ok("my ($a, $b, $c) = (1, 2, 3)");
18935        assert_eq!(p.statements.len(), 1);
18936    }
18937
18938    #[test]
18939    fn parse_our_scalar() {
18940        let p = parse_ok("our $VERSION = '1.0'");
18941        assert_eq!(p.statements.len(), 1);
18942    }
18943
18944    #[test]
18945    fn parse_local_scalar() {
18946        let p = parse_ok("local $/ = undef");
18947        assert_eq!(p.statements.len(), 1);
18948    }
18949
18950    #[test]
18951    fn parse_state_variable() {
18952        let p = parse_ok("fn Test::counter { state $n = 0; $n++ }");
18953        assert_eq!(p.statements.len(), 1);
18954    }
18955
18956    #[test]
18957    fn parse_postfix_if() {
18958        let p = parse_ok("print 1 if $x");
18959        assert_eq!(p.statements.len(), 1);
18960    }
18961
18962    #[test]
18963    fn parse_postfix_unless() {
18964        let p = parse_ok("die 'error' unless $ok");
18965        assert_eq!(p.statements.len(), 1);
18966    }
18967
18968    #[test]
18969    fn parse_postfix_while() {
18970        let p = parse_ok("$x++ while $x < 10");
18971        assert_eq!(p.statements.len(), 1);
18972    }
18973
18974    #[test]
18975    fn parse_postfix_for() {
18976        let p = parse_ok("print for @arr");
18977        assert_eq!(p.statements.len(), 1);
18978    }
18979
18980    #[test]
18981    fn parse_last_next_redo() {
18982        let p = parse_ok("for (@a) { next if $_ < 0; last if $_ > 10 }");
18983        assert_eq!(p.statements.len(), 1);
18984    }
18985
18986    #[test]
18987    fn parse_return_statement() {
18988        let p = parse_ok("fn foo { return 42 }");
18989        assert_eq!(p.statements.len(), 1);
18990    }
18991
18992    #[test]
18993    fn parse_wantarray() {
18994        let p = parse_ok("fn foo { wantarray ? @a : $a }");
18995        assert_eq!(p.statements.len(), 1);
18996    }
18997
18998    #[test]
18999    fn parse_caller_builtin() {
19000        let p = parse_ok("my @c = caller");
19001        assert_eq!(p.statements.len(), 1);
19002    }
19003
19004    #[test]
19005    fn parse_ref_to_array() {
19006        let p = parse_ok("my $r = \\@arr");
19007        assert_eq!(p.statements.len(), 1);
19008    }
19009
19010    #[test]
19011    fn parse_ref_to_hash() {
19012        let p = parse_ok("my $r = \\%hash");
19013        assert_eq!(p.statements.len(), 1);
19014    }
19015
19016    #[test]
19017    fn parse_ref_to_scalar() {
19018        let p = parse_ok("my $r = \\$x");
19019        assert_eq!(p.statements.len(), 1);
19020    }
19021
19022    #[test]
19023    fn parse_deref_scalar() {
19024        let p = parse_ok("my $v = $$r");
19025        assert_eq!(p.statements.len(), 1);
19026    }
19027
19028    #[test]
19029    fn parse_deref_array() {
19030        let p = parse_ok("my @a = @$r");
19031        assert_eq!(p.statements.len(), 1);
19032    }
19033
19034    #[test]
19035    fn parse_deref_hash() {
19036        let p = parse_ok("my %h = %$r");
19037        assert_eq!(p.statements.len(), 1);
19038    }
19039
19040    #[test]
19041    fn parse_blessed_ref() {
19042        let p = parse_ok("bless $r, 'Foo'");
19043        assert_eq!(p.statements.len(), 1);
19044    }
19045
19046    #[test]
19047    fn parse_heredoc_basic() {
19048        let p = parse_ok("my $s = <<END;\nfoo\nEND");
19049        assert_eq!(p.statements.len(), 1);
19050    }
19051
19052    #[test]
19053    fn parse_heredoc_quoted() {
19054        let p = parse_ok("my $s = <<'END';\nfoo\nEND");
19055        assert_eq!(p.statements.len(), 1);
19056    }
19057
19058    #[test]
19059    fn parse_do_block() {
19060        let p = parse_ok("my $x = do { 1 + 2 }");
19061        assert_eq!(p.statements.len(), 1);
19062    }
19063
19064    #[test]
19065    fn parse_do_file() {
19066        let p = parse_ok(r#"do "foo.pl""#);
19067        assert_eq!(p.statements.len(), 1);
19068    }
19069
19070    #[test]
19071    fn parse_map_expression() {
19072        let p = parse_ok("my @b = map { $_ * 2 } @a");
19073        assert_eq!(p.statements.len(), 1);
19074    }
19075
19076    #[test]
19077    fn parse_grep_expression() {
19078        let p = parse_ok("my @b = grep { $_ > 0 } @a");
19079        assert_eq!(p.statements.len(), 1);
19080    }
19081
19082    #[test]
19083    fn parse_sort_expression() {
19084        let p = parse_ok("my @b = sort { $a <=> $b } @a");
19085        assert_eq!(p.statements.len(), 1);
19086    }
19087
19088    #[test]
19089    fn parse_pipe_forward() {
19090        let p = parse_ok("@a |> map { $_ * 2 }");
19091        assert_eq!(p.statements.len(), 1);
19092    }
19093
19094    #[test]
19095    fn parse_expression_from_str_simple() {
19096        let e = parse_expression_from_str("$x + 1", "-e").unwrap();
19097        assert!(matches!(e.kind, ExprKind::BinOp { .. }));
19098    }
19099
19100    #[test]
19101    fn parse_expression_from_str_extra_tokens_error() {
19102        let err = parse_expression_from_str("$x; $y", "-e").unwrap_err();
19103        assert!(err.message.contains("Extra tokens"));
19104    }
19105
19106    #[test]
19107    fn parse_slice_indices_from_str_basic() {
19108        let indices = parse_slice_indices_from_str("0, 1, 2", "-e").unwrap();
19109        assert_eq!(indices.len(), 3);
19110    }
19111
19112    #[test]
19113    fn parse_format_value_line_empty() {
19114        let exprs = parse_format_value_line("").unwrap();
19115        assert!(exprs.is_empty());
19116    }
19117
19118    #[test]
19119    fn parse_format_value_line_single() {
19120        let exprs = parse_format_value_line("$x").unwrap();
19121        assert_eq!(exprs.len(), 1);
19122    }
19123
19124    #[test]
19125    fn parse_format_value_line_multiple() {
19126        let exprs = parse_format_value_line("$a, $b, $c").unwrap();
19127        assert_eq!(exprs.len(), 3);
19128    }
19129
19130    #[test]
19131    fn parse_unclosed_brace_error() {
19132        let err = parse_err("fn foo {");
19133        assert!(!err.is_empty());
19134    }
19135
19136    #[test]
19137    fn parse_unclosed_paren_error() {
19138        let err = parse_err("print (1, 2");
19139        assert!(!err.is_empty());
19140    }
19141
19142    #[test]
19143    fn parse_invalid_statement_error() {
19144        let err = parse_err("???");
19145        assert!(!err.is_empty());
19146    }
19147
19148    #[test]
19149    fn merge_expr_list_single() {
19150        let e = Expr {
19151            kind: ExprKind::Integer(1),
19152            line: 1,
19153        };
19154        let merged = merge_expr_list(vec![e.clone()]);
19155        matches!(merged.kind, ExprKind::Integer(1));
19156    }
19157
19158    #[test]
19159    fn merge_expr_list_multiple() {
19160        let e1 = Expr {
19161            kind: ExprKind::Integer(1),
19162            line: 1,
19163        };
19164        let e2 = Expr {
19165            kind: ExprKind::Integer(2),
19166            line: 1,
19167        };
19168        let merged = merge_expr_list(vec![e1, e2]);
19169        matches!(merged.kind, ExprKind::List(_));
19170    }
19171}