Skip to main content

stryke/
parser.rs

1use crate::ast::*;
2use crate::error::{ErrorKind, StrykeError, StrykeResult};
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/// `Parser` — see fields for layout.
82pub struct Parser {
83    /// `tokens` field.
84    tokens: Vec<(Token, usize)>,
85    /// `pos` field.
86    pos: usize,
87    /// Monotonic slot id for `rate_limit(...)` sliding-window state in the interpreter.
88    next_rate_limit_slot: u32,
89    /// When > 0, `expr` `(` is not parsed as [`ExprKind::IndirectCall`] — e.g. `sort $k (1)` must
90    /// treat `(1)` as the sort list, not `$k(1)`.
91    suppress_indirect_paren_call: u32,
92    /// When > 0, the current expression is being parsed as the RHS of `|>`
93    /// (pipe-forward). Builtins that normally require a list/string/second arg
94    /// (`map`, `grep`, `sort`, `join`, `reverse` / `reversed`, `split`, …) may accept a
95    /// placeholder when this flag is set, because [`Self::pipe_forward_apply`]
96    /// will substitute the piped value in afterwards.
97    pipe_rhs_depth: u32,
98    /// When > 0 we are parsing inside a `{ … }` block (function body, `map`/`grep`,
99    /// `for`, `if`, anonymous coderef, etc.). Inside any block, bare `_` is a topic
100    /// reference (`$_[0]`/`$_`), so `my $i = _` means "capture the topic" and must
101    /// NOT be auto-wrapped as an implicit zero-arg coderef. Only at the true top
102    /// level (depth 0 — module scope) is `_` unbound, allowing `my $f = _ * 2` to
103    /// parse as `my $f = fn { _ * 2 }`. Bumped in [`Self::parse_block`].
104    block_depth: u32,
105    /// When > 0, [`Self::parse_pipe_forward`] will **not** consume a trailing `|>`
106    /// and leaves it for an outer parser instead. Bumped while parsing paren-less
107    /// arg lists (`parse_list_until_terminator`, paren-less method args, `map`/`grep`
108    /// LIST, …) so `@a |> head 2 |> join "-"` chains left-associatively as
109    /// `(@a |> head 2) |> join "-"` instead of `head` swallowing the outer `|>`
110    /// as part of its first arg. Reset to 0 on entry to any parenthesized
111    /// arg list (`parse_arg_list`) so `head(2 |> foo, 3)` still works.
112    no_pipe_forward_depth: u32,
113    /// When > 0, `{` after a scalar / scalar deref is not `%hash{key}` / `->{}`, so
114    /// `if let` / `while let` scrutinees can be followed by `{ ... }`.
115    suppress_scalar_hash_brace: u32,
116    /// Counter for `while let` / similar desugar temps (`$__while_let_0`, …).
117    next_desugar_tmp: u32,
118    /// Source path for [`StrykeError`] (matches lexer / `parse_with_file`).
119    error_file: String,
120    /// User-declared sub names (for allowing UDF to shadow stryke extensions in compat mode).
121    declared_subs: std::collections::HashSet<String>,
122    /// When > 0, `parse_named_expr` will not consume following barewords as paren-less
123    /// function arguments. Used by thread macro to prevent `t Color::Red p` from
124    /// interpreting `p` as an argument to the enum constructor instead of a stage.
125    suppress_parenless_call: u32,
126    /// Pre-built input expression for the next `parse_thread_macro_inner`
127    /// call. Used by `~p>` continuation parsing (`||>` / `|then|`) to
128    /// thread the par_reduce result into a normal `~>` continuation
129    /// without re-parsing a source expression.
130    pending_thread_input: Option<Expr>,
131    /// When > 0, `parse_multiplication` will not consume `Token::Slash` as division.
132    /// Used by thread macro so `/pattern/` is left for the stage parser to handle.
133    suppress_slash_as_div: u32,
134    /// When > 0, the lexer should not interpret `m/`, `s/`, etc. as regex-starters.
135    /// Used by thread macro to prevent `/m/` from being misparsed.
136    pub suppress_m_regex: u32,
137    /// When > 0, `parse_range` will not consume `:` as the short-form range operator.
138    /// Bumped while parsing the then-branch of a ternary `? :` so `a ? b : c` doesn't
139    /// misparse `b : c` as a range.
140    suppress_colon_range: u32,
141    /// Counter (depth-tracked like [`Self::suppress_colon_range`]) that
142    /// disables `~` as a range separator. Used inside paired `~...~` char-
143    /// index/slice subscripts so the closing `~` doesn't get eaten as a
144    /// range op. `:` range is still allowed inside (e.g. `$_~1:3~` is a
145    /// slice with a `:` range as the index).
146    suppress_tilde_range: u32,
147    /// When true, `pipe_forward_apply` uses thread-last semantics (append to args)
148    /// instead of thread-first (prepend). Set by `->>` thread macro.
149    thread_last_mode: bool,
150    /// When true, we're parsing a module (via `use`/`require`), not user code.
151    /// Modules are allowed to shadow builtins; user code is not (unless `--compat`).
152    pub parsing_module: bool,
153    /// `self.pos` immediately after consuming a paren-list close (`(EXPR)`,
154    /// `(EXPR, …)`, `()`) or `qw(…)` in `parse_primary`. The `x` operator
155    /// reads this at parse time to distinguish `(LIST) x N` (list repetition)
156    /// from `EXPR x N` (scalar string repetition). The compare is exact: any
157    /// postfix consumption (`->method()`, `[idx]`, …) advances `self.pos`
158    /// past this checkpoint, so list-repeat fires only when `x` is the very
159    /// next token after the closing paren.
160    list_construct_close_pos: Option<usize>,
161    /// Synthetic SubDecl statements queued by anonymous-sub overload handlers
162    /// (`use overload "+" => sub { ... }`) — drained at the end of
163    /// [`Self::parse_program`] and prepended to the top-level statements so
164    /// the package-qualified synthetic name resolves at runtime. (PARITY-012)
165    pending_synthetic_subs: Vec<Statement>,
166    /// Counter for unique anonymous-overload-handler names.
167    next_overload_anon_id: u32,
168    /// Token-vector indices where the lexer emitted a *bare* positional alias
169    /// (`_`, `_0`, `_1`, …) — i.e. without a leading `$` sigil. Populated by
170    /// [`crate::lexer::Lexer::tokenize`]. Consulted by [`Self::parse_my_our_local`]
171    /// to auto-wrap an RHS expression that contains free positional aliases
172    /// into an implicit zero-arg coderef, so `my $f = _ * 2` ≡
173    /// `my $f = fn { _ * 2 }`.
174    pub bare_positional_indices: std::collections::HashSet<usize>,
175    /// Current package context — updated by `parse_package`. Defaults to
176    /// `"main"`. Used by [`Self::check_udf_shadows_builtin`] to allow
177    /// `fn name(...)` inside `package Foo` to shadow stryke builtins
178    /// (the bare `name` becomes `Foo::name`, so the builtin remains
179    /// reachable via the unqualified call from outside the package).
180    current_package: String,
181}
182
183impl Parser {
184    /// `new` — see implementation.
185    pub fn new(tokens: Vec<(Token, usize)>) -> Self {
186        Self::new_with_file(tokens, "-e")
187    }
188    /// `new_with_file` — see implementation.
189    pub fn new_with_file(tokens: Vec<(Token, usize)>, file: impl Into<String>) -> Self {
190        Self {
191            tokens,
192            pos: 0,
193            next_rate_limit_slot: 0,
194            suppress_indirect_paren_call: 0,
195            pipe_rhs_depth: 0,
196            no_pipe_forward_depth: 0,
197            suppress_scalar_hash_brace: 0,
198            next_desugar_tmp: 0,
199            error_file: file.into(),
200            declared_subs: std::collections::HashSet::new(),
201            suppress_parenless_call: 0,
202            pending_thread_input: None,
203            suppress_slash_as_div: 0,
204            suppress_m_regex: 0,
205            suppress_colon_range: 0,
206            suppress_tilde_range: 0,
207            thread_last_mode: false,
208            pending_synthetic_subs: Vec::new(),
209            next_overload_anon_id: 0,
210            parsing_module: false,
211            list_construct_close_pos: None,
212            bare_positional_indices: std::collections::HashSet::new(),
213            block_depth: 0,
214            current_package: "main".to_string(),
215        }
216    }
217
218    fn alloc_desugar_tmp(&mut self) -> u32 {
219        let n = self.next_desugar_tmp;
220        self.next_desugar_tmp = self.next_desugar_tmp.saturating_add(1);
221        n
222    }
223
224    /// True when we are currently parsing the RHS of a `|>` pipe-forward.
225    /// Used by builtins (`map`, `grep`, `sort`, `join`, …) to supply a
226    /// placeholder list instead of erroring on a missing operand.
227    #[inline]
228    fn in_pipe_rhs(&self) -> bool {
229        self.pipe_rhs_depth > 0
230    }
231
232    /// List-slurping builtin: the operand is entirely the LHS of `|>` (no following list tokens).
233    /// A newline after the builtin name also terminates the pipe stage (implicit semicolon).
234    fn pipe_supplies_slurped_list_operand(&self) -> bool {
235        self.in_pipe_rhs()
236            && (matches!(
237                self.peek(),
238                Token::Semicolon
239                    | Token::RBrace
240                    | Token::RParen
241                    | Token::Eof
242                    | Token::Comma
243                    | Token::PipeForward
244            ) || self.peek_line() > self.prev_line())
245    }
246
247    /// Empty placeholder list used as a stand-in for the list operand of
248    /// list-taking builtins when they appear on the RHS of `|>`.
249    /// [`Self::pipe_forward_apply`] rewrites this slot with the actual piped
250    /// value at desugar time, so the placeholder is never evaluated.
251    #[inline]
252    fn pipe_placeholder_list(&self, line: usize) -> Expr {
253        Expr {
254            kind: ExprKind::List(vec![]),
255            line,
256        }
257    }
258
259    /// List builtins that take `{ BLOCK }, LIST` and accept the threaded list at
260    /// `args[1]` via [`Self::pipe_forward_apply`]. Used by both the pipe-forward
261    /// dispatcher and `parse_thread_stage_with_block` so `~> @a NAME { ... }` and
262    /// `@a |> NAME { ... }` route through the same substitution.
263    fn is_block_then_list_pipe_builtin(name: &str) -> bool {
264        matches!(
265            name,
266            "pfirst"
267                | "pany"
268                | "any"
269                | "all"
270                | "none"
271                | "first"
272                | "find_index"
273                | "firstidx"
274                | "first_index"
275                | "take_while"
276                | "drop_while"
277                | "skip_while"
278                | "reject"
279                | "grepv"
280                | "tap"
281                | "peek"
282                | "group_by"
283                | "chunk_by"
284                | "partition"
285                | "min_by"
286                | "max_by"
287                | "zip_with"
288                | "count_by"
289        )
290    }
291
292    /// Lift a `Bareword("f")` to `FuncCall { f, [$_] }`.
293    ///
294    /// stryke extension contexts (map/grep/fore expression forms, pipe-forward)
295    /// call this so that `map sha512, @list` invokes `sha512($_)` for each
296    /// element instead of stringifying the bareword.  Non-bareword expressions
297    /// pass through unchanged.
298    ///
299    /// Also injects `$_` into known builtins that were parsed with zero
300    /// arguments (e.g. `fore unlink`, `map stat`) so they operate on the
301    /// topic variable instead of being no-ops.
302    fn lift_bareword_to_topic_call(expr: Expr) -> Expr {
303        let line = expr.line;
304        let topic = || Expr {
305            kind: ExprKind::ScalarVar("_".into()),
306            line,
307        };
308        match expr.kind {
309            ExprKind::Bareword(ref name) => Expr {
310                kind: ExprKind::FuncCall {
311                    name: name.clone(),
312                    args: vec![topic()],
313                },
314                line,
315            },
316            // Builtins that take Vec<Expr> args — inject $_ when empty.
317            ExprKind::Unlink(ref args) if args.is_empty() => Expr {
318                kind: ExprKind::Unlink(vec![topic()]),
319                line,
320            },
321            ExprKind::Chmod(ref args) if args.is_empty() => Expr {
322                kind: ExprKind::Chmod(vec![topic()]),
323                line,
324            },
325            // Builtins that take Box<Expr> — inject $_ when arg is implicit.
326            ExprKind::Stat(_) => expr,
327            ExprKind::Lstat(_) => expr,
328            ExprKind::Readlink(_) => expr,
329            // rev with empty list should use $_
330            ExprKind::Rev(ref inner) => {
331                if matches!(inner.kind, ExprKind::List(ref v) if v.is_empty()) {
332                    Expr {
333                        kind: ExprKind::Rev(Box::new(topic())),
334                        line,
335                    }
336                } else {
337                    expr
338                }
339            }
340            _ => expr,
341        }
342    }
343
344    /// `parse_assign_expr` with `no_pipe_forward_depth` bumped for the
345    /// duration, so any trailing `|>` is left to the enclosing parser instead
346    /// of being absorbed into this sub-expression. Used by paren-less arg
347    /// parsers (`parse_list_until_terminator`, `chunked`/`windowed` paren-less,
348    /// paren-less method args, …) so `@a |> head 2 |> join "-"` chains
349    /// left-associatively instead of letting `head`'s first arg swallow the
350    /// outer `|>`. The counter is restored on both success and error paths.
351    fn parse_assign_expr_stop_at_pipe(&mut self) -> StrykeResult<Expr> {
352        self.no_pipe_forward_depth = self.no_pipe_forward_depth.saturating_add(1);
353        let r = self.parse_assign_expr();
354        self.no_pipe_forward_depth = self.no_pipe_forward_depth.saturating_sub(1);
355        r
356    }
357
358    fn syntax_err(&self, message: impl Into<String>, line: usize) -> StrykeError {
359        StrykeError::new(ErrorKind::Syntax, message, line, self.error_file.clone())
360    }
361
362    /// Coderef-in-block-position helper for tier-2 list builtins (`any`,
363    /// `all`, `none`, `first`, `take_while`, …). Returns `Some([f, list])`
364    /// when the next tokens look like `$f [,] LIST` (or `$f` alone in
365    /// pipe-RHS); `None` when the caller should fall through to the block
366    /// form. The first arg is any coderef-shaped expression — runtime
367    /// checks `as_code_ref()` and dispatches.
368    fn try_parse_coderef_listop_args(&mut self, line: usize) -> StrykeResult<Option<Vec<Expr>>> {
369        if !matches!(self.peek(), Token::ScalarVar(_) | Token::Backslash) {
370            return Ok(None);
371        }
372        let f = self.parse_assign_expr_stop_at_pipe()?;
373        let _ = self.eat(&Token::Comma);
374        let list = if self.in_pipe_rhs()
375            && matches!(
376                self.peek(),
377                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
378            ) {
379            self.pipe_placeholder_list(line)
380        } else {
381            self.parse_expression()?
382        };
383        Ok(Some(vec![f, list]))
384    }
385
386    fn alloc_rate_limit_slot(&mut self) -> u32 {
387        let s = self.next_rate_limit_slot;
388        self.next_rate_limit_slot = self.next_rate_limit_slot.saturating_add(1);
389        s
390    }
391
392    fn peek(&self) -> &Token {
393        self.tokens
394            .get(self.pos)
395            .map(|(t, _)| t)
396            .unwrap_or(&Token::Eof)
397    }
398
399    fn peek_line(&self) -> usize {
400        self.tokens.get(self.pos).map(|(_, l)| *l).unwrap_or(0)
401    }
402
403    fn peek_at(&self, offset: usize) -> &Token {
404        self.tokens
405            .get(self.pos + offset)
406            .map(|(t, _)| t)
407            .unwrap_or(&Token::Eof)
408    }
409
410    fn advance(&mut self) -> (Token, usize) {
411        let tok = self
412            .tokens
413            .get(self.pos)
414            .cloned()
415            .unwrap_or((Token::Eof, 0));
416        self.pos += 1;
417        tok
418    }
419
420    /// Line number of the most recently consumed token (the token at `pos - 1`).
421    fn prev_line(&self) -> usize {
422        if self.pos > 0 {
423            self.tokens.get(self.pos - 1).map(|(_, l)| *l).unwrap_or(0)
424        } else {
425            0
426        }
427    }
428
429    /// Check if `{ ... }` starting at current position looks like a hashref rather than a block.
430    /// Heuristics (assuming current token is `{`):
431    /// - `{ bareword =>` → hashref
432    /// - `{ "string" =>` → hashref
433    /// - `{ $var =>` → hashref
434    /// - `{ 0 =>` → hashref (numeric key)
435    /// - `{ %hash }` or `{ %hash, ...}` → hashref (spread)
436    /// - `{ }` (empty) → hashref
437    fn looks_like_hashref(&self) -> bool {
438        debug_assert!(matches!(self.peek(), Token::LBrace));
439        let tok1 = self.peek_at(1);
440        let tok2 = self.peek_at(2);
441        match tok1 {
442            Token::RBrace => true,
443            Token::Ident(_)
444            | Token::SingleString(_)
445            | Token::DoubleString(_)
446            | Token::ScalarVar(_)
447            | Token::Integer(_) => matches!(tok2, Token::FatArrow),
448            Token::HashVar(_) => matches!(tok2, Token::RBrace | Token::Comma),
449            _ => false,
450        }
451    }
452
453    fn expect(&mut self, expected: &Token) -> StrykeResult<usize> {
454        let (tok, line) = self.advance();
455        if std::mem::discriminant(&tok) == std::mem::discriminant(expected) {
456            Ok(line)
457        } else {
458            Err(self.syntax_err(format!("Expected {:?}, got {:?}", expected, tok), line))
459        }
460    }
461
462    fn eat(&mut self, expected: &Token) -> bool {
463        if std::mem::discriminant(self.peek()) == std::mem::discriminant(expected) {
464            self.advance();
465            true
466        } else {
467            false
468        }
469    }
470
471    fn at_eof(&self) -> bool {
472        matches!(self.peek(), Token::Eof)
473    }
474
475    /// True when a file test (`-d`, `-f`, …) may omit its operand and use `$_` (Perl filetest default).
476    fn filetest_allows_implicit_topic(tok: &Token) -> bool {
477        matches!(
478            tok,
479            Token::RParen
480                | Token::Semicolon
481                | Token::Comma
482                | Token::RBrace
483                | Token::Eof
484                | Token::LogAnd
485                | Token::LogOr
486                | Token::LogAndWord
487                | Token::LogOrWord
488                | Token::PipeForward
489        )
490    }
491
492    /// True when the next token is a statement-starting keyword on a *different*
493    /// line from `stmt_line`.  Used by `parse_use` / `parse_no` to stop parsing
494    /// import lists when semicolons are omitted (stryke extension).
495    fn next_is_new_stmt_keyword(&self, stmt_line: usize) -> bool {
496        // Semicolons-optional is a stryke extension; in compat mode, require them.
497        if crate::compat_mode() {
498            return false;
499        }
500        if self.peek_line() == stmt_line {
501            return false;
502        }
503        matches!(
504            self.peek(),
505            Token::Ident(ref kw) if matches!(kw.as_str(),
506                "use" | "no" | "my" | "our" | "local" | "sub" | "struct" | "enum"
507                | "if" | "unless" | "while" | "until" | "for" | "foreach"
508                | "return" | "last" | "next" | "redo" | "package" | "require"
509                | "BEGIN" | "END" | "UNITCHECK" | "frozen" | "const" | "typed"
510                // stryke-specific declaration keywords that start a new
511                // statement on a fresh line. Without these, a bare `use
512                // strict` / `use warnings` followed by `fn foo { ... }`
513                // on the next line swallows `foo` as an import argument.
514                | "fn" | "class" | "abstract" | "final" | "trait"
515                | "state" | "mysync" | "oursync" | "var" | "val"
516            )
517        )
518    }
519
520    /// True when the next token is on a different line from `stmt_line` and could
521    /// start a new statement. More permissive than `next_is_new_stmt_keyword` —
522    /// includes sigil-prefixed variables like `$var`, `@arr`, `%hash`.
523    fn next_is_new_statement_start(&self, stmt_line: usize) -> bool {
524        if crate::compat_mode() {
525            return false;
526        }
527        if self.peek_line() == stmt_line {
528            return false;
529        }
530        matches!(
531            self.peek(),
532            Token::ScalarVar(_)
533                | Token::DerefScalarVar(_)
534                | Token::ArrayVar(_)
535                | Token::HashVar(_)
536                | Token::LBrace
537        ) || self.next_is_new_stmt_keyword(stmt_line)
538    }
539
540    // ── Top level ──
541    /// `parse_program` — see implementation.
542    pub fn parse_program(&mut self) -> StrykeResult<Program> {
543        let mut statements = self.parse_statements()?;
544        // Prepend any synthetic SubDecl stubs queued by anonymous overload
545        // handlers so the package-qualified synthetic names resolve when the
546        // overload table is consulted at runtime. (PARITY-012)
547        if !self.pending_synthetic_subs.is_empty() {
548            let synthetics = std::mem::take(&mut self.pending_synthetic_subs);
549            let mut combined = Vec::with_capacity(synthetics.len() + statements.len());
550            combined.extend(synthetics);
551            combined.append(&mut statements);
552            statements = combined;
553        }
554        Ok(Program { statements })
555    }
556
557    /// Parse statements until EOF. Used by parse_program and parse_block_from_str.
558    pub fn parse_statements(&mut self) -> StrykeResult<Vec<Statement>> {
559        let mut statements = Vec::new();
560        while !self.at_eof() {
561            if matches!(self.peek(), Token::Semicolon) {
562                let line = self.peek_line();
563                self.advance();
564                statements.push(Statement {
565                    label: None,
566                    kind: StmtKind::Empty,
567                    line,
568                });
569                continue;
570            }
571            statements.push(self.parse_statement()?);
572        }
573        Ok(statements)
574    }
575
576    // ── Statements ──
577
578    fn parse_statement(&mut self) -> StrykeResult<Statement> {
579        let line = self.peek_line();
580
581        // Statement label `FOO:` / `boot:` / `BAR_BAZ:` (not `Foo::` — that is `Ident` + `::`).
582        // Uppercase-only was too strict: XSLoader.pm uses `boot:` before `my $xs = ...`.
583        let label = match self.peek().clone() {
584            Token::Ident(_) => {
585                if matches!(self.peek_at(1), Token::Colon)
586                    && !matches!(self.peek_at(2), Token::Colon)
587                {
588                    let (tok, _) = self.advance();
589                    let l = match tok {
590                        Token::Ident(l) => l,
591                        _ => unreachable!(),
592                    };
593                    self.advance(); // ':'
594                    Some(l)
595                } else {
596                    None
597                }
598            }
599            _ => None,
600        };
601
602        let mut stmt = match self.peek().clone() {
603            Token::FormatDecl { .. } => {
604                let tok_line = self.peek_line();
605                let (tok, _) = self.advance();
606                match tok {
607                    Token::FormatDecl { name, lines } => Statement {
608                        label: label.clone(),
609                        kind: StmtKind::FormatDecl { name, lines },
610                        line: tok_line,
611                    },
612                    _ => unreachable!(),
613                }
614            }
615            Token::Ident(ref kw) => match kw.as_str() {
616                "if" => self.parse_if()?,
617                "unless" => self.parse_unless()?,
618                "while" => {
619                    let mut s = self.parse_while()?;
620                    if let StmtKind::While {
621                        label: ref mut lbl, ..
622                    } = s.kind
623                    {
624                        *lbl = label.clone();
625                    }
626                    s
627                }
628                "until" => {
629                    let mut s = self.parse_until()?;
630                    if let StmtKind::Until {
631                        label: ref mut lbl, ..
632                    } = s.kind
633                    {
634                        *lbl = label.clone();
635                    }
636                    s
637                }
638                "for" => {
639                    let mut s = self.parse_for_or_foreach()?;
640                    match s.kind {
641                        StmtKind::For {
642                            label: ref mut lbl, ..
643                        }
644                        | StmtKind::Foreach {
645                            label: ref mut lbl, ..
646                        } => *lbl = label.clone(),
647                        _ => {}
648                    }
649                    s
650                }
651                "foreach" => {
652                    let mut s = self.parse_foreach()?;
653                    if let StmtKind::Foreach {
654                        label: ref mut lbl, ..
655                    } = s.kind
656                    {
657                        *lbl = label.clone();
658                    }
659                    s
660                }
661                "sub" => {
662                    if crate::no_interop_mode() {
663                        return Err(self.syntax_err(
664                            "stryke uses `fn` instead of `sub` (--no-interop is active)",
665                            self.peek_line(),
666                        ));
667                    }
668                    self.parse_sub_decl(true)?
669                }
670                "fn" => self.parse_sub_decl(false)?,
671                "struct" => {
672                    if crate::compat_mode() {
673                        return Err(self.syntax_err(
674                            "`struct` is a stryke extension (disabled by --compat)",
675                            self.peek_line(),
676                        ));
677                    }
678                    self.parse_struct_decl()?
679                }
680                "enum" => {
681                    if crate::compat_mode() {
682                        return Err(self.syntax_err(
683                            "`enum` is a stryke extension (disabled by --compat)",
684                            self.peek_line(),
685                        ));
686                    }
687                    self.parse_enum_decl()?
688                }
689                "class" => {
690                    if crate::compat_mode() {
691                        // TODO: parse Perl 5.38 class syntax with :isa()
692                        return Err(self.syntax_err(
693                            "Perl 5.38 `class` syntax not yet implemented in --compat mode",
694                            self.peek_line(),
695                        ));
696                    }
697                    self.parse_class_decl(false, false)?
698                }
699                "abstract" => {
700                    self.advance(); // abstract
701                    if !matches!(self.peek(), Token::Ident(ref s) if s == "class") {
702                        return Err(self.syntax_err(
703                            "`abstract` must be followed by `class`",
704                            self.peek_line(),
705                        ));
706                    }
707                    self.parse_class_decl(true, false)?
708                }
709                "final" => {
710                    self.advance(); // final
711                    if !matches!(self.peek(), Token::Ident(ref s) if s == "class") {
712                        return Err(self
713                            .syntax_err("`final` must be followed by `class`", self.peek_line()));
714                    }
715                    self.parse_class_decl(false, true)?
716                }
717                "trait" => {
718                    if crate::compat_mode() {
719                        return Err(self.syntax_err(
720                            "`trait` is a stryke extension (disabled by --compat)",
721                            self.peek_line(),
722                        ));
723                    }
724                    self.parse_trait_decl()?
725                }
726                "my" => self.parse_my_our_local("my", false)?,
727                // `var $x = …` — Kotlin/Scala/Java-style synonym for `my`.
728                // Parses identically (same StmtKind::My output, same scoping
729                // rules) so existing tooling, error messages, and bytecode
730                // emission don't fork. Consume the `var` token then dispatch
731                // through `parse_my_our_local("my", false)` which advances
732                // past what it thinks is `my`.
733                "var" => {
734                    if crate::compat_mode() {
735                        return Err(self.syntax_err(
736                            "`var` is a stryke extension (disabled by --compat)",
737                            self.peek_line(),
738                        ));
739                    }
740                    self.parse_my_our_local("my", false)?
741                }
742                // `val $x = …` — Kotlin/Scala-style synonym for `const my`.
743                // Same desugaring as `const my $x = …`: parse as `my`,
744                // then mark every decl as frozen so reassignment is a
745                // compile-time error. Type annotations (`val $x : Int = …`)
746                // permitted on the same grounds as `const my`.
747                "val" => {
748                    if crate::compat_mode() {
749                        return Err(self.syntax_err(
750                            "`val` is a stryke extension (disabled by --compat)",
751                            self.peek_line(),
752                        ));
753                    }
754                    let mut stmt = self.parse_my_our_local("my", true)?;
755                    if let StmtKind::My(ref mut decls) = stmt.kind {
756                        for decl in decls.iter_mut() {
757                            decl.frozen = true;
758                        }
759                    }
760                    stmt
761                }
762                "state" => self.parse_my_our_local("state", false)?,
763                "mysync" => {
764                    if crate::compat_mode() {
765                        return Err(self.syntax_err(
766                            "`mysync` is a stryke extension (disabled by --compat)",
767                            self.peek_line(),
768                        ));
769                    }
770                    self.parse_my_our_local("mysync", false)?
771                }
772                "oursync" => {
773                    if crate::compat_mode() {
774                        return Err(self.syntax_err(
775                            "`oursync` is a stryke extension (disabled by --compat)",
776                            self.peek_line(),
777                        ));
778                    }
779                    self.parse_my_our_local("oursync", false)?
780                }
781                "frozen" | "const" => {
782                    let leading = kw.as_str().to_string();
783                    if crate::compat_mode() {
784                        return Err(self.syntax_err(
785                            format!("`{leading}` is a stryke extension (disabled by --compat)"),
786                            self.peek_line(),
787                        ));
788                    }
789                    // `frozen my $x = val;` / `const my $x = val;` — the
790                    // two spellings are interchangeable (`const` is the
791                    // more-familiar name for new users). Expects `my`
792                    // to follow.
793                    self.advance(); // consume "frozen"/"const"
794                    if let Token::Ident(ref kw) = self.peek().clone() {
795                        if kw == "my" {
796                            // Accept type annotations the same way `typed
797                            // my $x : Int` does — `const`/`frozen` is
798                            // orthogonal to typing, and `: Type` after a
799                            // name is unambiguous in either form.
800                            let mut stmt = self.parse_my_our_local("my", true)?;
801                            if let StmtKind::My(ref mut decls) = stmt.kind {
802                                for decl in decls.iter_mut() {
803                                    decl.frozen = true;
804                                }
805                            }
806                            stmt
807                        } else {
808                            return Err(self.syntax_err(
809                                format!("Expected 'my' after '{leading}'"),
810                                self.peek_line(),
811                            ));
812                        }
813                    } else {
814                        return Err(self.syntax_err(
815                            format!("Expected 'my' after '{leading}'"),
816                            self.peek_line(),
817                        ));
818                    }
819                }
820                "typed" => {
821                    if crate::compat_mode() {
822                        return Err(self.syntax_err(
823                            "`typed` is a stryke extension (disabled by --compat)",
824                            self.peek_line(),
825                        ));
826                    }
827                    self.advance();
828                    if let Token::Ident(ref kw) = self.peek().clone() {
829                        if kw == "my" {
830                            self.parse_my_our_local("my", true)?
831                        } else {
832                            return Err(
833                                self.syntax_err("Expected 'my' after 'typed'", self.peek_line())
834                            );
835                        }
836                    } else {
837                        return Err(
838                            self.syntax_err("Expected 'my' after 'typed'", self.peek_line())
839                        );
840                    }
841                }
842                "our" => self.parse_my_our_local("our", false)?,
843                "local" => self.parse_my_our_local("local", false)?,
844                "package" => self.parse_package()?,
845                "use" => self.parse_use()?,
846                "no" => self.parse_no()?,
847                "return" => self.parse_return()?,
848                "last" => {
849                    self.advance();
850                    let lbl = self.try_take_loop_label();
851                    let stmt = Statement {
852                        label: None,
853                        kind: StmtKind::Last(lbl.or(label.clone())),
854                        line,
855                    };
856                    self.parse_stmt_postfix_modifier(stmt)?
857                }
858                "next" => {
859                    self.advance();
860                    let lbl = self.try_take_loop_label();
861                    let stmt = Statement {
862                        label: None,
863                        kind: StmtKind::Next(lbl.or(label.clone())),
864                        line,
865                    };
866                    self.parse_stmt_postfix_modifier(stmt)?
867                }
868                "redo" => {
869                    self.advance();
870                    let lbl = self.try_take_loop_label();
871                    let stmt = Statement {
872                        label: None,
873                        kind: StmtKind::Redo(lbl.or(label.clone())),
874                        line,
875                    };
876                    self.parse_stmt_postfix_modifier(stmt)?
877                }
878                "BEGIN" => {
879                    self.advance();
880                    let block = self.parse_block()?;
881                    Statement {
882                        label: None,
883                        kind: StmtKind::Begin(block),
884                        line,
885                    }
886                }
887                "END" => {
888                    self.advance();
889                    let block = self.parse_block()?;
890                    Statement {
891                        label: None,
892                        kind: StmtKind::End(block),
893                        line,
894                    }
895                }
896                "UNITCHECK" => {
897                    self.advance();
898                    let block = self.parse_block()?;
899                    Statement {
900                        label: None,
901                        kind: StmtKind::UnitCheck(block),
902                        line,
903                    }
904                }
905                "CHECK" => {
906                    self.advance();
907                    let block = self.parse_block()?;
908                    Statement {
909                        label: None,
910                        kind: StmtKind::Check(block),
911                        line,
912                    }
913                }
914                "INIT" => {
915                    self.advance();
916                    let block = self.parse_block()?;
917                    Statement {
918                        label: None,
919                        kind: StmtKind::Init(block),
920                        line,
921                    }
922                }
923                "goto" => {
924                    self.advance();
925                    let target = self.parse_expression()?;
926                    let stmt = Statement {
927                        label: None,
928                        kind: StmtKind::Goto {
929                            target: Box::new(target),
930                        },
931                        line,
932                    };
933                    // `goto $l if COND;` / `goto &$cr if defined &$cr;` (XSLoader.pm)
934                    self.parse_stmt_postfix_modifier(stmt)?
935                }
936                "continue" => {
937                    self.advance();
938                    let block = self.parse_block()?;
939                    Statement {
940                        label: None,
941                        kind: StmtKind::Continue(block),
942                        line,
943                    }
944                }
945                "before"
946                    if matches!(
947                        self.peek_at(1),
948                        Token::SingleString(_) | Token::DoubleString(_)
949                    ) =>
950                {
951                    self.parse_advice_decl(crate::ast::AdviceKind::Before)?
952                }
953                "after"
954                    if matches!(
955                        self.peek_at(1),
956                        Token::SingleString(_) | Token::DoubleString(_)
957                    ) =>
958                {
959                    self.parse_advice_decl(crate::ast::AdviceKind::After)?
960                }
961                "around"
962                    if matches!(
963                        self.peek_at(1),
964                        Token::SingleString(_) | Token::DoubleString(_)
965                    ) =>
966                {
967                    self.parse_advice_decl(crate::ast::AdviceKind::Around)?
968                }
969                "try" => self.parse_try_catch()?,
970                "defer" => self.parse_defer_stmt()?,
971                "tie" => self.parse_tie_stmt()?,
972                "given" => self.parse_given()?,
973                "when" => self.parse_when_stmt()?,
974                "default" => self.parse_default_stmt()?,
975                "eval_timeout" => self.parse_eval_timeout()?,
976                "do" => {
977                    if matches!(self.peek_at(1), Token::LBrace) {
978                        self.advance();
979                        let body = self.parse_block()?;
980                        if let Token::Ident(ref w) = self.peek().clone() {
981                            if w == "while" {
982                                self.advance();
983                                self.expect(&Token::LParen)?;
984                                let mut condition = self.parse_expression()?;
985                                Self::mark_match_scalar_g_for_boolean_condition(&mut condition);
986                                self.expect(&Token::RParen)?;
987                                self.eat(&Token::Semicolon);
988                                Statement {
989                                    label: label.clone(),
990                                    kind: StmtKind::DoWhile { body, condition },
991                                    line,
992                                }
993                            } else {
994                                let inner_line = body.first().map(|s| s.line).unwrap_or(line);
995                                let inner = Expr {
996                                    kind: ExprKind::CodeRef {
997                                        params: vec![],
998                                        body,
999                                    },
1000                                    line: inner_line,
1001                                };
1002                                let expr = Expr {
1003                                    kind: ExprKind::Do(Box::new(inner)),
1004                                    line,
1005                                };
1006                                let stmt = Statement {
1007                                    label: label.clone(),
1008                                    kind: StmtKind::Expression(expr),
1009                                    line,
1010                                };
1011                                // `do { } if EXPR` / `do { } unless EXPR` — postfix modifier, not a new `if (` statement.
1012                                self.parse_stmt_postfix_modifier(stmt)?
1013                            }
1014                        } else {
1015                            let inner_line = body.first().map(|s| s.line).unwrap_or(line);
1016                            let inner = Expr {
1017                                kind: ExprKind::CodeRef {
1018                                    params: vec![],
1019                                    body,
1020                                },
1021                                line: inner_line,
1022                            };
1023                            let expr = Expr {
1024                                kind: ExprKind::Do(Box::new(inner)),
1025                                line,
1026                            };
1027                            let stmt = Statement {
1028                                label: label.clone(),
1029                                kind: StmtKind::Expression(expr),
1030                                line,
1031                            };
1032                            self.parse_stmt_postfix_modifier(stmt)?
1033                        }
1034                    } else {
1035                        if let Some(expr) = self.try_parse_bareword_stmt_call() {
1036                            let stmt = self.maybe_postfix_modifier(expr)?;
1037                            self.parse_stmt_postfix_modifier(stmt)?
1038                        } else {
1039                            let expr = self.parse_expression()?;
1040                            let stmt = self.maybe_postfix_modifier(expr)?;
1041                            self.parse_stmt_postfix_modifier(stmt)?
1042                        }
1043                    }
1044                }
1045                _ => {
1046                    // `foo;` or `{ foo }` — bareword statement is a zero-arg call (topic `$_` at runtime).
1047                    if let Some(expr) = self.try_parse_bareword_stmt_call() {
1048                        let stmt = self.maybe_postfix_modifier(expr)?;
1049                        self.parse_stmt_postfix_modifier(stmt)?
1050                    } else {
1051                        let expr = self.parse_expression()?;
1052                        let stmt = self.maybe_postfix_modifier(expr)?;
1053                        self.parse_stmt_postfix_modifier(stmt)?
1054                    }
1055                }
1056            },
1057            Token::LBrace => {
1058                // Disambiguate hashref `{ k => v }` from block `{ stmt; stmt }`.
1059                // If it looks like a hashref, parse as expression; otherwise parse as block.
1060                if self.looks_like_hashref() {
1061                    let expr = self.parse_expression()?;
1062                    let stmt = self.maybe_postfix_modifier(expr)?;
1063                    self.parse_stmt_postfix_modifier(stmt)?
1064                } else {
1065                    let block = self.parse_block()?;
1066                    let stmt = Statement {
1067                        label: None,
1068                        kind: StmtKind::Block(block),
1069                        line,
1070                    };
1071                    // `{ … } if EXPR` / `{ … } unless EXPR` — same postfix rule as `do { } if …` (not `if (`).
1072                    self.parse_stmt_postfix_modifier(stmt)?
1073                }
1074            }
1075            _ => {
1076                let expr = self.parse_expression()?;
1077                let stmt = self.maybe_postfix_modifier(expr)?;
1078                self.parse_stmt_postfix_modifier(stmt)?
1079            }
1080        };
1081
1082        stmt.label = label;
1083        Ok(stmt)
1084    }
1085
1086    /// Consume an immediately-following loop label after `next`/`last`/`redo`.
1087    /// Matches identifiers that look like Perl loop labels — uppercase letters,
1088    /// digits, or `_`, and must start with a non-digit. Anything else
1089    /// (lowercase function names, `if`, `unless`, `(EXPR`, …) is left for the
1090    /// `EXPR`-form / postfix-modifier paths.
1091    fn try_take_loop_label(&mut self) -> Option<String> {
1092        let Token::Ident(s) = self.peek() else {
1093            return None;
1094        };
1095        let mut chars = s.chars();
1096        let first = chars.next()?;
1097        if !(first.is_ascii_uppercase() || first == '_') {
1098            return None;
1099        }
1100        let ok = s
1101            .chars()
1102            .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_');
1103        if !ok {
1104            return None;
1105        }
1106        let (Token::Ident(l), _) = self.advance() else {
1107            unreachable!()
1108        };
1109        Some(l)
1110    }
1111
1112    /// Handle postfix if/unless on statement-level keywords like last/next.
1113    fn parse_stmt_postfix_modifier(&mut self, stmt: Statement) -> StrykeResult<Statement> {
1114        let line = stmt.line;
1115        // Implicit semicolon: a modifier keyword on a new line is a new
1116        // statement, not a postfix modifier.  This prevents semicolon-less
1117        // code like `my $x = "val"\nif ($x) { ... }` from being mis-parsed
1118        // as `my $x = "val" if ($x) { ... }`.
1119        if self.peek_line() > self.prev_line() {
1120            self.eat(&Token::Semicolon);
1121            return Ok(stmt);
1122        }
1123        if let Token::Ident(ref kw) = self.peek().clone() {
1124            match kw.as_str() {
1125                "if" => {
1126                    self.advance();
1127                    let mut cond = self.parse_expression()?;
1128                    Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
1129                    self.eat(&Token::Semicolon);
1130                    return Ok(Statement {
1131                        label: None,
1132                        kind: StmtKind::If {
1133                            condition: cond,
1134                            body: vec![stmt],
1135                            elsifs: vec![],
1136                            else_block: None,
1137                        },
1138                        line,
1139                    });
1140                }
1141                "unless" => {
1142                    self.advance();
1143                    let mut cond = self.parse_expression()?;
1144                    Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
1145                    self.eat(&Token::Semicolon);
1146                    return Ok(Statement {
1147                        label: None,
1148                        kind: StmtKind::Unless {
1149                            condition: cond,
1150                            body: vec![stmt],
1151                            else_block: None,
1152                        },
1153                        line,
1154                    });
1155                }
1156                "while" | "until" | "for" | "foreach" => {
1157                    // `do { } for @a` / `{ } while COND` — same postfix forms as [`maybe_postfix_modifier`],
1158                    // not a new `for (` / `while (` statement (which would require `(` after `for`).
1159                    if let Some(expr) = Self::stmt_into_postfix_body_expr(stmt) {
1160                        let out = self.maybe_postfix_modifier(expr)?;
1161                        self.eat(&Token::Semicolon);
1162                        return Ok(out);
1163                    }
1164                    return Err(self.syntax_err(
1165                        format!("postfix `{}` is not supported on this statement form", kw),
1166                        self.peek_line(),
1167                    ));
1168                }
1169                // `{ } pmap @a` / `{ } pflat_map @a` / `{ } pfor @a` / `do { } …` — same shapes as prefix forms.
1170                "pmap" | "pflat_map" | "pgrep" | "pfor" | "preduce" | "pcache" | "par" => {
1171                    let line = stmt.line;
1172                    let block = self.stmt_into_parallel_block(stmt)?;
1173                    let which = kw.as_str();
1174                    self.advance();
1175                    self.eat(&Token::Comma);
1176                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
1177                    self.eat(&Token::Semicolon);
1178                    let list = Box::new(list);
1179                    let progress = progress.map(Box::new);
1180                    let kind = match which {
1181                        "pmap" => ExprKind::PMapExpr {
1182                            block,
1183                            list,
1184                            progress,
1185                            flat_outputs: false,
1186                            on_cluster: None,
1187                            stream: false,
1188                        },
1189                        "pflat_map" => ExprKind::PMapExpr {
1190                            block,
1191                            list,
1192                            progress,
1193                            flat_outputs: true,
1194                            on_cluster: None,
1195                            stream: false,
1196                        },
1197                        "pgrep" => ExprKind::PGrepExpr {
1198                            block,
1199                            list,
1200                            progress,
1201                            stream: false,
1202                        },
1203                        "pfor" => ExprKind::PForExpr {
1204                            block,
1205                            list,
1206                            progress,
1207                        },
1208                        "preduce" => ExprKind::PReduceExpr {
1209                            block,
1210                            list,
1211                            progress,
1212                        },
1213                        "pcache" => ExprKind::PcacheExpr {
1214                            block,
1215                            list,
1216                            progress,
1217                        },
1218                        "par" => ExprKind::ParExpr { block, list },
1219                        _ => unreachable!(),
1220                    };
1221                    return Ok(Statement {
1222                        label: None,
1223                        kind: StmtKind::Expression(Expr { kind, line }),
1224                        line,
1225                    });
1226                }
1227                _ => {}
1228            }
1229        }
1230        self.eat(&Token::Semicolon);
1231        Ok(stmt)
1232    }
1233
1234    /// Block body for postfix `pmap` / `pfor` / … — bare `{ }`, `do { }`, or any expression
1235    /// statement (wrapped as a one-line block, e.g. `` `cmd` pfor @a ``).
1236    fn stmt_into_parallel_block(&self, stmt: Statement) -> StrykeResult<Block> {
1237        let line = stmt.line;
1238        match stmt.kind {
1239            StmtKind::Block(block) => Ok(block),
1240            StmtKind::Expression(expr) => {
1241                if let ExprKind::Do(ref inner) = expr.kind {
1242                    if let ExprKind::CodeRef { ref body, .. } = inner.kind {
1243                        return Ok(body.clone());
1244                    }
1245                }
1246                Ok(vec![Statement {
1247                    label: None,
1248                    kind: StmtKind::Expression(expr),
1249                    line,
1250                }])
1251            }
1252            _ => Err(self.syntax_err(
1253                "postfix parallel op expects `do { }`, a bare `{ }` block, or an expression statement",
1254                line,
1255            )),
1256        }
1257    }
1258
1259    /// `StmtKind::Expression` or a bare block (`StmtKind::Block`) as an [`Expr`] for postfix
1260    /// `while` / `until` / `for` / `foreach` (mirrors `do { }` → [`ExprKind::Do`]\([`ExprKind::CodeRef`]\)).
1261    fn stmt_into_postfix_body_expr(stmt: Statement) -> Option<Expr> {
1262        match stmt.kind {
1263            StmtKind::Expression(expr) => Some(expr),
1264            StmtKind::Block(block) => {
1265                let line = stmt.line;
1266                let inner = Expr {
1267                    kind: ExprKind::CodeRef {
1268                        params: vec![],
1269                        body: block,
1270                    },
1271                    line,
1272                };
1273                Some(Expr {
1274                    kind: ExprKind::Do(Box::new(inner)),
1275                    line,
1276                })
1277            }
1278            _ => None,
1279        }
1280    }
1281
1282    /// Statement-modifier keywords that must not be consumed as part of a comma-separated list
1283    /// (same set as [`parse_list_until_terminator`]).
1284    fn peek_is_postfix_stmt_modifier_keyword(&self) -> bool {
1285        matches!(
1286            self.peek(),
1287            Token::Ident(ref kw)
1288                if matches!(
1289                    kw.as_str(),
1290                    "if" | "unless" | "while" | "until" | "for" | "foreach"
1291                )
1292        )
1293    }
1294
1295    /// Token classes whose precedence sits below a Perl-style named unary
1296    /// operator. When one of these is the next token after a unary keyword
1297    /// (`length`, `len`, `cnt`, …), the keyword takes no explicit argument
1298    /// and the surrounding expression continues. Mirrors the `parse_one_arg_or_default`
1299    /// boundary set; kept as a separate predicate so other parse paths can
1300    /// reuse it without committing to default-to-`$_` semantics.
1301    fn peek_is_named_unary_terminator(&self) -> bool {
1302        matches!(
1303            self.peek(),
1304            Token::Semicolon
1305                | Token::RBrace
1306                | Token::RParen
1307                | Token::RBracket
1308                | Token::Eof
1309                | Token::Comma
1310                | Token::FatArrow
1311                | Token::PipeForward
1312                | Token::Question
1313                | Token::Colon
1314                | Token::NumEq
1315                | Token::NumNe
1316                | Token::NumLt
1317                | Token::NumGt
1318                | Token::NumLe
1319                | Token::NumGe
1320                | Token::Spaceship
1321                | Token::StrEq
1322                | Token::StrNe
1323                | Token::StrLt
1324                | Token::StrGt
1325                | Token::StrLe
1326                | Token::StrGe
1327                | Token::StrCmp
1328                | Token::LogAnd
1329                | Token::LogOr
1330                | Token::LogAndWord
1331                | Token::LogOrWord
1332                | Token::DefinedOr
1333                | Token::Range
1334                | Token::RangeExclusive
1335                | Token::Assign
1336                | Token::PlusAssign
1337                | Token::MinusAssign
1338                | Token::MulAssign
1339                | Token::DivAssign
1340                | Token::ModAssign
1341                | Token::PowAssign
1342                | Token::DotAssign
1343                | Token::AndAssign
1344                | Token::OrAssign
1345                | Token::XorAssign
1346                | Token::DefinedOrAssign
1347                | Token::ShiftLeftAssign
1348                | Token::ShiftRightAssign
1349                | Token::BitAndAssign
1350                | Token::BitOrAssign
1351        )
1352    }
1353
1354    fn maybe_postfix_modifier(&mut self, expr: Expr) -> StrykeResult<Statement> {
1355        let line = expr.line;
1356        // Implicit semicolon: modifier keyword on a new line starts a new statement.
1357        if self.peek_line() > self.prev_line() {
1358            return Ok(Statement {
1359                label: None,
1360                kind: StmtKind::Expression(expr),
1361                line,
1362            });
1363        }
1364        match self.peek() {
1365            Token::Ident(ref kw) => match kw.as_str() {
1366                "if" => {
1367                    self.advance();
1368                    let cond = self.parse_expression()?;
1369                    Ok(Statement {
1370                        label: None,
1371                        kind: StmtKind::Expression(Expr {
1372                            kind: ExprKind::PostfixIf {
1373                                expr: Box::new(expr),
1374                                condition: Box::new(cond),
1375                            },
1376                            line,
1377                        }),
1378                        line,
1379                    })
1380                }
1381                "unless" => {
1382                    self.advance();
1383                    let cond = self.parse_expression()?;
1384                    Ok(Statement {
1385                        label: None,
1386                        kind: StmtKind::Expression(Expr {
1387                            kind: ExprKind::PostfixUnless {
1388                                expr: Box::new(expr),
1389                                condition: Box::new(cond),
1390                            },
1391                            line,
1392                        }),
1393                        line,
1394                    })
1395                }
1396                "while" => {
1397                    self.advance();
1398                    let cond = self.parse_expression()?;
1399                    Ok(Statement {
1400                        label: None,
1401                        kind: StmtKind::Expression(Expr {
1402                            kind: ExprKind::PostfixWhile {
1403                                expr: Box::new(expr),
1404                                condition: Box::new(cond),
1405                            },
1406                            line,
1407                        }),
1408                        line,
1409                    })
1410                }
1411                "until" => {
1412                    self.advance();
1413                    let cond = self.parse_expression()?;
1414                    Ok(Statement {
1415                        label: None,
1416                        kind: StmtKind::Expression(Expr {
1417                            kind: ExprKind::PostfixUntil {
1418                                expr: Box::new(expr),
1419                                condition: Box::new(cond),
1420                            },
1421                            line,
1422                        }),
1423                        line,
1424                    })
1425                }
1426                "for" | "foreach" => {
1427                    self.advance();
1428                    let list = self.parse_expression()?;
1429                    Ok(Statement {
1430                        label: None,
1431                        kind: StmtKind::Expression(Expr {
1432                            kind: ExprKind::PostfixForeach {
1433                                expr: Box::new(expr),
1434                                list: Box::new(list),
1435                            },
1436                            line,
1437                        }),
1438                        line,
1439                    })
1440                }
1441                _ => Ok(Statement {
1442                    label: None,
1443                    kind: StmtKind::Expression(expr),
1444                    line,
1445                }),
1446            },
1447            _ => Ok(Statement {
1448                label: None,
1449                kind: StmtKind::Expression(expr),
1450                line,
1451            }),
1452        }
1453    }
1454
1455    /// `name;` or `name}` — a bare identifier statement is a sub call with no explicit args (`$_` implied).
1456    fn try_parse_bareword_stmt_call(&mut self) -> Option<Expr> {
1457        let saved = self.pos;
1458        let line = self.peek_line();
1459        let mut name = match self.peek() {
1460            Token::Ident(n) => n.clone(),
1461            _ => return None,
1462        };
1463        // Names that begin `parse_named_expr` (builtins / `undef` / …) must use that path, not a sub call.
1464        if name.starts_with('\x00') || !Self::bareword_stmt_may_be_sub(&name) {
1465            return None;
1466        }
1467        self.advance();
1468        while self.eat(&Token::PackageSep) {
1469            match self.advance() {
1470                (Token::Ident(part), _) => {
1471                    name = format!("{}::{}", name, part);
1472                }
1473                _ => {
1474                    self.pos = saved;
1475                    return None;
1476                }
1477            }
1478        }
1479        match self.peek() {
1480            Token::Semicolon | Token::RBrace => Some(Expr {
1481                kind: ExprKind::FuncCall { name, args: vec![] },
1482                line,
1483            }),
1484            _ => {
1485                self.pos = saved;
1486                None
1487            }
1488        }
1489    }
1490
1491    /// Map an operator-keyword token (the lexer converts `eq`, `ne`, …, `and`,
1492    /// `or`, `not`, `x` to dedicated tokens) back to its identifier spelling.
1493    /// Used in hash-key contexts where the bareword form is the user's intent.
1494    pub(crate) fn operator_keyword_to_ident_str(tok: &Token) -> Option<&'static str> {
1495        Some(match tok {
1496            Token::StrEq => "eq",
1497            Token::StrNe => "ne",
1498            Token::StrLt => "lt",
1499            Token::StrGt => "gt",
1500            Token::StrLe => "le",
1501            Token::StrGe => "ge",
1502            Token::StrCmp => "cmp",
1503            Token::LogAndWord => "and",
1504            Token::LogOrWord => "or",
1505            Token::LogNotWord => "not",
1506            Token::X => "x",
1507            _ => return None,
1508        })
1509    }
1510
1511    /// Bare names that resolve to the topic-slot scalar matrix:
1512    /// `_`, `_0`, `_1`, …, `_N`, plus `_<+`, `_N<+` for the 4-deep outer chain.
1513    /// These must NOT be treated as zero-arg sub calls — they're scalar var refs.
1514    pub(crate) fn is_underscore_topic_slot(name: &str) -> bool {
1515        if name == "_" {
1516            return true;
1517        }
1518        if !name.starts_with('_') || name.len() < 2 {
1519            return false;
1520        }
1521        let bytes = name.as_bytes();
1522        let mut i = 1;
1523        // Optional digit run (positional slot index).
1524        while i < bytes.len() && bytes[i].is_ascii_digit() {
1525            i += 1;
1526        }
1527        // Then any number of `<` chevrons (runtime cap at 5; lexer accepts more).
1528        let chevrons_start = i;
1529        while i < bytes.len() && bytes[i] == b'<' {
1530            i += 1;
1531        }
1532        // Must be one of: `_`, `_N`, `_<+`, `_N<+`. No other trailing chars.
1533        i == bytes.len() && (i > 1 || chevrons_start > 1)
1534    }
1535
1536    /// Bareword names that map to Perl special variables / filehandles /
1537    /// compile-time tokens. A user-defined sub with any of these names
1538    /// would shadow the special variable's expression-position usage and
1539    /// produce silently-broken code. Reject at parse time with a
1540    /// foot-gun error message.
1541    ///
1542    /// Sigil-form spellings (`$@`, `$!`, `@ARGV`, `%ENV`, etc.) are caught
1543    /// separately via the `parse_sub_decl` catch-all branch — those don't
1544    /// even lex as `Token::Ident` so they hit a different code path.
1545    pub(crate) fn is_reserved_special_var_name(name: &str) -> bool {
1546        matches!(
1547            name,
1548            // Standard filehandles (Perl: STDIN, STDOUT, STDERR, ARGV, …)
1549            "STDIN" | "STDOUT" | "STDERR" | "ARGV" | "ARGVOUT" | "DATA"
1550            // Package globals, normally accessed via sigils (@ARGV, %ENV,
1551            // @INC, %SIG, @ISA, %ENV, etc.) — bareword shadow is a foot-gun.
1552            // NOTE: `AUTOLOAD` is intentionally NOT in this list — `fn
1553            // AUTOLOAD { ... }` is the legitimate Perl idiom for handling
1554            // missing-method dispatch. The runtime sets `$AUTOLOAD` to the
1555            // missing sub's qualified name before invoking the user's
1556            // AUTOLOAD sub. Adding it here would break that mechanism.
1557            | "ENV" | "INC" | "SIG" | "ISA"
1558            | "EXPORT" | "EXPORT_OK" | "EXPORT_TAGS"
1559            | "VERSION"
1560            // Compile-time tokens (resolve to constants at parse time).
1561            | "__FILE__" | "__LINE__" | "__PACKAGE__" | "__SUB__"
1562            | "__DATA__" | "__END__"
1563        )
1564    }
1565
1566    /// Identifiers that start a [`parse_named_expr`] arm (builtins / special forms), not a bare sub call.
1567    fn bareword_stmt_may_be_sub(name: &str) -> bool {
1568        // Topic-slot scalar names (`_`, `_N`, `_<+`, `_N<+`) are scalar
1569        // variables, not zero-arg sub calls. Without this guard, the
1570        // statement-position parser would emit `Op::Call("_0", 0)` and fail
1571        // at runtime with "Undefined subroutine &_0".
1572        if Self::is_underscore_topic_slot(name) {
1573            return false;
1574        }
1575        !matches!(
1576            name,
1577            "__FILE__"
1578                | "__LINE__"
1579                | "__PACKAGE__"
1580                | "__SUB__"
1581                | "abs"
1582                | "async"
1583                | "spawn"
1584                | "atan2"
1585                | "await"
1586                | "barrier"
1587                | "bless"
1588                | "burp"
1589                | "caller"
1590                | "capture"
1591                | "cat"
1592                | "chdir"
1593                | "chmod"
1594                | "chomp"
1595                | "chop"
1596                | "chr"
1597                | "chown"
1598                | "closedir"
1599                | "close"
1600                | "collect"
1601                | "cos"
1602                | "crypt"
1603                | "defined"
1604                | "dec"
1605                | "delete"
1606                | "die"
1607                | "deque"
1608                | "do"
1609                | "each"
1610                | "eof"
1611                | "fore"
1612                | "eval"
1613                | "exec"
1614                | "exists"
1615                | "exit"
1616                | "exp"
1617                | "fan"
1618                | "fan_cap"
1619                | "fc"
1620                | "fetch_url"
1621                | "d"
1622                | "dirs"
1623                | "dr"
1624                | "f"
1625                | "fi"
1626                | "files"
1627                | "filesf"
1628                | "filter"
1629                | "fr"
1630                | "getcwd"
1631                | "glob_par"
1632                | "par_sed"
1633                | "glob"
1634                | "god"
1635                | "grep"
1636                | "greps"
1637                | "heap"
1638                | "hex"
1639                | "inc"
1640                | "index"
1641                | "int"
1642                | "join"
1643                | "keys"
1644                | "lcfirst"
1645                | "lc"
1646                | "length"
1647                | "link"
1648                | "log"
1649                | "lstat"
1650                | "map"
1651                | "flat_map"
1652                | "maps"
1653                | "flat_maps"
1654                | "flatten"
1655                | "frequencies"
1656                | "freq"
1657                | "pfrequencies"
1658                | "pfreq"
1659                | "interleave"
1660                | "ddump"
1661                | "stringify"
1662                | "str"
1663                | "s"
1664                | "input"
1665                | "lines"
1666                | "words"
1667                | "chars"
1668                | "digits"
1669                | "letters"
1670                | "letters_uc"
1671                | "letters_lc"
1672                | "punctuation"
1673                | "sentences"
1674                | "paragraphs"
1675                | "sections"
1676                | "numbers"
1677                | "graphemes"
1678                | "columns"
1679                | "trim"
1680                | "avg"
1681                | "top"
1682                | "pager"
1683                | "pg"
1684                | "less"
1685                | "count_by"
1686                | "to_file"
1687                | "to_json"
1688                | "to_csv"
1689                | "grep_v"
1690                | "select_keys"
1691                | "pluck"
1692                | "clamp"
1693                | "normalize"
1694                | "stddev"
1695                | "squared"
1696                | "square"
1697                | "cubed"
1698                | "cube"
1699                | "expt"
1700                | "pow"
1701                | "pw"
1702                | "snake_case"
1703                | "camel_case"
1704                | "kebab_case"
1705                | "to_toml"
1706                | "to_yaml"
1707                | "to_xml"
1708                | "to_html"
1709                | "to_markdown"
1710                | "xopen"
1711                | "clip"
1712                | "paste"
1713                | "to_table"
1714                | "sparkline"
1715                | "bar_chart"
1716                | "flame"
1717                | "set"
1718                | "list_count"
1719                | "list_size"
1720                | "count"
1721                | "size"
1722                | "cnt"
1723                | "len"
1724                | "all"
1725                | "any"
1726                | "none"
1727                | "take_while"
1728                | "drop_while"
1729                | "skip_while"
1730                | "skip"
1731                | "first_or"
1732                | "tap"
1733                | "peek"
1734                | "partition"
1735                | "min_by"
1736                | "max_by"
1737                | "zip_with"
1738                | "group_by"
1739                | "chunk_by"
1740                | "with_index"
1741                | "puniq"
1742                | "pfirst"
1743                | "pany"
1744                | "uniq"
1745                | "distinct"
1746                | "shuffle"
1747                | "shuffled"
1748                | "chunked"
1749                | "windowed"
1750                | "match"
1751                | "mkdir"
1752                | "every"
1753                | "gen"
1754                | "oct"
1755                | "open"
1756                | "p"
1757                | "opendir"
1758                | "ord"
1759                | "par"
1760                | "par_lines"
1761                | "par_walk"
1762                | "pipe"
1763                | "pipes"
1764                | "block_devices"
1765                | "char_devices"
1766                | "exe"
1767                | "executables"
1768                | "rate_limit"
1769                | "retry"
1770                | "pcache"
1771                | "pchannel"
1772                | "pfor"
1773                | "pgrep"
1774                | "pgreps"
1775                | "pipeline"
1776                | "pmap_chunked"
1777                | "pmap_reduce"
1778                | "par_reduce"
1779                | "pmap_on"
1780                | "pflat_map_on"
1781                | "pmap"
1782                | "pmaps"
1783                | "pflat_map"
1784                | "pflat_maps"
1785                | "pop"
1786                | "pos"
1787                | "ppool"
1788                | "preduce_init"
1789                | "preduce"
1790                | "pselect"
1791                | "printf"
1792                | "print"
1793                | "pr"
1794                | "psort"
1795                | "push"
1796                | "pwatch"
1797                | "rand"
1798                | "readdir"
1799                | "readlink"
1800                | "reduce"
1801                | "fold"
1802                | "inject"
1803                | "first"
1804                | "detect"
1805                | "find"
1806                | "find_all"
1807                | "find_index"
1808                | "firstidx"
1809                | "first_index"
1810                | "ref"
1811                | "rename"
1812                | "require"
1813                | "rev"
1814                | "reverse"
1815                | "reversed"
1816                | "rewinddir"
1817                | "rindex"
1818                | "rmdir"
1819                | "rm"
1820                | "say"
1821                | "scalar"
1822                | "seekdir"
1823                | "shift"
1824                | "sin"
1825                | "slurp"
1826                | "swallow"
1827                | "ingest"
1828                | "sockets"
1829                | "sort"
1830                | "splice"
1831                | "splice_last"
1832                | "splice1"
1833                | "spl_last"
1834                | "split"
1835                | "sprintf"
1836                | "sqrt"
1837                | "srand"
1838                | "stat"
1839                | "study"
1840                | "substr"
1841                | "symlink"
1842                | "sym_links"
1843                | "system"
1844                | "telldir"
1845                | "timer"
1846                | "trace"
1847                | "ucfirst"
1848                | "uc"
1849                | "undef"
1850                | "umask"
1851                | "unlink"
1852                | "unshift"
1853                | "utime"
1854                | "values"
1855                | "wantarray"
1856                | "warn"
1857                | "watch"
1858                | "yield"
1859                | "sub"
1860        )
1861    }
1862
1863    fn parse_block(&mut self) -> StrykeResult<Block> {
1864        self.expect(&Token::LBrace)?;
1865        // Statements inside a block are NOT pipe RHS - reset depth so nested `~>`
1866        // parses its own input instead of using `$_[0]` placeholder.
1867        let saved_pipe_rhs_depth = self.pipe_rhs_depth;
1868        self.pipe_rhs_depth = 0;
1869        self.block_depth += 1;
1870        let mut stmts = Vec::new();
1871        // `{ |$a, $b| body }` — Ruby-style block params.
1872        // Desugars to `my $a = $_` (1 param), `my $a = $a; my $b = $b` (2 — sort/reduce),
1873        // or `my $p = $_N` for positional N≥3.
1874        if let Some(param_stmts) = self.try_parse_block_params()? {
1875            stmts.extend(param_stmts);
1876        }
1877        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
1878            if self.eat(&Token::Semicolon) {
1879                continue;
1880            }
1881            stmts.push(self.parse_statement()?);
1882        }
1883        self.expect(&Token::RBrace)?;
1884        self.pipe_rhs_depth = saved_pipe_rhs_depth;
1885        self.block_depth -= 1;
1886        Self::default_topic_for_sole_bareword(&mut stmts);
1887        Ok(stmts)
1888    }
1889
1890    /// Try to parse `|$var1, $var2, ...|` at the start of a block.
1891    /// Returns `None` if the leading `|` is not block-param syntax.
1892    /// When successful, returns `my $var = <implicit>` assignment statements
1893    /// that alias the block's positional arguments.
1894    fn try_parse_block_params(&mut self) -> StrykeResult<Option<Vec<Statement>>> {
1895        if !matches!(self.peek(), Token::BitOr) {
1896            return Ok(None);
1897        }
1898        // Lookahead: `| $scalar [, $scalar]* |` — verify before consuming.
1899        let mut i = 1; // skip the opening `|`
1900        loop {
1901            match self.peek_at(i) {
1902                Token::ScalarVar(_) => i += 1,
1903                _ => return Ok(None), // not `|$var...|`
1904            }
1905            match self.peek_at(i) {
1906                Token::BitOr => break,  // closing `|`
1907                Token::Comma => i += 1, // more params
1908                _ => return Ok(None),   // not block params
1909            }
1910        }
1911        // Confirmed — consume and build assignments.
1912        let line = self.peek_line();
1913        self.advance(); // eat opening `|`
1914        let mut names = Vec::new();
1915        loop {
1916            if let Token::ScalarVar(ref name) = self.peek().clone() {
1917                names.push(name.clone());
1918                self.advance();
1919            }
1920            if self.eat(&Token::BitOr) {
1921                break;
1922            }
1923            self.expect(&Token::Comma)?;
1924        }
1925        // Generate `my $name = <source>` for each param.
1926        // 1 param  → source is `$_` (map/grep/each/for topic)
1927        // 2 params → sources are `$a`, `$b` (sort/reduce)
1928        // N params → sources are `$_`, `$_1`, `$_2`, … (positional)
1929        let sources: Vec<&str> = match names.len() {
1930            1 => vec!["_"],
1931            2 => vec!["a", "b"],
1932            n => {
1933                // Can't return borrowed from a generated vec, handle below.
1934                let _ = n;
1935                vec![] // sentinel — handled in the else branch
1936            }
1937        };
1938        let mut stmts = Vec::with_capacity(names.len());
1939        if !sources.is_empty() {
1940            for (name, src) in names.iter().zip(sources.iter()) {
1941                stmts.push(Statement {
1942                    label: None,
1943                    kind: StmtKind::My(vec![VarDecl {
1944                        sigil: Sigil::Scalar,
1945                        name: name.clone(),
1946                        initializer: Some(Expr {
1947                            kind: ExprKind::ScalarVar(src.to_string()),
1948                            line,
1949                        }),
1950                        frozen: false,
1951                        type_annotation: None,
1952                        list_context: false,
1953                    }]),
1954                    line,
1955                });
1956            }
1957        } else {
1958            // N≥3: positional `$_`, `$_1`, `$_2`, …
1959            for (idx, name) in names.iter().enumerate() {
1960                let src = if idx == 0 {
1961                    "_".to_string()
1962                } else {
1963                    format!("_{idx}")
1964                };
1965                stmts.push(Statement {
1966                    label: None,
1967                    kind: StmtKind::My(vec![VarDecl {
1968                        sigil: Sigil::Scalar,
1969                        name: name.clone(),
1970                        initializer: Some(Expr {
1971                            kind: ExprKind::ScalarVar(src),
1972                            line,
1973                        }),
1974                        frozen: false,
1975                        type_annotation: None,
1976                        list_context: false,
1977                    }]),
1978                    line,
1979                });
1980            }
1981        }
1982        Ok(Some(stmts))
1983    }
1984
1985    /// Block shorthand: when the body is literally one bare builtin call
1986    /// (`{ uc }`, `{ basename }`, `{ to_json }`), inject `$_` as its first
1987    /// argument so `map { basename }` == `map { basename($_) }` uniformly.
1988    ///
1989    /// Without this, the ExprKind-modeled core names (`uc`/`lc`/`length`/…)
1990    /// default to `$_` via their own parse arms, but generic `FuncCall`-
1991    /// dispatched builtins (`basename`/`to_json`/`tj`/`bn`) are called with
1992    /// empty args and return the wrong value. This rewrite levels the
1993    /// playing field at parse time — no per-builtin handling needed.
1994    ///
1995    /// Narrow by design: fires only when the block has *exactly one*
1996    /// expression statement whose sole content is a known-bareword call
1997    /// with zero args. Multi-statement blocks and blocks with any other
1998    /// content are untouched.
1999    fn default_topic_for_sole_bareword(stmts: &mut [Statement]) {
2000        let [only] = stmts else { return };
2001        let StmtKind::Expression(ref mut expr) = only.kind else {
2002            return;
2003        };
2004        let topic_line = expr.line;
2005        let topic_arg = || Expr {
2006            kind: ExprKind::ScalarVar("_".to_string()),
2007            line: topic_line,
2008        };
2009        match expr.kind {
2010            // Zero-arg FuncCall whose name is a known builtin → inject `$_`.
2011            ExprKind::FuncCall {
2012                ref name,
2013                ref mut args,
2014            } if args.is_empty()
2015                && (Self::is_known_bareword(name) || Self::is_try_builtin_name(name)) =>
2016            {
2017                args.push(topic_arg());
2018            }
2019            // Lone bareword (the parser sometimes keeps a bareword as a
2020            // `Bareword` node instead of a zero-arg `FuncCall` —
2021            // e.g. `{ to_json }`, `{ ddump }`). Promote to a call.
2022            ExprKind::Bareword(ref name)
2023                if (Self::is_known_bareword(name) || Self::is_try_builtin_name(name)) =>
2024            {
2025                let n = name.clone();
2026                expr.kind = ExprKind::FuncCall {
2027                    name: n,
2028                    args: vec![topic_arg()],
2029                };
2030            }
2031            _ => {}
2032        }
2033    }
2034
2035    /// `defer { BLOCK }` — register a block to run when the current scope exits.
2036    /// Desugars to a `defer__internal(fn { BLOCK })` function call that the compiler
2037    /// handles specially by emitting Op::DeferBlock.
2038    fn parse_defer_stmt(&mut self) -> StrykeResult<Statement> {
2039        let line = self.peek_line();
2040        self.advance(); // defer
2041        let body = self.parse_block()?;
2042        self.eat(&Token::Semicolon);
2043        // Desugar: defer { BLOCK } → defer__internal(fn { BLOCK })
2044        let coderef = Expr {
2045            kind: ExprKind::CodeRef {
2046                params: vec![],
2047                body,
2048            },
2049            line,
2050        };
2051        Ok(Statement {
2052            label: None,
2053            kind: StmtKind::Expression(Expr {
2054                kind: ExprKind::FuncCall {
2055                    name: "defer__internal".to_string(),
2056                    args: vec![coderef],
2057                },
2058                line,
2059            }),
2060            line,
2061        })
2062    }
2063
2064    /// `try { } catch ($err) { }` with optional `finally { }`
2065    fn parse_try_catch(&mut self) -> StrykeResult<Statement> {
2066        let line = self.peek_line();
2067        self.advance(); // try
2068        let try_block = self.parse_block()?;
2069        match self.peek() {
2070            Token::Ident(ref k) if k == "catch" => {
2071                self.advance();
2072            }
2073            _ => {
2074                return Err(self.syntax_err("expected 'catch' after try block", self.peek_line()));
2075            }
2076        }
2077        self.expect(&Token::LParen)?;
2078        let catch_var = self.parse_scalar_var_name()?;
2079        self.expect(&Token::RParen)?;
2080        let catch_block = self.parse_block()?;
2081        let finally_block = match self.peek() {
2082            Token::Ident(ref k) if k == "finally" => {
2083                self.advance();
2084                Some(self.parse_block()?)
2085            }
2086            _ => None,
2087        };
2088        self.eat(&Token::Semicolon);
2089        Ok(Statement {
2090            label: None,
2091            kind: StmtKind::TryCatch {
2092                try_block,
2093                catch_var,
2094                catch_block,
2095                finally_block,
2096            },
2097            line,
2098        })
2099    }
2100
2101    /// `thread EXPR stage1 stage2 ...` — Clojure-style threading macro.
2102    /// Desugars to `EXPR |> stage1 |> stage2 |> ...`
2103    ///
2104    /// When `thread_last` is true (`->>` syntax), injects as last arg instead of first.
2105    ///
2106    /// When invoked as the RHS of `|>` (e.g. `LHS |> t s1 s2 ...`), the init
2107    /// is not parsed from tokens — using `parse_unary()` there lets the first
2108    /// bareword greedily consume the next token as its arg, which misparses
2109    /// `t inc pow($_, 2) p` as init=`inc(pow(…))` + stage=`p` instead of three
2110    /// separate stages. Instead, seed init with `$_[0]`, run every remaining
2111    /// token through the stage loop, and wrap the resulting chain in a
2112    /// `CodeRef`. The outer `pipe_forward_apply` then calls it with `lhs` as
2113    /// `$_[0]`, giving `LHS |> t s1 s2 s3` == `LHS |> s1 |> s2 |> s3`.
2114    fn parse_thread_macro(&mut self, _line: usize, thread_last: bool) -> StrykeResult<Expr> {
2115        self.parse_thread_macro_inner(_line, thread_last, None)
2116    }
2117
2118    /// Shared core for `~>` / `~>>` / `~s>` / `~s>>`. When
2119    /// `parallel_collector` is `Some` (streaming-mode entry from `~s>` /
2120    /// `~s>>`), after each stage is parsed we push the (just-built) stage
2121    /// expression into the collector and reset `result` to `$_` so the
2122    /// next stage parses against a fresh topic. The collector ends up
2123    /// with one Expr per stage where each stage's input is `$_`, ready
2124    /// to be wrapped as a `fn { ... }` closure for the per-item
2125    /// streaming runtime (`_thread_par_run`).
2126    fn parse_thread_macro_inner(
2127        &mut self,
2128        _line: usize,
2129        thread_last: bool,
2130        mut parallel_collector: Option<&mut Vec<Expr>>,
2131    ) -> StrykeResult<Expr> {
2132        // Set thread-last mode for pipe_forward_apply calls within this macro
2133        let saved_thread_last = self.thread_last_mode;
2134        self.thread_last_mode = thread_last;
2135
2136        let pipe_rhs_wrap = self.in_pipe_rhs();
2137        // `pending_thread_input` (set by `~p>` continuation parsing after
2138        // `||>` / `|then|`) supplies a pre-built input expression so we
2139        // skip parsing a source.
2140        let mut result = if let Some(pre) = self.pending_thread_input.take() {
2141            pre
2142        } else if pipe_rhs_wrap {
2143            Expr {
2144                kind: ExprKind::ArrayElement {
2145                    array: "_".to_string(),
2146                    index: Box::new(Expr {
2147                        kind: ExprKind::Integer(0),
2148                        line: _line,
2149                    }),
2150                },
2151                line: _line,
2152            }
2153        } else {
2154            // Suppress paren-less function calls so `t Color::Red p` parses
2155            // the enum variant without consuming `p` as an argument.
2156            self.suppress_parenless_call = self.suppress_parenless_call.saturating_add(1);
2157            let expr = self.parse_thread_input();
2158            self.suppress_parenless_call = self.suppress_parenless_call.saturating_sub(1);
2159            expr?
2160        };
2161        // Capture the source expression for parallel mode BEFORE any stage
2162        // is parsed, then reset `result` to `$_` so the first stage's parse
2163        // reads the topic instead of the source.
2164        let source_for_par = if parallel_collector.is_some() {
2165            let src = std::mem::replace(
2166                &mut result,
2167                Expr {
2168                    kind: ExprKind::ScalarVar("_".into()),
2169                    line: _line,
2170                },
2171            );
2172            Some(src)
2173        } else {
2174            None
2175        };
2176
2177        // Track line where the last stage ended (initially the input expression's line).
2178        let mut last_stage_end_line = self.prev_line();
2179
2180        // Parse stages until we hit a statement terminator
2181        loop {
2182            // Newline termination: if the next token is on a different line than where
2183            // the previous stage ended, the thread macro terminates. This allows
2184            // `~> @arr map { $_ * 2 }` on one line followed by `my @b = ...` on the next
2185            // without requiring a semicolon.
2186            if self.peek_line() > last_stage_end_line {
2187                break;
2188            }
2189
2190            // Check for terminators - |> ends thread and allows piping the result.
2191            // Variables ($x, @x, %x) and declaration keywords (my, our, local, state)
2192            // cannot be stages, so they implicitly terminate the thread macro.
2193            match self.peek() {
2194                Token::Semicolon
2195                | Token::RBrace
2196                | Token::RParen
2197                | Token::RBracket
2198                | Token::PipeForward
2199                | Token::Eof
2200                | Token::ScalarVar(_)
2201                | Token::ArrayVar(_)
2202                | Token::HashVar(_)
2203                | Token::Comma => break,
2204                // `||>` (LogOr + NumGt): chunk-parallel → sequential boundary
2205                // for `~p>` macros. Other thread macros never see this in
2206                // practice; if it appears, terminate the macro and let the
2207                // outer parser handle it.
2208                Token::LogOr if matches!(self.peek_at(1), Token::NumGt) => break,
2209                // `|then|` (BitOr + Ident("then") + BitOr): same boundary.
2210                Token::BitOr
2211                    if matches!(self.peek_at(1), Token::Ident(ref n) if n == "then")
2212                        && matches!(self.peek_at(2), Token::BitOr) =>
2213                {
2214                    break
2215                }
2216                Token::Ident(ref kw)
2217                    if matches!(
2218                        kw.as_str(),
2219                        "my" | "var" | "val" | "our"
2220                            | "local"
2221                            | "state"
2222                            | "if"
2223                            | "unless"
2224                            | "while"
2225                            | "until"
2226                            | "for"
2227                            | "foreach"
2228                            | "return"
2229                            | "last"
2230                            | "next"
2231                            | "redo"
2232                    ) =>
2233                {
2234                    break
2235                }
2236                _ => {}
2237            }
2238
2239            let stage_line = self.peek_line();
2240
2241            // Parse a stage and apply it to result via pipe
2242            match self.peek().clone() {
2243                // `>{ block }` — standalone anonymous block (sugar for fn { })
2244                Token::ArrowBrace => {
2245                    self.advance(); // consume `>{`
2246                    let mut stmts = Vec::new();
2247                    while !matches!(self.peek(), Token::RBrace | Token::Eof) {
2248                        if self.eat(&Token::Semicolon) {
2249                            continue;
2250                        }
2251                        stmts.push(self.parse_statement()?);
2252                    }
2253                    self.expect(&Token::RBrace)?;
2254                    let code_ref = Expr {
2255                        kind: ExprKind::CodeRef {
2256                            params: vec![],
2257                            body: stmts,
2258                        },
2259                        line: stage_line,
2260                    };
2261                    result = self.pipe_forward_apply(result, code_ref, stage_line)?;
2262                }
2263                // `sub { block }` — blocked in no-interop mode
2264                Token::Ident(ref name) if name == "sub" => {
2265                    if crate::no_interop_mode() {
2266                        return Err(self.syntax_err(
2267                            "stryke uses `fn {}` instead of `sub {}` (--no-interop)",
2268                            stage_line,
2269                        ));
2270                    }
2271                    self.advance(); // consume `sub`
2272                    let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
2273                    let body = self.parse_block()?;
2274                    let code_ref = Expr {
2275                        kind: ExprKind::CodeRef { params, body },
2276                        line: stage_line,
2277                    };
2278                    result = self.pipe_forward_apply(result, code_ref, stage_line)?;
2279                }
2280                // `fn { block }` — stryke anonymous function
2281                Token::Ident(ref name) if name == "fn" => {
2282                    self.advance(); // consume `fn`
2283                    let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
2284                    self.parse_sub_attributes()?;
2285                    let body = self.parse_fn_eq_body_or_block(false)?;
2286                    let code_ref = Expr {
2287                        kind: ExprKind::CodeRef { params, body },
2288                        line: stage_line,
2289                    };
2290                    result = self.pipe_forward_apply(result, code_ref, stage_line)?;
2291                }
2292                // `ident` possibly followed by block (or namespaced like `Foo::Bar::func`)
2293                Token::Ident(ref name) => {
2294                    let mut func_name = name.clone();
2295                    self.advance();
2296
2297                    // Collect namespaced function name (e.g., Rosetta::Stack::push)
2298                    while matches!(self.peek(), Token::PackageSep) {
2299                        self.advance(); // consume `::`
2300                        if let Token::Ident(ref part) = self.peek().clone() {
2301                            func_name.push_str("::");
2302                            func_name.push_str(part);
2303                            self.advance();
2304                        } else {
2305                            return Err(self.syntax_err(
2306                                format!(
2307                                    "Expected identifier after `::` in thread stage, got {:?}",
2308                                    self.peek()
2309                                ),
2310                                stage_line,
2311                            ));
2312                        }
2313                    }
2314
2315                    // Handle s/// and tr/// encoded tokens
2316                    if func_name.starts_with('\x00') {
2317                        let parts: Vec<&str> = func_name.split('\x00').collect();
2318                        if parts.len() >= 4 && parts[1] == "s" {
2319                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
2320                            let stage = Expr {
2321                                kind: ExprKind::Substitution {
2322                                    expr: Box::new(result.clone()),
2323                                    pattern: parts[2].to_string(),
2324                                    replacement: parts[3].to_string(),
2325                                    flags: format!("{}r", parts.get(4).unwrap_or(&"")),
2326                                    delim,
2327                                },
2328                                line: stage_line,
2329                            };
2330                            result = stage;
2331                            last_stage_end_line = self.prev_line();
2332                            continue;
2333                        }
2334                        if parts.len() >= 4 && parts[1] == "tr" {
2335                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
2336                            let stage = Expr {
2337                                kind: ExprKind::Transliterate {
2338                                    expr: Box::new(result.clone()),
2339                                    from: parts[2].to_string(),
2340                                    to: parts[3].to_string(),
2341                                    flags: format!("{}r", parts.get(4).unwrap_or(&"")),
2342                                    delim,
2343                                },
2344                                line: stage_line,
2345                            };
2346                            result = stage;
2347                            last_stage_end_line = self.prev_line();
2348                            continue;
2349                        }
2350                        return Err(
2351                            self.syntax_err("Unexpected encoded token in thread", stage_line)
2352                        );
2353                    }
2354
2355                    // `map +{ ... }` — hashref expression form (not a code block).
2356                    // The `+` disambiguates: `+{` is always a hashref constructor.
2357                    // Desugars to `MapExprComma` so pipe_forward_apply threads the
2358                    // list correctly: `t LIST map +{k => $_}` → `map +{k => $_}, LIST`.
2359                    if matches!(self.peek(), Token::Plus)
2360                        && matches!(self.peek_at(1), Token::LBrace)
2361                    {
2362                        self.advance(); // consume `+`
2363                        self.expect(&Token::LBrace)?;
2364                        // try_parse_hash_ref consumes the closing `}`
2365                        let pairs = self.try_parse_hash_ref()?;
2366                        let hashref_expr = Expr {
2367                            kind: ExprKind::HashRef(pairs),
2368                            line: stage_line,
2369                        };
2370                        let flatten_array_refs =
2371                            matches!(func_name.as_str(), "flat_map" | "flat_maps");
2372                        let stream = matches!(func_name.as_str(), "maps" | "flat_maps");
2373                        // Placeholder list — pipe_forward_apply replaces it with `result`.
2374                        let placeholder = Expr {
2375                            kind: ExprKind::Undef,
2376                            line: stage_line,
2377                        };
2378                        let map_node = Expr {
2379                            kind: ExprKind::MapExprComma {
2380                                expr: Box::new(hashref_expr),
2381                                list: Box::new(placeholder),
2382                                flatten_array_refs,
2383                                stream,
2384                            },
2385                            line: stage_line,
2386                        };
2387                        result = self.pipe_forward_apply(result, map_node, stage_line)?;
2388                    // `pmap_chunked CHUNK_SIZE { BLOCK }` — parallel chunked map
2389                    } else if func_name == "pmap_chunked" {
2390                        let chunk_size = self.parse_assign_expr()?;
2391                        let block = self.parse_block_or_bareword_block()?;
2392                        let placeholder = self.pipe_placeholder_list(stage_line);
2393                        let stage = Expr {
2394                            kind: ExprKind::PMapChunkedExpr {
2395                                chunk_size: Box::new(chunk_size),
2396                                block,
2397                                list: Box::new(placeholder),
2398                                progress: None,
2399                            },
2400                            line: stage_line,
2401                        };
2402                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2403                    // `preduce_init INIT { BLOCK }` — parallel reduce with init value
2404                    } else if func_name == "preduce_init" {
2405                        let init = self.parse_assign_expr()?;
2406                        let block = self.parse_block_or_bareword_block()?;
2407                        let placeholder = self.pipe_placeholder_list(stage_line);
2408                        let stage = Expr {
2409                            kind: ExprKind::PReduceInitExpr {
2410                                init: Box::new(init),
2411                                block,
2412                                list: Box::new(placeholder),
2413                                progress: None,
2414                            },
2415                            line: stage_line,
2416                        };
2417                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2418                    // `pmap_reduce { MAP } { REDUCE }` — parallel map-reduce
2419                    } else if func_name == "pmap_reduce" {
2420                        let map_block = self.parse_block_or_bareword_block()?;
2421                        let reduce_block = if matches!(self.peek(), Token::LBrace) {
2422                            self.parse_block()?
2423                        } else {
2424                            self.expect(&Token::Comma)?;
2425                            self.parse_block_or_bareword_cmp_block()?
2426                        };
2427                        let placeholder = self.pipe_placeholder_list(stage_line);
2428                        let stage = Expr {
2429                            kind: ExprKind::PMapReduceExpr {
2430                                map_block,
2431                                reduce_block,
2432                                list: Box::new(placeholder),
2433                                progress: None,
2434                            },
2435                            line: stage_line,
2436                        };
2437                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2438                    // `par_reduce { extract } [ { merge } ]` — chunk-extract-merge.
2439                    // First block runs per chunk in parallel; optional second
2440                    // block reduces pairwise across chunks (omit for auto-merge
2441                    // by result type).
2442                    } else if func_name == "par_reduce" {
2443                        let extract_block = self.parse_block_or_bareword_block()?;
2444                        let reduce_block = if matches!(self.peek(), Token::LBrace) {
2445                            Some(self.parse_block()?)
2446                        } else {
2447                            None
2448                        };
2449                        let placeholder = self.pipe_placeholder_list(stage_line);
2450                        let stage = Expr {
2451                            kind: ExprKind::ParReduceExpr {
2452                                extract_block,
2453                                reduce_block,
2454                                list: Box::new(placeholder),
2455                            },
2456                            line: stage_line,
2457                        };
2458                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2459                    // `pmap_on $cluster { BLOCK }` — parallel map dispatched to a remote
2460                    // cluster. Mirrors the `pmap_chunked` thread-stage shape; the cluster
2461                    // expression is parsed before the block, the threaded list slots in
2462                    // as the placeholder.
2463                    } else if func_name == "pmap_on" || func_name == "pflat_map_on" {
2464                        // Suppress `$cluster { ... }` auto-arrow (`$h->{...}`) so the
2465                        // brace opens the block, not a hash subscript.
2466                        self.suppress_scalar_hash_brace =
2467                            self.suppress_scalar_hash_brace.saturating_add(1);
2468                        let cluster = self.parse_assign_expr();
2469                        self.suppress_scalar_hash_brace =
2470                            self.suppress_scalar_hash_brace.saturating_sub(1);
2471                        let cluster = cluster?;
2472                        // Optional comma between cluster and block (matches the
2473                        // canonical `pmap_on $c, { BLOCK } @list` form in the LSP docs).
2474                        self.eat(&Token::Comma);
2475                        let block = self.parse_block_or_bareword_block()?;
2476                        let placeholder = self.pipe_placeholder_list(stage_line);
2477                        let stage = Expr {
2478                            kind: ExprKind::PMapExpr {
2479                                block,
2480                                list: Box::new(placeholder),
2481                                progress: None,
2482                                flat_outputs: func_name == "pflat_map_on",
2483                                on_cluster: Some(Box::new(cluster)),
2484                                stream: false,
2485                            },
2486                            line: stage_line,
2487                        };
2488                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2489                    // Check if followed by a block (like `filter { }`, `sort { }`, `map { }`)
2490                    } else if matches!(self.peek(), Token::LBrace) {
2491                        // Parse as a block-taking builtin
2492                        self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_add(1);
2493                        let stage = self.parse_thread_stage_with_block(&func_name, stage_line)?;
2494                        self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_sub(1);
2495                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2496                    } else if matches!(self.peek(), Token::LParen) {
2497                        // Special handling for join(sep) and split(pattern) in thread context.
2498                        // These take the threaded list/string as their data argument, not as $_.
2499                        if func_name == "join" {
2500                            self.advance(); // consume `(`
2501                            let separator = self.parse_assign_expr()?;
2502                            self.expect(&Token::RParen)?;
2503                            let placeholder = self.pipe_placeholder_list(stage_line);
2504                            let stage = Expr {
2505                                kind: ExprKind::JoinExpr {
2506                                    separator: Box::new(separator),
2507                                    list: Box::new(placeholder),
2508                                },
2509                                line: stage_line,
2510                            };
2511                            result = self.pipe_forward_apply(result, stage, stage_line)?;
2512                        } else if func_name == "split" {
2513                            self.advance(); // consume `(`
2514                            let pattern = self.parse_assign_expr()?;
2515                            let limit = if self.eat(&Token::Comma) {
2516                                Some(Box::new(self.parse_assign_expr()?))
2517                            } else {
2518                                None
2519                            };
2520                            self.expect(&Token::RParen)?;
2521                            let placeholder = Expr {
2522                                kind: ExprKind::ScalarVar("_".to_string()),
2523                                line: stage_line,
2524                            };
2525                            let stage = Expr {
2526                                kind: ExprKind::SplitExpr {
2527                                    pattern: Box::new(pattern),
2528                                    string: Box::new(placeholder),
2529                                    limit,
2530                                },
2531                                line: stage_line,
2532                            };
2533                            result = self.pipe_forward_apply(result, stage, stage_line)?;
2534                        } else {
2535                            // `name($_-bearing-args)` — parse explicit args, require at
2536                            // least one `$_` placeholder, then wrap as a `>{...}` block
2537                            // so the threaded value binds to `$_` at any position.
2538                            // Examples:
2539                            //   t 10 add2($_, 5) p      → add2(10, 5)
2540                            //   t 10 sub2(20, $_) p     → sub2(20, 10)
2541                            //   t 10 add3($_, 5, 10) p  → add3(10, 5, 10)
2542                            // To pass the threaded value as a sole arg, use bare form:
2543                            //   t 10 add2 p   (not `add2()`)
2544                            self.advance(); // consume `(`
2545                            let mut call_args = Vec::new();
2546                            while !matches!(self.peek(), Token::RParen | Token::Eof) {
2547                                call_args.push(self.parse_assign_expr()?);
2548                                if !self.eat(&Token::Comma) {
2549                                    break;
2550                                }
2551                            }
2552                            self.expect(&Token::RParen)?;
2553                            // If no `$_` placeholder, auto-inject threaded value.
2554                            // Thread-first: `t data to_file("/tmp/o.html")` → `to_file($_, "/tmp/o.html")`
2555                            // Thread-last: `->> data to_file("/tmp/o.html")` → `to_file("/tmp/o.html", $_)`
2556                            if !call_args.iter().any(Self::expr_contains_topic_var) {
2557                                let topic = Expr {
2558                                    kind: ExprKind::ScalarVar("_".to_string()),
2559                                    line: stage_line,
2560                                };
2561                                if self.thread_last_mode {
2562                                    call_args.push(topic);
2563                                } else {
2564                                    call_args.insert(0, topic);
2565                                }
2566                            }
2567                            let call_expr = Expr {
2568                                kind: ExprKind::FuncCall {
2569                                    name: func_name.clone(),
2570                                    args: call_args,
2571                                },
2572                                line: stage_line,
2573                            };
2574                            let code_ref = Expr {
2575                                kind: ExprKind::CodeRef {
2576                                    params: vec![],
2577                                    body: vec![Statement {
2578                                        label: None,
2579                                        kind: StmtKind::Expression(call_expr),
2580                                        line: stage_line,
2581                                    }],
2582                                },
2583                                line: stage_line,
2584                            };
2585                            result = self.pipe_forward_apply(result, code_ref, stage_line)?;
2586                        }
2587                    } else {
2588                        // Bare function name — handle unary builtins specially
2589                        result = self.thread_apply_bare_func(&func_name, result, stage_line)?;
2590                    }
2591                }
2592                // `/pattern/flags` — grep filter (desugar to `grep { /pattern/flags }`)
2593                Token::Regex(ref pattern, ref flags, delim) => {
2594                    let pattern = pattern.clone();
2595                    let flags = flags.clone();
2596                    self.advance();
2597                    result =
2598                        self.thread_regex_grep_stage(result, pattern, flags, delim, stage_line);
2599                }
2600                // Handle `/` that was lexed as Slash (division) because it followed a term.
2601                // In thread stage context, `/pattern/` should be a regex filter.
2602                Token::Slash => {
2603                    self.advance(); // consume opening /
2604
2605                    // Special case: if next token is Ident("m") or similar followed by Regex,
2606                    // the lexer interpreted `/m/` as `/ m/pattern/` where `m/` started a new regex.
2607                    // We need to handle this: the pattern is just "m" (or whatever the ident is).
2608                    if let Token::Ident(ref ident_s) = self.peek().clone() {
2609                        if matches!(ident_s.as_str(), "m" | "s" | "tr" | "y" | "qr")
2610                            && matches!(self.peek_at(1), Token::Regex(..))
2611                        {
2612                            // The `m` (or s/tr/y/qr) is our pattern, the Regex token was misparsed
2613                            self.advance(); // consume the ident
2614                                            // The Token::Regex after it was a misparsed `m/...` - we need to
2615                                            // extract what would have been the closing `/` situation.
2616                                            // Actually, the lexer consumed everything. Let's just use the ident
2617                                            // as the pattern and expect a closing slash.
2618                            if let Token::Regex(ref misparsed_pattern, ref misparsed_flags, _) =
2619                                self.peek().clone()
2620                            {
2621                                // The misparsed regex ate our closing `/`.
2622                                // For `/m/`, lexer saw `m/` and parsed until next `/`, finding nothing or wrong content.
2623                                // Actually for `/m/ less`, after Slash, lexer sees `m`, then `/`,
2624                                // interprets as m// regex start, reads until next `/` (none) -> error.
2625                                // So we shouldn't reach here if there was an error.
2626                                // But if lexer succeeded parsing `m/ less/` as regex, we'd have wrong pattern.
2627                                // This is getting complicated. Let me try a different approach.
2628                                // Just consume the Regex token and issue a warning? No, let's reconstruct.
2629                                // Skip for now and fall through to manual parsing.
2630                                let _ = (misparsed_pattern, misparsed_flags);
2631                            }
2632                        }
2633                    }
2634
2635                    // Manually parse the regex pattern from tokens until we hit another Slash
2636                    let mut pattern = String::new();
2637                    loop {
2638                        match self.peek().clone() {
2639                            Token::Slash => {
2640                                self.advance(); // consume closing /
2641                                break;
2642                            }
2643                            Token::Eof | Token::Semicolon | Token::Newline => {
2644                                return Err(self
2645                                    .syntax_err("Unterminated regex in thread stage", stage_line));
2646                            }
2647                            // Handle case where lexer misparsed m/pattern/ as Ident("m") + Regex
2648                            Token::Regex(ref inner_pattern, ref inner_flags, delim) => {
2649                                // This means `/m/` was lexed as Slash, then `m/` started a regex.
2650                                // The Regex token contains whatever was between the inner `m/` and closing `/`.
2651                                // For `/m/ less`, lexer would fail earlier. For `/m/i`, it might work weirdly.
2652                                // The safest: if we see a Regex token here and pattern is empty or just "m"/"s"/etc,
2653                                // treat the previous ident as the whole pattern and this Regex as misparsed.
2654                                // Actually, let's just prepend the ident we may have seen and use empty pattern.
2655                                // This is a lexer bug workaround.
2656                                if pattern.is_empty()
2657                                    || matches!(pattern.as_str(), "m" | "s" | "tr" | "y" | "qr")
2658                                {
2659                                    // The whole thing was probably `/X/` where X is m/s/tr/y/qr
2660                                    // and lexer misparsed. The Regex token is garbage.
2661                                    // Just use the ident as pattern and ignore this Regex.
2662                                    // But we already advanced past the ident...
2663                                    // This is messy. Let me try a cleaner approach.
2664                                    let _ = (inner_pattern, inner_flags, delim);
2665                                }
2666                                // For now, error out - this case is too complex
2667                                return Err(self.syntax_err(
2668                                    "Complex regex in thread stage - use m/pattern/ syntax instead",
2669                                    stage_line,
2670                                ));
2671                            }
2672                            Token::Ident(ref s) => {
2673                                pattern.push_str(s);
2674                                self.advance();
2675                            }
2676                            Token::Integer(n) => {
2677                                pattern.push_str(&n.to_string());
2678                                self.advance();
2679                            }
2680                            Token::ScalarVar(ref v) => {
2681                                pattern.push('$');
2682                                pattern.push_str(v);
2683                                self.advance();
2684                            }
2685                            Token::Dot => {
2686                                pattern.push('.');
2687                                self.advance();
2688                            }
2689                            Token::Star => {
2690                                pattern.push('*');
2691                                self.advance();
2692                            }
2693                            Token::Plus => {
2694                                pattern.push('+');
2695                                self.advance();
2696                            }
2697                            Token::Question => {
2698                                pattern.push('?');
2699                                self.advance();
2700                            }
2701                            Token::LParen => {
2702                                pattern.push('(');
2703                                self.advance();
2704                            }
2705                            Token::RParen => {
2706                                pattern.push(')');
2707                                self.advance();
2708                            }
2709                            Token::LBracket => {
2710                                pattern.push('[');
2711                                self.advance();
2712                            }
2713                            Token::RBracket => {
2714                                pattern.push(']');
2715                                self.advance();
2716                            }
2717                            Token::Backslash => {
2718                                pattern.push('\\');
2719                                self.advance();
2720                            }
2721                            Token::BitOr => {
2722                                pattern.push('|');
2723                                self.advance();
2724                            }
2725                            Token::Power => {
2726                                pattern.push_str("**");
2727                                self.advance();
2728                            }
2729                            Token::BitXor => {
2730                                pattern.push('^');
2731                                self.advance();
2732                            }
2733                            Token::Minus => {
2734                                pattern.push('-');
2735                                self.advance();
2736                            }
2737                            _ => {
2738                                return Err(self.syntax_err(
2739                                    format!("Unexpected token in regex pattern: {:?}", self.peek()),
2740                                    stage_line,
2741                                ));
2742                            }
2743                        }
2744                    }
2745                    // Parse optional flags (sequence of letters after closing /)
2746                    // Be careful: single letters like 'e' could be regex flags OR thread
2747                    // stages like `fore`/`e`. If followed by `{`, it's a stage, not a flag.
2748                    let mut flags = String::new();
2749                    if let Token::Ident(ref s) = self.peek().clone() {
2750                        let is_flag_only =
2751                            s.chars().all(|c| "gimsxecor".contains(c)) && s.len() <= 6;
2752                        let followed_by_brace = matches!(self.peek_at(1), Token::LBrace);
2753                        if is_flag_only && !followed_by_brace {
2754                            flags.push_str(s);
2755                            self.advance();
2756                        }
2757                    }
2758                    result = self.thread_regex_grep_stage(result, pattern, flags, '/', stage_line);
2759                }
2760                tok => {
2761                    return Err(self.syntax_err(
2762                        format!(
2763                            "thread: expected stage (ident, fn {{}}, s///, tr///, or /re/), got {:?}",
2764                            tok
2765                        ),
2766                        stage_line,
2767                    ));
2768                }
2769            };
2770            last_stage_end_line = self.prev_line();
2771            // Parallel mode: each iteration of the loop has produced a
2772            // stage expression where `$_` is the input. Push it into the
2773            // collector and reset `result` to `$_` so the next stage
2774            // parses against a fresh topic.
2775            if let Some(stages) = parallel_collector.as_mut() {
2776                let stage_body = std::mem::replace(
2777                    &mut result,
2778                    Expr {
2779                        kind: ExprKind::ScalarVar("_".into()),
2780                        line: stage_line,
2781                    },
2782                );
2783                stages.push(stage_body);
2784            }
2785        }
2786
2787        // Restore thread-last mode
2788        self.thread_last_mode = saved_thread_last;
2789
2790        // Parallel mode: lower to `_thread_par_run(source_expr, [stage_closures], thread_last)`.
2791        // The runtime treats the source value as a list-of-items and feeds
2792        // each item into stage 1 via a bounded channel. Each stage runs in
2793        // its own worker; stages are wrapped as `fn { body }` closures so
2794        // the runtime sets `$_` to the current item before invoking.
2795        if let Some(stages) = parallel_collector {
2796            let source_expr = source_for_par.unwrap_or(result);
2797            if stages.is_empty() {
2798                return Err(self.syntax_err(
2799                    "~p> / ~p>> require at least one stage after the source",
2800                    _line,
2801                ));
2802            }
2803            // Wrap each stage body in `[ ... ]` (an ArrayRef) so list-returning
2804            // ops like `map`/`grep` propagate their full output instead of
2805            // collapsing to a scalar count. The runtime worker peels one
2806            // level of array-ref via `map_flatten_outputs(true)` so each
2807            // element flows downstream as its own item.
2808            let stage_closures: Vec<Expr> = stages
2809                .drain(..)
2810                .map(|body| {
2811                    let body_line = body.line;
2812                    let wrapped = Expr {
2813                        kind: ExprKind::ArrayRef(vec![body]),
2814                        line: body_line,
2815                    };
2816                    Expr {
2817                        kind: ExprKind::CodeRef {
2818                            params: vec![],
2819                            body: vec![Statement {
2820                                label: None,
2821                                kind: StmtKind::Expression(wrapped),
2822                                line: body_line,
2823                            }],
2824                        },
2825                        line: body_line,
2826                    }
2827                })
2828                .collect();
2829            let stages_arr = Expr {
2830                kind: ExprKind::ArrayRef(stage_closures),
2831                line: _line,
2832            };
2833            let thread_last_flag = Expr {
2834                kind: ExprKind::Integer(if thread_last { 1 } else { 0 }),
2835                line: _line,
2836            };
2837            // Argument order: stages, thread_last, source... — source
2838            // is LAST so its list expansion (`(1,2,3)`, `@a`, ranges)
2839            // lands in the variadic tail. Pre-fix the source was first
2840            // and any list source flattened across the slot, breaking
2841            // the `args.len() == 3` invariant in `_thread_par_run` and
2842            // hitting "expected 3 args" for `~s> (1,2,3) sum` etc.
2843            return Ok(Expr {
2844                kind: ExprKind::FuncCall {
2845                    name: "_thread_par_run".into(),
2846                    args: vec![stages_arr, thread_last_flag, source_expr],
2847                },
2848                line: _line,
2849            });
2850        }
2851
2852        if pipe_rhs_wrap {
2853            // Wrap as `fn { …stages threaded from $_[0]… }` so the outer
2854            // `pipe_forward_apply` can invoke it with `lhs` as the arg.
2855            let body_line = result.line;
2856            return Ok(Expr {
2857                kind: ExprKind::CodeRef {
2858                    params: vec![],
2859                    body: vec![Statement {
2860                        label: None,
2861                        kind: StmtKind::Expression(result),
2862                        line: body_line,
2863                    }],
2864                },
2865                line: _line,
2866            });
2867        }
2868        Ok(result)
2869    }
2870
2871    /// Build a grep filter stage from a regex pattern for the thread macro.
2872    fn thread_regex_grep_stage(
2873        &self,
2874        list: Expr,
2875        pattern: String,
2876        flags: String,
2877        delim: char,
2878        line: usize,
2879    ) -> Expr {
2880        let topic = Expr {
2881            kind: ExprKind::ScalarVar("_".to_string()),
2882            line,
2883        };
2884        let match_expr = Expr {
2885            kind: ExprKind::Match {
2886                expr: Box::new(topic),
2887                pattern,
2888                flags,
2889                scalar_g: false,
2890                delim,
2891            },
2892            line,
2893        };
2894        let block = vec![Statement {
2895            label: None,
2896            kind: StmtKind::Expression(match_expr),
2897            line,
2898        }];
2899        Expr {
2900            kind: ExprKind::GrepExpr {
2901                block,
2902                list: Box::new(list),
2903                keyword: crate::ast::GrepBuiltinKeyword::Grep,
2904            },
2905            line,
2906        }
2907    }
2908
2909    /// Check whether an expression contains a `$_` reference anywhere in its sub-tree.
2910    /// Used by the thread macro to validate `name(args)` call-stages: the threaded
2911    /// value is bound to `$_` via a wrapping CodeRef, so at least one `$_` placeholder
2912    /// must appear in the args, otherwise the threaded value is silently dropped.
2913    ///
2914    /// Implementation uses Rust's `Debug` to serialize the entire sub-tree once and
2915    /// scan for the canonical `ScalarVar("_")` representation. This avoids a
2916    /// per-variant walker that would need to be updated whenever new `ExprKind`
2917    /// variants are added (and would silently miss any it forgot to handle).
2918    /// Parse-time perf is non-critical and the AST is small at this scope.
2919    fn expr_contains_topic_var(e: &Expr) -> bool {
2920        format!("{:?}", e).contains("ScalarVar(\"_\")")
2921    }
2922
2923    /// Walk tokens in `[rhs_start, rhs_end)` looking for a *free* bare
2924    /// topic-slot index (one in `self.bare_positional_indices` at
2925    /// brace-depth 0 within the RHS). Any `_` inside `{ ... }` is
2926    /// considered bound by whatever defines that block (closure,
2927    /// hash literal, map/grep/sort/match arm) and doesn't count.
2928    ///
2929    /// Special case: when the RHS *starts* with a thread-macro intro
2930    /// token (`~>`, `->>`, `~>>`, `~p>`, `~s>`, `~d>` and `-Last`
2931    /// variants), the macro itself binds `_` for all stage expressions
2932    /// — only the immediate input expression after the arrow can
2933    /// trigger the wrap. `->> 10 div(_, 2)` is eager (input = `10`,
2934    /// `_` is the threaded placeholder), but `~> _ uc` wraps (input
2935    /// is the free bare `_`).
2936    ///
2937    /// Drives the implicit-coderef sugar in `parse_my_our_local`.
2938    fn rhs_has_free_bare_topic_slot(&self, rhs_start: usize, rhs_end: usize) -> bool {
2939        let end = rhs_end.min(self.tokens.len());
2940        if rhs_start < end && Self::is_thread_arrow(&self.tokens[rhs_start].0) {
2941            // Only the input expression (first token after the arrow)
2942            // can trigger the wrap; everything else is a stage and
2943            // its bare `_` is the threaded placeholder.
2944            let input = rhs_start + 1;
2945            return input < end && self.bare_positional_indices.contains(&input);
2946        }
2947        let mut brace_depth = 0i32;
2948        for i in rhs_start..end {
2949            if brace_depth == 0 && self.bare_positional_indices.contains(&i) {
2950                return true;
2951            }
2952            match &self.tokens[i].0 {
2953                Token::LBrace | Token::ArrowBrace => brace_depth += 1,
2954                Token::RBrace => brace_depth -= 1,
2955                _ => {}
2956            }
2957        }
2958        false
2959    }
2960
2961    fn is_thread_arrow(tok: &Token) -> bool {
2962        matches!(
2963            tok,
2964            Token::ThreadArrow
2965                | Token::ThreadArrowLast
2966                | Token::ThreadArrowStream
2967                | Token::ThreadArrowStreamLast
2968                | Token::ThreadArrowPar
2969                | Token::ThreadArrowParLast
2970                | Token::ThreadArrowDist
2971                | Token::ThreadArrowDistLast
2972        )
2973    }
2974
2975    /// Apply a bare function name in thread context, handling unary builtins specially.
2976    fn thread_apply_bare_func(&self, name: &str, arg: Expr, line: usize) -> StrykeResult<Expr> {
2977        let kind = match name {
2978            // String functions
2979            "uc" => ExprKind::Uc(Box::new(arg)),
2980            "lc" => ExprKind::Lc(Box::new(arg)),
2981            "ucfirst" | "ufc" => ExprKind::Ucfirst(Box::new(arg)),
2982            "lcfirst" | "lfc" => ExprKind::Lcfirst(Box::new(arg)),
2983            "fc" => ExprKind::Fc(Box::new(arg)),
2984            "chomp" => ExprKind::Chomp(Box::new(arg)),
2985            "chop" => ExprKind::Chop(Box::new(arg)),
2986            "length" => ExprKind::Length(Box::new(arg)),
2987            "len" | "cnt" => ExprKind::FuncCall {
2988                name: "count".to_string(),
2989                args: vec![arg],
2990            },
2991            "quotemeta" | "qm" => ExprKind::Quotemeta(Box::new(arg)),
2992            // Numeric functions
2993            "abs" => ExprKind::Abs(Box::new(arg)),
2994            "int" => ExprKind::Int(Box::new(arg)),
2995            "sqrt" | "sq" => ExprKind::Sqrt(Box::new(arg)),
2996            "sin" => ExprKind::Sin(Box::new(arg)),
2997            "cos" => ExprKind::Cos(Box::new(arg)),
2998            "exp" => ExprKind::Exp(Box::new(arg)),
2999            "log" => ExprKind::Log(Box::new(arg)),
3000            "hex" => ExprKind::Hex(Box::new(arg)),
3001            "oct" => ExprKind::Oct(Box::new(arg)),
3002            "chr" => ExprKind::Chr(Box::new(arg)),
3003            "ord" => ExprKind::Ord(Box::new(arg)),
3004            "rand" => ExprKind::Rand(Some(Box::new(arg))),
3005            "srand" => ExprKind::Srand(Some(Box::new(arg))),
3006            // Type/ref functions
3007            "defined" | "def" => ExprKind::Defined(Box::new(arg)),
3008            "ref" => ExprKind::Ref(Box::new(arg)),
3009            "scalar" => {
3010                if crate::no_interop_mode() {
3011                    return Err(self.syntax_err(
3012                        "stryke uses `len` (also `cnt` / `count`) instead of `scalar` (--no-interop)",
3013                        line,
3014                    ));
3015                }
3016                ExprKind::ScalarContext(Box::new(arg))
3017            }
3018            // Array/hash functions
3019            "keys" => ExprKind::Keys(Box::new(arg)),
3020            "values" => ExprKind::Values(Box::new(arg)),
3021            "each" => ExprKind::Each(Box::new(arg)),
3022            "pop" => ExprKind::Pop(Box::new(arg)),
3023            "shift" => ExprKind::Shift(Box::new(arg)),
3024            "reverse" => {
3025                if crate::no_interop_mode() {
3026                    return Err(self.syntax_err(
3027                        "stryke uses `rev` instead of `reverse` (--no-interop)",
3028                        line,
3029                    ));
3030                }
3031                ExprKind::ReverseExpr(Box::new(arg))
3032            }
3033            "reversed" | "rv" | "rev" => ExprKind::Rev(Box::new(arg)),
3034            "sort" | "so" => ExprKind::SortExpr {
3035                cmp: None,
3036                list: Box::new(arg),
3037            },
3038            "psort" => ExprKind::PSortExpr {
3039                cmp: None,
3040                list: Box::new(arg),
3041                progress: None,
3042            },
3043            "uniq" | "distinct" | "uq" => ExprKind::FuncCall {
3044                name: "uniq".to_string(),
3045                args: vec![arg],
3046            },
3047            "trim" | "tm" => ExprKind::FuncCall {
3048                name: "trim".to_string(),
3049                args: vec![arg],
3050            },
3051            "flatten" | "fl" => ExprKind::FuncCall {
3052                name: "flatten".to_string(),
3053                args: vec![arg],
3054            },
3055            "compact" | "cpt" => ExprKind::FuncCall {
3056                name: "compact".to_string(),
3057                args: vec![arg],
3058            },
3059            "shuffle" | "shuf" => ExprKind::FuncCall {
3060                name: "shuffle".to_string(),
3061                args: vec![arg],
3062            },
3063            "frequencies" | "freq" | "frq" => ExprKind::FuncCall {
3064                name: "frequencies".to_string(),
3065                args: vec![arg],
3066            },
3067            "pfrequencies" | "pfreq" | "pfrq" => ExprKind::FuncCall {
3068                name: "pfrequencies".to_string(),
3069                args: vec![arg],
3070            },
3071            "dedup" | "dup" => ExprKind::FuncCall {
3072                name: "dedup".to_string(),
3073                args: vec![arg],
3074            },
3075            "enumerate" | "en" => ExprKind::FuncCall {
3076                name: "enumerate".to_string(),
3077                args: vec![arg],
3078            },
3079            "lines" | "ln" => ExprKind::FuncCall {
3080                name: "lines".to_string(),
3081                args: vec![arg],
3082            },
3083            "words" | "wd" => ExprKind::FuncCall {
3084                name: "words".to_string(),
3085                args: vec![arg],
3086            },
3087            "chars" | "ch" => ExprKind::FuncCall {
3088                name: "chars".to_string(),
3089                args: vec![arg],
3090            },
3091            "digits" | "dg" => ExprKind::FuncCall {
3092                name: "digits".to_string(),
3093                args: vec![arg],
3094            },
3095            "letters" | "lts" => ExprKind::FuncCall {
3096                name: "letters".to_string(),
3097                args: vec![arg],
3098            },
3099            "letters_uc" => ExprKind::FuncCall {
3100                name: "letters_uc".to_string(),
3101                args: vec![arg],
3102            },
3103            "letters_lc" => ExprKind::FuncCall {
3104                name: "letters_lc".to_string(),
3105                args: vec![arg],
3106            },
3107            "punctuation" | "punct" => ExprKind::FuncCall {
3108                name: "punctuation".to_string(),
3109                args: vec![arg],
3110            },
3111            "sentences" | "sents" => ExprKind::FuncCall {
3112                name: "sentences".to_string(),
3113                args: vec![arg],
3114            },
3115            "paragraphs" | "paras" => ExprKind::FuncCall {
3116                name: "paragraphs".to_string(),
3117                args: vec![arg],
3118            },
3119            "sections" | "sects" => ExprKind::FuncCall {
3120                name: "sections".to_string(),
3121                args: vec![arg],
3122            },
3123            "numbers" | "nums" => ExprKind::FuncCall {
3124                name: "numbers".to_string(),
3125                args: vec![arg],
3126            },
3127            "graphemes" | "grs" => ExprKind::FuncCall {
3128                name: "graphemes".to_string(),
3129                args: vec![arg],
3130            },
3131            "columns" | "cols" => ExprKind::FuncCall {
3132                name: "columns".to_string(),
3133                args: vec![arg],
3134            },
3135            // File functions
3136            "slurp" | "sl" => ExprKind::Slurp(Box::new(arg)),
3137            "swallow" | "swa" => ExprKind::Swallow(Box::new(arg)),
3138            "ingest" | "ing" => ExprKind::Ingest(Box::new(arg)),
3139            "burp" => ExprKind::Burp(Box::new(arg)),
3140            "god" => ExprKind::God(Box::new(arg)),
3141            "glob" => ExprKind::Glob(vec![arg]),
3142            "chdir" => ExprKind::Chdir(Box::new(arg)),
3143            "stat" => ExprKind::Stat(Box::new(arg)),
3144            "lstat" => ExprKind::Lstat(Box::new(arg)),
3145            "readlink" => ExprKind::Readlink(Box::new(arg)),
3146            "readdir" => ExprKind::Readdir(Box::new(arg)),
3147            "close" => ExprKind::Close(Box::new(arg)),
3148            "basename" | "bn" => ExprKind::FuncCall {
3149                name: "basename".to_string(),
3150                args: vec![arg],
3151            },
3152            "dirname" | "dn" => ExprKind::FuncCall {
3153                name: "dirname".to_string(),
3154                args: vec![arg],
3155            },
3156            "realpath" | "rp" => ExprKind::FuncCall {
3157                name: "realpath".to_string(),
3158                args: vec![arg],
3159            },
3160            "which" | "wh" => ExprKind::FuncCall {
3161                name: "which".to_string(),
3162                args: vec![arg],
3163            },
3164            // Other
3165            "eval" => ExprKind::Eval(Box::new(arg)),
3166            "require" => ExprKind::Require(Box::new(arg)),
3167            "study" => ExprKind::Study(Box::new(arg)),
3168            // Case conversion
3169            "snake_case" | "sc" => ExprKind::FuncCall {
3170                name: "snake_case".to_string(),
3171                args: vec![arg],
3172            },
3173            "camel_case" | "cc" => ExprKind::FuncCall {
3174                name: "camel_case".to_string(),
3175                args: vec![arg],
3176            },
3177            "kebab_case" | "kc" => ExprKind::FuncCall {
3178                name: "kebab_case".to_string(),
3179                args: vec![arg],
3180            },
3181            // Serialization
3182            "to_json" | "tj" => ExprKind::FuncCall {
3183                name: "to_json".to_string(),
3184                args: vec![arg],
3185            },
3186            "to_yaml" | "ty" => ExprKind::FuncCall {
3187                name: "to_yaml".to_string(),
3188                args: vec![arg],
3189            },
3190            "to_toml" | "tt" => ExprKind::FuncCall {
3191                name: "to_toml".to_string(),
3192                args: vec![arg],
3193            },
3194            "to_csv" | "tc" => ExprKind::FuncCall {
3195                name: "to_csv".to_string(),
3196                args: vec![arg],
3197            },
3198            "to_xml" | "tx" => ExprKind::FuncCall {
3199                name: "to_xml".to_string(),
3200                args: vec![arg],
3201            },
3202            "to_html" | "th" => ExprKind::FuncCall {
3203                name: "to_html".to_string(),
3204                args: vec![arg],
3205            },
3206            "to_markdown" | "to_md" | "tmd" => ExprKind::FuncCall {
3207                name: "to_markdown".to_string(),
3208                args: vec![arg],
3209            },
3210            "xopen" | "xo" => ExprKind::FuncCall {
3211                name: "xopen".to_string(),
3212                args: vec![arg],
3213            },
3214            "clip" | "clipboard" | "pbcopy" => ExprKind::FuncCall {
3215                name: "clip".to_string(),
3216                args: vec![arg],
3217            },
3218            "to_table" | "table" | "tbl" => ExprKind::FuncCall {
3219                name: "to_table".to_string(),
3220                args: vec![arg],
3221            },
3222            "sparkline" | "spark" => ExprKind::FuncCall {
3223                name: "sparkline".to_string(),
3224                args: vec![arg],
3225            },
3226            "bar_chart" | "bars" => ExprKind::FuncCall {
3227                name: "bar_chart".to_string(),
3228                args: vec![arg],
3229            },
3230            "flame" | "flamechart" => ExprKind::FuncCall {
3231                name: "flame".to_string(),
3232                args: vec![arg],
3233            },
3234            "ddump" | "dd" => ExprKind::FuncCall {
3235                name: "ddump".to_string(),
3236                args: vec![arg],
3237            },
3238            "say" => {
3239                if crate::no_interop_mode() {
3240                    return Err(
3241                        self.syntax_err("stryke uses `p` instead of `say` (--no-interop)", line)
3242                    );
3243                }
3244                ExprKind::Say {
3245                    handle: None,
3246                    args: vec![arg],
3247                }
3248            }
3249            "p" => ExprKind::Say {
3250                handle: None,
3251                args: vec![arg],
3252            },
3253            "print" => ExprKind::Print {
3254                handle: None,
3255                args: vec![arg],
3256            },
3257            "warn" => ExprKind::Warn(vec![arg]),
3258            "die" => ExprKind::Die(vec![arg]),
3259            "stringify" | "str" => ExprKind::FuncCall {
3260                name: "stringify".to_string(),
3261                args: vec![arg],
3262            },
3263            "json_decode" | "jd" => ExprKind::FuncCall {
3264                name: "json_decode".to_string(),
3265                args: vec![arg],
3266            },
3267            "yaml_decode" | "yd" => ExprKind::FuncCall {
3268                name: "yaml_decode".to_string(),
3269                args: vec![arg],
3270            },
3271            "toml_decode" | "td" => ExprKind::FuncCall {
3272                name: "toml_decode".to_string(),
3273                args: vec![arg],
3274            },
3275            "xml_decode" | "xd" => ExprKind::FuncCall {
3276                name: "xml_decode".to_string(),
3277                args: vec![arg],
3278            },
3279            "json_encode" | "je" => ExprKind::FuncCall {
3280                name: "json_encode".to_string(),
3281                args: vec![arg],
3282            },
3283            "yaml_encode" | "ye" => ExprKind::FuncCall {
3284                name: "yaml_encode".to_string(),
3285                args: vec![arg],
3286            },
3287            "toml_encode" | "te" => ExprKind::FuncCall {
3288                name: "toml_encode".to_string(),
3289                args: vec![arg],
3290            },
3291            "xml_encode" | "xe" => ExprKind::FuncCall {
3292                name: "xml_encode".to_string(),
3293                args: vec![arg],
3294            },
3295            // Encoding
3296            "base64_encode" | "b64e" => ExprKind::FuncCall {
3297                name: "base64_encode".to_string(),
3298                args: vec![arg],
3299            },
3300            "base64_decode" | "b64d" => ExprKind::FuncCall {
3301                name: "base64_decode".to_string(),
3302                args: vec![arg],
3303            },
3304            "hex_encode" | "hxe" => ExprKind::FuncCall {
3305                name: "hex_encode".to_string(),
3306                args: vec![arg],
3307            },
3308            "hex_decode" | "hxd" => ExprKind::FuncCall {
3309                name: "hex_decode".to_string(),
3310                args: vec![arg],
3311            },
3312            "url_encode" | "uri_escape" | "ue" => ExprKind::FuncCall {
3313                name: "url_encode".to_string(),
3314                args: vec![arg],
3315            },
3316            "url_decode" | "uri_unescape" | "ud" => ExprKind::FuncCall {
3317                name: "url_decode".to_string(),
3318                args: vec![arg],
3319            },
3320            "gzip" | "gz" => ExprKind::FuncCall {
3321                name: "gzip".to_string(),
3322                args: vec![arg],
3323            },
3324            "gunzip" | "ugz" => ExprKind::FuncCall {
3325                name: "gunzip".to_string(),
3326                args: vec![arg],
3327            },
3328            "zstd" | "zst" => ExprKind::FuncCall {
3329                name: "zstd".to_string(),
3330                args: vec![arg],
3331            },
3332            "zstd_decode" | "uzst" => ExprKind::FuncCall {
3333                name: "zstd_decode".to_string(),
3334                args: vec![arg],
3335            },
3336            // Crypto
3337            "sha256" | "s256" => ExprKind::FuncCall {
3338                name: "sha256".to_string(),
3339                args: vec![arg],
3340            },
3341            "sha1" | "s1" => ExprKind::FuncCall {
3342                name: "sha1".to_string(),
3343                args: vec![arg],
3344            },
3345            "md5" | "m5" => ExprKind::FuncCall {
3346                name: "md5".to_string(),
3347                args: vec![arg],
3348            },
3349            "uuid" | "uid" => ExprKind::FuncCall {
3350                name: "uuid".to_string(),
3351                args: vec![arg],
3352            },
3353            // Datetime
3354            "datetime_utc" | "utc" => ExprKind::FuncCall {
3355                name: "datetime_utc".to_string(),
3356                args: vec![arg],
3357            },
3358            // Bare `e` / `fore` / `ep` in thread context: foreach element, say it.
3359            // `t @list e` == `@list |> e p` == `@list |> ep` == foreach (@list) { say }
3360            "e" | "fore" | "ep" => ExprKind::ForEachExpr {
3361                block: vec![Statement {
3362                    label: None,
3363                    kind: StmtKind::Expression(Expr {
3364                        kind: ExprKind::Say {
3365                            handle: None,
3366                            args: vec![Expr {
3367                                kind: ExprKind::ScalarVar("_".into()),
3368                                line,
3369                            }],
3370                        },
3371                        line,
3372                    }),
3373                    line,
3374                }],
3375                list: Box::new(arg),
3376            },
3377            // Default: generic function call
3378            _ => ExprKind::FuncCall {
3379                name: name.to_string(),
3380                args: vec![arg],
3381            },
3382        };
3383        Ok(Expr { kind, line })
3384    }
3385
3386    /// Parse a thread stage that has a block: `map { }`, `filter { }`, `sort { }`, etc.
3387    /// In thread context, we only parse the block - the list comes from the piped result.
3388    fn parse_thread_stage_with_block(&mut self, name: &str, line: usize) -> StrykeResult<Expr> {
3389        let block = self.parse_block()?;
3390        // Use a placeholder for the list - pipe_forward_apply will replace it
3391        let placeholder = self.pipe_placeholder_list(line);
3392
3393        match name {
3394            "map" | "flat_map" | "maps" | "flat_maps" => {
3395                let flatten_array_refs = matches!(name, "flat_map" | "flat_maps");
3396                let stream = matches!(name, "maps" | "flat_maps");
3397                Ok(Expr {
3398                    kind: ExprKind::MapExpr {
3399                        block,
3400                        list: Box::new(placeholder),
3401                        flatten_array_refs,
3402                        stream,
3403                    },
3404                    line,
3405                })
3406            }
3407            "grep" | "greps" | "filter" | "fi" | "find_all" | "gr" => {
3408                let keyword = match name {
3409                    "grep" | "gr" => crate::ast::GrepBuiltinKeyword::Grep,
3410                    "greps" => crate::ast::GrepBuiltinKeyword::Greps,
3411                    "filter" | "fi" => crate::ast::GrepBuiltinKeyword::Filter,
3412                    "find_all" => crate::ast::GrepBuiltinKeyword::FindAll,
3413                    _ => unreachable!(),
3414                };
3415                Ok(Expr {
3416                    kind: ExprKind::GrepExpr {
3417                        block,
3418                        list: Box::new(placeholder),
3419                        keyword,
3420                    },
3421                    line,
3422                })
3423            }
3424            "sort" | "so" => Ok(Expr {
3425                kind: ExprKind::SortExpr {
3426                    cmp: Some(SortComparator::Block(block)),
3427                    list: Box::new(placeholder),
3428                },
3429                line,
3430            }),
3431            "reduce" | "rd" => Ok(Expr {
3432                kind: ExprKind::ReduceExpr {
3433                    block,
3434                    list: Box::new(placeholder),
3435                },
3436                line,
3437            }),
3438            "fore" | "e" | "ep" => Ok(Expr {
3439                kind: ExprKind::ForEachExpr {
3440                    block,
3441                    list: Box::new(placeholder),
3442                },
3443                line,
3444            }),
3445            "par" => Ok(Expr {
3446                kind: ExprKind::ParExpr {
3447                    block,
3448                    list: Box::new(placeholder),
3449                },
3450                line,
3451            }),
3452            "pmap" | "pflat_map" | "pmaps" | "pflat_maps" => Ok(Expr {
3453                kind: ExprKind::PMapExpr {
3454                    block,
3455                    list: Box::new(placeholder),
3456                    progress: None,
3457                    flat_outputs: name == "pflat_map" || name == "pflat_maps",
3458                    on_cluster: None,
3459                    stream: name == "pmaps" || name == "pflat_maps",
3460                },
3461                line,
3462            }),
3463            "pgrep" | "pgreps" => Ok(Expr {
3464                kind: ExprKind::PGrepExpr {
3465                    block,
3466                    list: Box::new(placeholder),
3467                    progress: None,
3468                    stream: name == "pgreps",
3469                },
3470                line,
3471            }),
3472            "pfor" => Ok(Expr {
3473                kind: ExprKind::PForExpr {
3474                    block,
3475                    list: Box::new(placeholder),
3476                    progress: None,
3477                },
3478                line,
3479            }),
3480            "preduce" => Ok(Expr {
3481                kind: ExprKind::PReduceExpr {
3482                    block,
3483                    list: Box::new(placeholder),
3484                    progress: None,
3485                },
3486                line,
3487            }),
3488            "pcache" => Ok(Expr {
3489                kind: ExprKind::PcacheExpr {
3490                    block,
3491                    list: Box::new(placeholder),
3492                    progress: None,
3493                },
3494                line,
3495            }),
3496            "psort" => Ok(Expr {
3497                kind: ExprKind::PSortExpr {
3498                    cmp: Some(block),
3499                    list: Box::new(placeholder),
3500                    progress: None,
3501                },
3502                line,
3503            }),
3504            _ => {
3505                // Generic: parse block and treat as FuncCall with code ref arg.
3506                // Block-then-list pipe builtins (`pfirst`, `any`, `take_while`, etc.)
3507                // need the threaded list slot pre-allocated at args[1] so
3508                // `pipe_forward_apply` can substitute the lhs there (parser.rs:5823).
3509                // For everything else, the generic pipe-forward arm prepends or
3510                // appends the lhs based on `thread_last_mode`.
3511                let code_ref = Expr {
3512                    kind: ExprKind::CodeRef {
3513                        params: vec![],
3514                        body: block,
3515                    },
3516                    line,
3517                };
3518                let args = if Self::is_block_then_list_pipe_builtin(name) {
3519                    vec![code_ref, placeholder]
3520                } else {
3521                    vec![code_ref]
3522                };
3523                Ok(Expr {
3524                    kind: ExprKind::FuncCall {
3525                        name: name.to_string(),
3526                        args,
3527                    },
3528                    line,
3529                })
3530            }
3531        }
3532    }
3533
3534    /// `tie %hash | tie @arr | tie $x , 'Class', ...args`
3535    fn parse_tie_stmt(&mut self) -> StrykeResult<Statement> {
3536        let line = self.peek_line();
3537        self.advance(); // tie
3538                        // `tie my $x, Class` and `tie our $x, Class` — common Perl idiom.
3539                        // Desugar by emitting an implicit `my $x` (or `our $x`) declaration
3540                        // before the tie. The tie target then references the just-declared
3541                        // variable. Without this, `tie my $x, Class, ARGS` errors with
3542                        // "tie expects $scalar, @array, or %hash, got Ident(\"my\")".
3543        let mut implicit_decl: Option<Statement> = None;
3544        if let Token::Ident(kw) = self.peek().clone() {
3545            if matches!(kw.as_str(), "my" | "var" | "val" | "our") {
3546                let kw_line = self.peek_line();
3547                self.advance(); // my / var / val / our
3548                                // Read the variable being declared (must be Scalar/Array/Hash).
3549                let (decl_sigil, decl_name) = match self.peek().clone() {
3550                    Token::ScalarVar(s) => (Sigil::Scalar, s),
3551                    Token::ArrayVar(a) => (Sigil::Array, a),
3552                    Token::HashVar(h) => (Sigil::Hash, h),
3553                    tok => {
3554                        return Err(self.syntax_err(
3555                            format!("expected variable after `tie {}`, got {:?}", kw, tok),
3556                            self.peek_line(),
3557                        ));
3558                    }
3559                };
3560                let decls = vec![VarDecl {
3561                    sigil: decl_sigil,
3562                    name: decl_name.clone(),
3563                    initializer: None,
3564                    frozen: false,
3565                    type_annotation: None,
3566                    list_context: false,
3567                }];
3568                implicit_decl = Some(Statement {
3569                    label: None,
3570                    kind: if kw == "my" {
3571                        StmtKind::My(decls)
3572                    } else {
3573                        StmtKind::Our(decls)
3574                    },
3575                    line: kw_line,
3576                });
3577                // Don't advance past the variable token here — fall through
3578                // to the existing match below so `target` is built from the
3579                // same token (the ScalarVar/ArrayVar/HashVar path will
3580                // advance and capture the name).
3581            }
3582        }
3583        let target = match self.peek().clone() {
3584            Token::HashVar(h) => {
3585                self.advance();
3586                TieTarget::Hash(h)
3587            }
3588            Token::ArrayVar(a) => {
3589                self.advance();
3590                TieTarget::Array(a)
3591            }
3592            Token::ScalarVar(s) => {
3593                self.advance();
3594                TieTarget::Scalar(s)
3595            }
3596            tok => {
3597                return Err(self.syntax_err(
3598                    format!("tie expects $scalar, @array, or %hash, got {:?}", tok),
3599                    self.peek_line(),
3600                ));
3601            }
3602        };
3603        self.expect(&Token::Comma)?;
3604        let class = self.parse_assign_expr()?;
3605        let mut args = Vec::new();
3606        while self.eat(&Token::Comma) {
3607            if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof) {
3608                break;
3609            }
3610            args.push(self.parse_assign_expr()?);
3611        }
3612        self.eat(&Token::Semicolon);
3613        let tie_stmt = Statement {
3614            label: None,
3615            kind: StmtKind::Tie {
3616                target,
3617                class,
3618                args,
3619            },
3620            line,
3621        };
3622        if let Some(decl) = implicit_decl {
3623            // Wrap the implicit `my $x` + tie in a `StmtGroup` so they live
3624            // in the same lexical block (the parser desugar is invisible to
3625            // callers; `StmtGroup` runs statements in order without a frame
3626            // push).
3627            Ok(Statement {
3628                label: None,
3629                kind: StmtKind::StmtGroup(vec![decl, tie_stmt]),
3630                line,
3631            })
3632        } else {
3633            Ok(tie_stmt)
3634        }
3635    }
3636
3637    /// `given (EXPR) { ... }`
3638    fn parse_given(&mut self) -> StrykeResult<Statement> {
3639        let line = self.peek_line();
3640        self.advance();
3641        self.expect(&Token::LParen)?;
3642        let topic = self.parse_expression()?;
3643        self.expect(&Token::RParen)?;
3644        let body = self.parse_block()?;
3645        self.eat(&Token::Semicolon);
3646        Ok(Statement {
3647            label: None,
3648            kind: StmtKind::Given { topic, body },
3649            line,
3650        })
3651    }
3652
3653    /// `when (COND) { ... }` — only meaningful inside `given`
3654    fn parse_when_stmt(&mut self) -> StrykeResult<Statement> {
3655        let line = self.peek_line();
3656        self.advance();
3657        self.expect(&Token::LParen)?;
3658        let cond = self.parse_expression()?;
3659        self.expect(&Token::RParen)?;
3660        let body = self.parse_block()?;
3661        self.eat(&Token::Semicolon);
3662        Ok(Statement {
3663            label: None,
3664            kind: StmtKind::When { cond, body },
3665            line,
3666        })
3667    }
3668
3669    /// `default { ... }` — only meaningful inside `given`
3670    fn parse_default_stmt(&mut self) -> StrykeResult<Statement> {
3671        let line = self.peek_line();
3672        self.advance();
3673        let body = self.parse_block()?;
3674        self.eat(&Token::Semicolon);
3675        Ok(Statement {
3676            label: None,
3677            kind: StmtKind::DefaultCase { body },
3678            line,
3679        })
3680    }
3681
3682    /// `cond { EXPR => RESULT, ..., default => RESULT }`
3683    ///
3684    /// Desugars to an if/elsif/else chain at parse time.
3685    /// Each arm is `condition => { body }` or `condition => expr`.
3686    /// `default => ...` becomes the else branch.
3687    fn parse_cond_expr(&mut self, line: usize) -> StrykeResult<Expr> {
3688        self.expect(&Token::LBrace)?;
3689
3690        let mut arms: Vec<(Expr, Block)> = Vec::new();
3691        let mut else_block: Option<Block> = None;
3692
3693        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
3694            let arm_line = self.peek_line();
3695
3696            // Check for `default =>`
3697            let is_default = matches!(self.peek(), Token::Ident(ref s) if s == "default")
3698                && matches!(self.peek_at(1), Token::FatArrow);
3699
3700            if is_default {
3701                self.advance(); // consume `default`
3702                self.advance(); // consume `=>`
3703                let body = if matches!(self.peek(), Token::LBrace) {
3704                    self.parse_block()?
3705                } else {
3706                    let expr = self.parse_assign_expr()?;
3707                    vec![Statement {
3708                        label: None,
3709                        kind: StmtKind::Expression(expr),
3710                        line: arm_line,
3711                    }]
3712                };
3713                else_block = Some(body);
3714                self.eat(&Token::Comma);
3715                break; // default must be last
3716            }
3717
3718            // Parse condition expression (stop before `=>`)
3719            let condition = self.parse_assign_expr()?;
3720            self.expect(&Token::FatArrow)?;
3721
3722            let body = if matches!(self.peek(), Token::LBrace) {
3723                self.parse_block()?
3724            } else {
3725                let expr = self.parse_assign_expr()?;
3726                vec![Statement {
3727                    label: None,
3728                    kind: StmtKind::Expression(expr),
3729                    line: arm_line,
3730                }]
3731            };
3732
3733            arms.push((condition, body));
3734            self.eat(&Token::Comma);
3735        }
3736
3737        self.expect(&Token::RBrace)?;
3738
3739        if arms.is_empty() {
3740            return Err(self.syntax_err("cond requires at least one condition arm", line));
3741        }
3742
3743        // Build if/elsif/else chain from the arms.
3744        let (first_cond, first_body) = arms.remove(0);
3745        let elsifs: Vec<(Expr, Block)> = arms;
3746
3747        // Wrap in a do-block so `cond { ... }` is an expression.
3748        let if_stmt = Statement {
3749            label: None,
3750            kind: StmtKind::If {
3751                condition: first_cond,
3752                body: first_body,
3753                elsifs,
3754                else_block,
3755            },
3756            line,
3757        };
3758        let inner = Expr {
3759            kind: ExprKind::CodeRef {
3760                params: vec![],
3761                body: vec![if_stmt],
3762            },
3763            line,
3764        };
3765        Ok(Expr {
3766            kind: ExprKind::Do(Box::new(inner)),
3767            line,
3768        })
3769    }
3770
3771    /// `match (EXPR) { PATTERN => EXPR, ... }`
3772    fn parse_algebraic_match_expr(&mut self, line: usize) -> StrykeResult<Expr> {
3773        self.expect(&Token::LParen)?;
3774        let subject = self.parse_expression()?;
3775        self.expect(&Token::RParen)?;
3776        self.expect(&Token::LBrace)?;
3777        let mut arms = Vec::new();
3778        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
3779            if self.eat(&Token::Semicolon) {
3780                continue;
3781            }
3782            let pattern = self.parse_match_pattern()?;
3783            let guard = if matches!(self.peek(), Token::Ident(ref s) if s == "if") {
3784                self.advance();
3785                // Use assign-level parsing so `=>` after the guard is not consumed as a comma/fat-comma
3786                // separator (see [`Self::parse_comma_expr`]).
3787                Some(Box::new(self.parse_assign_expr()?))
3788            } else {
3789                None
3790            };
3791            self.expect(&Token::FatArrow)?;
3792            // Use assign-level parsing so commas separate arms, not `List` elements.
3793            let body = self.parse_assign_expr()?;
3794            arms.push(MatchArm {
3795                pattern,
3796                guard,
3797                body,
3798            });
3799            self.eat(&Token::Comma);
3800        }
3801        self.expect(&Token::RBrace)?;
3802        Ok(Expr {
3803            kind: ExprKind::AlgebraicMatch {
3804                subject: Box::new(subject),
3805                arms,
3806            },
3807            line,
3808        })
3809    }
3810
3811    fn parse_match_pattern(&mut self) -> StrykeResult<MatchPattern> {
3812        match self.peek().clone() {
3813            Token::Regex(pattern, flags, _delim) => {
3814                self.advance();
3815                Ok(MatchPattern::Regex { pattern, flags })
3816            }
3817            Token::Ident(ref s) if s == "_" => {
3818                self.advance();
3819                Ok(MatchPattern::Any)
3820            }
3821            Token::Ident(ref s) if s == "Some" => {
3822                self.advance();
3823                self.expect(&Token::LParen)?;
3824                let name = self.parse_scalar_var_name()?;
3825                self.expect(&Token::RParen)?;
3826                Ok(MatchPattern::OptionSome(name))
3827            }
3828            Token::LBracket => self.parse_match_array_pattern(),
3829            Token::LBrace => self.parse_match_hash_pattern(),
3830            Token::LParen => {
3831                self.advance();
3832                let e = self.parse_expression()?;
3833                self.expect(&Token::RParen)?;
3834                Ok(MatchPattern::Value(Box::new(e)))
3835            }
3836            _ => {
3837                let e = self.parse_assign_expr()?;
3838                Ok(MatchPattern::Value(Box::new(e)))
3839            }
3840        }
3841    }
3842
3843    /// Contents of `[ ... ]` for algebraic array patterns and `sub ($a, [ ... ])` signatures.
3844    fn parse_match_array_elems_until_rbracket(&mut self) -> StrykeResult<Vec<MatchArrayElem>> {
3845        let mut elems = Vec::new();
3846        if self.eat(&Token::RBracket) {
3847            return Ok(vec![]);
3848        }
3849        loop {
3850            if matches!(self.peek(), Token::Star) {
3851                self.advance();
3852                elems.push(MatchArrayElem::Rest);
3853                self.eat(&Token::Comma);
3854                if !matches!(self.peek(), Token::RBracket) {
3855                    return Err(self.syntax_err(
3856                        "`*` must be the last element in an array match pattern",
3857                        self.peek_line(),
3858                    ));
3859                }
3860                self.expect(&Token::RBracket)?;
3861                return Ok(elems);
3862            }
3863            if let Token::ArrayVar(name) = self.peek().clone() {
3864                self.advance();
3865                elems.push(MatchArrayElem::RestBind(name));
3866                self.eat(&Token::Comma);
3867                if !matches!(self.peek(), Token::RBracket) {
3868                    return Err(self.syntax_err(
3869                        "`@name` rest bind must be the last element in an array match pattern",
3870                        self.peek_line(),
3871                    ));
3872                }
3873                self.expect(&Token::RBracket)?;
3874                return Ok(elems);
3875            }
3876            if let Token::ScalarVar(name) = self.peek().clone() {
3877                self.advance();
3878                elems.push(MatchArrayElem::CaptureScalar(name));
3879                if self.eat(&Token::Comma) {
3880                    if matches!(self.peek(), Token::RBracket) {
3881                        break;
3882                    }
3883                    continue;
3884                }
3885                break;
3886            }
3887            let e = self.parse_assign_expr()?;
3888            elems.push(MatchArrayElem::Expr(e));
3889            if self.eat(&Token::Comma) {
3890                if matches!(self.peek(), Token::RBracket) {
3891                    break;
3892                }
3893                continue;
3894            }
3895            break;
3896        }
3897        self.expect(&Token::RBracket)?;
3898        Ok(elems)
3899    }
3900
3901    fn parse_match_array_pattern(&mut self) -> StrykeResult<MatchPattern> {
3902        self.expect(&Token::LBracket)?;
3903        let elems = self.parse_match_array_elems_until_rbracket()?;
3904        Ok(MatchPattern::Array(elems))
3905    }
3906
3907    fn parse_match_hash_pattern(&mut self) -> StrykeResult<MatchPattern> {
3908        self.expect(&Token::LBrace)?;
3909        let mut pairs = Vec::new();
3910        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
3911            if self.eat(&Token::Semicolon) {
3912                continue;
3913            }
3914            let key = self.parse_assign_expr()?;
3915            self.expect(&Token::FatArrow)?;
3916            match self.advance().0 {
3917                Token::Ident(ref s) if s == "_" => {
3918                    pairs.push(MatchHashPair::KeyOnly { key });
3919                }
3920                Token::ScalarVar(name) => {
3921                    pairs.push(MatchHashPair::Capture { key, name });
3922                }
3923                tok => {
3924                    return Err(self.syntax_err(
3925                        format!(
3926                            "hash match pattern must bind with `=> $name` or `=> _`, got {:?}",
3927                            tok
3928                        ),
3929                        self.peek_line(),
3930                    ));
3931                }
3932            }
3933            self.eat(&Token::Comma);
3934        }
3935        self.expect(&Token::RBrace)?;
3936        Ok(MatchPattern::Hash(pairs))
3937    }
3938
3939    /// `eval_timeout SECS { ... }`
3940    fn parse_eval_timeout(&mut self) -> StrykeResult<Statement> {
3941        let line = self.peek_line();
3942        self.advance();
3943        let timeout = self.parse_postfix()?;
3944        let body = self.parse_block_or_bareword_block_no_args()?;
3945        self.eat(&Token::Semicolon);
3946        Ok(Statement {
3947            label: None,
3948            kind: StmtKind::EvalTimeout { timeout, body },
3949            line,
3950        })
3951    }
3952
3953    fn mark_match_scalar_g_for_boolean_condition(cond: &mut Expr) {
3954        match &mut cond.kind {
3955            ExprKind::Match {
3956                flags, scalar_g, ..
3957            } if flags.contains('g') => {
3958                *scalar_g = true;
3959            }
3960            ExprKind::UnaryOp {
3961                op: UnaryOp::LogNot,
3962                expr,
3963            } => {
3964                if let ExprKind::Match {
3965                    flags, scalar_g, ..
3966                } = &mut expr.kind
3967                {
3968                    if flags.contains('g') {
3969                        *scalar_g = true;
3970                    }
3971                }
3972            }
3973            _ => {}
3974        }
3975    }
3976
3977    fn parse_if(&mut self) -> StrykeResult<Statement> {
3978        let line = self.peek_line();
3979        self.advance(); // 'if'
3980        if matches!(self.peek(), Token::Ident(ref s) if s == "let") {
3981            if crate::compat_mode() {
3982                return Err(self.syntax_err(
3983                    "`if let` is a stryke extension (disabled by --compat)",
3984                    line,
3985                ));
3986            }
3987            return self.parse_if_let(line);
3988        }
3989        self.expect(&Token::LParen)?;
3990        let mut cond = self.parse_expression()?;
3991        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
3992        self.expect(&Token::RParen)?;
3993        let body = self.parse_block()?;
3994
3995        let mut elsifs = Vec::new();
3996        let mut else_block = None;
3997
3998        loop {
3999            if let Token::Ident(ref kw) = self.peek().clone() {
4000                if kw == "elsif" {
4001                    self.advance();
4002                    self.expect(&Token::LParen)?;
4003                    let mut c = self.parse_expression()?;
4004                    Self::mark_match_scalar_g_for_boolean_condition(&mut c);
4005                    self.expect(&Token::RParen)?;
4006                    let b = self.parse_block()?;
4007                    elsifs.push((c, b));
4008                    continue;
4009                }
4010                if kw == "else" {
4011                    self.advance();
4012                    else_block = Some(self.parse_block()?);
4013                }
4014            }
4015            break;
4016        }
4017
4018        Ok(Statement {
4019            label: None,
4020            kind: StmtKind::If {
4021                condition: cond,
4022                body,
4023                elsifs,
4024                else_block,
4025            },
4026            line,
4027        })
4028    }
4029
4030    /// `if let PAT = EXPR { ... } [ else { ... } ]` — desugars to [`ExprKind::AlgebraicMatch`].
4031    fn parse_if_let(&mut self, line: usize) -> StrykeResult<Statement> {
4032        self.advance(); // `let`
4033        let pattern = self.parse_match_pattern()?;
4034        self.expect(&Token::Assign)?;
4035        // Use assign-level parsing so a following `{ ... }` is the `if let` body, not an anon hash.
4036        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_add(1);
4037        let rhs = self.parse_assign_expr();
4038        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_sub(1);
4039        let rhs = rhs?;
4040        let then_block = self.parse_block()?;
4041        let else_block_opt = match self.peek().clone() {
4042            Token::Ident(ref kw) if kw == "else" => {
4043                self.advance();
4044                Some(self.parse_block()?)
4045            }
4046            Token::Ident(ref kw) if kw == "elsif" => {
4047                return Err(self.syntax_err(
4048                    "`if let` does not support `elsif`; use `else { }` or a full `match`",
4049                    self.peek_line(),
4050                ));
4051            }
4052            _ => None,
4053        };
4054        let then_expr = Self::expr_do_anon_block(then_block, line);
4055        let else_expr = if let Some(eb) = else_block_opt {
4056            Self::expr_do_anon_block(eb, line)
4057        } else {
4058            Expr {
4059                kind: ExprKind::Undef,
4060                line,
4061            }
4062        };
4063        let arms = vec![
4064            MatchArm {
4065                pattern,
4066                guard: None,
4067                body: then_expr,
4068            },
4069            MatchArm {
4070                pattern: MatchPattern::Any,
4071                guard: None,
4072                body: else_expr,
4073            },
4074        ];
4075        Ok(Statement {
4076            label: None,
4077            kind: StmtKind::Expression(Expr {
4078                kind: ExprKind::AlgebraicMatch {
4079                    subject: Box::new(rhs),
4080                    arms,
4081                },
4082                line,
4083            }),
4084            line,
4085        })
4086    }
4087
4088    fn expr_do_anon_block(block: Block, outer_line: usize) -> Expr {
4089        let inner_line = block.first().map(|s| s.line).unwrap_or(outer_line);
4090        Expr {
4091            kind: ExprKind::Do(Box::new(Expr {
4092                kind: ExprKind::CodeRef {
4093                    params: vec![],
4094                    body: block,
4095                },
4096                line: inner_line,
4097            })),
4098            line: outer_line,
4099        }
4100    }
4101
4102    fn parse_unless(&mut self) -> StrykeResult<Statement> {
4103        let line = self.peek_line();
4104        self.advance(); // 'unless'
4105        self.expect(&Token::LParen)?;
4106        let mut cond = self.parse_expression()?;
4107        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
4108        self.expect(&Token::RParen)?;
4109        let body = self.parse_block()?;
4110        let else_block = if let Token::Ident(ref kw) = self.peek().clone() {
4111            if kw == "else" {
4112                self.advance();
4113                Some(self.parse_block()?)
4114            } else {
4115                None
4116            }
4117        } else {
4118            None
4119        };
4120        Ok(Statement {
4121            label: None,
4122            kind: StmtKind::Unless {
4123                condition: cond,
4124                body,
4125                else_block,
4126            },
4127            line,
4128        })
4129    }
4130
4131    fn parse_while(&mut self) -> StrykeResult<Statement> {
4132        let line = self.peek_line();
4133        self.advance(); // 'while'
4134        if matches!(self.peek(), Token::Ident(ref s) if s == "let") {
4135            if crate::compat_mode() {
4136                return Err(self.syntax_err(
4137                    "`while let` is a stryke extension (disabled by --compat)",
4138                    line,
4139                ));
4140            }
4141            return self.parse_while_let(line);
4142        }
4143        self.expect(&Token::LParen)?;
4144        let mut cond = self.parse_expression()?;
4145        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
4146        self.expect(&Token::RParen)?;
4147        let body = self.parse_block()?;
4148        let continue_block = self.parse_optional_continue_block()?;
4149        Ok(Statement {
4150            label: None,
4151            kind: StmtKind::While {
4152                condition: cond,
4153                body,
4154                label: None,
4155                continue_block,
4156            },
4157            line,
4158        })
4159    }
4160
4161    /// `while let PAT = EXPR { ... }` — desugars to a `match` that returns 0/1 plus `unless ($tmp) { last }`
4162    /// so bytecode does not run `last` inside a tree-assisted [`Op::AlgebraicMatch`] arm.
4163    fn parse_while_let(&mut self, line: usize) -> StrykeResult<Statement> {
4164        self.advance(); // `let`
4165        let pattern = self.parse_match_pattern()?;
4166        self.expect(&Token::Assign)?;
4167        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_add(1);
4168        let rhs = self.parse_assign_expr();
4169        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_sub(1);
4170        let rhs = rhs?;
4171        let mut user_body = self.parse_block()?;
4172        let continue_block = self.parse_optional_continue_block()?;
4173        user_body.push(Statement::new(
4174            StmtKind::Expression(Expr {
4175                kind: ExprKind::Integer(1),
4176                line,
4177            }),
4178            line,
4179        ));
4180        let tmp = format!("__while_let_{}", self.alloc_desugar_tmp());
4181        let match_expr = Expr {
4182            kind: ExprKind::AlgebraicMatch {
4183                subject: Box::new(rhs),
4184                arms: vec![
4185                    MatchArm {
4186                        pattern,
4187                        guard: None,
4188                        body: Self::expr_do_anon_block(user_body, line),
4189                    },
4190                    MatchArm {
4191                        pattern: MatchPattern::Any,
4192                        guard: None,
4193                        body: Expr {
4194                            kind: ExprKind::Integer(0),
4195                            line,
4196                        },
4197                    },
4198                ],
4199            },
4200            line,
4201        };
4202        let my_stmt = Statement::new(
4203            StmtKind::My(vec![VarDecl {
4204                sigil: Sigil::Scalar,
4205                name: tmp.clone(),
4206                initializer: Some(match_expr),
4207                frozen: false,
4208                type_annotation: None,
4209                list_context: false,
4210            }]),
4211            line,
4212        );
4213        let unless_last = Statement::new(
4214            StmtKind::Unless {
4215                condition: Expr {
4216                    kind: ExprKind::ScalarVar(tmp),
4217                    line,
4218                },
4219                body: vec![Statement::new(StmtKind::Last(None), line)],
4220                else_block: None,
4221            },
4222            line,
4223        );
4224        Ok(Statement::new(
4225            StmtKind::While {
4226                condition: Expr {
4227                    kind: ExprKind::Integer(1),
4228                    line,
4229                },
4230                body: vec![my_stmt, unless_last],
4231                label: None,
4232                continue_block,
4233            },
4234            line,
4235        ))
4236    }
4237
4238    fn parse_until(&mut self) -> StrykeResult<Statement> {
4239        let line = self.peek_line();
4240        self.advance(); // 'until'
4241        self.expect(&Token::LParen)?;
4242        let mut cond = self.parse_expression()?;
4243        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
4244        self.expect(&Token::RParen)?;
4245        let body = self.parse_block()?;
4246        let continue_block = self.parse_optional_continue_block()?;
4247        Ok(Statement {
4248            label: None,
4249            kind: StmtKind::Until {
4250                condition: cond,
4251                body,
4252                label: None,
4253                continue_block,
4254            },
4255            line,
4256        })
4257    }
4258
4259    /// `continue { ... }` after a loop body (optional).
4260    fn parse_optional_continue_block(&mut self) -> StrykeResult<Option<Block>> {
4261        if let Token::Ident(ref kw) = self.peek().clone() {
4262            if kw == "continue" {
4263                self.advance();
4264                return Ok(Some(self.parse_block()?));
4265            }
4266        }
4267        Ok(None)
4268    }
4269
4270    fn parse_for_or_foreach(&mut self) -> StrykeResult<Statement> {
4271        let line = self.peek_line();
4272        self.advance(); // 'for'
4273
4274        // Peek to determine if C-style for or foreach
4275        // C-style: for (init; cond; step)
4276        // foreach-style: for $var (list) or for (list)
4277        match self.peek() {
4278            Token::LParen => {
4279                // Check if next after ( is a semicolon or an assignment — C-style
4280                // Or if it's a list — foreach-style
4281                // Heuristic: if the token after ( is 'my' or '$' followed by
4282                // content that contains ';', it's C-style.
4283                let saved = self.pos;
4284                self.advance(); // consume (
4285                                // Look for semicolon at paren depth 0
4286                let mut depth = 1;
4287                let mut has_semi = false;
4288                let mut scan = self.pos;
4289                while scan < self.tokens.len() {
4290                    match &self.tokens[scan].0 {
4291                        Token::LParen => depth += 1,
4292                        Token::RParen => {
4293                            depth -= 1;
4294                            if depth == 0 {
4295                                break;
4296                            }
4297                        }
4298                        Token::Semicolon if depth == 1 => {
4299                            has_semi = true;
4300                            break;
4301                        }
4302                        _ => {}
4303                    }
4304                    scan += 1;
4305                }
4306                self.pos = saved;
4307
4308                if has_semi {
4309                    self.parse_c_style_for(line)
4310                } else {
4311                    // foreach without explicit var — uses $_
4312                    self.expect(&Token::LParen)?;
4313                    let list = self.parse_expression()?;
4314                    self.expect(&Token::RParen)?;
4315                    let body = self.parse_block()?;
4316                    let continue_block = self.parse_optional_continue_block()?;
4317                    Ok(Statement {
4318                        label: None,
4319                        kind: StmtKind::Foreach {
4320                            var: "_".to_string(),
4321                            list,
4322                            body,
4323                            label: None,
4324                            continue_block,
4325                        },
4326                        line,
4327                    })
4328                }
4329            }
4330            Token::Ident(ref kw) if kw == "my" => {
4331                self.advance(); // 'my'
4332                let var = self.parse_scalar_var_name()?;
4333                self.expect(&Token::LParen)?;
4334                let list = self.parse_expression()?;
4335                self.expect(&Token::RParen)?;
4336                let body = self.parse_block()?;
4337                let continue_block = self.parse_optional_continue_block()?;
4338                Ok(Statement {
4339                    label: None,
4340                    kind: StmtKind::Foreach {
4341                        var,
4342                        list,
4343                        body,
4344                        label: None,
4345                        continue_block,
4346                    },
4347                    line,
4348                })
4349            }
4350            Token::ScalarVar(_) => {
4351                let var = self.parse_scalar_var_name()?;
4352                self.expect(&Token::LParen)?;
4353                let list = self.parse_expression()?;
4354                self.expect(&Token::RParen)?;
4355                let body = self.parse_block()?;
4356                let continue_block = self.parse_optional_continue_block()?;
4357                Ok(Statement {
4358                    label: None,
4359                    kind: StmtKind::Foreach {
4360                        var,
4361                        list,
4362                        body,
4363                        label: None,
4364                        continue_block,
4365                    },
4366                    line,
4367                })
4368            }
4369            _ => self.parse_c_style_for(line),
4370        }
4371    }
4372
4373    fn parse_c_style_for(&mut self, line: usize) -> StrykeResult<Statement> {
4374        self.expect(&Token::LParen)?;
4375        let init = if self.eat(&Token::Semicolon) {
4376            None
4377        } else {
4378            let s = self.parse_statement()?;
4379            self.eat(&Token::Semicolon);
4380            Some(Box::new(s))
4381        };
4382        let mut condition = if matches!(self.peek(), Token::Semicolon) {
4383            None
4384        } else {
4385            Some(self.parse_expression()?)
4386        };
4387        if let Some(ref mut c) = condition {
4388            Self::mark_match_scalar_g_for_boolean_condition(c);
4389        }
4390        self.expect(&Token::Semicolon)?;
4391        let step = if matches!(self.peek(), Token::RParen) {
4392            None
4393        } else {
4394            Some(self.parse_expression()?)
4395        };
4396        self.expect(&Token::RParen)?;
4397        let body = self.parse_block()?;
4398        let continue_block = self.parse_optional_continue_block()?;
4399        Ok(Statement {
4400            label: None,
4401            kind: StmtKind::For {
4402                init,
4403                condition,
4404                step,
4405                body,
4406                label: None,
4407                continue_block,
4408            },
4409            line,
4410        })
4411    }
4412
4413    fn parse_foreach(&mut self) -> StrykeResult<Statement> {
4414        let line = self.peek_line();
4415        self.advance(); // 'foreach'
4416        let var = match self.peek() {
4417            Token::Ident(ref kw) if kw == "my" => {
4418                self.advance();
4419                self.parse_scalar_var_name()?
4420            }
4421            Token::ScalarVar(_) => self.parse_scalar_var_name()?,
4422            _ => "_".to_string(),
4423        };
4424        self.expect(&Token::LParen)?;
4425        let list = self.parse_expression()?;
4426        self.expect(&Token::RParen)?;
4427        let body = self.parse_block()?;
4428        let continue_block = self.parse_optional_continue_block()?;
4429        Ok(Statement {
4430            label: None,
4431            kind: StmtKind::Foreach {
4432                var,
4433                list,
4434                body,
4435                label: None,
4436                continue_block,
4437            },
4438            line,
4439        })
4440    }
4441
4442    fn parse_scalar_var_name(&mut self) -> StrykeResult<String> {
4443        match self.advance() {
4444            (Token::ScalarVar(name), _) => Ok(name),
4445            (tok, line) => {
4446                Err(self.syntax_err(format!("Expected scalar variable, got {:?}", tok), line))
4447            }
4448        }
4449    }
4450
4451    /// After `(` was consumed: Perl5 prototype characters until `)` (or `$)` + `{`).
4452    fn parse_legacy_sub_prototype_tail(&mut self) -> StrykeResult<String> {
4453        let mut s = String::new();
4454        loop {
4455            match self.peek().clone() {
4456                Token::RParen => {
4457                    self.advance();
4458                    break;
4459                }
4460                Token::Eof => {
4461                    return Err(self.syntax_err(
4462                        "Unterminated sub prototype (expected ')' before end of input)",
4463                        self.peek_line(),
4464                    ));
4465                }
4466                Token::ScalarVar(v) if v == ")" => {
4467                    // Lexer merges `$` + `)` into one token (`$)`). In `sub name ($) {`, the
4468                    // closing `)` of the prototype is not a separate `RParen` — next is `{`.
4469                    self.advance();
4470                    s.push('$');
4471                    if matches!(self.peek(), Token::LBrace) {
4472                        break;
4473                    }
4474                }
4475                Token::Ident(i) => {
4476                    let i = i.clone();
4477                    self.advance();
4478                    s.push_str(&i);
4479                }
4480                Token::Semicolon => {
4481                    self.advance();
4482                    s.push(';');
4483                }
4484                Token::LParen => {
4485                    self.advance();
4486                    s.push('(');
4487                }
4488                Token::LBracket => {
4489                    self.advance();
4490                    s.push('[');
4491                }
4492                Token::RBracket => {
4493                    self.advance();
4494                    s.push(']');
4495                }
4496                Token::Backslash => {
4497                    self.advance();
4498                    s.push('\\');
4499                }
4500                Token::Comma => {
4501                    self.advance();
4502                    s.push(',');
4503                }
4504                Token::ScalarVar(v) => {
4505                    let v = v.clone();
4506                    self.advance();
4507                    s.push('$');
4508                    s.push_str(&v);
4509                }
4510                Token::ArrayVar(v) => {
4511                    let v = v.clone();
4512                    self.advance();
4513                    s.push('@');
4514                    s.push_str(&v);
4515                }
4516                // Bare `@` / `%` in prototypes (e.g. Try::Tiny's `sub try (&;@)`).
4517                Token::ArrayAt => {
4518                    self.advance();
4519                    s.push('@');
4520                }
4521                Token::HashVar(v) => {
4522                    let v = v.clone();
4523                    self.advance();
4524                    s.push('%');
4525                    s.push_str(&v);
4526                }
4527                Token::HashPercent => {
4528                    self.advance();
4529                    s.push('%');
4530                }
4531                Token::Plus => {
4532                    self.advance();
4533                    s.push('+');
4534                }
4535                Token::Minus => {
4536                    self.advance();
4537                    s.push('-');
4538                }
4539                Token::BitAnd => {
4540                    self.advance();
4541                    s.push('&');
4542                }
4543                tok => {
4544                    return Err(self.syntax_err(
4545                        format!("Unexpected token in sub prototype: {:?}", tok),
4546                        self.peek_line(),
4547                    ));
4548                }
4549            }
4550        }
4551        Ok(s)
4552    }
4553
4554    fn sub_signature_list_starts_here(&self) -> bool {
4555        match self.peek() {
4556            Token::LBrace | Token::LBracket => true,
4557            Token::ScalarVar(name) if name != "$$" && name != ")" => true,
4558            Token::ArrayVar(_) | Token::HashVar(_) => true,
4559            _ => false,
4560        }
4561    }
4562
4563    fn parse_sub_signature_hash_key(&mut self) -> StrykeResult<String> {
4564        let (tok, line) = self.advance();
4565        match tok {
4566            Token::Ident(i) => Ok(i),
4567            Token::SingleString(s) | Token::DoubleString(s) => Ok(s),
4568            tok => Err(self.syntax_err(
4569                format!(
4570                    "sub signature: expected hash key (identifier or string), got {:?}",
4571                    tok
4572                ),
4573                line,
4574            )),
4575        }
4576    }
4577
4578    fn parse_sub_signature_param_list(&mut self) -> StrykeResult<Vec<SubSigParam>> {
4579        let mut params = Vec::new();
4580        loop {
4581            if matches!(self.peek(), Token::RParen) {
4582                break;
4583            }
4584            match self.peek().clone() {
4585                Token::ScalarVar(name) => {
4586                    if name == "$$" || name == ")" {
4587                        return Err(self.syntax_err(
4588                            format!(
4589                                "`{name}` cannot start a stryke sub signature (use legacy prototype `($$)` etc.)"
4590                            ),
4591                            self.peek_line(),
4592                        ));
4593                    }
4594                    self.advance();
4595                    let ty = if self.eat(&Token::Colon) {
4596                        match self.peek() {
4597                            Token::Ident(ref tname) => {
4598                                let tname = tname.clone();
4599                                self.advance();
4600                                Some(match tname.as_str() {
4601                                    "Int" => PerlTypeName::Int,
4602                                    "Str" => PerlTypeName::Str,
4603                                    "Float" => PerlTypeName::Float,
4604                                    "Bool" => PerlTypeName::Bool,
4605                                    "Array" => PerlTypeName::Array,
4606                                    "Hash" => PerlTypeName::Hash,
4607                                    "Ref" => PerlTypeName::Ref,
4608                                    "Any" => PerlTypeName::Any,
4609                                    _ => PerlTypeName::Struct(tname),
4610                                })
4611                            }
4612                            _ => {
4613                                return Err(self.syntax_err(
4614                                    "expected type name after `:` in sub signature",
4615                                    self.peek_line(),
4616                                ));
4617                            }
4618                        }
4619                    } else {
4620                        None
4621                    };
4622                    // Check for default value: `$x = expr`
4623                    let default = if self.eat(&Token::Assign) {
4624                        Some(Box::new(self.parse_ternary()?))
4625                    } else {
4626                        None
4627                    };
4628                    params.push(SubSigParam::Scalar(name, ty, default));
4629                }
4630                Token::ArrayVar(name) => {
4631                    self.advance();
4632                    let default = if self.eat(&Token::Assign) {
4633                        Some(Box::new(self.parse_ternary()?))
4634                    } else {
4635                        None
4636                    };
4637                    params.push(SubSigParam::Array(name, default));
4638                }
4639                Token::HashVar(name) => {
4640                    self.advance();
4641                    let default = if self.eat(&Token::Assign) {
4642                        Some(Box::new(self.parse_ternary()?))
4643                    } else {
4644                        None
4645                    };
4646                    params.push(SubSigParam::Hash(name, default));
4647                }
4648                Token::LBracket => {
4649                    self.advance();
4650                    let elems = self.parse_match_array_elems_until_rbracket()?;
4651                    params.push(SubSigParam::ArrayDestruct(elems));
4652                }
4653                Token::LBrace => {
4654                    self.advance();
4655                    let mut pairs = Vec::new();
4656                    loop {
4657                        if matches!(self.peek(), Token::RBrace | Token::Eof) {
4658                            break;
4659                        }
4660                        if self.eat(&Token::Comma) {
4661                            continue;
4662                        }
4663                        let key = self.parse_sub_signature_hash_key()?;
4664                        self.expect(&Token::FatArrow)?;
4665                        let bind = self.parse_scalar_var_name()?;
4666                        pairs.push((key, bind));
4667                        self.eat(&Token::Comma);
4668                    }
4669                    self.expect(&Token::RBrace)?;
4670                    params.push(SubSigParam::HashDestruct(pairs));
4671                }
4672                tok => {
4673                    return Err(self.syntax_err(
4674                        format!(
4675                            "expected `$name`, `[ ... ]`, or `{{ ... }}` in sub signature, got {:?}",
4676                            tok
4677                        ),
4678                        self.peek_line(),
4679                    ));
4680                }
4681            }
4682            match self.peek() {
4683                Token::Comma => {
4684                    self.advance();
4685                    if matches!(self.peek(), Token::RParen) {
4686                        return Err(self.syntax_err(
4687                            "trailing `,` before `)` in sub signature",
4688                            self.peek_line(),
4689                        ));
4690                    }
4691                }
4692                Token::RParen => break,
4693                _ => {
4694                    return Err(self.syntax_err(
4695                        format!(
4696                            "expected `,` or `)` after sub signature parameter, got {:?}",
4697                            self.peek()
4698                        ),
4699                        self.peek_line(),
4700                    ));
4701                }
4702            }
4703        }
4704        Ok(params)
4705    }
4706
4707    /// Optional `sub` parens: either a Perl 5 prototype string or a stryke **`$name` / `{ k => $v }`** signature.
4708    fn parse_sub_sig_or_prototype_opt(
4709        &mut self,
4710    ) -> StrykeResult<(Vec<SubSigParam>, Option<String>)> {
4711        if !matches!(self.peek(), Token::LParen) {
4712            return Ok((vec![], None));
4713        }
4714        self.advance();
4715        if matches!(self.peek(), Token::RParen) {
4716            self.advance();
4717            return Ok((vec![], Some(String::new())));
4718        }
4719        if self.sub_signature_list_starts_here() {
4720            let params = self.parse_sub_signature_param_list()?;
4721            self.expect(&Token::RParen)?;
4722            return Ok((params, None));
4723        }
4724        let proto = self.parse_legacy_sub_prototype_tail()?;
4725        Ok((vec![], Some(proto)))
4726    }
4727
4728    /// Optional subroutine attributes after name/prototype: `sub foo : lvalue { }`, `sub : ATTR(ARGS) { }`.
4729    fn parse_sub_attributes(&mut self) -> StrykeResult<()> {
4730        while self.eat(&Token::Colon) {
4731            match self.advance() {
4732                (Token::Ident(_), _) => {}
4733                (tok, line) => {
4734                    return Err(self.syntax_err(
4735                        format!("Expected attribute name after `:`, got {:?}", tok),
4736                        line,
4737                    ));
4738                }
4739            }
4740            if self.eat(&Token::LParen) {
4741                let mut depth = 1usize;
4742                while depth > 0 {
4743                    match self.advance().0 {
4744                        Token::LParen => depth += 1,
4745                        Token::RParen => {
4746                            depth -= 1;
4747                        }
4748                        Token::Eof => {
4749                            return Err(self.syntax_err(
4750                                "Unterminated sub attribute argument list",
4751                                self.peek_line(),
4752                            ));
4753                        }
4754                        _ => {}
4755                    }
4756                }
4757            }
4758        }
4759        Ok(())
4760    }
4761
4762    /// After `fn` + optional `(SIG)` + attrs: stryke-only `= EXPR` (one assign-level expression;
4763    /// no top-level `,` after the expression). Returns `None` if the next token is not `=`.
4764    fn try_parse_fn_assign_shorthand_body(&mut self) -> StrykeResult<Option<Block>> {
4765        if !self.eat(&Token::Assign) {
4766            return Ok(None);
4767        }
4768        let expr = self.parse_assign_expr()?;
4769        if matches!(self.peek(), Token::Comma) {
4770            return Err(self.syntax_err(
4771                "`fn ... =` allows only a single expression; use `fn ... { ... }` for multiple statements",
4772                self.peek_line(),
4773            ));
4774        }
4775        let eline = expr.line;
4776        self.eat(&Token::Semicolon);
4777        let mut body = vec![Statement {
4778            label: None,
4779            kind: StmtKind::Expression(expr),
4780            line: eline,
4781        }];
4782        Self::default_topic_for_sole_bareword(&mut body);
4783        Ok(Some(body))
4784    }
4785
4786    /// After `fn` + optional `(SIG)` + attrs: `{ ... }` or stryke-only `= EXPR` (see
4787    /// [`Self::try_parse_fn_assign_shorthand_body`]). `sub` always requires `{ ... }`.
4788    fn parse_fn_eq_body_or_block(&mut self, is_sub_keyword: bool) -> StrykeResult<Block> {
4789        if !is_sub_keyword {
4790            if let Some(block) = self.try_parse_fn_assign_shorthand_body()? {
4791                return Ok(block);
4792            }
4793        }
4794        self.parse_block()
4795    }
4796
4797    fn parse_sub_decl(&mut self, is_sub_keyword: bool) -> StrykeResult<Statement> {
4798        let line = self.peek_line();
4799        self.advance(); // 'sub' or 'fn'
4800        match self.peek().clone() {
4801            Token::Ident(_) => {
4802                let name = self.parse_package_qualified_identifier()?;
4803                // Topic-slot barewords (`_`, `_<`, `_<<`, `_<<<`, `_<<<<`,
4804                // `_0`, `_1`, …, `_N`, plus `_N<+` chain forms) are scalar
4805                // refs to the current/positional/outer topic. A user-defined
4806                // sub with any of these names — bare or package-qualified —
4807                // would shadow the topic in expression position and silently
4808                // break every `_`-aware builtin (`map { _ }`, `say _`,
4809                // `lc _`, …). Reject ALL forms at parse time, including
4810                // `Foo::_`, `Pkg::_0`, `My::Module::_<<<<`.
4811                let bare = name.rsplit("::").next().unwrap_or(&name);
4812                if Self::is_underscore_topic_slot(bare) {
4813                    return Err(self.syntax_err(
4814                        format!(
4815                            "`fn {}` would shadow the topic-slot scalar; pick a different name",
4816                            name
4817                        ),
4818                        line,
4819                    ));
4820                }
4821                if Self::is_reserved_special_var_name(bare) {
4822                    return Err(self.syntax_err(
4823                        format!(
4824                            "`fn {}` would shadow a Perl special variable / filehandle / compile-time token; pick a different name",
4825                            name
4826                        ),
4827                        line,
4828                    ));
4829                }
4830                // Allow shadowing builtins:
4831                // - In compat mode (full Perl 5)
4832                // - When parsing a module (imports should work)
4833                // Block shadowing:
4834                // - In user code (default mode, not parsing module)
4835                // - Always in no-interop mode
4836                let allow_shadow =
4837                    crate::compat_mode() || (self.parsing_module && !crate::no_interop_mode());
4838                if !allow_shadow {
4839                    self.check_udf_shadows_builtin(&name, line)?;
4840                }
4841                self.declared_subs.insert(name.clone());
4842                let (params, prototype) = self.parse_sub_sig_or_prototype_opt()?;
4843                self.parse_sub_attributes()?;
4844                let body = self.parse_fn_eq_body_or_block(is_sub_keyword)?;
4845                Ok(Statement {
4846                    label: None,
4847                    kind: StmtKind::SubDecl {
4848                        name,
4849                        params,
4850                        body,
4851                        prototype,
4852                    },
4853                    line,
4854                })
4855            }
4856            Token::LParen | Token::LBrace | Token::Colon => {
4857                // In no-interop mode, `sub {}` anonymous is not allowed — must use `fn {}`
4858                if is_sub_keyword && crate::no_interop_mode() {
4859                    return Err(self.syntax_err(
4860                        "stryke uses `fn {}` instead of `sub {}` (--no-interop)",
4861                        line,
4862                    ));
4863                }
4864                // Statement-level anonymous sub: `fn { }`, `sub () { }`, `sub :lvalue { }`
4865                let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
4866                self.parse_sub_attributes()?;
4867                let body = self.parse_fn_eq_body_or_block(is_sub_keyword)?;
4868                Ok(Statement {
4869                    label: None,
4870                    kind: StmtKind::Expression(Expr {
4871                        kind: ExprKind::CodeRef { params, body },
4872                        line,
4873                    }),
4874                    line,
4875                })
4876            }
4877            tok => {
4878                // Sigil-form topic-slot names (`fn $_`, `fn $_<`, `fn $_0`,
4879                // `fn @_`, `fn %_`, …) are also rejected with the same
4880                // foot-gun message as the bareword form. Without this branch
4881                // the user gets a confusing generic "Expected sub name" error.
4882                let topic_name = match &tok {
4883                    Token::ScalarVar(n) | Token::ArrayVar(n) | Token::HashVar(n)
4884                        if Self::is_underscore_topic_slot(n) =>
4885                    {
4886                        Some((
4887                            match &tok {
4888                                Token::ScalarVar(_) => '$',
4889                                Token::ArrayVar(_) => '@',
4890                                Token::HashVar(_) => '%',
4891                                _ => unreachable!(),
4892                            },
4893                            n.clone(),
4894                        ))
4895                    }
4896                    _ => None,
4897                };
4898                if let Some((sigil, n)) = topic_name {
4899                    return Err(self.syntax_err(
4900                        format!(
4901                            "`fn {}{}` would shadow the topic-slot scalar; pick a different name",
4902                            sigil, n
4903                        ),
4904                        self.peek_line(),
4905                    ));
4906                }
4907                // Sigil-form Perl special variables / globals — same foot-gun.
4908                // Catches `fn $@`, `fn $!`, `fn $/`, `fn $\\`, `fn $,`, `fn $;`,
4909                // `fn $"`, `fn $.`, `fn $0`, `fn $$`, `fn $?`, `fn $1`-`$9`,
4910                // `fn $^I`, `fn @ARGV`, `fn @INC`, `fn %ENV`, `fn %SIG`, etc.
4911                let special_var = match &tok {
4912                    Token::ScalarVar(n) | Token::ArrayVar(n) | Token::HashVar(n) => Some((
4913                        match &tok {
4914                            Token::ScalarVar(_) => '$',
4915                            Token::ArrayVar(_) => '@',
4916                            Token::HashVar(_) => '%',
4917                            _ => unreachable!(),
4918                        },
4919                        n.clone(),
4920                    )),
4921                    _ => None,
4922                };
4923                if let Some((sigil, n)) = special_var {
4924                    return Err(self.syntax_err(
4925                        format!(
4926                            "`fn {}{}` would shadow a Perl special variable / global; pick a different name",
4927                            sigil, n
4928                        ),
4929                        self.peek_line(),
4930                    ));
4931                }
4932                // After `fn`, `%` lexes as `Token::Percent` (modulo) rather
4933                // than a hash sigil — but `fn %ENV { }`, `fn %SIG { }`,
4934                // `fn %_ { }`, etc. all reach here. Emit the same foot-gun
4935                // message as the sigil-form catch above.
4936                if matches!(tok, Token::Percent) {
4937                    return Err(self.syntax_err(
4938                        "`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 { ... }`",
4939                        self.peek_line(),
4940                    ));
4941                }
4942                Err(self.syntax_err(
4943                    format!("Expected sub name, `(`, `{{`, or `:`, got {:?}", tok),
4944                    self.peek_line(),
4945                ))
4946            }
4947        }
4948    }
4949
4950    /// `before|after|around "<glob>" { ... }` — register AOP advice.
4951    /// The pattern is a glob (`*`, `?`) matched against the called sub's bare name.
4952    fn parse_advice_decl(&mut self, kind: crate::ast::AdviceKind) -> StrykeResult<Statement> {
4953        let line = self.peek_line();
4954        self.advance(); // before/after/around
4955        let pattern = match self.advance() {
4956            (Token::SingleString(s), _) | (Token::DoubleString(s), _) => s,
4957            (tok, err_line) => {
4958                return Err(self.syntax_err(
4959                    format!(
4960                        "Expected string-literal pattern after `{}`, got {:?}",
4961                        match kind {
4962                            crate::ast::AdviceKind::Before => "before",
4963                            crate::ast::AdviceKind::After => "after",
4964                            crate::ast::AdviceKind::Around => "around",
4965                        },
4966                        tok
4967                    ),
4968                    err_line,
4969                ));
4970            }
4971        };
4972        let body = self.parse_block()?;
4973        Ok(Statement {
4974            label: None,
4975            kind: StmtKind::AdviceDecl {
4976                kind,
4977                pattern,
4978                body,
4979            },
4980            line,
4981        })
4982    }
4983
4984    /// `struct Name { field => Type, ... ; fn method { } }`
4985    fn parse_struct_decl(&mut self) -> StrykeResult<Statement> {
4986        let line = self.peek_line();
4987        self.advance(); // struct
4988        let raw_name = self.parse_package_qualified_identifier().map_err(|_| {
4989            self.syntax_err(
4990                format!("Expected struct name, got {:?}", self.peek()),
4991                self.peek_line(),
4992            )
4993        })?;
4994        let name = if raw_name.contains("::") || self.current_package == "main" {
4995            raw_name
4996        } else {
4997            format!("{}::{}", self.current_package, raw_name)
4998        };
4999        self.expect(&Token::LBrace)?;
5000        let mut fields = Vec::new();
5001        let mut methods = Vec::new();
5002        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
5003            // Check for method definition: `fn name { }` or `fn name { }`
5004            let is_method = match self.peek() {
5005                Token::Ident(s) => s == "fn" || s == "sub",
5006                _ => false,
5007            };
5008            if is_method {
5009                let is_sub_keyword = matches!(self.peek(), Token::Ident(ref s) if s == "sub");
5010                self.advance(); // fn/sub
5011                let method_name = match self.advance() {
5012                    (Token::Ident(n), _) => n,
5013                    (tok, err_line) => {
5014                        return Err(self
5015                            .syntax_err(format!("Expected method name, got {:?}", tok), err_line))
5016                    }
5017                };
5018                // Parse optional signature: `($self, $arg: Type, ...)`
5019                let params = if self.eat(&Token::LParen) {
5020                    let p = self.parse_sub_signature_param_list()?;
5021                    self.expect(&Token::RParen)?;
5022                    p
5023                } else {
5024                    Vec::new()
5025                };
5026                let body = if is_sub_keyword {
5027                    self.parse_block()?
5028                } else {
5029                    self.parse_fn_eq_body_or_block(false)?
5030                };
5031                methods.push(crate::ast::StructMethod {
5032                    name: method_name,
5033                    params,
5034                    body,
5035                });
5036                // Optional trailing comma/semicolon after method
5037                self.eat(&Token::Comma);
5038                self.eat(&Token::Semicolon);
5039                continue;
5040            }
5041
5042            let field_name = match self.advance() {
5043                (Token::Ident(n), _) => n,
5044                (tok, err_line) => {
5045                    return Err(
5046                        self.syntax_err(format!("Expected field name, got {:?}", tok), err_line)
5047                    )
5048                }
5049            };
5050            // Support three forms:
5051            //   - `field => Type`   (Perl-style fat-comma)
5052            //   - `field: Type`     (Rust/class-style colon)
5053            //   - bare `field`      (implies Any type)
5054            let ty = if self.eat(&Token::FatArrow) || self.eat(&Token::Colon) {
5055                self.parse_type_name()?
5056            } else {
5057                crate::ast::PerlTypeName::Any
5058            };
5059            let default = if self.eat(&Token::Assign) {
5060                // Use parse_ternary to avoid consuming commas (next field separator)
5061                Some(self.parse_ternary()?)
5062            } else {
5063                None
5064            };
5065            fields.push(StructField {
5066                name: field_name,
5067                ty,
5068                default,
5069            });
5070            if !self.eat(&Token::Comma) {
5071                // Also allow semicolons as field separators
5072                self.eat(&Token::Semicolon);
5073            }
5074        }
5075        self.expect(&Token::RBrace)?;
5076        self.eat(&Token::Semicolon);
5077        Ok(Statement {
5078            label: None,
5079            kind: StmtKind::StructDecl {
5080                def: StructDef {
5081                    name,
5082                    fields,
5083                    methods,
5084                },
5085            },
5086            line,
5087        })
5088    }
5089
5090    /// `enum Name { Variant1, Variant2 => Type, ... }`
5091    fn parse_enum_decl(&mut self) -> StrykeResult<Statement> {
5092        let line = self.peek_line();
5093        self.advance(); // enum
5094        let raw_name = self.parse_package_qualified_identifier().map_err(|_| {
5095            self.syntax_err(
5096                format!("Expected enum name, got {:?}", self.peek()),
5097                self.peek_line(),
5098            )
5099        })?;
5100        let name = if raw_name.contains("::") || self.current_package == "main" {
5101            raw_name
5102        } else {
5103            format!("{}::{}", self.current_package, raw_name)
5104        };
5105        self.expect(&Token::LBrace)?;
5106        let mut variants = Vec::new();
5107        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
5108            let variant_name = match self.advance() {
5109                (Token::Ident(n), _) => n,
5110                (tok, err_line) => {
5111                    return Err(
5112                        self.syntax_err(format!("Expected variant name, got {:?}", tok), err_line)
5113                    )
5114                }
5115            };
5116            let ty = if self.eat(&Token::FatArrow) {
5117                Some(self.parse_type_name()?)
5118            } else {
5119                None
5120            };
5121            variants.push(EnumVariant {
5122                name: variant_name,
5123                ty,
5124            });
5125            if !self.eat(&Token::Comma) {
5126                self.eat(&Token::Semicolon);
5127            }
5128        }
5129        self.expect(&Token::RBrace)?;
5130        self.eat(&Token::Semicolon);
5131        Ok(Statement {
5132            label: None,
5133            kind: StmtKind::EnumDecl {
5134                def: EnumDef { name, variants },
5135            },
5136            line,
5137        })
5138    }
5139
5140    /// `[abstract|final] class Name extends Parent impl Trait { fields; methods }`
5141    fn parse_class_decl(&mut self, is_abstract: bool, is_final: bool) -> StrykeResult<Statement> {
5142        use crate::ast::{ClassDef, ClassField, ClassMethod, ClassStaticField, Visibility};
5143        let line = self.peek_line();
5144        self.advance(); // class
5145        let raw_name = self.parse_package_qualified_identifier().map_err(|_| {
5146            self.syntax_err(
5147                format!("Expected class name, got {:?}", self.peek()),
5148                self.peek_line(),
5149            )
5150        })?;
5151        // Bare `class Point` inside `package Geo` registers as `Geo::Point`,
5152        // matching the unqualified-fn rule. Already-qualified names pass
5153        // through unchanged, and `main` keeps the bare spelling so
5154        // existing test code that calls `Point->new(...)` still resolves.
5155        let name = if raw_name.contains("::") || self.current_package == "main" {
5156            raw_name
5157        } else {
5158            format!("{}::{}", self.current_package, raw_name)
5159        };
5160
5161        // Parse `extends Parent1, Parent2` (each may be namespaced: `Foo::Base`)
5162        let mut extends = Vec::new();
5163        if matches!(self.peek(), Token::Ident(ref s) if s == "extends") {
5164            self.advance(); // extends
5165            loop {
5166                let parent = self.parse_package_qualified_identifier().map_err(|_| {
5167                    self.syntax_err(
5168                        format!(
5169                            "Expected parent class name after `extends`, got {:?}",
5170                            self.peek()
5171                        ),
5172                        self.peek_line(),
5173                    )
5174                })?;
5175                extends.push(parent);
5176                if !self.eat(&Token::Comma) {
5177                    break;
5178                }
5179            }
5180        }
5181
5182        // Parse `impl Trait1, Trait2` (each may be namespaced: `Foo::Trait`)
5183        let mut implements = Vec::new();
5184        if matches!(self.peek(), Token::Ident(ref s) if s == "impl") {
5185            self.advance(); // impl
5186            loop {
5187                let trait_name = self.parse_package_qualified_identifier().map_err(|_| {
5188                    self.syntax_err(
5189                        format!("Expected trait name after `impl`, got {:?}", self.peek()),
5190                        self.peek_line(),
5191                    )
5192                })?;
5193                implements.push(trait_name);
5194                if !self.eat(&Token::Comma) {
5195                    break;
5196                }
5197            }
5198        }
5199
5200        self.expect(&Token::LBrace)?;
5201        let mut fields = Vec::new();
5202        let mut methods = Vec::new();
5203        let mut static_fields = Vec::new();
5204
5205        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
5206            // Check for visibility modifier
5207            let visibility = match self.peek() {
5208                Token::Ident(ref s) if s == "pub" => {
5209                    self.advance();
5210                    Visibility::Public
5211                }
5212                Token::Ident(ref s) if s == "priv" => {
5213                    self.advance();
5214                    Visibility::Private
5215                }
5216                Token::Ident(ref s) if s == "prot" => {
5217                    self.advance();
5218                    Visibility::Protected
5219                }
5220                _ => Visibility::Public, // default public
5221            };
5222
5223            // Check for static field: `static name: Type = default`
5224            if matches!(self.peek(), Token::Ident(ref s) if s == "static") {
5225                self.advance(); // static
5226
5227                // Could be a static method (`static fn`) or static field
5228                if matches!(self.peek(), Token::Ident(ref s) if s == "fn" || s == "sub") {
5229                    // static fn is same as fn Self.name — handled below but not here
5230                    return Err(self.syntax_err(
5231                        "use `fn Self.name` for static methods, not `static fn`",
5232                        self.peek_line(),
5233                    ));
5234                }
5235
5236                let field_name = match self.advance() {
5237                    (Token::Ident(n), _) => n,
5238                    (tok, err_line) => {
5239                        return Err(self.syntax_err(
5240                            format!("Expected static field name, got {:?}", tok),
5241                            err_line,
5242                        ))
5243                    }
5244                };
5245
5246                let ty = if self.eat(&Token::Colon) {
5247                    self.parse_type_name()?
5248                } else {
5249                    crate::ast::PerlTypeName::Any
5250                };
5251
5252                let default = if self.eat(&Token::Assign) {
5253                    Some(self.parse_ternary()?)
5254                } else {
5255                    None
5256                };
5257
5258                static_fields.push(ClassStaticField {
5259                    name: field_name,
5260                    ty,
5261                    visibility,
5262                    default,
5263                });
5264
5265                if !self.eat(&Token::Comma) {
5266                    self.eat(&Token::Semicolon);
5267                }
5268                continue;
5269            }
5270
5271            // Check for `final` modifier before fn
5272            let method_is_final = matches!(self.peek(), Token::Ident(ref s) if s == "final");
5273            if method_is_final {
5274                self.advance(); // final
5275            }
5276
5277            // Check for method: `fn name` or `fn Self.name` (static)
5278            let is_method = matches!(self.peek(), Token::Ident(ref s) if s == "fn" || s == "sub");
5279            if is_method {
5280                self.advance(); // fn/sub
5281
5282                // Check for static method: `fn Self.name`
5283                let is_static = matches!(self.peek(), Token::Ident(ref s) if s == "Self");
5284                if is_static {
5285                    self.advance(); // Self
5286                    self.expect(&Token::Dot)?;
5287                }
5288
5289                let method_name = match self.advance() {
5290                    (Token::Ident(n), _) => n,
5291                    (tok, err_line) => {
5292                        return Err(self
5293                            .syntax_err(format!("Expected method name, got {:?}", tok), err_line))
5294                    }
5295                };
5296
5297                // Parse optional signature
5298                let params = if self.eat(&Token::LParen) {
5299                    let p = self.parse_sub_signature_param_list()?;
5300                    self.expect(&Token::RParen)?;
5301                    p
5302                } else {
5303                    Vec::new()
5304                };
5305
5306                // Body: `{ ... }`, or `= expr` (same rules as top-level `fn`), or omitted (abstract)
5307                let body = if let Some(b) = self.try_parse_fn_assign_shorthand_body()? {
5308                    Some(b)
5309                } else if matches!(self.peek(), Token::LBrace) {
5310                    Some(self.parse_block()?)
5311                } else {
5312                    None
5313                };
5314
5315                methods.push(ClassMethod {
5316                    name: method_name,
5317                    params,
5318                    body,
5319                    visibility,
5320                    is_static,
5321                    is_final: method_is_final,
5322                });
5323                self.eat(&Token::Comma);
5324                self.eat(&Token::Semicolon);
5325                continue;
5326            } else if method_is_final {
5327                return Err(self.syntax_err("`final` must be followed by `fn`", self.peek_line()));
5328            }
5329
5330            // Parse field: `name: Type = default`
5331            let field_name = match self.advance() {
5332                (Token::Ident(n), _) => n,
5333                (tok, err_line) => {
5334                    return Err(
5335                        self.syntax_err(format!("Expected field name, got {:?}", tok), err_line)
5336                    )
5337                }
5338            };
5339
5340            // Type via colon (`name: Type`) OR fat-comma (`name => Type`).
5341            // The Perl-flavored struct-style fat-comma is accepted on
5342            // classes for symmetry with struct fields.
5343            let ty = if self.eat(&Token::Colon) || self.eat(&Token::FatArrow) {
5344                self.parse_type_name()?
5345            } else {
5346                crate::ast::PerlTypeName::Any
5347            };
5348
5349            // Default value after `=`
5350            let default = if self.eat(&Token::Assign) {
5351                Some(self.parse_ternary()?)
5352            } else {
5353                None
5354            };
5355
5356            fields.push(ClassField {
5357                name: field_name,
5358                ty,
5359                visibility,
5360                default,
5361            });
5362
5363            if !self.eat(&Token::Comma) {
5364                self.eat(&Token::Semicolon);
5365            }
5366        }
5367
5368        self.expect(&Token::RBrace)?;
5369        self.eat(&Token::Semicolon);
5370
5371        Ok(Statement {
5372            label: None,
5373            kind: StmtKind::ClassDecl {
5374                def: ClassDef {
5375                    name,
5376                    is_abstract,
5377                    is_final,
5378                    extends,
5379                    implements,
5380                    fields,
5381                    methods,
5382                    static_fields,
5383                },
5384            },
5385            line,
5386        })
5387    }
5388
5389    /// `trait Name { fn required; fn with_default { } }`
5390    fn parse_trait_decl(&mut self) -> StrykeResult<Statement> {
5391        use crate::ast::{ClassMethod, TraitDef, Visibility};
5392        let line = self.peek_line();
5393        self.advance(); // trait
5394        let raw_name = self.parse_package_qualified_identifier().map_err(|_| {
5395            self.syntax_err(
5396                format!("Expected trait name, got {:?}", self.peek()),
5397                self.peek_line(),
5398            )
5399        })?;
5400        let name = if raw_name.contains("::") || self.current_package == "main" {
5401            raw_name
5402        } else {
5403            format!("{}::{}", self.current_package, raw_name)
5404        };
5405
5406        self.expect(&Token::LBrace)?;
5407        let mut methods = Vec::new();
5408
5409        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
5410            // Optional visibility
5411            let visibility = match self.peek() {
5412                Token::Ident(ref s) if s == "pub" => {
5413                    self.advance();
5414                    Visibility::Public
5415                }
5416                Token::Ident(ref s) if s == "priv" => {
5417                    self.advance();
5418                    Visibility::Private
5419                }
5420                Token::Ident(ref s) if s == "prot" => {
5421                    self.advance();
5422                    Visibility::Protected
5423                }
5424                _ => Visibility::Public,
5425            };
5426
5427            // Expect `fn` or `sub`
5428            if !matches!(self.peek(), Token::Ident(ref s) if s == "fn" || s == "sub") {
5429                return Err(self.syntax_err("Expected `fn` in trait definition", self.peek_line()));
5430            }
5431            self.advance(); // fn/sub
5432
5433            let method_name = match self.advance() {
5434                (Token::Ident(n), _) => n,
5435                (tok, err_line) => {
5436                    return Err(
5437                        self.syntax_err(format!("Expected method name, got {:?}", tok), err_line)
5438                    )
5439                }
5440            };
5441
5442            // Optional signature
5443            let params = if self.eat(&Token::LParen) {
5444                let p = self.parse_sub_signature_param_list()?;
5445                self.expect(&Token::RParen)?;
5446                p
5447            } else {
5448                Vec::new()
5449            };
5450
5451            // Body: `{ ... }`, `= expr`, or omitted (required method)
5452            let body = if let Some(b) = self.try_parse_fn_assign_shorthand_body()? {
5453                Some(b)
5454            } else if matches!(self.peek(), Token::LBrace) {
5455                Some(self.parse_block()?)
5456            } else {
5457                None
5458            };
5459
5460            methods.push(ClassMethod {
5461                name: method_name,
5462                params,
5463                body,
5464                visibility,
5465                is_static: false,
5466                is_final: false,
5467            });
5468
5469            self.eat(&Token::Comma);
5470            self.eat(&Token::Semicolon);
5471        }
5472
5473        self.expect(&Token::RBrace)?;
5474        self.eat(&Token::Semicolon);
5475
5476        Ok(Statement {
5477            label: None,
5478            kind: StmtKind::TraitDecl {
5479                def: TraitDef { name, methods },
5480            },
5481            line,
5482        })
5483    }
5484
5485    fn local_simple_target_to_var_decl(target: &Expr) -> Option<VarDecl> {
5486        match &target.kind {
5487            ExprKind::ScalarVar(name) => Some(VarDecl {
5488                sigil: Sigil::Scalar,
5489                name: name.clone(),
5490                initializer: None,
5491                frozen: false,
5492                type_annotation: None,
5493                list_context: false,
5494            }),
5495            ExprKind::ArrayVar(name) => Some(VarDecl {
5496                sigil: Sigil::Array,
5497                name: name.clone(),
5498                initializer: None,
5499                frozen: false,
5500                type_annotation: None,
5501                list_context: false,
5502            }),
5503            ExprKind::HashVar(name) => Some(VarDecl {
5504                sigil: Sigil::Hash,
5505                name: name.clone(),
5506                initializer: None,
5507                frozen: false,
5508                type_annotation: None,
5509                list_context: false,
5510            }),
5511            ExprKind::Typeglob(name) => Some(VarDecl {
5512                sigil: Sigil::Typeglob,
5513                name: name.clone(),
5514                initializer: None,
5515                frozen: false,
5516                type_annotation: None,
5517                list_context: false,
5518            }),
5519            _ => None,
5520        }
5521    }
5522
5523    fn parse_decl_array_destructure(
5524        &mut self,
5525        keyword: &str,
5526        line: usize,
5527    ) -> StrykeResult<Statement> {
5528        self.expect(&Token::LBracket)?;
5529        let elems = self.parse_match_array_elems_until_rbracket()?;
5530        self.expect(&Token::Assign)?;
5531        self.suppress_scalar_hash_brace += 1;
5532        let rhs = self.parse_expression()?;
5533        self.suppress_scalar_hash_brace -= 1;
5534        let stmt = self.desugar_array_destructure(keyword, line, elems, rhs)?;
5535        self.parse_stmt_postfix_modifier(stmt)
5536    }
5537
5538    fn parse_decl_hash_destructure(
5539        &mut self,
5540        keyword: &str,
5541        line: usize,
5542    ) -> StrykeResult<Statement> {
5543        let MatchPattern::Hash(pairs) = self.parse_match_hash_pattern()? else {
5544            unreachable!("parse_match_hash_pattern returns Hash");
5545        };
5546        self.expect(&Token::Assign)?;
5547        self.suppress_scalar_hash_brace += 1;
5548        let rhs = self.parse_expression()?;
5549        self.suppress_scalar_hash_brace -= 1;
5550        let stmt = self.desugar_hash_destructure(keyword, line, pairs, rhs)?;
5551        self.parse_stmt_postfix_modifier(stmt)
5552    }
5553
5554    fn desugar_array_destructure(
5555        &mut self,
5556        keyword: &str,
5557        line: usize,
5558        elems: Vec<MatchArrayElem>,
5559        rhs: Expr,
5560    ) -> StrykeResult<Statement> {
5561        let tmp = format!("__stryke_ds_{}", self.alloc_desugar_tmp());
5562        let mut stmts: Vec<Statement> = Vec::new();
5563        stmts.push(destructure_stmt_from_var_decls(
5564            keyword,
5565            vec![VarDecl {
5566                sigil: Sigil::Scalar,
5567                name: tmp.clone(),
5568                initializer: Some(rhs),
5569                frozen: false,
5570                type_annotation: None,
5571                list_context: false,
5572            }],
5573            line,
5574        ));
5575
5576        let has_rest = elems
5577            .iter()
5578            .any(|e| matches!(e, MatchArrayElem::Rest | MatchArrayElem::RestBind(_)));
5579        let fixed_slots = elems
5580            .iter()
5581            .filter(|e| {
5582                matches!(
5583                    e,
5584                    MatchArrayElem::CaptureScalar(_) | MatchArrayElem::Expr(_)
5585                )
5586            })
5587            .count();
5588        if !has_rest {
5589            let cond = Expr {
5590                kind: ExprKind::BinOp {
5591                    left: Box::new(destructure_expr_array_len(&tmp, line)),
5592                    op: BinOp::NumEq,
5593                    right: Box::new(Expr {
5594                        kind: ExprKind::Integer(fixed_slots as i64),
5595                        line,
5596                    }),
5597                },
5598                line,
5599            };
5600            stmts.push(destructure_stmt_unless_die(
5601                line,
5602                cond,
5603                "array destructure: length mismatch",
5604            ));
5605        }
5606
5607        let mut idx: i64 = 0;
5608        for elem in elems {
5609            match elem {
5610                MatchArrayElem::Rest => break,
5611                MatchArrayElem::RestBind(name) => {
5612                    let list_source = Expr {
5613                        kind: ExprKind::Deref {
5614                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
5615                            kind: Sigil::Array,
5616                        },
5617                        line,
5618                    };
5619                    let last_ix = Expr {
5620                        kind: ExprKind::BinOp {
5621                            left: Box::new(destructure_expr_array_len(&tmp, line)),
5622                            op: BinOp::Sub,
5623                            right: Box::new(Expr {
5624                                kind: ExprKind::Integer(1),
5625                                line,
5626                            }),
5627                        },
5628                        line,
5629                    };
5630                    let range = Expr {
5631                        kind: ExprKind::Range {
5632                            from: Box::new(Expr {
5633                                kind: ExprKind::Integer(idx),
5634                                line,
5635                            }),
5636                            to: Box::new(last_ix),
5637                            exclusive: false,
5638                            step: None,
5639                        },
5640                        line,
5641                    };
5642                    let slice = Expr {
5643                        kind: ExprKind::AnonymousListSlice {
5644                            source: Box::new(list_source),
5645                            indices: vec![range],
5646                        },
5647                        line,
5648                    };
5649                    stmts.push(destructure_stmt_from_var_decls(
5650                        keyword,
5651                        vec![VarDecl {
5652                            sigil: Sigil::Array,
5653                            name,
5654                            initializer: Some(slice),
5655                            frozen: false,
5656                            type_annotation: None,
5657                            list_context: false,
5658                        }],
5659                        line,
5660                    ));
5661                    break;
5662                }
5663                MatchArrayElem::CaptureScalar(name) => {
5664                    let arrow = Expr {
5665                        kind: ExprKind::ArrowDeref {
5666                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
5667                            index: Box::new(Expr {
5668                                kind: ExprKind::Integer(idx),
5669                                line,
5670                            }),
5671                            kind: DerefKind::Array,
5672                        },
5673                        line,
5674                    };
5675                    stmts.push(destructure_stmt_from_var_decls(
5676                        keyword,
5677                        vec![VarDecl {
5678                            sigil: Sigil::Scalar,
5679                            name,
5680                            initializer: Some(arrow),
5681                            frozen: false,
5682                            list_context: false,
5683                            type_annotation: None,
5684                        }],
5685                        line,
5686                    ));
5687                    idx += 1;
5688                }
5689                MatchArrayElem::Expr(e) => {
5690                    let elem_subj = Expr {
5691                        kind: ExprKind::ArrowDeref {
5692                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
5693                            index: Box::new(Expr {
5694                                kind: ExprKind::Integer(idx),
5695                                line,
5696                            }),
5697                            kind: DerefKind::Array,
5698                        },
5699                        line,
5700                    };
5701                    let match_expr = Expr {
5702                        kind: ExprKind::AlgebraicMatch {
5703                            subject: Box::new(elem_subj),
5704                            arms: vec![
5705                                MatchArm {
5706                                    pattern: MatchPattern::Value(Box::new(e.clone())),
5707                                    guard: None,
5708                                    body: Expr {
5709                                        kind: ExprKind::Integer(0),
5710                                        line,
5711                                    },
5712                                },
5713                                MatchArm {
5714                                    pattern: MatchPattern::Any,
5715                                    guard: None,
5716                                    body: Expr {
5717                                        kind: ExprKind::Die(vec![Expr {
5718                                            kind: ExprKind::String(
5719                                                "array destructure: element pattern mismatch"
5720                                                    .to_string(),
5721                                            ),
5722                                            line,
5723                                        }]),
5724                                        line,
5725                                    },
5726                                },
5727                            ],
5728                        },
5729                        line,
5730                    };
5731                    stmts.push(Statement {
5732                        label: None,
5733                        kind: StmtKind::Expression(match_expr),
5734                        line,
5735                    });
5736                    idx += 1;
5737                }
5738            }
5739        }
5740
5741        Ok(Statement {
5742            label: None,
5743            kind: StmtKind::StmtGroup(stmts),
5744            line,
5745        })
5746    }
5747
5748    fn desugar_hash_destructure(
5749        &mut self,
5750        keyword: &str,
5751        line: usize,
5752        pairs: Vec<MatchHashPair>,
5753        rhs: Expr,
5754    ) -> StrykeResult<Statement> {
5755        let tmp = format!("__stryke_ds_{}", self.alloc_desugar_tmp());
5756        let mut stmts: Vec<Statement> = Vec::new();
5757        stmts.push(destructure_stmt_from_var_decls(
5758            keyword,
5759            vec![VarDecl {
5760                sigil: Sigil::Scalar,
5761                name: tmp.clone(),
5762                initializer: Some(rhs),
5763                frozen: false,
5764                type_annotation: None,
5765                list_context: false,
5766            }],
5767            line,
5768        ));
5769
5770        for pair in pairs {
5771            match pair {
5772                MatchHashPair::KeyOnly { key } => {
5773                    let exists_op = Expr {
5774                        kind: ExprKind::Exists(Box::new(Expr {
5775                            kind: ExprKind::ArrowDeref {
5776                                expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
5777                                index: Box::new(key),
5778                                kind: DerefKind::Hash,
5779                            },
5780                            line,
5781                        })),
5782                        line,
5783                    };
5784                    stmts.push(destructure_stmt_unless_die(
5785                        line,
5786                        exists_op,
5787                        "hash destructure: missing required key",
5788                    ));
5789                }
5790                MatchHashPair::Capture { key, name } => {
5791                    let init = Expr {
5792                        kind: ExprKind::ArrowDeref {
5793                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
5794                            index: Box::new(key),
5795                            kind: DerefKind::Hash,
5796                        },
5797                        line,
5798                    };
5799                    stmts.push(destructure_stmt_from_var_decls(
5800                        keyword,
5801                        vec![VarDecl {
5802                            sigil: Sigil::Scalar,
5803                            name,
5804                            initializer: Some(init),
5805                            frozen: false,
5806                            type_annotation: None,
5807                            list_context: false,
5808                        }],
5809                        line,
5810                    ));
5811                }
5812            }
5813        }
5814
5815        Ok(Statement {
5816            label: None,
5817            kind: StmtKind::StmtGroup(stmts),
5818            line,
5819        })
5820    }
5821
5822    fn parse_my_our_local(
5823        &mut self,
5824        keyword: &str,
5825        allow_type_annotation: bool,
5826    ) -> StrykeResult<Statement> {
5827        let line = self.peek_line();
5828        self.advance(); // 'my'/'our'/'local'
5829
5830        if keyword == "local"
5831            && !matches!(self.peek(), Token::LParen | Token::LBracket | Token::LBrace)
5832        {
5833            let target = self.parse_postfix()?;
5834            let mut initializer: Option<Expr> = None;
5835            if self.eat(&Token::Assign) {
5836                initializer = Some(self.parse_expression()?);
5837            } else if matches!(
5838                self.peek(),
5839                Token::OrAssign | Token::DefinedOrAssign | Token::AndAssign
5840            ) {
5841                if matches!(&target.kind, ExprKind::Typeglob(_)) {
5842                    return Err(self.syntax_err(
5843                        "compound assignment on typeglob declaration is not supported",
5844                        self.peek_line(),
5845                    ));
5846                }
5847                let op = match self.peek().clone() {
5848                    Token::OrAssign => BinOp::LogOr,
5849                    Token::DefinedOrAssign => BinOp::DefinedOr,
5850                    Token::AndAssign => BinOp::LogAnd,
5851                    _ => unreachable!(),
5852                };
5853                self.advance();
5854                let rhs = self.parse_assign_expr()?;
5855                let tgt_line = target.line;
5856                initializer = Some(Expr {
5857                    kind: ExprKind::CompoundAssign {
5858                        target: Box::new(target.clone()),
5859                        op,
5860                        value: Box::new(rhs),
5861                    },
5862                    line: tgt_line,
5863                });
5864            }
5865
5866            let kind = if let Some(mut decl) = Self::local_simple_target_to_var_decl(&target) {
5867                decl.initializer = initializer;
5868                StmtKind::Local(vec![decl])
5869            } else {
5870                StmtKind::LocalExpr {
5871                    target,
5872                    initializer,
5873                }
5874            };
5875            let stmt = Statement {
5876                label: None,
5877                kind,
5878                line,
5879            };
5880            return self.parse_stmt_postfix_modifier(stmt);
5881        }
5882
5883        if matches!(self.peek(), Token::LBracket) {
5884            return self.parse_decl_array_destructure(keyword, line);
5885        }
5886        if matches!(self.peek(), Token::LBrace) {
5887            return self.parse_decl_hash_destructure(keyword, line);
5888        }
5889
5890        let mut decls = Vec::new();
5891        let used_parens = self.eat(&Token::LParen);
5892
5893        if used_parens {
5894            // my ($a, @b, %c)
5895            while !matches!(self.peek(), Token::RParen | Token::Eof) {
5896                let decl = self.parse_var_decl(allow_type_annotation)?;
5897                decls.push(decl);
5898                if !self.eat(&Token::Comma) {
5899                    break;
5900                }
5901            }
5902            self.expect(&Token::RParen)?;
5903        } else {
5904            decls.push(self.parse_var_decl(allow_type_annotation)?);
5905        }
5906        // my ($x) = @a  → list context on the scalar (gets first element, not count)
5907        if used_parens {
5908            for decl in &mut decls {
5909                decl.list_context = true;
5910            }
5911        }
5912
5913        // Optional initializer: my $x = expr — plus `our @EXPORT = our @EXPORT_OK = qw(...)` (Try::Tiny).
5914        if self.eat(&Token::Assign) {
5915            if keyword == "our" && decls.len() == 1 {
5916                while matches!(self.peek(), Token::Ident(ref i) if i == "our") {
5917                    self.advance();
5918                    decls.push(self.parse_var_decl(allow_type_annotation)?);
5919                    if !self.eat(&Token::Assign) {
5920                        return Err(self.syntax_err(
5921                            "expected `=` after `our` in chained our-declaration",
5922                            self.peek_line(),
5923                        ));
5924                    }
5925                }
5926            }
5927            let rhs_start_pos = self.pos;
5928            let mut val = self.parse_expression()?;
5929            let rhs_end_pos = self.pos;
5930            // Stryke implicit-coderef sugar: when the RHS contains a
5931            // *free* bare topic-slot reference (`_`, `_0`, `_1`, `_<`,
5932            // `_<<`, etc. — no `$` sigil), auto-wrap the RHS in
5933            // `fn { ... }`. Forces consistent coderef semantics across
5934            // every topic-using form:
5935            //   `my $sq  = _ * _`                     → CODE ref
5936            //   `my $up  = uc _`                      → CODE ref
5937            //   `my $rev = ~> _ >{...} rev join("")`  → CODE ref
5938            // To compute eagerly using the current topic, use the
5939            // explicit `$_` / `$_N` / `$_<` sigil-prefixed forms —
5940            // those keep Perl semantics and never auto-wrap.
5941            //
5942            // "Free" = at brace-depth 0 within the RHS token stream.
5943            // Any `_` inside `{ ... }` (closure body, hash literal,
5944            // map/grep/sort/match block) is bound to whatever defines
5945            // that block and doesn't trigger the wrap, so
5946            //   `my $r = call(fn { _ < 4 })`   — `_` is inner-fn's
5947            //   `my $h = { k => _ }`           — `_` is hash value at depth 1
5948            //   `my $kind = match { _ => x }`  — `_` is wildcard pattern
5949            // all stay eager.
5950            if !crate::compat_mode()
5951                && self.block_depth == 0
5952                && decls.len() == 1
5953                && matches!(decls[0].sigil, Sigil::Scalar)
5954                && !matches!(
5955                    val.kind,
5956                    ExprKind::CodeRef { .. }
5957                        | ExprKind::SubroutineRef(_)
5958                        | ExprKind::SubroutineCodeRef(_)
5959                        | ExprKind::DynamicSubCodeRef(_)
5960                )
5961                && self.rhs_has_free_bare_topic_slot(rhs_start_pos, rhs_end_pos)
5962            {
5963                let val_line = val.line;
5964                val = Expr {
5965                    kind: ExprKind::CodeRef {
5966                        params: Vec::new(),
5967                        body: vec![Statement {
5968                            label: None,
5969                            kind: StmtKind::Expression(val),
5970                            line: val_line,
5971                        }],
5972                    },
5973                    line: val_line,
5974                };
5975            }
5976            // Validate assignment for single variable declarations (not destructuring)
5977            // `my ($a, $b) = (1, 2)` is destructuring, not scalar-from-list
5978            if !crate::compat_mode() && decls.len() == 1 {
5979                let decl = &decls[0];
5980                let target_kind = match decl.sigil {
5981                    Sigil::Scalar => ExprKind::ScalarVar(decl.name.clone()),
5982                    Sigil::Array => ExprKind::ArrayVar(decl.name.clone()),
5983                    Sigil::Hash => ExprKind::HashVar(decl.name.clone()),
5984                    Sigil::Typeglob => {
5985                        // Skip validation for typeglob
5986                        if decls.len() == 1 {
5987                            decls[0].initializer = Some(val);
5988                        } else {
5989                            for d in &mut decls {
5990                                d.initializer = Some(val.clone());
5991                            }
5992                        }
5993                        return Ok(Statement {
5994                            label: None,
5995                            kind: match keyword {
5996                                "my" => StmtKind::My(decls),
5997                                "mysync" => StmtKind::MySync(decls),
5998                                "our" => StmtKind::Our(decls),
5999                                "oursync" => StmtKind::OurSync(decls),
6000                                "local" => StmtKind::Local(decls),
6001                                "state" => StmtKind::State(decls),
6002                                _ => unreachable!(),
6003                            },
6004                            line,
6005                        });
6006                    }
6007                };
6008                let target = Expr {
6009                    kind: target_kind,
6010                    line,
6011                };
6012                self.validate_assignment(&target, &val, line)?;
6013            }
6014            if decls.len() == 1 {
6015                decls[0].initializer = Some(val);
6016            } else {
6017                for decl in &mut decls {
6018                    decl.initializer = Some(val.clone());
6019                }
6020            }
6021        } else if decls.len() == 1 {
6022            // `our $Verbose ||= 0` (Exporter.pm) — compound assign on a single decl
6023            let op = match self.peek().clone() {
6024                Token::OrAssign => Some(BinOp::LogOr),
6025                Token::DefinedOrAssign => Some(BinOp::DefinedOr),
6026                Token::AndAssign => Some(BinOp::LogAnd),
6027                _ => None,
6028            };
6029            if let Some(op) = op {
6030                let d = &decls[0];
6031                if matches!(d.sigil, Sigil::Typeglob) {
6032                    return Err(self.syntax_err(
6033                        "compound assignment on typeglob declaration is not supported",
6034                        self.peek_line(),
6035                    ));
6036                }
6037                self.advance();
6038                let rhs = self.parse_assign_expr()?;
6039                let target = Expr {
6040                    kind: match d.sigil {
6041                        Sigil::Scalar => ExprKind::ScalarVar(d.name.clone()),
6042                        Sigil::Array => ExprKind::ArrayVar(d.name.clone()),
6043                        Sigil::Hash => ExprKind::HashVar(d.name.clone()),
6044                        Sigil::Typeglob => unreachable!(),
6045                    },
6046                    line,
6047                };
6048                decls[0].initializer = Some(Expr {
6049                    kind: ExprKind::CompoundAssign {
6050                        target: Box::new(target),
6051                        op,
6052                        value: Box::new(rhs),
6053                    },
6054                    line,
6055                });
6056            }
6057        }
6058
6059        let kind = match keyword {
6060            "my" => StmtKind::My(decls),
6061            "mysync" => StmtKind::MySync(decls),
6062            "our" => StmtKind::Our(decls),
6063            "oursync" => StmtKind::OurSync(decls),
6064            "local" => StmtKind::Local(decls),
6065            "state" => StmtKind::State(decls),
6066            _ => unreachable!(),
6067        };
6068        let stmt = Statement {
6069            label: None,
6070            kind,
6071            line,
6072        };
6073        // `my $x = 1 if $y;` — statement modifier applies to the whole declaration (Perl).
6074        self.parse_stmt_postfix_modifier(stmt)
6075    }
6076
6077    fn parse_var_decl(&mut self, allow_type_annotation: bool) -> StrykeResult<VarDecl> {
6078        let mut decl = match self.advance() {
6079            (Token::ScalarVar(name), _) => VarDecl {
6080                sigil: Sigil::Scalar,
6081                name,
6082                initializer: None,
6083                frozen: false,
6084                type_annotation: None,
6085                list_context: false,
6086            },
6087            (Token::ArrayVar(name), _) => VarDecl {
6088                sigil: Sigil::Array,
6089                name,
6090                initializer: None,
6091                frozen: false,
6092                type_annotation: None,
6093                list_context: false,
6094            },
6095            (Token::HashVar(name), line) => {
6096                if !crate::compat_mode() {
6097                    self.check_hash_shadows_reserved(&name, line)?;
6098                }
6099                VarDecl {
6100                    sigil: Sigil::Hash,
6101                    name,
6102                    initializer: None,
6103                    frozen: false,
6104                    type_annotation: None,
6105                    list_context: false,
6106                }
6107            }
6108            (Token::Star, _line) => {
6109                let name = match self.advance() {
6110                    (Token::Ident(n), _) => n,
6111                    (tok, l) => {
6112                        return Err(self
6113                            .syntax_err(format!("Expected identifier after *, got {:?}", tok), l));
6114                    }
6115                };
6116                VarDecl {
6117                    sigil: Sigil::Typeglob,
6118                    name,
6119                    initializer: None,
6120                    frozen: false,
6121                    type_annotation: None,
6122                    list_context: false,
6123                }
6124            }
6125            // `my ($a, undef, $c) = (1, 2, 3)` — Perl idiom for discarding a
6126            // slot in a list assignment. The interpreter treats `undef`-named
6127            // scalar decls as throwaway: declared into a unique sink so the
6128            // distribute-to-decls loop advances past the slot.
6129            (Token::Ident(ref kw), _) if kw == "undef" => VarDecl {
6130                sigil: Sigil::Scalar,
6131                // Synthesize a name that user code cannot reference. Each
6132                // sink slot in a list-assign gets its own unique name so the
6133                // declarations don't collide.
6134                name: format!("__undef_sink_{}", self.pos),
6135                initializer: None,
6136                frozen: false,
6137                type_annotation: None,
6138                list_context: false,
6139            },
6140            (tok, line) => {
6141                return Err(self.syntax_err(
6142                    format!("Expected variable in declaration, got {:?}", tok),
6143                    line,
6144                ));
6145            }
6146        };
6147        if allow_type_annotation && self.eat(&Token::Colon) {
6148            let ty = self.parse_type_name()?;
6149            if decl.sigil != Sigil::Scalar {
6150                return Err(self.syntax_err(
6151                    "`: Type` is only valid for scalar declarations (typed my $name : Int)",
6152                    self.peek_line(),
6153                ));
6154            }
6155            decl.type_annotation = Some(ty);
6156        }
6157        Ok(decl)
6158    }
6159
6160    fn parse_type_name(&mut self) -> StrykeResult<PerlTypeName> {
6161        match self.advance() {
6162            (Token::Ident(name), _) => match name.as_str() {
6163                "Int" => Ok(PerlTypeName::Int),
6164                "Str" => Ok(PerlTypeName::Str),
6165                "Float" => Ok(PerlTypeName::Float),
6166                "Bool" => Ok(PerlTypeName::Bool),
6167                "Array" => Ok(PerlTypeName::Array),
6168                "Hash" => Ok(PerlTypeName::Hash),
6169                "Ref" => Ok(PerlTypeName::Ref),
6170                "Any" => Ok(PerlTypeName::Any),
6171                _ => Ok(PerlTypeName::Struct(name)),
6172            },
6173            (tok, err_line) => Err(self.syntax_err(
6174                format!("Expected type name after `:`, got {:?}", tok),
6175                err_line,
6176            )),
6177        }
6178    }
6179
6180    fn parse_package(&mut self) -> StrykeResult<Statement> {
6181        let line = self.peek_line();
6182        self.advance(); // 'package'
6183        let name = match self.advance() {
6184            (Token::Ident(n), _) => n,
6185            (tok, line) => {
6186                return Err(self.syntax_err(format!("Expected package name, got {:?}", tok), line))
6187            }
6188        };
6189        // Handle Foo::Bar
6190        let mut full_name = name;
6191        while self.eat(&Token::PackageSep) {
6192            if let (Token::Ident(part), _) = self.advance() {
6193                full_name = format!("{}::{}", full_name, part);
6194            }
6195        }
6196        self.eat(&Token::Semicolon);
6197        // Track the active package so subsequent `fn name(...)` decls can be
6198        // recognised as `Pkg::name` for shadow-of-builtin checks.
6199        self.current_package = full_name.clone();
6200        Ok(Statement {
6201            label: None,
6202            kind: StmtKind::Package { name: full_name },
6203            line,
6204        })
6205    }
6206
6207    fn parse_use(&mut self) -> StrykeResult<Statement> {
6208        let line = self.peek_line();
6209        self.advance(); // 'use'
6210        let (tok, tok_line) = self.advance();
6211        match tok {
6212            Token::Float(v) => {
6213                self.eat(&Token::Semicolon);
6214                Ok(Statement {
6215                    label: None,
6216                    kind: StmtKind::UsePerlVersion { version: v },
6217                    line,
6218                })
6219            }
6220            Token::Integer(n) => {
6221                if matches!(self.peek(), Token::Semicolon | Token::Eof) {
6222                    self.eat(&Token::Semicolon);
6223                    Ok(Statement {
6224                        label: None,
6225                        kind: StmtKind::UsePerlVersion { version: n as f64 },
6226                        line,
6227                    })
6228                } else {
6229                    Err(self.syntax_err(
6230                        format!("Expected ';' after use VERSION (got {:?})", self.peek()),
6231                        line,
6232                    ))
6233                }
6234            }
6235            Token::Ident(n) => {
6236                let mut full_name = n;
6237                while self.eat(&Token::PackageSep) {
6238                    if let (Token::Ident(part), _) = self.advance() {
6239                        full_name = format!("{}::{}", full_name, part);
6240                    }
6241                }
6242                if full_name == "overload" {
6243                    let mut pairs = Vec::new();
6244                    let mut parse_overload_pairs = |this: &mut Self| -> StrykeResult<()> {
6245                        loop {
6246                            if matches!(this.peek(), Token::RParen | Token::Semicolon | Token::Eof)
6247                            {
6248                                break;
6249                            }
6250                            let key_e = this.parse_assign_expr()?;
6251                            this.expect(&Token::FatArrow)?;
6252                            let val_e = this.parse_assign_expr()?;
6253                            let key = this.expr_to_overload_key(&key_e)?;
6254                            let val = this.expr_to_overload_sub(&val_e)?;
6255                            pairs.push((key, val));
6256                            if !this.eat(&Token::Comma) {
6257                                break;
6258                            }
6259                        }
6260                        Ok(())
6261                    };
6262                    if self.eat(&Token::LParen) {
6263                        // `use overload ();` — common in JSON::PP and other modules.
6264                        parse_overload_pairs(self)?;
6265                        self.expect(&Token::RParen)?;
6266                    } else if !matches!(self.peek(), Token::Semicolon | Token::Eof) {
6267                        parse_overload_pairs(self)?;
6268                    }
6269                    self.eat(&Token::Semicolon);
6270                    return Ok(Statement {
6271                        label: None,
6272                        kind: StmtKind::UseOverload { pairs },
6273                        line,
6274                    });
6275                }
6276                let mut imports = Vec::new();
6277                // Imports must start on the SAME LINE as `use Module`.
6278                // Without this, a bare `use K8s` followed by `p "…"`
6279                // on the next line silently swallowed the `p` call as
6280                // an import expression — failing later with the
6281                // confusing "pragma import must be a compile-time
6282                // string" error pointing at the next-line statement.
6283                // The legitimate multi-line form uses `,` to continue.
6284                let on_same_line = self.peek_line() == tok_line;
6285                if on_same_line
6286                    && !matches!(self.peek(), Token::Semicolon | Token::Eof)
6287                    && !self.next_is_new_statement_start(tok_line)
6288                {
6289                    loop {
6290                        if matches!(self.peek(), Token::Semicolon | Token::Eof) {
6291                            break;
6292                        }
6293                        imports.push(self.parse_expression()?);
6294                        if !self.eat(&Token::Comma) {
6295                            break;
6296                        }
6297                    }
6298                }
6299                self.eat(&Token::Semicolon);
6300                Ok(Statement {
6301                    label: None,
6302                    kind: StmtKind::Use {
6303                        module: full_name,
6304                        imports,
6305                    },
6306                    line,
6307                })
6308            }
6309            other => Err(self.syntax_err(
6310                format!("Expected module name or version after use, got {:?}", other),
6311                tok_line,
6312            )),
6313        }
6314    }
6315
6316    fn parse_no(&mut self) -> StrykeResult<Statement> {
6317        let line = self.peek_line();
6318        self.advance(); // 'no'
6319        let module = match self.advance() {
6320            (Token::Ident(n), tok_line) => (n, tok_line),
6321            (tok, line) => {
6322                return Err(self.syntax_err(
6323                    format!("Expected module name after no, got {:?}", tok),
6324                    line,
6325                ))
6326            }
6327        };
6328        let (module_name, tok_line) = module;
6329        let mut full_name = module_name;
6330        while self.eat(&Token::PackageSep) {
6331            if let (Token::Ident(part), _) = self.advance() {
6332                full_name = format!("{}::{}", full_name, part);
6333            }
6334        }
6335        let mut imports = Vec::new();
6336        if !matches!(self.peek(), Token::Semicolon | Token::Eof)
6337            && !self.next_is_new_statement_start(tok_line)
6338        {
6339            loop {
6340                if matches!(self.peek(), Token::Semicolon | Token::Eof) {
6341                    break;
6342                }
6343                imports.push(self.parse_expression()?);
6344                if !self.eat(&Token::Comma) {
6345                    break;
6346                }
6347            }
6348        }
6349        self.eat(&Token::Semicolon);
6350        Ok(Statement {
6351            label: None,
6352            kind: StmtKind::No {
6353                module: full_name,
6354                imports,
6355            },
6356            line,
6357        })
6358    }
6359
6360    fn parse_return(&mut self) -> StrykeResult<Statement> {
6361        let line = self.peek_line();
6362        self.advance(); // 'return'
6363                        // No-value return: terminator tokens AND any postfix statement-modifier
6364                        // keyword (`if`/`unless`/`while`/`until`/`for`/`foreach`). Without this
6365                        // the postfix-modifier check below never fires for valueless returns —
6366                        // `parse_assign_expr` would see `if` and look it up as a sub call,
6367                        // producing the misleading "Undefined subroutine &if" error.
6368        let val = if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof)
6369            || self.peek_is_postfix_stmt_modifier_keyword()
6370        {
6371            None
6372        } else {
6373            // Parse the operand as a comma-list — Perl's `return` is a
6374            // list-operator, so `return 1, 2, 3` returns the list (1, 2, 3).
6375            // (BUG-010) Stay below pipe-forward and stop at postfix
6376            // statement-modifier keywords like `if` / `unless`.
6377            let first = self.parse_assign_expr()?;
6378            if matches!(self.peek(), Token::Comma | Token::FatArrow) {
6379                let mut items = vec![first];
6380                while self.eat(&Token::Comma) || self.eat(&Token::FatArrow) {
6381                    if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof)
6382                        || self.peek_is_postfix_stmt_modifier_keyword()
6383                    {
6384                        break;
6385                    }
6386                    items.push(self.parse_assign_expr()?);
6387                }
6388                let line = items.first().map(|e| e.line).unwrap_or(line);
6389                Some(Expr {
6390                    kind: ExprKind::List(items),
6391                    line,
6392                })
6393            } else {
6394                Some(first)
6395            }
6396        };
6397        // Check for postfix modifiers on return
6398        let stmt = Statement {
6399            label: None,
6400            kind: StmtKind::Return(val),
6401            line,
6402        };
6403        if let Token::Ident(ref kw) = self.peek().clone() {
6404            match kw.as_str() {
6405                "if" => {
6406                    self.advance();
6407                    let cond = self.parse_expression()?;
6408                    self.eat(&Token::Semicolon);
6409                    return Ok(Statement {
6410                        label: None,
6411                        kind: StmtKind::If {
6412                            condition: cond,
6413                            body: vec![stmt],
6414                            elsifs: vec![],
6415                            else_block: None,
6416                        },
6417                        line,
6418                    });
6419                }
6420                "unless" => {
6421                    self.advance();
6422                    let cond = self.parse_expression()?;
6423                    self.eat(&Token::Semicolon);
6424                    return Ok(Statement {
6425                        label: None,
6426                        kind: StmtKind::Unless {
6427                            condition: cond,
6428                            body: vec![stmt],
6429                            else_block: None,
6430                        },
6431                        line,
6432                    });
6433                }
6434                _ => {}
6435            }
6436        }
6437        self.eat(&Token::Semicolon);
6438        Ok(stmt)
6439    }
6440
6441    // ── Expressions (Pratt / precedence climbing) ──
6442
6443    fn parse_expression(&mut self) -> StrykeResult<Expr> {
6444        self.parse_comma_expr()
6445    }
6446
6447    fn parse_comma_expr(&mut self) -> StrykeResult<Expr> {
6448        // Word-op precedence (or/and/not) sits ABOVE assignment in Perl —
6449        // `EXPR or $err = $@` parses as `EXPR or ($err = $@)`, NOT
6450        // `(EXPR or $err) = $@`. Entering through `parse_or_word` here
6451        // (instead of `parse_assign_expr` directly) gives `or`/`and`/`not`
6452        // looser binding than `=`, matching `perlop`. The deeper chain
6453        // (`parse_not_word → parse_assign_expr → parse_ternary → … →
6454        // parse_log_or → …`) handles tighter operators normally.
6455        let expr = self.parse_or_word()?;
6456        let mut exprs = vec![expr];
6457        while self.eat(&Token::Comma) || self.eat(&Token::FatArrow) {
6458            if matches!(
6459                self.peek(),
6460                Token::RParen | Token::RBracket | Token::RBrace | Token::Semicolon | Token::Eof
6461            ) {
6462                break; // trailing comma
6463            }
6464            exprs.push(self.parse_or_word()?);
6465        }
6466        if exprs.len() == 1 {
6467            return Ok(exprs.pop().unwrap());
6468        }
6469        let line = exprs[0].line;
6470        Ok(Expr {
6471            kind: ExprKind::List(exprs),
6472            line,
6473        })
6474    }
6475
6476    fn parse_assign_expr(&mut self) -> StrykeResult<Expr> {
6477        let expr = self.parse_ternary()?;
6478        let line = expr.line;
6479
6480        match self.peek().clone() {
6481            Token::Assign => {
6482                self.advance();
6483                let right = self.parse_assign_expr()?;
6484                // Desugar `$obj->field = value` into `$obj->field(value)` (setter call)
6485                if let ExprKind::MethodCall { ref args, .. } = expr.kind {
6486                    if args.is_empty() {
6487                        // Destructure again to take ownership
6488                        let ExprKind::MethodCall {
6489                            object,
6490                            method,
6491                            super_call,
6492                            ..
6493                        } = expr.kind
6494                        else {
6495                            unreachable!()
6496                        };
6497                        return Ok(Expr {
6498                            kind: ExprKind::MethodCall {
6499                                object,
6500                                method,
6501                                args: vec![right],
6502                                super_call,
6503                            },
6504                            line,
6505                        });
6506                    }
6507                }
6508                self.validate_assignment(&expr, &right, line)?;
6509                Ok(Expr {
6510                    kind: ExprKind::Assign {
6511                        target: Box::new(expr),
6512                        value: Box::new(right),
6513                    },
6514                    line,
6515                })
6516            }
6517            Token::PlusAssign => {
6518                self.advance();
6519                let r = self.parse_assign_expr()?;
6520                Ok(Expr {
6521                    kind: ExprKind::CompoundAssign {
6522                        target: Box::new(expr),
6523                        op: BinOp::Add,
6524                        value: Box::new(r),
6525                    },
6526                    line,
6527                })
6528            }
6529            Token::MinusAssign => {
6530                self.advance();
6531                let r = self.parse_assign_expr()?;
6532                Ok(Expr {
6533                    kind: ExprKind::CompoundAssign {
6534                        target: Box::new(expr),
6535                        op: BinOp::Sub,
6536                        value: Box::new(r),
6537                    },
6538                    line,
6539                })
6540            }
6541            Token::MulAssign => {
6542                self.advance();
6543                let r = self.parse_assign_expr()?;
6544                Ok(Expr {
6545                    kind: ExprKind::CompoundAssign {
6546                        target: Box::new(expr),
6547                        op: BinOp::Mul,
6548                        value: Box::new(r),
6549                    },
6550                    line,
6551                })
6552            }
6553            Token::DivAssign => {
6554                self.advance();
6555                let r = self.parse_assign_expr()?;
6556                Ok(Expr {
6557                    kind: ExprKind::CompoundAssign {
6558                        target: Box::new(expr),
6559                        op: BinOp::Div,
6560                        value: Box::new(r),
6561                    },
6562                    line,
6563                })
6564            }
6565            Token::ModAssign => {
6566                self.advance();
6567                let r = self.parse_assign_expr()?;
6568                Ok(Expr {
6569                    kind: ExprKind::CompoundAssign {
6570                        target: Box::new(expr),
6571                        op: BinOp::Mod,
6572                        value: Box::new(r),
6573                    },
6574                    line,
6575                })
6576            }
6577            Token::PowAssign => {
6578                self.advance();
6579                let r = self.parse_assign_expr()?;
6580                Ok(Expr {
6581                    kind: ExprKind::CompoundAssign {
6582                        target: Box::new(expr),
6583                        op: BinOp::Pow,
6584                        value: Box::new(r),
6585                    },
6586                    line,
6587                })
6588            }
6589            Token::XAssign => {
6590                // `$s x= N` has no matching `BinOp::Repeat`; desugar to
6591                // `$s = $s x N` so we can reuse the existing `ExprKind::Repeat`
6592                // evaluator (scalar-repeat path; list-repeat fires only when
6593                // the LHS is a syntactic list literal).
6594                self.advance();
6595                let r = self.parse_assign_expr()?;
6596                let lhs_for_repeat = expr.clone();
6597                Ok(Expr {
6598                    kind: ExprKind::Assign {
6599                        target: Box::new(expr),
6600                        value: Box::new(Expr {
6601                            kind: ExprKind::Repeat {
6602                                expr: Box::new(lhs_for_repeat),
6603                                count: Box::new(r),
6604                                list_repeat: false,
6605                            },
6606                            line,
6607                        }),
6608                    },
6609                    line,
6610                })
6611            }
6612            Token::DotAssign => {
6613                self.advance();
6614                let r = self.parse_assign_expr()?;
6615                Ok(Expr {
6616                    kind: ExprKind::CompoundAssign {
6617                        target: Box::new(expr),
6618                        op: BinOp::Concat,
6619                        value: Box::new(r),
6620                    },
6621                    line,
6622                })
6623            }
6624            Token::BitAndAssign => {
6625                self.advance();
6626                let r = self.parse_assign_expr()?;
6627                Ok(Expr {
6628                    kind: ExprKind::CompoundAssign {
6629                        target: Box::new(expr),
6630                        op: BinOp::BitAnd,
6631                        value: Box::new(r),
6632                    },
6633                    line,
6634                })
6635            }
6636            Token::BitOrAssign => {
6637                self.advance();
6638                let r = self.parse_assign_expr()?;
6639                Ok(Expr {
6640                    kind: ExprKind::CompoundAssign {
6641                        target: Box::new(expr),
6642                        op: BinOp::BitOr,
6643                        value: Box::new(r),
6644                    },
6645                    line,
6646                })
6647            }
6648            Token::XorAssign => {
6649                self.advance();
6650                let r = self.parse_assign_expr()?;
6651                Ok(Expr {
6652                    kind: ExprKind::CompoundAssign {
6653                        target: Box::new(expr),
6654                        op: BinOp::BitXor,
6655                        value: Box::new(r),
6656                    },
6657                    line,
6658                })
6659            }
6660            Token::ShiftLeftAssign => {
6661                self.advance();
6662                let r = self.parse_assign_expr()?;
6663                Ok(Expr {
6664                    kind: ExprKind::CompoundAssign {
6665                        target: Box::new(expr),
6666                        op: BinOp::ShiftLeft,
6667                        value: Box::new(r),
6668                    },
6669                    line,
6670                })
6671            }
6672            Token::ShiftRightAssign => {
6673                self.advance();
6674                let r = self.parse_assign_expr()?;
6675                Ok(Expr {
6676                    kind: ExprKind::CompoundAssign {
6677                        target: Box::new(expr),
6678                        op: BinOp::ShiftRight,
6679                        value: Box::new(r),
6680                    },
6681                    line,
6682                })
6683            }
6684            Token::OrAssign => {
6685                self.advance();
6686                let r = self.parse_assign_expr()?;
6687                Ok(Expr {
6688                    kind: ExprKind::CompoundAssign {
6689                        target: Box::new(expr),
6690                        op: BinOp::LogOr,
6691                        value: Box::new(r),
6692                    },
6693                    line,
6694                })
6695            }
6696            Token::DefinedOrAssign => {
6697                self.advance();
6698                let r = self.parse_assign_expr()?;
6699                Ok(Expr {
6700                    kind: ExprKind::CompoundAssign {
6701                        target: Box::new(expr),
6702                        op: BinOp::DefinedOr,
6703                        value: Box::new(r),
6704                    },
6705                    line,
6706                })
6707            }
6708            Token::AndAssign => {
6709                self.advance();
6710                let r = self.parse_assign_expr()?;
6711                Ok(Expr {
6712                    kind: ExprKind::CompoundAssign {
6713                        target: Box::new(expr),
6714                        op: BinOp::LogAnd,
6715                        value: Box::new(r),
6716                    },
6717                    line,
6718                })
6719            }
6720            _ => Ok(expr),
6721        }
6722    }
6723
6724    fn parse_ternary(&mut self) -> StrykeResult<Expr> {
6725        let expr = self.parse_pipe_forward()?;
6726        if self.eat(&Token::Question) {
6727            let line = expr.line;
6728            self.suppress_colon_range = self.suppress_colon_range.saturating_add(1);
6729            let then_expr = self.parse_assign_expr();
6730            self.suppress_colon_range = self.suppress_colon_range.saturating_sub(1);
6731            let then_expr = then_expr?;
6732            self.expect(&Token::Colon)?;
6733            let else_expr = self.parse_assign_expr()?;
6734            return Ok(Expr {
6735                kind: ExprKind::Ternary {
6736                    condition: Box::new(expr),
6737                    then_expr: Box::new(then_expr),
6738                    else_expr: Box::new(else_expr),
6739                },
6740                line,
6741            });
6742        }
6743        Ok(expr)
6744    }
6745
6746    /// `EXPR |> CALL` — pipe-forward (F#/Elixir). Left-associative; the LHS is threaded
6747    /// in as the **first argument** of the RHS call at parse time (pure AST rewrite,
6748    /// no runtime cost). `x |> f(a, b)` → `f(x, a, b)`; `x |> f` → `f(x)`; chain
6749    /// `x |> f |> g(2)` → `g(f(x), 2)`. Precedence sits between `?:` and `||`, so
6750    /// `x + 1 |> f || y` parses as `f(x + 1) || y`.
6751    fn parse_pipe_forward(&mut self) -> StrykeResult<Expr> {
6752        // After moving word-ops (or/and/not) above the assignment level,
6753        // pipe_forward must descend into `parse_range` (which itself
6754        // descends into `parse_log_or`) — calling `parse_or_word` here
6755        // would re-introduce `or` at a wrong place in the precedence chain
6756        // (it now sits above `parse_comma_expr`). We skip past `parse_range`
6757        // rather than `parse_log_or` so `..` stays reachable.
6758        let mut left = self.parse_range()?;
6759        // Inside a paren-less arg list, `|>` is a hard terminator for the
6760        // enclosing call — leave it for the outer `parse_pipe_forward` loop
6761        // so `qw(…) |> head 2 |> join "-"` chains left-to-right as
6762        // `(qw(…) |> head 2) |> join "-"` instead of `head` swallowing the
6763        // outer `|>` via its first-arg `parse_assign_expr`.
6764        if self.no_pipe_forward_depth > 0 {
6765            return Ok(left);
6766        }
6767        while matches!(self.peek(), Token::PipeForward) {
6768            if crate::compat_mode() {
6769                return Err(self.syntax_err(
6770                    "pipe-forward operator `|>` is a stryke extension (disabled by --compat)",
6771                    left.line,
6772                ));
6773            }
6774            let line = left.line;
6775            self.advance();
6776            // Set pipe-RHS context so list-taking builtins (`map`, `grep`,
6777            // `join`, …) accept a placeholder in place of their list operand.
6778            self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_add(1);
6779            // RHS of `|>` parses at the same precedence as the LHS — see
6780            // the comment at the top of `parse_pipe_forward` for why this
6781            // descends into `parse_range` instead of `parse_or_word`.
6782            let right_result = self.parse_range();
6783            self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_sub(1);
6784            let right = right_result?;
6785            left = self.pipe_forward_apply(left, right, line)?;
6786        }
6787        Ok(left)
6788    }
6789
6790    /// Desugar `lhs |> rhs`: thread `lhs` into the call that `rhs` represents as
6791    /// its **first** argument (Elixir / R / proposed-JS convention).
6792    ///
6793    /// The strategy depends on the shape of `rhs`:
6794    /// - Generic calls (`FuncCall`, `MethodCall`, `IndirectCall`) and variadic
6795    ///   builtins (`Print`, `Say`, `Printf`, `Die`, `Warn`, `Sprintf`, `System`,
6796    ///   `Exec`, `Unlink`, `Chmod`, `Chown`, `Glob`, …) — **prepend** `lhs` to
6797    ///   the args list. So `URL |> json_jq ".[]"` → `json_jq(URL, ".[]")`,
6798    ///   matching the `(data, filter)` signature the builtin expects.
6799    /// - Unary-style builtins (`Length`, `Abs`, `Lc`, `Uc`, `Defined`, `Ref`,
6800    ///   `Keys`, `Values`, `Pop`, `Shift`, …) — **replace** the sole operand with
6801    ///   `lhs` (these parse a single default `$_` when called without an arg, so
6802    ///   piping overrides that default; first-arg and last-arg are identical).
6803    /// - List-taking higher-order forms (`map`, `flat_map`, `grep`, `sort`, `join`, `reduce`, `fold`,
6804    ///   `pmap`, `pflat_map`, `pgrep`, `pfor`, …) — **replace** the `list` field with `lhs`, so
6805    ///   `@arr |> map { $_ * 2 }` becomes `map { $_ * 2 } @arr`.
6806    /// - `Bareword("f")` — lift to `FuncCall { f, [lhs] }`.
6807    /// - Scalar / deref / coderef expressions — wrap in `IndirectCall` with `lhs`
6808    ///   as the sole argument.
6809    /// - Ambiguous forms (binary ops, ternaries, literals, lists) — parse error,
6810    ///   since silently calling a non-callable at runtime would be worse.
6811    fn pipe_forward_apply(&self, lhs: Expr, rhs: Expr, line: usize) -> StrykeResult<Expr> {
6812        let Expr { kind, line: rline } = rhs;
6813        let new_kind = match kind {
6814            // ── Generic / user-defined calls ───────────────────────────────────
6815            ExprKind::FuncCall { name, mut args } => {
6816                // Stryke builtins are unprefixed; `CORE::` callers route back to the
6817                // bare-name pipe-forward dispatch below.
6818                let dispatch_name: &str = name.strip_prefix("CORE::").unwrap_or(name.as_str());
6819                match dispatch_name {
6820                    "puniq" | "uniq" | "distinct" | "flatten" | "set" | "list_count"
6821                    | "list_size" | "count" | "size" | "cnt" | "len" | "with_index" | "shuffle"
6822                    | "shuffled" | "frequencies" | "freq" | "pfrequencies" | "pfreq"
6823                    | "interleave" | "ddump" | "stringify" | "str" | "lines" | "words"
6824                    | "chars" | "digits" | "letters" | "letters_uc" | "letters_lc"
6825                    | "punctuation" | "numbers" | "graphemes" | "columns" | "sentences"
6826                    | "paragraphs" | "sections" | "trim" | "avg" | "to_json" | "to_csv"
6827                    | "to_toml" | "to_yaml" | "to_xml" | "to_html" | "from_json" | "from_csv"
6828                    | "from_toml" | "from_yaml" | "from_xml" | "to_markdown" | "to_table"
6829                    | "xopen" | "clip" | "sparkline" | "bar_chart" | "flame" | "stddev"
6830                    | "squared" | "sq" | "square" | "cubed" | "cb" | "cube" | "normalize"
6831                    | "snake_case" | "camel_case" | "kebab_case" => {
6832                        if args.is_empty() {
6833                            args.push(lhs);
6834                        } else {
6835                            args[0] = lhs;
6836                        }
6837                    }
6838                    "chunked" | "windowed" => {
6839                        if args.is_empty() {
6840                            return Err(self.syntax_err(
6841                                "|>: chunked(N) / windowed(N) needs size — e.g. `@a |> windowed(2)`",
6842                                line,
6843                            ));
6844                        }
6845                        args.insert(0, lhs);
6846                    }
6847                    "reduce" | "fold" => {
6848                        args.push(lhs);
6849                    }
6850                    "grep_v" | "pluck" | "tee" | "nth" | "chunk" => {
6851                        // data |> grep_v "pattern" → grep_v("pattern", data...)
6852                        // data |> pluck "key" → pluck("key", data...)
6853                        // data |> tee "file" → tee("file", data...)
6854                        // data |> nth N → nth(N, data...)
6855                        // data |> chunk N → chunk(N, data...)
6856                        args.push(lhs);
6857                    }
6858                    "enumerate" | "dedup" => {
6859                        // data |> enumerate → enumerate(data)
6860                        // data |> dedup → dedup(data)
6861                        args.insert(0, lhs);
6862                    }
6863                    "clamp" => {
6864                        // data |> clamp MIN, MAX → clamp(MIN, MAX, data...)
6865                        args.push(lhs);
6866                    }
6867                    n if Self::is_block_then_list_pipe_builtin(n) => {
6868                        if args.len() < 2 {
6869                            return Err(self.syntax_err(
6870                                format!(
6871                                    "|>: `{name}` needs {{ BLOCK }}, LIST so the list can receive the pipe"
6872                                ),
6873                                line,
6874                            ));
6875                        }
6876                        args[1] = lhs;
6877                    }
6878                    "take" | "head" | "tail" | "drop" => {
6879                        if args.is_empty() {
6880                            return Err(self.syntax_err(
6881                                "|>: `{name}` needs N last — e.g. `@a |> take(3)` for `take(@a, 3)`",
6882                                line,
6883                            ));
6884                        }
6885                        // `LIST |> take N` → `take(LIST, N)` (prepend piped list before trailing count)
6886                        args.insert(0, lhs);
6887                    }
6888                    _ => {
6889                        if self.thread_last_mode {
6890                            args.push(lhs);
6891                        } else {
6892                            args.insert(0, lhs);
6893                        }
6894                    }
6895                }
6896                ExprKind::FuncCall { name, args }
6897            }
6898            ExprKind::MethodCall {
6899                object,
6900                method,
6901                mut args,
6902                super_call,
6903            } => {
6904                if self.thread_last_mode {
6905                    args.push(lhs);
6906                } else {
6907                    args.insert(0, lhs);
6908                }
6909                ExprKind::MethodCall {
6910                    object,
6911                    method,
6912                    args,
6913                    super_call,
6914                }
6915            }
6916            ExprKind::IndirectCall {
6917                target,
6918                mut args,
6919                ampersand,
6920                pass_caller_arglist: _,
6921            } => {
6922                if self.thread_last_mode {
6923                    args.push(lhs);
6924                } else {
6925                    args.insert(0, lhs);
6926                }
6927                ExprKind::IndirectCall {
6928                    target,
6929                    args,
6930                    ampersand,
6931                    // Prepending an explicit first arg means this is no longer
6932                    // "pass the caller's @_" — that form is only bare `&$cr`.
6933                    pass_caller_arglist: false,
6934                }
6935            }
6936
6937            // ── Print-like / diagnostic ops (variadic) ─────────────────────────
6938            ExprKind::Print { handle, mut args } => {
6939                if self.thread_last_mode {
6940                    args.push(lhs);
6941                } else {
6942                    args.insert(0, lhs);
6943                }
6944                ExprKind::Print { handle, args }
6945            }
6946            ExprKind::Say { handle, mut args } => {
6947                if self.thread_last_mode {
6948                    args.push(lhs);
6949                } else {
6950                    args.insert(0, lhs);
6951                }
6952                ExprKind::Say { handle, args }
6953            }
6954            ExprKind::Printf { handle, mut args } => {
6955                if self.thread_last_mode {
6956                    args.push(lhs);
6957                } else {
6958                    args.insert(0, lhs);
6959                }
6960                ExprKind::Printf { handle, args }
6961            }
6962            ExprKind::Die(mut args) => {
6963                if self.thread_last_mode {
6964                    args.push(lhs);
6965                } else {
6966                    args.insert(0, lhs);
6967                }
6968                ExprKind::Die(args)
6969            }
6970            ExprKind::Warn(mut args) => {
6971                if self.thread_last_mode {
6972                    args.push(lhs);
6973                } else {
6974                    args.insert(0, lhs);
6975                }
6976                ExprKind::Warn(args)
6977            }
6978
6979            // ── Sprintf: first-arg pipe threads lhs into the `format` slot ─────
6980            //   `"n=%d" |> sprintf(42)` → `sprintf("n=%d", 42)` is awkward,
6981            //   but piping the format string is the rarer case. Prepending
6982            //   to the values list gives `sprintf(format, lhs, ...args)` for
6983            //   the common `$n |> sprintf "count=%d"` case.
6984            ExprKind::Sprintf { format, mut args } => {
6985                if self.thread_last_mode {
6986                    args.push(lhs);
6987                } else {
6988                    args.insert(0, lhs);
6989                }
6990                ExprKind::Sprintf { format, args }
6991            }
6992
6993            // ── System / exec / globbing / filesystem variadics ────────────────
6994            ExprKind::System(mut args) => {
6995                if self.thread_last_mode {
6996                    args.push(lhs);
6997                } else {
6998                    args.insert(0, lhs);
6999                }
7000                ExprKind::System(args)
7001            }
7002            ExprKind::Exec(mut args) => {
7003                if self.thread_last_mode {
7004                    args.push(lhs);
7005                } else {
7006                    args.insert(0, lhs);
7007                }
7008                ExprKind::Exec(args)
7009            }
7010            ExprKind::Unlink(mut args) => {
7011                if self.thread_last_mode {
7012                    args.push(lhs);
7013                } else {
7014                    args.insert(0, lhs);
7015                }
7016                ExprKind::Unlink(args)
7017            }
7018            ExprKind::Chmod(mut args) => {
7019                if self.thread_last_mode {
7020                    args.push(lhs);
7021                } else {
7022                    args.insert(0, lhs);
7023                }
7024                ExprKind::Chmod(args)
7025            }
7026            ExprKind::Chown(mut args) => {
7027                if self.thread_last_mode {
7028                    args.push(lhs);
7029                } else {
7030                    args.insert(0, lhs);
7031                }
7032                ExprKind::Chown(args)
7033            }
7034            ExprKind::Glob(mut args) => {
7035                if self.thread_last_mode {
7036                    args.push(lhs);
7037                } else {
7038                    args.insert(0, lhs);
7039                }
7040                ExprKind::Glob(args)
7041            }
7042            ExprKind::Files(mut args) => {
7043                if self.thread_last_mode {
7044                    args.push(lhs);
7045                } else {
7046                    args.insert(0, lhs);
7047                }
7048                ExprKind::Files(args)
7049            }
7050            ExprKind::Filesf(mut args) => {
7051                if self.thread_last_mode {
7052                    args.push(lhs);
7053                } else {
7054                    args.insert(0, lhs);
7055                }
7056                ExprKind::Filesf(args)
7057            }
7058            ExprKind::FilesfRecursive(mut args) => {
7059                if self.thread_last_mode {
7060                    args.push(lhs);
7061                } else {
7062                    args.insert(0, lhs);
7063                }
7064                ExprKind::FilesfRecursive(args)
7065            }
7066            ExprKind::Dirs(mut args) => {
7067                if self.thread_last_mode {
7068                    args.push(lhs);
7069                } else {
7070                    args.insert(0, lhs);
7071                }
7072                ExprKind::Dirs(args)
7073            }
7074            ExprKind::DirsRecursive(mut args) => {
7075                if self.thread_last_mode {
7076                    args.push(lhs);
7077                } else {
7078                    args.insert(0, lhs);
7079                }
7080                ExprKind::DirsRecursive(args)
7081            }
7082            ExprKind::SymLinks(mut args) => {
7083                if self.thread_last_mode {
7084                    args.push(lhs);
7085                } else {
7086                    args.insert(0, lhs);
7087                }
7088                ExprKind::SymLinks(args)
7089            }
7090            ExprKind::Sockets(mut args) => {
7091                if self.thread_last_mode {
7092                    args.push(lhs);
7093                } else {
7094                    args.insert(0, lhs);
7095                }
7096                ExprKind::Sockets(args)
7097            }
7098            ExprKind::Pipes(mut args) => {
7099                if self.thread_last_mode {
7100                    args.push(lhs);
7101                } else {
7102                    args.insert(0, lhs);
7103                }
7104                ExprKind::Pipes(args)
7105            }
7106            ExprKind::BlockDevices(mut args) => {
7107                if self.thread_last_mode {
7108                    args.push(lhs);
7109                } else {
7110                    args.insert(0, lhs);
7111                }
7112                ExprKind::BlockDevices(args)
7113            }
7114            ExprKind::CharDevices(mut args) => {
7115                if self.thread_last_mode {
7116                    args.push(lhs);
7117                } else {
7118                    args.insert(0, lhs);
7119                }
7120                ExprKind::CharDevices(args)
7121            }
7122            ExprKind::GlobPar { mut args, progress } => {
7123                if self.thread_last_mode {
7124                    args.push(lhs);
7125                } else {
7126                    args.insert(0, lhs);
7127                }
7128                ExprKind::GlobPar { args, progress }
7129            }
7130            ExprKind::ParSed { mut args, progress } => {
7131                if self.thread_last_mode {
7132                    args.push(lhs);
7133                } else {
7134                    args.insert(0, lhs);
7135                }
7136                ExprKind::ParSed { args, progress }
7137            }
7138
7139            // ── Unary-style builtins: replace the lone operand with `lhs` ──────
7140            ExprKind::Length(_) => ExprKind::Length(Box::new(lhs)),
7141            ExprKind::Abs(_) => ExprKind::Abs(Box::new(lhs)),
7142            ExprKind::Int(_) => ExprKind::Int(Box::new(lhs)),
7143            ExprKind::Sqrt(_) => ExprKind::Sqrt(Box::new(lhs)),
7144            ExprKind::Sin(_) => ExprKind::Sin(Box::new(lhs)),
7145            ExprKind::Cos(_) => ExprKind::Cos(Box::new(lhs)),
7146            ExprKind::Exp(_) => ExprKind::Exp(Box::new(lhs)),
7147            ExprKind::Log(_) => ExprKind::Log(Box::new(lhs)),
7148            ExprKind::Hex(_) => ExprKind::Hex(Box::new(lhs)),
7149            ExprKind::Oct(_) => ExprKind::Oct(Box::new(lhs)),
7150            ExprKind::Lc(_) => ExprKind::Lc(Box::new(lhs)),
7151            ExprKind::Uc(_) => ExprKind::Uc(Box::new(lhs)),
7152            ExprKind::Lcfirst(_) => ExprKind::Lcfirst(Box::new(lhs)),
7153            ExprKind::Ucfirst(_) => ExprKind::Ucfirst(Box::new(lhs)),
7154            ExprKind::Fc(_) => ExprKind::Fc(Box::new(lhs)),
7155            ExprKind::Chr(_) => ExprKind::Chr(Box::new(lhs)),
7156            ExprKind::Ord(_) => ExprKind::Ord(Box::new(lhs)),
7157            ExprKind::Chomp(_) => ExprKind::Chomp(Box::new(lhs)),
7158            ExprKind::Chop(_) => ExprKind::Chop(Box::new(lhs)),
7159            ExprKind::Defined(_) => ExprKind::Defined(Box::new(lhs)),
7160            ExprKind::Ref(_) => ExprKind::Ref(Box::new(lhs)),
7161            ExprKind::ScalarContext(_) => ExprKind::ScalarContext(Box::new(lhs)),
7162            ExprKind::Keys(_) => ExprKind::Keys(Box::new(lhs)),
7163            ExprKind::Values(_) => ExprKind::Values(Box::new(lhs)),
7164            ExprKind::Each(_) => ExprKind::Each(Box::new(lhs)),
7165            ExprKind::Pop(_) => ExprKind::Pop(Box::new(lhs)),
7166            ExprKind::Shift(_) => ExprKind::Shift(Box::new(lhs)),
7167            ExprKind::Delete(_) => ExprKind::Delete(Box::new(lhs)),
7168            ExprKind::Exists(_) => ExprKind::Exists(Box::new(lhs)),
7169            ExprKind::ReverseExpr(_) => ExprKind::ReverseExpr(Box::new(lhs)),
7170            ExprKind::Rev(_) => ExprKind::Rev(Box::new(lhs)),
7171            ExprKind::Slurp(_) => ExprKind::Slurp(Box::new(lhs)),
7172            ExprKind::Swallow(_) => ExprKind::Swallow(Box::new(lhs)),
7173            ExprKind::Ingest(_) => ExprKind::Ingest(Box::new(lhs)),
7174            ExprKind::Burp(_) => ExprKind::Burp(Box::new(lhs)),
7175            ExprKind::God(_) => ExprKind::God(Box::new(lhs)),
7176            ExprKind::Capture(_) => ExprKind::Capture(Box::new(lhs)),
7177            ExprKind::Qx(_) => ExprKind::Qx(Box::new(lhs)),
7178            ExprKind::FetchUrl(_) => ExprKind::FetchUrl(Box::new(lhs)),
7179            ExprKind::Close(_) => ExprKind::Close(Box::new(lhs)),
7180            ExprKind::Chdir(_) => ExprKind::Chdir(Box::new(lhs)),
7181            ExprKind::Readdir(_) => ExprKind::Readdir(Box::new(lhs)),
7182            ExprKind::Closedir(_) => ExprKind::Closedir(Box::new(lhs)),
7183            ExprKind::Rewinddir(_) => ExprKind::Rewinddir(Box::new(lhs)),
7184            ExprKind::Telldir(_) => ExprKind::Telldir(Box::new(lhs)),
7185            ExprKind::Stat(_) => ExprKind::Stat(Box::new(lhs)),
7186            ExprKind::Lstat(_) => ExprKind::Lstat(Box::new(lhs)),
7187            ExprKind::Readlink(_) => ExprKind::Readlink(Box::new(lhs)),
7188            ExprKind::Study(_) => ExprKind::Study(Box::new(lhs)),
7189            ExprKind::Await(_) => ExprKind::Await(Box::new(lhs)),
7190            ExprKind::Eval(_) => ExprKind::Eval(Box::new(lhs)),
7191            ExprKind::Rand(_) => ExprKind::Rand(Some(Box::new(lhs))),
7192            ExprKind::Srand(_) => ExprKind::Srand(Some(Box::new(lhs))),
7193            ExprKind::Pos(_) => ExprKind::Pos(Some(Box::new(lhs))),
7194            ExprKind::Exit(_) => ExprKind::Exit(Some(Box::new(lhs))),
7195
7196            // ── Higher-order / list-taking forms: replace the `list` slot ──────
7197            ExprKind::MapExpr {
7198                block,
7199                list: _,
7200                flatten_array_refs,
7201                stream,
7202            } => ExprKind::MapExpr {
7203                block,
7204                list: Box::new(lhs),
7205                flatten_array_refs,
7206                stream,
7207            },
7208            ExprKind::MapExprComma {
7209                expr,
7210                list: _,
7211                flatten_array_refs,
7212                stream,
7213            } => ExprKind::MapExprComma {
7214                expr,
7215                list: Box::new(lhs),
7216                flatten_array_refs,
7217                stream,
7218            },
7219            ExprKind::GrepExpr {
7220                block,
7221                list: _,
7222                keyword,
7223            } => ExprKind::GrepExpr {
7224                block,
7225                list: Box::new(lhs),
7226                keyword,
7227            },
7228            ExprKind::GrepExprComma {
7229                expr,
7230                list: _,
7231                keyword,
7232            } => ExprKind::GrepExprComma {
7233                expr,
7234                list: Box::new(lhs),
7235                keyword,
7236            },
7237            ExprKind::ForEachExpr { block, list: _ } => ExprKind::ForEachExpr {
7238                block,
7239                list: Box::new(lhs),
7240            },
7241            ExprKind::SortExpr { cmp, list: _ } => ExprKind::SortExpr {
7242                cmp,
7243                list: Box::new(lhs),
7244            },
7245            ExprKind::JoinExpr { separator, list: _ } => ExprKind::JoinExpr {
7246                separator,
7247                list: Box::new(lhs),
7248            },
7249            ExprKind::ReduceExpr { block, list: _ } => ExprKind::ReduceExpr {
7250                block,
7251                list: Box::new(lhs),
7252            },
7253            ExprKind::PMapExpr {
7254                block,
7255                list: _,
7256                progress,
7257                flat_outputs,
7258                on_cluster,
7259                stream,
7260            } => ExprKind::PMapExpr {
7261                block,
7262                list: Box::new(lhs),
7263                progress,
7264                flat_outputs,
7265                on_cluster,
7266                stream,
7267            },
7268            ExprKind::ParExpr { block, list: _ } => ExprKind::ParExpr {
7269                block,
7270                list: Box::new(lhs),
7271            },
7272            ExprKind::ParReduceExpr {
7273                extract_block,
7274                reduce_block,
7275                list: _,
7276            } => ExprKind::ParReduceExpr {
7277                extract_block,
7278                reduce_block,
7279                list: Box::new(lhs),
7280            },
7281            ExprKind::PMapChunkedExpr {
7282                chunk_size,
7283                block,
7284                list: _,
7285                progress,
7286            } => ExprKind::PMapChunkedExpr {
7287                chunk_size,
7288                block,
7289                list: Box::new(lhs),
7290                progress,
7291            },
7292            ExprKind::PGrepExpr {
7293                block,
7294                list: _,
7295                progress,
7296                stream,
7297            } => ExprKind::PGrepExpr {
7298                block,
7299                list: Box::new(lhs),
7300                progress,
7301                stream,
7302            },
7303            ExprKind::PForExpr {
7304                block,
7305                list: _,
7306                progress,
7307            } => ExprKind::PForExpr {
7308                block,
7309                list: Box::new(lhs),
7310                progress,
7311            },
7312            ExprKind::PSortExpr {
7313                cmp,
7314                list: _,
7315                progress,
7316            } => ExprKind::PSortExpr {
7317                cmp,
7318                list: Box::new(lhs),
7319                progress,
7320            },
7321            ExprKind::PReduceExpr {
7322                block,
7323                list: _,
7324                progress,
7325            } => ExprKind::PReduceExpr {
7326                block,
7327                list: Box::new(lhs),
7328                progress,
7329            },
7330            ExprKind::PcacheExpr {
7331                block,
7332                list: _,
7333                progress,
7334            } => ExprKind::PcacheExpr {
7335                block,
7336                list: Box::new(lhs),
7337                progress,
7338            },
7339            ExprKind::PReduceInitExpr {
7340                init,
7341                block,
7342                list: _,
7343                progress,
7344            } => ExprKind::PReduceInitExpr {
7345                init,
7346                block,
7347                list: Box::new(lhs),
7348                progress,
7349            },
7350            ExprKind::PMapReduceExpr {
7351                map_block,
7352                reduce_block,
7353                list: _,
7354                progress,
7355            } => ExprKind::PMapReduceExpr {
7356                map_block,
7357                reduce_block,
7358                list: Box::new(lhs),
7359                progress,
7360            },
7361
7362            // ── Push / unshift: first arg is the array, so pipe the LHS
7363            //     into the **values** list — `"x" |> push(@arr)` → `push @arr, "x"`
7364            //     is unchanged, but `@arr |> push "x"` is unnatural; use push
7365            //     directly for that.
7366            ExprKind::Push { array, mut values } => {
7367                values.insert(0, lhs);
7368                ExprKind::Push { array, values }
7369            }
7370            ExprKind::Unshift { array, mut values } => {
7371                values.insert(0, lhs);
7372                ExprKind::Unshift { array, values }
7373            }
7374
7375            // ── Split: pipe the subject string — `$line |> split /,/` ─────────
7376            ExprKind::SplitExpr {
7377                pattern,
7378                string: _,
7379                limit,
7380            } => ExprKind::SplitExpr {
7381                pattern,
7382                string: Box::new(lhs),
7383                limit,
7384            },
7385
7386            // ── Regex ops: pipe the subject — `$str |> s/\n//g` ────────────────
7387            //    Auto-inject `r` flag so the substitution returns the modified
7388            //    string instead of the match count (non-destructive / Perl /r).
7389            ExprKind::Substitution {
7390                pattern,
7391                replacement,
7392                mut flags,
7393                expr: _,
7394                delim,
7395            } => {
7396                if !flags.contains('r') {
7397                    flags.push('r');
7398                }
7399                ExprKind::Substitution {
7400                    expr: Box::new(lhs),
7401                    pattern,
7402                    replacement,
7403                    flags,
7404                    delim,
7405                }
7406            }
7407            ExprKind::Transliterate {
7408                from,
7409                to,
7410                mut flags,
7411                expr: _,
7412                delim,
7413            } => {
7414                if !flags.contains('r') {
7415                    flags.push('r');
7416                }
7417                ExprKind::Transliterate {
7418                    expr: Box::new(lhs),
7419                    from,
7420                    to,
7421                    flags,
7422                    delim,
7423                }
7424            }
7425            ExprKind::Match {
7426                pattern,
7427                flags,
7428                scalar_g,
7429                expr: _,
7430                delim,
7431            } => ExprKind::Match {
7432                expr: Box::new(lhs),
7433                pattern,
7434                flags,
7435                scalar_g,
7436                delim,
7437            },
7438            // Bare `/regex/` (no explicit `m`): promote to Match on piped LHS
7439            ExprKind::Regex(pattern, flags) => ExprKind::Match {
7440                expr: Box::new(lhs),
7441                pattern,
7442                flags,
7443                scalar_g: false,
7444                delim: '/',
7445            },
7446
7447            // ── Bareword function name → plain unary call ──────────────────────
7448            ExprKind::Bareword(name) => match name.as_str() {
7449                "reverse" => {
7450                    if crate::no_interop_mode() {
7451                        return Err(self.syntax_err(
7452                            "stryke uses `rev` instead of `reverse` (--no-interop)",
7453                            line,
7454                        ));
7455                    }
7456                    ExprKind::ReverseExpr(Box::new(lhs))
7457                }
7458                "rv" | "reversed" | "rev" => ExprKind::Rev(Box::new(lhs)),
7459                "uq" | "uniq" | "distinct" => ExprKind::FuncCall {
7460                    name: "uniq".to_string(),
7461                    args: vec![lhs],
7462                },
7463                "fl" | "flatten" => ExprKind::FuncCall {
7464                    name: "flatten".to_string(),
7465                    args: vec![lhs],
7466                },
7467                _ => ExprKind::FuncCall {
7468                    name,
7469                    args: vec![lhs],
7470                },
7471            },
7472
7473            // ── Callable scalars / coderefs / derefs → IndirectCall ────────────
7474            kind @ (ExprKind::ScalarVar(_)
7475            | ExprKind::ArrayElement { .. }
7476            | ExprKind::HashElement { .. }
7477            | ExprKind::Deref { .. }
7478            | ExprKind::ArrowDeref { .. }
7479            | ExprKind::CodeRef { .. }
7480            | ExprKind::SubroutineRef(_)
7481            | ExprKind::SubroutineCodeRef(_)
7482            | ExprKind::DynamicSubCodeRef(_)) => ExprKind::IndirectCall {
7483                target: Box::new(Expr { kind, line: rline }),
7484                args: vec![lhs],
7485                ampersand: false,
7486                pass_caller_arglist: false,
7487            },
7488
7489            // `LHS |> >{ BLOCK }` — the `>{}` form is parsed everywhere as `Do(CodeRef)` (IIFE).
7490            // On the RHS of `|>` we want pipe-apply semantics instead: unwrap the Do and invoke
7491            // the inner coderef with `lhs` as `$_[0]`, matching `LHS |> fn { ... }`.
7492            ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. }) => {
7493                ExprKind::IndirectCall {
7494                    target: inner,
7495                    args: vec![lhs],
7496                    ampersand: false,
7497                    pass_caller_arglist: false,
7498                }
7499            }
7500
7501            other => {
7502                return Err(self.syntax_err(
7503                    format!(
7504                        "right-hand side of `|>` must be a call, builtin, or coderef \
7505                         expression (got {})",
7506                        Self::expr_kind_name(&other)
7507                    ),
7508                    line,
7509                ));
7510            }
7511        };
7512        Ok(Expr {
7513            kind: new_kind,
7514            line,
7515        })
7516    }
7517
7518    /// Short label for an `ExprKind` (used in `|>` error messages).
7519    fn expr_kind_name(kind: &ExprKind) -> &'static str {
7520        match kind {
7521            ExprKind::Integer(_) | ExprKind::Float(_) => "numeric literal",
7522            ExprKind::String(_) | ExprKind::InterpolatedString(_) => "string literal",
7523            ExprKind::BinOp { .. } => "binary expression",
7524            ExprKind::UnaryOp { .. } => "unary expression",
7525            ExprKind::Ternary { .. } => "ternary expression",
7526            ExprKind::Assign { .. } | ExprKind::CompoundAssign { .. } => "assignment",
7527            ExprKind::List(_) => "list expression",
7528            ExprKind::Range { .. } => "range expression",
7529            _ => "expression",
7530        }
7531    }
7532
7533    // or / not (lowest precedence word operators)
7534    fn parse_or_word(&mut self) -> StrykeResult<Expr> {
7535        let mut left = self.parse_and_word()?;
7536        while matches!(self.peek(), Token::LogOrWord) {
7537            let line = left.line;
7538            self.advance();
7539            let right = self.parse_and_word()?;
7540            left = Expr {
7541                kind: ExprKind::BinOp {
7542                    left: Box::new(left),
7543                    op: BinOp::LogOrWord,
7544                    right: Box::new(right),
7545                },
7546                line,
7547            };
7548        }
7549        Ok(left)
7550    }
7551
7552    fn parse_and_word(&mut self) -> StrykeResult<Expr> {
7553        let mut left = self.parse_not_word()?;
7554        while matches!(self.peek(), Token::LogAndWord) {
7555            let line = left.line;
7556            self.advance();
7557            let right = self.parse_not_word()?;
7558            left = Expr {
7559                kind: ExprKind::BinOp {
7560                    left: Box::new(left),
7561                    op: BinOp::LogAndWord,
7562                    right: Box::new(right),
7563                },
7564                line,
7565            };
7566        }
7567        Ok(left)
7568    }
7569
7570    fn parse_not_word(&mut self) -> StrykeResult<Expr> {
7571        if matches!(self.peek(), Token::LogNotWord) {
7572            let line = self.peek_line();
7573            self.advance();
7574            let expr = self.parse_not_word()?;
7575            return Ok(Expr {
7576                kind: ExprKind::UnaryOp {
7577                    op: UnaryOp::LogNotWord,
7578                    expr: Box::new(expr),
7579                },
7580                line,
7581            });
7582        }
7583        // Descend into assignment level — `not` sits ABOVE `=` in Perl
7584        // precedence, so `not $x = 5` parses as `not ($x = 5)`.
7585        self.parse_assign_expr()
7586    }
7587
7588    fn parse_log_or(&mut self) -> StrykeResult<Expr> {
7589        let mut left = self.parse_log_and()?;
7590        loop {
7591            let op = match self.peek() {
7592                Token::LogOr => BinOp::LogOr,
7593                Token::DefinedOr => BinOp::DefinedOr,
7594                _ => break,
7595            };
7596            let line = left.line;
7597            self.advance();
7598            let right = self.parse_log_and()?;
7599            left = Expr {
7600                kind: ExprKind::BinOp {
7601                    left: Box::new(left),
7602                    op,
7603                    right: Box::new(right),
7604                },
7605                line,
7606            };
7607        }
7608        Ok(left)
7609    }
7610
7611    fn parse_log_and(&mut self) -> StrykeResult<Expr> {
7612        let mut left = self.parse_bit_or()?;
7613        while matches!(self.peek(), Token::LogAnd) {
7614            let line = left.line;
7615            self.advance();
7616            let right = self.parse_bit_or()?;
7617            left = Expr {
7618                kind: ExprKind::BinOp {
7619                    left: Box::new(left),
7620                    op: BinOp::LogAnd,
7621                    right: Box::new(right),
7622                },
7623                line,
7624            };
7625        }
7626        Ok(left)
7627    }
7628
7629    fn parse_bit_or(&mut self) -> StrykeResult<Expr> {
7630        let mut left = self.parse_bit_xor()?;
7631        while matches!(self.peek(), Token::BitOr) {
7632            let line = left.line;
7633            self.advance();
7634            let right = self.parse_bit_xor()?;
7635            left = Expr {
7636                kind: ExprKind::BinOp {
7637                    left: Box::new(left),
7638                    op: BinOp::BitOr,
7639                    right: Box::new(right),
7640                },
7641                line,
7642            };
7643        }
7644        Ok(left)
7645    }
7646
7647    fn parse_bit_xor(&mut self) -> StrykeResult<Expr> {
7648        let mut left = self.parse_bit_and()?;
7649        while matches!(self.peek(), Token::BitXor) {
7650            let line = left.line;
7651            self.advance();
7652            let right = self.parse_bit_and()?;
7653            left = Expr {
7654                kind: ExprKind::BinOp {
7655                    left: Box::new(left),
7656                    op: BinOp::BitXor,
7657                    right: Box::new(right),
7658                },
7659                line,
7660            };
7661        }
7662        Ok(left)
7663    }
7664
7665    fn parse_bit_and(&mut self) -> StrykeResult<Expr> {
7666        let mut left = self.parse_equality()?;
7667        while matches!(self.peek(), Token::BitAnd) {
7668            let line = left.line;
7669            self.advance();
7670            let right = self.parse_equality()?;
7671            left = Expr {
7672                kind: ExprKind::BinOp {
7673                    left: Box::new(left),
7674                    op: BinOp::BitAnd,
7675                    right: Box::new(right),
7676                },
7677                line,
7678            };
7679        }
7680        Ok(left)
7681    }
7682
7683    fn parse_equality(&mut self) -> StrykeResult<Expr> {
7684        let mut left = self.parse_comparison()?;
7685        loop {
7686            let op = match self.peek() {
7687                Token::NumEq => BinOp::NumEq,
7688                Token::NumNe => BinOp::NumNe,
7689                Token::StrEq => BinOp::StrEq,
7690                Token::StrNe => BinOp::StrNe,
7691                Token::Spaceship => BinOp::Spaceship,
7692                Token::StrCmp => BinOp::StrCmp,
7693                _ => break,
7694            };
7695            let line = left.line;
7696            self.advance();
7697            let right = self.parse_comparison()?;
7698            left = Expr {
7699                kind: ExprKind::BinOp {
7700                    left: Box::new(left),
7701                    op,
7702                    right: Box::new(right),
7703                },
7704                line,
7705            };
7706        }
7707        Ok(left)
7708    }
7709
7710    fn parse_comparison(&mut self) -> StrykeResult<Expr> {
7711        let left = self.parse_shift()?;
7712        let first_op = match self.peek() {
7713            Token::NumLt => BinOp::NumLt,
7714            Token::NumGt => BinOp::NumGt,
7715            Token::NumLe => BinOp::NumLe,
7716            Token::NumGe => BinOp::NumGe,
7717            Token::StrLt => BinOp::StrLt,
7718            Token::StrGt => BinOp::StrGt,
7719            Token::StrLe => BinOp::StrLe,
7720            Token::StrGe => BinOp::StrGe,
7721            _ => return Ok(left),
7722        };
7723        let line = left.line;
7724        self.advance();
7725        let middle = self.parse_shift()?;
7726
7727        let second_op = match self.peek() {
7728            Token::NumLt => Some(BinOp::NumLt),
7729            Token::NumGt => Some(BinOp::NumGt),
7730            Token::NumLe => Some(BinOp::NumLe),
7731            Token::NumGe => Some(BinOp::NumGe),
7732            Token::StrLt => Some(BinOp::StrLt),
7733            Token::StrGt => Some(BinOp::StrGt),
7734            Token::StrLe => Some(BinOp::StrLe),
7735            Token::StrGe => Some(BinOp::StrGe),
7736            _ => None,
7737        };
7738
7739        if second_op.is_none() {
7740            return Ok(Expr {
7741                kind: ExprKind::BinOp {
7742                    left: Box::new(left),
7743                    op: first_op,
7744                    right: Box::new(middle),
7745                },
7746                line,
7747            });
7748        }
7749
7750        // Chained comparison: `a < b < c` → `(a < b) && (b < c)`
7751        // Collect all operands and operators for chains like `1 < x < 10 < y`
7752        let mut operands = vec![left, middle];
7753        let mut ops = vec![first_op];
7754
7755        loop {
7756            let op = match self.peek() {
7757                Token::NumLt => BinOp::NumLt,
7758                Token::NumGt => BinOp::NumGt,
7759                Token::NumLe => BinOp::NumLe,
7760                Token::NumGe => BinOp::NumGe,
7761                Token::StrLt => BinOp::StrLt,
7762                Token::StrGt => BinOp::StrGt,
7763                Token::StrLe => BinOp::StrLe,
7764                Token::StrGe => BinOp::StrGe,
7765                _ => break,
7766            };
7767            self.advance();
7768            ops.push(op);
7769            operands.push(self.parse_shift()?);
7770        }
7771
7772        // Build `(a op0 b) && (b op1 c) && (c op2 d) && ...`
7773        let mut result = Expr {
7774            kind: ExprKind::BinOp {
7775                left: Box::new(operands[0].clone()),
7776                op: ops[0],
7777                right: Box::new(operands[1].clone()),
7778            },
7779            line,
7780        };
7781
7782        for i in 1..ops.len() {
7783            let cmp = Expr {
7784                kind: ExprKind::BinOp {
7785                    left: Box::new(operands[i].clone()),
7786                    op: ops[i],
7787                    right: Box::new(operands[i + 1].clone()),
7788                },
7789                line,
7790            };
7791            result = Expr {
7792                kind: ExprKind::BinOp {
7793                    left: Box::new(result),
7794                    op: BinOp::LogAnd,
7795                    right: Box::new(cmp),
7796                },
7797                line,
7798            };
7799        }
7800
7801        Ok(result)
7802    }
7803
7804    fn parse_shift(&mut self) -> StrykeResult<Expr> {
7805        let mut left = self.parse_addition()?;
7806        loop {
7807            let op = match self.peek() {
7808                Token::ShiftLeft => BinOp::ShiftLeft,
7809                Token::ShiftRight => BinOp::ShiftRight,
7810                _ => break,
7811            };
7812            let line = left.line;
7813            self.advance();
7814            let right = self.parse_addition()?;
7815            left = Expr {
7816                kind: ExprKind::BinOp {
7817                    left: Box::new(left),
7818                    op,
7819                    right: Box::new(right),
7820                },
7821                line,
7822            };
7823        }
7824        Ok(left)
7825    }
7826
7827    fn parse_addition(&mut self) -> StrykeResult<Expr> {
7828        let mut left = self.parse_multiplication()?;
7829        loop {
7830            // Implicit semicolon: `-` or `+` on a new line is a unary operator on
7831            // the next statement, not a binary operator continuing this expression.
7832            let op = match self.peek() {
7833                Token::Plus if self.peek_line() == self.prev_line() => BinOp::Add,
7834                Token::Minus if self.peek_line() == self.prev_line() => BinOp::Sub,
7835                Token::Dot => BinOp::Concat,
7836                _ => break,
7837            };
7838            let line = left.line;
7839            self.advance();
7840            let right = self.parse_multiplication()?;
7841            left = Expr {
7842                kind: ExprKind::BinOp {
7843                    left: Box::new(left),
7844                    op,
7845                    right: Box::new(right),
7846                },
7847                line,
7848            };
7849        }
7850        Ok(left)
7851    }
7852
7853    fn parse_multiplication(&mut self) -> StrykeResult<Expr> {
7854        let mut left = self.parse_regex_bind()?;
7855        loop {
7856            let op = match self.peek() {
7857                Token::Star => BinOp::Mul,
7858                Token::Slash if self.suppress_slash_as_div == 0 => BinOp::Div,
7859                // Implicit semicolon: `%` on a new line is a hash dereference or hash
7860                // sigil for the next statement, not modulo operator on this expression.
7861                Token::Percent if self.peek_line() == self.prev_line() => BinOp::Mod,
7862                Token::X => {
7863                    let line = left.line;
7864                    // List-repeat fires when the LHS was just closed by a
7865                    // list-constructor paren (`(EXPR)`, `(LIST)`, `()`) or
7866                    // `qw(...)`. `parse_primary` records the post-close
7867                    // position; an exact match against `self.pos` here means
7868                    // no postfix consumed any tokens between the close and
7869                    // the `x`, so the LHS is intrinsically a list construct.
7870                    let list_repeat = self.list_construct_close_pos == Some(self.pos);
7871                    self.advance();
7872                    let right = self.parse_regex_bind()?;
7873                    left = Expr {
7874                        kind: ExprKind::Repeat {
7875                            expr: Box::new(left),
7876                            count: Box::new(right),
7877                            list_repeat,
7878                        },
7879                        line,
7880                    };
7881                    continue;
7882                }
7883                _ => break,
7884            };
7885            let line = left.line;
7886            self.advance();
7887            let right = self.parse_regex_bind()?;
7888            left = Expr {
7889                kind: ExprKind::BinOp {
7890                    left: Box::new(left),
7891                    op,
7892                    right: Box::new(right),
7893                },
7894                line,
7895            };
7896        }
7897        Ok(left)
7898    }
7899
7900    fn parse_regex_bind(&mut self) -> StrykeResult<Expr> {
7901        let left = self.parse_unary()?;
7902        match self.peek() {
7903            Token::BindMatch => {
7904                let line = left.line;
7905                self.advance();
7906                match self.peek().clone() {
7907                    Token::Regex(pattern, flags, delim) => {
7908                        self.advance();
7909                        Ok(Expr {
7910                            kind: ExprKind::Match {
7911                                expr: Box::new(left),
7912                                pattern,
7913                                flags,
7914                                scalar_g: false,
7915                                delim,
7916                            },
7917                            line,
7918                        })
7919                    }
7920                    Token::Ident(ref s) if s.starts_with('\x00') => {
7921                        let (Token::Ident(encoded), _) = self.advance() else {
7922                            unreachable!()
7923                        };
7924                        let parts: Vec<&str> = encoded.split('\x00').collect();
7925                        if parts.len() >= 4 && parts[1] == "s" {
7926                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
7927                            Ok(Expr {
7928                                kind: ExprKind::Substitution {
7929                                    expr: Box::new(left),
7930                                    pattern: parts[2].to_string(),
7931                                    replacement: parts[3].to_string(),
7932                                    flags: parts.get(4).unwrap_or(&"").to_string(),
7933                                    delim,
7934                                },
7935                                line,
7936                            })
7937                        } else if parts.len() >= 4 && parts[1] == "tr" {
7938                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
7939                            Ok(Expr {
7940                                kind: ExprKind::Transliterate {
7941                                    expr: Box::new(left),
7942                                    from: parts[2].to_string(),
7943                                    to: parts[3].to_string(),
7944                                    flags: parts.get(4).unwrap_or(&"").to_string(),
7945                                    delim,
7946                                },
7947                                line,
7948                            })
7949                        } else {
7950                            Err(self.syntax_err("Invalid regex binding", line))
7951                        }
7952                    }
7953                    _ => {
7954                        let rhs = self.parse_unary()?;
7955                        Ok(Expr {
7956                            kind: ExprKind::BinOp {
7957                                left: Box::new(left),
7958                                op: BinOp::BindMatch,
7959                                right: Box::new(rhs),
7960                            },
7961                            line,
7962                        })
7963                    }
7964                }
7965            }
7966            Token::BindNotMatch => {
7967                let line = left.line;
7968                self.advance();
7969                match self.peek().clone() {
7970                    Token::Regex(pattern, flags, delim) => {
7971                        self.advance();
7972                        Ok(Expr {
7973                            kind: ExprKind::UnaryOp {
7974                                op: UnaryOp::LogNot,
7975                                expr: Box::new(Expr {
7976                                    kind: ExprKind::Match {
7977                                        expr: Box::new(left),
7978                                        pattern,
7979                                        flags,
7980                                        scalar_g: false,
7981                                        delim,
7982                                    },
7983                                    line,
7984                                }),
7985                            },
7986                            line,
7987                        })
7988                    }
7989                    Token::Ident(ref s) if s.starts_with('\x00') => {
7990                        let (Token::Ident(encoded), _) = self.advance() else {
7991                            unreachable!()
7992                        };
7993                        let parts: Vec<&str> = encoded.split('\x00').collect();
7994                        if parts.len() >= 4 && parts[1] == "s" {
7995                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
7996                            Ok(Expr {
7997                                kind: ExprKind::UnaryOp {
7998                                    op: UnaryOp::LogNot,
7999                                    expr: Box::new(Expr {
8000                                        kind: ExprKind::Substitution {
8001                                            expr: Box::new(left),
8002                                            pattern: parts[2].to_string(),
8003                                            replacement: parts[3].to_string(),
8004                                            flags: parts.get(4).unwrap_or(&"").to_string(),
8005                                            delim,
8006                                        },
8007                                        line,
8008                                    }),
8009                                },
8010                                line,
8011                            })
8012                        } else if parts.len() >= 4 && parts[1] == "tr" {
8013                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
8014                            Ok(Expr {
8015                                kind: ExprKind::UnaryOp {
8016                                    op: UnaryOp::LogNot,
8017                                    expr: Box::new(Expr {
8018                                        kind: ExprKind::Transliterate {
8019                                            expr: Box::new(left),
8020                                            from: parts[2].to_string(),
8021                                            to: parts[3].to_string(),
8022                                            flags: parts.get(4).unwrap_or(&"").to_string(),
8023                                            delim,
8024                                        },
8025                                        line,
8026                                    }),
8027                                },
8028                                line,
8029                            })
8030                        } else {
8031                            Err(self.syntax_err("Invalid regex binding after !~", line))
8032                        }
8033                    }
8034                    _ => {
8035                        let rhs = self.parse_unary()?;
8036                        Ok(Expr {
8037                            kind: ExprKind::BinOp {
8038                                left: Box::new(left),
8039                                op: BinOp::BindNotMatch,
8040                                right: Box::new(rhs),
8041                            },
8042                            line,
8043                        })
8044                    }
8045                }
8046            }
8047            _ => Ok(left),
8048        }
8049    }
8050
8051    /// Parse thread macro input. Like `parse_range` but suppresses `/` as division
8052    /// so that `/pattern/` is left for the thread stage parser to handle as regex filter.
8053    fn parse_thread_input(&mut self) -> StrykeResult<Expr> {
8054        self.suppress_slash_as_div = self.suppress_slash_as_div.saturating_add(1);
8055        let result = self.parse_range();
8056        self.suppress_slash_as_div = self.suppress_slash_as_div.saturating_sub(1);
8057        result
8058    }
8059
8060    /// Parse `~p>` / `~p>>` parallel-chunk thread-macros. Equivalent to
8061    /// `par_reduce { stage1 |> stage2 |> ... } SOURCE`, with optional
8062    /// `||>` or `|then|` mid-pipeline boundary that switches to a normal
8063    /// `~>` / `~>>` continuation operating on the auto-merged result.
8064    fn parse_thread_macro_chunk_par(
8065        &mut self,
8066        line: usize,
8067        thread_last: bool,
8068    ) -> StrykeResult<Expr> {
8069        // Source: same parsing rules as `~>`.
8070        self.suppress_parenless_call = self.suppress_parenless_call.saturating_add(1);
8071        let source_expr = self.parse_thread_input();
8072        self.suppress_parenless_call = self.suppress_parenless_call.saturating_sub(1);
8073        let source_expr = source_expr?;
8074
8075        // Per-chunk stage chain: stages operate on `@_` (the chunk elements)
8076        // which the par_reduce runtime binds as the argument array. Use
8077        // `pending_thread_input` to seed the stage chain with `@_`.
8078        self.pending_thread_input = Some(Expr {
8079            kind: ExprKind::ArrayVar("_".into()),
8080            line,
8081        });
8082        let chunk_chain = self.parse_thread_macro_inner(line, thread_last, None);
8083        self.pending_thread_input = None;
8084        let chunk_chain = chunk_chain?;
8085
8086        // `parse_thread_macro_inner` (under pipe_rhs_depth > 0) wraps its
8087        // result as `fn { ... stages applied to $_[0] ... }`. Unwrap to
8088        // get the bare Block (`Vec<Statement>`) for the `par_reduce`
8089        // extract slot.
8090        let extract_block: Block = match chunk_chain.kind {
8091            ExprKind::CodeRef { params: _, body } => body,
8092            _ => vec![Statement {
8093                label: None,
8094                kind: StmtKind::Expression(chunk_chain),
8095                line,
8096            }],
8097        };
8098
8099        let par_reduce = Expr {
8100            kind: ExprKind::ParReduceExpr {
8101                extract_block,
8102                reduce_block: None,
8103                list: Box::new(source_expr),
8104            },
8105            line,
8106        };
8107
8108        // Check for `||>` / `|then|` boundary; if present, parse the
8109        // continuation as a normal `~>` / `~>>` thread macro with the
8110        // par_reduce result as its input.
8111        if self.eat_chunk_par_split_boundary() {
8112            return self.parse_thread_macro_continuation(par_reduce, line, thread_last);
8113        }
8114        Ok(par_reduce)
8115    }
8116
8117    /// Parse `~d>` / `~d>>` distributed thread-macros. Same chunk-block
8118    /// semantics as `~p>` (stages operate on `@_`) but chunks ship to a
8119    /// `RemoteCluster` via the existing `cluster::run_cluster` dispatcher.
8120    /// Syntax: `~d> on EXPR SOURCE stage1 stage2 ...`. The `on EXPR` slot
8121    /// is required; without it the operator falls through to a syntax
8122    /// error (no implicit default-cluster in v1).
8123    fn parse_thread_macro_dist(&mut self, line: usize, thread_last: bool) -> StrykeResult<Expr> {
8124        // Required `on EXPR` — the cluster operand.
8125        let on_ok = matches!(self.peek(), Token::Ident(ref s) if s == "on");
8126        if !on_ok {
8127            return Err(
8128                self.syntax_err("~d>: expected `on <cluster-expr>` after the operator", line)
8129            );
8130        }
8131        self.advance(); // consume `on`
8132                        // Parse cluster expr — same parse-rules as a thread-macro input
8133                        // (avoid pulling stages into the cluster expression).
8134        self.suppress_parenless_call = self.suppress_parenless_call.saturating_add(1);
8135        // Without this, `on $cluster () map { … }` parses `()` as a postfix
8136        // indirect call on `$cluster`, stealing the empty list meant as SOURCE.
8137        // Zero-arg cluster from a scalar sub: `on ($factory())` or `on $f->()`.
8138        self.suppress_indirect_paren_call = self.suppress_indirect_paren_call.saturating_add(1);
8139        let cluster_expr = self.parse_thread_input();
8140        self.suppress_indirect_paren_call = self.suppress_indirect_paren_call.saturating_sub(1);
8141        self.suppress_parenless_call = self.suppress_parenless_call.saturating_sub(1);
8142        let cluster_expr = cluster_expr?;
8143
8144        // Source list: same rules as `~p>` source.
8145        self.suppress_parenless_call = self.suppress_parenless_call.saturating_add(1);
8146        let source_expr = self.parse_thread_input();
8147        self.suppress_parenless_call = self.suppress_parenless_call.saturating_sub(1);
8148        let source_expr = source_expr?;
8149
8150        // Stage chain seeded with `@_` — matches `~p>` chunk-block
8151        // semantics. The VM-side eval prepends `@_ = $_;` to the shipped
8152        // block source so the remote agent's `set_topic(chunk_flat_array)`
8153        // is reflected into `@_` before user stages run.
8154        self.pending_thread_input = Some(Expr {
8155            kind: ExprKind::ArrayVar("_".into()),
8156            line,
8157        });
8158        let chunk_chain = self.parse_thread_macro_inner(line, thread_last, None);
8159        self.pending_thread_input = None;
8160        let chunk_chain = chunk_chain?;
8161
8162        let extract_block: Block = match chunk_chain.kind {
8163            ExprKind::CodeRef { params: _, body } => body,
8164            _ => vec![Statement {
8165                label: None,
8166                kind: StmtKind::Expression(chunk_chain),
8167                line,
8168            }],
8169        };
8170
8171        let dist_reduce = Expr {
8172            kind: ExprKind::DistReduceExpr {
8173                cluster: Box::new(cluster_expr),
8174                extract_block,
8175                list: Box::new(source_expr),
8176            },
8177            line,
8178        };
8179
8180        // `||>` / `|then|` boundary continuation, same as `~p>`.
8181        if self.eat_chunk_par_split_boundary() {
8182            return self.parse_thread_macro_continuation(dist_reduce, line, thread_last);
8183        }
8184        Ok(dist_reduce)
8185    }
8186
8187    /// Parse a `~>` / `~>>` continuation after a `||>` / `|then|`
8188    /// chunk-parallel-to-sequential boundary. Reuses
8189    /// `parse_thread_macro_inner` with `result_init: Some(prior)` so the
8190    /// stage loop threads from the par_reduce result instead of parsing
8191    /// a fresh source expression.
8192    fn parse_thread_macro_continuation(
8193        &mut self,
8194        prior: Expr,
8195        line: usize,
8196        thread_last: bool,
8197    ) -> StrykeResult<Expr> {
8198        self.pending_thread_input = Some(prior);
8199        let res = self.parse_thread_macro_inner(line, thread_last, None);
8200        self.pending_thread_input = None;
8201        res
8202    }
8203
8204    /// Try to consume `||>` (LogOr followed by `>`) or `|then|`
8205    /// (`Pipe Ident("then") Pipe`) as the chunk-parallel → sequential
8206    /// switch marker. Returns true if a boundary was consumed.
8207    fn eat_chunk_par_split_boundary(&mut self) -> bool {
8208        // `||>` = `LogOr` token (already merged in lex) followed by `>`.
8209        if matches!(self.peek(), Token::LogOr) && matches!(self.peek_at(1), Token::NumGt) {
8210            self.advance(); // ||
8211            self.advance(); // >
8212            return true;
8213        }
8214        // `|then|` = `BitOr` + `Ident("then")` + `BitOr`.
8215        if matches!(self.peek(), Token::BitOr) {
8216            if let Token::Ident(name) = self.peek_at(1).clone() {
8217                if name == "then" && matches!(self.peek_at(2), Token::BitOr) {
8218                    self.advance(); // |
8219                    self.advance(); // then
8220                    self.advance(); // |
8221                    return true;
8222                }
8223            }
8224        }
8225        false
8226    }
8227
8228    /// Perl `..` / `...` operator — precedence sits between `?:` and `||` (`perlop`), so
8229    /// `$x .. $x + 3` parses as `$x .. ($x + 3)` and `1..$n||5` parses as `1..($n||5)`. Both
8230    /// operands recurse through `parse_log_or`, which in turn walks down through all tighter
8231    /// operators (additive, multiplicative, regex bind, unary). Non-associative: the right
8232    /// operand is a single `parse_log_or` so `1..5..10` is a parse error in Perl, but we accept
8233    /// it greedily (left-associated) because the lexer already forbids `..` after a range RHS.
8234    fn parse_range(&mut self) -> StrykeResult<Expr> {
8235        let left = self.parse_log_or()?;
8236        let line = left.line;
8237        // `1..10` (traditional inclusive) / `1...10` (exclusive) / `1:10`
8238        // (short form) / `1~10` (universal short form). The `~` separator
8239        // works for every range type and is the only viable separator for
8240        // IPv6 since IPv6 already uses `:` internally; `:` would collide.
8241        // It also dodges `!`'s collision with the `_!N!` paired char-index
8242        // syntax. Single-`~` (vs `!!!` triple) keeps the surface simple.
8243        let (exclusive, _colon_style) = if self.eat(&Token::RangeExclusive) {
8244            (true, false)
8245        } else if self.eat(&Token::Range) {
8246            (false, false)
8247        } else if self.suppress_colon_range == 0 && self.eat(&Token::Colon) {
8248            // `1:10` short form — only valid for numeric ranges, not ternary
8249            // Lookahead: must be followed by something that looks like a range endpoint
8250            (false, true)
8251        } else if self.suppress_tilde_range == 0 && self.eat(&Token::BitNot) {
8252            (false, true)
8253        } else {
8254            return Ok(left);
8255        };
8256        let right = self.parse_log_or()?;
8257        // Optional step: `1..100:2` / `1:100:2` / `IPV6~IPV6~STEP`. `~` is
8258        // gated by `suppress_tilde_range` so paired char-index (`$x~5~`)
8259        // doesn't get its closing delimiter eaten as a range op.
8260        let step = if self.eat(&Token::Colon)
8261            || (self.suppress_tilde_range == 0 && self.eat(&Token::BitNot))
8262        {
8263            Some(Box::new(self.parse_unary()?))
8264        } else {
8265            None
8266        };
8267        Ok(Expr {
8268            kind: ExprKind::Range {
8269                from: Box::new(left),
8270                to: Box::new(right),
8271                exclusive,
8272                step,
8273            },
8274            line,
8275        })
8276    }
8277
8278    /// `name` or `Foo::Bar::baz` — used after `sub`, unary `&`, etc.
8279    fn parse_package_qualified_identifier(&mut self) -> StrykeResult<String> {
8280        let mut name = match self.advance() {
8281            (Token::Ident(n), _) => n,
8282            (tok, l) => {
8283                return Err(self.syntax_err(format!("Expected identifier, got {:?}", tok), l));
8284            }
8285        };
8286        while self.eat(&Token::PackageSep) {
8287            match self.advance() {
8288                (Token::Ident(part), _) => {
8289                    name.push_str("::");
8290                    name.push_str(&part);
8291                }
8292                // Topic-slot scalars (`_`, `_<<<<`, `_3`, etc.) lex as
8293                // `Token::ScalarVar` per the lexer's reservation. Accept
8294                // them as the trailing segment of a package-qualified
8295                // name so callers (e.g. `parse_sub_decl`) can reject the
8296                // full name with a friendly "would shadow topic-slot"
8297                // message rather than a generic "Expected identifier
8298                // after `::`" lexer-level error.
8299                (Token::ScalarVar(part), _) if Self::is_underscore_topic_slot(&part) => {
8300                    name.push_str("::");
8301                    name.push_str(&part);
8302                }
8303                (tok, l) => {
8304                    return Err(self
8305                        .syntax_err(format!("Expected identifier after `::`, got {:?}", tok), l));
8306                }
8307            }
8308        }
8309        Ok(name)
8310    }
8311
8312    /// After consuming unary `&`: `name` or `Foo::Bar::baz` (Perl `&foo` / `&Foo::bar`).
8313    fn parse_qualified_subroutine_name(&mut self) -> StrykeResult<String> {
8314        self.parse_package_qualified_identifier()
8315    }
8316
8317    fn parse_unary(&mut self) -> StrykeResult<Expr> {
8318        let line = self.peek_line();
8319        match self.peek().clone() {
8320            Token::Minus => {
8321                self.advance();
8322                let expr = self.parse_power()?;
8323                Ok(Expr {
8324                    kind: ExprKind::UnaryOp {
8325                        op: UnaryOp::Negate,
8326                        expr: Box::new(expr),
8327                    },
8328                    line,
8329                })
8330            }
8331            // Unary `+EXPR` — Perl uses this to disambiguate barewords in hash subscripts (`$h{+Foo}`)
8332            // and for scalar context; treat as a no-op on the parsed operand.
8333            // Special case: `+{ ... }` forces hashref interpretation (Perl idiom),
8334            // even when the body is a list-yielding expression like `+{ map { ... } @arr }`.
8335            // Without this, `{ map { ... } @arr }` falls back to block/CodeRef parsing
8336            // because the body doesn't fit `KEY => VAL` shape.
8337            Token::Plus => {
8338                self.advance();
8339                if matches!(self.peek(), Token::LBrace) {
8340                    let line = self.peek_line();
8341                    self.advance(); // consume {
8342                    return self.parse_forced_hashref_body(line);
8343                }
8344                self.parse_unary()
8345            }
8346            Token::LogNot => {
8347                self.advance();
8348                let expr = self.parse_unary()?;
8349                Ok(Expr {
8350                    kind: ExprKind::UnaryOp {
8351                        op: UnaryOp::LogNot,
8352                        expr: Box::new(expr),
8353                    },
8354                    line,
8355                })
8356            }
8357            Token::BitNot => {
8358                self.advance();
8359                let expr = self.parse_unary()?;
8360                Ok(Expr {
8361                    kind: ExprKind::UnaryOp {
8362                        op: UnaryOp::BitNot,
8363                        expr: Box::new(expr),
8364                    },
8365                    line,
8366                })
8367            }
8368            Token::Increment => {
8369                self.advance();
8370                let expr = self.parse_postfix()?;
8371                Ok(Expr {
8372                    kind: ExprKind::UnaryOp {
8373                        op: UnaryOp::PreIncrement,
8374                        expr: Box::new(expr),
8375                    },
8376                    line,
8377                })
8378            }
8379            Token::Decrement => {
8380                self.advance();
8381                let expr = self.parse_postfix()?;
8382                Ok(Expr {
8383                    kind: ExprKind::UnaryOp {
8384                        op: UnaryOp::PreDecrement,
8385                        expr: Box::new(expr),
8386                    },
8387                    line,
8388                })
8389            }
8390            Token::BitAnd => {
8391                // Unary `&name` / `&Pkg::name` (call / coderef); binary `&` is in `parse_bit_and`.
8392                // `&$coderef(...)` — call sub whose ref is in a scalar (core `B.pm` / `&$recurse($sym)`).
8393                self.advance();
8394                if matches!(self.peek(), Token::LBrace) {
8395                    self.advance();
8396                    let inner = self.parse_expression()?;
8397                    self.expect(&Token::RBrace)?;
8398                    return Ok(Expr {
8399                        kind: ExprKind::DynamicSubCodeRef(Box::new(inner)),
8400                        line,
8401                    });
8402                }
8403                if matches!(self.peek(), Token::Ident(_)) {
8404                    let name = self.parse_qualified_subroutine_name()?;
8405                    return Ok(Expr {
8406                        kind: ExprKind::SubroutineRef(name),
8407                        line,
8408                    });
8409                }
8410                let target = self.parse_primary()?;
8411                if matches!(self.peek(), Token::LParen) {
8412                    self.advance();
8413                    let args = self.parse_arg_list()?;
8414                    self.expect(&Token::RParen)?;
8415                    return Ok(Expr {
8416                        kind: ExprKind::IndirectCall {
8417                            target: Box::new(target),
8418                            args,
8419                            ampersand: true,
8420                            pass_caller_arglist: false,
8421                        },
8422                        line,
8423                    });
8424                }
8425                // `&$coderef` / `&{expr}` with no `(...)` — call with caller's @_ (Perl `&$sub`).
8426                Ok(Expr {
8427                    kind: ExprKind::IndirectCall {
8428                        target: Box::new(target),
8429                        args: vec![],
8430                        ampersand: true,
8431                        pass_caller_arglist: true,
8432                    },
8433                    line,
8434                })
8435            }
8436            Token::Backslash => {
8437                self.advance();
8438                let expr = self.parse_unary()?;
8439                if let ExprKind::SubroutineRef(name) = expr.kind {
8440                    return Ok(Expr {
8441                        kind: ExprKind::SubroutineCodeRef(name),
8442                        line,
8443                    });
8444                }
8445                if matches!(expr.kind, ExprKind::DynamicSubCodeRef(_)) {
8446                    return Ok(expr);
8447                }
8448                // `\` uses `ScalarRef`; array/hash vars and `\@{...}` lower to binding or alias refs.
8449                Ok(Expr {
8450                    kind: ExprKind::ScalarRef(Box::new(expr)),
8451                    line,
8452                })
8453            }
8454            Token::FileTest(op) => {
8455                self.advance();
8456                // Perl: `-d` with no operand uses `$_` (e.g. `if (-d)` inside `for` / `while read`).
8457                let expr = if Self::filetest_allows_implicit_topic(self.peek()) {
8458                    Expr {
8459                        kind: ExprKind::ScalarVar("_".into()),
8460                        line: self.peek_line(),
8461                    }
8462                } else {
8463                    self.parse_unary()?
8464                };
8465                Ok(Expr {
8466                    kind: ExprKind::FileTest {
8467                        op,
8468                        expr: Box::new(expr),
8469                    },
8470                    line,
8471                })
8472            }
8473            _ => self.parse_power(),
8474        }
8475    }
8476
8477    fn parse_power(&mut self) -> StrykeResult<Expr> {
8478        let left = self.parse_postfix()?;
8479        if matches!(self.peek(), Token::Power) {
8480            let line = left.line;
8481            self.advance();
8482            let right = self.parse_unary()?; // right-associative
8483            return Ok(Expr {
8484                kind: ExprKind::BinOp {
8485                    left: Box::new(left),
8486                    op: BinOp::Pow,
8487                    right: Box::new(right),
8488                },
8489                line,
8490            });
8491        }
8492        Ok(left)
8493    }
8494
8495    fn parse_postfix(&mut self) -> StrykeResult<Expr> {
8496        let mut expr = self.parse_primary()?;
8497        loop {
8498            match self.peek().clone() {
8499                Token::Increment => {
8500                    // Implicit semicolon: `++` on a new line is a prefix operator
8501                    // on the next statement, not postfix on the previous expression.
8502                    if self.peek_line() > self.prev_line() {
8503                        break;
8504                    }
8505                    let line = expr.line;
8506                    self.advance();
8507                    expr = Expr {
8508                        kind: ExprKind::PostfixOp {
8509                            expr: Box::new(expr),
8510                            op: PostfixOp::Increment,
8511                        },
8512                        line,
8513                    };
8514                }
8515                Token::Decrement => {
8516                    // Implicit semicolon: `--` on a new line is a prefix operator
8517                    // on the next statement, not postfix on the previous expression.
8518                    if self.peek_line() > self.prev_line() {
8519                        break;
8520                    }
8521                    let line = expr.line;
8522                    self.advance();
8523                    expr = Expr {
8524                        kind: ExprKind::PostfixOp {
8525                            expr: Box::new(expr),
8526                            op: PostfixOp::Decrement,
8527                        },
8528                        line,
8529                    };
8530                }
8531                Token::LParen => {
8532                    if self.suppress_indirect_paren_call > 0 {
8533                        break;
8534                    }
8535                    // Implicit semicolon: `(` on a new line after an expression
8536                    // is a new statement, not a postfix code-ref call.
8537                    // e.g.  `my $x = $ENV{"KEY"}\n($y =~ s/.../.../)`
8538                    if self.peek_line() > self.prev_line() {
8539                        break;
8540                    }
8541                    let line = expr.line;
8542                    self.advance();
8543                    let args = self.parse_arg_list()?;
8544                    self.expect(&Token::RParen)?;
8545                    expr = Expr {
8546                        kind: ExprKind::IndirectCall {
8547                            target: Box::new(expr),
8548                            args,
8549                            ampersand: false,
8550                            pass_caller_arglist: false,
8551                        },
8552                        line,
8553                    };
8554                }
8555                Token::Arrow => {
8556                    let line = expr.line;
8557                    self.advance();
8558                    match self.peek().clone() {
8559                        Token::LBracket => {
8560                            self.advance();
8561                            let index = self.parse_expression()?;
8562                            self.expect(&Token::RBracket)?;
8563                            expr = Expr {
8564                                kind: ExprKind::ArrowDeref {
8565                                    expr: Box::new(expr),
8566                                    index: Box::new(index),
8567                                    kind: DerefKind::Array,
8568                                },
8569                                line,
8570                            };
8571                        }
8572                        Token::LBrace => {
8573                            self.advance();
8574                            let key = self.parse_hash_subscript_key()?;
8575                            self.expect(&Token::RBrace)?;
8576                            expr = Expr {
8577                                kind: ExprKind::ArrowDeref {
8578                                    expr: Box::new(expr),
8579                                    index: Box::new(key),
8580                                    kind: DerefKind::Hash,
8581                                },
8582                                line,
8583                            };
8584                        }
8585                        Token::LParen => {
8586                            self.advance();
8587                            let args = self.parse_arg_list()?;
8588                            self.expect(&Token::RParen)?;
8589                            expr = Expr {
8590                                kind: ExprKind::ArrowDeref {
8591                                    expr: Box::new(expr),
8592                                    index: Box::new(Expr {
8593                                        kind: ExprKind::List(args),
8594                                        line,
8595                                    }),
8596                                    kind: DerefKind::Call,
8597                                },
8598                                line,
8599                            };
8600                        }
8601                        Token::Ident(method) => {
8602                            self.advance();
8603                            if method == "SUPER" {
8604                                self.expect(&Token::PackageSep)?;
8605                                let real_method = match self.advance() {
8606                                    (Token::Ident(n), _) => n,
8607                                    (tok, l) => {
8608                                        return Err(self.syntax_err(
8609                                            format!(
8610                                                "Expected method name after SUPER::, got {:?}",
8611                                                tok
8612                                            ),
8613                                            l,
8614                                        ));
8615                                    }
8616                                };
8617                                let args = if self.eat(&Token::LParen) {
8618                                    let a = self.parse_arg_list()?;
8619                                    self.expect(&Token::RParen)?;
8620                                    a
8621                                } else {
8622                                    self.parse_method_arg_list_no_paren()?
8623                                };
8624                                expr = Expr {
8625                                    kind: ExprKind::MethodCall {
8626                                        object: Box::new(expr),
8627                                        method: real_method,
8628                                        args,
8629                                        super_call: true,
8630                                    },
8631                                    line,
8632                                };
8633                            } else {
8634                                let mut method_name = method;
8635                                while self.eat(&Token::PackageSep) {
8636                                    match self.advance() {
8637                                        (Token::Ident(part), _) => {
8638                                            method_name.push_str("::");
8639                                            method_name.push_str(&part);
8640                                        }
8641                                        (tok, l) => {
8642                                            return Err(self.syntax_err(
8643                                                format!(
8644                                                    "Expected identifier after :: in method name, got {:?}",
8645                                                    tok
8646                                                ),
8647                                                l,
8648                                            ));
8649                                        }
8650                                    }
8651                                }
8652                                let args = if self.eat(&Token::LParen) {
8653                                    let a = self.parse_arg_list()?;
8654                                    self.expect(&Token::RParen)?;
8655                                    a
8656                                } else {
8657                                    self.parse_method_arg_list_no_paren()?
8658                                };
8659                                expr = Expr {
8660                                    kind: ExprKind::MethodCall {
8661                                        object: Box::new(expr),
8662                                        method: method_name,
8663                                        args,
8664                                        super_call: false,
8665                                    },
8666                                    line,
8667                                };
8668                            }
8669                        }
8670                        // Postfix dereference (Perl 5.20+, default 5.24+):
8671                        //   `$ref->@*`         — full array      ≡ `@{$ref}`
8672                        //   `$ref->@[i,j]`     — array slice     ≡ `@{$ref}[i,j]`
8673                        //   `$ref->@{k,l}`     — hash slice (vals) ≡ `@{$ref}{k,l}`
8674                        //   `$ref->%*`         — full hash       ≡ `%{$ref}`
8675                        Token::ArrayAt => {
8676                            self.advance(); // consume `@`
8677                            match self.peek().clone() {
8678                                Token::Star => {
8679                                    self.advance();
8680                                    expr = Expr {
8681                                        kind: ExprKind::Deref {
8682                                            expr: Box::new(expr),
8683                                            kind: Sigil::Array,
8684                                        },
8685                                        line,
8686                                    };
8687                                }
8688                                Token::LBracket => {
8689                                    self.advance();
8690                                    let indices = self.parse_slice_arg_list(false)?;
8691                                    self.expect(&Token::RBracket)?;
8692                                    let source = Expr {
8693                                        kind: ExprKind::Deref {
8694                                            expr: Box::new(expr),
8695                                            kind: Sigil::Array,
8696                                        },
8697                                        line,
8698                                    };
8699                                    expr = Expr {
8700                                        kind: ExprKind::AnonymousListSlice {
8701                                            source: Box::new(source),
8702                                            indices,
8703                                        },
8704                                        line,
8705                                    };
8706                                }
8707                                Token::LBrace => {
8708                                    self.advance();
8709                                    let keys = self.parse_slice_arg_list(true)?;
8710                                    self.expect(&Token::RBrace)?;
8711                                    expr = Expr {
8712                                        kind: ExprKind::HashSliceDeref {
8713                                            container: Box::new(expr),
8714                                            keys,
8715                                        },
8716                                        line,
8717                                    };
8718                                }
8719                                tok => {
8720                                    return Err(self.syntax_err(
8721                                        format!(
8722                                            "Expected `*`, `[…]`, or `{{…}}` after `->@`, got {:?}",
8723                                            tok
8724                                        ),
8725                                        line,
8726                                    ));
8727                                }
8728                            }
8729                        }
8730                        Token::HashPercent => {
8731                            self.advance(); // consume `%`
8732                            match self.peek().clone() {
8733                                Token::Star => {
8734                                    self.advance();
8735                                    expr = Expr {
8736                                        kind: ExprKind::Deref {
8737                                            expr: Box::new(expr),
8738                                            kind: Sigil::Hash,
8739                                        },
8740                                        line,
8741                                    };
8742                                }
8743                                tok => {
8744                                    return Err(self.syntax_err(
8745                                        format!("Expected `*` after `->%`, got {:?}", tok),
8746                                        line,
8747                                    ));
8748                                }
8749                            }
8750                        }
8751                        // `x` is lexed as `Token::X` (repeat op); after `->` it is a method name.
8752                        Token::X => {
8753                            self.advance();
8754                            let args = if self.eat(&Token::LParen) {
8755                                let a = self.parse_arg_list()?;
8756                                self.expect(&Token::RParen)?;
8757                                a
8758                            } else {
8759                                self.parse_method_arg_list_no_paren()?
8760                            };
8761                            expr = Expr {
8762                                kind: ExprKind::MethodCall {
8763                                    object: Box::new(expr),
8764                                    method: "x".to_string(),
8765                                    args,
8766                                    super_call: false,
8767                                },
8768                                line,
8769                            };
8770                        }
8771                        _ => break,
8772                    }
8773                }
8774                Token::LBracket => {
8775                    // Implicit semicolon: `[` on a new line is a new statement (array literal),
8776                    // not an array subscript on the preceding expression.
8777                    if self.peek_line() > self.prev_line() {
8778                        break;
8779                    }
8780                    // `$a[i]` — or chained `$r->{k}[i]` / `$a[1][2]` — or list slice `(sort ...)[0]`.
8781                    let line = expr.line;
8782                    if matches!(expr.kind, ExprKind::ScalarVar(_)) {
8783                        if let ExprKind::ScalarVar(ref name) = expr.kind {
8784                            let name = name.clone();
8785                            self.advance();
8786                            // Parse full expression to handle comma operator correctly:
8787                            // `$a[1, 2]` evaluates comma expr (returns last value = 2)
8788                            let index = self.parse_expression()?;
8789                            self.expect(&Token::RBracket)?;
8790                            expr = Expr {
8791                                kind: ExprKind::ArrayElement {
8792                                    array: name,
8793                                    index: Box::new(index),
8794                                },
8795                                line,
8796                            };
8797                        }
8798                    } else if postfix_lbracket_is_arrow_container(&expr) {
8799                        self.advance();
8800                        let indices = self.parse_arg_list()?;
8801                        self.expect(&Token::RBracket)?;
8802                        expr = Expr {
8803                            kind: ExprKind::ArrowDeref {
8804                                expr: Box::new(expr),
8805                                index: Box::new(Expr {
8806                                    kind: ExprKind::List(indices),
8807                                    line,
8808                                }),
8809                                kind: DerefKind::Array,
8810                            },
8811                            line,
8812                        };
8813                    } else {
8814                        self.advance();
8815                        let indices = self.parse_arg_list()?;
8816                        self.expect(&Token::RBracket)?;
8817                        expr = Expr {
8818                            kind: ExprKind::AnonymousListSlice {
8819                                source: Box::new(expr),
8820                                indices,
8821                            },
8822                            line,
8823                        };
8824                    }
8825                }
8826                Token::LBrace => {
8827                    if self.suppress_scalar_hash_brace > 0 {
8828                        break;
8829                    }
8830                    // Implicit semicolon: `{` on a new line is a new statement (block/hashref),
8831                    // not a hash subscript on the preceding expression.
8832                    if self.peek_line() > self.prev_line() {
8833                        break;
8834                    }
8835                    // `$h{k}`, or chained `$h{k2}{k3}` / `$r->{a}{b}` / `$a[0]{k}` — second+ `{…}` is
8836                    // hash subscript on the scalar value (same as `-> { … }` without extra `->`).
8837                    let line = expr.line;
8838                    let is_scalar_named_hash = matches!(expr.kind, ExprKind::ScalarVar(_));
8839                    let is_chainable_hash_subscript = is_scalar_named_hash
8840                        || matches!(
8841                            expr.kind,
8842                            ExprKind::HashElement { .. }
8843                                | ExprKind::ArrayElement { .. }
8844                                | ExprKind::ArrowDeref { .. }
8845                                | ExprKind::Deref {
8846                                    kind: Sigil::Scalar,
8847                                    ..
8848                                }
8849                        );
8850                    if !is_chainable_hash_subscript {
8851                        break;
8852                    }
8853                    self.advance();
8854                    let key = self.parse_hash_subscript_key()?;
8855                    self.expect(&Token::RBrace)?;
8856                    expr = if is_scalar_named_hash {
8857                        if let ExprKind::ScalarVar(ref name) = expr.kind {
8858                            let name = name.clone();
8859                            // Perl: `$_ { k }` means `$_->{k}` (implicit arrow), not the `%_` stash hash.
8860                            if name == "_" {
8861                                Expr {
8862                                    kind: ExprKind::ArrowDeref {
8863                                        expr: Box::new(Expr {
8864                                            kind: ExprKind::ScalarVar("_".into()),
8865                                            line,
8866                                        }),
8867                                        index: Box::new(key),
8868                                        kind: DerefKind::Hash,
8869                                    },
8870                                    line,
8871                                }
8872                            } else {
8873                                Expr {
8874                                    kind: ExprKind::HashElement {
8875                                        hash: name,
8876                                        key: Box::new(key),
8877                                    },
8878                                    line,
8879                                }
8880                            }
8881                        } else {
8882                            unreachable!("is_scalar_named_hash implies ScalarVar");
8883                        }
8884                    } else {
8885                        Expr {
8886                            kind: ExprKind::ArrowDeref {
8887                                expr: Box::new(expr),
8888                                index: Box::new(key),
8889                                kind: DerefKind::Hash,
8890                            },
8891                            line,
8892                        }
8893                    };
8894                }
8895                Token::LogNot | Token::BitNot => {
8896                    // Stryke universal string-subscript sugar — paired `!…!`
8897                    // OR paired `~…~`: `$VAR!N!`, `$VAR~N~`, `$VAR!1:5:2!`,
8898                    // `_!N!`, `_~from:to:step~`. Returns substring of the
8899                    // scalar (Unicode chars).  Distinct from `[N]` which has
8900                    // Perl's `@VAR[N]` / `$_[N]` semantics. Both forms work on
8901                    // any scalar (named or topic) without colliding: `!` and
8902                    // `~` after a value have no current postfix meaning (`!=`
8903                    // / `!~` are pre-merged binary tokens; `~` is prefix-only
8904                    // bit-not). The opening and closing delimiter must match.
8905                    //
8906                    // Implementation: rewrite to ArrayElement with a
8907                    // synthetic name `__topicstr__$NAME`. The interpreter
8908                    // and VM strip the prefix and dispatch to char-of-string
8909                    // (and slice-of-string for Range indices).
8910                    if !matches!(expr.kind, ExprKind::ScalarVar(_)) {
8911                        break;
8912                    }
8913                    if self.peek_line() > self.prev_line() {
8914                        break;
8915                    }
8916                    let opener = self.peek().clone();
8917                    let line = expr.line;
8918                    let name = if let ExprKind::ScalarVar(ref n) = expr.kind {
8919                        n.clone()
8920                    } else {
8921                        unreachable!()
8922                    };
8923                    self.advance(); // consume opening `!` or `~`
8924                                    // Suppress `~` as a range separator while parsing the
8925                                    // paired index — `$_~5~` would otherwise consume the
8926                                    // closing `~` as a range op. `:` is still allowed so
8927                                    // `$_~1:3~` (slice with `:` range index) keeps working.
8928                    self.suppress_tilde_range = self.suppress_tilde_range.saturating_add(1);
8929                    let index_result = self.parse_expression();
8930                    self.suppress_tilde_range = self.suppress_tilde_range.saturating_sub(1);
8931                    let index = index_result?;
8932                    let close_match = matches!(
8933                        (&opener, self.peek()),
8934                        (Token::LogNot, Token::LogNot) | (Token::BitNot, Token::BitNot)
8935                    );
8936                    if !close_match {
8937                        let want = if matches!(opener, Token::LogNot) {
8938                            "!"
8939                        } else {
8940                            "~"
8941                        };
8942                        return Err(self.syntax_err(
8943                            format!("expected closing `{}` for string subscript", want),
8944                            self.peek_line(),
8945                        ));
8946                    }
8947                    self.advance(); // consume closing delimiter
8948                    expr = Expr {
8949                        kind: ExprKind::ArrayElement {
8950                            array: format!("__topicstr__{}", name),
8951                            index: Box::new(index),
8952                        },
8953                        line,
8954                    };
8955                }
8956                _ => break,
8957            }
8958        }
8959        Ok(expr)
8960    }
8961
8962    fn parse_primary(&mut self) -> StrykeResult<Expr> {
8963        let line = self.peek_line();
8964        // `my $x = …` (or `our` / `state` / `local`) used inside an expression —
8965        // typically `if (my $x = …)` / `while (my $line = <FH>)`.  Returns the
8966        // assigned value(s); has the side effect of declaring the variable in
8967        // the current scope.  See `ExprKind::MyExpr`.
8968        if let Token::Ident(ref kw) = self.peek().clone() {
8969            if matches!(
8970                kw.as_str(),
8971                "my" | "var" | "val" | "our" | "state" | "local"
8972            ) {
8973                let raw_kw = kw.clone();
8974                // `var` / `val` are surface aliases; normalize to `my` for
8975                // the inner parser (same as the statement-form dispatch).
8976                // `val` requires marking the resulting decls frozen so the
8977                // expression form `if (val $x = …) { … }` matches its
8978                // statement-form counterpart `if (const my $x = …) { … }`.
8979                let kw_owned: String = match raw_kw.as_str() {
8980                    "var" | "val" => "my".to_string(),
8981                    _ => raw_kw.clone(),
8982                };
8983                let allow_type = matches!(raw_kw.as_str(), "val");
8984                let mark_frozen = matches!(raw_kw.as_str(), "val");
8985                // Parse exactly like the statement form via `parse_my_our_local`,
8986                // then unwrap the resulting `StmtKind::*` back into a list of
8987                // `VarDecl`s for the expression node.  This re-uses the full
8988                // syntax (typed sigs, list destructuring, type annotations).
8989                let saved_pos = self.pos;
8990                let stmt = self.parse_my_our_local(&kw_owned, allow_type)?;
8991                let mut decls = match stmt.kind {
8992                    StmtKind::My(d)
8993                    | StmtKind::Our(d)
8994                    | StmtKind::State(d)
8995                    | StmtKind::Local(d) => d,
8996                    _ => {
8997                        // `local *FOO = …` / non-decl forms — fall back to the
8998                        // statement parser (already advanced); restore position
8999                        // and let the surrounding code handle it as a statement
9000                        // by erroring loudly here.
9001                        self.pos = saved_pos;
9002                        return Err(self.syntax_err(
9003                            "`my`/`our`/`local` in expression must declare variables",
9004                            line,
9005                        ));
9006                    }
9007                };
9008                if mark_frozen {
9009                    for d in decls.iter_mut() {
9010                        d.frozen = true;
9011                    }
9012                }
9013                return Ok(Expr {
9014                    kind: ExprKind::MyExpr {
9015                        keyword: kw_owned,
9016                        decls,
9017                    },
9018                    line,
9019                });
9020            }
9021        }
9022        match self.peek().clone() {
9023            Token::Integer(n) => {
9024                self.advance();
9025                Ok(Expr {
9026                    kind: ExprKind::Integer(n),
9027                    line,
9028                })
9029            }
9030            Token::Float(f) => {
9031                self.advance();
9032                Ok(Expr {
9033                    kind: ExprKind::Float(f),
9034                    line,
9035                })
9036            }
9037            // `>{ BLOCK }` — IIFE block expression (immediately-invoked anonymous sub).
9038            // Valid in any expression position; evaluates the block and yields its last value.
9039            // In thread-macro stage position (`EXPR |>` already consumed by the stage loop in
9040            // `parse_thread_macro`), the explicit branch at ~1417 wins and the block is
9041            // instead pipe-applied as a coderef — that path is never reached from here.
9042            Token::ArrowBrace => {
9043                self.advance();
9044                let mut stmts = Vec::new();
9045                while !matches!(self.peek(), Token::RBrace | Token::Eof) {
9046                    if self.eat(&Token::Semicolon) {
9047                        continue;
9048                    }
9049                    stmts.push(self.parse_statement()?);
9050                }
9051                self.expect(&Token::RBrace)?;
9052                let inner_line = stmts.first().map(|s| s.line).unwrap_or(line);
9053                let inner = Expr {
9054                    kind: ExprKind::CodeRef {
9055                        params: vec![],
9056                        body: stmts,
9057                    },
9058                    line: inner_line,
9059                };
9060                Ok(Expr {
9061                    kind: ExprKind::Do(Box::new(inner)),
9062                    line,
9063                })
9064            }
9065            Token::Star => {
9066                self.advance();
9067                if matches!(self.peek(), Token::LBrace) {
9068                    self.advance();
9069                    let inner = self.parse_expression()?;
9070                    self.expect(&Token::RBrace)?;
9071                    return Ok(Expr {
9072                        kind: ExprKind::Deref {
9073                            expr: Box::new(inner),
9074                            kind: Sigil::Typeglob,
9075                        },
9076                        line,
9077                    });
9078                }
9079                // `*$_{$k}`, `*${expr}`, `*$foo` — typeglob from a sigil expression (Perl 5 `*$globref`).
9080                if matches!(
9081                    self.peek(),
9082                    Token::ScalarVar(_)
9083                        | Token::ArrayVar(_)
9084                        | Token::HashVar(_)
9085                        | Token::DerefScalarVar(_)
9086                        | Token::HashPercent
9087                ) {
9088                    let inner = self.parse_postfix()?;
9089                    return Ok(Expr {
9090                        kind: ExprKind::TypeglobExpr(Box::new(inner)),
9091                        line,
9092                    });
9093                }
9094                // `x` tokenizes as `Token::X` (repeat op) — still a valid package/typeglob name.
9095                let mut full_name = match self.advance() {
9096                    (Token::Ident(n), _) => n,
9097                    (Token::X, _) => "x".to_string(),
9098                    (tok, l) => {
9099                        return Err(self
9100                            .syntax_err(format!("Expected identifier after *, got {:?}", tok), l));
9101                    }
9102                };
9103                while self.eat(&Token::PackageSep) {
9104                    match self.advance() {
9105                        (Token::Ident(part), _) => {
9106                            full_name = format!("{}::{}", full_name, part);
9107                        }
9108                        (Token::X, _) => {
9109                            full_name = format!("{}::x", full_name);
9110                        }
9111                        (tok, l) => {
9112                            return Err(self.syntax_err(
9113                                format!("Expected identifier after :: in typeglob, got {:?}", tok),
9114                                l,
9115                            ));
9116                        }
9117                    }
9118                }
9119                Ok(Expr {
9120                    kind: ExprKind::Typeglob(full_name),
9121                    line,
9122                })
9123            }
9124            Token::SingleString(s) => {
9125                self.advance();
9126                Ok(Expr {
9127                    kind: ExprKind::String(s),
9128                    line,
9129                })
9130            }
9131            Token::DoubleString(s) => {
9132                self.advance();
9133                self.parse_interpolated_string(&s, line)
9134            }
9135            Token::BacktickString(s) => {
9136                self.advance();
9137                let inner = self.parse_interpolated_string(&s, line)?;
9138                Ok(Expr {
9139                    kind: ExprKind::Qx(Box::new(inner)),
9140                    line,
9141                })
9142            }
9143            Token::HereDoc(_, body, interpolate) => {
9144                self.advance();
9145                if interpolate {
9146                    self.parse_interpolated_string(&body, line)
9147                } else {
9148                    Ok(Expr {
9149                        kind: ExprKind::String(body),
9150                        line,
9151                    })
9152                }
9153            }
9154            Token::Regex(pattern, flags, _delim) => {
9155                self.advance();
9156                Ok(Expr {
9157                    kind: ExprKind::Regex(pattern, flags),
9158                    line,
9159                })
9160            }
9161            Token::QW(words) => {
9162                self.advance();
9163                // `qw(a b c) x N` is list-repeat in Perl even without explicit
9164                // outer parens — `qw(...)` is itself a list constructor.
9165                self.list_construct_close_pos = Some(self.pos);
9166                Ok(Expr {
9167                    kind: ExprKind::QW(words),
9168                    line,
9169                })
9170            }
9171            Token::DerefScalarVar(name) => {
9172                self.advance();
9173                Ok(Expr {
9174                    kind: ExprKind::Deref {
9175                        expr: Box::new(Expr {
9176                            kind: ExprKind::ScalarVar(name),
9177                            line,
9178                        }),
9179                        kind: Sigil::Scalar,
9180                    },
9181                    line,
9182                })
9183            }
9184            Token::ScalarVar(name) => {
9185                self.advance();
9186                Ok(Expr {
9187                    kind: ExprKind::ScalarVar(name),
9188                    line,
9189                })
9190            }
9191            Token::ArrayVar(name) => {
9192                self.advance();
9193                // Check for slice: @arr[...] (array slice) or @hash{...} (hash slice)
9194                match self.peek() {
9195                    Token::LBracket => {
9196                        self.advance();
9197                        let indices = self.parse_slice_arg_list(false)?;
9198                        self.expect(&Token::RBracket)?;
9199                        Ok(Expr {
9200                            kind: ExprKind::ArraySlice {
9201                                array: name,
9202                                indices,
9203                            },
9204                            line,
9205                        })
9206                    }
9207                    Token::LBrace if self.suppress_scalar_hash_brace == 0 => {
9208                        self.advance();
9209                        let keys = self.parse_slice_arg_list(true)?;
9210                        self.expect(&Token::RBrace)?;
9211                        Ok(Expr {
9212                            kind: ExprKind::HashSlice { hash: name, keys },
9213                            line,
9214                        })
9215                    }
9216                    _ => Ok(Expr {
9217                        kind: ExprKind::ArrayVar(name),
9218                        line,
9219                    }),
9220                }
9221            }
9222            Token::HashVar(name) => {
9223                self.advance();
9224                // `%h{KEYS}` — Perl 5.20+ key-value slice. Parser-level
9225                // disambiguation: `%h` immediately followed by `{` is a kv-
9226                // slice; `%h` alone (or followed by `=`, list ops, etc.) is
9227                // the bare hash. (BUG-008)
9228                if matches!(self.peek(), Token::LBrace) && self.suppress_scalar_hash_brace == 0 {
9229                    self.advance(); // {
9230                    let keys = self.parse_slice_arg_list(true)?;
9231                    self.expect(&Token::RBrace)?;
9232                    return Ok(Expr {
9233                        kind: ExprKind::HashKvSlice { hash: name, keys },
9234                        line,
9235                    });
9236                }
9237                Ok(Expr {
9238                    kind: ExprKind::HashVar(name),
9239                    line,
9240                })
9241            }
9242            Token::HashPercent => {
9243                // `%$href` — hash ref deref; `%{ $expr }` — symbolic / braced form
9244                self.advance();
9245                if matches!(self.peek(), Token::ScalarVar(_)) {
9246                    let n = match self.advance() {
9247                        (Token::ScalarVar(n), _) => n,
9248                        (tok, l) => {
9249                            return Err(self.syntax_err(
9250                                format!("Expected scalar variable after %%, got {:?}", tok),
9251                                l,
9252                            ));
9253                        }
9254                    };
9255                    return Ok(Expr {
9256                        kind: ExprKind::Deref {
9257                            expr: Box::new(Expr {
9258                                kind: ExprKind::ScalarVar(n),
9259                                line,
9260                            }),
9261                            kind: Sigil::Hash,
9262                        },
9263                        line,
9264                    });
9265                }
9266                // `%[a => 1, b => 2]` — sugar for `%{+{a=>1,b=>2}}`: dereference an
9267                // anonymous hashref inline, using `[...]` as the delimiter to avoid
9268                // the block-vs-hashref ambiguity that `%{a=>1}` has in real Perl.
9269                // Real Perl errors on `%[...]` syntactically, so no compat risk.
9270                if matches!(self.peek(), Token::LBracket) {
9271                    self.advance();
9272                    let pairs = self.parse_hashref_pairs_until(&Token::RBracket)?;
9273                    self.expect(&Token::RBracket)?;
9274                    let href = Expr {
9275                        kind: ExprKind::HashRef(pairs),
9276                        line,
9277                    };
9278                    return Ok(Expr {
9279                        kind: ExprKind::Deref {
9280                            expr: Box::new(href),
9281                            kind: Sigil::Hash,
9282                        },
9283                        line,
9284                    });
9285                }
9286                self.expect(&Token::LBrace)?;
9287                // Peek to disambiguate `%{ $ref }` (deref a hashref expression) from
9288                // `%{ k => v }` (inline hash literal). Real Perl's block-vs-hashref
9289                // heuristic is famously unreliable — when the first non-whitespace
9290                // token is an ident/string followed by `=>`, treat the whole thing
9291                // as a hashref literal to make `%{a=>1,b=>2}` work reliably.
9292                let looks_like_pair = matches!(
9293                    self.peek(),
9294                    Token::Ident(_) | Token::SingleString(_) | Token::DoubleString(_)
9295                ) && matches!(self.peek_at(1), Token::FatArrow);
9296                let inner = if looks_like_pair {
9297                    let pairs = self.parse_hashref_pairs_until(&Token::RBrace)?;
9298                    Expr {
9299                        kind: ExprKind::HashRef(pairs),
9300                        line,
9301                    }
9302                } else {
9303                    self.parse_expression()?
9304                };
9305                self.expect(&Token::RBrace)?;
9306                Ok(Expr {
9307                    kind: ExprKind::Deref {
9308                        expr: Box::new(inner),
9309                        kind: Sigil::Hash,
9310                    },
9311                    line,
9312                })
9313            }
9314            Token::ArrayAt => {
9315                self.advance();
9316                // `@{ $expr }` / `@{ "Pkg::NAME" }` — symbolic array (e.g. `@{"$pkg\::EXPORT"}` in Exporter.pm)
9317                if matches!(self.peek(), Token::LBrace) {
9318                    self.advance();
9319                    let inner = self.parse_expression()?;
9320                    self.expect(&Token::RBrace)?;
9321                    // `@{$href}{k1,k2}` — hash slice through a hashref using
9322                    // the curly-brace deref form. Mirrors the `@$href{KEYS}`
9323                    // path (BUG-091/BUG-217). Likewise `@{$aref}[i,j]` is the
9324                    // array-slice-through-arrayref form.
9325                    if matches!(self.peek(), Token::LBrace) {
9326                        self.advance();
9327                        let keys = self.parse_slice_arg_list(true)?;
9328                        self.expect(&Token::RBrace)?;
9329                        return Ok(Expr {
9330                            kind: ExprKind::HashSliceDeref {
9331                                container: Box::new(inner),
9332                                keys,
9333                            },
9334                            line,
9335                        });
9336                    }
9337                    if matches!(self.peek(), Token::LBracket) {
9338                        self.advance();
9339                        let indices = self.parse_slice_arg_list(false)?;
9340                        self.expect(&Token::RBracket)?;
9341                        let source = Expr {
9342                            kind: ExprKind::Deref {
9343                                expr: Box::new(inner),
9344                                kind: Sigil::Array,
9345                            },
9346                            line,
9347                        };
9348                        return Ok(Expr {
9349                            kind: ExprKind::AnonymousListSlice {
9350                                source: Box::new(source),
9351                                indices,
9352                            },
9353                            line,
9354                        });
9355                    }
9356                    return Ok(Expr {
9357                        kind: ExprKind::Deref {
9358                            expr: Box::new(inner),
9359                            kind: Sigil::Array,
9360                        },
9361                        line,
9362                    });
9363                }
9364                // `@[a, b, c]` — sugar for `@{[a, b, c]}`: dereference an
9365                // anonymous arrayref inline. Real Perl rejects `@[...]` at
9366                // the parser level, so this extension has no compat risk.
9367                if matches!(self.peek(), Token::LBracket) {
9368                    self.advance();
9369                    let mut elems = Vec::new();
9370                    if !matches!(self.peek(), Token::RBracket) {
9371                        elems.push(self.parse_assign_expr()?);
9372                        while self.eat(&Token::Comma) {
9373                            if matches!(self.peek(), Token::RBracket) {
9374                                break;
9375                            }
9376                            elems.push(self.parse_assign_expr()?);
9377                        }
9378                    }
9379                    self.expect(&Token::RBracket)?;
9380                    let aref = Expr {
9381                        kind: ExprKind::ArrayRef(elems),
9382                        line,
9383                    };
9384                    return Ok(Expr {
9385                        kind: ExprKind::Deref {
9386                            expr: Box::new(aref),
9387                            kind: Sigil::Array,
9388                        },
9389                        line,
9390                    });
9391                }
9392                // `@$arr` — array dereference; `@$h{k1,k2}` — hash slice via hashref
9393                let container = match self.peek().clone() {
9394                    Token::ScalarVar(n) => {
9395                        self.advance();
9396                        Expr {
9397                            kind: ExprKind::ScalarVar(n),
9398                            line,
9399                        }
9400                    }
9401                    _ => {
9402                        return Err(self.syntax_err(
9403                            "Expected `$name`, `{`, or `[` after `@` (e.g. `@$aref`, `@{expr}`, `@[1,2,3]`, or `@$href{keys}`)",
9404                            line,
9405                        ));
9406                    }
9407                };
9408                if matches!(self.peek(), Token::LBrace) {
9409                    self.advance();
9410                    let keys = self.parse_slice_arg_list(true)?;
9411                    self.expect(&Token::RBrace)?;
9412                    return Ok(Expr {
9413                        kind: ExprKind::HashSliceDeref {
9414                            container: Box::new(container),
9415                            keys,
9416                        },
9417                        line,
9418                    });
9419                }
9420                Ok(Expr {
9421                    kind: ExprKind::Deref {
9422                        expr: Box::new(container),
9423                        kind: Sigil::Array,
9424                    },
9425                    line,
9426                })
9427            }
9428            Token::LParen => {
9429                self.advance();
9430                if matches!(self.peek(), Token::RParen) {
9431                    self.advance();
9432                    // Empty `() x 3` is a no-op list repeat — record the close
9433                    // position so `Token::X` knows the LHS was a list literal.
9434                    self.list_construct_close_pos = Some(self.pos);
9435                    return Ok(Expr {
9436                        kind: ExprKind::List(vec![]),
9437                        line,
9438                    });
9439                }
9440                // Inside parens, pipe-forward is allowed even if we're in a
9441                // paren-less arg context. Save and restore no_pipe_forward_depth.
9442                let saved_no_pipe = self.no_pipe_forward_depth;
9443                self.no_pipe_forward_depth = 0;
9444                // Thread-macro `on` may set `suppress_indirect_paren_call` so
9445                // `on $c ()` does not steal `()`; inside explicit `(...)` use
9446                // normal postfix-`(` rules (`on ($factory())`).
9447                let saved_indirect = self.suppress_indirect_paren_call;
9448                self.suppress_indirect_paren_call = 0;
9449                let expr = self.parse_expression();
9450                self.no_pipe_forward_depth = saved_no_pipe;
9451                self.suppress_indirect_paren_call = saved_indirect;
9452                let expr = expr?;
9453                self.expect(&Token::RParen)?;
9454                // Mark this paren as a list-constructor for the `x` operator
9455                // (parse_multiplication compares `self.pos` at the X token to
9456                // this checkpoint). Function-call parens (`f(args)`) don't
9457                // reach this branch; they're parsed by the call machinery.
9458                self.list_construct_close_pos = Some(self.pos);
9459                Ok(expr)
9460            }
9461            Token::LBracket => {
9462                self.advance();
9463                let elems = self.parse_arg_list()?;
9464                self.expect(&Token::RBracket)?;
9465                Ok(Expr {
9466                    kind: ExprKind::ArrayRef(elems),
9467                    line,
9468                })
9469            }
9470            Token::LBrace => {
9471                // Could be hash ref or block — disambiguate
9472                self.advance();
9473                // Try to parse as hash ref: { key => val, ... }
9474                let saved = self.pos;
9475                match self.try_parse_hash_ref() {
9476                    Ok(pairs) => Ok(Expr {
9477                        kind: ExprKind::HashRef(pairs),
9478                        line,
9479                    }),
9480                    Err(_) => {
9481                        self.pos = saved;
9482                        // Parse as block, wrap in code ref
9483                        let mut stmts = Vec::new();
9484                        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
9485                            if self.eat(&Token::Semicolon) {
9486                                continue;
9487                            }
9488                            stmts.push(self.parse_statement()?);
9489                        }
9490                        self.expect(&Token::RBrace)?;
9491                        Ok(Expr {
9492                            kind: ExprKind::CodeRef {
9493                                params: vec![],
9494                                body: stmts,
9495                            },
9496                            line,
9497                        })
9498                    }
9499                }
9500            }
9501            Token::Diamond => {
9502                self.advance();
9503                Ok(Expr {
9504                    kind: ExprKind::ReadLine(None),
9505                    line,
9506                })
9507            }
9508            Token::ReadLine(handle) => {
9509                self.advance();
9510                Ok(Expr {
9511                    kind: ExprKind::ReadLine(Some(handle)),
9512                    line,
9513                })
9514            }
9515
9516            // Named functions / builtins
9517            Token::ThreadArrow => {
9518                self.advance();
9519                self.parse_thread_macro(line, false)
9520            }
9521            Token::ThreadArrowLast => {
9522                self.advance();
9523                self.parse_thread_macro(line, true)
9524            }
9525            Token::ThreadArrowStream => {
9526                self.advance();
9527                let mut stages = Vec::new();
9528                self.parse_thread_macro_inner(line, false, Some(&mut stages))
9529            }
9530            Token::ThreadArrowStreamLast => {
9531                self.advance();
9532                let mut stages = Vec::new();
9533                self.parse_thread_macro_inner(line, true, Some(&mut stages))
9534            }
9535            Token::ThreadArrowPar => {
9536                self.advance();
9537                self.parse_thread_macro_chunk_par(line, false)
9538            }
9539            Token::ThreadArrowParLast => {
9540                self.advance();
9541                self.parse_thread_macro_chunk_par(line, true)
9542            }
9543            Token::ThreadArrowDist => {
9544                self.advance();
9545                self.parse_thread_macro_dist(line, false)
9546            }
9547            Token::ThreadArrowDistLast => {
9548                self.advance();
9549                self.parse_thread_macro_dist(line, true)
9550            }
9551            Token::Ident(ref name) => {
9552                let name = name.clone();
9553                // Handle s///
9554                if name.starts_with('\x00') {
9555                    self.advance();
9556                    let parts: Vec<&str> = name.split('\x00').collect();
9557                    if parts.len() >= 4 && parts[1] == "s" {
9558                        let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
9559                        return Ok(Expr {
9560                            kind: ExprKind::Substitution {
9561                                expr: Box::new(Expr {
9562                                    kind: ExprKind::ScalarVar("_".into()),
9563                                    line,
9564                                }),
9565                                pattern: parts[2].to_string(),
9566                                replacement: parts[3].to_string(),
9567                                flags: parts.get(4).unwrap_or(&"").to_string(),
9568                                delim,
9569                            },
9570                            line,
9571                        });
9572                    }
9573                    if parts.len() >= 4 && parts[1] == "tr" {
9574                        let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
9575                        return Ok(Expr {
9576                            kind: ExprKind::Transliterate {
9577                                expr: Box::new(Expr {
9578                                    kind: ExprKind::ScalarVar("_".into()),
9579                                    line,
9580                                }),
9581                                from: parts[2].to_string(),
9582                                to: parts[3].to_string(),
9583                                flags: parts.get(4).unwrap_or(&"").to_string(),
9584                                delim,
9585                            },
9586                            line,
9587                        });
9588                    }
9589                    return Err(self.syntax_err("Unexpected encoded token", line));
9590                }
9591                self.parse_named_expr(name)
9592            }
9593
9594            // `%name` when lexer emitted `Token::Percent` (due to preceding term context)
9595            // instead of `Token::HashVar`. This happens after `t` (thread macro) etc.
9596            Token::Percent => {
9597                self.advance();
9598                match self.peek().clone() {
9599                    Token::Ident(name) => {
9600                        self.advance();
9601                        Ok(Expr {
9602                            kind: ExprKind::HashVar(name),
9603                            line,
9604                        })
9605                    }
9606                    Token::ScalarVar(n) => {
9607                        self.advance();
9608                        Ok(Expr {
9609                            kind: ExprKind::Deref {
9610                                expr: Box::new(Expr {
9611                                    kind: ExprKind::ScalarVar(n),
9612                                    line,
9613                                }),
9614                                kind: Sigil::Hash,
9615                            },
9616                            line,
9617                        })
9618                    }
9619                    Token::LBrace => {
9620                        self.advance();
9621                        let looks_like_pair = matches!(
9622                            self.peek(),
9623                            Token::Ident(_) | Token::SingleString(_) | Token::DoubleString(_)
9624                        ) && matches!(self.peek_at(1), Token::FatArrow);
9625                        let inner = if looks_like_pair {
9626                            let pairs = self.parse_hashref_pairs_until(&Token::RBrace)?;
9627                            Expr {
9628                                kind: ExprKind::HashRef(pairs),
9629                                line,
9630                            }
9631                        } else {
9632                            self.parse_expression()?
9633                        };
9634                        self.expect(&Token::RBrace)?;
9635                        Ok(Expr {
9636                            kind: ExprKind::Deref {
9637                                expr: Box::new(inner),
9638                                kind: Sigil::Hash,
9639                            },
9640                            line,
9641                        })
9642                    }
9643                    Token::LBracket => {
9644                        self.advance();
9645                        let pairs = self.parse_hashref_pairs_until(&Token::RBracket)?;
9646                        self.expect(&Token::RBracket)?;
9647                        let href = Expr {
9648                            kind: ExprKind::HashRef(pairs),
9649                            line,
9650                        };
9651                        Ok(Expr {
9652                            kind: ExprKind::Deref {
9653                                expr: Box::new(href),
9654                                kind: Sigil::Hash,
9655                            },
9656                            line,
9657                        })
9658                    }
9659                    tok => Err(self.syntax_err(
9660                        format!(
9661                            "Expected identifier, `$`, `{{`, or `[` after `%`, got {:?}",
9662                            tok
9663                        ),
9664                        line,
9665                    )),
9666                }
9667            }
9668
9669            tok => Err(self.syntax_err(format!("Unexpected token {:?}", tok), line)),
9670        }
9671    }
9672
9673    fn parse_named_expr(&mut self, mut name: String) -> StrykeResult<Expr> {
9674        let line = self.peek_line();
9675        self.advance(); // consume the ident
9676        while self.eat(&Token::PackageSep) {
9677            match self.advance() {
9678                (Token::Ident(part), _) => {
9679                    name = format!("{}::{}", name, part);
9680                }
9681                (tok, err_line) => {
9682                    return Err(self.syntax_err(
9683                        format!("Expected identifier after `::`, got {:?}", tok),
9684                        err_line,
9685                    ));
9686                }
9687            }
9688        }
9689
9690        // Fat-arrow auto-quoting: ANY bareword (including keywords/builtins)
9691        // before `=>` is treated as a string key, matching Perl 5 semantics.
9692        // e.g. `(print => 1, pr => "x", sort => 3)` are all valid hash pairs.
9693        // Stryke exception: topic-slot barewords (`_`, `_<`, `_0`, `_0<`, …) are
9694        // scalar references to the topic / positional / outer-topic chain — they
9695        // must evaluate as the topic value, not the literal name.
9696        if matches!(self.peek(), Token::FatArrow) && !Self::is_underscore_topic_slot(&name) {
9697            return Ok(Expr {
9698                kind: ExprKind::String(name),
9699                line,
9700            });
9701        }
9702
9703        if crate::compat_mode() {
9704            if let Some(ext) = Self::stryke_extension_name(&name) {
9705                if !self.declared_subs.contains(&name) {
9706                    return Err(self.syntax_err(
9707                        format!("`{ext}` is a stryke extension (disabled by --compat)"),
9708                        line,
9709                    ));
9710                }
9711            }
9712        }
9713
9714        // `CORE::length(...)` etc. — strip the explicit core-dispatch prefix so
9715        // the keyword arms below match the bare name and produce the same
9716        // `ExprKind::Length` / `ExprKind::Print` / etc. as the unprefixed form.
9717        // Matches Perl 5's `CORE::` namespace, which routes back to the
9718        // built-in implementation regardless of any same-named user sub.
9719        // (PARITY-011)
9720        if let Some(rest) = name.strip_prefix("CORE::") {
9721            name = rest.to_string();
9722        }
9723
9724        match name.as_str() {
9725            "__FILE__" => Ok(Expr {
9726                kind: ExprKind::MagicConst(MagicConstKind::File),
9727                line,
9728            }),
9729            "__LINE__" => Ok(Expr {
9730                kind: ExprKind::MagicConst(MagicConstKind::Line),
9731                line,
9732            }),
9733            "__SUB__" => Ok(Expr {
9734                kind: ExprKind::MagicConst(MagicConstKind::Sub),
9735                line,
9736            }),
9737            // `__PACKAGE__` is a compile-time constant set to the currently
9738            // active package, so a sub body in `package Demo::P1` keeps
9739            // returning `"Demo::P1"` regardless of the caller's package
9740            // (Perl 5 documented behavior).
9741            "__PACKAGE__" => Ok(Expr {
9742                kind: ExprKind::String(self.current_package.clone()),
9743                line,
9744            }),
9745            "stdin" => Ok(Expr {
9746                kind: ExprKind::FuncCall {
9747                    name: "stdin".into(),
9748                    args: vec![],
9749                },
9750                line,
9751            }),
9752            "range" => {
9753                let args = self.parse_builtin_args()?;
9754                Ok(Expr {
9755                    kind: ExprKind::FuncCall {
9756                        name: "range".into(),
9757                        args,
9758                    },
9759                    line,
9760                })
9761            }
9762            "print" | "pr" => self.parse_print_like(|h, a| ExprKind::Print { handle: h, args: a }),
9763            "say" => {
9764                if crate::no_interop_mode() {
9765                    return Err(
9766                        self.syntax_err("stryke uses `p` instead of `say` (--no-interop)", line)
9767                    );
9768                }
9769                self.parse_print_like(|h, a| ExprKind::Say { handle: h, args: a })
9770            }
9771            "p" => self.parse_print_like(|h, a| ExprKind::Say { handle: h, args: a }),
9772            "printf" => self.parse_print_like(|h, a| ExprKind::Printf { handle: h, args: a }),
9773            "die" => {
9774                let args = self.parse_list_until_terminator()?;
9775                Ok(Expr {
9776                    kind: ExprKind::Die(args),
9777                    line,
9778                })
9779            }
9780            "warn" => {
9781                let args = self.parse_list_until_terminator()?;
9782                Ok(Expr {
9783                    kind: ExprKind::Warn(args),
9784                    line,
9785                })
9786            }
9787            // `croak` / `confess` — `Carp` builtins available without `use Carp`
9788            // (matches the doc claim in `lsp.rs:1243`). For now both desugar to
9789            // `die` — TODO: croak should report caller's file/line, confess
9790            // should append a full stack trace.
9791            "croak" | "confess" => {
9792                let args = self.parse_list_until_terminator()?;
9793                Ok(Expr {
9794                    kind: ExprKind::Die(args),
9795                    line,
9796                })
9797            }
9798            // `carp` / `cluck` — `Carp` warning siblings of `croak`/`confess`.
9799            "carp" | "cluck" => {
9800                let args = self.parse_list_until_terminator()?;
9801                Ok(Expr {
9802                    kind: ExprKind::Warn(args),
9803                    line,
9804                })
9805            }
9806            "chomp" => {
9807                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9808                    return Ok(e);
9809                }
9810                let a = self.parse_one_arg_or_default()?;
9811                Ok(Expr {
9812                    kind: ExprKind::Chomp(Box::new(a)),
9813                    line,
9814                })
9815            }
9816            "chop" => {
9817                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9818                    return Ok(e);
9819                }
9820                let a = self.parse_one_arg_or_default()?;
9821                Ok(Expr {
9822                    kind: ExprKind::Chop(Box::new(a)),
9823                    line,
9824                })
9825            }
9826            "length" => {
9827                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9828                    return Ok(e);
9829                }
9830                let a = self.parse_one_arg_or_default()?;
9831                Ok(Expr {
9832                    kind: ExprKind::Length(Box::new(a)),
9833                    line,
9834                })
9835            }
9836            "defined" => {
9837                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9838                    return Ok(e);
9839                }
9840                // Named-unary precedence: `defined X && Y` is `(defined X) && Y`,
9841                // not `defined(X && Y)`. The default `parse_one_arg_or_default`
9842                // path is greedy (calls `parse_assign_expr_stop_at_pipe`), which
9843                // would let `&&` bind into the argument and silently make
9844                // `defined $h{k} && $h{k} > 0`-style guards always-true when the
9845                // hash element existed. `parse_named_unary_arg` stops at shift
9846                // level so logical operators stay outside.
9847                let a = if matches!(
9848                    self.peek(),
9849                    Token::Semicolon
9850                        | Token::RBrace
9851                        | Token::RParen
9852                        | Token::RBracket
9853                        | Token::Eof
9854                        | Token::Comma
9855                        | Token::FatArrow
9856                        | Token::PipeForward
9857                        | Token::Question
9858                        | Token::Colon
9859                        | Token::NumEq
9860                        | Token::NumNe
9861                        | Token::NumLt
9862                        | Token::NumGt
9863                        | Token::NumLe
9864                        | Token::NumGe
9865                        | Token::Spaceship
9866                        | Token::StrEq
9867                        | Token::StrNe
9868                        | Token::StrLt
9869                        | Token::StrGt
9870                        | Token::StrLe
9871                        | Token::StrGe
9872                        | Token::StrCmp
9873                        | Token::LogAnd
9874                        | Token::LogOr
9875                        | Token::LogNot
9876                        | Token::LogAndWord
9877                        | Token::LogOrWord
9878                        | Token::LogNotWord
9879                        | Token::DefinedOr
9880                        | Token::Range
9881                        | Token::RangeExclusive
9882                        | Token::Assign
9883                        | Token::PlusAssign
9884                        | Token::MinusAssign
9885                        | Token::MulAssign
9886                        | Token::DivAssign
9887                        | Token::ModAssign
9888                        | Token::PowAssign
9889                        | Token::DotAssign
9890                        | Token::AndAssign
9891                        | Token::OrAssign
9892                        | Token::XorAssign
9893                        | Token::DefinedOrAssign
9894                        | Token::ShiftLeftAssign
9895                        | Token::ShiftRightAssign
9896                        | Token::BitAndAssign
9897                        | Token::BitOrAssign
9898                ) {
9899                    Expr {
9900                        kind: ExprKind::ScalarVar("_".into()),
9901                        line: self.peek_line(),
9902                    }
9903                } else if matches!(self.peek(), Token::LParen)
9904                    && matches!(self.peek_at(1), Token::RParen)
9905                {
9906                    let pl = self.peek_line();
9907                    self.advance();
9908                    self.advance();
9909                    Expr {
9910                        kind: ExprKind::ScalarVar("_".into()),
9911                        line: pl,
9912                    }
9913                } else {
9914                    self.parse_named_unary_arg()?
9915                };
9916                Ok(Expr {
9917                    kind: ExprKind::Defined(Box::new(a)),
9918                    line,
9919                })
9920            }
9921            "ref" => {
9922                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9923                    return Ok(e);
9924                }
9925                let a = self.parse_one_arg_or_default()?;
9926                Ok(Expr {
9927                    kind: ExprKind::Ref(Box::new(a)),
9928                    line,
9929                })
9930            }
9931            "undef" => {
9932                // `undef $var` sets `$var` to undef — but a variable on a new line
9933                // is a separate statement (implicit semicolon), not an argument.
9934                if self.peek_line() == self.prev_line()
9935                    && matches!(
9936                        self.peek(),
9937                        Token::ScalarVar(_) | Token::ArrayVar(_) | Token::HashVar(_)
9938                    )
9939                {
9940                    let target = self.parse_primary()?;
9941                    return Ok(Expr {
9942                        kind: ExprKind::Assign {
9943                            target: Box::new(target),
9944                            value: Box::new(Expr {
9945                                kind: ExprKind::Undef,
9946                                line,
9947                            }),
9948                        },
9949                        line,
9950                    });
9951                }
9952                Ok(Expr {
9953                    kind: ExprKind::Undef,
9954                    line,
9955                })
9956            }
9957            "scalar" => {
9958                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9959                    return Ok(e);
9960                }
9961                if crate::no_interop_mode() {
9962                    return Err(self.syntax_err(
9963                        "stryke uses `len` (also `cnt` / `count`) instead of `scalar` (--no-interop)",
9964                        line,
9965                    ));
9966                }
9967                let a = self.parse_one_arg_or_default()?;
9968                Ok(Expr {
9969                    kind: ExprKind::ScalarContext(Box::new(a)),
9970                    line,
9971                })
9972            }
9973            "abs" => {
9974                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9975                    return Ok(e);
9976                }
9977                let a = self.parse_one_arg_or_default()?;
9978                Ok(Expr {
9979                    kind: ExprKind::Abs(Box::new(a)),
9980                    line,
9981                })
9982            }
9983            // stryke unary numeric extensions — treat like `abs` so a bare
9984            // identifier in `map { inc }` / `for (…) { p inc }` becomes a
9985            // call with implicit `$_` rather than falling through to the
9986            // generic `Bareword` arm (which stringifies to `"inc"`).
9987            "inc" | "dec" => {
9988                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9989                    return Ok(e);
9990                }
9991                let a = self.parse_one_arg_or_default()?;
9992                Ok(Expr {
9993                    kind: ExprKind::FuncCall {
9994                        name,
9995                        args: vec![a],
9996                    },
9997                    line,
9998                })
9999            }
10000            "int" => {
10001                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10002                    return Ok(e);
10003                }
10004                let a = self.parse_one_arg_or_default()?;
10005                Ok(Expr {
10006                    kind: ExprKind::Int(Box::new(a)),
10007                    line,
10008                })
10009            }
10010            "sqrt" => {
10011                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10012                    return Ok(e);
10013                }
10014                let a = self.parse_one_arg_or_default()?;
10015                Ok(Expr {
10016                    kind: ExprKind::Sqrt(Box::new(a)),
10017                    line,
10018                })
10019            }
10020            "sin" => {
10021                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10022                    return Ok(e);
10023                }
10024                let a = self.parse_one_arg_or_default()?;
10025                Ok(Expr {
10026                    kind: ExprKind::Sin(Box::new(a)),
10027                    line,
10028                })
10029            }
10030            "cos" => {
10031                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10032                    return Ok(e);
10033                }
10034                let a = self.parse_one_arg_or_default()?;
10035                Ok(Expr {
10036                    kind: ExprKind::Cos(Box::new(a)),
10037                    line,
10038                })
10039            }
10040            "atan2" => {
10041                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10042                    return Ok(e);
10043                }
10044                let args = self.parse_builtin_args()?;
10045                if args.len() != 2 {
10046                    return Err(self.syntax_err("atan2 requires two arguments", line));
10047                }
10048                Ok(Expr {
10049                    kind: ExprKind::Atan2 {
10050                        y: Box::new(args[0].clone()),
10051                        x: Box::new(args[1].clone()),
10052                    },
10053                    line,
10054                })
10055            }
10056            "exp" => {
10057                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10058                    return Ok(e);
10059                }
10060                let a = self.parse_one_arg_or_default()?;
10061                Ok(Expr {
10062                    kind: ExprKind::Exp(Box::new(a)),
10063                    line,
10064                })
10065            }
10066            "log" => {
10067                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10068                    return Ok(e);
10069                }
10070                let a = self.parse_one_arg_or_default()?;
10071                Ok(Expr {
10072                    kind: ExprKind::Log(Box::new(a)),
10073                    line,
10074                })
10075            }
10076            "input" => {
10077                let args = if matches!(
10078                    self.peek(),
10079                    Token::Semicolon
10080                        | Token::RBrace
10081                        | Token::RParen
10082                        | Token::Eof
10083                        | Token::Comma
10084                        | Token::PipeForward
10085                ) {
10086                    vec![]
10087                } else if matches!(self.peek(), Token::LParen) {
10088                    self.advance();
10089                    if matches!(self.peek(), Token::RParen) {
10090                        self.advance();
10091                        vec![]
10092                    } else {
10093                        let a = self.parse_expression()?;
10094                        self.expect(&Token::RParen)?;
10095                        vec![a]
10096                    }
10097                } else {
10098                    let a = self.parse_one_arg()?;
10099                    vec![a]
10100                };
10101                Ok(Expr {
10102                    kind: ExprKind::FuncCall {
10103                        name: "input".to_string(),
10104                        args,
10105                    },
10106                    line,
10107                })
10108            }
10109            "rand" => {
10110                if matches!(
10111                    self.peek(),
10112                    Token::Semicolon
10113                        | Token::RBrace
10114                        | Token::RParen
10115                        | Token::Eof
10116                        | Token::Comma
10117                        | Token::PipeForward
10118                ) {
10119                    Ok(Expr {
10120                        kind: ExprKind::Rand(None),
10121                        line,
10122                    })
10123                } else if matches!(self.peek(), Token::LParen) {
10124                    self.advance();
10125                    if matches!(self.peek(), Token::RParen) {
10126                        self.advance();
10127                        Ok(Expr {
10128                            kind: ExprKind::Rand(None),
10129                            line,
10130                        })
10131                    } else {
10132                        let a = self.parse_expression()?;
10133                        self.expect(&Token::RParen)?;
10134                        Ok(Expr {
10135                            kind: ExprKind::Rand(Some(Box::new(a))),
10136                            line,
10137                        })
10138                    }
10139                } else {
10140                    let a = self.parse_one_arg()?;
10141                    Ok(Expr {
10142                        kind: ExprKind::Rand(Some(Box::new(a))),
10143                        line,
10144                    })
10145                }
10146            }
10147            "srand" => {
10148                if matches!(
10149                    self.peek(),
10150                    Token::Semicolon
10151                        | Token::RBrace
10152                        | Token::RParen
10153                        | Token::Eof
10154                        | Token::Comma
10155                        | Token::PipeForward
10156                ) {
10157                    Ok(Expr {
10158                        kind: ExprKind::Srand(None),
10159                        line,
10160                    })
10161                } else if matches!(self.peek(), Token::LParen) {
10162                    self.advance();
10163                    if matches!(self.peek(), Token::RParen) {
10164                        self.advance();
10165                        Ok(Expr {
10166                            kind: ExprKind::Srand(None),
10167                            line,
10168                        })
10169                    } else {
10170                        let a = self.parse_expression()?;
10171                        self.expect(&Token::RParen)?;
10172                        Ok(Expr {
10173                            kind: ExprKind::Srand(Some(Box::new(a))),
10174                            line,
10175                        })
10176                    }
10177                } else {
10178                    let a = self.parse_one_arg()?;
10179                    Ok(Expr {
10180                        kind: ExprKind::Srand(Some(Box::new(a))),
10181                        line,
10182                    })
10183                }
10184            }
10185            "hex" => {
10186                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10187                    return Ok(e);
10188                }
10189                let a = self.parse_one_arg_or_default()?;
10190                Ok(Expr {
10191                    kind: ExprKind::Hex(Box::new(a)),
10192                    line,
10193                })
10194            }
10195            "oct" => {
10196                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10197                    return Ok(e);
10198                }
10199                let a = self.parse_one_arg_or_default()?;
10200                Ok(Expr {
10201                    kind: ExprKind::Oct(Box::new(a)),
10202                    line,
10203                })
10204            }
10205            "chr" => {
10206                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10207                    return Ok(e);
10208                }
10209                let a = self.parse_one_arg_or_default()?;
10210                Ok(Expr {
10211                    kind: ExprKind::Chr(Box::new(a)),
10212                    line,
10213                })
10214            }
10215            "ord" => {
10216                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10217                    return Ok(e);
10218                }
10219                let a = self.parse_one_arg_or_default()?;
10220                Ok(Expr {
10221                    kind: ExprKind::Ord(Box::new(a)),
10222                    line,
10223                })
10224            }
10225            "lc" => {
10226                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10227                    return Ok(e);
10228                }
10229                let a = self.parse_one_arg_or_default()?;
10230                Ok(Expr {
10231                    kind: ExprKind::Lc(Box::new(a)),
10232                    line,
10233                })
10234            }
10235            "uc" => {
10236                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10237                    return Ok(e);
10238                }
10239                let a = self.parse_one_arg_or_default()?;
10240                Ok(Expr {
10241                    kind: ExprKind::Uc(Box::new(a)),
10242                    line,
10243                })
10244            }
10245            "lcfirst" => {
10246                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10247                    return Ok(e);
10248                }
10249                let a = self.parse_one_arg_or_default()?;
10250                Ok(Expr {
10251                    kind: ExprKind::Lcfirst(Box::new(a)),
10252                    line,
10253                })
10254            }
10255            "ucfirst" => {
10256                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10257                    return Ok(e);
10258                }
10259                let a = self.parse_one_arg_or_default()?;
10260                Ok(Expr {
10261                    kind: ExprKind::Ucfirst(Box::new(a)),
10262                    line,
10263                })
10264            }
10265            "fc" => {
10266                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10267                    return Ok(e);
10268                }
10269                let a = self.parse_one_arg_or_default()?;
10270                Ok(Expr {
10271                    kind: ExprKind::Fc(Box::new(a)),
10272                    line,
10273                })
10274            }
10275            "crypt" => {
10276                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10277                    return Ok(e);
10278                }
10279                let args = self.parse_builtin_args()?;
10280                if args.len() != 2 {
10281                    return Err(self.syntax_err("crypt requires two arguments", line));
10282                }
10283                Ok(Expr {
10284                    kind: ExprKind::Crypt {
10285                        plaintext: Box::new(args[0].clone()),
10286                        salt: Box::new(args[1].clone()),
10287                    },
10288                    line,
10289                })
10290            }
10291            "pos" => {
10292                if matches!(
10293                    self.peek(),
10294                    Token::Semicolon
10295                        | Token::RBrace
10296                        | Token::RParen
10297                        | Token::Eof
10298                        | Token::Comma
10299                        | Token::PipeForward
10300                ) {
10301                    Ok(Expr {
10302                        kind: ExprKind::Pos(None),
10303                        line,
10304                    })
10305                } else if matches!(self.peek(), Token::Assign) {
10306                    // Perl: `pos = EXPR` is `pos($_) = EXPR` (Text::Balanced `_eb_delims`).
10307                    self.advance();
10308                    let rhs = self.parse_assign_expr()?;
10309                    Ok(Expr {
10310                        kind: ExprKind::Assign {
10311                            target: Box::new(Expr {
10312                                kind: ExprKind::Pos(Some(Box::new(Expr {
10313                                    kind: ExprKind::ScalarVar("_".into()),
10314                                    line,
10315                                }))),
10316                                line,
10317                            }),
10318                            value: Box::new(rhs),
10319                        },
10320                        line,
10321                    })
10322                } else if matches!(self.peek(), Token::LParen) {
10323                    self.advance();
10324                    if matches!(self.peek(), Token::RParen) {
10325                        self.advance();
10326                        Ok(Expr {
10327                            kind: ExprKind::Pos(None),
10328                            line,
10329                        })
10330                    } else {
10331                        let a = self.parse_expression()?;
10332                        self.expect(&Token::RParen)?;
10333                        Ok(Expr {
10334                            kind: ExprKind::Pos(Some(Box::new(a))),
10335                            line,
10336                        })
10337                    }
10338                } else {
10339                    let saved = self.pos;
10340                    let subj = self.parse_unary()?;
10341                    if matches!(self.peek(), Token::Assign) {
10342                        self.advance();
10343                        let rhs = self.parse_assign_expr()?;
10344                        Ok(Expr {
10345                            kind: ExprKind::Assign {
10346                                target: Box::new(Expr {
10347                                    kind: ExprKind::Pos(Some(Box::new(subj))),
10348                                    line,
10349                                }),
10350                                value: Box::new(rhs),
10351                            },
10352                            line,
10353                        })
10354                    } else {
10355                        self.pos = saved;
10356                        let a = self.parse_one_arg()?;
10357                        Ok(Expr {
10358                            kind: ExprKind::Pos(Some(Box::new(a))),
10359                            line,
10360                        })
10361                    }
10362                }
10363            }
10364            "study" => {
10365                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10366                    return Ok(e);
10367                }
10368                let a = self.parse_one_arg_or_default()?;
10369                Ok(Expr {
10370                    kind: ExprKind::Study(Box::new(a)),
10371                    line,
10372                })
10373            }
10374            "push" => {
10375                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10376                    return Ok(e);
10377                }
10378                let args = self.parse_builtin_args()?;
10379                let (first, rest) = args
10380                    .split_first()
10381                    .ok_or_else(|| self.syntax_err("push requires arguments", line))?;
10382                // Perl 5.24+ rejects `push SCALAR, ...` at parse time. Reject any
10383                // first arg that is unambiguously a scalar (literal scalar var or
10384                // numeric/string literal). Array refs (`@$x`), bindings, slices,
10385                // and `our @a` style remain permitted.
10386                if matches!(
10387                    first.kind,
10388                    ExprKind::ScalarVar(_)
10389                        | ExprKind::Integer(_)
10390                        | ExprKind::Float(_)
10391                        | ExprKind::String(_)
10392                ) {
10393                    return Err(self
10394                        .syntax_err("Experimental push on scalar is now forbidden", line)
10395                        .with_near("at EOF"));
10396                }
10397                Ok(Expr {
10398                    kind: ExprKind::Push {
10399                        array: Box::new(first.clone()),
10400                        values: rest.to_vec(),
10401                    },
10402                    line,
10403                })
10404            }
10405            "pop" => {
10406                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10407                    return Ok(e);
10408                }
10409                let a = self.parse_one_arg_or_argv()?;
10410                Ok(Expr {
10411                    kind: ExprKind::Pop(Box::new(a)),
10412                    line,
10413                })
10414            }
10415            "shift" => {
10416                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10417                    return Ok(e);
10418                }
10419                let a = self.parse_one_arg_or_argv()?;
10420                Ok(Expr {
10421                    kind: ExprKind::Shift(Box::new(a)),
10422                    line,
10423                })
10424            }
10425            "unshift" => {
10426                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10427                    return Ok(e);
10428                }
10429                let args = self.parse_builtin_args()?;
10430                let (first, rest) = args
10431                    .split_first()
10432                    .ok_or_else(|| self.syntax_err("unshift requires arguments", line))?;
10433                Ok(Expr {
10434                    kind: ExprKind::Unshift {
10435                        array: Box::new(first.clone()),
10436                        values: rest.to_vec(),
10437                    },
10438                    line,
10439                })
10440            }
10441            "splice" => {
10442                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10443                    return Ok(e);
10444                }
10445                let args = self.parse_builtin_args()?;
10446                let mut iter = args.into_iter();
10447                let array = Box::new(
10448                    iter.next()
10449                        .ok_or_else(|| self.syntax_err("splice requires arguments", line))?,
10450                );
10451                let offset = iter.next().map(Box::new);
10452                let length = iter.next().map(Box::new);
10453                let replacement: Vec<Expr> = iter.collect();
10454                Ok(Expr {
10455                    kind: ExprKind::Splice {
10456                        array,
10457                        offset,
10458                        length,
10459                        replacement,
10460                    },
10461                    line,
10462                })
10463            }
10464            // `splice_last(@a, off[, n])` is the stryke spelling of Perl's
10465            // `scalar splice(@a, off, n)` — returns the LAST removed element
10466            // (or undef if nothing was removed). Desugars to `tail(splice(...))`
10467            // so the array is still mutated in place.
10468            "splice_last" | "splice1" | "spl_last" => {
10469                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10470                    return Ok(e);
10471                }
10472                let args = self.parse_builtin_args()?;
10473                let mut iter = args.into_iter();
10474                let array = Box::new(
10475                    iter.next()
10476                        .ok_or_else(|| self.syntax_err("splice_last requires arguments", line))?,
10477                );
10478                let offset = iter.next().map(Box::new);
10479                let length = iter.next().map(Box::new);
10480                let replacement: Vec<Expr> = iter.collect();
10481                let splice_expr = Expr {
10482                    kind: ExprKind::Splice {
10483                        array,
10484                        offset,
10485                        length,
10486                        replacement,
10487                    },
10488                    line,
10489                };
10490                Ok(Expr {
10491                    kind: ExprKind::FuncCall {
10492                        name: "tail".to_string(),
10493                        args: vec![splice_expr],
10494                    },
10495                    line,
10496                })
10497            }
10498            "delete" => {
10499                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10500                    return Ok(e);
10501                }
10502                let a = self.parse_postfix()?;
10503                Ok(Expr {
10504                    kind: ExprKind::Delete(Box::new(a)),
10505                    line,
10506                })
10507            }
10508            "exists" => {
10509                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10510                    return Ok(e);
10511                }
10512                // `parse_postfix` starts at `parse_primary` which doesn't
10513                // accept the leading `&` of `&subname` — call `parse_unary`
10514                // instead so `exists &main::myf` parses the same as
10515                // `defined &main::myf` already does.
10516                let a = self.parse_unary()?;
10517                Ok(Expr {
10518                    kind: ExprKind::Exists(Box::new(a)),
10519                    line,
10520                })
10521            }
10522            "keys" => {
10523                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10524                    return Ok(e);
10525                }
10526                let a = self.parse_one_arg_or_default()?;
10527                Ok(Expr {
10528                    kind: ExprKind::Keys(Box::new(a)),
10529                    line,
10530                })
10531            }
10532            "values" => {
10533                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10534                    return Ok(e);
10535                }
10536                let a = self.parse_one_arg_or_default()?;
10537                Ok(Expr {
10538                    kind: ExprKind::Values(Box::new(a)),
10539                    line,
10540                })
10541            }
10542            "each" => {
10543                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10544                    return Ok(e);
10545                }
10546                let a = self.parse_one_arg_or_default()?;
10547                Ok(Expr {
10548                    kind: ExprKind::Each(Box::new(a)),
10549                    line,
10550                })
10551            }
10552            "fore" | "e" | "ep" => {
10553                // `fore { BLOCK } LIST` / `ep` — forEach expression (pipe-forward friendly)
10554                if matches!(self.peek(), Token::LBrace) {
10555                    let (block, list) = self.parse_block_list()?;
10556                    Ok(Expr {
10557                        kind: ExprKind::ForEachExpr {
10558                            block,
10559                            list: Box::new(list),
10560                        },
10561                        line,
10562                    })
10563                } else if self.in_pipe_rhs() {
10564                    // `|> ep` — bare ep at end of pipe: default to `say $_`
10565                    // `|> fore say` / `|> e say` — blockless pipe form: wrap EXPR into a synthetic block
10566                    let is_terminal = matches!(
10567                        self.peek(),
10568                        Token::Semicolon
10569                            | Token::RParen
10570                            | Token::Eof
10571                            | Token::PipeForward
10572                            | Token::RBrace
10573                    );
10574                    let block = if name == "ep" && is_terminal {
10575                        vec![Statement {
10576                            label: None,
10577                            kind: StmtKind::Expression(Expr {
10578                                kind: ExprKind::Say {
10579                                    handle: None,
10580                                    args: vec![Expr {
10581                                        kind: ExprKind::ScalarVar("_".into()),
10582                                        line,
10583                                    }],
10584                                },
10585                                line,
10586                            }),
10587                            line,
10588                        }]
10589                    } else {
10590                        let expr = self.parse_assign_expr_stop_at_pipe()?;
10591                        let expr = Self::lift_bareword_to_topic_call(expr);
10592                        vec![Statement {
10593                            label: None,
10594                            kind: StmtKind::Expression(expr),
10595                            line,
10596                        }]
10597                    };
10598                    let list = self.pipe_placeholder_list(line);
10599                    Ok(Expr {
10600                        kind: ExprKind::ForEachExpr {
10601                            block,
10602                            list: Box::new(list),
10603                        },
10604                        line,
10605                    })
10606                } else {
10607                    // Two surface forms share this branch:
10608                    //   `fore EXPR, LIST` — comma form (explicit per-item EXPR + list)
10609                    //   `ep LIST`         — list-only form: print each item with `say $_`
10610                    // We disambiguate by peeking after the first parsed expression:
10611                    // if the next token is a comma we're in the EXPR-then-LIST form;
10612                    // otherwise the first parse *was* the LIST and we default the
10613                    // block to `say $_` (only for `ep` — `fore`/`e` keep their
10614                    // explicit-expression contract).
10615                    let expr = self.parse_assign_expr()?;
10616                    let expr = Self::lift_bareword_to_topic_call(expr);
10617                    if !matches!(self.peek(), Token::Comma) && name == "ep" {
10618                        let block = vec![Statement {
10619                            label: None,
10620                            kind: StmtKind::Expression(Expr {
10621                                kind: ExprKind::Say {
10622                                    handle: None,
10623                                    args: vec![Expr {
10624                                        kind: ExprKind::ScalarVar("_".into()),
10625                                        line,
10626                                    }],
10627                                },
10628                                line,
10629                            }),
10630                            line,
10631                        }];
10632                        return Ok(Expr {
10633                            kind: ExprKind::ForEachExpr {
10634                                block,
10635                                list: Box::new(expr),
10636                            },
10637                            line,
10638                        });
10639                    }
10640                    self.expect(&Token::Comma)?;
10641                    let list_parts = self.parse_list_until_terminator()?;
10642                    let list_expr = if list_parts.len() == 1 {
10643                        list_parts.into_iter().next().unwrap()
10644                    } else {
10645                        Expr {
10646                            kind: ExprKind::List(list_parts),
10647                            line,
10648                        }
10649                    };
10650                    let block = vec![Statement {
10651                        label: None,
10652                        kind: StmtKind::Expression(expr),
10653                        line,
10654                    }];
10655                    Ok(Expr {
10656                        kind: ExprKind::ForEachExpr {
10657                            block,
10658                            list: Box::new(list_expr),
10659                        },
10660                        line,
10661                    })
10662                }
10663            }
10664            "rev" => {
10665                // `rev` — context-aware reverse: string in scalar, list in list context.
10666                // List-operator precedence (so `rev 1..3` parses as `rev(1..3)`, not
10667                // `(rev 1)..3`). Defaults to $_ when no argument given.
10668                // Only use pipe placeholder when directly in pipe RHS (not inside a block).
10669                // RBrace means we're inside a block like `map { rev }` - use $_ default.
10670                let prev = self.prev_line();
10671                let a = if self.in_pipe_rhs()
10672                    && (matches!(
10673                        self.peek(),
10674                        Token::Semicolon | Token::RParen | Token::Eof | Token::PipeForward
10675                    ) || self.peek_line() > prev)
10676                {
10677                    self.pipe_placeholder_list(line)
10678                } else if self.peek_line() > prev {
10679                    // Newline boundary: argument is on a later line —
10680                    // default to `$_` so the next statement parses as
10681                    // its own thing instead of being slurped as the
10682                    // implicit operand. (Same rule as
10683                    // `parse_one_arg_or_default`.)
10684                    Expr {
10685                        kind: ExprKind::ScalarVar("_".into()),
10686                        line: prev,
10687                    }
10688                } else if matches!(
10689                    self.peek(),
10690                    Token::Semicolon
10691                        | Token::RBrace
10692                        | Token::RParen
10693                        | Token::RBracket
10694                        | Token::Eof
10695                        | Token::Comma
10696                        | Token::FatArrow
10697                        | Token::PipeForward
10698                ) {
10699                    Expr {
10700                        kind: ExprKind::ScalarVar("_".into()),
10701                        line: self.peek_line(),
10702                    }
10703                } else if matches!(self.peek(), Token::LParen)
10704                    && matches!(self.peek_at(1), Token::RParen)
10705                {
10706                    // `rev()` — empty parens default to `$_` (matches Perl's
10707                    // `length()` / `uc()` etc. and the `|> rev()` pipe form).
10708                    let pl = self.peek_line();
10709                    self.advance(); // (
10710                    self.advance(); // )
10711                    Expr {
10712                        kind: ExprKind::ScalarVar("_".into()),
10713                        line: pl,
10714                    }
10715                } else {
10716                    self.parse_one_arg()?
10717                };
10718                Ok(Expr {
10719                    kind: ExprKind::Rev(Box::new(a)),
10720                    line,
10721                })
10722            }
10723            "reverse" => {
10724                if crate::no_interop_mode() {
10725                    return Err(self.syntax_err(
10726                        "stryke uses `rev` instead of `reverse` (--no-interop)",
10727                        line,
10728                    ));
10729                }
10730                // On the RHS of `|>`, the operand is supplied by the piped LHS.
10731                let a = if self.in_pipe_rhs()
10732                    && matches!(
10733                        self.peek(),
10734                        Token::Semicolon
10735                            | Token::RBrace
10736                            | Token::RParen
10737                            | Token::Eof
10738                            | Token::PipeForward
10739                    ) {
10740                    self.pipe_placeholder_list(line)
10741                } else if matches!(self.peek(), Token::LParen)
10742                    && matches!(self.peek_at(1), Token::RParen)
10743                {
10744                    // `reverse()` — Perl-style empty list call returns the empty list.
10745                    self.advance();
10746                    self.advance();
10747                    Expr {
10748                        kind: ExprKind::List(Vec::new()),
10749                        line,
10750                    }
10751                } else {
10752                    self.parse_one_arg()?
10753                };
10754                Ok(Expr {
10755                    kind: ExprKind::ReverseExpr(Box::new(a)),
10756                    line,
10757                })
10758            }
10759            "reversed" | "rv" => {
10760                // On the RHS of `|>`, the operand is supplied by the piped LHS.
10761                let a = if self.in_pipe_rhs()
10762                    && matches!(
10763                        self.peek(),
10764                        Token::Semicolon
10765                            | Token::RBrace
10766                            | Token::RParen
10767                            | Token::Eof
10768                            | Token::PipeForward
10769                    ) {
10770                    self.pipe_placeholder_list(line)
10771                } else {
10772                    self.parse_one_arg()?
10773                };
10774                Ok(Expr {
10775                    kind: ExprKind::Rev(Box::new(a)),
10776                    line,
10777                })
10778            }
10779            "join" => {
10780                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10781                    return Ok(e);
10782                }
10783                let args = self.parse_builtin_args()?;
10784                if args.is_empty() {
10785                    return Err(self.syntax_err("join requires separator and list", line));
10786                }
10787                // `@list |> join(",")` — list slot is filled by the piped LHS.
10788                if args.len() < 2 && !self.in_pipe_rhs() {
10789                    return Err(self.syntax_err("join requires separator and list", line));
10790                }
10791                Ok(Expr {
10792                    kind: ExprKind::JoinExpr {
10793                        separator: Box::new(args[0].clone()),
10794                        list: Box::new(Expr {
10795                            kind: ExprKind::List(args[1..].to_vec()),
10796                            line,
10797                        }),
10798                    },
10799                    line,
10800                })
10801            }
10802            "split" => {
10803                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10804                    return Ok(e);
10805                }
10806                let args = self.parse_builtin_args()?;
10807                let pattern = args.first().cloned().unwrap_or(Expr {
10808                    kind: ExprKind::String(" ".into()),
10809                    line,
10810                });
10811                let string = args.get(1).cloned().unwrap_or(Expr {
10812                    kind: ExprKind::ScalarVar("_".into()),
10813                    line,
10814                });
10815                let limit = args.get(2).cloned().map(Box::new);
10816                Ok(Expr {
10817                    kind: ExprKind::SplitExpr {
10818                        pattern: Box::new(pattern),
10819                        string: Box::new(string),
10820                        limit,
10821                    },
10822                    line,
10823                })
10824            }
10825            "substr" => {
10826                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10827                    return Ok(e);
10828                }
10829                let args = self.parse_builtin_args()?;
10830                Ok(Expr {
10831                    kind: ExprKind::Substr {
10832                        string: Box::new(args[0].clone()),
10833                        offset: Box::new(args[1].clone()),
10834                        length: args.get(2).cloned().map(Box::new),
10835                        replacement: args.get(3).cloned().map(Box::new),
10836                    },
10837                    line,
10838                })
10839            }
10840            "index" => {
10841                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10842                    return Ok(e);
10843                }
10844                let args = self.parse_builtin_args()?;
10845                Ok(Expr {
10846                    kind: ExprKind::Index {
10847                        string: Box::new(args[0].clone()),
10848                        substr: Box::new(args[1].clone()),
10849                        position: args.get(2).cloned().map(Box::new),
10850                    },
10851                    line,
10852                })
10853            }
10854            "rindex" => {
10855                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10856                    return Ok(e);
10857                }
10858                let args = self.parse_builtin_args()?;
10859                Ok(Expr {
10860                    kind: ExprKind::Rindex {
10861                        string: Box::new(args[0].clone()),
10862                        substr: Box::new(args[1].clone()),
10863                        position: args.get(2).cloned().map(Box::new),
10864                    },
10865                    line,
10866                })
10867            }
10868            "sprintf" => {
10869                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10870                    return Ok(e);
10871                }
10872                let args = self.parse_builtin_args()?;
10873                let (first, rest) = args
10874                    .split_first()
10875                    .ok_or_else(|| self.syntax_err("sprintf requires format", line))?;
10876                Ok(Expr {
10877                    kind: ExprKind::Sprintf {
10878                        format: Box::new(first.clone()),
10879                        args: rest.to_vec(),
10880                    },
10881                    line,
10882                })
10883            }
10884            "map" | "flat_map" | "maps" | "flat_maps" => {
10885                let flatten_array_refs = matches!(name.as_str(), "flat_map" | "flat_maps");
10886                let stream = matches!(name.as_str(), "maps" | "flat_maps");
10887                if matches!(self.peek(), Token::LBrace) {
10888                    let (block, list) = self.parse_block_list()?;
10889                    Ok(Expr {
10890                        kind: ExprKind::MapExpr {
10891                            block,
10892                            list: Box::new(list),
10893                            flatten_array_refs,
10894                            stream,
10895                        },
10896                        line,
10897                    })
10898                } else {
10899                    let expr = self.parse_assign_expr_stop_at_pipe()?;
10900                    // Lift bareword to FuncCall($_) so `map sha512, @list`
10901                    // calls sha512($_) for each element instead of stringifying.
10902                    let expr = Self::lift_bareword_to_topic_call(expr);
10903                    let list_expr = if self.pipe_supplies_slurped_list_operand() {
10904                        self.pipe_placeholder_list(line)
10905                    } else {
10906                        self.expect(&Token::Comma)?;
10907                        let list_parts = self.parse_list_until_terminator()?;
10908                        if list_parts.len() == 1 {
10909                            list_parts.into_iter().next().unwrap()
10910                        } else {
10911                            Expr {
10912                                kind: ExprKind::List(list_parts),
10913                                line,
10914                            }
10915                        }
10916                    };
10917                    Ok(Expr {
10918                        kind: ExprKind::MapExprComma {
10919                            expr: Box::new(expr),
10920                            list: Box::new(list_expr),
10921                            flatten_array_refs,
10922                            stream,
10923                        },
10924                        line,
10925                    })
10926                }
10927            }
10928            "cond" => {
10929                if crate::compat_mode() {
10930                    return Err(self
10931                        .syntax_err("`cond` is a stryke extension (disabled by --compat)", line));
10932                }
10933                self.parse_cond_expr(line)
10934            }
10935            "match" => {
10936                if crate::compat_mode() {
10937                    return Err(self.syntax_err(
10938                        "algebraic `match` is a stryke extension (disabled by --compat)",
10939                        line,
10940                    ));
10941                }
10942                self.parse_algebraic_match_expr(line)
10943            }
10944            "grep" | "greps" | "filter" | "fi" | "find_all" => {
10945                let keyword = match name.as_str() {
10946                    "grep" => crate::ast::GrepBuiltinKeyword::Grep,
10947                    "greps" => crate::ast::GrepBuiltinKeyword::Greps,
10948                    "filter" | "fi" => crate::ast::GrepBuiltinKeyword::Filter,
10949                    "find_all" => crate::ast::GrepBuiltinKeyword::FindAll,
10950                    _ => unreachable!(),
10951                };
10952                if matches!(self.peek(), Token::LBrace) {
10953                    let (block, list) = self.parse_block_list()?;
10954                    Ok(Expr {
10955                        kind: ExprKind::GrepExpr {
10956                            block,
10957                            list: Box::new(list),
10958                            keyword,
10959                        },
10960                        line,
10961                    })
10962                } else {
10963                    let expr = self.parse_assign_expr_stop_at_pipe()?;
10964                    if self.pipe_supplies_slurped_list_operand() {
10965                        // Pipe-RHS blockless form: `|> grep EXPR`
10966                        // For literals, desugar to `$_ eq/== EXPR` so
10967                        // `|> filter 't'` keeps only elements equal to 't'.
10968                        // For regexes, desugar to `$_ =~ EXPR`.
10969                        let list = self.pipe_placeholder_list(line);
10970                        let topic = Expr {
10971                            kind: ExprKind::ScalarVar("_".into()),
10972                            line,
10973                        };
10974                        let test = match &expr.kind {
10975                            ExprKind::Integer(_) | ExprKind::Float(_) => Expr {
10976                                kind: ExprKind::BinOp {
10977                                    op: BinOp::NumEq,
10978                                    left: Box::new(topic),
10979                                    right: Box::new(expr),
10980                                },
10981                                line,
10982                            },
10983                            ExprKind::String(_) | ExprKind::InterpolatedString(_) => Expr {
10984                                kind: ExprKind::BinOp {
10985                                    op: BinOp::StrEq,
10986                                    left: Box::new(topic),
10987                                    right: Box::new(expr),
10988                                },
10989                                line,
10990                            },
10991                            ExprKind::Regex { .. } => Expr {
10992                                kind: ExprKind::BinOp {
10993                                    op: BinOp::BindMatch,
10994                                    left: Box::new(topic),
10995                                    right: Box::new(expr),
10996                                },
10997                                line,
10998                            },
10999                            _ => {
11000                                // Non-literal (e.g. `defined`, scalar coderef var,
11001                                // hash slot): lift barewords to topic-call, then
11002                                // route through GrepExprComma so the runtime
11003                                // coderef-dispatch in Op::GrepWithExpr handles
11004                                // both truthiness AND coderef-call uniformly.
11005                                let expr = Self::lift_bareword_to_topic_call(expr);
11006                                return Ok(Expr {
11007                                    kind: ExprKind::GrepExprComma {
11008                                        expr: Box::new(expr),
11009                                        list: Box::new(list),
11010                                        keyword,
11011                                    },
11012                                    line,
11013                                });
11014                            }
11015                        };
11016                        let block = vec![Statement {
11017                            label: None,
11018                            kind: StmtKind::Expression(test),
11019                            line,
11020                        }];
11021                        Ok(Expr {
11022                            kind: ExprKind::GrepExpr {
11023                                block,
11024                                list: Box::new(list),
11025                                keyword,
11026                            },
11027                            line,
11028                        })
11029                    } else {
11030                        let expr = Self::lift_bareword_to_topic_call(expr);
11031                        self.expect(&Token::Comma)?;
11032                        let list_parts = self.parse_list_until_terminator()?;
11033                        let list_expr = if list_parts.len() == 1 {
11034                            list_parts.into_iter().next().unwrap()
11035                        } else {
11036                            Expr {
11037                                kind: ExprKind::List(list_parts),
11038                                line,
11039                            }
11040                        };
11041                        Ok(Expr {
11042                            kind: ExprKind::GrepExprComma {
11043                                expr: Box::new(expr),
11044                                list: Box::new(list_expr),
11045                                keyword,
11046                            },
11047                            line,
11048                        })
11049                    }
11050                }
11051            }
11052            "sort" => {
11053                use crate::ast::SortComparator;
11054                if matches!(self.peek(), Token::LBrace) {
11055                    let block = self.parse_block()?;
11056                    let block_end_line = self.prev_line();
11057                    let _ = self.eat(&Token::Comma);
11058                    let list = if self.in_pipe_rhs()
11059                        && (matches!(
11060                            self.peek(),
11061                            Token::Semicolon
11062                                | Token::RBrace
11063                                | Token::RParen
11064                                | Token::Eof
11065                                | Token::PipeForward
11066                        ) || self.peek_line() > block_end_line)
11067                    {
11068                        self.pipe_placeholder_list(line)
11069                    } else {
11070                        self.parse_expression()?
11071                    };
11072                    Ok(Expr {
11073                        kind: ExprKind::SortExpr {
11074                            cmp: Some(SortComparator::Block(block)),
11075                            list: Box::new(list),
11076                        },
11077                        line,
11078                    })
11079                } else if matches!(self.peek(), Token::ScalarVar(ref v) if v == "a" || v == "b") {
11080                    // Blockless comparator: `sort $a <=> $b, @list`
11081                    let block = self.parse_block_or_bareword_cmp_block()?;
11082                    let _ = self.eat(&Token::Comma);
11083                    let list = if self.in_pipe_rhs()
11084                        && matches!(
11085                            self.peek(),
11086                            Token::Semicolon
11087                                | Token::RBrace
11088                                | Token::RParen
11089                                | Token::Eof
11090                                | Token::PipeForward
11091                        ) {
11092                        self.pipe_placeholder_list(line)
11093                    } else {
11094                        self.parse_expression()?
11095                    };
11096                    Ok(Expr {
11097                        kind: ExprKind::SortExpr {
11098                            cmp: Some(SortComparator::Block(block)),
11099                            list: Box::new(list),
11100                        },
11101                        line,
11102                    })
11103                } else if matches!(self.peek(), Token::ScalarVar(_)) {
11104                    // `sort $coderef (LIST)` — comparator is first; list often parenthesized.
11105                    // Pipe-RHS form `|> sort $coderef` uses placeholder LHS as the list.
11106                    self.suppress_indirect_paren_call =
11107                        self.suppress_indirect_paren_call.saturating_add(1);
11108                    let code = self.parse_assign_expr()?;
11109                    self.suppress_indirect_paren_call =
11110                        self.suppress_indirect_paren_call.saturating_sub(1);
11111                    let _ = self.eat(&Token::Comma);
11112                    let list = if self.in_pipe_rhs()
11113                        && matches!(
11114                            self.peek(),
11115                            Token::Semicolon
11116                                | Token::RBrace
11117                                | Token::RParen
11118                                | Token::Eof
11119                                | Token::PipeForward
11120                        ) {
11121                        self.pipe_placeholder_list(line)
11122                    } else if matches!(self.peek(), Token::LParen) {
11123                        self.advance();
11124                        let e = self.parse_expression()?;
11125                        self.expect(&Token::RParen)?;
11126                        e
11127                    } else {
11128                        self.parse_expression()?
11129                    };
11130                    Ok(Expr {
11131                        kind: ExprKind::SortExpr {
11132                            cmp: Some(SortComparator::Code(Box::new(code))),
11133                            list: Box::new(list),
11134                        },
11135                        line,
11136                    })
11137                } else if matches!(self.peek(), Token::Ident(ref name) if !Self::is_known_bareword(name))
11138                {
11139                    // Blockless comparator via bare sub name: `sort my_cmp @list`
11140                    let block = self.parse_block_or_bareword_cmp_block()?;
11141                    let _ = self.eat(&Token::Comma);
11142                    let list = if self.in_pipe_rhs()
11143                        && matches!(
11144                            self.peek(),
11145                            Token::Semicolon
11146                                | Token::RBrace
11147                                | Token::RParen
11148                                | Token::Eof
11149                                | Token::PipeForward
11150                        ) {
11151                        self.pipe_placeholder_list(line)
11152                    } else {
11153                        self.parse_expression()?
11154                    };
11155                    Ok(Expr {
11156                        kind: ExprKind::SortExpr {
11157                            cmp: Some(SortComparator::Block(block)),
11158                            list: Box::new(list),
11159                        },
11160                        line,
11161                    })
11162                } else {
11163                    // Bare `sort` with no comparator and no list: only allowed
11164                    // as the RHS of `|>`, where the list comes from the LHS.
11165                    // Treat a newline as an implicit pipeline terminator —
11166                    // `@a |> sort\nmy $x = ...` must NOT swallow the next
11167                    // `my` stmt as sort's argument list.
11168                    let list = if self.in_pipe_rhs()
11169                        && (matches!(
11170                            self.peek(),
11171                            Token::Semicolon
11172                                | Token::RBrace
11173                                | Token::RParen
11174                                | Token::Eof
11175                                | Token::PipeForward
11176                        ) || self.peek_line() > line)
11177                    {
11178                        self.pipe_placeholder_list(line)
11179                    } else {
11180                        self.parse_expression()?
11181                    };
11182                    Ok(Expr {
11183                        kind: ExprKind::SortExpr {
11184                            cmp: None,
11185                            list: Box::new(list),
11186                        },
11187                        line,
11188                    })
11189                }
11190            }
11191            "reduce" | "fold" | "inject" => {
11192                let (block, list) = self.parse_block_list()?;
11193                Ok(Expr {
11194                    kind: ExprKind::ReduceExpr {
11195                        block,
11196                        list: Box::new(list),
11197                    },
11198                    line,
11199                })
11200            }
11201            // Parallel extensions
11202            "pmap" => {
11203                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11204                Ok(Expr {
11205                    kind: ExprKind::PMapExpr {
11206                        block,
11207                        list: Box::new(list),
11208                        progress: progress.map(Box::new),
11209                        flat_outputs: false,
11210                        on_cluster: None,
11211                        stream: false,
11212                    },
11213                    line,
11214                })
11215            }
11216            "pmap_on" => {
11217                let (cluster, block, list, progress) =
11218                    self.parse_cluster_block_then_list_optional_progress()?;
11219                Ok(Expr {
11220                    kind: ExprKind::PMapExpr {
11221                        block,
11222                        list: Box::new(list),
11223                        progress: progress.map(Box::new),
11224                        flat_outputs: false,
11225                        on_cluster: Some(Box::new(cluster)),
11226                        stream: false,
11227                    },
11228                    line,
11229                })
11230            }
11231            "pflat_map" => {
11232                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11233                Ok(Expr {
11234                    kind: ExprKind::PMapExpr {
11235                        block,
11236                        list: Box::new(list),
11237                        progress: progress.map(Box::new),
11238                        flat_outputs: true,
11239                        on_cluster: None,
11240                        stream: false,
11241                    },
11242                    line,
11243                })
11244            }
11245            "pflat_map_on" => {
11246                let (cluster, block, list, progress) =
11247                    self.parse_cluster_block_then_list_optional_progress()?;
11248                Ok(Expr {
11249                    kind: ExprKind::PMapExpr {
11250                        block,
11251                        list: Box::new(list),
11252                        progress: progress.map(Box::new),
11253                        flat_outputs: true,
11254                        on_cluster: Some(Box::new(cluster)),
11255                        stream: false,
11256                    },
11257                    line,
11258                })
11259            }
11260            "pmaps" => {
11261                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11262                Ok(Expr {
11263                    kind: ExprKind::PMapExpr {
11264                        block,
11265                        list: Box::new(list),
11266                        progress: progress.map(Box::new),
11267                        flat_outputs: false,
11268                        on_cluster: None,
11269                        stream: true,
11270                    },
11271                    line,
11272                })
11273            }
11274            "pflat_maps" => {
11275                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11276                Ok(Expr {
11277                    kind: ExprKind::PMapExpr {
11278                        block,
11279                        list: Box::new(list),
11280                        progress: progress.map(Box::new),
11281                        flat_outputs: true,
11282                        on_cluster: None,
11283                        stream: true,
11284                    },
11285                    line,
11286                })
11287            }
11288            "pgreps" => {
11289                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11290                Ok(Expr {
11291                    kind: ExprKind::PGrepExpr {
11292                        block,
11293                        list: Box::new(list),
11294                        progress: progress.map(Box::new),
11295                        stream: true,
11296                    },
11297                    line,
11298                })
11299            }
11300            "pmap_chunked" => {
11301                let chunk_size = self.parse_assign_expr()?;
11302                let block = self.parse_block_or_bareword_block()?;
11303                self.eat(&Token::Comma);
11304                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
11305                Ok(Expr {
11306                    kind: ExprKind::PMapChunkedExpr {
11307                        chunk_size: Box::new(chunk_size),
11308                        block,
11309                        list: Box::new(list),
11310                        progress: progress.map(Box::new),
11311                    },
11312                    line,
11313                })
11314            }
11315            "pgrep" => {
11316                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11317                Ok(Expr {
11318                    kind: ExprKind::PGrepExpr {
11319                        block,
11320                        list: Box::new(list),
11321                        progress: progress.map(Box::new),
11322                        stream: false,
11323                    },
11324                    line,
11325                })
11326            }
11327            "pfor" => {
11328                if matches!(self.peek(), Token::LParen) {
11329                    self.expect(&Token::LParen)?;
11330                    let list = self.parse_expression()?;
11331                    self.expect(&Token::RParen)?;
11332                    let block = self.parse_block()?;
11333                    Ok(Expr {
11334                        kind: ExprKind::PForExpr {
11335                            block,
11336                            list: Box::new(list),
11337                            progress: None,
11338                        },
11339                        line,
11340                    })
11341                } else {
11342                    let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11343                    Ok(Expr {
11344                        kind: ExprKind::PForExpr {
11345                            block,
11346                            list: Box::new(list),
11347                            progress: progress.map(Box::new),
11348                        },
11349                        line,
11350                    })
11351                }
11352            }
11353            // `par { BLOCK } LIST` — generic parallel-chunk evaluator.
11354            // Splits LIST into chunks (UTF-8-aligned for strings,
11355            // element-aligned for arrays), runs BLOCK on each chunk in
11356            // parallel with `_` bound to the chunk, flattens results.
11357            // Available as a top-level expression, not just an `~>` stage.
11358            "par" => {
11359                let (block, list, _progress) = self.parse_block_then_list_optional_progress()?;
11360                Ok(Expr {
11361                    kind: ExprKind::ParExpr {
11362                        block,
11363                        list: Box::new(list),
11364                    },
11365                    line,
11366                })
11367            }
11368            "par_lines" | "par_walk" => {
11369                let args = self.parse_builtin_args()?;
11370                if args.len() < 2 {
11371                    return Err(
11372                        self.syntax_err(format!("{} requires at least two arguments", name), line)
11373                    );
11374                }
11375
11376                if name == "par_lines" {
11377                    Ok(Expr {
11378                        kind: ExprKind::ParLinesExpr {
11379                            path: Box::new(args[0].clone()),
11380                            callback: Box::new(args[1].clone()),
11381                            progress: None,
11382                        },
11383                        line,
11384                    })
11385                } else {
11386                    Ok(Expr {
11387                        kind: ExprKind::ParWalkExpr {
11388                            path: Box::new(args[0].clone()),
11389                            callback: Box::new(args[1].clone()),
11390                            progress: None,
11391                        },
11392                        line,
11393                    })
11394                }
11395            }
11396            "pwatch" | "watch" => {
11397                let args = self.parse_builtin_args()?;
11398                if args.len() < 2 {
11399                    return Err(
11400                        self.syntax_err(format!("{} requires at least two arguments", name), line)
11401                    );
11402                }
11403                Ok(Expr {
11404                    kind: ExprKind::PwatchExpr {
11405                        path: Box::new(args[0].clone()),
11406                        callback: Box::new(args[1].clone()),
11407                    },
11408                    line,
11409                })
11410            }
11411            "fan" => {
11412                // fan { BLOCK }            — no count, block body
11413                // fan COUNT { BLOCK }      — count + block body
11414                // fan EXPR;                — no count, blockless body (wrap EXPR as block)
11415                // fan COUNT EXPR;          — count + blockless body
11416                // Optional: `, progress => EXPR` or `progress => EXPR` (no comma before progress)
11417                let (count, block) = self.parse_fan_count_and_block(line)?;
11418                let progress = self.parse_fan_optional_progress("fan")?;
11419                Ok(Expr {
11420                    kind: ExprKind::FanExpr {
11421                        count,
11422                        block,
11423                        progress,
11424                        capture: false,
11425                    },
11426                    line,
11427                })
11428            }
11429            "fan_cap" => {
11430                let (count, block) = self.parse_fan_count_and_block(line)?;
11431                let progress = self.parse_fan_optional_progress("fan_cap")?;
11432                Ok(Expr {
11433                    kind: ExprKind::FanExpr {
11434                        count,
11435                        block,
11436                        progress,
11437                        capture: true,
11438                    },
11439                    line,
11440                })
11441            }
11442            "async" => {
11443                if !matches!(self.peek(), Token::LBrace) {
11444                    return Err(self.syntax_err("async must be followed by { BLOCK }", line));
11445                }
11446                let block = self.parse_block()?;
11447                Ok(Expr {
11448                    kind: ExprKind::AsyncBlock { body: block },
11449                    line,
11450                })
11451            }
11452            "spawn" => {
11453                if !matches!(self.peek(), Token::LBrace) {
11454                    return Err(self.syntax_err("spawn must be followed by { BLOCK }", line));
11455                }
11456                let block = self.parse_block()?;
11457                Ok(Expr {
11458                    kind: ExprKind::SpawnBlock { body: block },
11459                    line,
11460                })
11461            }
11462            "trace" => {
11463                if !matches!(self.peek(), Token::LBrace) {
11464                    return Err(self.syntax_err("trace must be followed by { BLOCK }", line));
11465                }
11466                let block = self.parse_block()?;
11467                Ok(Expr {
11468                    kind: ExprKind::Trace { body: block },
11469                    line,
11470                })
11471            }
11472            "timer" => {
11473                let block = self.parse_block_or_bareword_block_no_args()?;
11474                Ok(Expr {
11475                    kind: ExprKind::Timer { body: block },
11476                    line,
11477                })
11478            }
11479            "bench" => {
11480                let block = self.parse_block_or_bareword_block_no_args()?;
11481                let times = Box::new(self.parse_expression()?);
11482                Ok(Expr {
11483                    kind: ExprKind::Bench { body: block, times },
11484                    line,
11485                })
11486            }
11487            "spinner" => {
11488                // `spinner "msg" { BLOCK }` or `spinner { BLOCK }`
11489                let (message, body) = if matches!(self.peek(), Token::LBrace) {
11490                    let body = self.parse_block()?;
11491                    (
11492                        Box::new(Expr {
11493                            kind: ExprKind::String("working".to_string()),
11494                            line,
11495                        }),
11496                        body,
11497                    )
11498                } else {
11499                    let msg = self.parse_assign_expr()?;
11500                    let body = self.parse_block()?;
11501                    (Box::new(msg), body)
11502                };
11503                Ok(Expr {
11504                    kind: ExprKind::Spinner { message, body },
11505                    line,
11506                })
11507            }
11508            "thread" | "t" => {
11509                // `thread EXPR stage1 stage2 ...` — threading macro (thread-first)
11510                // `t` is a short alias for `thread`
11511                // Each stage is either:
11512                //   - `ident` — bare function call
11513                //   - `ident { block }` — function with block arg
11514                //   - `ident arg1 arg2 { block }` — function with args and optional block
11515                //   - `fn { block }` — standalone anonymous block
11516                //   - `>{ block }` — shorthand for standalone anonymous block
11517                // Desugars to: EXPR |> stage1 |> stage2 |> ...
11518                self.parse_thread_macro(line, false)
11519            }
11520            "retry" => {
11521                // `retry { BLOCK }` or `retry BAREWORD` — bareword becomes zero-arg call.
11522                // An optional comma before `times` is allowed in both forms.
11523                let body = if matches!(self.peek(), Token::LBrace) {
11524                    self.parse_block()?
11525                } else {
11526                    let bw_line = self.peek_line();
11527                    let Token::Ident(ref name) = self.peek().clone() else {
11528                        return Err(self
11529                            .syntax_err("retry: expected block or bareword function name", line));
11530                    };
11531                    let name = name.clone();
11532                    self.advance();
11533                    vec![Statement::new(
11534                        StmtKind::Expression(Expr {
11535                            kind: ExprKind::FuncCall { name, args: vec![] },
11536                            line: bw_line,
11537                        }),
11538                        bw_line,
11539                    )]
11540                };
11541                self.eat(&Token::Comma);
11542                match self.peek() {
11543                    Token::Ident(ref s) if s == "times" => {
11544                        self.advance();
11545                    }
11546                    _ => {
11547                        return Err(self.syntax_err("retry: expected `times =>` after block", line));
11548                    }
11549                }
11550                self.expect(&Token::FatArrow)?;
11551                let times = Box::new(self.parse_assign_expr()?);
11552                let mut backoff = RetryBackoff::None;
11553                if self.eat(&Token::Comma) {
11554                    match self.peek() {
11555                        Token::Ident(ref s) if s == "backoff" => {
11556                            self.advance();
11557                        }
11558                        _ => {
11559                            return Err(
11560                                self.syntax_err("retry: expected `backoff =>` after comma", line)
11561                            );
11562                        }
11563                    }
11564                    self.expect(&Token::FatArrow)?;
11565                    let Token::Ident(mode) = self.peek().clone() else {
11566                        return Err(self.syntax_err(
11567                            "retry: expected backoff mode (none, linear, exponential)",
11568                            line,
11569                        ));
11570                    };
11571                    backoff = match mode.as_str() {
11572                        "none" => RetryBackoff::None,
11573                        "linear" => RetryBackoff::Linear,
11574                        "exponential" => RetryBackoff::Exponential,
11575                        _ => {
11576                            return Err(
11577                                self.syntax_err(format!("retry: invalid backoff `{mode}`"), line)
11578                            );
11579                        }
11580                    };
11581                    self.advance();
11582                }
11583                Ok(Expr {
11584                    kind: ExprKind::RetryBlock {
11585                        body,
11586                        times,
11587                        backoff,
11588                    },
11589                    line,
11590                })
11591            }
11592            "rate_limit" => {
11593                self.expect(&Token::LParen)?;
11594                let max = Box::new(self.parse_assign_expr()?);
11595                self.expect(&Token::Comma)?;
11596                let window = Box::new(self.parse_assign_expr()?);
11597                self.expect(&Token::RParen)?;
11598                let body = self.parse_block_or_bareword_block_no_args()?;
11599                let slot = self.alloc_rate_limit_slot();
11600                Ok(Expr {
11601                    kind: ExprKind::RateLimitBlock {
11602                        slot,
11603                        max,
11604                        window,
11605                        body,
11606                    },
11607                    line,
11608                })
11609            }
11610            "every" => {
11611                // `every("500ms") { BLOCK }` or `every "500ms" BODY` — parens optional.
11612                // Body consumes `|>` (every is an infinite loop, not a pipeable source).
11613                let has_paren = self.eat(&Token::LParen);
11614                let interval = Box::new(self.parse_assign_expr()?);
11615                if has_paren {
11616                    self.expect(&Token::RParen)?;
11617                }
11618                let body = if matches!(self.peek(), Token::LBrace) {
11619                    self.parse_block()?
11620                } else {
11621                    let bline = self.peek_line();
11622                    let expr = self.parse_assign_expr()?;
11623                    vec![Statement::new(StmtKind::Expression(expr), bline)]
11624                };
11625                Ok(Expr {
11626                    kind: ExprKind::EveryBlock { interval, body },
11627                    line,
11628                })
11629            }
11630            "gen" => {
11631                if !matches!(self.peek(), Token::LBrace) {
11632                    return Err(self.syntax_err("gen must be followed by { BLOCK }", line));
11633                }
11634                let body = self.parse_block()?;
11635                Ok(Expr {
11636                    kind: ExprKind::GenBlock { body },
11637                    line,
11638                })
11639            }
11640            "yield" => {
11641                let e = self.parse_assign_expr()?;
11642                Ok(Expr {
11643                    kind: ExprKind::Yield(Box::new(e)),
11644                    line,
11645                })
11646            }
11647            "await" => {
11648                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11649                    return Ok(e);
11650                }
11651                // `await` defaults to `$_` so `map { await } @tasks` works
11652                // (Perl-style topic-defaulting unary).
11653                let a = self.parse_one_arg_or_default()?;
11654                Ok(Expr {
11655                    kind: ExprKind::Await(Box::new(a)),
11656                    line,
11657                })
11658            }
11659            "slurp" | "cat" | "c" => {
11660                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11661                    return Ok(e);
11662                }
11663                let a = self.parse_one_arg_or_default()?;
11664                Ok(Expr {
11665                    kind: ExprKind::Slurp(Box::new(a)),
11666                    line,
11667                })
11668            }
11669            "swallow" | "swa" => {
11670                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11671                    return Ok(e);
11672                }
11673                let a = self.parse_one_arg_or_default()?;
11674                Ok(Expr {
11675                    kind: ExprKind::Swallow(Box::new(a)),
11676                    line,
11677                })
11678            }
11679            "ingest" | "ing" => {
11680                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11681                    return Ok(e);
11682                }
11683                let a = self.parse_one_arg_or_default()?;
11684                Ok(Expr {
11685                    kind: ExprKind::Ingest(Box::new(a)),
11686                    line,
11687                })
11688            }
11689            "burp" => {
11690                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11691                    return Ok(e);
11692                }
11693                let a = self.parse_one_arg_or_default()?;
11694                Ok(Expr {
11695                    kind: ExprKind::Burp(Box::new(a)),
11696                    line,
11697                })
11698            }
11699            "god" => {
11700                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11701                    return Ok(e);
11702                }
11703                let a = self.parse_one_arg_or_default()?;
11704                Ok(Expr {
11705                    kind: ExprKind::God(Box::new(a)),
11706                    line,
11707                })
11708            }
11709            "capture" => {
11710                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11711                    return Ok(e);
11712                }
11713                let a = self.parse_one_arg()?;
11714                Ok(Expr {
11715                    kind: ExprKind::Capture(Box::new(a)),
11716                    line,
11717                })
11718            }
11719            "fetch_url" => {
11720                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11721                    return Ok(e);
11722                }
11723                let a = self.parse_one_arg()?;
11724                Ok(Expr {
11725                    kind: ExprKind::FetchUrl(Box::new(a)),
11726                    line,
11727                })
11728            }
11729            "pchannel" => {
11730                let capacity = if self.eat(&Token::LParen) {
11731                    if matches!(self.peek(), Token::RParen) {
11732                        self.advance();
11733                        None
11734                    } else {
11735                        let e = self.parse_expression()?;
11736                        self.expect(&Token::RParen)?;
11737                        Some(Box::new(e))
11738                    }
11739                } else {
11740                    None
11741                };
11742                Ok(Expr {
11743                    kind: ExprKind::Pchannel { capacity },
11744                    line,
11745                })
11746            }
11747            "psort" => {
11748                if matches!(self.peek(), Token::LBrace)
11749                    || matches!(self.peek(), Token::ScalarVar(ref v) if v == "a" || v == "b")
11750                    || matches!(self.peek(), Token::Ident(ref name) if !Self::is_known_bareword(name))
11751                {
11752                    let block = self.parse_block_or_bareword_cmp_block()?;
11753                    // Mirror `sort`'s pipe-RHS handling — after the block,
11754                    // a newline (or any standard terminator token) inside a
11755                    // `|> psort { ... }` chain means the list comes from the
11756                    // pipe LHS, not from continued parsing into the next
11757                    // statement. Without this check `(@list) |> psort {
11758                    // _0 <=> _1 }\nmy $n = ...` silently swallowed `my $n =
11759                    // ...` as the list operand.
11760                    let block_end_line = self.prev_line();
11761                    self.eat(&Token::Comma);
11762                    let use_placeholder = self.in_pipe_rhs()
11763                        && (matches!(
11764                            self.peek(),
11765                            Token::Semicolon
11766                                | Token::RBrace
11767                                | Token::RParen
11768                                | Token::Eof
11769                                | Token::PipeForward
11770                        ) || self.peek_line() > block_end_line);
11771                    let (list, progress) = if use_placeholder {
11772                        (self.pipe_placeholder_list(line), None)
11773                    } else {
11774                        self.parse_assign_expr_list_optional_progress()?
11775                    };
11776                    Ok(Expr {
11777                        kind: ExprKind::PSortExpr {
11778                            cmp: Some(block),
11779                            list: Box::new(list),
11780                            progress: progress.map(Box::new),
11781                        },
11782                        line,
11783                    })
11784                } else {
11785                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
11786                    Ok(Expr {
11787                        kind: ExprKind::PSortExpr {
11788                            cmp: None,
11789                            list: Box::new(list),
11790                            progress: progress.map(Box::new),
11791                        },
11792                        line,
11793                    })
11794                }
11795            }
11796            "preduce" => {
11797                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11798                Ok(Expr {
11799                    kind: ExprKind::PReduceExpr {
11800                        block,
11801                        list: Box::new(list),
11802                        progress: progress.map(Box::new),
11803                    },
11804                    line,
11805                })
11806            }
11807            "preduce_init" => {
11808                let (init, block, list, progress) =
11809                    self.parse_init_block_then_list_optional_progress()?;
11810                Ok(Expr {
11811                    kind: ExprKind::PReduceInitExpr {
11812                        init: Box::new(init),
11813                        block,
11814                        list: Box::new(list),
11815                        progress: progress.map(Box::new),
11816                    },
11817                    line,
11818                })
11819            }
11820            "pmap_reduce" => {
11821                let map_block = self.parse_block_or_bareword_block()?;
11822                // After the map block, expect either a `{ REDUCE }` block, or
11823                // after an eaten comma, a blockless reduce expr (`$a + $b`).
11824                let reduce_block = if matches!(self.peek(), Token::LBrace) {
11825                    self.parse_block()?
11826                } else {
11827                    // comma separates blockless map from blockless reduce
11828                    self.expect(&Token::Comma)?;
11829                    self.parse_block_or_bareword_cmp_block()?
11830                };
11831                self.eat(&Token::Comma);
11832                let line = self.peek_line();
11833                if let Token::Ident(ref kw) = self.peek().clone() {
11834                    if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
11835                        self.advance();
11836                        self.expect(&Token::FatArrow)?;
11837                        let prog = self.parse_assign_expr()?;
11838                        return Ok(Expr {
11839                            kind: ExprKind::PMapReduceExpr {
11840                                map_block,
11841                                reduce_block,
11842                                list: Box::new(Expr {
11843                                    kind: ExprKind::List(vec![]),
11844                                    line,
11845                                }),
11846                                progress: Some(Box::new(prog)),
11847                            },
11848                            line,
11849                        });
11850                    }
11851                }
11852                // BUG-301 fix: in pipe-RHS context (after `LHS |> pmap_reduce { } { }`),
11853                // the list operand comes from the pipe placeholder — there is no inline
11854                // list. Without this guard, `parse_assign_expr` below would greedily
11855                // consume the next statement (e.g. `p "after"` or `my $x = …`) as the
11856                // list, silently absorbing it into the pmap_reduce call. Mirror the
11857                // terminator check above to also bail when the next token signals "no
11858                // list here" in pipe-RHS context.
11859                if self.pipe_supplies_slurped_list_operand()
11860                    || matches!(
11861                        self.peek(),
11862                        Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
11863                    )
11864                {
11865                    return Ok(Expr {
11866                        kind: ExprKind::PMapReduceExpr {
11867                            map_block,
11868                            reduce_block,
11869                            list: Box::new(Expr {
11870                                kind: ExprKind::List(vec![]),
11871                                line,
11872                            }),
11873                            progress: None,
11874                        },
11875                        line,
11876                    });
11877                }
11878                let mut parts = vec![self.parse_assign_expr()?];
11879                loop {
11880                    if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
11881                        break;
11882                    }
11883                    if matches!(
11884                        self.peek(),
11885                        Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
11886                    ) {
11887                        break;
11888                    }
11889                    if let Token::Ident(ref kw) = self.peek().clone() {
11890                        if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
11891                            self.advance();
11892                            self.expect(&Token::FatArrow)?;
11893                            let prog = self.parse_assign_expr()?;
11894                            return Ok(Expr {
11895                                kind: ExprKind::PMapReduceExpr {
11896                                    map_block,
11897                                    reduce_block,
11898                                    list: Box::new(merge_expr_list(parts)),
11899                                    progress: Some(Box::new(prog)),
11900                                },
11901                                line,
11902                            });
11903                        }
11904                    }
11905                    parts.push(self.parse_assign_expr()?);
11906                }
11907                Ok(Expr {
11908                    kind: ExprKind::PMapReduceExpr {
11909                        map_block,
11910                        reduce_block,
11911                        list: Box::new(merge_expr_list(parts)),
11912                        progress: None,
11913                    },
11914                    line,
11915                })
11916            }
11917            "puniq" => {
11918                if self.pipe_supplies_slurped_list_operand() {
11919                    return Ok(Expr {
11920                        kind: ExprKind::FuncCall {
11921                            name: "puniq".to_string(),
11922                            args: vec![],
11923                        },
11924                        line,
11925                    });
11926                }
11927                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
11928                let mut args = vec![list];
11929                if let Some(p) = progress {
11930                    args.push(p);
11931                }
11932                Ok(Expr {
11933                    kind: ExprKind::FuncCall {
11934                        name: "puniq".to_string(),
11935                        args,
11936                    },
11937                    line,
11938                })
11939            }
11940            "pfirst" => {
11941                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11942                let cr = Expr {
11943                    kind: ExprKind::CodeRef {
11944                        params: vec![],
11945                        body: block,
11946                    },
11947                    line,
11948                };
11949                let mut args = vec![cr, list];
11950                if let Some(p) = progress {
11951                    args.push(p);
11952                }
11953                Ok(Expr {
11954                    kind: ExprKind::FuncCall {
11955                        name: "pfirst".to_string(),
11956                        args,
11957                    },
11958                    line,
11959                })
11960            }
11961            "pany" => {
11962                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11963                let cr = Expr {
11964                    kind: ExprKind::CodeRef {
11965                        params: vec![],
11966                        body: block,
11967                    },
11968                    line,
11969                };
11970                let mut args = vec![cr, list];
11971                if let Some(p) = progress {
11972                    args.push(p);
11973                }
11974                Ok(Expr {
11975                    kind: ExprKind::FuncCall {
11976                        name: "pany".to_string(),
11977                        args,
11978                    },
11979                    line,
11980                })
11981            }
11982            "uniq" | "distinct" => {
11983                if self.pipe_supplies_slurped_list_operand() {
11984                    return Ok(Expr {
11985                        kind: ExprKind::FuncCall {
11986                            name: name.clone(),
11987                            args: vec![],
11988                        },
11989                        line,
11990                    });
11991                }
11992                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
11993                if progress.is_some() {
11994                    return Err(self.syntax_err(
11995                        "`progress =>` is not supported for uniq (use puniq for parallel + progress)",
11996                        line,
11997                    ));
11998                }
11999                Ok(Expr {
12000                    kind: ExprKind::FuncCall {
12001                        name: name.clone(),
12002                        args: vec![list],
12003                    },
12004                    line,
12005                })
12006            }
12007            "flatten" => {
12008                if self.pipe_supplies_slurped_list_operand() {
12009                    return Ok(Expr {
12010                        kind: ExprKind::FuncCall {
12011                            name: "flatten".to_string(),
12012                            args: vec![],
12013                        },
12014                        line,
12015                    });
12016                }
12017                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
12018                if progress.is_some() {
12019                    return Err(self.syntax_err("`progress =>` is not supported for flatten", line));
12020                }
12021                Ok(Expr {
12022                    kind: ExprKind::FuncCall {
12023                        name: "flatten".to_string(),
12024                        args: vec![list],
12025                    },
12026                    line,
12027                })
12028            }
12029            "set" => {
12030                if self.pipe_supplies_slurped_list_operand() {
12031                    return Ok(Expr {
12032                        kind: ExprKind::FuncCall {
12033                            name: "set".to_string(),
12034                            args: vec![],
12035                        },
12036                        line,
12037                    });
12038                }
12039                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
12040                if progress.is_some() {
12041                    return Err(self.syntax_err("`progress =>` is not supported for set", line));
12042                }
12043                Ok(Expr {
12044                    kind: ExprKind::FuncCall {
12045                        name: "set".to_string(),
12046                        args: vec![list],
12047                    },
12048                    line,
12049                })
12050            }
12051            // `size` is the file-size builtin (Perl `-s`), not a list-count alias.
12052            // Defaults to `$_` when no arg is given, like `length`. See
12053            // `builtin_file_size` in builtins.rs for the runtime behavior.
12054            "size" => {
12055                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12056                    return Ok(e);
12057                }
12058                if self.pipe_supplies_slurped_list_operand() {
12059                    return Ok(Expr {
12060                        kind: ExprKind::FuncCall {
12061                            name: "size".to_string(),
12062                            args: vec![],
12063                        },
12064                        line,
12065                    });
12066                }
12067                let a = self.parse_one_arg_or_default()?;
12068                Ok(Expr {
12069                    kind: ExprKind::FuncCall {
12070                        name: "size".to_string(),
12071                        args: vec![a],
12072                    },
12073                    line,
12074                })
12075            }
12076            "list_count" | "list_size" | "count" | "len" | "cnt" => {
12077                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12078                    return Ok(e);
12079                }
12080                if self.pipe_supplies_slurped_list_operand() {
12081                    return Ok(Expr {
12082                        kind: ExprKind::FuncCall {
12083                            name: name.clone(),
12084                            args: vec![],
12085                        },
12086                        line,
12087                    });
12088                }
12089                // `len(EXPR)` / `cnt(EXPR)` / `count(EXPR)` with a tight `(` —
12090                // the parens are function-call syntax, not a parenthesized
12091                // list: stop the argument at `)` so `len(@a) % 2 == 1` is
12092                // `(len(@a)) % 2 == 1`, not `len(@a % 2 == 1)`. Empty parens
12093                // `len()` collapse to a zero-arg call (use the piped operand
12094                // or `$_`). Bare `len` followed by a low-precedence operator
12095                // (`==`, `&&`, `?`, …) also defaults to a zero-arg call so
12096                // `{ len == 0 }` works as a block predicate on the topic.
12097                // Bare `len EXPR` (no parens, e.g. `len @arr`) goes through
12098                // the greedy list-arg parser; this means `len @a + len @b`
12099                // is `len(@a + len(@b))` (returning the length of the sum
12100                // string), not `(len @a) + (len @b)`. Use explicit parens
12101                // when combining `len` with `+`, `-`, comparisons, etc.
12102                let args = if matches!(self.peek(), Token::LParen) {
12103                    self.advance();
12104                    if matches!(self.peek(), Token::RParen) {
12105                        self.advance();
12106                        Vec::new()
12107                    } else {
12108                        let inner = self.parse_expression()?;
12109                        self.expect(&Token::RParen)?;
12110                        vec![inner]
12111                    }
12112                } else if self.peek_is_named_unary_terminator() {
12113                    Vec::new()
12114                } else {
12115                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
12116                    if progress.is_some() {
12117                        return Err(self.syntax_err(
12118                            "`progress =>` is not supported for list_count / list_size / count / cnt",
12119                            line,
12120                        ));
12121                    }
12122                    vec![list]
12123                };
12124                Ok(Expr {
12125                    kind: ExprKind::FuncCall {
12126                        name: name.clone(),
12127                        args,
12128                    },
12129                    line,
12130                })
12131            }
12132            "shuffle" | "shuffled" => {
12133                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12134                    return Ok(e);
12135                }
12136                if self.pipe_supplies_slurped_list_operand() {
12137                    return Ok(Expr {
12138                        kind: ExprKind::FuncCall {
12139                            name: "shuffle".to_string(),
12140                            args: vec![],
12141                        },
12142                        line,
12143                    });
12144                }
12145                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
12146                if progress.is_some() {
12147                    return Err(self.syntax_err("`progress =>` is not supported for shuffle", line));
12148                }
12149                Ok(Expr {
12150                    kind: ExprKind::FuncCall {
12151                        name: "shuffle".to_string(),
12152                        args: vec![list],
12153                    },
12154                    line,
12155                })
12156            }
12157            "chunked" => {
12158                let mut parts = Vec::new();
12159                if self.eat(&Token::LParen) {
12160                    if !matches!(self.peek(), Token::RParen) {
12161                        parts.push(self.parse_assign_expr()?);
12162                        while self.eat(&Token::Comma) {
12163                            if matches!(self.peek(), Token::RParen) {
12164                                break;
12165                            }
12166                            parts.push(self.parse_assign_expr()?);
12167                        }
12168                    }
12169                    self.expect(&Token::RParen)?;
12170                } else {
12171                    // Paren-less `chunked N`: `|>` is a hard terminator, not
12172                    // an operator inside the arg (see
12173                    // `parse_assign_expr_stop_at_pipe`).
12174                    parts.push(self.parse_assign_expr_stop_at_pipe()?);
12175                    loop {
12176                        if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
12177                            break;
12178                        }
12179                        if matches!(
12180                            self.peek(),
12181                            Token::Semicolon
12182                                | Token::RBrace
12183                                | Token::RParen
12184                                | Token::Eof
12185                                | Token::PipeForward
12186                        ) {
12187                            break;
12188                        }
12189                        if self.peek_is_postfix_stmt_modifier_keyword() {
12190                            break;
12191                        }
12192                        parts.push(self.parse_assign_expr_stop_at_pipe()?);
12193                    }
12194                }
12195                if parts.len() == 1 {
12196                    let n = parts.pop().unwrap();
12197                    return Ok(Expr {
12198                        kind: ExprKind::FuncCall {
12199                            name: "chunked".to_string(),
12200                            args: vec![n],
12201                        },
12202                        line,
12203                    });
12204                }
12205                if parts.is_empty() {
12206                    return Ok(Expr {
12207                        kind: ExprKind::FuncCall {
12208                            name: "chunked".to_string(),
12209                            args: parts,
12210                        },
12211                        line,
12212                    });
12213                }
12214                if parts.len() == 2 {
12215                    let n = parts.pop().unwrap();
12216                    let list = parts.pop().unwrap();
12217                    return Ok(Expr {
12218                        kind: ExprKind::FuncCall {
12219                            name: "chunked".to_string(),
12220                            args: vec![list, n],
12221                        },
12222                        line,
12223                    });
12224                }
12225                Err(self.syntax_err(
12226                    "chunked: use LIST |> chunked(N) or chunked((1,2,3), 2)",
12227                    line,
12228                ))
12229            }
12230            "windowed" => {
12231                let mut parts = Vec::new();
12232                if self.eat(&Token::LParen) {
12233                    if !matches!(self.peek(), Token::RParen) {
12234                        parts.push(self.parse_assign_expr()?);
12235                        while self.eat(&Token::Comma) {
12236                            if matches!(self.peek(), Token::RParen) {
12237                                break;
12238                            }
12239                            parts.push(self.parse_assign_expr()?);
12240                        }
12241                    }
12242                    self.expect(&Token::RParen)?;
12243                } else {
12244                    // Paren-less `windowed N`: same `|>`-terminator rule as
12245                    // `chunked` above.
12246                    parts.push(self.parse_assign_expr_stop_at_pipe()?);
12247                    loop {
12248                        if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
12249                            break;
12250                        }
12251                        if matches!(
12252                            self.peek(),
12253                            Token::Semicolon
12254                                | Token::RBrace
12255                                | Token::RParen
12256                                | Token::Eof
12257                                | Token::PipeForward
12258                        ) {
12259                            break;
12260                        }
12261                        if self.peek_is_postfix_stmt_modifier_keyword() {
12262                            break;
12263                        }
12264                        parts.push(self.parse_assign_expr_stop_at_pipe()?);
12265                    }
12266                }
12267                if parts.len() == 1 {
12268                    let n = parts.pop().unwrap();
12269                    return Ok(Expr {
12270                        kind: ExprKind::FuncCall {
12271                            name: "windowed".to_string(),
12272                            args: vec![n],
12273                        },
12274                        line,
12275                    });
12276                }
12277                if parts.is_empty() {
12278                    return Ok(Expr {
12279                        kind: ExprKind::FuncCall {
12280                            name: "windowed".to_string(),
12281                            args: parts,
12282                        },
12283                        line,
12284                    });
12285                }
12286                if parts.len() == 2 {
12287                    let n = parts.pop().unwrap();
12288                    let list = parts.pop().unwrap();
12289                    return Ok(Expr {
12290                        kind: ExprKind::FuncCall {
12291                            name: "windowed".to_string(),
12292                            args: vec![list, n],
12293                        },
12294                        line,
12295                    });
12296                }
12297                Err(self.syntax_err(
12298                    "windowed: use LIST |> windowed(N) or windowed((1,2,3), 2)",
12299                    line,
12300                ))
12301            }
12302            "any" | "all" | "none" => {
12303                // `any(CODEREF, LIST)` with parens — parse as normal call.
12304                if matches!(self.peek(), Token::LParen) {
12305                    self.advance();
12306                    let args = self.parse_arg_list()?;
12307                    self.expect(&Token::RParen)?;
12308                    return Ok(Expr {
12309                        kind: ExprKind::FuncCall {
12310                            name: name.clone(),
12311                            args,
12312                        },
12313                        line,
12314                    });
12315                }
12316                // Coderef-in-block-position: `any $f LIST` / `any $f, LIST` /
12317                // `LIST |> any $f`. Same shape as the block form but uses a
12318                // value expression where `{ BLOCK }` would go.
12319                if let Some(args) = self.try_parse_coderef_listop_args(line)? {
12320                    return Ok(Expr {
12321                        kind: ExprKind::FuncCall {
12322                            name: name.clone(),
12323                            args,
12324                        },
12325                        line,
12326                    });
12327                }
12328                // `any BLOCK LIST` without parens.
12329                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
12330                if progress.is_some() {
12331                    return Err(self.syntax_err(
12332                        "`progress =>` is not supported for any/all/none (use pany for parallel + progress)",
12333                        line,
12334                    ));
12335                }
12336                let cr = Expr {
12337                    kind: ExprKind::CodeRef {
12338                        params: vec![],
12339                        body: block,
12340                    },
12341                    line,
12342                };
12343                Ok(Expr {
12344                    kind: ExprKind::FuncCall {
12345                        name: name.clone(),
12346                        args: vec![cr, list],
12347                    },
12348                    line,
12349                })
12350            }
12351            // Ruby `detect` / `find` — same as `first` (first element matching block).
12352            "first" | "detect" | "find" | "find_index" | "firstidx" | "first_index" => {
12353                let canonical =
12354                    if matches!(name.as_str(), "find_index" | "firstidx" | "first_index") {
12355                        "find_index"
12356                    } else {
12357                        "first"
12358                    };
12359                // `first(CODEREF, LIST)` with parens — parse as normal call.
12360                if matches!(self.peek(), Token::LParen) {
12361                    self.advance();
12362                    let args = self.parse_arg_list()?;
12363                    self.expect(&Token::RParen)?;
12364                    return Ok(Expr {
12365                        kind: ExprKind::FuncCall {
12366                            name: canonical.to_string(),
12367                            args,
12368                        },
12369                        line,
12370                    });
12371                }
12372                // Coderef-in-block-position: `first $f LIST` / `LIST |> first $f`.
12373                if let Some(args) = self.try_parse_coderef_listop_args(line)? {
12374                    return Ok(Expr {
12375                        kind: ExprKind::FuncCall {
12376                            name: canonical.to_string(),
12377                            args,
12378                        },
12379                        line,
12380                    });
12381                }
12382                // `first BLOCK LIST` without parens.
12383                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
12384                if progress.is_some() {
12385                    return Err(self.syntax_err(
12386                        "`progress =>` is not supported for first/detect/find/find_index (use pfirst for parallel + progress)",
12387                        line,
12388                    ));
12389                }
12390                let cr = Expr {
12391                    kind: ExprKind::CodeRef {
12392                        params: vec![],
12393                        body: block,
12394                    },
12395                    line,
12396                };
12397                Ok(Expr {
12398                    kind: ExprKind::FuncCall {
12399                        name: canonical.to_string(),
12400                        args: vec![cr, list],
12401                    },
12402                    line,
12403                })
12404            }
12405            "take_while" | "drop_while" | "skip_while" | "reject" | "grepv" | "tap" | "peek"
12406            | "partition" | "min_by" | "max_by" | "zip_with" | "count_by" => {
12407                // Coderef-in-block-position: `take_while $f LIST` etc.
12408                if let Some(args) = self.try_parse_coderef_listop_args(line)? {
12409                    return Ok(Expr {
12410                        kind: ExprKind::FuncCall {
12411                            name: name.to_string(),
12412                            args,
12413                        },
12414                        line,
12415                    });
12416                }
12417                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
12418                if progress.is_some() {
12419                    return Err(
12420                        self.syntax_err(format!("`progress =>` is not supported for {name}"), line)
12421                    );
12422                }
12423                let cr = Expr {
12424                    kind: ExprKind::CodeRef {
12425                        params: vec![],
12426                        body: block,
12427                    },
12428                    line,
12429                };
12430                Ok(Expr {
12431                    kind: ExprKind::FuncCall {
12432                        name: name.to_string(),
12433                        args: vec![cr, list],
12434                    },
12435                    line,
12436                })
12437            }
12438            "group_by" | "chunk_by" => {
12439                if matches!(self.peek(), Token::LBrace) {
12440                    let (block, list) = self.parse_block_list()?;
12441                    let cr = Expr {
12442                        kind: ExprKind::CodeRef {
12443                            params: vec![],
12444                            body: block,
12445                        },
12446                        line,
12447                    };
12448                    Ok(Expr {
12449                        kind: ExprKind::FuncCall {
12450                            name: name.to_string(),
12451                            args: vec![cr, list],
12452                        },
12453                        line,
12454                    })
12455                } else {
12456                    let key_expr = self.parse_assign_expr()?;
12457                    self.expect(&Token::Comma)?;
12458                    let list_parts = self.parse_list_until_terminator()?;
12459                    let list_expr = if list_parts.len() == 1 {
12460                        list_parts.into_iter().next().unwrap()
12461                    } else {
12462                        Expr {
12463                            kind: ExprKind::List(list_parts),
12464                            line,
12465                        }
12466                    };
12467                    Ok(Expr {
12468                        kind: ExprKind::FuncCall {
12469                            name: name.to_string(),
12470                            args: vec![key_expr, list_expr],
12471                        },
12472                        line,
12473                    })
12474                }
12475            }
12476            "with_index" => {
12477                if self.pipe_supplies_slurped_list_operand() {
12478                    return Ok(Expr {
12479                        kind: ExprKind::FuncCall {
12480                            name: "with_index".to_string(),
12481                            args: vec![],
12482                        },
12483                        line,
12484                    });
12485                }
12486                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
12487                if progress.is_some() {
12488                    return Err(
12489                        self.syntax_err("`progress =>` is not supported for with_index", line)
12490                    );
12491                }
12492                Ok(Expr {
12493                    kind: ExprKind::FuncCall {
12494                        name: "with_index".to_string(),
12495                        args: vec![list],
12496                    },
12497                    line,
12498                })
12499            }
12500            "pcache" => {
12501                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
12502                Ok(Expr {
12503                    kind: ExprKind::PcacheExpr {
12504                        block,
12505                        list: Box::new(list),
12506                        progress: progress.map(Box::new),
12507                    },
12508                    line,
12509                })
12510            }
12511            "pselect" => {
12512                let paren = self.eat(&Token::LParen);
12513                let (receivers, timeout) = self.parse_comma_expr_list_with_timeout_tail(paren)?;
12514                if paren {
12515                    self.expect(&Token::RParen)?;
12516                }
12517                if receivers.is_empty() {
12518                    return Err(self.syntax_err("pselect needs at least one receiver", line));
12519                }
12520                Ok(Expr {
12521                    kind: ExprKind::PselectExpr {
12522                        receivers,
12523                        timeout: timeout.map(Box::new),
12524                    },
12525                    line,
12526                })
12527            }
12528            "open" => {
12529                let paren = matches!(self.peek(), Token::LParen);
12530                if paren {
12531                    self.advance();
12532                }
12533                if matches!(self.peek(), Token::Ident(ref s) if s == "my") {
12534                    self.advance();
12535                    let name = self.parse_scalar_var_name()?;
12536                    self.expect(&Token::Comma)?;
12537                    let mode = self.parse_assign_expr()?;
12538                    let file = if self.eat(&Token::Comma) {
12539                        Some(self.parse_assign_expr()?)
12540                    } else {
12541                        None
12542                    };
12543                    if paren {
12544                        self.expect(&Token::RParen)?;
12545                    }
12546                    Ok(Expr {
12547                        kind: ExprKind::Open {
12548                            handle: Box::new(Expr {
12549                                kind: ExprKind::OpenMyHandle { name },
12550                                line,
12551                            }),
12552                            mode: Box::new(mode),
12553                            file: file.map(Box::new),
12554                        },
12555                        line,
12556                    })
12557                } else {
12558                    // Perl convention: `open FH, "<", path` — an all-uppercase
12559                    // (or `_`) bareword in the filehandle slot is a literal
12560                    // handle name, never a constant / sub / bareword
12561                    // expression. Without this special-case, registered
12562                    // constants like `PI` / `TAU` / `E` would override the
12563                    // documented Perl idiom and the handle would register
12564                    // under the constant's numeric value.
12565                    let handle_lit = self.take_bareword_filehandle();
12566                    if handle_lit.is_some() {
12567                        // Consume the comma after the bareword filehandle so
12568                        // the arg parser starts at the mode expression.
12569                        self.expect(&Token::Comma)?;
12570                    }
12571                    let args = if paren {
12572                        self.parse_arg_list()?
12573                    } else {
12574                        self.parse_list_until_terminator()?
12575                    };
12576                    if paren {
12577                        self.expect(&Token::RParen)?;
12578                    }
12579                    let total = handle_lit.is_some() as usize + args.len();
12580                    if total < 2 {
12581                        return Err(self.syntax_err("open requires at least 2 arguments", line));
12582                    }
12583                    let (handle_expr, mode_expr, file_expr) = match handle_lit {
12584                        Some(name) => {
12585                            let h = Expr {
12586                                kind: ExprKind::String(name),
12587                                line,
12588                            };
12589                            (h, args[0].clone(), args.get(1).cloned())
12590                        }
12591                        None => (args[0].clone(), args[1].clone(), args.get(2).cloned()),
12592                    };
12593                    Ok(Expr {
12594                        kind: ExprKind::Open {
12595                            handle: Box::new(handle_expr),
12596                            mode: Box::new(mode_expr),
12597                            file: file_expr.map(Box::new),
12598                        },
12599                        line,
12600                    })
12601                }
12602            }
12603            "close" => {
12604                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12605                    return Ok(e);
12606                }
12607                // `close FH` — bareword filehandle slot takes a literal name.
12608                let a = self
12609                    .take_bareword_filehandle_arg(line)
12610                    .map(Ok)
12611                    .unwrap_or_else(|| self.parse_one_arg_or_default())?;
12612                Ok(Expr {
12613                    kind: ExprKind::Close(Box::new(a)),
12614                    line,
12615                })
12616            }
12617            "opendir" => {
12618                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12619                    return Ok(e);
12620                }
12621                let args = self.parse_builtin_args()?;
12622                if args.len() != 2 {
12623                    return Err(self.syntax_err("opendir requires two arguments", line));
12624                }
12625                Ok(Expr {
12626                    kind: ExprKind::Opendir {
12627                        handle: Box::new(args[0].clone()),
12628                        path: Box::new(args[1].clone()),
12629                    },
12630                    line,
12631                })
12632            }
12633            "readdir" => {
12634                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12635                    return Ok(e);
12636                }
12637                let a = self.parse_one_arg()?;
12638                Ok(Expr {
12639                    kind: ExprKind::Readdir(Box::new(a)),
12640                    line,
12641                })
12642            }
12643            "closedir" => {
12644                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12645                    return Ok(e);
12646                }
12647                let a = self.parse_one_arg()?;
12648                Ok(Expr {
12649                    kind: ExprKind::Closedir(Box::new(a)),
12650                    line,
12651                })
12652            }
12653            "rewinddir" => {
12654                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12655                    return Ok(e);
12656                }
12657                let a = self.parse_one_arg()?;
12658                Ok(Expr {
12659                    kind: ExprKind::Rewinddir(Box::new(a)),
12660                    line,
12661                })
12662            }
12663            "telldir" => {
12664                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12665                    return Ok(e);
12666                }
12667                let a = self.parse_one_arg()?;
12668                Ok(Expr {
12669                    kind: ExprKind::Telldir(Box::new(a)),
12670                    line,
12671                })
12672            }
12673            "seekdir" => {
12674                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12675                    return Ok(e);
12676                }
12677                let args = self.parse_builtin_args()?;
12678                if args.len() != 2 {
12679                    return Err(self.syntax_err("seekdir requires two arguments", line));
12680                }
12681                Ok(Expr {
12682                    kind: ExprKind::Seekdir {
12683                        handle: Box::new(args[0].clone()),
12684                        position: Box::new(args[1].clone()),
12685                    },
12686                    line,
12687                })
12688            }
12689            "eof" => {
12690                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12691                    return Ok(e);
12692                }
12693                // `eof FH` — bareword filehandle slot (no parens) takes a
12694                // literal name. `eof(FH)` / `eof($fh)` / `eof("FH")` keep
12695                // their general-expression handling.
12696                if let Some(a) = self.take_bareword_filehandle_arg(line) {
12697                    return Ok(Expr {
12698                        kind: ExprKind::Eof(Some(Box::new(a))),
12699                        line,
12700                    });
12701                }
12702                if matches!(self.peek(), Token::LParen) {
12703                    self.advance();
12704                    if matches!(self.peek(), Token::RParen) {
12705                        self.advance();
12706                        Ok(Expr {
12707                            kind: ExprKind::Eof(None),
12708                            line,
12709                        })
12710                    } else {
12711                        // Inside the parens, bareword still wins as a handle.
12712                        let a = self
12713                            .take_bareword_filehandle_arg(line)
12714                            .map(Ok)
12715                            .unwrap_or_else(|| self.parse_expression())?;
12716                        self.expect(&Token::RParen)?;
12717                        Ok(Expr {
12718                            kind: ExprKind::Eof(Some(Box::new(a))),
12719                            line,
12720                        })
12721                    }
12722                } else {
12723                    Ok(Expr {
12724                        kind: ExprKind::Eof(None),
12725                        line,
12726                    })
12727                }
12728            }
12729            "system" => {
12730                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12731                    return Ok(e);
12732                }
12733                let args = self.parse_builtin_args()?;
12734                Ok(Expr {
12735                    kind: ExprKind::System(args),
12736                    line,
12737                })
12738            }
12739            "exec" => {
12740                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12741                    return Ok(e);
12742                }
12743                let args = self.parse_builtin_args()?;
12744                Ok(Expr {
12745                    kind: ExprKind::Exec(args),
12746                    line,
12747                })
12748            }
12749            "eval" => {
12750                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12751                    return Ok(e);
12752                }
12753                let a = if matches!(self.peek(), Token::LBrace) {
12754                    let block = self.parse_block()?;
12755                    Expr {
12756                        kind: ExprKind::CodeRef {
12757                            params: vec![],
12758                            body: block,
12759                        },
12760                        line,
12761                    }
12762                } else {
12763                    self.parse_one_arg_or_default()?
12764                };
12765                Ok(Expr {
12766                    kind: ExprKind::Eval(Box::new(a)),
12767                    line,
12768                })
12769            }
12770            "do" => {
12771                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12772                    return Ok(e);
12773                }
12774                let a = self.parse_one_arg()?;
12775                Ok(Expr {
12776                    kind: ExprKind::Do(Box::new(a)),
12777                    line,
12778                })
12779            }
12780            "require" => {
12781                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12782                    return Ok(e);
12783                }
12784                let a = self.parse_one_arg()?;
12785                Ok(Expr {
12786                    kind: ExprKind::Require(Box::new(a)),
12787                    line,
12788                })
12789            }
12790            "exit" => {
12791                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12792                    return Ok(e);
12793                }
12794                if matches!(
12795                    self.peek(),
12796                    Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
12797                ) {
12798                    Ok(Expr {
12799                        kind: ExprKind::Exit(None),
12800                        line,
12801                    })
12802                } else {
12803                    let a = self.parse_one_arg()?;
12804                    Ok(Expr {
12805                        kind: ExprKind::Exit(Some(Box::new(a))),
12806                        line,
12807                    })
12808                }
12809            }
12810            "chdir" => {
12811                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12812                    return Ok(e);
12813                }
12814                let a = self.parse_one_arg_or_default()?;
12815                Ok(Expr {
12816                    kind: ExprKind::Chdir(Box::new(a)),
12817                    line,
12818                })
12819            }
12820            "mkdir" => {
12821                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12822                    return Ok(e);
12823                }
12824                let args = self.parse_builtin_args()?;
12825                Ok(Expr {
12826                    kind: ExprKind::Mkdir {
12827                        path: Box::new(args[0].clone()),
12828                        mode: args.get(1).cloned().map(Box::new),
12829                    },
12830                    line,
12831                })
12832            }
12833            "unlink" | "rm" => {
12834                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12835                    return Ok(e);
12836                }
12837                let args = self.parse_builtin_args()?;
12838                Ok(Expr {
12839                    kind: ExprKind::Unlink(args),
12840                    line,
12841                })
12842            }
12843            "rename" => {
12844                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12845                    return Ok(e);
12846                }
12847                let args = self.parse_builtin_args()?;
12848                if args.len() != 2 {
12849                    return Err(self.syntax_err("rename requires two arguments", line));
12850                }
12851                Ok(Expr {
12852                    kind: ExprKind::Rename {
12853                        old: Box::new(args[0].clone()),
12854                        new: Box::new(args[1].clone()),
12855                    },
12856                    line,
12857                })
12858            }
12859            "chmod" => {
12860                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12861                    return Ok(e);
12862                }
12863                let args = self.parse_builtin_args()?;
12864                if args.len() < 2 {
12865                    return Err(self.syntax_err("chmod requires mode and at least one file", line));
12866                }
12867                Ok(Expr {
12868                    kind: ExprKind::Chmod(args),
12869                    line,
12870                })
12871            }
12872            "chown" => {
12873                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12874                    return Ok(e);
12875                }
12876                let args = self.parse_builtin_args()?;
12877                if args.len() < 3 {
12878                    return Err(
12879                        self.syntax_err("chown requires uid, gid, and at least one file", line)
12880                    );
12881                }
12882                Ok(Expr {
12883                    kind: ExprKind::Chown(args),
12884                    line,
12885                })
12886            }
12887            "stat" => {
12888                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12889                    return Ok(e);
12890                }
12891                let args = self.parse_builtin_args()?;
12892                let arg = if args.len() == 1 {
12893                    args[0].clone()
12894                } else if args.is_empty() {
12895                    Expr {
12896                        kind: ExprKind::ScalarVar("_".into()),
12897                        line,
12898                    }
12899                } else {
12900                    return Err(self.syntax_err("stat requires zero or one argument", line));
12901                };
12902                Ok(Expr {
12903                    kind: ExprKind::Stat(Box::new(arg)),
12904                    line,
12905                })
12906            }
12907            "lstat" => {
12908                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12909                    return Ok(e);
12910                }
12911                let args = self.parse_builtin_args()?;
12912                let arg = if args.len() == 1 {
12913                    args[0].clone()
12914                } else if args.is_empty() {
12915                    Expr {
12916                        kind: ExprKind::ScalarVar("_".into()),
12917                        line,
12918                    }
12919                } else {
12920                    return Err(self.syntax_err("lstat requires zero or one argument", line));
12921                };
12922                Ok(Expr {
12923                    kind: ExprKind::Lstat(Box::new(arg)),
12924                    line,
12925                })
12926            }
12927            "link" => {
12928                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12929                    return Ok(e);
12930                }
12931                let args = self.parse_builtin_args()?;
12932                if args.len() != 2 {
12933                    return Err(self.syntax_err("link requires two arguments", line));
12934                }
12935                Ok(Expr {
12936                    kind: ExprKind::Link {
12937                        old: Box::new(args[0].clone()),
12938                        new: Box::new(args[1].clone()),
12939                    },
12940                    line,
12941                })
12942            }
12943            "symlink" => {
12944                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12945                    return Ok(e);
12946                }
12947                let args = self.parse_builtin_args()?;
12948                if args.len() != 2 {
12949                    return Err(self.syntax_err("symlink requires two arguments", line));
12950                }
12951                Ok(Expr {
12952                    kind: ExprKind::Symlink {
12953                        old: Box::new(args[0].clone()),
12954                        new: Box::new(args[1].clone()),
12955                    },
12956                    line,
12957                })
12958            }
12959            "readlink" => {
12960                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12961                    return Ok(e);
12962                }
12963                let args = self.parse_builtin_args()?;
12964                let arg = if args.len() == 1 {
12965                    args[0].clone()
12966                } else if args.is_empty() {
12967                    Expr {
12968                        kind: ExprKind::ScalarVar("_".into()),
12969                        line,
12970                    }
12971                } else {
12972                    return Err(self.syntax_err("readlink requires zero or one argument", line));
12973                };
12974                Ok(Expr {
12975                    kind: ExprKind::Readlink(Box::new(arg)),
12976                    line,
12977                })
12978            }
12979            "files" => {
12980                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12981                    return Ok(e);
12982                }
12983                let args = self.parse_builtin_args()?;
12984                Ok(Expr {
12985                    kind: ExprKind::Files(args),
12986                    line,
12987                })
12988            }
12989            "filesf" | "f" => {
12990                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12991                    return Ok(e);
12992                }
12993                let args = self.parse_builtin_args()?;
12994                Ok(Expr {
12995                    kind: ExprKind::Filesf(args),
12996                    line,
12997                })
12998            }
12999            "fr" => {
13000                let args = self.parse_builtin_args()?;
13001                Ok(Expr {
13002                    kind: ExprKind::FilesfRecursive(args),
13003                    line,
13004                })
13005            }
13006            "dirs" | "d" => {
13007                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13008                    return Ok(e);
13009                }
13010                let args = self.parse_builtin_args()?;
13011                Ok(Expr {
13012                    kind: ExprKind::Dirs(args),
13013                    line,
13014                })
13015            }
13016            "dr" => {
13017                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13018                    return Ok(e);
13019                }
13020                let args = self.parse_builtin_args()?;
13021                Ok(Expr {
13022                    kind: ExprKind::DirsRecursive(args),
13023                    line,
13024                })
13025            }
13026            "sym_links" => {
13027                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13028                    return Ok(e);
13029                }
13030                let args = self.parse_builtin_args()?;
13031                Ok(Expr {
13032                    kind: ExprKind::SymLinks(args),
13033                    line,
13034                })
13035            }
13036            "sockets" => {
13037                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13038                    return Ok(e);
13039                }
13040                let args = self.parse_builtin_args()?;
13041                Ok(Expr {
13042                    kind: ExprKind::Sockets(args),
13043                    line,
13044                })
13045            }
13046            "pipes" => {
13047                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13048                    return Ok(e);
13049                }
13050                let args = self.parse_builtin_args()?;
13051                Ok(Expr {
13052                    kind: ExprKind::Pipes(args),
13053                    line,
13054                })
13055            }
13056            "block_devices" => {
13057                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13058                    return Ok(e);
13059                }
13060                let args = self.parse_builtin_args()?;
13061                Ok(Expr {
13062                    kind: ExprKind::BlockDevices(args),
13063                    line,
13064                })
13065            }
13066            "char_devices" => {
13067                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13068                    return Ok(e);
13069                }
13070                let args = self.parse_builtin_args()?;
13071                Ok(Expr {
13072                    kind: ExprKind::CharDevices(args),
13073                    line,
13074                })
13075            }
13076            "exe" | "executables" => {
13077                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13078                    return Ok(e);
13079                }
13080                let args = self.parse_builtin_args()?;
13081                Ok(Expr {
13082                    kind: ExprKind::Executables(args),
13083                    line,
13084                })
13085            }
13086            "glob" => {
13087                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13088                    return Ok(e);
13089                }
13090                let args = self.parse_builtin_args()?;
13091                Ok(Expr {
13092                    kind: ExprKind::Glob(args),
13093                    line,
13094                })
13095            }
13096            "glob_par" => {
13097                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13098                    return Ok(e);
13099                }
13100                let (args, progress) = self.parse_glob_par_or_par_sed_args()?;
13101                Ok(Expr {
13102                    kind: ExprKind::GlobPar { args, progress },
13103                    line,
13104                })
13105            }
13106            "par_sed" => {
13107                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13108                    return Ok(e);
13109                }
13110                let (args, progress) = self.parse_glob_par_or_par_sed_args()?;
13111                Ok(Expr {
13112                    kind: ExprKind::ParSed { args, progress },
13113                    line,
13114                })
13115            }
13116            "bless" => {
13117                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13118                    return Ok(e);
13119                }
13120                let args = self.parse_builtin_args()?;
13121                Ok(Expr {
13122                    kind: ExprKind::Bless {
13123                        ref_expr: Box::new(args[0].clone()),
13124                        class: args.get(1).cloned().map(Box::new),
13125                    },
13126                    line,
13127                })
13128            }
13129            "caller" => {
13130                if matches!(self.peek(), Token::LParen) {
13131                    self.advance();
13132                    if matches!(self.peek(), Token::RParen) {
13133                        self.advance();
13134                        Ok(Expr {
13135                            kind: ExprKind::Caller(None),
13136                            line,
13137                        })
13138                    } else {
13139                        let a = self.parse_expression()?;
13140                        self.expect(&Token::RParen)?;
13141                        Ok(Expr {
13142                            kind: ExprKind::Caller(Some(Box::new(a))),
13143                            line,
13144                        })
13145                    }
13146                } else {
13147                    Ok(Expr {
13148                        kind: ExprKind::Caller(None),
13149                        line,
13150                    })
13151                }
13152            }
13153            "wantarray" => {
13154                if crate::no_interop_mode() {
13155                    return Err(self.syntax_err(
13156                        "stryke `wantarray` is rejected under --no-interop — \
13157                         use explicit return-shape (`@result` vs `$scalar`) \
13158                         or pass a flag arg instead of context-sniffing",
13159                        line,
13160                    ));
13161                }
13162                if matches!(self.peek(), Token::LParen) {
13163                    self.advance();
13164                    self.expect(&Token::RParen)?;
13165                }
13166                Ok(Expr {
13167                    kind: ExprKind::Wantarray,
13168                    line,
13169                })
13170            }
13171            "sub" => {
13172                // In no-interop mode, `sub {}` is not valid — must use `fn {}`
13173                if crate::no_interop_mode() {
13174                    return Err(self.syntax_err(
13175                        "stryke uses `fn {}` instead of `sub {}` (--no-interop)",
13176                        line,
13177                    ));
13178                }
13179                // Anonymous sub — optional prototype `sub () { }` (e.g. Carp.pm `*X = sub () { 1 }`)
13180                let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
13181                let body = self.parse_block()?;
13182                Ok(Expr {
13183                    kind: ExprKind::CodeRef { params, body },
13184                    line,
13185                })
13186            }
13187            "fn" => {
13188                // Anonymous fn — stryke syntax for anonymous subroutines
13189                let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
13190                self.parse_sub_attributes()?;
13191                let body = self.parse_fn_eq_body_or_block(false)?;
13192                Ok(Expr {
13193                    kind: ExprKind::CodeRef { params, body },
13194                    line,
13195                })
13196            }
13197            _ => {
13198                // Generic function call
13199                // Check for fat arrow (bareword string in hash) — except for
13200                // topic-slot barewords (`_`, `_<`, `_0`, `_0<`, …), which must
13201                // resolve to the topic value, not the literal name.
13202                if matches!(self.peek(), Token::FatArrow) && !Self::is_underscore_topic_slot(&name)
13203                {
13204                    return Ok(Expr {
13205                        kind: ExprKind::String(name),
13206                        line,
13207                    });
13208                }
13209                // Bare `_` in expression position → topic variable `$_`.
13210                // Allows concise blocks: `map { _ * 2 }`, `fi { _ > 5 }`.
13211                // Also handles the outer-topic chain: `_<`, `_<<`, `_<<<`,
13212                // `_<<<<` for 1..4 frames up — and the positional matrix:
13213                // `_0<<<<`, `_1<<<<`, `_N<<<<` (N positionals × 5 levels).
13214                // `_0` is canonically aliased to `_` at every level (see
13215                // `Scope::set_closure_args`).
13216                //
13217                // Stryke string-index sugar: `_[N]` (bareword, no sigil) is
13218                // an alias for `_!N!` — char-of-topic substring. The sigil
13219                // form `$_[N]` keeps Perl's `@_`-access semantics (first
13220                // positional arg). We dispatch here, before the generic
13221                // ArrayElement path, so the AST for `_[N]` carries the
13222                // synthetic `__topicstr__$NAME` flag the interpreter / VM
13223                // strip and route to char-of-string.
13224                if Self::is_underscore_topic_slot(&name) {
13225                    if matches!(self.peek(), Token::LBracket) && self.peek_line() == line {
13226                        self.advance(); // [
13227                        let index = self.parse_expression()?;
13228                        self.expect(&Token::RBracket)?;
13229                        return Ok(Expr {
13230                            kind: ExprKind::ArrayElement {
13231                                array: format!("__topicstr__{}", name),
13232                                index: Box::new(index),
13233                            },
13234                            line,
13235                        });
13236                    }
13237                    return Ok(Expr {
13238                        kind: ExprKind::ScalarVar(name.clone()),
13239                        line,
13240                    });
13241                }
13242                // Function call with optional parens
13243                if matches!(self.peek(), Token::LParen) {
13244                    self.advance();
13245                    let args = self.parse_arg_list()?;
13246                    self.expect(&Token::RParen)?;
13247                    Ok(Expr {
13248                        kind: ExprKind::FuncCall { name, args },
13249                        line,
13250                    })
13251                } else if self.peek().is_term_start()
13252                    && !(matches!(self.peek(), Token::Ident(ref kw) if kw == "sub")
13253                        && matches!(self.peek_at(1), Token::Ident(_)))
13254                    && !(self.suppress_parenless_call > 0 && matches!(self.peek(), Token::Ident(_)))
13255                    && !(matches!(self.peek(), Token::LBrace)
13256                        && self.peek_line() > self.prev_line())
13257                    && !(matches!(self.peek(), Token::BitNot)
13258                        && self.suppress_tilde_range == 0
13259                        && matches!(
13260                            self.peek_at(1),
13261                            Token::Ident(_) | Token::Integer(_) | Token::Float(_)
13262                        ))
13263                {
13264                    // Perl allows func arg without parens
13265                    // Guard: `sub <name> { }` is a named sub declaration (new
13266                    // statement), not an argument to the preceding call.
13267                    // Guard: suppress_parenless_call > 0 with Ident prevents consuming
13268                    // barewords (used by thread macro so `t Color::Red p` treats
13269                    // `p` as a stage, not an argument to the enum variant), but
13270                    // still allows `{` for struct/hash literals like `t Foo { x => 1 } p`.
13271                    // Guard: `{` on a new line is a new statement (hashref/block),
13272                    // not an argument to the preceding bareword call.
13273                    // Guard: `~Ident` / `~Integer` / `~Float` after a bareword is
13274                    // the universal-tilde range separator (`I~M~5`, `Mon~Fri`,
13275                    // `Jan~Dec~2`), not unary BitNot of an arg. Bail to Bareword
13276                    // so the outer `parse_range` consumes `~` as the range op.
13277                    let args = self.parse_list_until_terminator()?;
13278                    Ok(Expr {
13279                        kind: ExprKind::FuncCall { name, args },
13280                        line,
13281                    })
13282                } else {
13283                    // No parens, no visible arguments — emit a Bareword.
13284                    // At runtime, Bareword tries sub resolution first (zero-arg
13285                    // call) and falls back to a string value.  stryke extension
13286                    // contexts (pipe-forward, map/fore) lift Bareword → FuncCall
13287                    // with `$_` injection separately.
13288                    Ok(Expr {
13289                        kind: ExprKind::Bareword(name),
13290                        line,
13291                    })
13292                }
13293            }
13294        }
13295    }
13296
13297    /// `open FH, ...` / `close FH` / `eof FH` — Perl's convention is that an
13298    /// all-uppercase (letters / digits / `_`, starting with a letter or `_`)
13299    /// bareword in the filehandle slot is a literal handle name, never a
13300    /// constant or sub call. This shadows registered constants like `PI`,
13301    /// `TAU`, `E` and the rare uppercase-letter filehandles (`H`, `O`, …)
13302    /// that would otherwise route through the bareword resolver.
13303    ///
13304    /// Returns `Some(name)` when the next token is such a bareword and the
13305    /// token after it is one of the accepted terminators (any of `accept`,
13306    /// or — when `accept` is empty — any of `,`, `;`, `)`, `}`, `|>`, Eof).
13307    /// Otherwise returns `None` and leaves the cursor untouched.
13308    fn take_bareword_filehandle_if(&mut self, accept: &[Token]) -> Option<String> {
13309        let Token::Ident(h) = self.peek().clone() else {
13310            return None;
13311        };
13312        // `main::STDOUT` is the qualified form of `STDOUT` — `main` is
13313        // the default package for special filehandles per Perl. The
13314        // lexer emits this as three tokens (Ident("main"), PackageSep,
13315        // Ident("STDOUT")), so detect the trio and consume the prefix
13316        // up front, then check the leaf as a normal uppercase handle.
13317        let saved = self.pos;
13318        let qualified = h == "main"
13319            && matches!(self.peek_at(1), Token::PackageSep)
13320            && matches!(self.peek_at(2), Token::Ident(s) if !s.is_empty());
13321        if qualified {
13322            self.advance(); // main
13323            self.advance(); // ::
13324        }
13325        let leaf = match self.peek().clone() {
13326            Token::Ident(s) => s,
13327            _ => {
13328                self.pos = saved;
13329                return None;
13330            }
13331        };
13332        let mut chars = leaf.chars();
13333        let first = match chars.next() {
13334            Some(c) => c,
13335            None => {
13336                self.pos = saved;
13337                return None;
13338            }
13339        };
13340        if !(first.is_ascii_uppercase() || first == '_') {
13341            self.pos = saved;
13342            return None;
13343        }
13344        if !leaf
13345            .chars()
13346            .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')
13347        {
13348            self.pos = saved;
13349            return None;
13350        }
13351        let next = self.peek_at(1);
13352        let ok = if accept.is_empty() {
13353            matches!(
13354                next,
13355                Token::Comma
13356                    | Token::Semicolon
13357                    | Token::RParen
13358                    | Token::RBrace
13359                    | Token::Eof
13360                    | Token::PipeForward
13361            )
13362        } else {
13363            accept
13364                .iter()
13365                .any(|t| std::mem::discriminant(t) == std::mem::discriminant(next))
13366        };
13367        if !ok {
13368            self.pos = saved;
13369            return None;
13370        }
13371        self.advance();
13372        Some(leaf)
13373    }
13374
13375    /// `open FH, …` — bareword filehandle followed by a comma.
13376    fn take_bareword_filehandle(&mut self) -> Option<String> {
13377        self.take_bareword_filehandle_if(&[Token::Comma])
13378    }
13379
13380    /// `close FH` / `eof FH` — bareword filehandle followed by a statement
13381    /// terminator. Returns a `String` expression to splice into the arg
13382    /// slot, or `None` if the next token isn't a literal filehandle.
13383    fn take_bareword_filehandle_arg(&mut self, line: usize) -> Option<Expr> {
13384        self.take_bareword_filehandle_if(&[]).map(|name| Expr {
13385            kind: ExprKind::String(name),
13386            line,
13387        })
13388    }
13389
13390    fn parse_print_like(
13391        &mut self,
13392        make: impl FnOnce(Option<String>, Vec<Expr>) -> ExprKind,
13393    ) -> StrykeResult<Expr> {
13394        let line = self.peek_line();
13395        // Check for filehandle: print STDERR "msg"  /  print $fh "msg"
13396        let handle = if let Token::Ident(ref h) = self.peek().clone() {
13397            // `print main::STDERR "msg"` — `main` is the default package
13398            // for special filehandles per Perl. The lexer emits
13399            // `main::STDOUT` as three tokens (Ident("main"), PackageSep,
13400            // Ident("STDOUT")), so peek + lookahead for the qualified
13401            // form and consume the `main::` prefix up front so the
13402            // existing uppercase-bareword path picks up the bare leaf.
13403            if h == "main"
13404                && matches!(self.peek_at(1), Token::PackageSep)
13405                && matches!(
13406                    self.peek_at(2),
13407                    Token::Ident(s) if !s.is_empty()
13408                        && s.chars().all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')
13409                        && s.chars().next().is_some_and(|c| c.is_ascii_uppercase() || c == '_'))
13410            {
13411                self.advance(); // main
13412                self.advance(); // ::
13413            }
13414            let h = match self.peek().clone() {
13415                Token::Ident(s) => s,
13416                _ => h.clone(),
13417            };
13418            if h.chars().all(|c| c.is_uppercase() || c == '_')
13419                && !matches!(self.peek(), Token::LParen)
13420            {
13421                let saved = self.pos;
13422                self.advance();
13423                // Verify next token is a term start (not operator).
13424                // Guard: `~Ident` / `~Integer` / `~Float` is a universal-tilde
13425                // range separator (`p I~M~5`, `p Mon~Fri`), not unary BitNot of
13426                // an arg. Bail filehandle detection so the bareword `I` flows
13427                // into the regular expression path where `parse_range` consumes
13428                // `~` as the range op.
13429                let is_tilde_range_after = matches!(self.peek(), Token::BitNot)
13430                    && self.suppress_tilde_range == 0
13431                    && matches!(
13432                        self.peek_at(1),
13433                        Token::Ident(_) | Token::Integer(_) | Token::Float(_)
13434                    );
13435                if !is_tilde_range_after
13436                    && (self.peek().is_term_start()
13437                        || matches!(
13438                            self.peek(),
13439                            Token::DoubleString(_)
13440                                | Token::BacktickString(_)
13441                                | Token::SingleString(_)
13442                        ))
13443                {
13444                    Some(h)
13445                } else {
13446                    self.pos = saved;
13447                    None
13448                }
13449            } else {
13450                None
13451            }
13452        } else if let Token::ScalarVar(ref v) = self.peek().clone() {
13453            // `print $fh "msg"` — scalar variable as indirect filehandle.
13454            // Treat as handle when the next token (after $var) is a term-start or
13455            // string literal *without* a preceding comma/operator, matching Perl's
13456            // indirect-object heuristic.
13457            // Exclude `$_` — it's virtually always the topic variable, not a handle.
13458            // Exclude `[` and `{` — those are array/hash subscripts on the variable
13459            // itself (`print $F[0]`, `print $h{k}`), not separate print arguments.
13460            // Exclude `(` — `$f(...)` is a coderef call expression (`p $f(5)` is
13461            // `p( $f(5) )`, not `print $f ARGS`). Without this guard, calling
13462            // stored coderefs in print/say/printf/p position required parens
13463            // wrappers (`p ( $f(5) )`) or a temporary scalar.
13464            // Exclude statement modifiers (`if`/`unless`/`while`/`until`/`for`/`foreach`)
13465            // — `print $_ if COND` prints `$_` to STDOUT, not to a handle named `$_`.
13466            // Exclude tokens on a later line — a newline ends the print statement
13467            // in stryke, so `p $j\nmy $k = …` must not absorb the following `my`.
13468            let v = v.clone();
13469            if v == "_" || matches!(self.peek_at(1), Token::LParen) {
13470                None
13471            } else {
13472                let saved = self.pos;
13473                let var_line = self.peek_line();
13474                self.advance();
13475                let next = self.peek().clone();
13476                let next_line = self.peek_line();
13477                let is_stmt_modifier = matches!(&next, Token::Ident(kw)
13478                    if matches!(kw.as_str(), "if" | "unless" | "while" | "until" | "for" | "foreach"));
13479                if !is_stmt_modifier
13480                    && next_line == var_line
13481                    && !matches!(next, Token::LBracket | Token::LBrace)
13482                    && (next.is_term_start()
13483                        || matches!(
13484                            next,
13485                            Token::DoubleString(_)
13486                                | Token::BacktickString(_)
13487                                | Token::SingleString(_)
13488                        ))
13489                {
13490                    // Next token looks like a print argument — $var is the handle.
13491                    Some(format!("${v}"))
13492                } else {
13493                    self.pos = saved;
13494                    None
13495                }
13496            }
13497        } else {
13498            None
13499        };
13500        // `print()` / `say()` / `printf()` — empty parens default to `$_`,
13501        // matching Perl 5: `perldoc -f print` / `-f say` say "If no arguments
13502        // are given, prints $_." (Same convention as the topic-default unary
13503        // builtins handled in `parse_one_arg_or_default`.)
13504        // Use `parse_list_until_terminator_allow_pipe` so that `p @a |> sum`
13505        // parses as `p(sum(@a))`, matching `~>` thread-first behavior.
13506        let args =
13507            if matches!(self.peek(), Token::LParen) && matches!(self.peek_at(1), Token::RParen) {
13508                let line_topic = self.peek_line();
13509                self.advance(); // (
13510                self.advance(); // )
13511                vec![Expr {
13512                    kind: ExprKind::ScalarVar("_".into()),
13513                    line: line_topic,
13514                }]
13515            } else {
13516                self.parse_list_until_terminator_allow_pipe()?
13517            };
13518        Ok(Expr {
13519            kind: make(handle, args),
13520            line,
13521        })
13522    }
13523
13524    fn parse_block_list(&mut self) -> StrykeResult<(Block, Expr)> {
13525        let block = self.parse_block()?;
13526        let block_end_line = self.prev_line();
13527        self.eat(&Token::Comma);
13528        // On the RHS of `|>`, the list operand is supplied by the piped LHS
13529        // and will be substituted at desugar time — accept a placeholder when
13530        // we're at a terminator here or on a new line (implicit semicolon).
13531        if self.in_pipe_rhs()
13532            && (matches!(
13533                self.peek(),
13534                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
13535            ) || self.peek_line() > block_end_line)
13536        {
13537            let line = self.peek_line();
13538            return Ok((block, self.pipe_placeholder_list(line)));
13539        }
13540        let list = self.parse_expression()?;
13541        Ok((block, list))
13542    }
13543
13544    /// Comma-separated expressions with optional trailing `timeout => SECS` (for `pselect`).
13545    /// When `paren` is true, stops at `)` as well as normal terminators.
13546    fn parse_comma_expr_list_with_timeout_tail(
13547        &mut self,
13548        paren: bool,
13549    ) -> StrykeResult<(Vec<Expr>, Option<Expr>)> {
13550        let mut parts = vec![self.parse_assign_expr()?];
13551        loop {
13552            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
13553                break;
13554            }
13555            if paren && matches!(self.peek(), Token::RParen) {
13556                break;
13557            }
13558            if matches!(
13559                self.peek(),
13560                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
13561            ) {
13562                break;
13563            }
13564            if self.peek_is_postfix_stmt_modifier_keyword() {
13565                break;
13566            }
13567            if let Token::Ident(ref kw) = self.peek().clone() {
13568                if kw == "timeout" && matches!(self.peek_at(1), Token::FatArrow) {
13569                    self.advance();
13570                    self.expect(&Token::FatArrow)?;
13571                    let t = self.parse_assign_expr()?;
13572                    return Ok((parts, Some(t)));
13573                }
13574            }
13575            parts.push(self.parse_assign_expr()?);
13576        }
13577        Ok((parts, None))
13578    }
13579
13580    /// `preduce_init EXPR, BLOCK, LIST` with optional `, progress => EXPR`.
13581    fn parse_init_block_then_list_optional_progress(
13582        &mut self,
13583    ) -> StrykeResult<(Expr, Block, Expr, Option<Expr>)> {
13584        let init = self.parse_assign_expr()?;
13585        self.expect(&Token::Comma)?;
13586        let block = self.parse_block_or_bareword_block()?;
13587        self.eat(&Token::Comma);
13588        let line = self.peek_line();
13589        if let Token::Ident(ref kw) = self.peek().clone() {
13590            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
13591                self.advance();
13592                self.expect(&Token::FatArrow)?;
13593                let prog = self.parse_assign_expr()?;
13594                return Ok((
13595                    init,
13596                    block,
13597                    Expr {
13598                        kind: ExprKind::List(vec![]),
13599                        line,
13600                    },
13601                    Some(prog),
13602                ));
13603            }
13604        }
13605        if matches!(
13606            self.peek(),
13607            Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
13608        ) {
13609            return Ok((
13610                init,
13611                block,
13612                Expr {
13613                    kind: ExprKind::List(vec![]),
13614                    line,
13615                },
13616                None,
13617            ));
13618        }
13619        let mut parts = vec![self.parse_assign_expr()?];
13620        loop {
13621            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
13622                break;
13623            }
13624            if matches!(
13625                self.peek(),
13626                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
13627            ) {
13628                break;
13629            }
13630            if self.peek_is_postfix_stmt_modifier_keyword() {
13631                break;
13632            }
13633            if let Token::Ident(ref kw) = self.peek().clone() {
13634                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
13635                    self.advance();
13636                    self.expect(&Token::FatArrow)?;
13637                    let prog = self.parse_assign_expr()?;
13638                    return Ok((init, block, merge_expr_list(parts), Some(prog)));
13639                }
13640            }
13641            parts.push(self.parse_assign_expr()?);
13642        }
13643        Ok((init, block, merge_expr_list(parts), None))
13644    }
13645
13646    /// `pmap_on CLUSTER { BLOCK } LIST [, progress => EXPR]` — cluster expr, then same tail as [`Self::parse_block_then_list_optional_progress`].
13647    fn parse_cluster_block_then_list_optional_progress(
13648        &mut self,
13649    ) -> StrykeResult<(Expr, Block, Expr, Option<Expr>)> {
13650        // `pmap_on $c { BLOCK } @list` — suppress `$c { ... }` hash-subscript
13651        // auto-arrow so the brace opens the BLOCK, not a `$c->{...}` deref.
13652        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_add(1);
13653        let cluster = self.parse_assign_expr();
13654        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_sub(1);
13655        let cluster = cluster?;
13656        // Accept the canonical `pmap_on $c, { BLOCK } @list` LSP-doc form too.
13657        self.eat(&Token::Comma);
13658        let block = self.parse_block_or_bareword_block()?;
13659        let block_end_line = self.prev_line();
13660        self.eat(&Token::Comma);
13661        let line = self.peek_line();
13662        if let Token::Ident(ref kw) = self.peek().clone() {
13663            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
13664                self.advance();
13665                self.expect(&Token::FatArrow)?;
13666                let prog = self.parse_assign_expr_stop_at_pipe()?;
13667                return Ok((
13668                    cluster,
13669                    block,
13670                    Expr {
13671                        kind: ExprKind::List(vec![]),
13672                        line,
13673                    },
13674                    Some(prog),
13675                ));
13676            }
13677        }
13678        let empty_list_ok = matches!(
13679            self.peek(),
13680            Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
13681        ) || (self.in_pipe_rhs()
13682            && (matches!(self.peek(), Token::Comma) || self.peek_line() > block_end_line));
13683        if empty_list_ok {
13684            return Ok((
13685                cluster,
13686                block,
13687                Expr {
13688                    kind: ExprKind::List(vec![]),
13689                    line,
13690                },
13691                None,
13692            ));
13693        }
13694        let mut parts = vec![self.parse_assign_expr_stop_at_pipe()?];
13695        loop {
13696            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
13697                break;
13698            }
13699            if matches!(
13700                self.peek(),
13701                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
13702            ) {
13703                break;
13704            }
13705            if self.peek_is_postfix_stmt_modifier_keyword() {
13706                break;
13707            }
13708            if let Token::Ident(ref kw) = self.peek().clone() {
13709                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
13710                    self.advance();
13711                    self.expect(&Token::FatArrow)?;
13712                    let prog = self.parse_assign_expr_stop_at_pipe()?;
13713                    return Ok((cluster, block, merge_expr_list(parts), Some(prog)));
13714                }
13715            }
13716            parts.push(self.parse_assign_expr_stop_at_pipe()?);
13717        }
13718        Ok((cluster, block, merge_expr_list(parts), None))
13719    }
13720
13721    /// Like [`parse_block_list`] but supports a trailing `, progress => EXPR`
13722    /// (`pmap`, `pgrep`, `preduce`, `pfor`, `pcache`, `psort`, …).
13723    ///
13724    /// Always invoked for paren-less trailing forms (`pmap { … } LIST`,
13725    /// `pmap { … } LIST, progress => EXPR`), so `|>` must terminate the whole
13726    /// stage — individual list parts and the progress value parse through
13727    /// [`Self::parse_assign_expr_stop_at_pipe`] to keep pipe-forward
13728    /// left-associative in `@a |> pmap { $_ * 2 }, progress => 0 |> join ','`.
13729    fn parse_block_then_list_optional_progress(
13730        &mut self,
13731    ) -> StrykeResult<(Block, Expr, Option<Expr>)> {
13732        let block = self.parse_block_or_bareword_block()?;
13733        let block_end_line = self.prev_line();
13734        self.eat(&Token::Comma);
13735        let line = self.peek_line();
13736        if let Token::Ident(ref kw) = self.peek().clone() {
13737            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
13738                self.advance();
13739                self.expect(&Token::FatArrow)?;
13740                let prog = self.parse_assign_expr_stop_at_pipe()?;
13741                return Ok((
13742                    block,
13743                    Expr {
13744                        kind: ExprKind::List(vec![]),
13745                        line,
13746                    },
13747                    Some(prog),
13748                ));
13749            }
13750        }
13751        // An empty list operand is allowed when the next token terminates the
13752        // enclosing context. Inside a pipe-forward RHS, a trailing `,` also
13753        // counts — `foo(bar, @a |> pmap { $_ * 2 }, baz)`. `|>` is also a
13754        // terminator — left-associative chaining leaves the outer `|>` for
13755        // the enclosing pipe-forward loop. A newline after the block also
13756        // terminates in pipe-RHS — the LHS supplies the list, so we must NOT
13757        // greedily eat the next statement (matches `parse_block_list`).
13758        let empty_list_ok = matches!(
13759            self.peek(),
13760            Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
13761        ) || (self.in_pipe_rhs()
13762            && (matches!(self.peek(), Token::Comma) || self.peek_line() > block_end_line));
13763        if empty_list_ok {
13764            return Ok((
13765                block,
13766                Expr {
13767                    kind: ExprKind::List(vec![]),
13768                    line,
13769                },
13770                None,
13771            ));
13772        }
13773        let mut parts = vec![self.parse_assign_expr_stop_at_pipe()?];
13774        loop {
13775            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
13776                break;
13777            }
13778            if matches!(
13779                self.peek(),
13780                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
13781            ) {
13782                break;
13783            }
13784            if self.peek_is_postfix_stmt_modifier_keyword() {
13785                break;
13786            }
13787            if let Token::Ident(ref kw) = self.peek().clone() {
13788                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
13789                    self.advance();
13790                    self.expect(&Token::FatArrow)?;
13791                    let prog = self.parse_assign_expr_stop_at_pipe()?;
13792                    return Ok((block, merge_expr_list(parts), Some(prog)));
13793                }
13794            }
13795            parts.push(self.parse_assign_expr_stop_at_pipe()?);
13796        }
13797        Ok((block, merge_expr_list(parts), None))
13798    }
13799
13800    /// Parse fan/fan_cap arguments: optional count + block or blockless expression.
13801    fn parse_fan_count_and_block(
13802        &mut self,
13803        line: usize,
13804    ) -> StrykeResult<(Option<Box<Expr>>, Block)> {
13805        // `fan { BLOCK }` — no count
13806        if matches!(self.peek(), Token::LBrace) {
13807            let block = self.parse_block()?;
13808            return Ok((None, block));
13809        }
13810        let saved = self.pos;
13811        // Not a brace — first expr could be count or body
13812        let first = self.parse_postfix()?;
13813        if matches!(self.peek(), Token::LBrace) {
13814            // `fan COUNT { BLOCK }`
13815            let block = self.parse_block()?;
13816            Ok((Some(Box::new(first)), block))
13817        } else if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof)
13818            || (matches!(self.peek(), Token::Comma)
13819                && matches!(self.peek_at(1), Token::Ident(ref kw) if kw == "progress"))
13820        {
13821            // `fan EXPR;` — no count, first is the body
13822            let block = self.bareword_to_no_arg_block(first);
13823            Ok((None, block))
13824        } else if matches!(first.kind, ExprKind::Integer(_)) {
13825            // `fan COUNT EXPR` or `fan COUNT, EXPR` — integer count + body
13826            self.eat(&Token::Comma);
13827            let body = self.parse_fan_blockless_body(line)?;
13828            Ok((Some(Box::new(first)), body))
13829        } else {
13830            // Non-integer first (e.g. `$_`) followed by binary op (e.g. `* $_`)
13831            // — backtrack and re-parse as a full body expression.
13832            self.pos = saved;
13833            let body = self.parse_fan_blockless_body(line)?;
13834            Ok((None, body))
13835        }
13836    }
13837
13838    /// Parse a blockless fan/fan_cap body as a full expression (not just postfix).
13839    fn parse_fan_blockless_body(&mut self, line: usize) -> StrykeResult<Block> {
13840        if matches!(self.peek(), Token::LBrace) {
13841            return self.parse_block();
13842        }
13843        // Check for bareword (zero-arg sub call) terminated by ; } EOF , or pipe
13844        if let Token::Ident(ref name) = self.peek().clone() {
13845            if matches!(
13846                self.peek_at(1),
13847                Token::Comma | Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
13848            ) {
13849                let name = name.clone();
13850                self.advance();
13851                let body = Expr {
13852                    kind: ExprKind::FuncCall { name, args: vec![] },
13853                    line,
13854                };
13855                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
13856            }
13857        }
13858        // Full expression (handles `$_ * $_`, `$_ + 1`, etc.)
13859        let expr = self.parse_assign_expr_stop_at_pipe()?;
13860        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
13861    }
13862
13863    /// Wrap a parsed expression as a single-statement block, converting bare
13864    /// identifiers to zero-arg calls (`work` → `work()`).
13865    fn bareword_to_no_arg_block(&self, expr: Expr) -> Block {
13866        let line = expr.line;
13867        let body = match &expr.kind {
13868            ExprKind::Bareword(name) => Expr {
13869                kind: ExprKind::FuncCall {
13870                    name: name.clone(),
13871                    args: vec![],
13872                },
13873                line,
13874            },
13875            _ => expr,
13876        };
13877        vec![Statement::new(StmtKind::Expression(body), line)]
13878    }
13879
13880    /// Parse either a `{ BLOCK }` or a bare expression and wrap it as a synthetic block.
13881    ///
13882    /// When the next token is `{`, delegates to [`Self::parse_block`].
13883    /// Otherwise parses a single postfix expression and wraps it as a call
13884    /// with `$_` as argument (for barewords) or a plain expression statement:
13885    ///
13886    /// - Bareword `foo` → `{ foo($_) }`
13887    /// - Other expr     → `{ EXPR }`
13888    fn parse_block_or_bareword_block(&mut self) -> StrykeResult<Block> {
13889        if matches!(self.peek(), Token::LBrace) {
13890            return self.parse_block();
13891        }
13892        let line = self.peek_line();
13893        // A lone identifier followed by a list-terminator is a bare sub name:
13894        // `pmap double, @list` → block is `{ double($_) }`, rest is list.
13895        if let Token::Ident(ref name) = self.peek().clone() {
13896            if matches!(
13897                self.peek_at(1),
13898                Token::Comma | Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
13899            ) {
13900                let name = name.clone();
13901                self.advance();
13902                let body = Expr {
13903                    kind: ExprKind::FuncCall {
13904                        name,
13905                        args: vec![Expr {
13906                            kind: ExprKind::ScalarVar("_".to_string()),
13907                            line,
13908                        }],
13909                    },
13910                    line,
13911                };
13912                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
13913            }
13914        }
13915        // Not a simple bareword — parse as expression (e.g. `$_ * 2`, `uc $_`)
13916        let expr = self.parse_assign_expr_stop_at_pipe()?;
13917        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
13918    }
13919
13920    /// Like [`parse_block_or_bareword_block`] but for fan/timer/bench where the
13921    /// bare function takes no args (body runs stand-alone, not per-element).
13922    /// Only consumes a single bareword identifier — does NOT let `parse_primary`
13923    /// greedily swallow subsequent tokens as function arguments.
13924    fn parse_block_or_bareword_block_no_args(&mut self) -> StrykeResult<Block> {
13925        if matches!(self.peek(), Token::LBrace) {
13926            return self.parse_block();
13927        }
13928        let line = self.peek_line();
13929        if let Token::Ident(ref name) = self.peek().clone() {
13930            if matches!(
13931                self.peek_at(1),
13932                Token::Comma
13933                    | Token::Semicolon
13934                    | Token::RBrace
13935                    | Token::Eof
13936                    | Token::PipeForward
13937                    | Token::Integer(_)
13938            ) {
13939                let name = name.clone();
13940                self.advance();
13941                let body = Expr {
13942                    kind: ExprKind::FuncCall { name, args: vec![] },
13943                    line,
13944                };
13945                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
13946            }
13947        }
13948        let expr = self.parse_postfix()?;
13949        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
13950    }
13951
13952    /// Returns true if `name` is a Perl keyword/builtin that should NOT be
13953    /// treated as a bare sub name (e.g. inside `sort`).
13954    /// True for any bareword the parser treats as a known builtin / keyword —
13955    /// Perl 5 core *or* a stryke extension. Used to suppress "call as user
13956    /// sub" interpretations (e.g. `sort my_cmp @list` only treats `my_cmp`
13957    /// as a comparator name if it *isn't* a known bareword). Previously named
13958    /// `is_perl_keyword`, which was misleading.
13959    fn is_known_bareword(name: &str) -> bool {
13960        Self::is_perl5_core(name) || Self::stryke_extension_name(name).is_some()
13961    }
13962
13963    /// True iff `name` appears as any spelling (primary *or* alias) in a
13964    /// `try_builtin` match arm. Picks up the ~300 aliases that don't show
13965    /// up in the parser-level keyword lists but are still callable at
13966    /// runtime — so `map { tj }` can default to `tj($_)` the same way
13967    /// `map { to_json }` does.
13968    fn is_try_builtin_name(name: &str) -> bool {
13969        crate::builtins::BUILTIN_ARMS
13970            .iter()
13971            .any(|arm| arm.contains(&name))
13972    }
13973
13974    /// True iff `name` is a Perl 5 core keyword/builtin (as shipped in stock
13975    /// `perl`). Extensions (`pmap`, `fan`, `timer`, …) are *not* included
13976    /// here — those live in `stryke_extension_name`. `%stryke::perl_compats`
13977    /// is derived from this list by `build.rs`.
13978    fn is_perl5_core(name: &str) -> bool {
13979        matches!(
13980            name,
13981            // ── array / list ────────────────────────────────────────────
13982            "map" | "grep" | "sort" | "reverse" | "join" | "split"
13983            | "push" | "pop" | "shift" | "unshift" | "splice"
13984            | "splice_last" | "splice1" | "spl_last"
13985            | "pack" | "unpack"
13986            | "unpack_first" | "unpack1" | "up1"
13987            // ── hash ────────────────────────────────────────────────────
13988            | "keys" | "values" | "each"
13989            // ── string ──────────────────────────────────────────────────
13990            | "chomp" | "chop" | "chr" | "ord" | "hex" | "oct"
13991            | "lc" | "uc" | "lcfirst" | "ucfirst"
13992            | "length" | "substr" | "index" | "rindex"
13993            | "sprintf" | "printf" | "print" | "say"
13994            | "pos" | "quotemeta" | "study"
13995            // ── numeric ─────────────────────────────────────────────────
13996            | "abs" | "int" | "sqrt" | "sin" | "cos" | "atan2"
13997            | "exp" | "log" | "rand" | "srand"
13998            // ── time ────────────────────────────────────────────────────
13999            | "time" | "localtime" | "gmtime"
14000            // ── type / reflection ───────────────────────────────────────
14001            | "defined" | "undef" | "ref" | "scalar" | "wantarray"
14002            | "caller" | "delete" | "exists" | "bless" | "prototype"
14003            | "tie" | "untie" | "tied"
14004            // ── io ──────────────────────────────────────────────────────
14005            | "open" | "close" | "read" | "readline" | "write" | "seek" | "tell"
14006            | "eof" | "binmode" | "getc" | "fileno" | "truncate"
14007            | "format" | "formline" | "select" | "vec"
14008            | "sysopen" | "sysread" | "sysseek" | "syswrite"
14009            // ── filesystem ──────────────────────────────────────────────
14010            | "stat" | "lstat" | "rename" | "unlink" | "utime"
14011            | "mkdir" | "rmdir" | "chdir" | "chmod" | "chown"
14012            | "glob" | "opendir" | "readdir" | "closedir"
14013            | "link" | "readlink" | "symlink"
14014            // ── ipc ─────────────────────────────────────────────────────
14015            | "fcntl" | "flock" | "ioctl" | "pipe" | "dbmopen" | "dbmclose"
14016            // ── sysv ipc ────────────────────────────────────────────────
14017            | "msgctl" | "msgget" | "msgrcv" | "msgsnd"
14018            | "semctl" | "semget" | "semop"
14019            | "shmctl" | "shmget" | "shmread" | "shmwrite"
14020            // ── process / system ────────────────────────────────────────
14021            | "system" | "exec" | "exit" | "die" | "warn" | "dump"
14022            | "fork" | "wait" | "waitpid" | "kill" | "syscall" | "alarm" | "sleep"
14023            | "chroot" | "times" | "umask" | "reset"
14024            | "getpgrp" | "setpgrp" | "getppid"
14025            | "getpriority" | "setpriority"
14026            // ── socket ──────────────────────────────────────────────────
14027            | "socket" | "socketpair" | "connect" | "listen" | "accept" | "shutdown"
14028            | "send" | "recv" | "bind" | "setsockopt" | "getsockopt"
14029            | "getpeername" | "getsockname"
14030            // ── posix metadata ──────────────────────────────────────────
14031            | "getpwnam" | "getpwuid" | "getpwent" | "setpwent"
14032            | "getgrnam" | "getgrgid" | "getgrent" | "setgrent"
14033            | "getlogin"
14034            | "gethostbyname" | "gethostbyaddr" | "gethostent"
14035            | "getnetbyname" | "getnetent"
14036            | "getprotobyname" | "getprotoent"
14037            | "getservbyname" | "getservent"
14038            | "sethostent" | "setnetent" | "setprotoent" | "setservent"
14039            | "endpwent" | "endgrent"
14040            | "endhostent" | "endnetent" | "endprotoent" | "endservent"
14041            // ── control flow ────────────────────────────────────────────
14042            | "return" | "do" | "eval" | "require"
14043            | "my" | "var" | "val" | "our" | "local" | "use" | "no"
14044            | "sub" | "if" | "unless" | "while" | "until"
14045            | "for" | "foreach" | "last" | "next" | "redo" | "goto"
14046            | "not" | "and" | "or"
14047            // ── quoting ─────────────────────────────────────────────────
14048            | "qw" | "qq" | "q"
14049            // ── phase blocks ────────────────────────────────────────────
14050            | "BEGIN" | "END"
14051        )
14052    }
14053
14054    /// If `name` is a stryke-only extension keyword/builtin, return it; else `None`.
14055    /// Used by `--compat` to reject extensions at parse time.
14056    fn stryke_extension_name(name: &str) -> Option<&str> {
14057        match name {
14058            // ── persistence / introspection ───────────────────────────────
14059            // ExprKind-special builtins (parsed as Burp / God / Swallow /
14060            // Ingest variants, not routed through try_builtin dispatch).
14061            // Registered HERE so the reflection registry (%b, %all, %c)
14062            // sees them via build.rs's category extractor.
14063            // (User bug 2026-05-27: "burp doesnt show up in %all" —
14064            // these names lived only in is_reserved_special_var_name
14065            // (shadow protection) and never reached the categorizer.
14066            // Pinned by reflection::exprkind_special_builtins_present_in_
14067            // reflection_registry. Section label kept under 40 chars
14068            // per build.rs:parse_section_header's length cap.)
14069            | "burp" | "god" | "swallow" | "ingest"
14070            // ── distributed / congregation ─────────────────────────────────
14071            // Scriptable distributed-compute primitives. Wrap the existing
14072            // controller.rs / agent.rs TCP+bincode infrastructure with a
14073            // scatter-gather API. See builtins.rs::builtin_congregation /
14074            // builtin_pray / builtin_annex and docs/killer-features-
14075            // brainstorm.md "Scriptable Master/Slave" for the full design.
14076            | "congregation" | "ordain" | "muster" | "pray" | "annex"
14077            | "harvest" | "excommunicate" | "smite" | "bestow"
14078            | "enshrine" | "exhume" | "smother" | "amen"
14079            | "anoint" | "welcome" | "pilgrimage" | "bow"
14080            | "lick" | "peruse"
14081            | "chant" | "profess" | "apostatize" | "cathedral" | "cloister"
14082            | "recant" | "martyr" | "resurrect" | "divine" | "interrogate"
14083            // ── numerical stability + modern IDs ───────────────────────────
14084            | "ulid" | "is_ulid" | "ulid_timestamp"
14085            | "kahan_sum" | "welford_mean" | "welford_variance"
14086            | "welford_stddev" | "welford_pop_variance"
14087            // ── Shell-like REPL (Tier S) ───────────────────────────────────
14088            | "clear" | "cls" | "whoami" | "groups"
14089            | "pushd" | "popd" | "dir_stack"
14090            | "history" | "repl_alias" | "repl_unalias" | "set_alias" | "unset_alias"
14091            | "term_size" | "term_width" | "term_height"
14092            | "set_title" | "beep" | "ring_bell" | "man" | "manpage"
14093            | "run" | "exec_script" | "source" | "src"
14094            // ── Shell-like REPL (Tier A) ───────────────────────────────────
14095            | "rm" | "mktemp" | "mktempdir" | "whereis"
14096            | "nice" | "renice"
14097            | "tree" | "comm" | "column" | "xargs"
14098            | "openurl" | "xdg_open"
14099            | "curl_get" | "curl_post"
14100            | "iconv" | "strftime"
14101            | "tac" | "rev_lines"
14102            | "tty_raw" | "tty_cooked"
14103            // ── probabilistic data structures ──────────────────────────────
14104            | "bloom_filter" | "bloom_add" | "bloom_contains" | "bloom_len"
14105            | "bloom_clear" | "bloom_merge" | "bloom_fpr" | "bloom_bits"
14106            | "bloom_serialize" | "bloom_deserialize"
14107            | "hll" | "hyperloglog" | "hll_add" | "hll_count" | "hll_merge"
14108            | "hll_clear" | "hll_precision" | "hll_serialize" | "hll_deserialize"
14109            | "cms" | "count_min_sketch" | "cms_add" | "cms_count" | "cms_query"
14110            | "cms_merge" | "cms_clear" | "cms_serialize" | "cms_deserialize"
14111            | "topk" | "top_k_sketch" | "topk_add" | "topk_heavies" | "topk_count"
14112            | "topk_size" | "topk_merge" | "topk_clear"
14113            | "topk_serialize" | "topk_deserialize"
14114            | "t_digest" | "tdg" | "tdigest" | "td_add" | "td_quantile" | "td_count"
14115            | "td_min" | "td_max" | "td_sum" | "td_mean" | "td_merge" | "td_clear"
14116            | "td_serialize" | "td_deserialize"
14117            | "roaring" | "roaring_bitmap" | "rbm" | "rb_add" | "rb_remove" | "rb_contains"
14118            | "rb_len" | "rb_min" | "rb_max" | "rb_to_array" | "rb_rank"
14119            | "rb_or" | "rb_and" | "rb_xor" | "rb_andnot" | "rb_clear"
14120            | "rb_serialize" | "rb_deserialize"
14121            // ── Rate limiters / hash ring / LSH / trees / diff ────────────
14122            | "token_bucket" | "leaky_bucket" | "rl_try_take" | "rl_available"
14123            | "hash_ring" | "consistent_hash" | "hr_add" | "hr_remove" | "hr_get" | "hr_nodes"
14124            | "simhash" | "sh_add" | "sh_digest" | "sh_similarity"
14125            | "minhash" | "mh_add" | "mh_jaccard" | "mh_merge"
14126            | "interval_tree" | "it_insert" | "it_query_point" | "it_query_range"
14127            | "it_remove" | "it_len"
14128            | "bk_tree" | "bk_insert" | "bk_query" | "bk_len"
14129            | "rope" | "rope_insert" | "rope_delete" | "rope_substring"
14130            | "rope_to_string" | "rope_len"
14131            | "myers_diff" | "patience_diff"
14132            // ── rkyv KV store ──────────────────────────────────────────────
14133            | "kv_open" | "kv_new" | "kv_put" | "kv_set" | "kv_get"
14134            | "kv_del" | "kv_delete" | "kv_remove" | "kv_exists" | "kv_has"
14135            | "kv_keys" | "kv_scan" | "kv_len" | "kv_count" | "kv_size"
14136            | "kv_commit" | "kv_flush" | "kv_batch" | "kv_close"
14137            | "kv_stats" | "kv_info"
14138            // ── aop ────────────────────────────────────────────────────────
14139            | "proceed" | "intercept_list" | "intercept_remove" | "intercept_clear"
14140            // ── parallel ────────────────────────────────────────────────────
14141            | "pmap" | "pmap_on" | "pflat_map" | "pflat_map_on" | "pmap_chunked"
14142            | "pgrep" | "pfor" | "psort" | "preduce" | "preduce_init" | "pmap_reduce"
14143            | "pcache" | "pchannel" | "pselect" | "puniq" | "pfirst" | "pany"
14144            | "fan" | "fan_cap" | "par_lines" | "par_walk" | "par_sed"
14145            | "par_find_files" | "par_line_count" | "pwatch" | "par_pipeline_stream"
14146            | "glob_par" | "ppool" | "barrier" | "pipeline" | "cluster"
14147            | "pmaps" | "pflat_maps" | "pgreps"
14148            // ── controller / agent (script-level mode entry) ────────────────
14149            | "controller" | "agent"
14150            // ── provenance / lineage ───────────────────────────────────────
14151            | "mark" | "provenance" | "unmark"
14152            // ── network probes (TCP knock, UDP send, active probes) ───────
14153            | "kick" | "udp_send"
14154            | "tcp_probe" | "tcp_banner" | "whois_query"
14155            // ── P2P NAT traversal (persistent socket pool + STUN + punch) ──
14156            | "udp_open" | "udp_send_to" | "udp_recv" | "udp_recv_from"
14157            | "udp_close" | "stun" | "stun_classify" | "punch"
14158            // ── TURN relay fallback (RFC 8656) for symmetric NATs ──────────
14159            | "turn_allocate" | "turn_permission" | "turn_send"
14160            | "turn_recv" | "turn_refresh"
14161            // ── ipc / shared memory teleport ───────────────────────────────
14162            | "teleport" | "arrive"
14163            // ── peer-pair keepalive ────────────────────────────────────────
14164            | "turnbuckle" | "tb_alive" | "tb_ping" | "tb_close"
14165            // ── slow-trickle emitter ───────────────────────────────────────
14166            | "weep"
14167            // ── functional / iterator ───────────────────────────────────────
14168            | "fore" | "e" | "ep" | "flat_map" | "flat_maps" | "maps" | "filter" | "fi" | "find_all" | "reduce" | "fold"
14169            | "inject" | "collect" | "uniq" | "distinct" | "any" | "all" | "none"
14170            | "first" | "detect" | "find" | "find_index" | "firstidx" | "first_index"
14171            | "compact" | "concat" | "chain" | "reject" | "grepv" | "flatten" | "set"
14172            | "min_by" | "max_by" | "sort_by" | "tally"
14173            | "each_with_index" | "count" | "cnt" |"len" | "group_by" | "chunk_by"
14174            | "zip" | "chunk" | "chunked" | "sliding_window" | "windowed"
14175            | "enumerate" | "with_index" | "shuffle" | "shuffled"| "heap"
14176            | "take_while" | "drop_while" | "skip_while" | "tap" | "peek" | "partition"
14177            | "zip_with" | "count_by" | "skip" | "first_or"
14178            // ── cli / argv ──────────────────────────────────────────────────
14179            | "getopts"
14180            // ── pipeline / string helpers ───────────────────────────────────
14181            | "input" | "lines" | "words" | "chars" | "cindex" | "crindex"
14182            | "digits" | "letters" | "letters_uc" | "letters_lc"
14183            | "punctuation" | "punct"
14184            | "sentences" | "sents"
14185            | "paragraphs" | "paras" | "sections" | "sects"
14186            | "numbers" | "nums" | "graphemes" | "grs" | "columns" | "cols"
14187            | "trim" | "avg" | "stddev"
14188            | "squared" | "sq" | "square" | "cubed" | "cb" | "cube" | "expt" | "pow" | "pw"
14189            | "normalize" | "snake_case" | "camel_case" | "kebab_case"
14190            | "frequencies" | "freq" | "pfrequencies" | "pfreq"
14191            | "interleave" | "ddump" | "stringify" | "str" | "top"
14192            | "to_json" | "to_csv" | "to_toml" | "to_yaml" | "to_xml"
14193            | "to_html" | "to_markdown" | "to_table" | "xopen"
14194            | "from_json" | "from_csv" | "from_toml" | "from_yaml" | "from_xml"
14195            | "clip" | "clipboard" | "paste" | "pbcopy" | "pbpaste" | "preview"
14196            | "sparkline" | "spark" | "bar_chart" | "bars" | "flame" | "flamechart"
14197            | "histo" | "gauge" | "spinner" | "spinner_start" | "spinner_stop"
14198            | "to_hash" | "to_set"
14199            | "to_file" | "read_lines" | "append_file" | "write_json" | "read_json"
14200            | "tempfile" | "tempdir" | "list_count" | "list_size" | "size"
14201            | "clamp" | "grep_v" | "select_keys" | "pluck" | "glob_match" | "which_all"
14202            | "dedup" | "nth" | "tail" | "take" | "drop" | "tee" | "range"
14203            | "inc" | "dec" | "elapsed"
14204            // ── filesystem extensions ───────────────────────────────────────
14205            | "files" | "filesf" | "f" | "fr" | "dirs" | "d" | "dr" | "sym_links"
14206            | "sockets" | "pipes" | "block_devices" | "char_devices" | "exe" | "executables"
14207            | "basename" | "dirname" | "fileparse" | "realpath" | "canonpath"
14208            | "copy" | "cp" | "move" | "spurt" | "spit" | "read_bytes" | "which"
14209            | "getcwd" | "cd" | "ls" | "touch" | "gethostname" | "uname"
14210            | "file" | "xxd"
14211            // ── data / network ──────────────────────────────────────────────
14212            | "csv_read" | "csv_write" | "dataframe" | "sqlite"
14213            | "fetch" | "fetch_json" | "fetch_async" | "fetch_async_json"
14214            | "par_fetch" | "par_csv_read" | "par_pipeline"
14215            | "json_encode" | "json_decode" | "json_jq"
14216            | "http_request" | "serve" | "ssh"
14217            | "html_parse" | "css_select" | "xml_parse" | "xpath"
14218            | "smtp_send"
14219            | "net_interfaces" | "net_ipv4" | "net_ipv6" | "net_mac"
14220            | "net_public_ip" | "net_dns" | "net_reverse_dns"
14221            | "net_ping" | "net_port_open" | "net_ports_scan"
14222            | "net_latency" | "net_download" | "net_headers"
14223            | "net_dns_servers" | "net_gateway" | "net_whois" | "net_hostname"
14224            // ── git ─────────────────────────────────────────────────────────
14225            | "git_log" | "git_status" | "git_diff" | "git_branches"
14226            | "git_tags" | "git_blame" | "git_authors" | "git_files"
14227            | "git_show" | "git_root"
14228            // ── github / gh REST API ─────────────────────────────────────────
14229            | "gh_get" | "gh_user" | "gh_org" | "gh_followers" | "gh_following"
14230            | "gh_repo" | "gh_repos" | "gh_org_repos" | "gh_starred"
14231            | "gh_gists" | "gh_gist"
14232            | "gh_issues" | "gh_prs" | "gh_commits" | "gh_branches"
14233            | "gh_tags" | "gh_releases" | "gh_contributors" | "gh_forks"
14234            | "gh_stargazers" | "gh_topics" | "gh_languages"
14235            | "gh_readme" | "gh_workflows" | "gh_runs"
14236            | "gh_search_repos" | "gh_search_users" | "gh_search_code" | "gh_search_issues"
14237            | "gh_rate_limit" | "gh_meta" | "gh_emojis" | "gh_zen"
14238            // ── audio / media ───────────────────────────────────────────────
14239            | "audio_convert" | "audio_info" | "id3_read" | "id3_write"
14240            // ── pdf ─────────────────────────────────────────────────────────
14241            | "to_pdf" | "pdf_text" | "pdf_pages"
14242            // ── serialization (stryke-only encoders) ────────────────────────
14243            | "toml_encode" | "toml_decode"
14244            | "yaml_encode" | "yaml_decode"
14245            | "xml_encode" | "xml_decode"
14246            // ── crypto / encoding ───────────────────────────────────────────
14247            | "md5" | "sha1" | "sha224" | "sha256" | "sha384" | "sha512"
14248            | "sha3_256" | "s3_256" | "sha3_512" | "s3_512"
14249            | "shake128" | "shake256"
14250            | "hmac_sha256" | "hmac_sha1" | "hmac_sha384" | "hmac_sha512" | "hmac_md5"
14251            | "uuid" | "crc32"
14252            | "blake2b" | "b2b" | "blake2s" | "b2s" | "blake3" | "b3"
14253            | "ripemd160" | "rmd160" | "md4"
14254            | "xxh32" | "xxhash32" | "xxh64" | "xxhash64" | "xxh3" | "xxhash3" | "xxh3_128" | "xxhash3_128"
14255            | "murmur3" | "murmur3_32" | "murmur3_128"
14256            | "siphash" | "siphash_keyed"
14257            | "hkdf_sha256" | "hkdf" | "hkdf_sha512"
14258            | "poly1305" | "poly1305_mac"
14259            | "base32_encode" | "b32e" | "base32_decode" | "b32d"
14260            | "base58_encode" | "b58e" | "base58_decode" | "b58d"
14261            | "totp" | "totp_generate" | "totp_verify" | "hotp" | "hotp_generate"
14262            | "aes_cbc_encrypt" | "aes_cbc_enc" | "aes_cbc_decrypt" | "aes_cbc_dec"
14263            | "blowfish_encrypt" | "bf_enc" | "blowfish_decrypt" | "bf_dec"
14264            | "des3_encrypt" | "3des_enc" | "tdes_enc" | "des3_decrypt" | "3des_dec" | "tdes_dec"
14265            | "twofish_encrypt" | "tf_enc" | "twofish_decrypt" | "tf_dec"
14266            | "camellia_encrypt" | "cam_enc" | "camellia_decrypt" | "cam_dec"
14267            | "cast5_encrypt" | "cast5_enc" | "cast5_decrypt" | "cast5_dec"
14268            | "salsa20" | "salsa20_encrypt" | "salsa20_decrypt"
14269            | "xsalsa20" | "xsalsa20_encrypt" | "xsalsa20_decrypt"
14270            | "secretbox" | "secretbox_seal" | "secretbox_open"
14271            | "nacl_box_keygen" | "box_keygen" | "nacl_box" | "nacl_box_seal" | "box_seal"
14272            | "nacl_box_open" | "box_open"
14273            | "qr_ascii" | "qr" | "qr_png" | "qr_svg"
14274            | "barcode_code128" | "code128" | "barcode_code39" | "code39"
14275            | "barcode_ean13" | "ean13" | "barcode_svg"
14276            | "argon2_hash" | "argon2" | "argon2_verify"
14277            | "bcrypt_hash" | "bcrypt" | "bcrypt_verify"
14278            | "scrypt_hash" | "scrypt" | "scrypt_verify"
14279            | "pbkdf2" | "pbkdf2_derive"
14280            | "random_bytes" | "randbytes" | "random_bytes_hex" | "randhex"
14281            | "aes_encrypt" | "aes_enc" | "aes_decrypt" | "aes_dec"
14282            | "chacha_encrypt" | "chacha_enc" | "chacha_decrypt" | "chacha_dec"
14283            | "rsa_keygen" | "rsa_encrypt" | "rsa_enc" | "rsa_decrypt" | "rsa_dec"
14284            | "rsa_encrypt_pkcs1" | "rsa_decrypt_pkcs1" | "rsa_sign" | "rsa_verify"
14285            | "ecdsa_p256_keygen" | "p256_keygen" | "ecdsa_p256_sign" | "p256_sign"
14286            | "ecdsa_p256_verify" | "p256_verify"
14287            | "ecdsa_p384_keygen" | "p384_keygen" | "ecdsa_p384_sign" | "p384_sign"
14288            | "ecdsa_p384_verify" | "p384_verify"
14289            | "ecdsa_secp256k1_keygen" | "secp256k1_keygen"
14290            | "ecdsa_secp256k1_sign" | "secp256k1_sign"
14291            | "ecdsa_secp256k1_verify" | "secp256k1_verify"
14292            | "ecdh_p256" | "p256_dh" | "ecdh_p384" | "p384_dh"
14293            | "ed25519_keygen" | "ed_keygen" | "ed25519_sign" | "ed_sign"
14294            | "ed25519_verify" | "ed_verify"
14295            | "x25519_keygen" | "x_keygen" | "x25519_dh" | "x_dh"
14296            | "base64_encode" | "base64_decode"
14297            | "hex_encode" | "hex_decode"
14298            // ── steganography ───────────────────────────────────────────────
14299            | "hide" | "reveal" | "hide_capacity"
14300            | "url_encode" | "url_decode"
14301            | "gzip" | "gunzip" | "gz" | "ugz" | "zstd" | "zstd_decode" | "zst" | "uzst"
14302            | "brotli" | "br" | "brotli_decode" | "ubr"
14303            | "xz" | "lzma" | "xz_decode" | "unxz" | "unlzma"
14304            | "bzip2" | "bz2" | "bzip2_decode" | "bunzip2" | "ubz2"
14305            | "lz4" | "lz4_decode" | "unlz4"
14306            | "snappy" | "snp" | "snappy_decode" | "unsnappy"
14307            | "lzw" | "lzw_decode" | "unlzw"
14308            | "tar_create" | "tar" | "tar_extract" | "untar" | "tar_list"
14309            | "tar_gz_create" | "tgz" | "tar_gz_extract" | "untgz"
14310            | "zip_create" | "zip_archive" | "zip_extract" | "unzip_archive" | "zip_list"
14311            // ── special math functions ────────────────────────────────────────
14312            | "erf" | "erfc" | "gamma" | "tgamma" | "lgamma" | "ln_gamma"
14313            | "digamma" | "psi" | "beta_fn" | "lbeta" | "ln_beta"
14314            | "betainc" | "beta_reg" | "gammainc" | "gamma_li"
14315            | "gammaincc" | "gamma_ui" | "gammainc_reg" | "gamma_lr"
14316            | "gammaincc_reg" | "gamma_ur"
14317            // ── date / time ─────────────────────────────────────────────────
14318            | "datetime_utc" | "datetime_now_tz" | "now"
14319            | "datetime_format_tz" | "datetime_add_seconds"
14320            | "datetime_from_epoch"
14321            | "datetime_parse_rfc3339" | "datetime_parse_local"
14322            | "datetime_strftime"
14323            | "dateseq" | "dategrep" | "dateround" | "datesort"
14324            // ── jwt ─────────────────────────────────────────────────────────
14325            | "jwt_encode" | "jwt_decode" | "jwt_decode_unsafe"
14326            // ── logging ─────────────────────────────────────────────────────
14327            | "log_info" | "log_warn" | "log_error"
14328            | "log_debug" | "log_trace" | "log_json" | "log_level"
14329            // ── concurrency / timing ────────────────────────────────────────
14330            | "async" | "spawn" | "trace" | "timer" | "bench"
14331            | "eval_timeout" | "retry" | "rate_limit" | "every"
14332            | "gen" | "watch"
14333            // ── caching ────────────────────────────────────────────────────────
14334            | "cache_clear" | "cache_exists" | "cache_stats" | "cacheview"
14335            // ── testing framework ────────────────────────────────────────────
14336            | "assert_eq" | "assert_ne" | "assert_ok" | "assert_err"
14337            | "assert_true" | "assert_false"
14338            | "assert_gt" | "assert_lt" | "assert_ge" | "assert_le"
14339            | "assert_match" | "assert_contains" | "assert_near" | "assert_dies"
14340            | "test_run" | "run_tests" | "test_skip" | "skip_test" | "skip_assert"
14341            // ── system info ─────────────────────────────────────────────────
14342            | "mounts" | "du" | "du_tree" | "process_list"
14343            | "thread_count" | "pool_info" | "par_bench"
14344            | "perfview" | "pfv"
14345            | "docs" | "help" | "h"
14346            | "banner"
14347            // ── network / ip / cidr ─────────────────────────────────────────
14348            | "ip_parse" | "ip_is_valid" | "ip_version" | "ip_family"
14349            | "ip_to_int" | "int_to_ip" | "ip_to_bytes" | "bytes_to_ip"
14350            | "ip_to_bits" | "bits_to_ip"
14351            | "ip_is_private" | "ip_is_loopback" | "ip_is_multicast"
14352            | "ip_is_link_local" | "ip_is_unspecified" | "ip_is_global"
14353            | "ip_is_documentation" | "ip_is_benchmarking" | "ip_is_shared"
14354            | "ip_is_reserved" | "ip_is_broadcast"
14355            | "ip_canonical" | "ip_reverse" | "ip_arpa"
14356            | "ip_compare" | "ip_sort" | "ip_random"
14357            | "ipv4_parse" | "ipv4_is_valid" | "ipv4_classful_class"
14358            | "ipv6_parse" | "ipv6_is_valid" | "ipv6_canonical"
14359            | "ipv6_expand" | "ipv6_compress" | "ipv6_strip_zone" | "ipv6_zone_id"
14360            | "ipv6_link_local" | "ipv6_unique_local" | "ipv6_solicited_node"
14361            | "ipv6_eui64_addr" | "ipv6_link_local_from_mac"
14362            | "ipv4_to_ipv6_mapped" | "ipv4_to_ipv6_6to4" | "ipv6_to_ipv4_compat"
14363            | "ipv6_is_6to4" | "ipv6_6to4_extract"
14364            | "ipv6_is_teredo" | "ipv6_teredo_extract"
14365            | "ipv6_is_isatap" | "ipv6_isatap_extract"
14366            | "cidr_parse" | "cidr_valid_subnet" | "cidr_format"
14367            | "cidr_prefix_len" | "cidr_class"
14368            | "cidr_network" | "cidr_broadcast" | "cidr_netmask"
14369            | "cidr_hostmask" | "cidr_wildcard"
14370            | "cidr_to_netmask" | "netmask_to_prefix"
14371            | "cidr_first_host" | "cidr_last_host" | "cidr_num_hosts"
14372            | "cidr_size" | "cidr_hosts" | "cidr_iterate"
14373            | "cidr_contains" | "ip_in_cidr" | "ip_in_subnet"
14374            | "cidr_subnet" | "cidr_supernet" | "cidr_subnets" | "cidr_split"
14375            | "cidr_overlaps" | "cidr_aggregate" | "cidr_summarize"
14376            | "cidr_intersection" | "cidr_difference" | "cidr_union"
14377            | "cidr_minimum_covering" | "cidr_is_aggregable"
14378            | "cidr_next" | "cidr_prev" | "cidr_distance"
14379            | "cidr_random_ip" | "ip_random_in_cidr"
14380            | "cidr_compare" | "cidr_sort"
14381            | "mac_parse" | "mac_is_valid" | "mac_normalize" | "mac_format"
14382            | "mac_to_int" | "int_to_mac" | "mac_to_bytes" | "bytes_to_mac"
14383            | "mac_oui" | "mac_vendor_lookup" | "mac_lookup_vendor"
14384            | "mac_is_unicast" | "mac_is_multicast" | "mac_is_broadcast"
14385            | "mac_is_locally_administered" | "mac_is_universally_administered"
14386            | "mac_random" | "mac_random_local" | "mac_compare"
14387            | "eui48_to_eui64" | "eui64_to_eui48" | "eui64_from_mac"
14388            | "port_name" | "port_is_well_known" | "port_is_assigned"
14389            | "port_is_registered" | "port_is_ephemeral" | "port_is_dynamic"
14390            | "port_to_service" | "port_service_lookup"
14391            | "port_parse_range" | "port_random_ephemeral"
14392            | "ws_handshake_key" | "ws_handshake_accept"
14393            | "ws_mask" | "ws_unmask" | "ws_frame_encode" | "ws_frame_decode"
14394            | "ws_close_frame"
14395            | "cookie_parse" | "cookie_format"
14396            | "cookie_jar_new" | "cookie_jar_add" | "cookie_jar_get"
14397            | "cookie_is_session" | "cookie_is_expired"
14398            | "cookie_domain_matches" | "cookie_path_matches"
14399            | "cookie_set_max_age"
14400            | "http_method_is_idempotent" | "http_method_is_safe"
14401            | "http_method_has_body"
14402            | "http_status_class"
14403            | "http_status_is_informational" | "http_status_is_success"
14404            | "http_status_is_redirect" | "http_status_is_client_error"
14405            | "http_status_is_server_error"
14406            | "http_status_text" | "http_date_parse" | "http_date_format"
14407            | "mime_type_for_extension" | "mime_extension_for_type"
14408            | "mime_is_text" | "mime_is_image" | "mime_is_audio"
14409            | "mime_is_video" | "mime_is_application"
14410            | "bandwidth_format" | "bandwidth_parse"
14411            | "latency_ms" | "packet_loss" | "jitter_ms"
14412            | "rtt_min" | "rtt_max" | "rtt_avg"
14413            // ── validation / input checks ──
14414            | "is_alpha_only" | "is_alphanumeric_only" | "is_numeric_only"
14415            | "is_ascii_only" | "is_printable_ascii" | "is_utf8"
14416            | "is_lowercase" | "is_uppercase" | "is_titlecase"
14417            | "is_palindrome_str"
14418            | "is_hex" | "is_octal" | "is_binary" | "is_base32"
14419            | "is_md5_hash" | "is_sha1_hash" | "is_sha256_hash"
14420            | "is_ipv6" | "is_cidr" | "is_mac"
14421            | "is_url_http" | "is_url_https"
14422            | "is_uuid_v4" | "is_uuid_v7"
14423            | "is_jwt" | "is_email_strict"
14424            | "luhn_digit" | "is_imei" | "is_imsi"
14425            | "is_vin" | "vin_decode"
14426            | "is_ean13" | "is_upc"
14427            | "is_isbn" | "isbn10_to_isbn13" | "isbn13_to_isbn10"
14428            | "iban_format" | "iban_country" | "is_bic" | "is_swift"
14429            | "is_phone" | "is_phone_e164"
14430            | "is_zip_us" | "is_zip_plus4" | "is_postal_code" | "is_ssn_us"
14431            | "semver_compare" | "semver_satisfies"
14432            | "semver_increment_major" | "semver_increment_minor" | "semver_increment_patch"
14433            // ── math / number theory extras ──
14434            | "extended_gcd" | "modinverse" | "modpow" | "modular_sqrt"
14435            | "stirling_1" | "stirling_2" | "catalan_number" | "lucas_n"
14436            | "prime_count_below" | "divisor_count" | "divisor_sum" | "sigma_divisors"
14437            | "sum_digits" | "product_digits" | "collatz_steps"
14438            | "hyperoperation" | "busy_beaver"
14439            | "quadratic_residue" | "is_quadratic_residue"
14440            | "discrete_log" | "order_modulo" | "square_free"
14441            | "perfect_number" | "abundant" | "deficient"
14442            // ── random / sampling extras ──
14443            | "random_bernoulli" | "random_normal" | "random_lognormal"
14444            | "random_exponential" | "random_poisson" | "random_gamma" | "random_beta"
14445            | "random_alphanumeric" | "random_alphabetic" | "random_password"
14446            | "random_choices_weighted"
14447            | "sample_weighted_unique" | "reservoir_sample_weighted"
14448            | "seeded_rng" | "save_random_state" | "restore_random_state"
14449            // ── complex / geom / color / trig ──
14450            | "complex_new" | "complex_real" | "complex_imag"
14451            | "complex_polar" | "complex_from_polar"
14452            | "complex_magnitude" | "complex_abs" | "complex_phase" | "complex_angle"
14453            | "complex_conjugate"
14454            | "complex_add" | "complex_sub" | "complex_mul" | "complex_div"
14455            | "complex_pow" | "complex_sqrt" | "complex_exp" | "complex_log"
14456            | "complex_sin" | "complex_cos" | "complex_tan"
14457            | "complex_sinh" | "complex_cosh" | "complex_tanh"
14458            | "complex_equal"
14459            | "point_angle"
14460            | "line_intersect" | "line_segment_intersect" | "line_distance_point"
14461            | "polygon_signed_area" | "polygon_orientation" | "polygon_reverse"
14462            | "polygon_contains_point" | "polygon_convex"
14463            | "polygon_simplify_dp" | "polygon_convex_hull_2d"
14464            | "triangle_area" | "triangle_centroid"
14465            | "triangle_circumcircle" | "triangle_incircle"
14466            | "triangle_contains_point"
14467            | "circle_circumference" | "circle_area"
14468            | "circle_intersects_line" | "circle_intersects_circle"
14469            | "rect_area" | "rect_perimeter" | "rect_intersect"
14470            | "rect_contains_point" | "rect_union"
14471            | "ellipse_area"
14472            | "sphere_surface_area" | "cylinder_surface_area"
14473            | "cone_surface_area" | "torus_surface_area"
14474            | "srgb_to_rgb" | "rgb_to_srgb"
14475            | "rgb_to_p3" | "p3_to_rgb"
14476            | "rgb_to_adobe_rgb" | "adobe_rgb_to_rgb"
14477            | "xyz_d65_to_d50" | "xyz_d50_to_d65"
14478            | "gamma_apply" | "gamma_remove"
14479            | "white_point_d65" | "white_point_d50"
14480            | "color_temperature_to_rgb" | "rgb_to_color_temperature"
14481            | "chromatic_adaptation"
14482            | "color_interpolate_rgb" | "color_interpolate_hsl"
14483            | "color_interpolate_lab" | "color_interpolate_oklab"
14484            | "color_blend_screen"
14485            | "atan2_deg" | "atan2_quadrant"
14486            | "polar_to_cartesian" | "cartesian_to_polar"
14487            | "spherical_to_cartesian" | "cartesian_to_spherical"
14488            | "cylindrical_to_cartesian" | "cartesian_to_cylindrical"
14489            | "versine_fn"
14490            // ── iterator + string-distance extras ──
14491            | "triples" | "n_tuples" | "peekable" | "runs" | "unique_by"
14492            | "multipeek" | "lookahead_n"
14493            | "sliding_average" | "sliding_sum" | "sliding_max" | "sliding_min"
14494            | "top_n_by" | "bottom_n_by" | "all_equal" | "take_n_random"
14495            | "unzip3" | "roundrobin" | "mode_iter" | "distinct_sample"
14496            | "ranked_choice" | "boyer_moore_majority"
14497            | "quickselect_nth" | "quickselect_median"
14498            | "top_k_min_heap" | "bottom_k_max_heap"
14499            | "unique_consecutive" | "exclude" | "exclude_first" | "exclude_last"
14500            | "weave_n" | "pad_left_n" | "pad_right_n"
14501            | "collect_into_string" | "collect_into_hashset" | "collect_into_btreeset"
14502            | "collect_into_hashmap" | "collect_into_btreemap"
14503            | "foldl1_iter" | "foldr1_iter"
14504            | "sort_by_cached_key"
14505            | "position_max" | "position_min" | "position_max_by" | "position_min_by"
14506            | "group_map"
14507            | "levenshtein_normalized" | "ratcliff_obershelp" | "match_rating"
14508            | "str_lcs" | "str_lcs_length" | "str_longest_common_substring"
14509            | "str_kmp" | "str_boyer_moore" | "str_rabin_karp"
14510            | "str_aho_corasick" | "str_z_array" | "str_suffix_array"
14511            | "str_rotations" | "str_compress_rle" | "str_decompress_rle"
14512            | "str_huffman_encode" | "str_huffman_decode"
14513            | "str_compress_lzss" | "str_decompress_lzss"
14514            | "str_isogram" | "fold_case"
14515            // ── extras ──
14516            | "bignum_new" | "bignum_from_str" | "bignum_to_str" | "bignum_to_int"
14517            | "bignum_add" | "bignum_sub" | "bignum_mul" | "bignum_div" | "bignum_mod"
14518            | "bignum_pow" | "bignum_modpow" | "bignum_gcd" | "bignum_lcm"
14519            | "bignum_factorial" | "bignum_sqrt" | "bignum_bit_length"
14520            | "bignum_set_bit" | "bignum_clear_bit" | "bignum_test_bit"
14521            | "bignum_and" | "bignum_or" | "bignum_xor" | "bignum_not"
14522            | "bignum_shl" | "bignum_shr" | "bignum_compare"
14523            | "bignum_negate" | "bignum_abs" | "bignum_sign"
14524            | "bignum_is_zero" | "bignum_is_negative" | "bignum_is_prime"
14525            | "bignum_random"
14526            | "gravity_constant" | "physics_apply_force" | "physics_apply_impulse"
14527            | "physics_collide_aabb" | "physics_collide_sphere"
14528            | "physics_raycast" | "physics_step"
14529            | "particle_emit" | "particle_update"
14530            | "vector2_new" | "vector2_add" | "vector2_sub" | "vector2_scale"
14531            | "vector2_dot" | "vector2_cross" | "vector2_length"
14532            | "vector2_normalize" | "vector2_distance" | "vector2_rotate"
14533            | "quaternion_new" | "quaternion_from_axis_angle"
14534            | "quaternion_multiply" | "quaternion_normalize" | "quaternion_to_matrix"
14535            | "freq_to_note" | "note_to_freq" | "midi_note_to_name"
14536            | "chord_notes" | "scale_notes" | "transpose_note"
14537            | "window_tukey" | "zero_crossing_rate" | "peak_db"
14538            | "audio_normalize" | "audio_fade_in" | "audio_fade_out"
14539            | "audio_to_mono" | "audio_to_stereo"
14540            | "biquad_lowpass" | "biquad_highpass" | "biquad_bandpass" | "biquad_notch"
14541            | "oscillator_sine" | "oscillator_square"
14542            | "oscillator_sawtooth" | "oscillator_triangle"
14543            | "adsr_envelope" | "ar_envelope" | "crossfade"
14544            | "fade_curve_linear" | "fade_curve_logarithmic" | "fade_curve_exponential"
14545            | "bbox_contains" | "bbox_union" | "bbox_intersect"
14546            | "bbox_center" | "bbox_area"
14547            | "mercator_unproject" | "geohash_precision"
14548            // ── extras ──
14549            | "jq_get" | "jq_set" | "jq_delete" | "jq_select"
14550            | "jq_keys_at" | "jq_values_at" | "jq_length_at"
14551            | "jq_type" | "jq_has" | "jq_paths" | "jq_leaf_paths"
14552            | "jq_walk" | "jq_map_values" | "jq_filter"
14553            | "jq_to_entries" | "jq_from_entries" | "jq_with_entries"
14554            | "jq_recurse" | "jq_min_by" | "jq_max_by"
14555            | "jq_sort_by" | "jq_group_by" | "jq_unique_by"
14556            | "jq_any" | "jq_all" | "jq_flatten"
14557            | "jq_index" | "jq_indices" | "jq_first" | "jq_last"
14558            | "jq_split_at" | "jq_chunks" | "jq_zip" | "jq_combinations"
14559            | "json_diff" | "json_patch" | "json_merge_patch"
14560            | "json_pointer_resolve" | "json_pointer_set"
14561 | "html_to_text" | "html_pretty" | "html_minify"
14562            | "html_sanitize" | "html_strip_tags" | "html_strip_scripts" | "html_strip_styles"
14563            | "html_extract_links" | "html_extract_images" | "html_extract_text"
14564            | "html_extract_meta" | "html_extract_title"
14565            | "html_extract_headings" | "html_extract_tables"
14566            | "html_inner_text" | "html_canonical_url"
14567            | "html_meta_charset" | "html_meta_keywords" | "html_meta_description"
14568            | "html_meta_og" | "html_meta_twitter"
14569            | "html_to_markdown" | "markdown_to_html" | "markdown_render"
14570 | "xml_pretty" | "xml_minify"
14571            | "xml_namespace" | "xml_text" | "xml_attrs"
14572            | "xml_children_by_tag" | "xml_root"
14573            | "xpath_select_one" | "xpath_attribute" | "xpath_text"
14574            | "xml_to_json" | "json_to_xml" | "xml_canonicalize"
14575            | "css_parse" | "css_minify" | "css_pretty"
14576            | "css_selector_parse" | "css_rule_extract" | "css_specificity"
14577            | "css_var_resolve" | "css_property_set" | "css_property_get"
14578            | "css_url_extract" | "css_import_extract" | "css_font_extract"
14579            | "selector_to_xpath" | "xpath_to_selector"
14580            // ── extras ──
14581            | "http_status_continue" | "http_status_switching_protocols"
14582            | "http_status_ok" | "http_status_created" | "http_status_accepted"
14583            | "http_status_no_content" | "http_status_partial_content"
14584            | "http_status_multiple_choices" | "http_status_moved_permanently"
14585            | "http_status_found" | "http_status_see_other" | "http_status_not_modified"
14586            | "http_status_temporary_redirect" | "http_status_permanent_redirect"
14587            | "http_status_bad_request" | "http_status_unauthorized"
14588            | "http_status_payment_required" | "http_status_forbidden"
14589            | "http_status_not_found" | "http_status_method_not_allowed"
14590            | "http_status_not_acceptable" | "http_status_conflict" | "http_status_gone"
14591            | "http_status_length_required" | "http_status_precondition_failed"
14592            | "http_status_payload_too_large" | "http_status_uri_too_long"
14593            | "http_status_unsupported_media_type" | "http_status_range_not_satisfiable"
14594            | "http_status_expectation_failed" | "http_status_im_a_teapot"
14595            | "http_status_unprocessable_entity" | "http_status_too_many_requests"
14596            | "http_status_internal_server_error" | "http_status_not_implemented"
14597            | "http_status_bad_gateway" | "http_status_service_unavailable"
14598            | "http_status_gateway_timeout" | "http_status_http_version_not_supported"
14599            | "http_method_get" | "http_method_post" | "http_method_put"
14600            | "http_method_delete" | "http_method_patch" | "http_method_head"
14601            | "http_method_options" | "http_method_trace" | "http_method_connect"
14602            | "dbeta" | "qbeta" | "rbeta" | "dcauchy" | "qcauchy" | "rcauchy"
14603            | "dexp" | "qexp" | "rexp" | "dgamma" | "qgamma" | "rgamma"
14604            | "dlnorm" | "qlnorm" | "rlnorm" | "dlogis" | "qlogis" | "rlogis"
14605            | "dpois" | "qpois" | "rpois" | "dweibull" | "qweibull" | "rweibull"
14606            | "qnorm" | "rnorm" | "qunif" | "runif"
14607            | "qbinom" | "rbinom" | "qgeom" | "rgeom" | "qhyper" | "rhyper"
14608            | "qchisq" | "rchisq" | "qf" | "rf" | "qt" | "rt"
14609            // ── extras ──
14610            | "currency_format" | "currency_parse" | "currency_round"
14611            | "currency_split_thousands" | "currency_code_to_symbol"
14612            | "currency_symbol_to_code" | "currency_convert" | "currency_rate"
14613            | "currency_iso_4217" | "currency_decimal_places"
14614            | "money_add" | "money_sub" | "money_mul" | "money_div" | "money_compare"
14615            | "tokenize_simple" | "tokenize_word" | "tokenize_subword"
14616            | "tokenize_bpe" | "tokenize_sentencepiece" | "embed_text"
14617            | "cosine_similarity" | "euclidean_distance" | "manhattan_distance"
14618            | "dot_product" | "normalize_vector"
14619            | "vector_add" | "vector_sub" | "vector_scale" | "vector_mean"
14620            | "top_k_indices" | "softmax" | "sigmoid" | "log_softmax" | "cross_entropy"
14621            | "path_canonical" | "path_relative_to" | "path_components"
14622            | "path_filename" | "path_stem" | "path_extension"
14623            | "path_join_many" | "path_with_extension" | "path_with_filename"
14624            | "path_is_subdirectory" | "path_common_ancestor" | "path_strip_prefix"
14625            | "path_glob_match_regex"
14626            | "file_mime" | "file_kind" | "file_attr_get" | "file_attr_set"
14627            | "xattr_get" | "xattr_set" | "xattr_list"
14628            | "file_chmod_string" | "file_chmod_octal" | "file_locked"
14629            | "file_acl_get" | "file_acl_set"
14630            | "locale_parse" | "locale_format" | "locale_language"
14631            | "locale_region" | "locale_script" | "locale_variant" | "locale_canonical"
14632            | "bcp47_parse" | "bcp47_format" | "bcp47_validate"
14633            | "language_tag_match" | "language_tag_subtags"
14634            | "locale_likely_subtags" | "locale_minimize" | "locale_collation"
14635            | "locale_calendar" | "locale_currency"
14636            | "locale_number_format" | "locale_date_format" | "locale_time_format"
14637            | "locale_decimal_separator" | "locale_group_separator"
14638            | "locale_first_day_of_week" | "locale_measurement_system"
14639            | "country_code_alpha2" | "country_code_alpha3" | "country_code_numeric"
14640            | "country_name" | "country_phone_prefix" | "country_currency"
14641            | "country_languages"
14642            | "language_iso_639_1" | "language_iso_639_2" | "language_iso_639_3"
14643            | "language_name"
14644            | "channel_unbounded" | "channel_bounded" | "channel_sync"
14645            | "channel_send_timeout" | "channel_recv_timeout"
14646            | "channel_try_recv" | "channel_try_send"
14647            | "channel_drain" | "channel_close" | "channel_is_closed"
14648            | "broadcast_channel_new" | "broadcast_channel_subscribe"
14649            | "broadcast_channel_publish"
14650            | "mpsc_new" | "mpmc_new" | "spmc_new" | "oneshot_new"
14651            // ── mutex + counting semaphore ─────────────────────────────────
14652            | "mutex" | "mutex_lock" | "mutex_unlock" | "mutex_try_lock" | "mutex_is_locked"
14653            | "semaphore" | "sem"
14654            | "semaphore_acquire" | "sem_acquire"
14655            | "semaphore_release" | "sem_release"
14656            | "semaphore_try_acquire" | "sem_try_acquire"
14657            | "semaphore_permits" | "sem_permits"
14658            | "semaphore_limit" | "sem_limit"
14659            // ── stress testing ──────────────────────────────────────────────
14660            | "stress_cpu" | "scpu" | "stress_mem" | "smem"
14661            | "stress_io" | "sio" | "stress_test" | "st"
14662            | "heat" | "fire" | "fire_and_forget" | "pin"
14663            // ── I/O extensions ──────────────────────────────────────────────
14664            | "slurp" | "cat" | "c" | "capture" | "pager" | "pg" | "less"
14665            | "stdin"
14666            // ── internal ────────────────────────────────────────────────────
14667            | "__stryke_rust_compile"
14668            | "vec_set_value"
14669            // ── short aliases ───────────────────────────────────────────────
14670            | "p" | "rev"
14671            // ── trivial numeric / predicate builtins ────────────────────────
14672            | "even" | "odd" | "zero" | "nonzero"
14673            | "positive" | "pos_n" | "negative" | "neg_n"
14674            | "sign" | "negate" | "double" | "triple" | "half"
14675            | "identity" | "id"
14676            | "round" | "floor" | "ceil" | "ceiling" | "trunc" | "truncn"
14677            | "gcd" | "lcm" | "min2" | "max2"
14678            | "log2" | "log10" | "hypot"
14679            | "rad_to_deg" | "r2d" | "deg_to_rad" | "d2r"
14680            | "pow2" | "abs_diff"
14681            | "factorial" | "fact" | "fibonacci" | "fib"
14682            | "is_prime" | "is_square" | "is_power_of_two" | "is_pow2"
14683            | "cbrt" | "exp2" | "percent" | "pct" | "inverse"
14684            | "median" | "mode_val" | "variance"
14685            // ── trivial string ops ──────────────────────────────────────────
14686            | "is_empty" | "is_blank" | "is_numeric"
14687            | "is_upper" | "is_lower" | "is_alpha" | "is_digit" | "is_alnum"
14688            | "is_space" | "is_whitespace"
14689            | "starts_with" | "sw" | "ends_with" | "ew" | "contains"
14690            | "capitalize" | "cap" | "swap_case" | "repeat"
14691            | "title_case" | "title" | "squish"
14692            | "pad_left" | "lpad" | "pad_right" | "rpad" | "center"
14693            | "truncate_at" | "shorten" | "reverse_str" | "rev_str"
14694            | "char_count" | "word_count" | "wc" | "line_count" | "lc_lines"
14695            // ── trivial type predicates ─────────────────────────────────────
14696            | "is_array" | "is_arrayref" | "is_hash" | "is_hashref"
14697            | "is_code" | "is_coderef" | "is_ref"
14698            | "is_undef" | "is_defined" | "is_def"
14699            | "is_string" | "is_str" | "is_int" | "is_integer" | "is_float"
14700            // ── hash helpers ────────────────────────────────────────────────
14701            | "invert" | "merge_hash"
14702            | "hash_map_values" | "hash_filter_keys" | "hash_filter_values"
14703            | "has_key" | "hk" | "has_any_key" | "has_all_keys"
14704            // ── boolean combinators ─────────────────────────────────────────
14705            | "both" | "either" | "neither" | "xor_bool" | "bool_to_int" | "b2i"
14706            // ── collection helpers (trivial) ────────────────────────────────
14707            | "riffle" | "intersperse" | "every_nth"
14708            | "drop_n" | "take_n" | "rotate" | "swap_pairs"
14709            // ── base conversion ─────────────────────────────────────────────
14710            | "to_bin" | "bin_of" | "to_hex" | "hex_of" | "to_oct" | "oct_of"
14711            | "from_bin" | "from_hex" | "from_oct" | "to_base" | "from_base"
14712            | "bits_count" | "popcount" | "leading_zeros" | "lz"
14713            | "trailing_zeros" | "tz" | "bit_length" | "bitlen"
14714            // ── bit ops ─────────────────────────────────────────────────────
14715            | "bit_and" | "bit_or" | "bit_xor" | "bit_not"
14716            | "shift_left" | "shl" | "shift_right" | "shr"
14717            | "bit_set" | "bit_clear" | "bit_toggle" | "bit_test"
14718            // ── unit conversions: temperature ───────────────────────────────
14719            | "c_to_f" | "f_to_c" | "c_to_k" | "k_to_c" | "f_to_k" | "k_to_f"
14720            // ── unit conversions: distance ──────────────────────────────────
14721            | "miles_to_km" | "km_to_miles" | "miles_to_m" | "m_to_miles"
14722            | "feet_to_m" | "m_to_feet" | "inches_to_cm" | "cm_to_inches"
14723            | "yards_to_m" | "m_to_yards"
14724            // ── unit conversions: mass ──────────────────────────────────────
14725            | "kg_to_lbs" | "lbs_to_kg" | "g_to_oz" | "oz_to_g"
14726            | "stone_to_kg" | "kg_to_stone"
14727            // ── unit conversions: digital ───────────────────────────────────
14728            | "bytes_to_kb" | "b_to_kb" | "kb_to_bytes" | "kb_to_b"
14729            | "bytes_to_mb" | "mb_to_bytes" | "bytes_to_gb" | "gb_to_bytes"
14730            | "kb_to_mb" | "mb_to_gb"
14731            | "bits_to_bytes" | "bytes_to_bits"
14732            // ── unit conversions: time ──────────────────────────────────────
14733            | "seconds_to_minutes" | "s_to_m" | "minutes_to_seconds" | "m_to_s"
14734            | "seconds_to_hours" | "hours_to_seconds"
14735            | "seconds_to_days" | "days_to_seconds"
14736            | "minutes_to_hours" | "hours_to_minutes"
14737            | "hours_to_days" | "days_to_hours"
14738            // ── date helpers ────────────────────────────────────────────────
14739            | "is_leap_year" | "is_leap" | "days_in_month"
14740            | "month_name" | "month_short"
14741            | "weekday_name" | "weekday_short" | "quarter_of"
14742            // ── now / timestamp ─────────────────────────────────────────────
14743            | "now_ms" | "now_us" | "now_ns"
14744            | "unix_epoch" | "epoch" | "unix_epoch_ms" | "epoch_ms"
14745            // ── color / ANSI ────────────────────────────────────────────────
14746            | "rgb_to_hex" | "hex_to_rgb"
14747            | "ansi_red" | "ansi_green" | "ansi_yellow" | "ansi_blue"
14748            | "ansi_magenta" | "ansi_cyan" | "ansi_white" | "ansi_black"
14749            | "ansi_bold" | "ansi_dim" | "ansi_underline" | "ansi_reverse"
14750            | "strip_ansi"
14751            | "red" | "green" | "yellow" | "blue" | "magenta" | "purple" | "cyan"
14752            | "white" | "black" | "bold" | "dim" | "italic" | "underline"
14753            | "strikethrough" | "ansi_off" | "off" | "gray" | "grey"
14754            | "bright_red" | "bright_green" | "bright_yellow" | "bright_blue"
14755            | "bright_magenta" | "bright_cyan" | "bright_white"
14756            | "bg_red" | "bg_green" | "bg_yellow" | "bg_blue"
14757            | "bg_magenta" | "bg_cyan" | "bg_white" | "bg_black"
14758            | "red_bold" | "bold_red" | "green_bold" | "bold_green"
14759            | "yellow_bold" | "bold_yellow" | "blue_bold" | "bold_blue"
14760            | "magenta_bold" | "bold_magenta" | "cyan_bold" | "bold_cyan"
14761            | "white_bold" | "bold_white"
14762            | "blink" | "rapid_blink" | "hidden" | "overline"
14763            | "bg_bright_red" | "bg_bright_green" | "bg_bright_yellow" | "bg_bright_blue"
14764            | "bg_bright_magenta" | "bg_bright_cyan" | "bg_bright_white"
14765            | "rgb" | "bg_rgb" | "color256" | "c256" | "bg_color256" | "bg_c256"
14766            // ── network / validation ────────────────────────────────────────
14767            | "ipv4_to_int" | "int_to_ipv4"
14768            | "is_valid_ipv4" | "is_valid_ipv6" | "is_valid_email" | "is_valid_url"
14769            // ── path helpers ────────────────────────────────────────────────
14770            | "path_ext" | "path_parent" | "path_join" | "path_split"
14771            | "strip_prefix" | "strip_suffix" | "ensure_prefix" | "ensure_suffix"
14772            // ── functional primitives ───────────────────────────────────────
14773            | "const_fn" | "always_true" | "always_false"
14774            | "flip_args" | "first_arg" | "second_arg" | "last_arg"
14775            // ── more list helpers ───────────────────────────────────────────
14776            | "count_eq" | "count_ne" | "all_eq"
14777            | "all_distinct" | "all_unique" | "has_duplicates"
14778            | "sum_of" | "product_of" | "max_of" | "min_of" | "range_of"
14779            // ── string quote / escape ───────────────────────────────────────
14780            | "quote" | "single_quote" | "unquote"
14781            | "extract_between" | "ellipsis"
14782            // ── random ──────────────────────────────────────────────────────
14783            | "coin_flip" | "dice_roll"
14784            | "random_int" | "random_float" | "random_bool"
14785            | "random_choice" | "random_between"
14786            | "random_string" | "random_alpha" | "random_digit"
14787            // ── symbol table ────────────────────────────────────────────────
14788            | "refresh_stashes"
14789            // ── system introspection ────────────────────────────────────────
14790            | "os_name" | "os_arch" | "num_cpus"
14791            | "pid" | "ppid" | "uid" | "gid"
14792            | "username" | "home_dir" | "temp_dir"
14793            | "mem_total" | "mem_free" | "mem_used"
14794            | "swap_total" | "swap_free" | "swap_used"
14795            | "disk_total" | "disk_free" | "disk_avail" | "disk_used"
14796            | "load_avg" | "sys_uptime" | "page_size"
14797            | "os_version" | "os_family" | "endianness" | "pointer_width"
14798            | "proc_mem" | "rss"
14799            // ── collection more ─────────────────────────────────────────────
14800            | "transpose" | "unzip"
14801            | "run_length_encode" | "rle" | "run_length_decode" | "rld"
14802            | "sliding_pairs" | "consecutive_eq" | "flatten_deep"
14803            // ── trig / math ───────────────────────────────────────
14804            | "tan" | "asin" | "acos" | "atan"
14805            | "sinh" | "cosh" | "tanh" | "asinh" | "acosh" | "atanh"
14806            | "sqr" | "cube_fn"
14807            | "mod_op" | "ceil_div" | "floor_div"
14808            | "is_finite" | "is_infinite" | "is_inf" | "is_nan"
14809            | "degrees" | "radians"
14810            | "min_abs" | "max_abs"
14811            | "saturate" | "sat01" | "wrap_around"
14812            // ── string ────────────────────────────────────────────
14813            | "rot13" | "rot47" | "caesar_shift" | "reverse_words"
14814            | "count_vowels" | "count_consonants" | "is_vowel" | "is_consonant"
14815            | "first_word" | "last_word"
14816            | "left_str" | "head_str" | "right_str" | "tail_str" | "mid_str"
14817            | "lowercase" | "uppercase"
14818            | "pascal_case" | "pc_case"
14819            | "constant_case" | "upper_snake" | "dot_case" | "path_case"
14820            | "is_palindrome" | "hamming_distance"
14821            | "longest_common_prefix" | "lcp"
14822            | "ascii_ord" | "ascii_chr" | "count_char" | "indexes_of"
14823            | "replace_first" | "replace_all_str"
14824            | "contains_any" | "contains_all"
14825            | "starts_with_any" | "ends_with_any"
14826            // ── predicates ────────────────────────────────────────
14827            | "is_pair" | "is_triple"
14828            | "is_sorted" | "is_asc" | "is_sorted_desc" | "is_desc"
14829            | "is_empty_arr" | "is_empty_hash"
14830            | "is_subset" | "is_superset" | "is_permutation"
14831            // ── collection ────────────────────────────────────────
14832            | "first_eq" | "last_eq"
14833            | "index_of" | "last_index_of" | "positions_of"
14834            | "batch" | "binary_search" | "bsearch" | "linear_search" | "lsearch"
14835            | "distinct_count" | "longest" | "shortest"
14836            | "array_union" | "list_union"
14837            | "array_intersection" | "list_intersection"
14838            | "array_difference" | "list_difference"
14839            | "symmetric_diff" | "group_of_n" | "chunk_n"
14840            | "repeat_list" | "cycle_n" | "random_sample" | "sample_n"
14841            // ── hash ops ──────────────────────────────────────────
14842            | "pick_keys" | "pick" | "omit_keys" | "omit"
14843            | "map_keys_fn" | "map_values_fn"
14844            | "hash_size" | "hash_from_pairs" | "pairs_from_hash"
14845            | "hash_eq" | "keys_sorted" | "values_sorted" | "remove_keys"
14846            // ── date ──────────────────────────────────────────────
14847            | "today" | "yesterday" | "tomorrow" | "is_weekend" | "is_weekday"
14848            // ── json helpers ────────────────────────────────────────────────
14849            | "json_pretty" | "json_minify" | "escape_json" | "json_escape"
14850            // ── process / env ───────────────────────────────────────────────
14851            | "cmd_exists" | "env_get" | "env_has" | "env_keys"
14852            | "argc" | "script_name"
14853            | "has_stdin_tty" | "has_stdout_tty" | "has_stderr_tty"
14854            // ── id helpers ──────────────────────────────────────────────────
14855            | "uuid_v4" | "nanoid" | "short_id" | "is_uuid" | "token"
14856            // ── url / email parts ───────────────────────────────────────────
14857            | "email_domain" | "email_local"
14858            | "url_host" | "url_path" | "url_query" | "url_scheme"
14859            // ── file stat / path ────────────────────────────────────────────
14860            | "file_size" | "fsize" | "file_mtime" | "mtime"
14861            | "file_atime" | "atime" | "file_ctime" | "ctime"
14862            | "is_symlink" | "is_readable" | "is_writable" | "is_executable"
14863            | "path_is_abs" | "path_is_rel"
14864            // ── stats / sort / array / format / cmp / regex / time conv / volume / force ──
14865            | "min_max" | "percentile" | "harmonic_mean" | "geometric_mean" | "zscore"
14866            | "sorted" | "sorted_desc" | "sorted_nums" | "sorted_by_length"
14867            | "reverse_list" | "list_reverse"
14868            | "without" | "without_nth" | "take_last" | "drop_last"
14869            | "pairwise" | "zipmap"
14870            | "format_bytes" | "human_bytes"
14871            | "format_duration" | "human_duration"
14872            | "format_number" | "group_number"
14873            | "format_percent" | "pad_number"
14874            | "spaceship" | "cmp_num" | "cmp_str"
14875            | "compare_versions" | "version_cmp"
14876            | "hash_insert" | "hash_update" | "hash_delete"
14877            | "matches_regex" | "re_match"
14878            | "count_regex_matches" | "regex_extract"
14879            | "regex_split_str" | "regex_replace_str"
14880            | "shuffle_chars" | "random_char" | "nth_word"
14881            | "head_lines" | "tail_lines" | "count_substring"
14882            | "is_valid_hex" | "hex_upper" | "hex_lower"
14883            | "ms_to_s" | "s_to_ms" | "ms_to_ns" | "ns_to_ms"
14884            | "us_to_ns" | "ns_to_us"
14885            | "liters_to_gallons" | "gallons_to_liters"
14886            | "liters_to_ml" | "ml_to_liters"
14887            | "cups_to_ml" | "ml_to_cups"
14888            | "newtons_to_lbf" | "lbf_to_newtons"
14889            | "joules_to_cal" | "cal_to_joules"
14890            | "watts_to_hp" | "hp_to_watts"
14891            | "pascals_to_psi" | "psi_to_pascals"
14892            | "bar_to_pascals" | "pascals_to_bar"
14893            // ── algebraic match ─────────────────────────────────────────────
14894            | "match"
14895            // ── clojure stdlib (only names not matched above) ─────────────────
14896            | "fst" | "rest" | "rst" | "second" | "snd"
14897            | "last_clj" | "lastc" | "butlast" | "bl"
14898            | "ffirst" | "ffs" | "fnext" | "fne" | "nfirst" | "nfs" | "nnext" | "nne"
14899            | "cons" | "conj"
14900            | "peek_clj" | "pkc" | "pop_clj" | "popc"
14901            | "some" | "not_any" | "not_every"
14902            | "comp" | "compose" | "partial" | "constantly" | "complement" | "compl"
14903            | "fnil" | "juxt"
14904            | "memoize" | "memo" | "curry" | "once"
14905            | "deep_clone" | "dclone" | "deep_merge" | "dmerge" | "deep_equal" | "deq"
14906            | "iterate" | "iter" | "repeatedly" | "rptd" | "cycle" | "cyc"
14907            | "mapcat" | "mcat" | "keep" | "kp" | "remove_clj" | "remc"
14908            | "reductions" | "rdcs"
14909            | "partition_by" | "pby" | "partition_all" | "pall"
14910            | "split_at" | "spat" | "split_with" | "spw"
14911            | "assoc" | "dissoc" | "get_in" | "gin" | "assoc_in" | "ain" | "update_in" | "uin"
14912            | "into" | "empty_clj" | "empc" | "seq" | "vec_clj" | "vecc"
14913            | "apply" | "appl"
14914            // ── python/ruby stdlib ───────────────────────────────────────────
14915            | "divmod" | "dm" | "accumulate" | "accum" | "starmap" | "smap"
14916            | "zip_longest" | "zipl" | "zip_fill" | "zipf" | "combinations" | "comb" | "permutations" | "perm"
14917            | "cartesian_product" | "cprod" | "compress" | "cmpr" | "filterfalse" | "falf"
14918            | "islice" | "isl" | "chain_from" | "chfr" | "pairwise_iter" | "pwi"
14919            | "tee_iter" | "teei" | "groupby_iter" | "gbi"
14920            | "each_slice" | "eslice" | "each_cons" | "econs"
14921            | "one" | "none_match" | "nonem"
14922            | "find_index_fn" | "fidx" | "rindex_fn" | "ridx"
14923            | "minmax" | "mmx" | "minmax_by" | "mmxb"
14924            | "dig" | "values_at" | "vat" | "fetch_val" | "fv" | "slice_arr" | "sla"
14925            | "transform_keys" | "tkeys" | "transform_values" | "tvals"
14926            | "sum_by" | "sumb" | "uniq_by" | "uqb"
14927            | "flat_map_fn" | "fmf" | "then_fn" | "thfn" | "times_fn" | "timf"
14928            | "step" | "upto" | "downto"
14929            // ── javascript array/object methods ─────────────────────────────
14930            | "find_last" | "fndl" | "find_last_index" | "fndli"
14931            | "at_index" | "ati" | "replace_at" | "repa"
14932            | "to_sorted" | "tsrt" | "to_reversed" | "trev" | "to_spliced" | "tspl"
14933            | "flat_depth" | "fltd" | "fill_arr" | "filla" | "includes_val" | "incv"
14934            | "object_keys" | "okeys" | "object_values" | "ovals"
14935            | "object_entries" | "oents" | "object_from_entries" | "ofents"
14936            // ── haskell list functions ──────────────────────────────────────
14937            | "span_fn" | "spanf" | "break_fn" | "brkf" | "group_runs" | "gruns"
14938            | "nub" | "sort_on" | "srton"
14939            | "intersperse_val" | "isp" | "intercalate" | "ical"
14940            | "replicate_val" | "repv" | "elem_of" | "elof" | "not_elem" | "ntelm"
14941            | "lookup_assoc" | "lkpa" | "scanl" | "scanr" | "unfoldr" | "unfr"
14942            // ── rust iterator methods ───────────────────────────────────────
14943            | "find_map" | "fndm" | "filter_map" | "fltm" | "fold_right" | "fldr"
14944            | "partition_either" | "peith" | "try_fold" | "tfld"
14945            | "map_while" | "mapw" | "inspect" | "insp"
14946            // ── ruby enumerable extras ──────────────────────────────────────
14947            | "tally_by" | "talb" | "sole" | "chunk_while" | "chkw" | "count_while" | "cntw"
14948            // ── go/general functional utilities ─────────────────────────────
14949            | "insert_at" | "insa" | "delete_at" | "dela" | "update_at" | "upda"
14950            | "split_on" | "spon" | "words_from" | "wfrm" | "unwords" | "unwds"
14951            | "lines_from" | "lfrm" | "unlines" | "unlns"
14952            | "window_n" | "winn" | "adjacent_pairs" | "adjp"
14953            | "zip_all" | "zall" | "unzip_pairs" | "uzp"
14954            | "interpose" | "ipos" | "partition_n" | "partn"
14955            | "map_indexed" | "mapi" | "reduce_indexed" | "redi" | "filter_indexed" | "flti"
14956            | "group_by_fn" | "gbf" | "index_by" | "idxb" | "associate" | "assoc_fn"
14957            // ── additional missing stdlib functions ─────────────────────────
14958            | "combinations_rep" | "combrep" | "inits" | "tails" | "subsequences" | "subseqs"
14959            | "nub_by" | "nubb" | "slice_when" | "slcw" | "slice_before" | "slcb" | "slice_after" | "slca"
14960            | "each_with_object" | "ewo" | "reduce_right" | "redr"
14961            | "is_sorted_by" | "issrtb" | "intersperse_with" | "ispw"
14962            | "running_reduce" | "runred" | "windowed_circular" | "wincirc"
14963            | "distinct_by" | "distb" | "average" | "mean" | "copy_within" | "cpyw"
14964            | "and_list" | "andl" | "or_list" | "orl" | "concat_map" | "cmap"
14965            | "elem_index" | "elidx" | "elem_indices" | "elidxs" | "find_indices" | "fndidxs"
14966            | "delete_first" | "delfst" | "delete_by" | "delby" | "insert_sorted" | "inssrt"
14967            | "union_list" | "unionl" | "intersect_list" | "intl"
14968            | "maximum_by" | "maxby" | "minimum_by" | "minby" | "batched" | "btch"
14969            // ── Extended stdlib: Text Processing ─────────────────────────────
14970            | "match_all" | "mall" | "capture_groups" | "capg" | "is_match" | "ism"
14971            | "split_regex" | "splre" | "replace_regex" | "replre"
14972            | "is_ascii" | "isasc" | "to_ascii" | "toasc"
14973            | "char_at" | "chat" | "code_point_at" | "cpat" | "from_code_point" | "fcp"
14974            | "normalize_spaces" | "nrmsp" | "remove_whitespace" | "rmws"
14975            | "pluralize" | "plur" | "ordinalize" | "ordn"
14976            | "parse_int" | "pint" | "parse_float" | "pflt" | "parse_bool" | "pbool"
14977            | "levenshtein" | "lev" | "soundex" | "sdx" | "similarity" | "sim"
14978            | "common_prefix" | "cpfx" | "common_suffix" | "csfx"
14979            | "wrap_text" | "wrpt" | "dedent" | "ddt" | "indent" | "idt"
14980            // ── Extended stdlib: Advanced Numeric ────────────────────────────
14981            | "lerp" | "inv_lerp" | "ilerp" | "smoothstep" | "smst" | "remap"
14982 | "dotp" | "cross_product" | "crossp"
14983            | "matrix_mul" | "matmul" | "mm"
14984            | "magnitude" | "mag" | "normalize_vec" | "nrmv"
14985            | "distance" | "dist" | "mdist"
14986            | "covariance" | "cov" | "correlation" | "corr"
14987            | "iqr" | "quantile" | "qntl" | "quantiles" | "qntls"
14988            | "lsp_completion_words" | "lsp_words"
14989            | "doctor" | "health"
14990            | "clamp_int" | "clpi"
14991            | "in_range" | "inrng" | "wrap_range" | "wrprng"
14992            | "sum_squares" | "sumsq" | "rms" | "cumsum" | "csum" | "cumprod" | "cprod_acc" | "diff"
14993            // ── Extended stdlib: Date/Time ───────────────────────────────────
14994            | "add_days" | "addd" | "add_hours" | "addh" | "add_minutes" | "addm"
14995            | "diff_days" | "diffd" | "diff_hours" | "diffh"
14996            | "start_of_day" | "sod" | "end_of_day" | "eod"
14997            | "start_of_hour" | "soh" | "start_of_minute" | "som"
14998            // ── Extended stdlib: Encoding/Hashing ────────────────────────────
14999            | "urle" | "urld"
15000            | "html_encode" | "htmle" | "html_decode" | "htmld"
15001            | "adler32" | "adl32" | "fnv1a" | "djb2"
15002            // ── Extended stdlib: Validation ──────────────────────────────────
15003            | "is_credit_card" | "iscc" | "is_isbn10" | "isbn10" | "is_isbn13" | "isbn13"
15004            | "is_iban" | "isiban" | "is_hex_str" | "ishex" | "is_binary_str" | "isbin"
15005            | "is_octal_str" | "isoct" | "is_json" | "isjson" | "is_base64" | "isb64"
15006            | "is_semver" | "issv" | "is_slug" | "isslug" | "slugify" | "slug"
15007            // ── Extended stdlib: Collection Advanced ─────────────────────────
15008            | "mode_stat" | "mstat" | "sampn" | "weighted_sample" | "wsamp"
15009            | "shuffle_arr" | "shuf" | "argmax" | "amax" | "argmin" | "amin"
15010            | "argsort" | "asrt" | "rank" | "rnk" | "dense_rank" | "drnk"
15011            | "partition_point" | "ppt" | "lower_bound" | "lbound"
15012            | "upper_bound" | "ubound" | "equal_range" | "eqrng"
15013            // ── Extended stdlib: Matrix Operations ───────────────────────────
15014            | "matrix_add" | "madd" | "matrix_sub" | "msub" | "matrix_mult" | "mmult"
15015            | "matrix_scalar" | "mscal" | "matrix_identity" | "mident"
15016            | "matrix_zeros" | "mzeros" | "matrix_ones" | "mones"
15017            | "matrix_diag" | "mdiag" | "matrix_trace" | "mtrace"
15018            | "matrix_row" | "mrow" | "matrix_col" | "mcol"
15019            | "matrix_shape" | "mshape" | "matrix_det" | "mdet"
15020            | "matrix_scale" | "mat_scale" | "diagonal" | "diag"
15021            // ── Extended stdlib: Graph Algorithms ────────────────────────────
15022            | "topological_sort" | "toposort" | "bfs_traverse" | "bfs"
15023            | "dfs_traverse" | "dfs" | "shortest_path_bfs" | "spbfs"
15024            | "connected_components_graph" | "ccgraph"
15025            | "has_cycle_graph" | "hascyc" | "is_bipartite_graph" | "isbip"
15026            // ── Extended stdlib: Data Validation ─────────────────────────────
15027            | "is_ipv4_addr" | "isip4" | "is_ipv6_addr" | "isip6"
15028            | "is_mac_addr" | "ismac" | "is_port_num" | "isport"
15029            | "is_hostname_valid" | "ishost"
15030            | "is_iso_date" | "isisodt" | "is_iso_time" | "isisotm"
15031            | "is_iso_datetime" | "isisodtm"
15032            | "is_phone_num" | "isphone" | "is_us_zip" | "iszip"
15033            // ── Extended stdlib: String Utilities Novel ──────────────────────
15034            | "word_wrap_text" | "wwrap" | "center_text" | "ctxt"
15035            | "ljust_text" | "ljt" | "rjust_text" | "rjt" | "zfill_num" | "zfill"
15036            | "remove_all_str" | "rmall" | "replace_n_times" | "repln"
15037            | "find_all_indices" | "fndalli"
15038            | "text_between" | "txbtwn" | "text_before" | "txbef" | "text_after" | "txaft"
15039            | "text_before_last" | "txbefl" | "text_after_last" | "txaftl"
15040            // ── Extended stdlib: Math Novel ──────────────────────────────────
15041            | "is_even_num" | "iseven" | "is_odd_num" | "isodd"
15042            | "is_positive_num" | "ispos" | "is_negative_num" | "isneg"
15043            | "is_zero_num" | "iszero" | "is_whole_num" | "iswhole"
15044            | "log_with_base" | "logb" | "nth_root_of" | "nroot"
15045            | "frac_part" | "fracp" | "reciprocal_of" | "recip"
15046            | "copy_sign" | "cpsgn" | "fused_mul_add" | "fmadd"
15047            | "floor_mod" | "fmod" | "floor_div_op" | "fdivop"
15048            | "signum_of" | "sgnum" | "midpoint_of" | "midpt"
15049            // ── Extended stdlib: Array Analysis ──────────────────────
15050            | "longest_run" | "lrun" | "longest_increasing" | "linc"
15051            | "longest_decreasing" | "ldec" | "max_sum_subarray" | "maxsub"
15052            | "majority_element" | "majority" | "kth_largest" | "kthl"
15053            | "kth_smallest" | "kths" | "count_inversions" | "cinv"
15054            | "is_monotonic" | "ismono" | "equilibrium_index" | "eqidx"
15055            // ── Extended stdlib: Set Operations ──────────────────────
15056            | "jaccard_index" | "jaccard" | "dice_coefficient" | "dicecoef"
15057            | "overlap_coefficient" | "overlapcoef"
15058            | "power_set" | "powerset" | "cartesian_power" | "cartpow"
15059            // ── Extended stdlib: Advanced String ─────────────────────
15060            | "is_isogram" | "isiso" | "is_heterogram" | "ishet"
15061            | "hamdist" | "jaro_similarity" | "jarosim"
15062            | "longest_common_substring" | "lcsub"
15063            | "longest_common_subsequence" | "lcseq"
15064            | "count_words" | "wcount" | "count_lines" | "lcount"
15065            | "count_chars" | "ccount" | "count_bytes" | "bcount"
15066            // ── Extended stdlib: More Math ───────────────────────────
15067            | "binomial" | "binom" | "catalan" | "catn" | "pascal_row" | "pascrow"
15068            | "is_coprime" | "iscopr" | "euler_totient" | "etot"
15069            | "mobius" | "mob" | "is_squarefree" | "issqfr"
15070            | "digital_root" | "digroot" | "is_narcissistic" | "isnarc"
15071            | "is_harshad" | "isharsh" | "is_kaprekar" | "iskap"
15072            // ── Extended stdlib: Date/Time Additional ────────────────
15073            | "day_of_year" | "doy" | "week_of_year" | "woy"
15074            | "days_in_month_fn" | "daysinmo" | "is_valid_date" | "isvdate"
15075            | "age_in_years" | "ageyrs"
15076            // ── functional combinators ──────────────────────────────────────
15077
15078            | "when_true" | "when_false" | "if_else" | "clamp_fn"
15079            | "attempt" | "try_fn" | "safe_div" | "safe_mod" | "safe_sqrt" | "safe_log"
15080            | "juxt2" | "juxt3" | "tap_val" | "debug_val" | "converge"
15081            | "iterate_n" | "unfold" | "arity_of" | "is_callable"
15082            | "coalesce" | "default_to" | "fallback"
15083            | "apply_list" | "zip_apply" | "scan"
15084            | "keep_if" | "reject_if" | "group_consecutive"
15085            | "after_n" | "before_n" | "clamp_list" | "normalize_list"
15086
15087            // ── matrix / linear algebra ─────────────────────────────────────
15088
15089
15090            | "matrix_multiply" | "mat_mul"
15091            | "identity_matrix" | "eye" | "zeros_matrix" | "zeros" | "ones_matrix" | "ones"
15092
15093
15094
15095            | "vec_normalize" | "unit_vec" | "vec_add" | "vec_sub" | "vec_scale"
15096            | "linspace" | "arange"
15097            // ── more regex ──────────────────────────────────────────────────
15098            | "re_test" | "re_find_all" | "re_groups" | "re_escape"
15099            | "re_split_limit" | "glob_to_regex" | "is_regex_valid"
15100            // ── more process / system ───────────────────────────────────────
15101            | "cwd" | "pwd_str" | "cpu_count" | "is_root" | "uptime_secs"
15102            | "env_pairs" | "env_set" | "env_remove" | "hostname_str" | "is_tty" | "signal_name"
15103            // ── data structure helpers ───────────────────────────────────────
15104            | "stack_new" | "queue_new" | "lru_new"
15105            | "counter" | "counter_most_common" | "defaultdict" | "ordered_set"
15106            | "bitset_new" | "bitset_set" | "bitset_test" | "bitset_clear"
15107            // ── trivial numeric helpers ─────────────────────────────
15108            | "abs_ceil" | "abs_each" | "abs_floor" | "ceil_each" | "dec_each"
15109            | "double_each" | "floor_each" | "half_each" | "inc_each" | "length_each"
15110            | "negate_each" | "not_each" | "offset_each" | "reverse_each" | "round_each"
15111            | "scale_each" | "sqrt_each" | "square_each" | "to_float_each" | "to_int_each"
15112            | "trim_each" | "type_each" | "upcase_each" | "downcase_each" | "bool_each"
15113            // ── math / physics constants ──────────────────────────────────────
15114            | "avogadro" | "boltzmann" | "golden_ratio" | "gravity" | "ln10" | "ln2"
15115            | "planck" | "speed_of_light" | "sqrt2"
15116            // ── physics formulas ──────────────────────────────────────────────
15117            | "bmi_calc" | "compound_interest" | "dew_point" | "discount_amount"
15118            | "force_mass_acc" | "freq_wavelength" | "future_value" | "haversine"
15119            | "heat_index" | "kinetic_energy" | "margin_price" | "markup_price"
15120            | "mortgage_payment" | "ohms_law_i" | "ohms_law_r" | "ohms_law_v"
15121            | "potential_energy" | "present_value" | "simple_interest" | "speed_distance_time"
15122            | "tax_amount" | "tip_amount" | "wavelength_freq" | "wind_chill"
15123            // ── math functions ────────────────────────────────────────────────
15124            | "angle_between_deg" | "approx_eq" | "chebyshev_distance" | "copysign"
15125 | "cube_root" | "entropy" | "float_bits" | "fma"
15126            | "int_bits" | "jaccard_similarity" | "log_base" | "mae" | "mse" | "nth_root"
15127            | "r_squared" | "reciprocal" | "relu" | "rmse" | "rotate_point" | "round_to"
15128 | "signum" | "square_root"
15129            // ── sequences ─────────────────────────────────────────────────────
15130            | "cubes_seq" | "fibonacci_seq" | "powers_of_seq" | "primes_seq"
15131            | "squares_seq" | "triangular_seq"
15132            // ── string helpers ──────────────────────────────────────
15133            | "alternate_case" | "angle_bracket" | "bracket" | "byte_length"
15134            | "bytes_to_hex_str" | "camel_words" | "char_length" | "chars_to_string"
15135            | "chomp_str" | "chop_str" | "filter_chars" | "from_csv_line" | "hex_to_bytes"
15136            | "insert_str" | "intersperse_char" | "ljust" | "map_chars" | "mirror_string"
15137            | "normalize_whitespace" | "only_alnum" | "only_alpha" | "only_ascii"
15138            | "only_digits" | "parenthesize" | "remove_str" | "repeat_string" | "rjust"
15139            | "sentence_case" | "string_count" | "string_sort" | "string_to_chars"
15140            | "string_unique_chars" | "substring" | "to_csv_line" | "trim_left" | "trim_right"
15141            | "xor_strings"
15142            // ── list helpers ─────────────────────────────────────────
15143            | "adjacent_difference" | "append_elem" | "consecutive_pairs" | "contains_elem"
15144            | "count_elem" | "drop_every" | "duplicate_count" | "elem_at" | "find_first"
15145            | "first_elem" | "flatten_once" | "fold_left" | "from_digits" | "from_pairs"
15146            | "group_by_size" | "hash_from_list"
15147            | "hash_merge_deep" | "hash_to_list" | "hash_zip" | "head_n" | "histogram_bins"
15148            | "index_of_elem" | "init_list" | "interleave_lists" | "last_elem" | "least_common"
15149            | "list_compact" | "list_eq" | "list_flatten_deep" | "max_list" | "mean_list"
15150            | "min_list" | "mode_list" | "most_common" | "partition_two" | "prefix_sums"
15151            | "prepend" | "product_list" | "remove_at" | "remove_elem" | "remove_first_elem"
15152            | "repeat_elem" | "running_max" | "running_min" | "sample_one" | "scan_left"
15153            | "second_elem" | "span" | "suffix_sums" | "sum_list" | "tail_n" | "take_every"
15154            | "third_elem" | "to_array" | "to_pairs" | "trimmed_mean" | "unique_count_of"
15155            | "wrap_index" | "digits_of"
15156            // ── predicates ──────────────────────────────────────────
15157            | "all_match" | "any_match" | "is_between" | "is_blank_or_nil" | "is_divisible_by"
15158            | "is_email" | "is_even" | "is_falsy" | "is_fibonacci" | "is_hex_color"
15159            | "is_in_range" | "is_ipv4" | "is_multiple_of" | "is_negative" | "is_nil"
15160            | "is_nonzero" | "is_odd" | "is_perfect_square" | "is_positive" | "is_power_of"
15161            | "is_prefix" | "is_present" | "is_strictly_decreasing" | "is_strictly_increasing"
15162            | "is_suffix" | "is_triangular" | "is_truthy" | "is_url" | "is_whole" | "is_zero"
15163            // ── counters ────────────────────────────────────────────
15164            | "count_digits" | "count_letters" | "count_lower" | "count_match"
15165            | "count_punctuation" | "count_spaces" | "count_upper" | "defined_count"
15166            | "empty_count" | "falsy_count" | "nonempty_count" | "numeric_count"
15167            | "truthy_count" | "undef_count"
15168            // ── conversion / utility ────────────────────────────────
15169            | "assert_type" | "between" | "clamp_each" | "die_if" | "die_unless"
15170            | "join_colons" | "join_commas" | "join_dashes" | "join_dots" | "join_lines"
15171            | "join_pipes" | "join_slashes" | "join_spaces" | "join_tabs" | "measure"
15172            | "max_float" | "min_float" | "noop_val" | "nop" | "pass" | "pred" | "succ"
15173            | "tap_debug" | "to_bool" | "to_float" | "to_int" | "to_string" | "void"
15174            | "range_exclusive" | "range_inclusive"
15175            // ── math / numeric extras ─────────────────────────────────────────
15176            | "aliquot_sum" | "autocorrelation" | "bell_number" | "cagr" | "coeff_of_variation"
15177            | "collatz_length" | "collatz_sequence" | "convolution"
15178            | "depreciation_double" | "depreciation_linear" | "discount" | "divisors"
15179            | "epsilon" | "euler_number" | "exponential_moving_average"
15180            | "f64_max" | "f64_min" | "fft_magnitude" | "goldbach" | "i64_max" | "i64_min"
15181            | "kurtosis" | "linear_regression" | "look_and_say" | "lucas" | "luhn_check"
15182            | "mean_absolute_error" | "mean_squared_error" | "median_absolute_deviation"
15183            | "minkowski_distance" | "moving_average" | "multinomial" | "neg_inf" | "npv"
15184            | "num_divisors" | "partition_number" | "pascals_triangle" | "skewness"
15185            | "standard_error" | "subfactorial" | "sum_divisors" | "totient_sum"
15186            | "tribonacci" | "weighted_mean" | "winsorize"
15187            // ── statistics (extended) ─────────────────────────────────────────
15188            | "chi_square_stat" | "describe" | "five_number_summary"
15189            | "gini" | "gini_coefficient" | "lorenz_curve" | "outliers_iqr"
15190            | "percentile_rank" | "quartiles" | "sample_stddev" | "sample_variance"
15191            | "spearman_correlation" | "t_test_one_sample" | "t_test_two_sample"
15192            | "z_score" | "z_scores"
15193            // ── number theory / primes ──────────────────────────────────────────
15194            | "abundant_numbers" | "deficient_numbers" | "is_abundant" | "is_deficient"
15195            | "is_pentagonal" | "is_perfect" | "is_smith" | "next_prime" | "nth_prime"
15196            | "pentagonal_number" | "perfect_numbers" | "prev_prime" | "prime_factors"
15197            | "prime_pi" | "primes_up_to" | "triangular_number" | "twin_primes"
15198            // ── geometry / physics ──────────────────────────────────────────────
15199            | "area_circle" | "area_ellipse" | "area_rectangle" | "area_trapezoid" | "area_triangle"
15200            | "bearing" | "circumference" | "cone_volume" | "cylinder_volume" | "heron_area"
15201            | "midpoint" | "perimeter_rectangle" | "perimeter_triangle" | "point_distance"
15202            | "polygon_area" | "slope" | "sphere_surface" | "sphere_volume" | "triangle_hypotenuse"
15203            // ── geometry (extended) ───────────────────────────────────────────
15204            | "angle_between" | "arc_length" | "bounding_box" | "centroid"
15205            | "circle_from_three_points" | "circ3" | "convex_hull" | "ellipse_perimeter" | "ellper"
15206            | "frustum_volume" | "haversine_distance" | "line_intersection"
15207            | "point_in_polygon" | "pip" | "polygon_perimeter" | "polyper" | "pyramid_volume"
15208            | "reflect_point" | "scale_point" | "sector_area"
15209            | "torus_surface" | "torus_volume" | "translate_point"
15210            | "vector_angle" | "vector_cross" | "vector_dot" | "vector_magnitude" | "vector_normalize"
15211            // ── constants ───────────────────────────────────────────────────────
15212            | "avogadro_number" | "boltzmann_constant" | "electron_mass" | "elementary_charge"
15213            | "gravitational_constant" | "phi" | "pi" | "PI" | "planck_constant"
15214            | "proton_mass" | "sol" | "tau" | "TAU" | "E"
15215            // ── finance ─────────────────────────────────────────────────────────
15216            | "bac_estimate" | "bmi" | "break_even" | "margin" | "markup" | "roi" | "tax" | "tip"
15217            // ── finance (extended) ────────────────────────────────────────────
15218            | "amortization_schedule" | "black_scholes_call" | "black_scholes_put"
15219            | "bond_price" | "bond_yield" | "capm" | "continuous_compound" | "ccomp"
15220            | "discounted_payback" | "duration" | "irr"
15221            | "max_drawdown" | "mdd" | "modified_duration" | "mod_dur" | "nper" | "num_periods" | "payback_period"
15222            | "pmt" | "pv" | "rule_of_72" | "sharpe_ratio" | "sortino_ratio"
15223            | "wacc" | "xirr"
15224            // ── string processing extras ──────────────────────────────────────
15225            | "acronym" | "atbash" | "bigrams" | "camel_to_snake" | "char_frequencies"
15226            | "chunk_string" | "collapse_whitespace" | "dedent_text" | "indent_text"
15227            | "initials" | "leetspeak" | "mask_string" | "ngrams" | "pig_latin"
15228            | "remove_consonants" | "remove_vowels" | "reverse_each_word" | "snake_to_camel"
15229            | "sort_words" | "string_distance" | "string_multiply" | "strip_html"
15230            | "trigrams" | "unique_words" | "word_frequencies" | "zalgo"
15231            // ── encoding / phonetics ────────────────────────────────────────────
15232            | "braille_encode" | "double_metaphone" | "metaphone" | "morse_decode"
15233            | "morse_encode" | "nato_phonetic" | "phonetic_digit" | "subscript" | "superscript"
15234            | "to_emoji_num"
15235            // ── roman numerals ──────────────────────────────────────────────────
15236            | "int_to_roman" | "roman_add" | "roman_numeral_list" | "roman_to_int"
15237            // ── base / gray code ────────────────────────────────────────────────
15238            | "base_convert" | "binary_to_gray" | "gray_code_sequence" | "gray_to_binary"
15239            // ── color operations ────────────────────────────────────────────────
15240            | "ansi_256" | "ansi_truecolor" | "color_blend" | "color_complement"
15241            | "color_darken" | "color_distance" | "color_grayscale" | "color_invert"
15242            | "color_lighten" | "hsl_to_rgb" | "hsv_to_rgb" | "random_color"
15243            | "rgb_to_hsl" | "rgb_to_hsv"
15244            // ── matrix operations extras ──────────────────────────────────────
15245            | "matrix_flatten" | "matrix_from_rows" | "matrix_hadamard" | "matrix_inverse"
15246            | "matrix_map" | "matrix_max" | "matrix_min" | "matrix_power" | "matrix_sum"
15247            | "matrix_transpose"
15248            // ── array / list operations extras ────────────────────────────────
15249            | "binary_insert" | "bucket" | "clamp_array" | "group_consecutive_by"
15250            | "histogram" | "merge_sorted" | "next_permutation" | "normalize_array"
15251            | "normalize_range" | "peak_detect" | "range_compress" | "range_expand"
15252            | "reservoir_sample" | "run_length_decode_str" | "run_length_encode_str"
15253            | "zero_crossings"
15254            // ── DSP / signal (extended) ───────────────────────────────────────
15255            | "apply_window" | "bandpass_filter" | "cross_correlation" | "dft"
15256            | "downsample" | "decimate" | "energy" | "envelope" | "hilbert_env" | "highpass_filter" | "idft"
15257            | "lowpass_filter" | "median_filter" | "normalize_signal" | "phase_spectrum"
15258            | "power_spectrum" | "psd" | "resample" | "spectral_centroid" | "spectrogram" | "stft" | "upsample" | "interpolate"
15259            | "window_blackman" | "window_hamming" | "window_hann" | "window_kaiser"
15260            // ── validation predicates extras ──────────────────────────────────
15261            | "is_anagram" | "is_balanced_parens" | "is_control" | "is_numeric_string"
15262            | "is_pangram" | "is_printable" | "is_valid_cidr" | "is_valid_cron"
15263            | "is_valid_hex_color" | "is_valid_latitude" | "is_valid_longitude" | "is_valid_mime"
15264            // ── algorithms / puzzles ────────────────────────────────────────────
15265            | "eval_rpn" | "fizzbuzz" | "game_of_life_step" | "mandelbrot_char"
15266            | "sierpinski" | "tower_of_hanoi" | "truth_table"
15267            // ── misc / utility ──────────────────────────────────────────────────
15268            | "byte_size" | "degrees_to_compass" | "to_string_val" | "type_of"
15269            // ── math formulas ───────────────────────────────────────────────────
15270            | "quadratic_roots" | "quadratic_discriminant" | "arithmetic_series"
15271            | "geometric_series" | "stirling_approx"
15272            | "double_factorial" | "rising_factorial" | "falling_factorial"
15273            | "gamma_approx" | "erf_approx" | "normal_pdf" | "normal_cdf"
15274            | "poisson_pmf" | "exponential_pdf" | "inverse_lerp"
15275            | "map_range"
15276            // ── physics formulas ────────────────────────────────────────────────
15277            | "momentum" | "impulse" | "work" | "power_phys" | "torque" | "angular_velocity"
15278            | "centripetal_force" | "escape_velocity" | "orbital_velocity" | "orbital_period"
15279            | "gravitational_force" | "coulomb_force" | "electric_field" | "capacitance"
15280            | "capacitor_energy" | "inductor_energy" | "resonant_frequency"
15281            | "rc_time_constant" | "rl_time_constant" | "impedance_rlc"
15282            | "relativistic_mass" | "lorentz_factor" | "time_dilation" | "length_contraction"
15283            | "relativistic_energy" | "rest_energy" | "de_broglie_wavelength"
15284            | "photon_energy" | "photon_energy_wavelength" | "schwarzschild_radius"
15285            | "stefan_boltzmann" | "wien_displacement" | "ideal_gas_pressure" | "ideal_gas_volume"
15286            | "projectile_range" | "projectile_max_height" | "projectile_time"
15287            | "spring_force" | "spring_energy" | "pendulum_period" | "doppler_frequency"
15288            | "decibel_ratio" | "snells_law" | "brewster_angle" | "critical_angle"
15289            | "lens_power" | "thin_lens" | "magnification_lens"
15290            // ── math constants ──────────────────────────────────────────────────
15291            | "euler_mascheroni" | "apery_constant" | "feigenbaum_delta" | "feigenbaum_alpha"
15292            | "catalan_constant" | "khinchin_constant" | "glaisher_constant"
15293            | "plastic_number" | "silver_ratio" | "supergolden_ratio"
15294            // ── physics constants ───────────────────────────────────────────────
15295            | "vacuum_permittivity" | "vacuum_permeability" | "coulomb_constant"
15296            | "fine_structure_constant" | "rydberg_constant" | "bohr_radius"
15297            | "bohr_magneton" | "nuclear_magneton" | "stefan_boltzmann_constant"
15298            | "wien_constant" | "gas_constant" | "faraday_constant" | "neutron_mass"
15299            | "atomic_mass_unit" | "earth_mass" | "earth_radius" | "sun_mass" | "sun_radius"
15300            | "astronomical_unit" | "light_year" | "parsec" | "hubble_constant"
15301            | "planck_length" | "planck_time" | "planck_mass" | "planck_temperature"
15302            // ── linear algebra (extended) ──────────────────────────────────
15303            | "matrix_solve" | "msolve" | "solve"
15304            | "matrix_lu" | "mlu" | "matrix_qr" | "mqr"
15305            | "matrix_eigenvalues" | "meig" | "eigenvalues" | "eig"
15306            | "matrix_norm" | "mnorm" | "matrix_cond" | "mcond" | "cond"
15307            | "matrix_pinv" | "mpinv" | "pinv"
15308            | "matrix_cholesky" | "mchol" | "cholesky"
15309            | "matrix_det_general" | "mdetg" | "det"
15310            // ── statistics tests (extended) ────────────────────────────────
15311            | "welch_ttest" | "welcht" | "paired_ttest" | "pairedt"
15312            | "cohen_d" | "cohend" | "anova_oneway" | "anova" | "anova1"
15313            | "spearman_corr" | "rho" | "kendall_tau" | "kendall" | "ktau"
15314            | "confidence_interval" | "ci"
15315            // ── distributions (extended) ──────────────────────────────────
15316            | "beta_pdf" | "betapdf" | "gamma_pdf" | "gammapdf"
15317            | "chi2_pdf" | "chi2pdf" | "chi_squared_pdf"
15318            | "t_pdf" | "tpdf" | "student_pdf"
15319            | "f_pdf" | "fpdf" | "fisher_pdf"
15320            | "lognormal_pdf" | "lnormpdf" | "weibull_pdf" | "weibpdf"
15321            | "cauchy_pdf" | "cauchypdf" | "laplace_pdf" | "laplacepdf"
15322            | "pareto_pdf" | "paretopdf"
15323            // ── interpolation & curve fitting ─────────────────────────────
15324            | "lagrange_interp" | "lagrange" | "linterp"
15325            | "cubic_spline" | "cspline" | "spline"
15326            | "poly_eval" | "polyval" | "polynomial_fit" | "polyfit"
15327            // ── numerical integration & differentiation ───────────────────
15328            | "trapz" | "trapezoid" | "simpson" | "simps"
15329            | "numerical_diff" | "numdiff" | "diff_array"
15330            | "cumtrapz" | "cumulative_trapz"
15331            // ── optimization / root finding ────────────────────────────────
15332            | "bisection" | "bisect" | "newton_method" | "newton" | "newton_raphson"
15333            | "golden_section" | "golden" | "gss"
15334            // ── ODE solvers ───────────────────────────────────────────────
15335            | "rk4" | "runge_kutta" | "rk4_ode" | "euler_ode" | "euler_method"
15336            // ── graph algorithms (extended) ────────────────────────────────
15337            | "dijkstra" | "shortest_path" | "bellman_ford" | "bellmanford"
15338            | "floyd_warshall" | "floydwarshall" | "apsp"
15339            | "prim_mst" | "mst" | "prim"
15340            // ── trig extensions ───────────────────────────────────────────
15341            | "cot" | "sec" | "csc" | "acot" | "asec" | "acsc" | "sinc" | "versin" | "versine"
15342            // ── ML activation functions ───────────────────────────────────
15343            | "leaky_relu" | "lrelu" | "elu" | "selu" | "gelu"
15344            | "silu" | "swish" | "mish" | "softplus"
15345            | "hard_sigmoid" | "hardsigmoid" | "hard_swish" | "hardswish"
15346            // ── special functions ─────────────────────────────────────────
15347            | "bessel_j0" | "j0" | "bessel_j1" | "j1"
15348            | "lambert_w" | "lambertw" | "productlog"
15349            // ── Wolfram-Math parity: Bessel/Airy/Hankel/Struve/Kelvin ─────
15350            | "bessel_j" | "bessel_y" | "bessel_i" | "bessel_k"
15351            | "hankel_h1" | "hankel_h2" | "bessel_j_zero"
15352            | "airy_ai" | "airy_bi" | "airy_ai_prime" | "airy_bi_prime"
15353            | "spherical_bessel_j" | "spherical_bessel_y"
15354            | "struve_h" | "struve_l" | "kelvin_ber" | "kelvin_bei"
15355            // ── orthogonal polynomials ────────────────────────────────────
15356            | "legendre_p" | "legendre_q" | "assoc_legendre_p"
15357            | "hermite_h" | "hermite_he" | "laguerre_l" | "assoc_laguerre_l"
15358            | "jacobi_p" | "gegenbauer_c" | "chebyshev_t" | "chebyshev_u"
15359            | "spherical_harmonic_y" | "zernike_r"
15360            // ── elliptic integrals + Jacobi/Weierstrass/theta ─────────────
15361            | "elliptic_k" | "elliptic_e" | "elliptic_pi" | "elliptic_f"
15362            | "elliptic_e_inc" | "elliptic_pi_inc"
15363            | "carlson_rf" | "carlson_rd" | "carlson_rj"
15364            | "jacobi_sn" | "jacobi_cn" | "jacobi_dn" | "jacobi_am"
15365            | "elliptic_theta"
15366            | "weierstrass_p" | "weierstrass_zeta" | "weierstrass_sigma"
15367            // ── zeta / polylog / Lerch ────────────────────────────────────
15368            | "zeta" | "riemann_zeta" | "hurwitz_zeta"
15369            | "polylog" | "dilog" | "lerch_phi"
15370            | "riemann_siegel_z" | "riemann_siegel_theta"
15371            | "dirichlet_eta" | "dirichlet_beta"
15372            // ── hypergeometric ────────────────────────────────────────────
15373            | "hypergeometric_2f1" | "hyper_2f1"
15374            | "hypergeometric_1f1" | "hyper_1f1" | "kummer_m"
15375            | "hypergeometric_0f1" | "hyper_0f1"
15376            | "hypergeometric_pfq" | "hyper_pfq"
15377            | "hypergeometric_u" | "tricomi_u"
15378            // ── modular forms ─────────────────────────────────────────────
15379            | "dedekind_eta" | "klein_j" | "klein_invariant_j"
15380            | "modular_lambda" | "ramanujan_tau"
15381            // ── integrals: Si / Ci / Ei / Li / Fresnel ────────────────────
15382            | "sin_integral" | "si_int" | "cos_integral" | "ci_int"
15383            | "sinh_integral" | "shi_int" | "cosh_integral" | "chi_int"
15384            | "exp_integral_e" | "ei_n" | "exp_integral_ei" | "ei_int"
15385            | "log_integral" | "li_int" | "fresnel_s" | "fresnel_c"
15386            // ── number-theory gaps ────────────────────────────────────────
15387            | "jacobi_symbol" | "kronecker_symbol"
15388            | "primitive_root" | "multiplicative_order"
15389            | "mangoldt_lambda" | "von_mangoldt" | "carmichael_lambda"
15390            | "squares_r" | "thue_morse" | "rudin_shapiro"
15391            | "farey_sequence" | "farey"
15392            | "frobenius_number" | "frobenius_solve" | "stern_brocot"
15393            // ── combinatorial gaps ────────────────────────────────────────
15394            | "stirling_s1" | "stirling_first" | "bell_polynomial_b" | "bell_y"
15395            | "clebsch_gordan" | "three_j_symbol" | "wigner_3j"
15396            | "six_j_symbol" | "wigner_6j" | "nine_j_symbol" | "wigner_9j"
15397            | "debruijn_sequence" | "debruijn" | "wigner_d"
15398            // ── q-series, Mittag-Leffler, Coulomb wave ────────────────────
15399            | "q_pochhammer" | "q_factorial" | "q_binomial"
15400            | "q_hypergeometric_pfq"
15401            | "mittag_leffler_e" | "mittag_leffler"
15402            | "coulomb_wave_f" | "coulomb_wave_g"
15403            // ── inverse special functions ─────────────────────────────────
15404            | "inverse_erf" | "erfinv" | "inverse_erfc" | "erfcinv"
15405            | "inverse_gamma_regularized" | "gamma_lr_inv"
15406            | "inverse_beta_regularized" | "beta_reg_inv"
15407            | "inverse_jacobi_sn"
15408            // ── piecewise / symbolic primitives ───────────────────────────
15409            | "dirac_delta" | "heaviside_theta" | "heaviside"
15410            | "unit_box" | "unit_triangle"
15411            | "square_wave" | "triangle_wave" | "sawtooth_wave" | "dirac_comb"
15412            // ── Tier A: number theory extensions ──────────────────────────
15413            | "liouville_lambda" | "jordan_totient" | "ramanujan_sum"
15414            | "cyclotomic_polynomial" | "cyclotomic" | "legendre_symbol"
15415            | "pythagorean_triple_q" | "gen_pythagorean_triple"
15416            | "sophie_germain_q" | "mersenne_q"
15417            | "lucas_lehmer_test" | "lucas_lehmer"
15418            | "continued_fraction" | "from_continued_fraction" | "convergents"
15419            | "best_rational_approximation" | "best_rational"
15420            // ── Tier B: combinatorial sequences ───────────────────────────
15421            | "motzkin_number" | "motzkin"
15422            | "narayana_number" | "narayana"
15423            | "delannoy_number" | "delannoy"
15424            | "schroder_number" | "schroder" | "large_schroder"
15425            | "small_schroder_number" | "small_schroder"
15426            | "eulerian_number"
15427            | "bernoulli_polynomial" | "euler_polynomial"
15428            | "pell_number" | "pell" | "pell_lucas_number" | "pell_lucas"
15429            | "perrin_number" | "perrin" | "padovan_number" | "padovan"
15430            // ── Tier C: linear algebra extras ─────────────────────────────
15431            | "kronecker_product" | "tensor_product" | "tensor_contract"
15432            | "matrix_rank" | "mrank"
15433            | "companion_matrix" | "companion"
15434            | "characteristic_polynomial" | "charpoly"
15435            | "singular_values" | "svals"
15436            | "nullspace" | "null_space" | "kernel"
15437            // ── Tier D: polynomial algebra ────────────────────────────────
15438            | "polynomial_gcd" | "polygcd"
15439            | "polynomial_quotient" | "polyquot"
15440            | "polynomial_remainder" | "polyrem"
15441            | "polynomial_resultant" | "resultant"
15442            | "polynomial_discriminant" | "discriminant"
15443            | "polynomial_roots" | "polyroots"
15444            // ── Tier E: more distributions ────────────────────────────────
15445            | "gumbel_pdf" | "gumbel_cdf" | "gumbel_quantile"
15446            | "frechet_pdf" | "frechet_cdf" | "frechet_quantile"
15447            | "logistic_pdf" | "logistic_cdf" | "logistic_quantile"
15448            | "rayleigh_pdf" | "rayleigh_cdf" | "rayleigh_quantile"
15449            | "inverse_gamma_pdf" | "inverse_gamma_cdf" | "inverse_gamma_quantile"
15450            | "kumaraswamy_pdf" | "kumaraswamy_cdf" | "kumaraswamy_quantile"
15451            // ── Tier F: Mathieu ───────────────────────────────────────────
15452            | "mathieu_a" | "mathieu_characteristic_a"
15453            | "mathieu_ce" | "mathieu_se"
15454            // ── Tier G: Heun general ──────────────────────────────────────
15455            | "heun_g"
15456            // ── Tier H: wavelets ──────────────────────────────────────────
15457            | "haar_transform" | "haar" | "haar_inverse" | "ihaar"
15458            | "daubechies_db4" | "db4" | "daubechies_db4_inverse" | "idb4"
15459            // ── Tier I: graph algorithms ──────────────────────────────────
15460            | "topo_sort_adj"
15461            | "scc_tarjan" | "tarjan_scc" | "strongly_connected"
15462            | "bipartite_q" | "is_bipartite"
15463            | "max_flow_edmonds_karp" | "max_flow" | "edmonds_karp"
15464            | "min_cut" | "eccentricity"
15465            | "graph_diameter" | "graph_radius"
15466            // ── Tier J: misc fillers ──────────────────────────────────────
15467            | "stieltjes_constant" | "stieltjes"
15468            | "gauss_sum" | "kloosterman_sum"
15469            | "eta_quotient" | "root_approximant"
15470            // ── vector calculus ──────────────────────────────────
15471            | "numerical_gradient" | "ngrad"
15472            | "numerical_jacobian" | "njac"
15473            | "numerical_hessian" | "nhess"
15474            | "numerical_divergence" | "ndiv"
15475            | "numerical_curl" | "ncurl"
15476            | "numerical_laplacian" | "nlap"
15477            // ── optimization ─────────────────────────────────────
15478            | "nelder_mead" | "simplex_min"
15479            | "gradient_descent" | "gd_min"
15480            | "bfgs_minimize" | "bfgs"
15481            | "levenberg_marquardt" | "lev_marq" | "lm_min"
15482            | "conjugate_gradient" | "cg_solve"
15483            | "least_squares" | "lstsq"
15484            // ── integration extras ───────────────────────────────
15485            | "romberg" | "romberg_int"
15486            | "gauss_legendre_quad" | "glquad" | "gl_quad"
15487            | "monte_carlo_integrate" | "mc_int"
15488            | "adaptive_simpson" | "asimp"
15489            // ── LA extras ────────────────────────────────────────
15490            | "lu_decompose" | "ludec"
15491            | "qr_decompose" | "qrdec"
15492            | "householder_reflector" | "householder"
15493            | "givens_rotation" | "givens"
15494            | "forward_substitute" | "fwdsub"
15495            | "back_substitute" | "backsub"
15496            | "hessenberg_reduce" | "hessen"
15497            // ── polynomial helpers ───────────────────────────────
15498            | "poly_derivative" | "polyder"
15499            | "poly_integrate" | "polyint"
15500            | "poly_compose" | "poly_eval_horner" | "horner"
15501            | "pade_approximant" | "pade"
15502            // ── quaternions ──────────────────────────────────────
15503            | "quat_mul" | "quat_conj" | "quat_norm" | "quat_inv"
15504            | "quat_from_axis_angle" | "axis_angle_to_quat"
15505            | "quat_to_axis_angle"
15506            | "quat_to_matrix" | "quat_from_matrix" | "matrix_to_quat"
15507            | "quat_slerp" | "slerp"
15508            | "euler_zyx_to_matrix" | "matrix_to_euler_zyx"
15509            | "rotate_3d_vec"
15510            // ── information theory ───────────────────────────────
15511            | "kl_divergence" | "kl_div"
15512            | "js_divergence" | "js_div"
15513            | "mutual_information" | "mi"
15514            | "cross_entropy_arr" | "cross_entropy_dist"
15515            | "renyi_entropy" | "tsallis_entropy"
15516            // ── quantum ──────────────────────────────────────────
15517            | "pauli_x" | "pauli_y" | "pauli_z"
15518            | "pauli_id" | "pauli_i" | "pauli_identity"
15519            | "ket_bra" | "density_matrix" | "expectation_value" | "expval"
15520            | "commutator" | "anticommutator"
15521            | "partial_trace" | "ptrace"
15522            | "von_neumann_entropy" | "vn_entropy"
15523            // ── stat mech ────────────────────────────────────────
15524            | "bose_einstein" | "fermi_dirac"
15525            | "maxwell_boltzmann_speed" | "mb_speed"
15526            | "partition_function" | "z_partition"
15527            | "helmholtz_free_energy" | "free_energy_f"
15528            | "boltzmann_factor"
15529            | "einstein_specific_heat" | "einstein_cv"
15530            // ── optics ───────────────────────────────────────────
15531            | "fresnel_reflection_te" | "fresnel_reflection_tm"
15532            | "fresnel_transmission_te" | "fresnel_transmission_tm"
15533            | "abcd_thin_lens" | "abcd_free_space"
15534            | "gaussian_beam_q"
15535            // ── astrodynamics ────────────────────────────────────
15536            | "kepler_solve"
15537            | "true_to_eccentric" | "eccentric_to_mean"
15538            | "julian_date" | "j_date"
15539            | "jd_to_gregorian" | "jd_to_date"
15540            | "sidereal_time_gmst" | "gmst"
15541            | "vis_viva" | "orbital_period_kepler"
15542            | "orbital_elements_to_state" | "elem_to_state"
15543            // ── time series ──────────────────────────────────────
15544            | "kalman_step" | "kalman_filter"
15545            | "exponential_smoothing" | "exp_smooth"
15546            | "holt_winters" | "arma_yw_fit" | "ar_yw"
15547            // ── graph centrality ─────────────────────────────────
15548            | "pagerank" | "betweenness_centrality" | "closeness_centrality"
15549            | "eigenvector_centrality" | "degree_centrality" | "triangle_count"
15550            // ── random samplers ──────────────────────────────────
15551            | "rgumbel" | "rfrechet" | "rrayleigh"
15552            | "rlogistic" | "rkumaraswamy" | "rinverse_gamma" | "rinvgamma"
15553            // ── 2D geometry ──────────────────────────────────────
15554            | "graham_scan" | "convex_hull_2d"
15555            | "line_line_intersect_2d" | "ll_intersect_2d"
15556            | "point_segment_distance" | "p_seg_dist"
15557            // ── auto-diff ────────────────────────────────────────
15558            | "forward_diff" | "fdiff"
15559            | "forward_diff_grad" | "fdiff_grad"
15560            // ── stat tests ───────────────────────────────────────
15561            | "bartlett_test" | "levene_test"
15562            | "fishers_exact_test_2x2" | "fishers_exact"
15563            | "mcnemar_test"
15564            | "runs_test" | "wald_wolfowitz"
15565            | "friedman_test" | "kruskal_wallis_test" | "kruskal"
15566            | "sign_test"
15567            | "anderson_darling_normality" | "ad_normality"
15568            | "jarque_bera_test" | "jb_test"
15569            | "ljung_box_test" | "ljung_box"
15570            | "durbin_watson_stat" | "durbin_watson"
15571            // ── distance metrics ─────────────────────────────────
15572            | "mahalanobis_distance" | "mahalanobis_dist"
15573            | "cosine_distance" | "canberra_distance"
15574            | "bray_curtis_distance" | "bray_curtis"
15575            | "l1_distance"
15576            | "chi_squared_distance"
15577            // ── more distributions ───────────────────────────────
15578            | "multivariate_normal_pdf" | "mvn_pdf"
15579            | "multivariate_normal_sample" | "rmvn"
15580            | "dirichlet_pdf" | "dirichlet_sample" | "rdirichlet"
15581            | "skellam_pmf"
15582            | "inverse_gaussian_pdf" | "wald_pdf"
15583            | "inverse_gaussian_cdf" | "wald_cdf"
15584            | "inverse_gaussian_sample" | "rwald"
15585            | "non_central_chi2_pdf" | "ncchi2_pdf"
15586            // ── matrix functions ─────────────────────────────────
15587            | "matrix_exp" | "expm" | "matrix_log" | "logm"
15588            | "matrix_sqrt" | "sqrtm" | "matrix_sin" | "sinm"
15589            | "matrix_cos" | "cosm"
15590            // ── adaptive ODE ─────────────────────────────────────
15591            | "rk45_dormand_prince" | "rk45" | "dopri5"
15592            | "midpoint_step" | "ode_midpoint"
15593            | "heun_step" | "ode_heun"
15594            | "verlet_step" | "ode_verlet"
15595            // ── GLM ──────────────────────────────────────────────
15596            | "logistic_regression" | "logit_fit"
15597            | "poisson_regression"
15598            | "ridge_regression" | "ridge"
15599            | "lasso_coord" | "lasso"
15600            // ── bootstrap/resampling ─────────────────────────────
15601            | "bootstrap_mean_ci" | "boot_mean_ci"
15602            | "jackknife_estimate" | "jackknife"
15603            | "permutation_test_diff" | "perm_test_diff"
15604            // ── time series extras ───────────────────────────────
15605            | "acf_at_lag" | "diff_op" | "lag_op"
15606            | "decompose_classical" | "decompose_ts"
15607            // ── combinatorial generators ─────────────────────────
15608            | "combinations_list" | "permutations_list"
15609            | "cyclic_permutations" | "subsets_of_size"
15610            // ── DP utilities ─────────────────────────────────────
15611            | "longest_increasing_subseq" | "lis"
15612            | "knapsack_01" | "knapsack"
15613            | "subset_sum_target" | "subset_sum"
15614            | "coin_change_min" | "coin_change_minimum"
15615            | "edit_distance_levenshtein" | "edit_distance"
15616            // ── ML metrics ───────────────────────────────────────
15617            | "one_hot_encode" | "onehot" | "label_encode"
15618            | "categorical_cross_entropy" | "cce"
15619            | "classification_metrics" | "binary_metrics"
15620            | "roc_auc" | "auroc"
15621            // ── DSP / image filters ──────────────────────────────
15622            | "gaussian_blur_kernel" | "sobel_x" | "sobel_y"
15623            | "prewitt_x" | "prewitt_y"
15624            | "laplacian_of_gaussian" | "log_kernel"
15625            // ── stochastic processes ─────────────────────────────
15626            | "brownian_path" | "wiener_path"
15627            | "geometric_brownian_path" | "gbm_path"
15628            | "poisson_process" | "random_walk_1d"
15629            // ── compression / info ───────────────────────────────
15630            | "lempel_ziv_complexity" | "lz_complexity"
15631            | "huffman_code_lengths" | "huffman"
15632            | "shannon_entropy_rate" | "block_entropy_rate"
15633            // ── physics / quantum ────────────────────────────────
15634            | "planck_blackbody" | "blackbody"
15635            | "rayleigh_jeans" | "compton_shift"
15636            | "rydberg_energy"
15637            | "hydrogen_radial_wavefunction" | "h_rad_psi"
15638            // ── number theory / algebra ──────────────────────────
15639            | "integer_log" | "ilog"
15640            | "aks_primality" | "aks"
15641            | "elliptic_curve_add" | "ec_add"
15642            | "berlekamp_massey" | "bm_lfsr"
15643            | "bezout_coefficients" | "bezout" | "extended_euclid"
15644            // ── CAS-lite ─────────────────────────────────────────
15645            | "factor_quadratic" | "complete_square"
15646            | "partial_fraction_simple" | "partial_fraction"
15647            // ── more quadrature ──────────────────────────────────
15648            | "gauss_chebyshev_quad" | "gc_quad"
15649            | "gauss_hermite_quad" | "gh_quad"
15650            | "gauss_laguerre_quad" | "glag_quad"
15651            | "clenshaw_curtis_quad" | "cc_quad"
15652            | "tanh_sinh_quad" | "ts_quad"
15653            | "gauss_legendre_2d" | "gl_2d"
15654            | "monte_carlo_2d" | "mc_2d"
15655            // ── more optimization ────────────────────────────────
15656            | "simulated_annealing" | "sa_min"
15657            | "simplex_lp" | "lp_simplex"
15658            | "particle_swarm" | "pso_min"
15659            // ── distributions ────────────────────────────────────
15660            | "gev_pdf" | "gev_cdf" | "gev_sample" | "rgev"
15661            | "gen_pareto_pdf" | "gen_pareto_cdf"
15662            | "gen_pareto_sample" | "rgenpareto"
15663            | "skew_normal_pdf" | "skew_normal_cdf"
15664            | "mixture_normal_pdf"
15665            | "categorical_sample" | "rcat"
15666            | "multinomial_pmf" | "multinomial_sample" | "rmultinom"
15667            | "truncated_normal_pdf"
15668            | "truncated_normal_sample" | "rtnorm"
15669            // ── clustering ───────────────────────────────────────
15670            | "dbscan" | "gmm_em_1d" | "gmm_1d"
15671            | "silhouette_score"
15672            | "davies_bouldin_index" | "db_index"
15673            | "calinski_harabasz_index" | "ch_index"
15674            | "mds_2d" | "pcoa_2d" | "mean_shift"
15675            // ── NN primitives ────────────────────────────────────
15676            | "batch_norm" | "layer_norm"
15677            | "dropout_mask"
15678            | "max_pool_1d" | "avg_pool_1d"
15679            | "attention_softmax" | "positional_encoding"
15680            | "glorot_init" | "xavier_init"
15681            | "he_init" | "kaiming_init"
15682            | "adam_step" | "rmsprop_step"
15683            // ── time series ──────────────────────────────────────
15684            | "ewma" | "ccf" | "periodogram"
15685            | "welch_psd" | "welch"
15686            | "lag_features"
15687            // ── image processing ─────────────────────────────────
15688            | "median_filter_2d"
15689            | "threshold_otsu" | "otsu"
15690            | "histogram_equalize" | "hist_eq"
15691            | "erode_2d" | "dilate_2d"
15692            // ── losses ───────────────────────────────────────────
15693            | "mse_loss" | "mae_loss" | "huber_loss"
15694            // ── spatial ──────────────────────────────────────────
15695            | "vincenty_distance" | "vincenty"
15696            | "mercator_project"
15697            | "destination_from_bearing" | "dest_bearing"
15698            // ── integer sequences ────────────────────────────────
15699            | "recaman" | "recaman_seq"
15700            | "sylvester" | "sylvester_seq"
15701            | "happy_q" | "is_happy"
15702            | "amicable_pair_q"
15703            | "aliquot_sequence"
15704            | "magic_constant"
15705            // ── graph metrics ────────────────────────────────────
15706            | "clustering_coefficient_local" | "cc_local"
15707            | "clustering_coefficient_global" | "cc_global"
15708            | "assortativity" | "common_neighbors" | "jaccard_neighbors"
15709            | "adamic_adar"
15710            | "preferential_attachment_score" | "pa_score"
15711            // ── 3D geometry ──────────────────────────────────────
15712            | "triangle_3d_normal" | "triangle_3d_area"
15713            | "tetrahedron_volume"
15714            | "plane_from_3_points" | "plane_from_pts"
15715            | "point_to_plane_distance" | "pt_plane_dist"
15716            | "ray_triangle_intersect" | "moller_trumbore"
15717            | "ray_sphere_intersect" | "aabb_overlap"
15718            // ── iterative solvers ────────────────────────────────
15719            | "gauss_seidel"
15720            | "jacobi_iteration" | "jacobi_solve"
15721            | "sor_solve" | "sor"
15722            | "thomas_tridiag_solve" | "thomas"
15723            | "richardson_extrapolation" | "richardson"
15724            | "finite_difference_5pt" | "fd5pt"
15725            // ── crypto / algebra ─────────────────────────────────
15726            | "tonelli_shanks_sqrt" | "tonelli_shanks"
15727            | "baby_step_giant_step" | "bsgs"
15728            | "pollard_rho_factor" | "pollard_rho"
15729            | "modular_lcm" | "mlcm"
15730            | "crt_general" | "crt_arbitrary"
15731            // ── physics / chemistry ──────────────────────────────
15732            | "van_der_waals_p" | "vdw_pressure"
15733            | "nernst_equation" | "nernst"
15734            | "arrhenius_rate" | "arrhenius"
15735            | "reduced_mass"
15736            | "ph_to_concentration" | "ph_to_h"
15737            // ── MCMC / SDE / HMM ─────────────────────────────────
15738            | "metropolis_hastings" | "mh_sampler"
15739            | "gibbs_sampler_step" | "gibbs_step"
15740            | "euler_maruyama" | "em_sde"
15741            | "milstein" | "milstein_sde"
15742            | "ornstein_uhlenbeck_path" | "ou_path"
15743            | "hmm_forward" | "hmm_viterbi" | "hmm_backward"
15744            // ── survival / alignment ─────────────────────────────
15745            | "kaplan_meier" | "km_estimator" | "log_rank_test"
15746            | "needleman_wunsch" | "nw_align"
15747            | "smith_waterman" | "sw_align"
15748            // ── chemistry ────────────────────────────────────────
15749            | "gibbs_free_energy" | "delta_g"
15750            | "henderson_hasselbalch" | "hh_eq"
15751            | "radioactive_decay"
15752            | "half_life_to_constant" | "hl_to_lambda"
15753            // ── control theory ───────────────────────────────────
15754            | "pid_step"
15755            | "transfer_function_eval" | "tf_eval"
15756            | "bode_magnitude_db" | "bode_mag_db"
15757            | "bode_phase_deg"
15758            | "lqr_2x2"
15759            // ── game theory ──────────────────────────────────────
15760            | "nash_eq_2x2" | "nash_2x2"
15761            | "shapley_value" | "expected_utility"
15762            // ── operations research ──────────────────────────────
15763            | "hungarian_assignment" | "hungarian"
15764            | "tsp_nearest_neighbor" | "tsp_nn"
15765            | "vertex_cover_2approx" | "vc_2approx"
15766            // ── PDE ──────────────────────────────────────────────
15767            | "heat_eq_1d" | "wave_eq_1d"
15768            | "laplace_2d_jacobi" | "laplace_jacobi"
15769            // ── Bayesian conjugate ───────────────────────────────
15770            | "beta_binomial_update"
15771            | "normal_normal_update"
15772            | "gamma_poisson_update"
15773            | "dirichlet_multinomial_update"
15774            // ── quantum gates ────────────────────────────────────
15775            | "hadamard_gate" | "h_gate"
15776            | "cnot_gate" | "cx_gate"
15777            | "swap_gate" | "cz_gate"
15778            | "qft_matrix" | "phase_gate"
15779            | "s_gate" | "t_gate"
15780            // ── splines ──────────────────────────────────────────
15781            | "bezier_eval"
15782            | "catmull_rom_eval" | "cmr_eval"
15783            | "cubic_hermite_eval" | "ch_eval"
15784            | "bspline_basis" | "nik_basis"
15785            // ── music ────────────────────────────────────────────
15786            | "freq_to_midi" | "midi_to_freq"
15787            | "equal_temperament_freq"
15788            | "cents_difference" | "cents_diff"
15789            // ── astronomy ────────────────────────────────────────
15790            | "redshift_z" | "hubble_distance" | "luminosity_distance"
15791            // ── fluid dynamics ───────────────────────────────────
15792            | "reynolds_number" | "mach_number"
15793            | "prandtl_number" | "bernoulli_velocity"
15794            // ── distributions ────────────────────────────────────
15795            | "negative_binomial_pmf" | "nb_pmf"
15796            | "hypergeometric_pmf"
15797            | "beta_binomial_pmf" | "bb_pmf"
15798            | "von_mises_pdf" | "vmf_pdf"
15799            // ── random graphs ────────────────────────────────────
15800            | "erdos_renyi_random" | "erdos_renyi"
15801            | "barabasi_albert_random" | "barabasi_albert"
15802            | "watts_strogatz_random" | "watts_strogatz"
15803            // ── color science ────────────────────────────────────
15804            | "rgb_to_lab" | "lab_to_rgb"
15805            | "kelvin_to_rgb" | "color_temp_rgb"
15806            // ── integer sequences ────────────────────────────────
15807            | "bell_triangle" | "surjection_count"
15808            | "distinct_partition_count" | "q_partition"
15809            | "fibonacci_q" | "is_fib_number"
15810            // ── stats / divergences / distribs / physics / astro / chem ──
15811            | "bonferroni_correction" | "bonferroni"
15812            | "benjamini_hochberg" | "bh_fdr"
15813            | "tukey_hsd"
15814            | "hellinger_distance"
15815            | "wasserstein_1d" | "earth_movers_1d"
15816            | "chi_squared_divergence"
15817            | "beta_geometric_pmf"
15818            | "generalized_gamma_pdf" | "gengamma_pdf"
15819            | "zip_pmf" | "zero_inflated_poisson_pmf"
15820            | "stefan_boltzmann_luminosity" | "stellar_luminosity"
15821            | "photon_momentum" | "photon_energy_ev"
15822            | "dipole_radiation_power" | "larmor_power"
15823            | "parallax_to_distance" | "hawking_temperature"
15824            | "roche_limit" | "apparent_magnitude" | "distance_modulus"
15825            | "beer_lambert" | "absorbance"
15826            | "rate_law_n"
15827            | "freezing_point_depression" | "fpd"
15828            | "mixed_nash_2x2" | "minimax_2x2"
15829            // ── graphics / DSP / image / clustering / combinatorics / NT ─
15830            | "barycentric_coords_2d" | "barycentric_2d"
15831            | "bresenham_line" | "bilinear_interp_2d"
15832            | "point_in_polygon_2d"
15833            | "hilbert_transform" | "cepstrum"
15834            | "butterworth_lowpass_coeffs" | "butter_lp"
15835            | "savitzky_golay_coeffs" | "sg_coeffs"
15836            | "savitzky_golay_filter" | "sg_filter"
15837            | "canny_edge_intensity" | "canny_intensity"
15838            | "bilateral_filter_basic" | "bilateral_filter"
15839            | "kmeans_pp_init" | "kpp_init"
15840            | "elbow_score" | "wcss"
15841            | "young_tableaux_count" | "syt_count"
15842            | "euler_alt_permutation" | "euler_zigzag"
15843            | "genocchi_number" | "lattice_paths_count"
15844            | "tetration"
15845            | "ackermann_limited" | "ackermann"
15846            | "perfect_power_q" | "b_smooth_q"
15847            // ── networks / crypto / quantum / geom / TS ──────────
15848            | "k_core"
15849            | "rich_club_coefficient" | "rich_club"
15850            | "rsa_basic_encrypt" | "rsa_enc_int"
15851            | "rsa_basic_decrypt" | "rsa_dec_int"
15852            | "dh_shared_secret"
15853            | "bell_state_phi_plus" | "bell_phi_plus"
15854            | "bell_state_psi_minus" | "bell_psi_minus"
15855            | "density_matrix_purity" | "rho_purity"
15856            | "concurrence_2qubit"
15857            | "point_in_circle"
15858            | "circle_circle_intersect_2d"
15859            | "polygon_centroid"
15860            | "sutherland_hodgman_clip" | "sh_clip"
15861            | "kalman_rts_smoother" | "rts_smoother"
15862            // ── bioinformatics ───────────────────────────────────
15863            | "gc_content" | "codon_to_aa"
15864            | "reverse_complement_dna" | "rev_comp_dna"
15865            | "hamming_dna"
15866            | "blosum62_pair_score" | "blosum62"
15867            | "kmer_count"
15868            // ── geographic ───────────────────────────────────────
15869            | "great_circle_bearing" | "gc_bearing"
15870            | "midpoint_lat_lon" | "mid_geo"
15871            | "utm_zone_for"
15872            | "area_polygon_lat_lon" | "geo_polygon_area"
15873            // ── finance ──────────────────────────────────────────
15874            | "crr_binomial_option" | "crr_option"
15875            | "bond_price_clean"
15876            | "bond_yield_to_maturity" | "bond_ytm"
15877            | "modified_duration_bond"
15878            | "convexity_bond" | "bond_convexity"
15879            // ── image quality ────────────────────────────────────
15880            | "ssim" | "psnr" | "mssim"
15881            // ── acoustics ────────────────────────────────────────
15882            | "db_spl_from_pa" | "db_spl"
15883            | "a_weighting_factor" | "a_weight"
15884            | "octave_band_center" | "octave_center"
15885            | "semitone_ratio"
15886            // ── genetics ─────────────────────────────────────────
15887            | "hardy_weinberg"
15888            | "expected_heterozygosity" | "het_e"
15889            | "fst_simple"
15890            | "allele_frequencies"
15891            // ── epidemiology ─────────────────────────────────────
15892            | "sir_step" | "sir_r0" | "doubling_time"
15893            // ── economics ────────────────────────────────────────
15894            | "theil_index"
15895            | "herfindahl_hirschman" | "hhi"
15896            | "atkinson_index"
15897            | "lorenz_curve_points"
15898            // ── APL/J primitives ─────────────────────────────────
15899            | "iota_range" | "iota"
15900            | "reshape_array" | "reshape"
15901            | "grade_up" | "grade_asc"
15902            | "grade_down" | "grade_desc"
15903            // ── plasma physics ───────────────────────────────────
15904            | "plasma_frequency" | "omega_p"
15905            | "debye_length" | "lambda_d"
15906            | "cyclotron_frequency" | "omega_c"
15907            | "larmor_radius" | "gyroradius"
15908            // ── string similarity ────────────────────────────────
15909            | "jaro_winkler_similarity" | "jaro_winkler"
15910            | "metaphone_simple"
15911            // ── rating systems ───────────────────────────────────
15912            | "elo_rating_update" | "elo"
15913            | "glicko_rating_update" | "glicko"
15914            | "dice_sum_pmf"
15915            // ── effect sizes ─────────────────────────────────────
15916            | "cohens_d" | "effect_size_d"
15917            | "cliff_delta"
15918            | "vargha_delaney_a12" | "a12"
15919            // ── control transient ────────────────────────────────
15920            | "step_response_2nd_order" | "step_2nd"
15921            | "overshoot_2nd_order" | "overshoot_pct"
15922            // ── matrix norms ─────────────────────────────────────
15923            | "frobenius_norm"
15924            | "spectral_norm" | "operator_norm_2"
15925            | "trace_matrix" | "tr_mat"
15926            // ── networks ─────────────────────────────────────────
15927            | "homophily_index" | "homophily"
15928            | "dyad_census" | "triad_census"
15929            // ── misc ─────────────────────────────────────────────
15930            | "sigmoid_inverse" | "logit"
15931            // ── list / string / date / color / music / astro / perm / linguistics / regression / combinatorics / PRNG ──
15932            | "partition_at" | "drop_at" | "insert_at_idx"
15933            | "replace_at_index" | "set_at"
15934            | "swap_indices" | "nth_largest" | "nth_smallest"
15935            | "position_of_all_matching" | "positions_of_all"
15936            | "string_take_first" | "string_take_last"
15937            | "string_drop_first" | "string_drop_last"
15938            | "pluralize_simple"
15939            | "singularize_simple" | "singularize"
15940            | "capitalize_words" | "title_words"
15941            | "format_table_simple" | "ascii_table"
15942            | "days_between" | "weeks_between"
15943            | "months_between" | "years_between"
15944            | "first_of_month" | "last_of_month"
15945            | "day_of_week_iso" | "iso_dow"
15946            | "easter_sunday" | "chinese_zodiac"
15947            | "iso_week_number" | "iso_week"
15948            | "relative_luminance" | "wcag_luminance"
15949            | "contrast_ratio_wcag" | "wcag_contrast"
15950            | "delta_e_76" | "delta_e"
15951            | "color_blend_t" | "lerp_color"
15952            | "chord_to_freqs" | "scale_to_intervals"
15953            | "interval_semitones"
15954            | "transpose_freq_semitones" | "transpose_semi"
15955            | "bpm_to_period" | "midi_to_pitch_class"
15956            | "key_signature_for" | "circle_of_fifths_step"
15957            | "moon_phase" | "equation_of_time"
15958            | "solar_declination" | "sidereal_day_period" | "ecliptic_obliquity"
15959            | "permutation_order"
15960            | "permutation_parity" | "perm_sign"
15961            | "identity_permutation"
15962            | "permutation_compose" | "perm_mul"
15963            | "flesch_reading_ease" | "flesch_kincaid_grade"
15964            | "gunning_fog"
15965            | "automated_readability_index" | "ari"
15966            | "lix"
15967            | "adjusted_r_squared" | "adj_r2"
15968            | "aic" | "bic"
15969            | "residuals_compute" | "compute_residuals"
15970            | "composition_count" | "weak_composition_count"
15971            | "necklace_count" | "bracelet_count"
15972            | "multiset_permutations_count" | "multinomial_count"
15973            | "pearson_hash_byte" | "pearson_hash"
15974            | "xorshift32_step" | "lcg_next_u32"
15975            | "fisher_yates_shuffle"
15976            // ── ──────────────────────────────────────────────────
15977            | "tetrahedral_number" | "square_pyramidal_number"
15978            | "octahedral_number" | "pentagonal_pyramidal_number"
15979            | "cake_number" | "cuban_number" | "centered_hexagonal_number"
15980            | "carmichael_q" | "is_carmichael"
15981            | "sphenic_q" | "is_sphenic"
15982            | "seven_smooth_q" | "is_7_smooth"
15983            | "cartesian_product_n" | "cart_n"
15984            | "multiset_union" | "multiset_intersection" | "multiset_difference"
15985            | "polynomial_roots_dk" | "durand_kerner"
15986            | "lin_bairstow_step" | "bairstow"
15987            | "heap_sift_down"
15988            | "fenwick_build" | "bit_build"
15989            | "fenwick_query" | "bit_query"
15990            | "segment_tree_sum" | "seg_sum"
15991            | "kmp_failure" | "kmp"
15992            | "z_array" | "z_func"
15993            | "suffix_array_naive"
15994            | "manacher_radii" | "manacher"
15995            | "rabin_karp_hash" | "lcp_array"
15996            | "regex_escape_simple"
15997            | "horspool_search" | "bm_horspool"
15998            | "lpt_schedule" | "lpt"
15999            | "johnsons_rule" | "johnson_2m"
16000            | "bit_reverse_32" | "bit_reverse"
16001            | "bin_to_gray" | "gray_to_bin"
16002            | "swap_bits_pos" | "swap_bits"
16003            | "hamming_weight" | "popcnt"
16004            | "hamming_distance_int" | "hamdist_int"
16005            | "internal_rate_of_return"
16006            | "modified_irr" | "mirr"
16007            | "payback_period_simple" | "payback_simple"
16008            | "rfc3339_format" | "rfc3339"
16009            | "rfc3339_parse"
16010            | "iso_ordinal_date" | "ordinal_date"
16011            // ── ──────────────────────────────────────────────────
16012            | "lazy_caterer" | "central_polygonal"
16013            | "centered_square" | "centered_triangular" | "centered_pentagonal"
16014            | "star_number" | "dodecahedral_number" | "icosahedral_number"
16015            | "pronic_number" | "squared_triangular"
16016            | "woodall_number" | "cullen_number"
16017            | "repunit" | "repdigit" | "kaprekar_routine_step"
16018            | "smith_q"
16019            | "keith_q" | "is_keith"
16020            | "armstrong_q" | "is_armstrong"
16021            | "fnv1a_hash" | "djb2_hash"
16022            | "jenkins_one_at_a_time" | "jenkins_oat"
16023            | "murmurhash3_x32"
16024            | "adler32_hash" | "crc16_ccitt"
16025            | "vec_dot"
16026            | "l1_norm" | "l2_norm" | "vec_l2"
16027            | "linf_norm" | "max_norm" | "lp_norm"
16028            | "unit_vector"
16029            | "vector_project" | "proj" | "vector_reject"
16030            | "orthogonalize_vectors" | "gram_schmidt"
16031            | "outer_product" | "vec_outer"
16032            | "matrix_diagonal" | "mdiagvec"
16033            | "matrix_anti_diagonal"
16034            | "matrix_symmetric_q" | "matrix_orthogonal_q"
16035            | "geometric_mean_arr" | "harmonic_mean_arr"
16036            | "quadratic_mean_arr" | "lehmer_mean"
16037            | "running_mean" | "running_variance"
16038            | "outlier_iqr_q" | "z_score_robust"
16039            | "geometric_sequence" | "arithmetic_sequence"
16040            | "log_sum_exp" | "lse"
16041            | "log_sigmoid" | "log1p_exp"
16042            | "string_chars"
16043            | "string_words_count" | "word_count_simple"
16044            | "string_lines_count" | "line_count_simple"
16045            | "string_intersperse" | "string_replicate"
16046            | "string_uniq_chars" | "string_letter_frequency"
16047            | "anagram_q" | "is_anagram_q"
16048            | "string_take_while" | "string_drop_while"
16049            | "string_split_at_first" | "string_partition_at_word"
16050            // ── ──────────────────────────────────────────────────
16051 | "relativistic_kinetic"
16052            | "lorentz_factor_v" | "doppler_relativistic"
16053            | "drag_force_quadratic" | "terminal_velocity"
16054            | "carnot_efficiency" | "otto_efficiency"
16055            | "brayton_efficiency" | "diesel_efficiency"
16056            | "specific_heat_const_v" | "speed_of_sound_ideal"
16057            | "kepler_period_au" | "synodic_period"
16058            | "hill_radius" | "jeans_length"
16059            | "chandrasekhar_mass" | "eddington_luminosity"
16060            | "schwarzschild_radius_m" | "gravity_at_radius"
16061            | "gravitational_pe"
16062            | "freefall_time" | "pendulum_freq" | "spring_period"
16063            | "centripetal_accel" | "lens_focal_length"
16064            | "avogadros_number" | "boltzmann_const"
16065            | "planck_const_h" | "gas_constant_r"
16066            | "concentration_dilute" | "partial_pressure"
16067            | "mole_fraction" | "molarity" | "molality"
16068            | "normality_chem" | "ionic_strength"
16069 | "titration_volume"
16070            | "atomic_radius_pm" | "de_broglie_wavelength_kg"
16071 | "lotka_volterra_step"
16072            | "michaelis_menten" | "hill_equation"
16073            | "lineweaver_burk" | "eadie_hofstee_y"
16074            | "arrhenius_temp_q10"
16075            | "body_surface_area_dubois" | "bsa_dubois"
16076            | "bmr_harris_benedict_male" | "bmr_harris_benedict_female"
16077            | "max_heart_rate" | "target_heart_rate"
16078            | "vo2_max_estimate" | "pulse_pressure"
16079            | "mean_arterial_pressure" | "map_bp"
16080            | "dew_point_magnus" | "heat_index_celsius"
16081            | "wind_chill_celsius" | "pressure_altitude_m"
16082            | "density_altitude_m" | "saturation_vapor_pressure"
16083            | "humidex" | "utci_simple"
16084            | "resistance_parallel" | "r_parallel"
16085            | "resistance_series" | "r_series"
16086            | "capacitance_parallel" | "c_parallel"
16087            | "capacitance_series" | "c_series"
16088            | "inductance_parallel" | "l_parallel"
16089            | "inductance_series" | "l_series"
16090            | "voltage_divider" | "current_divider"
16091            | "lc_resonant" | "q_factor_rlc"
16092            | "skin_depth" | "wire_resistance"
16093            | "motor_torque" | "efficiency_ratio"
16094            | "dB_voltage" | "db_voltage"
16095            | "dB_power" | "db_power"
16096            // ── ──────────────────────────────────────────────────
16097            | "bfs_distances" | "dfs_preorder" | "connected_components"
16098            | "graph_is_tree" | "graph_density"
16099            | "graph_average_degree" | "graph_max_degree" | "graph_min_degree"
16100            | "graph_complement"
16101            | "in_degree_directed" | "out_degree_directed"
16102            | "graph_eccentricity_all" | "is_connected"
16103            | "articulation_points" | "bridges_edges"
16104            | "eulerian_path_q" | "hamiltonian_brute"
16105            | "string_to_charcodes" | "charcodes_to_string"
16106            | "string_xor"
16107            | "string_camel_to_snake" | "string_snake_to_camel"
16108            | "string_kebab_to_snake" | "string_snake_to_kebab"
16109            | "palindromic_q" | "substring_count"
16110            | "string_truncate_ellipsis" | "string_expand_tabs"
16111            | "string_normalize_spaces"
16112 | "days_in_year" | "quarter_of_year"
16113            | "zeller_day_of_week" | "age_from_birthdate"
16114            | "business_days_between" | "unix_epoch_to_iso"
16115            | "loan_payment_pmt" | "loan_balance"
16116            | "amortization_total_interest"
16117            | "apr_to_apy" | "apy_to_apr"
16118            | "compound_interest_periods" | "simple_interest_compute"
16119
16120            | "perpetuity_value" | "growing_perpetuity"
16121            | "annuity_present_value" | "annuity_future_value"
16122            | "capm_expected_return"
16123            | "treynor_ratio"
16124            | "jensens_alpha" | "information_ratio"
16125            | "friction_factor_laminar" | "swamee_jain_factor"
16126            | "pipe_pressure_drop" | "orifice_velocity"
16127            | "chezy_velocity" | "manning_velocity"
16128            | "froude_number" | "weber_number" | "grashof_number"
16129            | "nusselt_dittus_boelter"
16130            // ── more extensions ────────────────────────────────────────────
16131            | "mollweide_project" | "robinson_project" | "sinusoidal_project"
16132            | "equirectangular_project" | "lambert_azimuthal_project" | "albers_conic_project"
16133            | "geohash_encode" | "geohash_decode" | "geohash_neighbor" | "geohash_bbox"
16134            | "gabor_kernel" | "unsharp_mask_kernel" | "emboss_kernel"
16135            | "box_blur_kernel" | "motion_blur_kernel" | "sharpen_kernel"
16136            | "edge_detect_kernel" | "sobel_diagonal_kernel" | "haar_2d_step"
16137            | "db4_coeffs" | "db6_coeffs" | "sym4_coeffs" | "coif1_coeffs"
16138            | "aes_sbox_byte" | "aes_inv_sbox_byte"
16139            | "chacha20_qround" | "xtea_round" | "speck_round" | "simon_round"
16140            | "kepler_hyperbolic" | "hohmann_dv1" | "hohmann_dv2" | "hohmann_total"
16141            | "bielliptic_total" | "lambert_simple"
16142            | "horizon_distance" | "solar_zenith_angle" | "air_mass_kasten"
16143            | "solar_constant" | "julian_centuries_j2000"
16144            | "mean_solar_longitude" | "mean_solar_anomaly" | "lst_to_solar"
16145            | "ra_dec_to_az_alt" | "ecliptic_to_equatorial" | "equatorial_to_galactic"
16146            | "orbital_eccentricity" | "semi_major_axis"
16147            | "specific_orbital_energy" | "specific_angular_momentum"
16148            | "toffoli_gate" | "ccx_gate" | "fredkin_gate" | "cswap_gate"
16149            | "iswap_gate" | "sqrt_swap_gate"
16150            | "rx_gate" | "ry_gate" | "rz_gate"
16151            | "ghz_state_n" | "w_state_n"
16152            | "depolarizing_channel" | "dephasing_channel" | "amplitude_damping_channel"
16153            | "quantum_fidelity_pure" | "trace_distance"
16154            | "bell_inequality_chsh" | "pauli_decomposition_2x2"
16155            | "quantum_relative_entropy" | "qft_4_real"
16156            | "bwt_encode" | "bwt_decode" | "mtf_encode" | "mtf_decode"
16157
16158            | "lyndon_factorize" | "christoffel_word" | "sturmian_word"
16159            | "z_function_alt" | "period_of_string" | "borders_of_string"
16160            | "thue_morse_string" | "fibonacci_word"
16161            | "mann_kendall_tau" | "theil_sen_slope" | "hodges_lehmann"
16162            | "huber_m_estimator" | "winsorized_variance_arr"
16163            | "bowley_skewness" | "pearson_skewness_2"
16164            | "concordance_correlation" | "quantile_p"
16165            | "label_propagation_step" | "modularity_q"
16166            | "clique_count_3" | "local_efficiency" | "global_efficiency"
16167            | "diameter_unweighted"
16168            | "aitken_delta_squared" | "wynn_epsilon"
16169            | "shanks_transform" | "levin_t_transform"
16170            | "harmonic_seq_sum" | "alternating_seq_sum"
16171            // ── more extensions (2) ────────────────────────────────────────
16172            | "sparse_csr_build" | "sparse_csr_mul_vec" | "sparse_density"
16173            | "lower_triangular_q" | "upper_triangular_q"
16174            | "diagonal_dominance_q" | "matrix_zero_q" | "matrix_identity_q"
16175            | "matrix_random_uniform" | "matrix_random_normal"
16176            | "andrew_monotone_chain" | "polygon_area_signed"
16177            | "polygon_convex_q" | "iou_2d_axis_aligned" | "hausdorff_distance_2d"
16178            | "minkowski_sum_simple" | "circle_3_points"
16179            | "polygon_winding_number" | "segment_length"
16180            | "segments_parallel_q" | "segments_perpendicular_q"
16181            | "burr_xii_pdf" | "burr_xii_cdf" | "dagum_pdf" | "lomax_pdf"
16182            | "birnbaum_saunders_pdf" | "tukey_lambda_quantile"
16183            | "half_cauchy_pdf" | "half_logistic_pdf" | "reciprocal_pdf"
16184            | "levy_pdf" | "voigt_profile_simple"
16185            | "gompertz_pdf" | "inverse_weibull_pdf"
16186            | "log_gamma_simple" | "inverse_chi2_pdf"
16187            | "poly1305_block_step" | "x25519_field_mul" | "curve25519_mul_simple"
16188            | "secp256k1_y_recover" | "hmac_step_xor"
16189            | "pkcs7_pad" | "pkcs7_unpad" | "xor_byte_string"
16190 | "atbash_cipher"
16191            | "vigenere_encrypt" | "vigenere_decrypt" | "xor_brute_keylen"
16192            | "arima_diff" | "seasonal_diff"
16193            | "garch_step" | "egarch_step"
16194            | "realized_volatility" | "max_drawdown_arr"
16195            | "calmar_ratio" | "omega_ratio" | "kelly_criterion"
16196            | "var_historical" | "cvar_historical"
16197            | "graph_degree_distribution" | "graph_count_edges"
16198            | "graph_bipartite_match_simple" | "graph_count_triangles"
16199            | "graph_avg_clustering" | "graph_transitivity"
16200            | "graph_max_clique_brute" | "graph_independent_set_brute"
16201            | "graph_count_paths_length_k" | "graph_pagerank_simple"
16202            // ── integration / ODE / root finding / optimization ─
16203            | "boole_rule" | "boole_int"
16204            | "gauss_legendre_5" | "gl5"
16205            | "gauss_kronrod_15" | "gk15"
16206
16207            | "midpoint_rule"
16208            | "adams_bashforth_4" | "ab4"
16209            | "heun_method" | "rk45_cash_karp" | "rkck"
16210            | "milne_pc" | "milne"
16211            | "modified_midpoint_ode" | "modmidpoint"
16212            | "backward_euler" | "implicit_euler"
16213            | "crank_nicolson_ode" | "cn_ode"
16214            | "brent_root" | "brent" | "ridders_root" | "ridders"
16215            | "steffensen_root" | "steffensen" | "halley_root" | "halley"
16216            | "householder_root" | "muller_root" | "muller"
16217            | "regula_falsi" | "false_position"
16218            | "secant_root" | "secant"
16219            | "anderson_step" | "aberth_step" | "inverse_quad_interp"
16220            | "lm_step" | "gradient_descent_step"
16221 | "nesterov_step" | "adagrad_step"
16222            | "cg_beta_pr" | "cg_beta_fr" | "bfgs_h_update_1d"
16223            | "wolfe_strong_q" | "dogleg_step"
16224            | "nelder_mead_reflect" | "nelder_mead_expand" | "nelder_mead_contract"
16225            | "sa_accept_prob" | "sa_boltzmann_temp" | "sa_cauchy_temp"
16226            | "sa_geometric_temp" | "acceptance_target"
16227            // ── financial pricing models ────────────────────────
16228            | "bs_call" | "blackscholes_call" | "bs_put" | "blackscholes_put"
16229 | "bs_theta_call" | "bs_rho_call"
16230 | "bachelier_call" | "black76_call"
16231            | "crr_american_call" | "crr_american_put" | "jr_european_call"
16232            | "trinomial_call" | "heston_price_simple" | "sabr_implied_vol"
16233            | "merton_jump_call" | "asian_call_mc" | "barrier_up_out_call"
16234            | "digital_call" | "lookback_call"
16235            | "macaulay_duration" | "forward_rate"
16236            | "discount_continuous" | "ytm_newton"
16237            | "vasicek_bond" | "cir_bond" | "hull_white_drift"
16238            | "cds_upfront" | "black_karasinski_drift" | "quanto_adjustment"
16239            | "fx_forward" | "garman_kohlhagen_call" | "margrabe" | "stulz_min_call"
16240            | "sharpe_annualized"
16241            | "jensen_alpha" | "modified_sharpe"
16242            // ── chemistry ───────────────────────────────────────
16243            | "ph_from_h" | "poh_from_oh" | "pka_from_ka"
16244 | "henderson_base"
16245            | "arrhenius_k" | "eyring_k"
16246            | "first_order_concentration" | "first_order_half_life"
16247            | "second_order_concentration" | "second_order_half_life"
16248            | "zero_order_concentration"
16249
16250            | "ideal_gas_n" | "redlich_kwong_p"
16251            | "compressibility_z"
16252            | "kc_from_rates" | "kp_from_kc" | "reaction_quotient" | "rxn_q"
16253            | "le_chatelier_dir"
16254            | "dg_from_k" | "k_from_dg" | "vant_hoff" | "clausius_clapeyron" | "antoine_p"
16255 | "emf_from_half_cells" | "faraday_mass_deposited"
16256 | "transmittance" | "ksp_from_concs"
16257 | "debye_huckel"
16258            | "cp_monatomic_ideal" | "cv_monatomic_ideal"
16259            | "heat_capacity_q" | "calorimeter_dt" | "enthalpy_reaction"
16260            | "avogadro_count" | "moles_from_mass"
16261            | "dilution_v2" | "raoult_law" | "bp_elevation" | "fp_depression"
16262            | "osmotic_pressure" | "rydberg_lambda" | "bohr_radius_n"
16263            | "bohr_energy_ev" | "photon_energy_freq" | "photon_energy_lambda"
16264            | "de_broglie"
16265            // ── biology / ecology ───────────────────────────────
16266 | "logistic_growth_step" | "logistic_growth_analytic"
16267            | "gompertz_growth_step" | "allee_growth_step"
16268 | "growth_rate_from_ratio"
16269 | "seir_step" | "seird_step" | "sis_step"
16270            | "r0_basic" | "rt_effective" | "herd_immunity_threshold" | "generation_time"
16271 | "inverse_simpson"
16272            | "pielou_evenness" | "margalef_richness" | "menhinick_richness"
16273            | "berger_parker" | "sorensen_dice"
16274            | "rao_quadratic_entropy"
16275 | "selection_step" | "nei_genetic_distance"
16276            | "effective_pop_size" | "carrying_capacity_from_data"
16277            | "petersen_estimator" | "chapman_estimator"
16278            | "lv_competition_step"
16279            | "holling_type1" | "holling_type2" | "holling_type3"
16280            | "leslie_step" | "net_reproductive_rate" | "generation_time_demo"
16281            | "finite_rate_lambda" | "kleibers_law" | "bergmann_adjust"
16282            | "q10" | "species_area" | "intrinsic_growth_rate"
16283            | "macarthur_wilson_immigration" | "macarthur_wilson_extinction"
16284            | "island_equilibrium"
16285            // ── EM / optics / relativity ────────────────────────
16286 | "efield_point" | "epotential_point"
16287 | "capacitor_charge"
16288            | "ohm_voltage" | "power_vi" | "power_i2r"
16289
16290 | "capacitance_parallel_sum"
16291            | "bfield_wire" | "bfield_solenoid" | "lorentz_force_mag"
16292 | "faraday_emf"
16293 | "lc_frequency" | "lc_omega"
16294            | "rc_tau" | "rl_tau"
16295            | "poynting_magnitude" | "em_intensity" | "radiation_pressure"
16296            | "em_wavelength" | "em_frequency"
16297            | "snell_theta2"
16298            | "index_from_speed" | "fresnel_reflection_normal"
16299            | "fresnel_rs" | "fresnel_rp"
16300            | "lensmaker" | "thin_lens_v" | "mirror_equation_v"
16301            | "lens_magnification" | "diffraction_grating_angle"
16302            | "single_slit_min" | "rayleigh_resolution"
16303            | "lorentz_gamma"
16304            | "rel_momentum" | "rel_ke" | "rel_total_energy" | "rel_energy_pm"
16305            | "relativistic_doppler" | "rel_velocity_add"
16306
16307            | "wave_string_speed" | "sound_solid" | "sound_gas"
16308            | "doppler_classical" | "standing_wave_fundamental"
16309            | "open_pipe_harmonic" | "closed_pipe_harmonic"
16310            | "sound_db"
16311            | "alfven_speed"
16312            | "grav_time_dilation" | "grav_redshift"
16313            // ── graph algorithms ────────────────────────────────
16314            | "kosaraju_scc" | "bridges"
16315            | "max_flow_ek" | "min_cut_value" | "hopcroft_karp"
16316
16317 | "katz_centrality" | "hits_simple"
16318            | "pagerank_damped" | "cc_count" | "cc_labels"
16319            | "topological_sort_kahn" | "has_cycle_directed" | "has_cycle_undirected"
16320 | "diameter_bfs" | "radius_bfs"
16321            | "num_edges" | "k_coreness"
16322            | "greedy_coloring" | "chromatic_number_greedy"
16323            | "sum_degrees" | "avg_degree" | "max_degree"
16324            | "is_tree" | "girth"
16325            // ── signal processing ───────────────────────────────
16326            | "hamming_window" | "hann_window" | "blackman_window"
16327            | "blackman_harris_window" | "bartlett_window" | "welch_window"
16328            | "kaiser_window" | "tukey_window" | "gaussian_window"
16329            | "hilbert_envelope"
16330            | "biquad_step" | "biquad_lowpass_coeffs" | "biquad_highpass_coeffs"
16331            | "biquad_bandpass_coeffs" | "biquad_notch_coeffs" | "biquad_allpass_coeffs"
16332            | "biquad_peak_coeffs" | "biquad_lowshelf_coeffs" | "biquad_highshelf_coeffs"
16333            | "butterworth_prewarp" | "butterworth_order"
16334            | "fir_moving_average" | "fir_lowpass_design"
16335 | "spectrogram_simple"
16336            | "zero_pad" | "resample_nearest" | "resample_linear" | "quantize"
16337            | "mu_law_encode" | "mu_law_decode" | "a_law_encode" | "a_law_decode"
16338            | "chirp_linear"
16339            // ── cryptography deep ───────────────────────────────
16340            | "fnv1a_32" | "fnv1a_64" | "sdbm_hash"
16341            | "siphash24"
16342            | "pbkdf2_hmac_step" | "scrypt_round" | "bcrypt_cost_iters"
16343            | "argon2_block_mix" | "hkdf_expand_step"
16344            | "lfsr_galois_step" | "mt19937_temper" | "xorshift64" | "xorshift32"
16345            | "pcg32_step" | "lcg_numrec_step" | "splitmix64_step" | "wyhash_mix"
16346
16347            | "xor_cipher_byte"
16348            | "railfence_encrypt" | "beaufort" | "affine_encrypt" | "substitution_encrypt"
16349            | "letter_frequency" | "english_chi2" | "index_of_coincidence" | "kasiski_repeats"
16350            | "deterministic_prime" | "dh_shared" | "rsa_encrypt_simple"
16351            | "monobit_test" | "approximate_entropy"
16352            // ── ML extensions ───────────────────────────────────
16353            | "gini_impurity" | "entropy_bits" | "information_gain" | "gain_ratio"
16354            | "nb_gaussian_likelihood" | "nb_bernoulli_likelihood" | "nb_multinomial_log_likelihood"
16355            | "adaboost_alpha" | "hinge_loss" | "squared_hinge"
16356            | "logistic_loss"
16357 | "sigmoid_grad" | "tanh_grad"
16358 | "relu_grad"
16359 | "softsign" | "prelu" | "threshold_act"
16360            | "confusion_counts" | "mcc" | "f_beta" | "specificity"
16361            | "balanced_accuracy" | "cohen_kappa" | "brier_score" | "log_loss"
16362            | "tversky" | "mahalanobis_1d"
16363 | "one_hot" | "topk_indices"
16364            | "minmax_scale" | "zscore_norm" | "robust_scale"
16365            // ── geometry / topology ─────────────────────────────
16366            | "triangle_area_heron" | "triangle_area_pts"
16367            | "triangle_inradius" | "triangle_circumradius"
16368            | "regular_ngon_area" | "regular_ngon_inradius" | "regular_ngon_circumradius"
16369 | "n_ball_volume"
16370 | "cylinder_surface" | "cone_surface"
16371
16372            | "ellipsoid_volume" | "ellipsoid_surface_approx"
16373            | "dist_point_line_2d" | "dist_point_plane_3d" | "closest_pt_segment_2d"
16374            | "bbox_from_points"
16375 | "euclidean_distance_nd"
16376 | "hamming_distance_str"
16377 | "great_circle_law_of_cos"
16378            | "initial_bearing" | "midpoint_great_circle"
16379            | "shoelace_area" | "polygon_is_convex" | "convex_hull_jarvis"
16380            | "euler_characteristic" | "genus_from_euler"
16381            | "spherical_triangle_area" | "polygon_with_holes_area" | "picks_theorem"
16382            | "centroid_nd" | "covariance_matrix_pts" | "simplex_volume_3d"
16383            // ── special functions extra ─────────────────────────
16384            | "hyper2f1" | "hyper1f1" | "hyper0f1" | "pochhammer"
16385            | "mathieu_ce0" | "mathieu_se1" | "parabolic_d0" | "parabolic_d1"
16386            | "whittaker_m" | "struve_h0" | "struve_h1"
16387            | "lambert_w0" | "wright_omega"
16388            | "sinhc" | "cosh_minus1_over_x2"
16389            | "sine_integral_si" | "cosine_integral_ci" | "exp_integral_e1"
16390 | "dawson_function" | "owen_t"
16391            | "spherical_bessel_j0" | "spherical_bessel_j1"
16392            | "spherical_bessel_y0" | "spherical_bessel_y1"
16393            | "mod_sph_bessel_i0" | "mod_sph_bessel_i1" | "mod_sph_bessel_k0"
16394            | "coulomb_f0"
16395            | "polylog_li2" | "polylog_n"
16396
16397 | "ti2" | "clausen_cl2"
16398            | "bose_einstein_g" | "fermi_dirac_int"
16399            | "theta3" | "theta2"
16400            | "jacobi_sn_small_q" | "jacobi_cn_small_q" | "jacobi_dn_small_q"
16401            | "riemann_xi" | "bessel_jn_general" | "bessel_in_general"
16402            // ── astronomy / music / color / units ───────────────
16403 | "absolute_magnitude"
16404            | "pc_to_ly" | "ly_to_pc" | "pc_to_au" | "au_to_m"
16405            | "solar_mass_to_kg" | "solar_luminosity_to_w"
16406            | "hubble_distance_mpc" | "comoving_distance_approx" | "critical_density"
16407            | "et_freq_ratio" | "midi_to_hz" | "hz_to_midi" | "cents_between"
16408            | "just_intonation_ratio" | "pythagorean_ratio"
16409            | "beat_frequency" | "bpm_to_spb" | "note_name_to_midi"
16410 | "rgb_to_yiq" | "rgb_to_yuv601"
16411            | "srgb_to_xyz" | "xyz_to_lab" | "delta_e_94"
16412
16413
16414            | "feet_to_meters" | "meters_to_feet"
16415            | "lb_to_kg" | "kg_to_lb"
16416            | "mph_to_kmh" | "kmh_to_mph" | "mps_to_kmh" | "kmh_to_mps" | "knots_to_kmh"
16417 | "atm_to_pa" | "pa_to_atm" | "mmhg_to_pa"
16418            | "ev_to_joules" | "joules_to_ev" | "btu_to_joules" | "kwh_to_joules"
16419            | "bpm_to_midi_tick_us" | "iso226_phon_adjustment"
16420            | "db_to_amp" | "amp_to_db"
16421            | "roman_encode" | "roman_decode" | "number_to_english"
16422            // ── cosmology / GR / FLRW ───────────────────────────
16423            | "hubble_lcdm" | "hubble_time" | "hubble_distance_si" | "critical_density_si"
16424            | "comoving_distance" | "angular_diameter_distance"
16425            | "lookback_time" | "age_at_z" | "scale_factor" | "redshift_from_a"
16426            | "omega_m_at_z" | "lcdm_eos" | "cpl_w" | "deceleration_q"
16427            | "schwarzschild_radius_kg" | "kerr_ergosphere_eq" | "kerr_horizon"
16428 | "bh_entropy" | "bh_evaporation_time"
16429            | "schwarzschild_isco" | "photon_sphere_radius"
16430            | "tidal_force" | "grav_dilation_factor" | "lense_thirring_omega"
16431            | "gw_strain_amplitude" | "chirp_mass" | "grav_binding_energy"
16432            | "roche_limit_rigid" | "roche_limit_fluid"
16433            | "lagrange_l1" | "sphere_of_influence"
16434            | "freefall_velocity_schwarzschild" | "einstein_ring_radius"
16435            | "microlensing_magnification" | "cosmic_distance_modulus_si"
16436            | "cmb_temperature" | "cmb_temperature_at_z"
16437 | "stefan_boltzmann_si" | "planck_spectral_radiance"
16438            | "schwarzschild_g_tt" | "schwarzschild_g_rr" | "kretschmann_schwarzschild"
16439            | "hill_velocity" | "vacuum_energy_density"
16440            | "sound_horizon_recomb" | "bao_scale_today" | "sigma8_default"
16441            | "lensing_convergence" | "sigma_crit"
16442            | "perihelion_precession" | "shapiro_delay" | "light_deflection_angle"
16443 | "tov_mass_limit"
16444            | "main_sequence_lifetime" | "schwarzschild_freefall_time"
16445            | "friedmann_density_total" | "cosmological_constant"
16446
16447 | "planck_energy"
16448            // ── quantum mechanics deep ──────────────────────────
16449            | "pure_state_density" | "purity"
16450            | "linear_entropy" | "quantum_mutual_info"
16451 | "eof_from_concurrence"
16452            | "bell_state_index" | "chsh_expectation" | "tsirelson_bound"
16453            | "pauli_real_part" | "pauli_y_imag"
16454            | "bloch_to_density_real" | "bloch_purity_check"
16455            | "fidelity_pure_real" | "l1_coherence" | "relative_entropy_coherence"
16456            | "kraus_apply" | "bit_flip_prob" | "phase_flip_prob"
16457            | "depolarizing_density_2x2" | "amplitude_damping_excited"
16458            | "quantum_fisher_info" | "cramer_rao_bound" | "squeezing_db" | "heisenberg_min"
16459            | "coherent_mean_photons" | "thermal_mean_photons" | "poisson_photon_pmf"
16460            | "bose_einstein_pmf" | "mandel_q" | "g2_zero"
16461            | "free_particle_energy" | "infinite_well_energy" | "harmonic_oscillator_energy"
16462            | "hydrogen_energy_n" | "stark_shift_linear"
16463            | "zeeman_energy" | "larmor_frequency" | "rabi_frequency"
16464            | "schrodinger_step_real" | "probability_density" | "state_norm" | "state_normalize"
16465 | "quantum_variance" | "spin_casimir"
16466            | "cg_simple" | "wigner_3j_bound" | "qho_ground_state"
16467            | "tunneling_prob" | "gamow_factor" | "compton_wavelength" | "uncertainty_position"
16468            | "berry_phase_spin_half" | "zeno_survival" | "decoherence_time"
16469            | "ramsey_visibility" | "fermi_golden_rule"
16470            // ── bioinformatics deep ─────────────────────────────
16471            | "needleman_wunsch_score" | "smith_waterman_score" | "pam250_score"
16472            | "tanimoto_bits" | "translate_dna" | "transcribe_dna_rna" | "reverse_transcribe"
16473            | "at_content" | "tm_wallace" | "tm_marmur" | "codon_adaptation_index"
16474            | "kmer_jaccard" | "sequence_shannon_info" | "pwm_score"
16475            | "msa_column_entropy" | "seq_logo_information"
16476 | "damerau_levenshtein" | "lcs_length"
16477 | "hirschberg_lcs_length" | "common_kmers"
16478            | "jukes_cantor_distance" | "kimura_2p_distance" | "felsenstein_step"
16479            | "branch_length_substitutions" | "num_unrooted_trees" | "bayes_posterior"
16480            | "hw_expected_counts" | "allele_frequency" | "ld_d" | "ld_r_squared"
16481 | "heterozygosity" | "ne_from_variance"
16482            | "expected_coverage" | "lander_waterman_gaps"
16483            | "bh_adjusted_p" | "zscore_count"
16484 | "go_enrichment_p" | "blosum45_score"
16485            | "henikoff_weight" | "hamming_protein" | "codon_usage_variance"
16486            | "dnds_ratio" | "mutation_rate" | "tajimas_d" | "wattersons_theta"
16487            | "coalescent_expected_time" | "coalescent_tree_length" | "nm_from_fst"
16488            // ── ODE advanced ────────────────────────────────────
16489            | "bdf1_step" | "bdf2_step" | "bdf3_step" | "bdf4_step" | "bdf5_step" | "bdf6_step"
16490            | "ab1_step" | "ab2_step" | "ab3_step"
16491            | "am2_step" | "am3_step" | "am4_step"
16492            | "ros2_step" | "imex_euler_step" | "symplectic_euler_step"
16493            | "leapfrog_step" | "stormer_verlet_step"
16494            | "rk4_single" | "dopri5_combine" | "rkf45_error"
16495            | "lobatto_iiia_2" | "lobatto_iiic_3" | "gauss_irk_2_stage" | "magnus_1st"
16496            | "euler_lte" | "trapezoidal_lte" | "pi_step_size"
16497            | "stiffness_ratio" | "spectral_radius"
16498            | "heun_euler_step" | "bogacki_shampine_step" | "verner_8_combine"
16499            | "rk_combine" | "ab_coeff_sum"
16500            | "newmark_beta_step" | "wilson_theta_step"
16501            | "strang_split" | "lie_split"
16502            | "exp_euler_step" | "etd_rk2" | "dde_euler_step"
16503            | "em_step" | "milstein_step" | "heun_sde_step" | "stratonovich_correction"
16504            | "predictor_corrector" | "numerical_jacobian_col"
16505            | "cn_coefficient" | "imex_theta_split" | "bulirsch_stoer_step"
16506            | "cfl_number" | "diffusion_stability"
16507            | "lax_friedrichs_flux" | "lax_wendroff_flux"
16508            | "van_leer_limiter" | "minmod_limiter" | "superbee_limiter" | "mc_limiter"
16509            // ── cryptanalysis & number theory deep ──────────────
16510            | "pollard_p_minus_1" | "fermat_factor"
16511            | "trial_smallest_factor" | "bsgs_discrete_log"
16512            | "mertens" | "liouville"
16513            | "is_b_smooth" | "primorial_n"
16514            | "pseudoprime_base2" | "strong_pseudoprime"
16515            | "aks_witness_count" | "qs_relation"
16516            | "index_calculus_naive" | "lll_2x2_step" | "coppersmith_bound"
16517            | "shor_period_prob" | "rsa_d_from_e" | "dh_secret"
16518            | "elgamal_encrypt" | "ecc_point_double" | "continued_fraction_sqrt"
16519            | "pell_fundamental" | "sum_two_squares" | "class_number_bound"
16520            | "smith_normal_2x2_step" | "regulator_naive"
16521            | "power_residue_check" | "wieferich_check" | "wilson_test"
16522            | "goldbach_pair" | "english_likeness" | "xor_break_singlebyte"
16523            | "bit_reverse_64"
16524            | "gf256_multiply" | "hash_combine"
16525            // ── econometrics ────────────────────────────────────
16526            | "arch_lm_test" | "breusch_pagan_test" | "white_robust_se"
16527            | "newey_west_se" | "hansen_j_test" | "gmm_moment_condition"
16528            | "hausman_test" | "breusch_godfrey_test" | "box_pierce_test"
16529            | "adf_test_stat" | "pp_test_stat" | "kpss_test_stat"
16530            | "dickey_fuller_critical" | "engle_granger_step"
16531            | "johansen_trace_step" | "vecm_alpha_beta"
16532            | "panel_within_estimator" | "panel_between_estimator"
16533            | "panel_random_effects" | "arellano_bond_step"
16534            | "ols_estimator" | "ols_residual_variance" | "ols_r_squared"
16535            | "ols_adjusted_r2" | "akaike_info_crit" | "bayesian_info_crit"
16536            | "hannan_quinn_ic" | "f_statistic_pooled" | "breusch_pagan_lm"
16537            | "ramsey_reset_test" | "chow_test_stat" | "white_test_stat"
16538            | "goldfeld_quandt" | "wald_test_stat" | "score_test_stat"
16539            | "likelihood_ratio_test" | "two_sls_iv" | "iv_estimator"
16540            | "mle_normal_log_lik" | "mle_exponential_log_lik"
16541            | "mle_poisson_log_lik" | "gmm_moment_function"
16542            | "pooling_test_stat" | "heteroskedasticity_test"
16543            | "robust_se_huber_white" | "bootstrap_se_estimate"
16544            | "heckman_correction" | "tobit_log_likelihood"
16545            | "probit_log_likelihood" | "logit_log_likelihood"
16546            | "multinomial_logit_prob" | "ordered_probit_threshold"
16547            | "panel_var_step" | "impulse_response_step"
16548            | "variance_decomposition" | "granger_causality_chi2"
16549            | "cointegration_residual" | "error_correction_step"
16550            | "random_walk_innovation" | "random_walk_drift_step"
16551            | "ar_model_likelihood" | "ma_model_likelihood"
16552            | "arma_model_innovation"
16553            // ── algebraic topology, knot theory, lie algebras ───
16554            | "euler_char_complex" | "betti_zero" | "betti_one" | "betti_two"
16555            | "genus_surface" | "chern_first_2d" | "genus_curve_arith"
16556            | "genus_curve_geo" | "hodge_diamond_value" | "poincare_duality"
16557            | "fundamental_group_zn" | "homology_rank" | "cohomology_rank"
16558            | "homotopy_group_sphere_pi" | "mapping_class_torus"
16559            | "linking_number_two" | "writhe_polygon" | "torsion_coefficient"
16560            | "simplex_volume_n" | "simplicial_volume" | "nerve_complex_count"
16561            | "cech_zero_cohomology" | "de_rham_zero"
16562            | "poincare_polynomial_eval" | "chromatic_homology_rank"
16563            | "khovanov_q_grading" | "hochschild_zero" | "cyclic_homology_step"
16564            | "group_cohomology_dim" | "group_homology_dim"
16565            | "abelianization_quotient" | "free_group_rank_lower"
16566            | "nilpotency_class_lower" | "solvable_length_upper"
16567            | "schreier_index" | "todd_genus_eval" | "hirzebruch_signature"
16568            | "chern_simons_action" | "gauss_bonnet_total"
16569            | "seifert_genus_lower" | "alexander_polynomial_at_one"
16570            | "jones_polynomial_at_minus_one" | "jones_polynomial_at_i"
16571            | "homfly_evaluation" | "kauffman_bracket_eval"
16572            | "cabling_pair_signature" | "seifert_form_2x2"
16573            | "turaev_alexander_step" | "v_polynomial_eval"
16574            | "polynomial_jones_skein" | "delta_complex_count"
16575            | "poset_zeta_two" | "mobius_poset_two" | "mobius_function_pair"
16576            | "mobius_inversion_step" | "incidence_algebra_dim"
16577            | "quiver_path_count" | "representation_dim_step"
16578            | "weyl_group_order" | "root_system_count"
16579            | "cartan_determinant_a2" | "cartan_matrix_b2"
16580            | "killing_form_su2" | "casimir_eigenvalue_su2"
16581            | "universal_enveloping_dim" | "verma_character_step"
16582            | "plethystic_substitution_value" | "schur_polynomial_eval"
16583            | "hall_inner_product_two" | "plactic_class_size"
16584            | "robinson_schensted_pair" | "yamanouchi_word_count"
16585            | "rsk_size" | "character_su2" | "character_sun"
16586            | "quantum_dimension_su2" | "quantum_dimension_q"
16587            | "fusion_rule_su2_step" | "modular_data_s_value"
16588            | "modular_data_t_value" | "verlinde_count_step"
16589            | "quantum_invariant_eval" | "operad_count_two"
16590            | "moduli_dimension_curves" | "hodge_polynomial_eval"
16591            | "mirror_symmetry_check" | "gromov_witten_invariant"
16592            | "donaldson_invariant" | "seiberg_witten_value"
16593            | "floer_homology_rank" | "khovanov_rasmussen_s"
16594            | "ozsvath_szabo_tau" | "heegaard_genus_lower"
16595            | "fintushel_stern_step" | "bauer_furuta_step"
16596            | "geometric_intersection_number"
16597            | "algebraic_intersection_number"
16598            // ── electrochemistry, batteries, fuel cells ─────────
16599            | "nernst_potential_full" | "electrode_potential_step"
16600            | "exchange_current_density" | "butler_volmer_current"
16601            | "tafel_anodic_current" | "tafel_cathodic_current"
16602            | "mass_transport_overpotential" | "limiting_current_density"
16603            | "diffusion_layer_thickness" | "faradaic_efficiency"
16604            | "coulombic_efficiency_cell" | "energy_efficiency_cell"
16605            | "voltaic_efficiency" | "charge_capacity_battery"
16606            | "energy_density_battery" | "power_density_battery"
16607            | "specific_capacity_active" | "columbic_capacity_lihalfcell"
16608            | "ragone_point" | "peukert_capacity" | "peukert_exponent_fit"
16609            | "shepherd_voltage_step" | "nernst_planck_flux"
16610            | "debye_length_electrolyte" | "debye_huckel_activity"
16611            | "gouy_chapman_potential" | "stern_layer_capacitance"
16612            | "double_layer_capacitance" | "helmholtz_capacitance"
16613            | "zeta_potential_estimate" | "electroosmotic_velocity"
16614            | "hagen_poiseuille_eo" | "diffuse_layer_thickness"
16615            | "poisson_boltzmann_step" | "linearized_pb_step"
16616            | "electrochem_impedance_z" | "randles_circuit_z"
16617            | "warburg_impedance" | "cole_cole_eis" | "nyquist_phase"
16618            | "charge_transfer_resistance" | "solution_resistance_estimate"
16619            | "ionic_conductivity_arrhenius" | "nernst_einstein_diffusivity"
16620            | "walden_product" | "kohlrausch_law"
16621            | "onsager_relation_two_species" | "trasatti_voltammetry_charge"
16622            | "randles_sevcik_peak" | "levich_current_rde"
16623            | "koutecky_levich_intercept" | "mott_schottky_capacitance"
16624            | "flat_band_potential" | "schottky_barrier_height"
16625            | "photocurrent_density" | "quantum_efficiency_photo"
16626            | "overall_efficiency_pec" | "fuel_cell_polarization"
16627            | "electrolyzer_voltage" | "faraday_efficiency_h2"
16628            | "overpotential_oer" | "overpotential_her"
16629            | "electrocrystallization_step" | "nucleation_rate_constant"
16630            | "metal_corrosion_rate" | "pourbaix_line_value"
16631            | "mixed_potential_step" | "electrochemiluminescence_yield"
16632            | "solid_electrolyte_capacity" | "ionic_liquid_viscosity_step"
16633            | "lithium_ion_diffusivity" | "soc_estimate_coulomb"
16634            | "soh_capacity_fade" | "ocv_lithium_ion_step"
16635            | "state_of_charge_kalman" | "thermal_runaway_threshold"
16636            | "joule_heating_battery" | "calorimetric_heat_battery"
16637            | "abuse_test_voltage" | "swelling_strain_step"
16638            | "sei_resistance_growth" | "binder_content_optimal"
16639            | "porosity_active_layer" | "tortuosity_estimate_bruggeman"
16640            | "electrolyte_decomposition_temp" | "gibbs_thomson_undercooling"
16641            | "nernst_diffusion_layer" | "diff_coeff_aqueous_estimate"
16642            | "salt_activity_coefficient" | "mean_activity_coeff_pitzer"
16643            | "osmotic_coefficient_pitzer" | "debye_huckel_screening_factor"
16644            | "ph_at_isoelectric" | "buffer_capacity_acid_base"
16645            | "henderson_hasselbalch_solve" | "titration_endpoint_index"
16646            // ── tensor calculus, GR, differential geometry ──────
16647            | "tensor_contract_two" | "tensor_outer_two" | "tensor_trace_index"
16648            | "tensor_symmetrize_two" | "tensor_antisymmetrize_two"
16649            | "levi_civita_three" | "levi_civita_four"
16650            | "kronecker_three" | "kronecker_four"
16651            | "metric_minkowski_eta_step" | "metric_schwarzschild_step"
16652            | "metric_kerr_step_simple" | "metric_frw_lapse"
16653            | "christoffel_first_kind_step" | "christoffel_second_kind_step"
16654            | "riemann_tensor_step_zero" | "riemann_curvature_normal_form"
16655            | "ricci_tensor_step_zero" | "scalar_curvature_step"
16656            | "einstein_tensor_step" | "weyl_tensor_step_zero"
16657            | "schouten_tensor_step" | "geodesic_equation_step_zero"
16658            | "parallel_transport_step" | "covariant_derivative_step"
16659            | "christoffel_symbol_normalize" | "ricci_identity_step"
16660            | "bianchi_first_identity_check" | "bianchi_second_identity_check"
16661            | "killing_vector_lie_step" | "lie_derivative_scalar_step"
16662            | "lie_derivative_vector_step" | "exterior_derivative_one_form"
16663            | "hodge_star_one_form" | "codifferential_step"
16664            | "laplace_de_rham_step" | "volume_form_riemannian"
16665            | "hodge_inner_product_one" | "sectional_curvature_two_plane"
16666            | "gauss_codazzi_step" | "mainardi_codazzi_step"
16667            | "weingarten_map_step" | "shape_operator_eig"
16668            | "mean_curvature_step" | "gaussian_curvature_step"
16669            | "extrinsic_principal_curv" | "intrinsic_principal_curv"
16670            | "geodesic_curvature_step" | "darboux_frame_step"
16671            | "fermi_normal_step" | "synge_world_function"
16672            | "raychaudhuri_step" | "expansion_scalar_step"
16673            | "shear_tensor_step" | "twist_tensor_step"
16674            | "optical_scalars_step" | "peeling_step_psi4"
16675            | "ads_metric_step" | "de_sitter_metric_step"
16676            | "warped_product_step_zero" | "kaluza_klein_step"
16677            | "brans_dicke_step" | "horndeski_step"
16678            | "einstein_dilaton_step" | "gauss_bonnet_term_2d"
16679            | "chern_pontryagin_4d_step" | "adm_mass_step"
16680            | "komar_mass_step" | "bondi_mass_step"
16681            | "brown_york_quasilocal" | "isolated_horizon_charge"
16682            | "trapped_surface_check" | "apparent_horizon_step"
16683            | "event_horizon_check" | "cosmological_constant_term"
16684            | "de_sitter_radius_step" | "anti_de_sitter_radius_step"
16685            | "penrose_diagram_factor" | "conformal_compactification_step"
16686            | "schwarzschild_kruskal_step" | "gullstrand_painleve_step"
16687            | "kerr_newman_charge_term" | "boyer_lindquist_step"
16688            | "hartle_thorne_metric" | "oppenheimer_volkoff_step"
16689            | "post_newtonian_step" | "shapiro_delay_step"
16690            | "mercury_perihelion_advance"
16691            | "gravitational_wave_quadrupole"
16692            | "plus_polarization_amp" | "cross_polarization_amp"
16693            | "chirp_mass_inspiral_step" | "isco_radius_kerr_step"
16694            | "spin_orbit_coupling_term" | "spin_spin_coupling_term"
16695            | "hawking_area_increase" | "unruh_temperature_full"
16696            | "bekenstein_entropy_step" | "holographic_entanglement_step"
16697            | "ryu_takayanagi_step" | "swampland_distance_check"
16698            // ── information theory, coding, signal processing ──
16699            | "conditional_entropy_step" | "joint_entropy_step"
16700            | "relative_entropy_kl" | "mutual_information_step"
16701            | "chain_rule_entropy" | "fano_inequality_bound"
16702            | "data_processing_inequality" | "arithmetic_coding_interval"
16703            | "range_coding_step" | "golomb_rice_code"
16704            | "elias_gamma_code" | "elias_delta_code" | "exp_golomb_code"
16705            | "fibonacci_code" | "shannon_fano_elias_code"
16706            | "huffman_balanced_step" | "arithmetic_decode_interval"
16707            | "range_decode_step" | "universal_code_length"
16708            | "ziv_lempel_estimate" | "lz77_match_length"
16709            | "lz78_dictionary_growth" | "lzw_step_dict"
16710            | "ppm_predict_prob" | "deflate_huffman_lit"
16711            | "brotli_distance_code_count" | "zstd_window_size_log"
16712            | "mpeg_quant_value" | "jpeg_zig_zag_index"
16713            | "jpeg_dct_8x8_quant" | "hadamard_walsh_transform_step"
16714            | "karhunen_loeve_step" | "discrete_haar_step"
16715            | "db4_wavelet_step" | "biorthogonal_step"
16716            | "beylkin_wavelet_step" | "coiflet_wavelet_step"
16717            | "mallat_pyramid_step" | "threshold_soft_value"
16718            | "threshold_hard_value" | "median_filter_window"
16719            | "mean_filter_window" | "gaussian_filter_window"
16720            | "unsharp_mask_step" | "sobel_kernel_value"
16721            | "prewitt_kernel_value" | "roberts_kernel_value"
16722            | "laplacian_kernel_value" | "canny_threshold_step"
16723            | "hough_accumulator_step" | "ransac_iteration_count"
16724            | "optical_flow_lk_step" | "horn_schunck_step"
16725            | "kalman_predict_state" | "kalman_update_state"
16726            | "particle_filter_resample" | "unscented_sigma_point"
16727            | "ekf_jacobian_step" | "markov_decision_value"
16728            | "bellman_equation_step" | "q_learning_update"
16729            | "policy_iteration_step" | "value_iteration_step"
16730            | "sarsa_update" | "double_q_learning_step"
16731            | "ucb1_action_value" | "thompson_sample_beta"
16732            | "boltzmann_softmax_action" | "explore_exploit_epsilon"
16733            | "montecarlo_returns_step" | "td_zero_update"
16734            | "td_lambda_update" | "gradient_temporal_diff"
16735            | "deep_q_target" | "ddpg_critic_loss_step"
16736            | "ppo_clip_term" | "trpo_kl_constraint"
16737            | "a3c_advantage_step" | "ppo_advantage_step"
16738            | "gae_advantage_step" | "generalized_advantage"
16739            | "information_bottleneck_step" | "free_energy_principle"
16740            | "fisher_info_metric" | "kullback_jensen_div"
16741            | "hellinger_distance_step" | "total_variation_distance"
16742            | "bhattacharyya_coefficient" | "wasserstein_dist_emp"
16743            | "chisquare_metric" | "hellinger_kernel"
16744            | "jensen_shannon_div" | "renyi_divergence_step"
16745            | "amari_alpha_div" | "csiszar_phi_div"
16746            | "sinkhorn_iteration_step" | "sliced_wasserstein"
16747            | "gromov_wasserstein_step" | "spectral_signature_match"
16748            | "mfcc_coeff_step" | "chroma_feature_step"
16749            // ── combinatorial optimization, scheduling ──────────
16750            | "tsp_lower_bound_mst" | "tsp_held_karp_step"
16751            | "christofides_ratio_bound" | "two_opt_swap_delta"
16752            | "or_opt_delta" | "three_opt_delta" | "lin_kernighan_step"
16753            | "nearest_neighbor_tour_step" | "greedy_edge_tour"
16754            | "nearest_insertion_step" | "farthest_insertion_step"
16755            | "cheapest_insertion_step" | "max_flow_ford_fulkerson_step"
16756            | "edmonds_karp_step" | "dinic_blocking_flow"
16757            | "push_relabel_step" | "boykov_kolmogorov_step"
16758            | "mincut_stoer_wagner" | "gomory_hu_step"
16759            | "karger_contract_edge" | "karger_min_cut_count"
16760            | "maximum_bipartite_matching" | "hopcroft_karp_phase"
16761            | "blossom_match_step" | "weighted_match_kuhn_step"
16762            | "hungarian_method_step" | "ap_jonker_volgenant_step"
16763            | "assignment_lower_bound" | "job_shop_makespan_lower"
16764            | "flow_shop_johnson_step" | "parallel_machine_lpt"
16765            | "parallel_machine_spt" | "list_scheduling_step"
16766            | "graham_2approx_bound" | "chc_bound_makespan"
16767            | "bin_packing_first_fit" | "bin_packing_best_fit"
16768            | "bin_packing_next_fit" | "bin_packing_lower_bound_l1"
16769            | "multidim_packing_step" | "knapsack_01_dp_value"
16770            | "knapsack_unbounded_dp" | "knapsack_fractional_step"
16771            | "knapsack_branch_bound" | "knapsack_lp_relaxation"
16772            | "multi_knapsack_step" | "quadratic_assignment_step"
16773            | "qap_lower_bound" | "graph_coloring_dsatur_step"
16774            | "graph_coloring_welsh_powell"
16775            | "graph_coloring_brooks_bound" | "graph_coloring_lp_bound"
16776            | "fractional_chromatic_lower" | "list_coloring_step"
16777            | "edge_coloring_vizing_step" | "clique_number_lower"
16778            | "independence_number_upper" | "vertex_cover_lp_round"
16779            | "dominating_set_greedy_step" | "dominating_set_lp_bound"
16780            | "set_cover_greedy_step" | "set_cover_lp_round"
16781            | "hitting_set_greedy" | "weighted_set_cover_step"
16782            | "matroid_greedy_step" | "matroid_intersection_step"
16783            | "submodular_greedy_step" | "submodular_curvature_bound"
16784            | "nemhauser_wolsey_bound" | "lp_relax_round"
16785            | "branch_and_bound_step" | "cutting_plane_step"
16786            | "gomory_cut_step" | "chvatal_gomory_cut"
16787            | "mixed_integer_round_up" | "mixed_integer_round_down"
16788            | "sos_constraint_check" | "column_generation_step"
16789            | "benders_decomposition_step" | "dantzig_wolfe_step"
16790            | "lagrangian_relax_step" | "lagrangian_dual_step"
16791            | "subgradient_step_size" | "nonlinear_dual_step"
16792            | "augmented_lagrangian_step" | "admm_primal_step"
16793            | "admm_dual_step" | "proximal_gradient_step"
16794            | "nesterov_accelerate_step" | "fista_step" | "ista_step"
16795            | "mirror_descent_step" | "frank_wolfe_step"
16796            | "conditional_gradient_step" | "greedy_set_cover_round"
16797            | "local_search_swap_step" | "tabu_search_move_score"
16798            | "simulated_annealing_step" | "genetic_crossover_one_point"
16799            | "mutation_bit_flip_prob" | "roulette_wheel_select_index"
16800            // ── climate, fluids, atmospheric ────────────────────
16801            | "stefan_boltzmann_radiation" | "emissivity_grey_body"
16802            | "albedo_blackbody_balance" | "solar_constant_at_distance"
16803            | "total_solar_irradiance_step" | "absorbed_short_wave"
16804            | "emitted_long_wave" | "clausius_clapeyron_full"
16805            | "relative_humidity_step" | "dewpoint_temperature_full"
16806            | "wet_bulb_potential" | "virtual_temperature_full"
16807            | "density_altitude_full" | "geopotential_height_full"
16808            | "geometric_height_full" | "adiabatic_lapse_rate_dry"
16809            | "adiabatic_lapse_rate_moist" | "brunt_vaisala_full"
16810            | "richardson_number_step" | "gradient_richardson_full"
16811            | "flux_richardson_full" | "turbulent_kinetic_energy_step"
16812            | "mixing_length_prandtl" | "monin_obukhov_length"
16813            | "similarity_function_phi" | "log_law_wind_profile"
16814            | "power_law_wind_profile" | "ekman_layer_depth"
16815            | "ekman_pumping_step" | "geostrophic_wind_step"
16816            | "gradient_wind_step" | "thermal_wind_step"
16817            | "quasi_geostrophic_omega" | "omega_equation_step"
16818            | "potential_temperature_step" | "equivalent_potential_temp"
16819            | "saturation_equivalent_pt" | "ipv_potential_vorticity"
16820            | "ertel_pv_step" | "absolute_vorticity_step"
16821            | "relative_vorticity_step" | "divergence_omega_step"
16822            | "streamfunction_step" | "velocity_potential_step"
16823            | "helmholtz_decomp_step" | "courant_friedrichs_lewy"
16824            | "peclet_number_step" | "prandtl_number_step"
16825            | "reynolds_full_number" | "schmidt_number_step"
16826            | "sherwood_number_step" | "nusselt_full_number"
16827            | "grashof_number_step" | "rayleigh_number_step"
16828            | "weber_number_step" | "froude_number_step"
16829            | "strouhal_full" | "mach_full_step"
16830            | "biot_number_step" | "fourier_number_step"
16831            | "turbulence_intensity_step" | "hurst_exponent_estimate"
16832            | "detrended_fluct_alpha" | "power_spectrum_slope"
16833            | "spectral_kappa_minus53" | "batchelor_scale_step"
16834            | "kolmogorov_microscale" | "taylor_microscale_step"
16835            | "integral_length_scale" | "turbulent_dissipation_eps"
16836            | "isotropic_relation_check" | "sst_anomaly_step"
16837            | "enso_index_step" | "amo_index_step" | "nao_index_step"
16838            | "soi_oscillation_index" | "pdo_index_step" | "mjo_phase_step"
16839            | "walker_circulation_step" | "hadley_cell_max_lat"
16840            | "ferrel_cell_step" | "itcz_position_lat" | "trade_wind_speed"
16841            | "westerlies_jet_speed" | "polar_vortex_radius"
16842            | "arctic_oscillation_step" | "indian_monsoon_index"
16843            | "african_monsoon_index" | "qbo_oscillation_step"
16844            | "solar_cycle_phase" | "sunspot_relative_number"
16845            | "geomagnetic_kp_index" | "ozone_dobson_total"
16846            | "chlorine_radical_decay" | "montreal_protocol_track"
16847            | "co2_growth_rate_step" | "methane_growth_rate"
16848            | "aerosol_optical_depth" | "ice_age_milankovitch"
16849            | "greenhouse_forcing_step"
16850            // ── game theory, mechanism design, social choice ────
16851            | "game_two_player_value" | "nash_equilibrium_pair"
16852            | "mixed_strategy_value" | "zero_sum_minmax"
16853            | "saddle_point_check" | "correlated_equilibrium_value"
16854            | "shapley_value_two_step" | "banzhaf_index_two"
16855            | "nucleolus_lp_step" | "core_membership_check"
16856            | "imputation_efficient_check" | "imputation_individual_rational"
16857            | "prisoners_dilemma_payoff" | "matching_pennies_payoff"
16858            | "chicken_game_payoff" | "stag_hunt_payoff"
16859            | "battle_sexes_payoff" | "public_goods_game_payoff"
16860            | "tragedy_commons_metric" | "ultimatum_acceptance_prob"
16861            | "dictator_game_share" | "trust_game_repayment"
16862            | "cooperative_game_value" | "characteristic_function"
16863            | "bargaining_set_check" | "kalai_smorodinsky_step"
16864            | "nash_bargaining_solution" | "egalitarian_solution"
16865            | "utilitarian_solution" | "social_welfare_sum"
16866            | "arrow_impossibility_check" | "gibbard_satterthwaite_check"
16867            | "borda_count_step" | "condorcet_winner_check"
16868            | "plurality_winner_step" | "kemeny_score_step"
16869            | "dodgson_swap_count" | "coombs_runoff_step"
16870            | "single_transferable_vote" | "range_voting_score"
16871            | "approval_voting_max" | "schulze_method_step"
16872            | "copeland_score_step" | "black_method_winner"
16873            | "median_voter_step" | "hotelling_location_step"
16874            | "arrow_pareto_check" | "fair_division_envy_free"
16875            | "proportional_share" | "maximin_share"
16876            | "egalitarian_split" | "nash_social_welfare"
16877            | "divisible_goods_proportional" | "indivisible_envy_free_check"
16878            | "adjusted_winner_pct" | "sealed_bid_first_price"
16879            | "sealed_bid_second_price" | "english_auction_step"
16880            | "dutch_auction_step" | "all_pay_auction_step"
16881            | "vcg_payment_step" | "revenue_equivalence_check"
16882            | "truthful_mechanism_check" | "incentive_compatibility_check"
16883            | "mechanism_design_obj" | "double_auction_step"
16884            | "combinatorial_auction_step" | "posted_price_offer_accept"
16885            | "matching_market_step" | "deferred_acceptance_step"
16886            | "boston_mechanism_step" | "top_trading_cycles_step"
16887            | "school_choice_match" | "roommate_match_step"
16888            | "network_formation_step" | "coordination_game_payoff"
16889            | "evolutionary_stable_strategy" | "replicator_dynamics_step"
16890            | "hawk_dove_payoff" | "fictitious_play_step"
16891            | "best_response_dynamic" | "quantal_response_logit"
16892            | "level_k_step" | "cognitive_hierarchy_step"
16893            | "sequential_eq_check" | "subgame_perfect_eq"
16894            | "stackelberg_step" | "cournot_quantity_step"
16895            | "bertrand_price_step" | "hotelling_price_step"
16896            | "collusion_payoff_step" | "folk_theorem_value"
16897            | "repeated_game_avg_payoff" | "discount_factor_step"
16898            | "trigger_strategy_payoff" | "grim_trigger_step"
16899            | "tit_for_tat_step" | "prisoners_repeated_eq"
16900            | "mertens_zamir_step" | "ex_post_value_check"
16901            | "ex_ante_value_check" | "common_knowledge_iterations"
16902            // ── symbolic CAS, decompositions, projections ───────
16903            | "cas_simplify_term" | "cas_expand_two_terms"
16904            | "cas_factor_quadratic" | "cas_partial_fraction_simple"
16905            | "cas_polynomial_gcd_step" | "cas_polynomial_div_step"
16906            | "cas_lagrange_interpolate" | "cas_chebyshev_eval"
16907            | "cas_legendre_eval" | "cas_hermite_eval"
16908            | "cas_laguerre_eval" | "cas_jacobi_eval"
16909            | "cas_gegenbauer_eval" | "cas_taylor_coefficient"
16910            | "cas_padé_diagonal" | "cas_continued_fraction_step"
16911            | "cas_resultant_two" | "cas_subresultant_two"
16912            | "cas_groebner_lt_step" | "cas_buchberger_step"
16913            | "cas_macaulay_matrix_step" | "cas_modular_inverse"
16914            | "cas_extended_euclid_step" | "cas_smith_normal_step"
16915            | "cas_hermite_normal_step" | "cas_radical_simplify"
16916            | "cas_minimal_polynomial" | "cas_gcd_polynomial_step"
16917            | "cas_resultant_x_y" | "cas_solve_linear"
16918            | "cas_solve_quadratic" | "cas_solve_cubic"
16919            | "cas_solve_quartic" | "cas_solve_polynomial_n"
16920            | "cas_root_isolate_step" | "cas_sturm_sequence_step"
16921            | "cas_descartes_rule_count" | "cas_companion_matrix_root"
16922            | "cas_polynomial_roots_kahan"
16923            | "cas_eigenvalue_inverse_iteration" | "cas_qr_iteration_step"
16924            | "cas_jacobi_eigen_step" | "cas_lanczos_iteration_step"
16925            | "cas_arnoldi_iteration_step" | "cas_givens_rotation_apply"
16926            | "cas_householder_reflection" | "cas_modified_gram_schmidt"
16927            | "cas_classical_gram_schmidt" | "cas_rank_revealing_qr"
16928            | "cas_pivoted_lu_step" | "cas_block_lu_step"
16929            | "cas_cholesky_step" | "cas_modified_cholesky"
16930            | "cas_ldlt_step" | "cas_bunch_kaufman_step"
16931            | "cas_woodbury_identity" | "cas_matrix_pencil_step"
16932            | "cas_generalized_eigen" | "cas_singular_value_step"
16933            | "cas_truncated_svd_value" | "cas_pseudoinverse_step"
16934            | "cas_polar_decomposition" | "cas_schur_decomposition_step"
16935            | "cas_quasi_triangular" | "cas_riccati_continuous_step"
16936            | "cas_riccati_discrete_step" | "cas_lyapunov_continuous_step"
16937            | "cas_lyapunov_discrete_step" | "cas_sylvester_equation_step"
16938            | "cas_kronecker_product_step" | "cas_vec_operator_step"
16939            | "cas_matrix_function_step" | "cas_matrix_log_step"
16940            | "cas_matrix_exp_pade" | "cas_matrix_sqrt_step"
16941            | "cas_drazin_inverse_step" | "cas_moore_penrose_step"
16942            | "cas_least_squares_solve" | "cas_total_least_squares"
16943            | "cas_constrained_ls_step" | "cas_truncated_lsq"
16944            | "cas_regularized_lsq_tikhonov" | "cas_basis_pursuit_step"
16945            | "cas_lasso_soft_threshold" | "cas_elastic_net_step"
16946            | "cas_omp_step" | "cas_iht_iteration"
16947            | "cas_cosamp_step" | "cas_admm_lasso_step"
16948            | "cas_proximal_l1_step" | "cas_proximal_l2_step"
16949            | "cas_proximal_l_inf_step" | "cas_indicator_simplex_proj"
16950            | "cas_proj_l1_ball" | "cas_proj_l2_ball"
16951            | "cas_proj_box" | "cas_proj_psd_cone"
16952            | "cas_proj_soc_step" | "cas_proj_exp_cone"
16953            | "cas_dykstra_step" | "cas_alternating_projection"
16954            | "cas_polya_enumeration_step" | "cas_burnside_count_step"
16955            // ── ML primitives — activations, losses, optimizers ─
16956            | "ml_relu_step" | "ml_leaky_relu_step" | "ml_elu_step"
16957            | "ml_selu_step" | "ml_gelu_step" | "ml_swish_step"
16958            | "ml_mish_step" | "ml_softplus_step" | "ml_softsign_step"
16959            | "ml_hard_sigmoid" | "ml_hard_tanh" | "ml_prelu_step"
16960            | "ml_celu_step" | "ml_silu_step" | "ml_logsumexp_step"
16961            | "ml_log_softmax_step" | "ml_log_sigmoid"
16962            | "ml_glu_step" | "ml_geglu_step" | "ml_swiglu_step"
16963            | "ml_attention_score_step" | "ml_scaled_dot_product"
16964            | "ml_multihead_avg" | "ml_softmax_temperature"
16965            | "ml_dropout_mask_prob" | "ml_layer_norm_step"
16966            | "ml_batch_norm_step" | "ml_group_norm_step"
16967            | "ml_rms_norm_step" | "ml_instance_norm_step"
16968            | "ml_weight_norm_step" | "ml_spectral_norm_step"
16969            | "ml_l2_normalize_step" | "ml_huber_loss_step"
16970            | "ml_smooth_l1_loss" | "ml_focal_loss_step"
16971            | "ml_dice_loss_step" | "ml_iou_loss_step"
16972            | "ml_giou_loss_step" | "ml_diou_loss_step"
16973            | "ml_ciou_loss_step" | "ml_contrastive_loss"
16974            | "ml_triplet_loss_step" | "ml_arcface_loss_step"
16975            | "ml_center_loss_step" | "ml_kl_divergence_loss"
16976            | "ml_cross_entropy_loss" | "ml_binary_cross_entropy"
16977            | "ml_label_smoothing" | "ml_mixup_lambda"
16978            | "ml_cutmix_box_iou" | "ml_random_erasing_step"
16979            | "ml_cosine_lr_schedule" | "ml_warmup_lr_step"
16980            | "ml_step_lr_schedule" | "ml_exponential_lr"
16981            | "ml_polynomial_lr" | "ml_one_cycle_lr"
16982            | "ml_inverse_sqrt_lr" | "ml_cyclic_lr_step"
16983            | "ml_sgd_step" | "ml_momentum_step"
16984            | "ml_nesterov_momentum" | "ml_adagrad_step"
16985            | "ml_rmsprop_step" | "ml_adam_step"
16986            | "ml_adamw_step" | "ml_adamax_step"
16987            | "ml_nadam_step" | "ml_radam_step"
16988            | "ml_lookahead_step" | "ml_lamb_step"
16989            | "ml_lars_step" | "ml_yogi_step"
16990            | "ml_amsgrad_step" | "ml_adabelief_step"
16991            | "ml_shampoo_step" | "ml_lion_step"
16992            | "ml_sophia_step" | "ml_gradient_clip_norm"
16993            | "ml_gradient_clip_value" | "ml_gradient_accumulate"
16994            | "ml_gradient_centralize" | "ml_weight_decay_step"
16995            | "ml_he_init_value" | "ml_xavier_init_value"
16996            | "ml_glorot_init_value" | "ml_orthogonal_init"
16997            | "ml_truncnormal_init" | "ml_kaiming_init"
16998            | "ml_lecun_init_value" | "ml_zero_init"
16999            | "ml_constant_init" | "ml_uniform_init"
17000            | "ml_one_hot_index" | "ml_label_to_id"
17001            | "ml_id_to_label_step" | "ml_token_logit_top_k"
17002            | "ml_topk_argmax" | "ml_nucleus_sample_p"
17003            | "ml_temperature_decay" | "ml_repetition_penalty"
17004            | "ml_eos_logit_boost"
17005            // ── NLP — ranking, similarity, language models ──────
17006            | "nlp_bm25_score" | "nlp_tf_idf_step" | "nlp_okapi_score"
17007            | "nlp_word_freq_value" | "nlp_doc_freq_step"
17008            | "nlp_inverse_doc_freq" | "nlp_cosine_similarity_two"
17009            | "nlp_jaccard_similarity_two" | "nlp_overlap_coefficient"
17010            | "nlp_dice_coefficient_two" | "nlp_simpson_coefficient"
17011            | "nlp_levenshtein_dist" | "nlp_damerau_levenshtein"
17012            | "nlp_jaro_distance" | "nlp_jaro_winkler"
17013            | "nlp_hamming_distance" | "nlp_lcs_length" | "nlp_lcs_ratio"
17014            | "nlp_meteor_score" | "nlp_bleu_score_n"
17015            | "nlp_rouge_score_n" | "nlp_chrf_score" | "nlp_ter_score"
17016            | "nlp_wer_score" | "nlp_cer_score" | "nlp_perplexity_value"
17017            | "nlp_bits_per_character" | "nlp_char_ngram_count"
17018            | "nlp_word_ngram_count" | "nlp_skip_gram_count"
17019            | "nlp_byte_pair_merge_step" | "nlp_wordpiece_score"
17020            | "nlp_unigram_lm_score" | "nlp_kneser_ney_step"
17021            | "nlp_witten_bell_step" | "nlp_good_turing_count"
17022            | "nlp_laplace_smoothing" | "nlp_lidstone_smoothing"
17023            | "nlp_jelinek_mercer" | "nlp_dirichlet_smoothing"
17024            | "nlp_query_likelihood_step" | "nlp_kl_lm_div"
17025            | "nlp_pmi_score" | "nlp_npmi_score"
17026            | "nlp_chi2_collocation" | "nlp_loglikelihood_collocation"
17027            | "nlp_t_score_collocation" | "nlp_dunning_log_likelihood"
17028            | "nlp_lda_alpha_step" | "nlp_lda_beta_step"
17029            | "nlp_lda_topic_dist" | "nlp_plsa_step"
17030            | "nlp_word2vec_skipgram_loss" | "nlp_word2vec_cbow_loss"
17031            | "nlp_glove_loss_step" | "nlp_fasttext_subword_count"
17032            | "nlp_byte_level_bpe_step" | "nlp_sentencepiece_score"
17033            | "nlp_unigram_subword_loss" | "nlp_subword_regularization"
17034            | "nlp_pointwise_attn_score" | "nlp_relative_position_bias"
17035            | "nlp_alibi_position_bias" | "nlp_rope_rotary_angle"
17036            | "nlp_rope_apply_step" | "nlp_position_encoding_sin"
17037            | "nlp_position_encoding_cos" | "nlp_pe_freq_band"
17038            | "nlp_max_seq_len_check" | "nlp_token_drop_rate"
17039            | "nlp_byte_frequency" | "nlp_char_frequency"
17040            | "nlp_punct_ratio" | "nlp_uppercase_ratio"
17041            | "nlp_digit_ratio" | "nlp_emoji_ratio"
17042            | "nlp_url_count" | "nlp_email_count" | "nlp_phone_count"
17043            | "nlp_hashtag_count" | "nlp_mention_count"
17044            | "nlp_token_overlap_two" | "nlp_word_mover_dist"
17045            | "nlp_sif_weight_step" | "nlp_doc_embedding_avg"
17046            | "nlp_attention_pool_step" | "nlp_max_pool_step"
17047            | "nlp_avg_pool_step" | "nlp_sum_pool_step"
17048            | "nlp_self_attn_compute_step" | "nlp_cross_attn_compute_step"
17049            | "nlp_window_attn_step" | "nlp_strided_attn_step"
17050            | "nlp_block_attn_step" | "nlp_sliding_window_step"
17051            | "nlp_local_attn_step" | "nlp_dilated_attn_step"
17052            | "nlp_global_attn_step" | "nlp_sparse_attn_score"
17053            | "nlp_linformer_step" | "nlp_performer_step"
17054            | "nlp_reformer_step" | "nlp_longformer_step"
17055            | "nlp_bigbird_step" | "nlp_routing_attn_step"
17056            // ── graphics, geometry, ray tracing, BRDF, color ────
17057            | "gfx_perspective_proj_x" | "gfx_perspective_proj_y"
17058            | "gfx_orthographic_proj" | "gfx_view_matrix_step"
17059            | "gfx_lookat_forward" | "gfx_lookat_right" | "gfx_lookat_up"
17060            | "gfx_quat_to_axis_angle" | "gfx_axis_angle_to_quat"
17061            | "gfx_quat_slerp_step" | "gfx_quat_nlerp_step"
17062            | "gfx_quat_dot_two" | "gfx_quat_inverse_step"
17063            | "gfx_quat_to_euler_pitch" | "gfx_quat_to_euler_yaw"
17064            | "gfx_quat_to_euler_roll" | "gfx_euler_to_quat_x"
17065            | "gfx_euler_to_quat_y" | "gfx_euler_to_quat_z"
17066            | "gfx_euler_to_quat_w" | "gfx_rotation_matrix_xx"
17067            | "gfx_rotation_matrix_yy" | "gfx_rotation_matrix_zz"
17068            | "gfx_translation_matrix_step" | "gfx_scale_matrix_step"
17069            | "gfx_shear_matrix_xy" | "gfx_homogeneous_divide"
17070            | "gfx_screen_space_x" | "gfx_screen_space_y"
17071            | "gfx_ndc_to_screen_x" | "gfx_ndc_to_screen_y"
17072            | "gfx_screen_to_ndc_x" | "gfx_screen_to_ndc_y"
17073            | "gfx_clip_polygon_step" | "gfx_sutherland_hodgman"
17074            | "gfx_cohen_sutherland_code" | "gfx_liang_barsky_t"
17075            | "gfx_bresenham_step_x" | "gfx_bresenham_step_y"
17076            | "gfx_xiaolin_wu_intensity" | "gfx_aabb_intersect_check"
17077            | "gfx_obb_overlap_step" | "gfx_sphere_intersect_t"
17078            | "gfx_ray_triangle_t" | "gfx_ray_plane_t" | "gfx_ray_box_t"
17079            | "gfx_ray_sphere_t" | "gfx_ray_disk_t"
17080            | "gfx_ray_cylinder_t" | "gfx_ray_cone_t"
17081            | "gfx_ray_ellipsoid_t" | "gfx_ray_torus_t_approx"
17082            | "gfx_barycentric_alpha" | "gfx_barycentric_beta"
17083            | "gfx_barycentric_gamma" | "gfx_phong_diffuse_step"
17084            | "gfx_phong_specular_step" | "gfx_phong_ambient_step"
17085            | "gfx_blinn_specular_step" | "gfx_lambert_term"
17086            | "gfx_oren_nayar_term" | "gfx_cook_torrance_d_ggx"
17087            | "gfx_cook_torrance_g_smith" | "gfx_cook_torrance_f_schlick"
17088            | "gfx_disney_principled_d" | "gfx_microfacet_brdf_step"
17089            | "gfx_subsurface_scattering_term" | "gfx_translucent_falloff"
17090            | "gfx_normal_distribution_ggx"
17091            | "gfx_geometric_attenuation_smith"
17092            | "gfx_fresnel_dielectric_step" | "gfx_fresnel_conductor_step"
17093            | "gfx_index_of_refraction" | "gfx_snells_law_angle"
17094            | "gfx_total_internal_reflection" | "gfx_refract_direction_x"
17095            | "gfx_reflect_direction_x" | "gfx_environment_map_uv_u"
17096            | "gfx_environment_map_uv_v" | "gfx_cube_map_face_index"
17097            | "gfx_octahedral_encode_x" | "gfx_octahedral_encode_y"
17098            | "gfx_spherical_harmonic_y00" | "gfx_spherical_harmonic_y10"
17099            | "gfx_spherical_harmonic_y11" | "gfx_spherical_harmonic_y20"
17100            | "gfx_zonal_harmonic_step" | "gfx_irradiance_sh_eval"
17101            | "gfx_radiance_sh_eval" | "gfx_skybox_uv_u" | "gfx_skybox_uv_v"
17102            | "gfx_tonemap_reinhard" | "gfx_tonemap_aces"
17103            | "gfx_tonemap_uncharted2" | "gfx_tonemap_filmic"
17104            | "gfx_gamma_correct_step" | "gfx_srgb_to_linear"
17105            | "gfx_linear_to_srgb" | "gfx_dither_bayer_4x4"
17106            | "gfx_dither_floyd_steinberg" | "gfx_oklab_l_step"
17107            | "gfx_oklab_a_step" | "gfx_oklab_b_step"
17108            | "gfx_oklch_chroma" | "gfx_oklch_hue"
17109            | "gfx_pcg_hash_step" | "gfx_xorshift_step"
17110            | "gfx_halton_step" | "gfx_sobol_step"
17111            | "gfx_van_der_corput" | "gfx_low_discrepancy_step"
17112            | "gfx_blue_noise_value" | "gfx_perlin_noise_step"
17113            | "gfx_simplex_noise_step" | "gfx_fbm_noise_step"
17114            | "gfx_worley_noise_step" | "gfx_voronoi_distance"
17115            | "gfx_curl_noise_step" | "gfx_gradient_noise_step"
17116            | "gfx_value_noise_step" | "gfx_signed_distance_box"
17117            | "gfx_signed_distance_sphere" | "gfx_signed_distance_capsule"
17118            // ── database internals, distributed systems ─────────
17119            | "db_b_tree_split" | "db_b_tree_merge"
17120            | "db_lsm_compaction_step" | "db_skiplist_height_pick"
17121            | "db_bloom_filter_bit_index" | "db_cuckoo_filter_fingerprint"
17122            | "db_quotient_filter_canonical" | "db_count_min_sketch_bin"
17123            | "db_hyperloglog_register_max" | "db_min_hash_value"
17124            | "db_simhash_bit" | "db_consistent_hash_index"
17125            | "db_rendezvous_hash_score" | "db_jump_hash_bucket"
17126            | "db_maglev_hash_step" | "db_lru_cache_eviction_age"
17127            | "db_lfu_cache_decay" | "db_arc_cache_score"
17128            | "db_clock_cache_hand" | "db_tinylfu_admit_score"
17129            | "db_w_tinylfu_freq" | "db_buffer_pool_score"
17130            | "db_query_plan_cost_step" | "db_join_selectivity_step"
17131            | "db_index_seek_cost" | "db_seq_scan_cost"
17132            | "db_index_scan_cost" | "db_sort_cost_estimate"
17133            | "db_hash_join_cost" | "db_merge_join_cost"
17134            | "db_nested_loop_cost" | "db_query_cardinality"
17135            | "db_histogram_bucket_index" | "db_quantile_estimate_p99"
17136            | "db_t_digest_centroid" | "db_kll_quantile_step"
17137            | "db_dd_sketch_bin" | "db_reservoir_sample_index"
17138            | "db_chao_estimator_step" | "db_jaccard_minhash_estimate"
17139            | "db_distinct_estimate_lpc" | "db_distinct_estimate_hll"
17140            | "db_throttle_token_step" | "db_leaky_bucket_step"
17141            | "db_token_bucket_step" | "db_circuit_breaker_step"
17142            | "db_two_phase_commit_step" | "db_three_phase_commit_step"
17143            | "db_paxos_propose_id" | "db_raft_term_advance"
17144            | "db_raft_log_match_check" | "db_zab_epoch_step"
17145            | "db_chubby_lease_step" | "db_logical_clock_step"
17146            | "db_lamport_timestamp" | "db_vector_clock_merge"
17147            | "db_hybrid_logical_clock" | "db_crdt_g_counter_merge"
17148            | "db_crdt_pn_counter_merge" | "db_crdt_lww_register_merge"
17149            | "db_crdt_set_or_merge" | "db_consensus_quorum_size"
17150            | "db_replication_lag_step" | "db_partitions_for_n"
17151            | "db_consistent_lookup_id" | "db_chord_finger_index"
17152            | "db_kademlia_xor_distance" | "db_pastry_routing_step"
17153            | "db_dht_replicate_factor" | "db_partition_failure_check"
17154            | "db_byzantine_quorum_size" | "db_pbft_view_change"
17155            | "db_honey_badger_step" | "db_avalanche_query_step"
17156            | "db_quorum_intersection_check" | "db_anti_entropy_step"
17157            | "db_merkle_node_hash" | "db_merkle_path_verify"
17158            | "db_gossip_fanout_step" | "db_anti_entropy_pull_step"
17159            | "db_split_brain_check" | "db_clock_skew_estimate"
17160            | "db_freshness_score" | "db_read_repair_step"
17161            | "db_hinted_handoff_step" | "db_compaction_score"
17162            | "db_levelled_compaction_step" | "db_size_tiered_compaction"
17163            | "db_universal_compaction_step" | "db_write_amplification"
17164            | "db_read_amplification" | "db_space_amplification"
17165            | "db_block_cache_hit_rate" | "db_page_cache_eviction_age"
17166            | "db_wal_fsync_cost" | "db_group_commit_count"
17167            | "db_replica_lag_threshold" | "db_synchronous_commit_check"
17168            | "db_async_commit_check" | "db_eventual_consistency_check"
17169            | "db_strong_consistency_check" | "db_linearizability_check"
17170            | "db_causal_consistency_check"
17171            // ── networking — TCP, AQM, MIMO, queueing ───────────
17172            | "net_tcp_cwnd_step" | "net_tcp_ssthresh_update"
17173            | "net_tcp_reno_step" | "net_tcp_cubic_step"
17174            | "net_tcp_bbr_step" | "net_tcp_vegas_step"
17175            | "net_tcp_westwood_step" | "net_tcp_compound_step"
17176            | "net_tcp_dctcp_step" | "net_tcp_yeah_step"
17177            | "net_tcp_htcp_step" | "net_tcp_hybla_step"
17178            | "net_tcp_illinois_step" | "net_tcp_lp_step"
17179            | "net_tcp_scalable_step" | "net_tcp_veno_step"
17180            | "net_aiad_step" | "net_aimd_step"
17181            | "net_miad_step" | "net_mimd_step"
17182            | "net_aqm_red_drop_prob" | "net_aqm_codel_target"
17183            | "net_aqm_pie_drop_rate" | "net_aqm_fq_codel_step"
17184            | "net_aqm_blue_step" | "net_aqm_choke_step"
17185            | "net_aqm_sfq_step" | "net_aqm_drr_step"
17186            | "net_aqm_wrr_step" | "net_token_rate_limit"
17187            | "net_traffic_shaper_step" | "net_priority_queue_index"
17188            | "net_packet_loss_estimate" | "net_jitter_estimate"
17189            | "net_latency_avg" | "net_rtt_smoothed"
17190            | "net_rtt_variation" | "net_rto_compute"
17191            | "net_bandwidth_delay_product" | "net_path_capacity_kleinrock"
17192            | "net_loss_rate_to_throughput" | "net_throughput_padhye"
17193            | "net_throughput_mathis" | "net_throughput_response"
17194            | "net_router_buffer_size" | "net_drop_tail_check"
17195            | "net_burst_size_compute" | "net_packet_pacing_step"
17196            | "net_link_capacity_share" | "net_proportional_fair_share"
17197            | "net_max_min_fair_step" | "net_alpha_fair_step"
17198            | "net_kelly_pricing_step" | "net_network_utility_max"
17199            | "net_lyapunov_drift_plus_penalty" | "net_backpressure_step"
17200            | "net_max_weight_match" | "net_qcsma_propose"
17201            | "net_csma_back_off" | "net_alohanet_throughput"
17202            | "net_slotted_aloha_throughput" | "net_csma_efficiency"
17203            | "net_token_ring_efficiency" | "net_polling_efficiency"
17204            | "net_radio_path_loss" | "net_friis_received_power"
17205            | "net_two_ray_ground_loss" | "net_okumura_hata_loss"
17206            | "net_log_distance_path" | "net_shadowing_normal"
17207            | "net_rician_k_factor" | "net_rayleigh_envelope"
17208            | "net_doppler_shift" | "net_capacity_shannon"
17209            | "net_mimo_capacity_step" | "net_zero_forcing_beam"
17210            | "net_mmse_beam_step" | "net_water_filling_power"
17211            | "net_amc_threshold_index" | "net_harq_combining_gain"
17212            | "net_turbo_decode_iter" | "net_ldpc_iteration_step"
17213            | "net_polar_decode_step" | "net_viterbi_step"
17214            | "net_bcjr_step" | "net_outage_probability"
17215            | "net_diversity_gain" | "net_array_gain"
17216            | "net_multiplexing_gain" | "net_coding_gain"
17217            | "net_pruning_gain" | "net_macro_diversity_step"
17218            | "net_micro_diversity_step" | "net_handoff_threshold"
17219            | "net_call_admission_check" | "net_blocking_probability"
17220            | "net_erlang_b_formula" | "net_erlang_c_formula"
17221            | "net_engset_formula" | "net_little_law_l"
17222            | "net_throughput_law" | "net_response_time_law"
17223            | "net_utilization_law" | "net_forced_flow_law"
17224            // ── OS internals — schedulers, I/O, memory ──────────
17225            | "os_priority_aging_step" | "os_mlfq_demote_step"
17226            | "os_mlfq_promote_step" | "os_round_robin_quantum"
17227            | "os_completely_fair_vruntime" | "os_lottery_ticket_count"
17228            | "os_stride_pass_step" | "os_eevdf_eligible"
17229            | "os_cfs_load_balance_step" | "os_eas_energy_estimate"
17230            | "os_smt_threading_share" | "os_numa_node_distance"
17231            | "os_cpu_affinity_score" | "os_thread_migration_cost"
17232            | "os_load_average_decay" | "os_runqueue_depth"
17233            | "os_io_scheduler_deadline" | "os_io_scheduler_cfq_step"
17234            | "os_io_scheduler_noop_step" | "os_io_scheduler_bfq_step"
17235            | "os_io_scheduler_kyber_step" | "os_io_scheduler_mq_deadline"
17236            | "os_anticipation_window" | "os_elevator_step"
17237            | "os_disk_seek_time" | "os_disk_rotational_lat"
17238            | "os_disk_transfer_time" | "os_pre_fetch_window"
17239            | "os_buffer_cache_pages" | "os_dirty_page_threshold"
17240            | "os_writeback_step" | "os_swappiness_factor"
17241            | "os_kswapd_wake_threshold" | "os_oom_score_step"
17242            | "os_page_replacement_lru" | "os_page_replacement_clock"
17243            | "os_page_replacement_2q" | "os_working_set_size"
17244            | "os_thrashing_threshold" | "os_demand_paging_step"
17245            | "os_copy_on_write_check" | "os_zero_page_optimization"
17246            | "os_huge_page_threshold" | "os_transparent_hugepage"
17247            | "os_kasan_shadow_offset" | "os_kfence_check"
17248            | "os_kfence_alloc_index" | "os_slub_object_size_round"
17249            | "os_slab_color_offset" | "os_per_cpu_cache_size"
17250            | "os_buddy_order_pick" | "os_compact_memory_step"
17251            | "os_kvm_vmcs_field_offset" | "os_apic_irq_priority"
17252            | "os_msi_x_vector_count" | "os_iommu_domain_step"
17253            | "os_pci_bus_address" | "os_acpi_state_transition"
17254            | "os_cpufreq_governor_step" | "os_intel_pstate_target"
17255            | "os_amd_pstate_target" | "os_thermal_zone_trip"
17256            | "os_throttle_temperature" | "os_battery_capacity_pct"
17257            | "os_powertop_score" | "os_idle_state_select"
17258            | "os_c_state_residency" | "os_p_state_voltage"
17259            | "os_dvfs_step" | "os_voltage_scaling_step"
17260            | "os_frequency_scaling_step" | "os_inotify_event_count"
17261            | "os_epoll_ctl_count" | "os_io_uring_sqe_count"
17262            | "os_io_uring_cqe_count" | "os_kqueue_event_count"
17263            | "os_systemd_journal_size" | "os_dmesg_severity_level"
17264            | "os_audit_event_priority" | "os_apparmor_profile_active"
17265            | "os_selinux_context_match" | "os_smack_label_compare"
17266            | "os_capability_check" | "os_seccomp_filter_step"
17267            | "os_namespace_isolation" | "os_cgroup_v1_count"
17268            | "os_cgroup_v2_count" | "os_pid_max_value"
17269            | "os_thread_max_value" | "os_file_max_value"
17270            | "os_open_files_count" | "os_socket_max_value"
17271            | "os_inotify_max_watches" | "os_oom_kill_score"
17272            | "os_zswap_compress_ratio" | "os_zram_compress_ratio"
17273            | "os_swap_pressure_score" | "os_pressure_stall_step"
17274            | "os_psi_avg10_step" | "os_psi_avg60_step"
17275            | "os_psi_avg300_step" | "os_load_proc_avg"
17276            | "os_load_user_avg" | "os_load_iowait_avg"
17277            // ── security — KDFs, MFA, PKI, web sec, TLS ─────────
17278            | "sec_argon2_memcost" | "sec_argon2_timecost"
17279            | "sec_argon2_parallelism" | "sec_argon2_block_step"
17280            | "sec_pbkdf2_iter" | "sec_scrypt_n_param"
17281            | "sec_scrypt_r_param" | "sec_scrypt_p_param"
17282            | "sec_balloon_hash_step" | "sec_yescrypt_step"
17283            | "sec_bcrypt_cost_factor" | "sec_bcrypt_round_step"
17284            | "sec_password_strength_zxcvbn" | "sec_haveibeenpwned_check"
17285            | "sec_diceware_word_index" | "sec_xkcd_passphrase_score"
17286            | "sec_passphrase_entropy" | "sec_chosen_charset_strength"
17287            | "sec_keystroke_timing_var" | "sec_2fa_totp_window"
17288            | "sec_totp_drift_check" | "sec_hotp_counter_step"
17289            | "sec_yubikey_otp_check" | "sec_webauthn_attestation_check"
17290            | "sec_fido2_assertion_check" | "sec_certificate_chain_depth"
17291            | "sec_revocation_ocsp_check" | "sec_crl_age_seconds"
17292            | "sec_pki_path_validate" | "sec_x509_subject_match"
17293            | "sec_san_match_count" | "sec_basic_constraints_ca"
17294            | "sec_pinning_compare" | "sec_certificate_transparency"
17295            | "sec_dane_tlsa_match" | "sec_hpkp_pin_match"
17296            | "sec_csp_directive_match" | "sec_csrf_token_match"
17297            | "sec_cors_origin_match" | "sec_xss_filter_score"
17298            | "sec_html_escape_check" | "sec_url_safe_encode_check"
17299            | "sec_path_traversal_detect" | "sec_sqli_pattern_score"
17300            | "sec_xxe_pattern_score" | "sec_xxe_dtd_check"
17301            | "sec_command_injection_score" | "sec_idor_check"
17302            | "sec_jwt_alg_safe" | "sec_jwt_kid_match"
17303            | "sec_jwt_signature_verify" | "sec_oauth2_state_validate"
17304            | "sec_oauth2_pkce_step" | "sec_oauth_nonce_check"
17305            | "sec_session_lifetime" | "sec_idle_timeout_step"
17306            | "sec_login_throttle_step" | "sec_account_lockout_step"
17307            | "sec_password_history_check" | "sec_complexity_policy_score"
17308            | "sec_dictionary_attack_check" | "sec_brute_force_attempts"
17309            | "sec_credential_stuffing_score" | "sec_kerberos_ticket_age"
17310            | "sec_kerberos_pac_check" | "sec_kerberos_pre_auth"
17311            | "sec_ldap_bind_step" | "sec_radius_auth_step"
17312            | "sec_diameter_avp_step" | "sec_saml_assertion_age"
17313            | "sec_oidc_id_token_age" | "sec_acme_dns_challenge"
17314            | "sec_dnssec_signature_check" | "sec_spf_pass_check"
17315            | "sec_dkim_signature_check" | "sec_dmarc_policy_check"
17316            | "sec_arc_chain_step" | "sec_smtp_ssl_check"
17317            | "sec_imap_starttls_check" | "sec_pop3_security_step"
17318            | "sec_tls_alert_severity" | "sec_tls13_handshake_step"
17319            | "sec_tls12_handshake_step" | "sec_tls11_deprecation_check"
17320            | "sec_ssl3_disabled_check" | "sec_cipher_suite_strength"
17321            | "sec_cbc_mac_block_count" | "sec_gcm_iv_unique_check"
17322            | "sec_chachapoly_nonce_check" | "sec_x25519_clamping_step"
17323            | "sec_ed25519_signature_step" | "sec_ed448_signature_step"
17324            | "sec_p384_curve_step" | "sec_secp256k1_step"
17325            | "sec_blake3_chunk_step" | "sec_keccak_round_step"
17326            | "sec_sha3_padding_step" | "sec_argon2_state_advance"
17327            | "sec_chacha20_quarterround" | "sec_aes_round_step"
17328            | "sec_aes_keyschedule_step" | "sec_des_round_step"
17329            | "sec_blowfish_round_step" | "sec_serpent_round_step"
17330            | "sec_twofish_round_step"
17331            // ── calendrical algorithms ──────────────────────────
17332            | "fixed_from_gregorian" | "gregorian_from_fixed"
17333            | "fixed_from_julian" | "julian_from_fixed"
17334            | "iso_week_date" | "hebrew_leap_year"
17335            | "hebrew_year_length" | "fixed_from_hebrew"
17336            | "islamic_leap_year" | "fixed_from_islamic"
17337            | "persian_arithmetic_leap" | "fixed_from_persian"
17338            | "coptic_from_fixed" | "ethiopic_from_fixed"
17339            | "french_revolutionary_leap" | "fixed_from_french"
17340            | "chinese_year_zodiac" | "chinese_lunation_winter"
17341            | "hindu_solar_year" | "hindu_lunisolar_month"
17342            | "maya_long_count_from_fixed" | "mayan_haab_from_fixed"
17343            | "mayan_tzolkin_from_fixed" | "badi_year_from_fixed"
17344            | "bahai_from_fixed" | "easter_gregorian_year"
17345            | "easter_orthodox_year" | "easter_julian_year"
17346            | "day_of_week_zeller" | "iso_day_number"
17347            | "weekday_name_short" | "leap_year_gregorian"
17348
17349            // ── R / SciPy distributions and tests ───────────────
17350            | "dnorm" | "dt" | "df_dist" | "dchisq"
17351            | "glm" | "aov" | "shapiro_wilk" | "anderson_darling"
17352            | "kolmogorov_smirnov" | "spearmanr" | "kendalltau" | "pearsonr"
17353            | "mannwhitneyu" | "wilcoxon" | "kruskal_h"
17354
17355            // ── APL/J/K array primitives ────────────────────────
17356            | "iota_n" | "reduce_axis" | "scan_axis" | "fold_axis"
17357            | "rotate_axis" | "transpose_axis" | "reshape_dim"
17358            | "encode_base" | "decode_base" | "nub_list" | "nub_count"
17359            | "membership_idx" | "deal_n_k" | "roll_n"
17360            | "permute_idx" | "invert_perm"
17361
17362            // ── astronomy / astrometry ──────────────────────────
17363            | "julian_day" | "jd_to_calendar" | "tt_to_tdb"
17364            | "ra_dec_to_alt_az" | "alt_az_to_ra_dec"
17365            | "precession_iau2006" | "nutation_iau2000a"
17366            | "aberration_annual" | "proper_motion_apply"
17367            | "parallax_correction" | "sun_position_low" | "sun_distance_au"
17368            | "moon_position_low" | "moon_phase_age" | "lunation_index"
17369            | "eclipse_magnitude" | "saros_cycle" | "metonic_cycle"
17370            | "orbit_kepler3" | "orbital_period_au" | "orbit_eccentric_anomaly"
17371            | "escape_velocity_body" | "hill_sphere_radius" | "tisserand_param"
17372            | "tle_mean_motion" | "sgp4_propagate_step" | "airy_disk_radius"
17373            | "rayleigh_criterion" | "strehl_ratio" | "au_to_km"
17374
17375            // ── sports analytics — ratings & sabermetric ────────
17376            | "elo_expected" | "elo_update" | "glicko_rating"
17377            | "trueskill_update" | "trueskill_match_quality"
17378            | "pythagorean_expectation" | "war_above_replacement"
17379            | "woba_weight" | "wrc_plus" | "ops_plus" | "era_plus"
17380            | "fip" | "xfip" | "siera" | "babip" | "wpa"
17381            | "win_probability" | "leverage_index" | "clutch_score"
17382            | "shooting_pct" | "save_pct" | "corsi_for" | "fenwick_for"
17383            | "goals_above_avg" | "tackle_efficiency" | "yards_per_attempt"
17384            | "qbr_metric" | "epa_per_play"
17385
17386            // ── Excel/Sheets + bond/loan financial ──────────────
17387            | "vlookup" | "hlookup" | "xlookup" | "index_match"
17388            | "indirect" | "choose" | "offset"
17389            | "sumif" | "countif" | "averageif"
17390            | "sumifs" | "countifs" | "averageifs"
17391            | "sumproduct" | "rank_eq" | "rank_avg" | "percentrank"
17392            | "quartile_inc" | "quartile_exc"
17393            | "xnpv" | "ppmt" | "ipmt" | "rate"
17394            | "macauley_duration" | "convexity" | "yield_to_maturity"
17395            | "accrued_interest" | "clean_price" | "dirty_price"
17396            | "coupon_count" | "skill_score" | "reliability_diagram"
17397            | "taylor_diagram_score"
17398
17399            // ── GIS — geohash, H3, S2, UTM, projections ─────────
17400            | "geohash_neighbors" | "h3_index" | "h3_geo_to_h3"
17401            | "h3_h3_to_geo" | "h3_k_ring" | "h3_neighbor" | "h3_resolution"
17402            | "s2_cell_id" | "s2_cell_at_lat_lng" | "s2_cell_neighbors"
17403            | "utm_from_lat_lng" | "utm_to_lat_lng"
17404            | "mgrs_encode" | "mgrs_decode"
17405            | "lat_lng_to_xy_mercator" | "lat_lng_to_xy_lambert"
17406            | "haversine_dist" | "vincenty_dist" | "andoyer_dist"
17407            | "rhumb_line_bearing"
17408            | "destination_point" | "tile_xyz_to_lat_lng" | "lat_lng_to_tile_xyz"
17409            | "polygon_winding_order" | "point_in_polygon_ray"
17410            | "point_in_polygon_winding" | "segment_intersection"
17411            | "segment_distance_point" | "convex_hull_chan"
17412
17413            // ── robotics & control ──────────────────────────────
17414            | "pid_anti_windup" | "pid_ziegler_nichols"
17415            | "smith_predictor_step" | "lqr_gain_continuous"
17416            | "lqr_gain_discrete" | "lqg_step" | "h_infinity_norm"
17417            | "bode_gain_margin" | "bode_phase_margin"
17418            | "nyquist_encirclement" | "nichols_chart_step"
17419            | "servo_position_velocity" | "servo_torque_step"
17420            | "imu_madgwick_step" | "imu_mahony_step" | "quaternion_from_imu"
17421            | "denavit_hartenberg_h" | "forward_kinematics_dh"
17422            | "inverse_kinematics_2link" | "jacobian_2dof"
17423            | "manipulability_yoshikawa" | "singularity_check_2link"
17424            | "path_dubins_lsl" | "path_dubins_rsr" | "path_reeds_shepp"
17425            | "rrt_extend" | "rrt_star_rewire" | "prm_node_connect"
17426
17427            // ── actuarial science ───────────────────────────────
17428            | "life_expectancy_e0" | "force_of_mortality" | "select_ultimate"
17429            | "annuity_due_an" | "annuity_immediate_an"
17430            | "term_life_a_n_t" | "whole_life_a"
17431            | "endowment_pure_e" | "endowment_combined_a"
17432            | "premium_net" | "level_premium"
17433            | "reserve_prospective" | "reserve_retrospective"
17434            | "gross_premium_load" | "experience_factor"
17435            | "mortality_table_q" | "select_period_step"
17436            | "multi_decrement_q" | "multi_state_pij"
17437            | "credibility_buhlmann" | "loss_severity_lognormal"
17438            | "loss_frequency_poisson" | "ruin_probability_lundberg"
17439            | "cramer_lundberg_step" | "bornhuetter_ferguson"
17440            | "chain_ladder_step" | "ibnr_estimate" | "run_off_triangle_step"
17441
17442            // ── epidemiology / public health ────────────────────
17443            | "r_naught_basic" | "r_effective_t" | "doubling_time_growth"
17444            | "sirs_step" | "seirs_step" | "susceptible_to_infected"
17445            | "attack_rate" | "vaccination_coverage_required"
17446            | "cfr_case_fatality" | "ifr_infection_fatality"
17447            | "dalys_disability_weight" | "qaly_lifetime" | "ylll_pml"
17448            | "rt_serial_interval" | "generation_time_step"
17449            | "gini_inequality_health" | "standardized_mortality_smr"
17450            | "indirect_age_adjusted" | "direct_age_adjusted"
17451            | "odds_ratio_2x2" | "risk_ratio_2x2" | "number_needed_to_treat"
17452            | "attributable_fraction_pop" | "preventive_fraction"
17453            | "contact_tracing_eff" | "cluster_attack_rate"
17454            | "transmission_pair_index"
17455
17456            // ── archive/encoding format primitives ──────────────
17457            | "tar_header_checksum" | "tar_pad_512" | "tar_member_record"
17458            | "zip_local_header" | "zip_central_dir" | "zip_eocd"
17459            | "gzip_member_step" | "gzip_crc32_init" | "gzip_isize"
17460            | "deflate_dynamic_huffman" | "deflate_static_block"
17461            | "lz4_block_step" | "lz4_match_offset"
17462            | "zstd_frame_header" | "brotli_huffman_table"
17463            | "brotli_meta_block" | "lzma_range_step"
17464            | "quoted_printable_encode" | "uuencode_step"
17465            | "modhex_encode" | "percent_encode_full"
17466            | "punycode_encode" | "idn_to_ascii" | "idn_to_unicode"
17467            | "msgpack_pack_int" | "msgpack_pack_str"
17468            | "cbor_encode_uint" | "cbor_encode_str"
17469
17470            // ── chemistry & biochemistry ────────────────────────
17471            | "molecular_weight_compound" | "molarity_dilution"
17472            | "gas_constant_value" | "eyring_rate" | "van_t_hoff_kp"
17473            | "henderson_buffer" | "titration_ph_endpoint"
17474            | "isoelectric_point_protein" | "ka_to_pka" | "pkb_to_kb"
17475            | "amphoteric_check" | "oxidation_number"
17476            | "half_reaction_balance" | "redox_potential_cell"
17477            | "electrolysis_mass" | "spectrophotometer_beer_lambert"
17478            | "epsilon_extinction" | "transmittance_to_a"
17479            | "crystal_field_ligand" | "jahn_teller_check"
17480            | "vsepr_geometry" | "lewis_dot_count"
17481            | "formal_charge" | "resonance_count"
17482            | "ramachandran_phi_psi" | "rg_radius_of_gyration"
17483            | "spectroscopic_factor" | "avogadro_constant"
17484
17485            // ── music theory ────────────────────────────────────
17486            | "cents_between_freqs" | "note_name_from_midi"
17487            | "interval_quality_size" | "scale_pitches_major"
17488            | "scale_pitches_minor" | "mode_pitches_dorian"
17489            | "mode_pitches_phrygian" | "mode_pitches_lydian"
17490            | "chord_root_inversion" | "chord_quality_classify"
17491            | "chord_voicing_close" | "key_signature_sharps"
17492            | "key_signature_flats" | "tempo_to_ms" | "beat_to_seconds"
17493            | "time_sig_subdivision" | "equal_tempered_freq"
17494            | "just_intonation_freq" | "pythagorean_freq"
17495            | "mean_tone_freq" | "werckmeister_iii" | "kirnberger_iii"
17496            | "dynamics_db_level" | "harmonics_partial"
17497
17498            // ── geology, seismology, mineralogy ─────────────────
17499            | "moment_magnitude_mw" | "richter_local_ml"
17500            | "surface_wave_ms" | "body_wave_mb"
17501            | "gutenberg_richter_b" | "omori_aftershock"
17502            | "pga_attenuation" | "arias_intensity" | "shake_map_pga"
17503            | "liquefaction_potential_index" | "spt_n_correction"
17504            | "mineral_mohs_hardness" | "streak_color_index"
17505            | "specific_gravity_water" | "feldspar_classify"
17506            | "silicate_classify" | "igneous_qapf"
17507            | "metamorphic_grade" | "crustal_density_depth"
17508            | "pwave_velocity_depth" | "swave_velocity_depth"
17509            | "gradient_geothermal" | "heat_flow_radiogenic"
17510
17511            // ── BLAS / LAPACK ───────────────────────────────────
17512            | "dgemm" | "sgemm" | "zgemm" | "cgemm"
17513            | "dgemv" | "sgemv" | "dtrsm" | "strsm"
17514            | "dgesv" | "dgetrf" | "dgeqrf" | "dgesvd"
17515            | "dsyevd" | "dpotrf" | "daxpy" | "ddot"
17516            | "dnrm2" | "dscal" | "dasum" | "idamax"
17517            | "dsyrk" | "dgerqf" | "dorgqr" | "dorglq"
17518            | "drot" | "drotg" | "dpbsv" | "dgbsv"
17519            | "dtbsv" | "dtrsv" | "ddrot" | "dgemm3m"
17520            | "dgels" | "dgelsd"
17521
17522            // ── logic, proof, SAT/SMT, type theory ──────────────
17523            | "cnf_unit_propagate" | "cnf_pure_literal_elim"
17524            | "cnf_dpll_branch" | "dpll_clause_learning"
17525            | "two_watched_literals" | "walksat_step"
17526            | "resolution_step" | "subsumption_check"
17527            | "tableau_branch_close" | "sequent_left_intro"
17528            | "sequent_right_intro" | "nbe_normalize"
17529            | "church_numeral_n" | "encode_pair" | "encode_succ"
17530            | "simply_typed_check" | "hindley_milner_step"
17531            | "unification_robinson" | "bdd_apply" | "bdd_restrict"
17532            | "bdd_quantify" | "aig_simplify_step"
17533            | "smt_qf_lia_solve_step" | "smt_qf_uf_combine"
17534            | "model_checking_ctl" | "model_checking_ltl"
17535            | "bisimulation_step" | "coq_tactic_apply"
17536            | "coq_unify_term" | "refl_check" | "sym_check" | "trans_check"
17537
17538            // ── compilers / parsing ─────────────────────────────
17539            | "nfa_to_dfa" | "subset_construction"
17540            | "dfa_minimize_hopcroft" | "regex_to_nfa_thompson"
17541            | "glushkov_construction" | "brzozowski_derivative"
17542            | "ll1_first_set" | "ll1_follow_set" | "ll1_predict_table"
17543            | "lr0_items_step" | "lalr_lookahead_compute"
17544            | "lr1_canonical_collection"
17545            | "earley_scan" | "earley_predict" | "earley_complete"
17546            | "packrat_parse_step" | "ascent_parser_step"
17547            | "pratt_parse_step" | "shunting_yard_step"
17548            | "regex_compile_thompson" | "regex_match_dfa"
17549            | "lex_keyword_classify"
17550            | "peg_seq" | "peg_choice" | "peg_repeat" | "peg_lookahead"
17551            | "dfa_simulate_step" | "bytecode_disasm_step"
17552            | "ssa_phi_insert" | "dom_tree_idom" | "dominance_frontier"
17553
17554            // ── computational linguistics ───────────────────────
17555            | "porter_stem_step" | "snowball_stem_english"
17556            | "snowball_stem_french" | "lemmatize_wordnet"
17557            | "lemmatize_lemmy" | "stem_lancaster"
17558            | "soundex_phonetic" | "metaphone_phonetic"
17559            | "caverphone_2" | "nysiis_phonetic"
17560            | "match_rating_codex" | "daitch_mokotoff"
17561            | "viterbi_pos_tag" | "forward_backward_pos"
17562            | "crf_log_likelihood" | "bigram_perplexity"
17563            | "trigram_perplexity" | "ner_bilou_decode"
17564            | "constituency_cyk" | "dependency_parse_eisner"
17565            | "transition_arc_eager" | "transition_arc_standard"
17566            | "word_alignment_ibm1" | "word_alignment_ibm2"
17567            | "lexicalized_parse" | "coreference_singleton"
17568            | "anaphora_distance" | "head_finding_collins"
17569            | "tree_kernel_collins"
17570
17571            // ── Postgres SQL strings, JSON, aggregates ─────────
17572            | "btrim" | "translate" | "ascii"
17573            | "regexp_split" | "regexp_matches" | "regexp_replace"
17574            | "json_build_object" | "jsonb_set"
17575            | "json_array_length" | "json_extract_path"
17576            | "json_strip_nulls" | "jsonb_pretty"
17577            | "jsonb_path_query" | "json_each"
17578            | "jsonb_array_length" | "jsonb_object_keys"
17579            | "jsonb_typeof" | "array_to_jsonb"
17580            | "ts_match" | "ts_rank" | "ts_headline"
17581            | "substring_similarity" | "levenshtein_dist"
17582            | "word_similarity" | "strict_word_similarity"
17583            | "hstore_to_array" | "array_to_hstore"
17584            | "string_agg" | "array_agg"
17585            | "corr_agg" | "covar_pop" | "covar_samp"
17586            | "regr_slope" | "regr_intercept" | "regr_r2"
17587            | "percentile_cont" | "percentile_disc" | "mode_agg"
17588            | "array_to_string" | "array_position" | "array_positions"
17589            | "array_remove" | "array_replace"
17590            | "xmlforest" | "xmlagg"
17591
17592            // ── Redis-flavour primitives ────────────────────────
17593            | "zadd" | "zrem" | "zrangebyscore"
17594            | "zrank" | "zrevrank" | "zincrby"
17595            | "zcard" | "zcount" | "zlexcount"
17596            | "lpush" | "rpush" | "lrange" | "lrem"
17597            | "hset" | "hget" | "hgetall" | "hlen"
17598            | "hkeys" | "hvals" | "hmset" | "hincrby"
17599            | "sadd" | "srem" | "smembers"
17600            | "sinter" | "sunion" | "sdiff"
17601            | "scard" | "sismember" | "spop"
17602            | "setex" | "setnx" | "expire"
17603            | "ttl" | "pttl" | "persist"
17604            | "incr" | "decr" | "incrby" | "decrby"
17605            | "getset" | "mset" | "mget" | "renamenx"
17606            | "dbsize" | "type_redis" | "exists_key"
17607            | "strlen" | "getrange" | "setrange" | "append_redis"
17608            | "bitcount" | "bitop" | "bitpos"
17609            | "pfadd" | "pfcount"
17610            | "geoadd" | "geodist" | "geohash"
17611            | "xadd" | "xlen" | "xrange"
17612            | "object_encoding" | "debug_object" | "cluster_slots"
17613
17614            // ── NumPy + scipy.special ──────────────────────────
17615            | "argpartition" | "bincount" | "nonzero_count"
17616            | "flatnonzero" | "searchsorted" | "digitize"
17617            | "histogram_bin_edges" | "unique_count"
17618            | "polyfit_rmse"
17619            | "ellipk" | "ellipe"
17620            | "hyp1f1" | "hyp2f1" | "mathieu_b"
17621            | "spherical_jn" | "spherical_yn"
17622            | "jv" | "yn" | "iv" | "kv"
17623            | "airyai" | "airybi"
17624            | "polygamma" | "trigamma" | "loggamma"
17625            | "factorial2" | "factorialk"
17626            | "owens_t" | "marcum_q" | "voigt_profile"
17627            | "chebyt" | "chebyu" | "sph_harm"
17628            | "wofz" | "erfcx" | "erfi" | "dawsn"
17629            | "interp1d"
17630            | "convolve_full" | "convolve_valid" | "correlate_full"
17631            | "kron_product"
17632            | "simpson_rule" | "romberg_quad" | "fixed_quad"
17633            | "ode45_step" | "ode_lsoda" | "solve_ivp_step"
17634            | "root_brentq" | "root_newton" | "root_secant"
17635            | "fmin_powell" | "fmin_cobyla"
17636
17637            // ── economics + game theory ─────────────────────────
17638            | "cobb_douglas" | "ces_production"
17639            | "leontief_input" | "leontief_output"
17640            | "slutsky_decompose"
17641            | "marshallian_demand" | "hicksian_demand"
17642            | "expenditure_function" | "indirect_utility"
17643            | "gale_shapley_step" | "deferred_acceptance"
17644            | "top_trading_cycle" | "vcg_payment" | "myerson_optimal"
17645            | "gini_market" | "hhi_concentration"
17646            | "cournot_eq" | "stackelberg_eq" | "bertrand_eq"
17647            | "monopoly_lerner"
17648            | "consumer_surplus" | "producer_surplus"
17649            | "deadweight_loss" | "tax_incidence"
17650            | "pareto_efficiency" | "edgeworth_box_alloc"
17651            | "social_welfare_utilitarian"
17652            | "social_welfare_rawls" | "social_welfare_nash"
17653            | "arrow_independence"
17654            | "vickrey_auction" | "first_price_seal"
17655            | "english_auction" | "dutch_auction"
17656            | "core_coalition" | "stable_matching_count"
17657            | "gale_optimal" | "pareto_dominance"
17658            | "lerner_index"
17659            | "price_elasticity" | "supply_elasticity"
17660            | "income_elasticity" | "engel_curve" | "cross_elasticity"
17661            | "diff_in_diff" | "did_estimator" | "rdd_estimate"
17662            // ── SciPy.signal — DSP filters, windows, transforms ──
17663            | "hann_w" | "hamming_w" | "blackman_w" | "barthann_w"
17664            | "nuttall_w" | "flattop_w" | "parzen_window" | "tukey_w"
17665            | "taylor_window" | "dpss_window" | "kaiserord_step"
17666            | "butter_lp_re" | "butter_hp_mag"
17667            | "cheby1_lp" | "cheby2_lp" | "ellip_lp" | "bessel_lp"
17668            | "notch_filter"
17669            | "sosfilt_step" | "lfilter_zi_init" | "filtfilt_pad"
17670            | "freqz_eval" | "freqs_eval" | "group_delay_eval"
17671            | "impulse_response_n"
17672            | "tf2zpk_step" | "zpk2tf_step" | "tf2sos_step"
17673            | "zpk2sos_step" | "sos2tf_step"
17674            | "bilinear_xform" | "bilinear_zpk_xform"
17675            | "firwin_lowpass" | "firwin_highpass"
17676            | "firwin_bandpass" | "firwin_bandstop"
17677            | "firwin2_freq" | "remez_design"
17678            | "stft_step" | "istft_step"
17679            | "cwt_morlet" | "ricker_wavelet" | "mexican_hat_wavelet"
17680            | "coherence_xy" | "csd_xy" | "welch_psd_avg"
17681            | "periodogram_basic" | "lombscargle_freq"
17682            | "hilbert_signal" | "envelope_amplitude"
17683            | "deconvolve_step" | "fftconvolve_step" | "oaconvolve_step"
17684            | "upfirdn_step" | "resample_poly_step" | "decimate_step"
17685            | "savgol_coef" | "detrend_linear"
17686            | "wiener_filter" | "medfilt_1d" | "peak_widths_at"
17687            // ── NetworkX graph algorithms ───────────────────────
17688            | "dijkstra_relax" | "bellman_ford_relax"
17689            | "floyd_warshall_step" | "johnson_reweight"
17690            | "astar_search" | "bidirectional_dijkstra"
17691            | "yen_k_shortest" | "ida_star"
17692            | "bfs_count" | "dfs_postorder_done" | "topo_kahn_step"
17693            | "tarjan_scc_step" | "kosaraju_step"
17694            | "kruskal_step" | "prim_step" | "boruvka_step"
17695            | "reverse_delete_step"
17696            | "ford_fulkerson_step" | "edmonds_karp_bfs"
17697            | "dinic_step" | "push_relabel_relabel"
17698            | "stoer_wagner_step" | "karger_step"
17699            | "pagerank_iter" | "hits_authority" | "hits_hub"
17700            | "personalized_pagerank"
17701            | "centrality_degree" | "centrality_closeness"
17702            | "centrality_betweenness" | "centrality_eigenvector"
17703            | "centrality_katz" | "harmonic_centrality" | "load_centrality"
17704            | "clustering_coefficient" | "triangles_count" | "transitivity"
17705            | "modularity_score" | "louvain_gain"
17706            | "label_propagation" | "girvan_newman"
17707            | "articulation_point" | "bridge_edge"
17708            | "edge_connectivity" | "vertex_connectivity"
17709            | "biconnected_components"
17710            | "gx_diameter" | "gx_radius" | "gx_eccentricity"
17711            | "warshall_step"
17712            | "tsp_held_karp" | "tsp_nn_step" | "tsp_christofides"
17713            | "graph_coloring_greedy" | "welsh_powell"
17714            | "vf2_consistent" | "subgraph_isomorphism"
17715            | "hungarian_step" | "hopcroft_karp_step"
17716            | "bron_kerbosch"
17717            | "min_vertex_cover" | "max_independent_set"
17718            | "dominating_set_greedy" | "hamiltonian_path"
17719            | "min_steiner_tree" | "k_shortest_spanning"
17720            | "random_walk_hitting" | "simrank"
17721            // ── Pandas DataFrame ops ────────────────────────────
17722            | "df_groupby" | "df_aggregate" | "df_apply"
17723            | "df_transform" | "df_pivot" | "df_pivot_table"
17724            | "df_melt" | "df_stack" | "df_unstack"
17725            | "df_explode" | "df_get_dummies" | "df_crosstab"
17726            | "df_merge" | "df_join" | "df_concat"
17727            | "df_resample" | "df_rolling" | "df_expanding"
17728            | "df_ewm" | "df_shift" | "df_diff"
17729            | "df_pct_change" | "df_corr" | "df_cov"
17730            | "df_corrwith" | "df_describe" | "df_kurtosis"
17731            | "df_skew" | "df_sem" | "df_mad"
17732            | "df_dropna" | "df_fillna" | "df_interpolate"
17733            | "df_replace" | "df_isnull" | "df_notnull"
17734            | "df_sort_values" | "df_rank" | "df_quantile"
17735            | "df_value_counts" | "df_sample" | "df_nlargest"
17736            | "df_nsmallest" | "df_idxmax" | "df_idxmin"
17737            | "df_clip" | "df_round" | "df_to_datetime"
17738            | "df_to_timedelta" | "df_to_numeric" | "df_eval"
17739            | "df_query" | "df_filter" | "df_drop_duplicates"
17740            | "df_duplicated" | "df_set_index" | "df_reset_index"
17741            // ── PIL/OpenCV image processing ─────────────────────
17742            | "image_resize" | "image_grayscale" | "image_threshold"
17743            | "image_blur_gaussian" | "image_blur_box" | "image_sharpen"
17744            | "image_edge_canny" | "image_edge_sobel" | "image_edge_laplacian"
17745            | "image_dilate" | "image_erode" | "image_morphology_open"
17746            | "image_morphology_close" | "image_histogram" | "image_equalize"
17747            | "image_clahe" | "image_contrast" | "image_brightness"
17748            | "image_gamma" | "image_invert" | "image_sepia"
17749            | "image_posterize" | "image_solarize" | "convolve_2d"
17750            | "filter_median" | "filter_bilateral" | "filter_nlmeans"
17751            | "gabor_filter" | "hog_features" | "harris_corners"
17752            | "shi_tomasi_corners" | "sift_keypoints" | "orb_keypoints"
17753            | "surf_keypoints" | "template_match" | "face_detect_haar"
17754            | "watershed_segment" | "slic_superpixels" | "felzenszwalb_segment"
17755            | "graph_cut_segment" | "hough_lines" | "hough_circles"
17756            | "ransac_homography" | "optical_flow_lk" | "optical_flow_farneback"
17757            | "corner_subpix" | "image_rotate" | "image_flip_h"
17758            | "image_flip_v" | "image_emboss" | "image_motion_blur"
17759            // ── statsmodels ─
17760            | "arima_fit" | "arima_forecast" | "arma_order_select"
17761            | "sarimax_fit" | "garch_fit" | "ewma_smooth"
17762            | "holt_winters_additive" | "holt_winters_multiplicative" | "kalman_filter_step"
17763            | "kalman_smoother_step" | "var_fit" | "vecm_fit"
17764            | "johansen_test" | "phillips_perron" | "adfuller"
17765            | "kpss_test" | "breusch_godfrey" | "ljung_box_q"
17766            | "durbin_watson_d" | "granger_causality" | "cointegration_eg"
17767            | "seasonal_decompose" | "stl_decompose" | "acf_basis"
17768            | "pacf_basis" | "moving_average_filter" | "exp_smooth_simple"
17769            | "exp_smooth_double" | "markov_switching_ar" | "markov_switching_mr"
17770            | "arch_lm" | "state_space_kalman" | "ucm_unobserved_components"
17771            | "spectral_density_estimate" | "bayesian_step" | "pivoted_cholesky_var"
17772            // ── sklearn ─
17773            | "sk_logistic_predict" | "sk_logistic_fit" | "sk_random_forest_fit"
17774            | "sk_gbt_fit" | "sk_xgb_fit" | "sk_lightgbm_fit"
17775            | "sk_svm_fit" | "sk_kmeans_fit" | "sk_dbscan_fit"
17776            | "sk_agglomerative_fit" | "sk_pca_fit" | "sk_tsne_fit"
17777            | "sk_umap_fit" | "sk_isolation_forest_fit" | "sk_lof_fit"
17778            | "sk_kfold_split" | "sk_stratified_kfold" | "sk_cross_val_score"
17779            | "sk_grid_search" | "sk_random_search" | "sk_bayes_search"
17780            | "sk_pipeline_fit" | "sk_standard_scaler" | "sk_min_max_scaler"
17781            | "sk_robust_scaler" | "sk_quantile_transform" | "sk_power_transform"
17782            | "sk_one_hot" | "sk_ordinal_encode" | "sk_label_encode"
17783            | "sk_tfidf" | "sk_count_vectorize" | "sk_silhouette"
17784            | "sk_calinski_harabasz" | "sk_davies_bouldin" | "sk_adjusted_rand"
17785            | "sk_mutual_info" | "sk_lda_topic" | "sk_nmf_topic"
17786            | "sk_word2vec_train" | "sk_doc2vec_train" | "sk_naive_bayes_predict"
17787            | "sk_knn_predict" | "sk_decision_tree_split"
17788            // ── quantum ─
17789            | "qubit_x" | "qubit_y" | "qubit_z"
17790            | "qubit_h" | "qubit_s" | "qubit_t"
17791            | "qubit_rx" | "qubit_ry" | "qubit_rz"
17792            | "qubit_u3" | "qubit_u2" | "qubit_u1"
17793            | "qubit_phase" | "qubit_cnot" | "qubit_cz"
17794            | "qubit_swap" | "qubit_ccx" | "qubit_measure"
17795            | "qubit_reset" | "bell_state" | "ghz_state"
17796            | "w_state" | "qft" | "inverse_qft"
17797            | "grover_iter" | "shor_period" | "vqe_step"
17798            | "qaoa_step" | "qpe_iteration" | "pauli_string_expect"
17799            | "circuit_depth" | "circuit_width" | "gate_decompose"
17800            | "ancilla_alloc" | "bloch_sphere_x" | "bloch_sphere_z"
17801            | "density_matrix_purity_q" | "entanglement_entropy" | "quantum_teleportation"
17802            | "superdense_coding" | "noise_model_depolarize"
17803            // ── b81-misc-utility ─
17804            | "mirr_excel" | "accrint" | "cumipmt"
17805            | "cumprinc" | "dollarde" | "dollarfr"
17806            | "received" | "yieldmat" | "yielddisc"
17807            | "duration_macaulay" | "mduration" | "odddyield"
17808            | "disc_excel" | "effect" | "nominal"
17809            | "intrate" | "price_disc" | "cityhash64"
17810            | "farmhash_64" | "metro_hash_64" | "spookyhash_128"
17811            | "t1ha" | "highway_hash" | "fnv0_32"
17812            | "lose_lose"
17813            | "oat_hash" | "lz4_encode_block" | "snappy_encode"
17814            | "zstd_encode_step" | "brotli_encode_meta" | "lzma_encode_step"
17815            | "bz2_encode_step" | "lzo_encode_step" | "deflate_encode_huffman"
17816            | "lzw_encode" | "gzip_encode_step" | "uri_template_expand"
17817            | "uri_resolve" | "uri_normalize" | "percent_decode_url"
17818            | "url_encode_form" | "url_decode_form" | "punycode_decode_step"
17819            | "idn_normalize" | "url_origin" | "etag_validate"
17820            | "cache_control_parse" | "vary_match" | "content_negotiate"
17821            | "accept_lang_pick" | "range_header_parse" | "if_match_check"
17822            | "if_none_match_check" | "digest_auth_quote" | "www_auth_parse"
17823            // ── b82-misc-utility ─
17824            | "iso8601_duration_parse" | "iso8601_duration_to_seconds" | "rrule_next_occurrence"
17825            | "cron_next_fire" | "date_round_iso" | "week_number_iso"
17826            | "fiscal_year_us" | "age_at_date" | "easter_western"
17827            | "easter_orthodox_year_2" | "chinese_new_year" | "solstice_winter"
17828            | "equinox_spring" | "rgb_to_oklab" | "oklab_to_rgb"
17829            | "rgb_to_cmyk" | "cmyk_to_rgb" | "rgb_to_xyz"
17830            | "xyz_to_rgb" | "rgb_to_yuv" | "yuv_to_rgb"
17831            | "luminance_relative" | "contrast_ratio" | "wcag_pass"
17832            | "color_temperature_kelvin" | "delta_e76" | "delta_e94"
17833            | "delta_e2000" | "color_blend_alpha" | "isbn10_check"
17834            | "isbn13_check" | "ean13_check" | "upc_check"
17835            | "eth_addr_check" | "btc_addr_check" | "ssn_check"
17836            | "vin_check" | "imei_check" | "iban_check"
17837            | "cusip_check" | "kde_silverman_bw" | "kde_scott_bw"
17838            | "kde_bandwidth_lscv" | "kde_epanechnikov" | "kde_gaussian_2d"
17839            | "kde_uniform" | "kde_triangular" | "kde_biweight"
17840            | "kde_triweight" | "kde_cosine" | "kde_logistic_kernel"
17841            // ── number theory (extended) ──────────────────────────────────
17842            | "mod_exp" | "modexp" | "powmod"
17843            | "mod_inv" | "modinv" | "chinese_remainder" | "crt"
17844            | "miller_rabin" | "millerrabin" | "is_probable_prime"
17845            // ── combinatorics (extended) ──────────────────────────────────
17846            | "derangements" | "stirling2" | "stirling_second"
17847            | "bernoulli_number" | "bernoulli" | "harmonic_number" | "harmonic"
17848            // ── physics (new) ─────────────────────────────────────────────
17849            | "drag_force" | "fdrag" | "ideal_gas" | "pv_nrt"
17850            // ── financial greeks & risk ───────────────────────────────────
17851            | "bs_delta" | "bsdelta" | "option_delta"
17852            | "bs_gamma" | "bsgamma" | "option_gamma"
17853            | "bs_vega" | "bsvega" | "option_vega"
17854            | "bs_theta" | "bstheta" | "option_theta"
17855            | "bs_rho" | "bsrho" | "option_rho"
17856            | "bond_duration" | "mac_duration"
17857            // ── DSP extensions ────────────────────────────────────────────
17858            | "dct" | "idct" | "goertzel" | "chirp" | "chirp_signal"
17859            // ── encoding extensions ───────────────────────────────────────
17860            | "base85_encode" | "b85e" | "ascii85_encode" | "a85e"
17861            | "base85_decode" | "b85d" | "ascii85_decode" | "a85d"
17862            // ── R base: distributions ─────────────────────────────────────
17863            | "pnorm" | "pbinom" | "dbinom" | "ppois"
17864            | "punif" | "pexp" | "pweibull" | "plnorm" | "pcauchy"
17865            // ── R base: matrix ops ────────────────────────────────────────
17866            | "rbind" | "cbind"
17867            | "row_sums" | "rowSums" | "col_sums" | "colSums"
17868            | "row_means" | "rowMeans" | "col_means" | "colMeans"
17869            | "outer" | "crossprod" | "tcrossprod"
17870            | "nrow" | "ncol" | "prop_table" | "proptable"
17871            // ── R base: vector ops ────────────────────────────────────────
17872            | "cummax" | "cummin" | "scale_vec" | "scale"
17873            | "which_fn" | "tabulate"
17874            | "duplicated" | "duped" | "rev_vec"
17875            | "seq_fn" | "rep_fn" | "rep"
17876            | "cut_bins" | "cut" | "find_interval" | "findInterval"
17877            | "ecdf_fn" | "ecdf" | "density_est" | "density"
17878            | "embed_ts" | "embed"
17879            // ── R base: stats tests ───────────────────────────────────────
17880            | "shapiro_test" | "shapiro" | "ks_test" | "ks"
17881            | "wilcox_test" | "wilcox" | "mann_whitney"
17882            | "prop_test" | "proptest" | "binom_test" | "binomtest"
17883            // ── R base: apply / functional ────────────────────────────────
17884            | "sapply" | "tapply" | "do_call" | "docall"
17885            // ── R base: ML / clustering ───────────────────────────────────
17886            | "kmeans" | "prcomp" | "pca"
17887            // ── R base: random generators ─────────────────────────────────
17888
17889
17890
17891            // ── R base: quantile functions ────────────────────────────────
17892
17893            // ── R base: additional CDFs ───────────────────────────────────
17894            | "pgamma" | "pbeta" | "pchisq" | "pt_cdf" | "pt" | "pf_cdf" | "pf"
17895            // ── R base: additional PMFs ───────────────────────────────────
17896            | "dgeom" | "dunif" | "dnbinom" | "dhyper"
17897            // ── R base: smoothing / interpolation ─────────────────────────
17898            | "lowess" | "loess" | "approx_fn" | "approx"
17899            // ── R base: linear models ─────────────────────────────────────
17900            | "lm_fit" | "lm"
17901            // ── R base: remaining quantiles ───────────────────────────────
17902 | "qt_fn" | "qf_fn"
17903
17904            // ── R base: time series ───────────────────────────────────────
17905            | "acf_fn" | "acf" | "pacf_fn" | "pacf"
17906            | "diff_lag" | "diff_ts" | "ts_filter" | "filter_ts"
17907            // ── R base: regression diagnostics ────────────────────────────
17908            | "predict_lm" | "predict" | "confint_lm" | "confint"
17909            // ── R base: multivariate stats ────────────────────────────────
17910            | "cor_matrix" | "cor_mat" | "cov_matrix" | "cov_mat"
17911            | "mahalanobis" | "mahal" | "dist_matrix" | "dist_mat"
17912            | "hclust" | "cutree" | "weighted_var" | "wvar" | "cov2cor"
17913            // ── SVG plotting ──────────────────────────────────────────────
17914            | "scatter_svg" | "scatter_plot" | "line_svg" | "line_plot"
17915            | "plot_svg" | "hist_svg" | "histogram_svg"
17916            | "boxplot_svg" | "box_plot" | "bar_svg" | "barchart_svg"
17917            | "pie_svg" | "pie_chart" | "heatmap_svg" | "heatmap"
17918            | "donut_svg" | "donut" | "area_svg" | "area_chart"
17919            | "hbar_svg" | "hbar" | "radar_svg" | "radar" | "spider"
17920            | "candlestick_svg" | "candlestick" | "ohlc"
17921            | "violin_svg" | "violin" | "cor_heatmap" | "cor_matrix_svg"
17922            | "stacked_bar_svg" | "stacked_bar"
17923            | "wordcloud_svg" | "wordcloud" | "wcloud"
17924            | "treemap_svg" | "treemap"
17925            | "pvw"
17926            // ── Cyberpunk terminal art ────────────────────────────────
17927            | "cyber_city" | "cyber_grid" | "cyber_rain" | "matrix_rain"
17928            | "cyber_glitch" | "glitch_text" | "cyber_banner" | "neon_banner"
17929            | "cyber_circuit" | "cyber_skull" | "cyber_eye"
17930            // ── AI primitives (docs/AI_PRIMITIVES.md) ─────────────────
17931            | "ai" | "ai_agent" | "prompt" | "stream_prompt" | "stream_prompt_cb"
17932            | "tokens_of"
17933            | "ai_estimate" | "ai_cost" | "ai_history" | "ai_history_clear"
17934            | "ai_cache_clear" | "ai_cache_size"
17935            | "ai_mock_install" | "ai_mock_clear"
17936            | "ai_config_get" | "ai_config_set" | "ai_routing_get" | "ai_routing_set"
17937            | "ai_register_tool" | "ai_unregister_tool" | "ai_clear_tools" | "ai_tools_list"
17938            | "ai_filter" | "ai_map" | "ai_classify" | "ai_match" | "ai_sort" | "ai_dedupe"
17939            | "ai_extract" | "ai_summarize" | "ai_translate" | "ai_template"
17940            | "ai_session_new" | "ai_session_send" | "ai_session_history"
17941            | "ai_session_close" | "ai_session_reset"
17942            | "ai_session_export" | "ai_session_import"
17943            | "ai_memory_save" | "ai_memory_recall" | "ai_memory_forget"
17944            | "ai_memory_count" | "ai_memory_clear"
17945            | "ai_vision" | "ai_pdf" | "ai_grounded" | "ai_citations"
17946            | "ai_transcribe" | "ai_speak" | "ai_image" | "ai_image_edit" | "ai_image_variation"
17947            | "ai_models" | "ai_describe" | "ai_pricing" | "ai_dashboard"
17948            | "ai_moderate" | "ai_chunk" | "ai_warm" | "ai_compare"
17949            | "ai_last_thinking" | "ai_budget" | "ai_batch" | "ai_pmap"
17950            | "ai_file_upload" | "ai_file_list" | "ai_file_get" | "ai_file_delete"
17951            | "ai_file_anthropic_upload" | "ai_file_anthropic_list" | "ai_file_anthropic_delete"
17952            | "vec_cosine" | "vec_search" | "vec_topk"
17953            // ── AI tool specs ────────────────────────────────────────
17954            | "web_search_tool" | "fetch_url_tool" | "read_file_tool" | "run_code_tool"
17955            // ── MCP (Model Context Protocol) ─────────────────────────
17956            | "mcp_connect" | "mcp_close" | "mcp_tools" | "mcp_call"
17957            | "mcp_resource" | "mcp_resources" | "mcp_prompt" | "mcp_prompts"
17958            | "mcp_attach_to_ai" | "mcp_detach_from_ai" | "mcp_attached"
17959            | "mcp_server_start" | "mcp_serve_registered_tools"
17960            // ── PTY / expect (docs/expect-feature-idea.md) ────────────
17961            | "pty_spawn" | "pty_send" | "pty_read" | "pty_expect" | "pty_expect_table"
17962            | "pty_buffer" | "pty_alive" | "pty_eof" | "pty_close" | "pty_interact"
17963            | "pty_strip_ansi" | "pty_after_eof" | "pty_pending_events"
17964            // ── Stress / telemetry extensions ─────────────────────────
17965            | "stress_fp" | "stress_int" | "stress_cache" | "stress_branch"
17966            | "stress_sort" | "stress_alloc" | "stress_mmap" | "stress_disk"
17967            | "stress_iops" | "stress_net" | "stress_http" | "stress_dns"
17968            | "stress_fork" | "stress_thread" | "stress_aes" | "stress_compress"
17969            | "stress_regex" | "stress_json" | "stress_burst" | "stress_ramp"
17970            | "stress_oscillate" | "stress_all" | "stress_temp" | "stress_thermal_zones"
17971            | "stress_freq" | "stress_throttled" | "stress_load" | "stress_meminfo"
17972            | "stress_cores" | "stress_arm_kill_switch" | "stress_killed"
17973            | "stress_disarm_kill_switch"
17974            | "stress_metrics_record" | "stress_metrics_clear" | "stress_metrics_count"
17975            | "stress_metrics_export" | "stress_metrics_prometheus"
17976            | "stress_metrics_json" | "stress_metrics_csv" | "stress_metrics_watch"
17977            // ── Compliance / secrets ─────────────────────────────────
17978            | "audit_log" | "audit_log_path"
17979            | "secrets_encrypt" | "secrets_decrypt" | "secrets_random_key" | "secrets_kdf"
17980            // ── Web framework (docs/WEB_FRAMEWORK.md) ─────────────────
17981            | "web_route" | "web_resources" | "web_root" | "web_routes_table"
17982            | "web_application_config" | "web_boot_application"
17983            | "web_render" | "web_render_partial" | "web_redirect"
17984            | "web_json" | "web_text" | "web_csv" | "web_markdown"
17985            | "web_params" | "web_request" | "web_set_header" | "web_status"
17986            | "web_before_action" | "web_after_action"
17987            | "web_session" | "web_session_set" | "web_session_get" | "web_session_clear"
17988            | "web_signed" | "web_unsigned"
17989            | "web_cookies" | "web_set_cookie"
17990            | "web_flash" | "web_flash_set" | "web_flash_get"
17991            | "web_validate" | "web_permit"
17992            | "web_password_hash" | "web_password_verify"
17993            | "web_token_for" | "web_token_consume" | "web_csrf_meta_tag"
17994            | "web_security_headers" | "web_can"
17995            | "web_h" | "web_truncate" | "web_pluralize" | "web_time_ago_in_words"
17996            | "web_image_tag" | "web_link_to" | "web_button_to"
17997            | "web_form_with" | "web_form_close"
17998            | "web_text_field" | "web_text_area" | "web_check_box"
17999            | "web_stylesheet_link_tag" | "web_javascript_link_tag"
18000            | "web_yield_content" | "web_content_for"
18001            | "web_etag" | "web_cache_get" | "web_cache_set"
18002            | "web_cache_delete" | "web_cache_clear"
18003            | "web_db_connect" | "web_db_execute" | "web_db_query"
18004            | "web_db_begin" | "web_db_commit" | "web_db_rollback"
18005            | "web_create_table" | "web_drop_table"
18006            | "web_add_column" | "web_remove_column"
18007            | "web_migrate" | "web_rollback"
18008            | "web_model_all" | "web_model_find" | "web_model_first" | "web_model_last"
18009            | "web_model_where" | "web_model_create" | "web_model_update"
18010            | "web_model_destroy" | "web_model_count" | "web_model_increment"
18011            | "web_model_paginate" | "web_model_search" | "web_model_soft_destroy"
18012            | "web_model_with"
18013            | "web_jobs_init" | "web_job_enqueue" | "web_job_dequeue"
18014            | "web_job_complete" | "web_job_fail"
18015            | "web_jobs_list" | "web_jobs_stats" | "web_job_purge"
18016            | "web_jsonapi_resource" | "web_jsonapi_collection" | "web_jsonapi_error"
18017            | "web_bearer_token" | "web_jwt_encode" | "web_jwt_decode"
18018            | "web_otp_secret" | "web_otp_generate" | "web_otp_verify"
18019            | "web_uuid" | "web_now" | "web_log" | "web_rate_limit"
18020            | "web_t" | "web_load_locale" | "web_openapi"
18021            | "web_faker_int" | "web_faker_email" | "web_faker_name"
18022            | "web_faker_sentence" | "web_faker_paragraph"
18023            // ── test runner ─────────────────────────────────────────────────
18024            // In-process equivalents of `stryke check` / `stryke test`. The
18025            // builtin form lets stryke programs (e.g. `exercism_run_all.stk`)
18026            // call them directly without `system "stryke check $f"`, so
18027            // `check_no_interop` from inside `pmaps` is fork-free and
18028            // race-free (per-thread no-interop TLS override).
18029            | "check" | "check_no_interop" | "check_ni"
18030            | "test" | "test_no_interop" | "test_ni"
18031            // ── linear algebra / graphs / dates / special math ──
18032            // ── bits / music theory / hashes / text / statistical tests ──
18033            // ── phonetic / geo projections / base58/91/z85 / astronomy / crc / color blends / compression ──
18034            // ── bioinformatics / 3d geometry / sequence alignment / file headers / hmm ──
18035            // ── game theory / ml inference / chemistry / ops research / info theory ──
18036            // ── cv kernels / information retrieval / rl / color spaces / windows / trie/fenwick/uf / network ──
18037            // ── combinatorics / audio synthesis / search / physics 2d / noise / rng variants ──
18038            // ── ratings / image morphology / computational geometry 2d / crypto / constants / case conversions / photography / unit conversions ──
18039            | "andrew_monotone_hull" | "aperture_stop_to_fnumber" | "arpad_predict" | "bilateral_filter_2d"
18040            | "black_hat_transform" | "canny_edges_full" | "case_alternating" | "case_constant"
18041            | "case_dot" | "case_pascal" | "case_path" | "case_sentence"
18042            | "case_swap" | "case_title_proper" | "case_train" | "closing_2d"
18043            | "cohen_sutherland_clip" | "constants_au_meters" | "constants_avogadro_n" | "constants_bohr_radius"
18044            | "constants_boltzmann_k" | "constants_earth_mass" | "constants_earth_radius" | "constants_electron_charge"
18045            | "constants_electron_mass" | "constants_faraday" | "constants_gas_r" | "constants_gravitational_g"
18046            | "constants_lightyear_meters" | "constants_neutron_mass" | "constants_parsec_meters" | "constants_planck_h"
18047            | "constants_planck_hbar" | "constants_planck" | "constants_proton_mass" | "constants_rydberg"
18048            | "constants_solar_mass" | "constants_solar_radius" | "constants_speed_of_light" | "constants_stefan_boltzmann"
18049            | "contour_area" | "contour_centroid" | "contour_find" | "contour_perimeter"
18050            | "convex_hull_3d" | "convex_hull_3d_simple" | "crop_factor" | "delaunay_triangulate_2d" | "depth_of_field_far"
18051            | "depth_of_field_near" | "dh_compute_shared" | "dilation_2d" | "ec_point_add"
18052            | "ec_point_double" | "ed25519_keypair_simple" | "ed25519_sign_simple" | "ed25519_verify_simple"
18053            | "erosion_2d" | "exposure_value" | "field_of_view" | "focal_length_35mm_equiv"
18054            | "glicko_rd_update" | "glicko_volatility" | "graham_scan_hull" | "hu_moments"
18055            | "hyperfocal_distance" | "liang_barsky_clip" | "minkowski_sum_2d" | "moment_image"
18056            | "morphological_gradient" | "opening_2d" | "pagerank_tournament" | "polygon_inflate"
18057            | "polygon_offset" | "polygon_self_intersects" | "polygon_shrink" | "polygon_simple_check"
18058            | "polygon_winding" | "prewitt_x_kernel" | "prewitt_y_kernel" | "ranking_average"
18059            | "ranking_kendall_tau" | "ranking_spearman_rho" | "roberts_cross_kernel" | "rsa_keypair_simple"
18060            | "rsa_modular_exp" | "scharr_x_kernel" | "scharr_y_kernel" | "schnorr_sign_simple"
18061            | "schnorr_verify_simple" | "shutter_speed_reciprocal" | "sobel_magnitude" | "sunny_16_rule"
18062            | "swiss_pairing" | "top_hat_transform" | "tournament_score" | "trueskill_simple"
18063            | "unit_energy" | "unit_pressure" | "unit_temperature" | "unit_volume_metric_to_us"
18064            | "unit_volume_us_to_metric" | "voronoi_cell_2d" | "weiler_atherton_clip" | "zernike_radial"
18065
18066            | "a_star_grid" | "all_pass_filter" | "am_synth" | "bidirectional_bfs"
18067            | "buoyancy_force" | "center_of_mass_2d" | "center_of_mass_3d" | "centered_polygonal"
18068            | "chorus_simple" | "collision_response_2d" | "comb_filter" | "compositions_count"
18069            | "critical_damping" | "cube_number" | "damping_factor" | "decagonal_number"
18070            | "derangement_count" | "dodecahedral" | "elastic_collision_1d" | "exponential_search"
18071            | "fbm_noise_2d" | "fibonacci_matrix" | "fibonacci_nth_fast" | "fir_filter"
18072            | "flanger_simple" | "floyd_cycle_detect" | "fm_synth_2op" | "freeverb_lite"
18073            | "gnomonic_number" | "greedy_best_first" | "hash_2d_int" | "heptagonal_number"
18074            | "hexagonal_number" | "hyperfactorial" | "icosahedral" | "ida_star_search"
18075            | "inelastic_collision_1d" | "interpolation_search" | "lattice_paths" | "lift_force"
18076            | "lucas_nth" | "moment_of_inertia_cylinder" | "moment_of_inertia_disc" | "moment_of_inertia_rod"
18077            | "moment_of_inertia_sphere" | "mulberry32_next" | "multinomial_coefficient" | "narayana_cow"
18078            | "nonagonal_number" | "octagonal_number" | "partitions_count" | "pcg32_next"
18079            | "pell_nth" | "perlin_2d" | "perlin_3d" | "phaser_simple"
18080            | "plate_reverb_simple" | "poisson_brackets" | "primorial" | "projectile_position"
18081            | "projectile_velocity" | "ridge_noise_2d" | "ring_modulate" | "schroeder_reverb"
18082            | "simplex_2d" | "splitmix64_next" | "spring_oscillator_pos" | "square_pyramidal"
18083            | "super_factorial" | "ternary_search" | "tetrahedral" | "tetranacci"
18084            | "torque_arm" | "turbulence_noise_2d" | "value_noise_2d" | "wavetable_synth"
18085            | "worley_2d" | "xorshift32_next"
18086
18087            | "adaptive_threshold" | "bayes_factor" | "bayesian_beta_update" | "bayesian_normal_update"
18088            | "bm25_score" | "boltzmann_choose" | "braycurtis_dist" | "canberra_dist"
18089            | "canny_edges_simple" | "chebyshev_norm" | "cidr_to_range" | "ciede2000_color_distance"
18090            | "ciede76_color_distance" | "ciede94_color_distance" | "conv1d_apply" | "conv2d_apply"
18091            | "correlate2d" | "cosine_sim_sparse" | "credible_interval_beta" | "credible_interval_normal"
18092            | "dice_coeff" | "earth_mover_1d" | "epsilon_greedy_choose" | "fenwick_new"
18093            | "fenwick_query_prefix" | "fenwick_query_range" | "fenwick_update" | "gaussian_kernel"
18094            | "gradient_magnitude_2d" | "harris_response" | "integral_image" | "ip_subnet_split"
18095            | "ipv6_global_unicast" | "jaccard_sim" | "laplacian_kernel" | "lch_to_rgb"
18096            | "mahalanobis_sq" | "manhattan_norm" | "maximum_a_posteriori" | "minkowski_norm"
18097            | "non_max_suppression" | "oklch_to_rgb" | "otsu_threshold" | "overlap_coeff"
18098            | "posterior_predictive_beta" | "posterior_predictive_normal" | "prior_jeffreys_uniform" | "qlearning_step"
18099            | "range_to_cidr" | "rgb_to_lch" | "rgb_to_oklch" | "rl_discount_returns"
18100            | "rl_n_step_return" | "rl_td_error" | "sarsa_step" | "sliding_dot_product"
18101            | "sobel_x_kernel" | "sobel_y_kernel" | "softmax_choose" | "tanimoto_coeff"
18102            | "tfidf_compute" | "thompson_beta_choose" | "trie_count" | "trie_insert"
18103            | "trie_keys" | "trie_lookup" | "trie_new" | "trie_prefix_search"
18104            | "trie_remove" | "tversky_index" | "ucb1_choose" | "union_find_components"
18105            | "union_find_find" | "union_find_new" | "union_find_union" | "window_bartlett"
18106            | "window_blackman_harris" | "window_flat_top" | "window_gaussian" | "window_welch"
18107
18108            | "alphabeta_value" | "chem_arrhenius_k" | "chem_avogadro" | "chem_balance_check"
18109            | "chem_boiling_point_elevation" | "chem_buffer_capacity" | "chem_celsius_to_fahrenheit" | "chem_celsius_to_kelvin"
18110            | "chem_concentration_to_molarity" | "chem_dilution" | "chem_fahrenheit_to_celsius" | "chem_fahrenheit_to_kelvin"
18111            | "chem_formula_parse" | "chem_freezing_point_depression" | "chem_h_from_ph" | "chem_henderson_hasselbalch"
18112            | "chem_ideal_gas_volume" | "chem_isoelectric_estimate" | "chem_kelvin_to_celsius" | "chem_kelvin_to_fahrenheit"
18113            | "chem_kelvin_to_rankine" | "chem_molality" | "chem_molar_mass" | "chem_molarity_to_normality"
18114            | "chem_partial_pressure" | "chem_ph_from_h" | "chem_pka_lookup" | "chem_rankine_to_kelvin"
18115            | "conditional_entropy" | "edmonds_karp_max_flow" | "expectiminimax_value" | "ford_fulkerson_max_flow"
18116            | "job_schedule_ljf" | "job_schedule_spt" | "joint_entropy" | "js_divergence_distributions"
18117            | "kl_divergence_distributions" | "knapsack_fractional" | "knapsack_unbounded" | "lp_simplex_max"
18118            | "lp_simplex_min" | "matching_bipartite_greedy" | "matching_bipartite_hungarian" | "minimax_value"
18119            | "mixed_strategy_2x2" | "ml_attention_score" | "ml_batch_norm" | "ml_dot_product_attention"
18120            | "ml_dropout_mask" | "ml_elu_layer" | "ml_gelu_layer" | "ml_hinge_loss"
18121            | "ml_huber_loss" | "ml_kl_div_loss" | "ml_label_smooth" | "ml_layer_norm"
18122            | "ml_leaky_relu_layer" | "ml_mae_loss" | "ml_mish_layer" | "ml_mse_loss"
18123            | "ml_one_hot_encode" | "ml_position_encoding" | "ml_relu_layer" | "ml_self_attention"
18124            | "ml_sigmoid_layer" | "ml_softmax_layer" | "ml_softplus_layer" | "ml_swish_layer"
18125            | "ml_tanh_layer" | "ngram_perplexity" | "ngram_prob" | "ngram_top_k_next"
18126            | "ngram_train" | "payoff_matrix" | "relative_entropy" | "tsp_2opt"
18127            | "zero_sum_value"
18128
18129            | "aabb_contains_point" | "aabb_intersects" | "aabb_new" | "aabb_union"
18130            | "aabb_volume" | "backward_algorithm" | "blast_kmer_index" | "bmp_header_read"
18131            | "bootstrap_resample" | "codon_optimize" | "codon_to_amino_acid" | "codon_usage_table"
18132            | "dna_at_content" | "dna_complement" | "dna_gc_content" | "dna_kmer_count"
18133            | "dna_kmer_index" | "dna_melting_temp" | "dna_reverse_complement" | "dna_transcribe"
18134            | "dna_translate" | "elf_header_read" | "forward_algorithm" | "gif_header_read"
18135            | "ico_header_read"
18136            | "jpeg_markers" | "levenshtein_edit_path" | "mach_o_header_read" | "markov_stationary"
18137            | "markov_transition_matrix" | "mat4_determinant" | "mat4_identity" | "mat4_inverse"
18138            | "mat4_look_at" | "mat4_multiply" | "mat4_orthographic" | "mat4_perspective"
18139            | "mat4_rotate_axis" | "mat4_rotate_x" | "mat4_rotate_y" | "mat4_rotate_z"
18140            | "mat4_scale" | "mat4_translate" | "mat4_transpose" | "nw_score"
18141            | "permutation_test" | "plane_distance_to_point" | "plane_normalize" | "png_header_read"
18142            | "profile_hmm_score" | "protein_charge_at_ph" | "protein_hydrophobicity" | "protein_molecular_weight"
18143            | "protein_pI" | "quat_conjugate" | "quat_dot" | "quat_from_euler"
18144            | "quat_identity" | "quat_inverse" | "quat_multiply" | "quat_normalize"
18145            | "quat_to_euler" | "quat_to_mat4" | "ray_aabb_intersect" | "ray_plane_intersect_2"
18146            | "ray_plane_intersect" | "rna_gc_content" | "rna_hamming" | "rna_reverse_complement"
18147            | "rna_to_dna" | "sequence_identity_pct" | "sequence_similarity_pct" | "shuffle_resample"
18148            | "sphere_aabb_intersect" | "sphere_sphere_intersect" | "sw_score" | "tar_header_read"
18149            | "triangle_area_3d" | "triangle_normal" | "vec3_add" | "vec3_cross"
18150            | "vec3_distance" | "vec3_dot" | "vec3_length" | "vec3_lerp"
18151            | "vec3_normalize" | "vec3_project" | "vec3_reflect" | "vec3_refract"
18152            | "vec3_scale" | "vec3_sub" | "vec4_add" | "vec4_dot"
18153            | "vec4_length" | "vec4_scale" | "vec4_sub" | "viterbi_decode"
18154            | "wav_header_read" | "zip_central_directory" | "zip_local_file_header"
18155
18156            | "adler32_combine" | "ase_palette_extract" | "base58check_decode" | "base58check_encode"
18157            | "base91_decode" | "basE91_decode" | "base91_encode" | "basE91_encode"
18158            | "bwt_invert" | "bwt_transform" | "caverphone" | "caverphone2"
18159            | "crc10_atm" | "crc12_dect" | "crc24" | "crc32_bzip2"
18160            | "crc32_jamcrc" | "crc32_mpeg2" | "crc32_xfer" | "crc6_itu"
18161            | "crc64_ecma" | "crc64_xz" | "delta_decode" | "delta_encode"
18162            | "destination_lat_lon" | "double_metaphone_primary" | "double_metaphone_secondary" | "fletcher16"
18163            | "fletcher32" | "fletcher64" | "full_moon_julian" | "fuzzy_substring_match"
18164            | "gamma_correct" | "gamma_uncorrect" | "geomag_declination" | "huffman_decode"
18165            | "huffman_encode" | "julian_to_unix" | "lambert_project" | "lat_lon_to_utm"
18166            | "match_rating_compare" | "mercator_project_x" | "mercator_project_y" | "mercator_unproject_lat"
18167            | "mercator_unproject_lon" | "modified_julian_date" | "moon_age_days" | "moon_distance_km"
18168            | "new_moon_julian" | "nysiis" | "phonex" | "rgb_blend_color_burn"
18169            | "rgb_blend_color_dodge" | "rgb_blend_darken" | "rgb_blend_lighten" | "rgb_blend_multiply"
18170            | "rgb_blend_normal" | "rgb_blend_overlay" | "rgb_blend_screen" | "rle_compress"
18171            | "rle_decompress" | "season_of_year" | "sidereal_time_greenwich" | "sidereal_time_local"
18172            | "solar_noon_unix" | "soundex_v1" | "soundex_v2" | "unix_to_julian"
18173            | "utm_to_lat_lon" | "utm_zone" | "varint_decode" | "varint_encode"
18174            | "vincenty_bearing" | "z85_decode" | "z85_encode" | "zigzag_decode"
18175            | "zigzag_encode"
18176
18177            | "anova_one_way" | "binomial_test" | "bit_clz"
18178            | "bit_count_ones" | "bit_count_zeros" | "bit_ctz" | "bit_extract"
18179            | "bit_first_clear" | "bit_first_set" | "bit_insert" | "bit_last_clear"
18180            | "bit_last_set" | "bit_log2_int" | "bit_parity" | "bit_reverse_u16"
18181            | "bit_reverse_u32" | "bit_reverse_u64" | "bit_reverse_u8" | "bit_rotate_left"
18182            | "bit_rotate_right" | "bit_swap_bytes" | "chi_square_goodness_fit" | "chi_square_independence"
18183            | "chord_augmented" | "chord_diminished" | "chord_diminished7" | "chord_dominant7"
18184            | "chord_major" | "chord_major7" | "chord_minor" | "chord_minor7"
18185            | "crc16_xmodem" | "crc16" | "crc32_zlib" | "crc32c"
18186            | "crc8" | "detab" | "entab" | "fisher_exact_2x2"
18187            | "gray_code_decode" | "gray_code_encode" | "hmac_md5_hex" | "hmac_sha1_hex"
18188            | "hmac_sha256_hex" | "hmac_sha384_hex" | "hmac_sha512_hex" | "indent_block"
18189            | "interval_name" | "jenkins_hash" | "justify_center" | "justify_left"
18190            | "justify_right" | "kruskal_wallis" | "ks_test_one_sample" | "ks_test_two_sample"
18191            | "loose_hash" | "mann_whitney_u" | "midi_to_note_name" | "popcount_u32"
18192            | "popcount_u64" | "proportion_test" | "rank_data" | "scale_blues"
18193            | "scale_chromatic" | "scale_dorian" | "scale_harmonic_minor" | "scale_locrian"
18194            | "scale_lydian" | "scale_major" | "scale_melodic_minor" | "scale_minor"
18195            | "scale_mixolydian" | "scale_pentatonic" | "scale_phrygian" | "seconds_per_beat"
18196            | "strip_indent" | "t_test_paired" | "tempo_to_ms_per_beat" | "truncate_middle"
18197            | "unicode_codepoints" | "wilcoxon_signed_rank" | "word_wrap"
18198
18199            | "beta_function" | "beta_incomplete" | "date_add_days" | "date_add_months"
18200            | "date_add_years" | "date_business_days_between" | "date_day" | "date_dayofweek"
18201            | "date_dayofyear" | "date_days_in_month" | "date_diff_days" | "date_diff_hours"
18202            | "date_diff_minutes" | "date_diff_seconds" | "date_easter" | "date_first_of_month"
18203            | "date_hour" | "date_is_leap" | "date_is_weekend" | "date_iso_format"
18204            | "date_iso_week" | "date_last_of_month" | "date_minute" | "date_month"
18205            | "date_quarter" | "date_second" | "date_str_to_unix" | "date_unix_to_str"
18206            | "date_weekofyear" | "date_year" | "ei" | "expint"
18207            | "gamma_regularized_p" | "gamma_regularized_q" | "graph_articulation_points" | "graph_bellman_ford"
18208            | "graph_betweenness" | "graph_bfs" | "graph_bridges" | "graph_closeness"
18209            | "graph_clustering_coefficient" | "graph_color_greedy" | "graph_connected_components" | "graph_cycle_detect"
18210            | "graph_degree" | "graph_dfs" | "graph_dijkstra" | "graph_eccentricity"
18211            | "graph_eigenvector_centrality" | "graph_floyd_warshall" | "graph_from_edges" | "graph_has_path"
18212            | "graph_in_degree" | "graph_is_bipartite" | "graph_is_connected" | "graph_kosaraju"
18213            | "graph_kruskal_mst" | "graph_out_degree" | "graph_pagerank" | "graph_prim_mst"
18214            | "graph_shortest_path" | "graph_strongly_connected_components" | "graph_tarjan" | "graph_to_adj_list"
18215            | "graph_to_adj_matrix" | "graph_topological_sort" | "hypergeom_1f1" | "hypergeom_2f1"
18216            | "li" | "matrix_adjugate" | "matrix_cholesky_decompose" | "matrix_cofactor"
18217            | "matrix_cols" | "matrix_concat_h" | "matrix_concat_v" | "matrix_determinant"
18218            | "matrix_from_cols" | "matrix_get" | "matrix_kronecker" | "matrix_lu_decompose"
18219            | "matrix_minor" | "matrix_new" | "matrix_norm_frobenius" | "matrix_norm_l1"
18220            | "matrix_norm_linf" | "matrix_outer_product" | "matrix_qr_decompose" | "matrix_reshape"
18221            | "matrix_rows" | "matrix_set" | "matrix_submatrix" | "matrix_swap_cols"
18222            | "matrix_swap_rows" | "matrix_to_string" | "matrix_vec_mul" | "si"
18223            | "sun_rise_unix" | "sun_set_unix" | "zeta_riemann" | "zodiac_sign"
18224            // ── quant / technical indicators / time-series / finance / optimization ──
18225            | "add_seasonality" | "trapezoidal_integrate" | "simpson_integrate"
18226            | "ode_euler" | "fit_curve_least_squares"
18227            | "adf_test" | "adx" | "atr" | "bollinger_lower" | "bollinger_middle"
18228            | "bollinger_upper" | "break_even_price" | "break_even_qty" | "candlestick_pattern_doji"
18229            | "candlestick_pattern_engulfing" | "candlestick_pattern_evening_star" | "candlestick_pattern_hammer" | "candlestick_pattern_morning_star"
18230            | "candlestick_pattern_three_black_crows" | "candlestick_pattern_three_white_soldiers" | "cci"
18231            | "dema" | "diff_pct" | "diff_series"
18232            | "discount_pct" | "donchian_lower" | "donchian_upper" | "double_exponential_smoothing"
18233            | "duration_modified" | "ema" | "expanding_mean" | "expanding_sum"
18234            | "fibonacci_extension" | "fibonacci_retracement" | "finite_difference_central"
18235            | "finite_difference_forward" | "hma"
18236            | "hurst_exponent" | "interp_lagrange"
18237            | "interp_linear" | "kama"
18238            | "keltner_lower" | "keltner_upper" | "lag_series"
18239            | "loan_interest_total" | "loan_payment" | "loan_remaining" | "log_returns"
18240            | "macd_histogram" | "macd_signal" | "macd" | "markup_pct" | "net_present_value" | "obv" | "parabolic_sar" | "pivot_points" | "profit_margin_pct" | "remove_seasonality" | "resistance_level"
18241            | "roc" | "rolling_kurtosis" | "rolling_max"
18242            | "rolling_mean" | "rolling_median" | "rolling_min" | "rolling_skew"
18243            | "rolling_std" | "rolling_sum" | "rolling_var"
18244            | "rsi" | "shift_series" | "simple_returns" | "sma" | "stoch_rsi" | "support_level"
18245            | "tema" | "trend_line" | "treynor"
18246            | "trix" | "true_range" | "twap" | "ulcer_index"
18247            | "volatility_annualized" | "volatility_realized" | "vwap" | "williams_r"
18248            | "wma"
18249            => Some(name),
18250            _ => None,
18251        }
18252    }
18253
18254    /// Reserved hash names that cannot be shadowed by user declarations.
18255    /// These are stryke's reflection hashes populated from builtins metadata.
18256    fn is_reserved_hash_name(name: &str) -> bool {
18257        matches!(
18258            name,
18259            "b" | "pc"
18260                | "e"
18261                | "a"
18262                | "d"
18263                | "c"
18264                | "p"
18265                | "k"
18266                | "all"
18267                | "stryke::builtins"
18268                | "stryke::perl_compats"
18269                | "stryke::extensions"
18270                | "stryke::aliases"
18271                | "stryke::descriptions"
18272                | "stryke::categories"
18273                | "stryke::primaries"
18274                | "stryke::keywords"
18275                | "stryke::all"
18276        )
18277    }
18278
18279    /// Check if a UDF name shadows a stryke builtin and error if so.
18280    /// Called only in non-compat mode — compat mode allows shadowing for Perl 5 parity.
18281    /// Reserved words that cannot be used as function names because they are
18282    /// lexer-level operators or language keywords that would be mis-tokenized.
18283    const RESERVED_FUNCTION_NAMES: &'static [&'static str] = &[
18284        "y",
18285        "tr",
18286        "s",
18287        "m",
18288        "q",
18289        "qq",
18290        "qw",
18291        "qx",
18292        "qr",
18293        "if",
18294        "unless",
18295        "while",
18296        "until",
18297        "for",
18298        "foreach",
18299        "given",
18300        "when",
18301        "else",
18302        "elsif",
18303        "do",
18304        "eval",
18305        "return",
18306        "last",
18307        "next",
18308        "redo",
18309        "goto",
18310        "my",
18311        "our",
18312        "local",
18313        "state",
18314        "sub",
18315        "fn",
18316        "class",
18317        "struct",
18318        "enum",
18319        "trait",
18320        "use",
18321        "no",
18322        "require",
18323        "package",
18324        "BEGIN",
18325        "END",
18326        "CHECK",
18327        "INIT",
18328        "UNITCHECK",
18329        "and",
18330        "or",
18331        "not",
18332        "x",
18333        "eq",
18334        "ne",
18335        "lt",
18336        "gt",
18337        "le",
18338        "ge",
18339        "cmp",
18340    ];
18341
18342    fn check_udf_shadows_builtin(&self, name: &str, line: usize) -> StrykeResult<()> {
18343        // Already namespaced (e.g. `Foo::y`) — package context makes the
18344        // name unambiguous, so it can never shadow a builtin.
18345        if name.contains("::") {
18346            return Ok(());
18347        }
18348        // Reserved syntactic words (`if`, `while`, `package`, …) break
18349        // parsing as function names regardless of package.
18350        if Self::RESERVED_FUNCTION_NAMES.contains(&name) {
18351            return Err(self.syntax_err(
18352                format!("`{name}` is a reserved word and cannot be used as a function name"),
18353                line,
18354            ));
18355        }
18356        // Bare `fn name(...)` inside a non-main `package Foo` registers
18357        // under `Foo::name`. The user sub is callable only via the
18358        // fully-qualified `Foo::name(...)` spelling — bare calls always
18359        // dispatch to the global builtin. Allow the declaration.
18360        if self.current_package != "main" {
18361            return Ok(());
18362        }
18363        // In `package main` (the default), there's no qualified spelling
18364        // to "escape" a builtin name. Reject `fn sum {}` here so callers
18365        // never wonder why bare `sum(1,2,3)` ignored their definition.
18366        if Self::is_known_bareword(name)
18367            || Self::is_try_builtin_name(name)
18368            || crate::list_builtins::is_list_builtin_name(name)
18369        {
18370            return Err(self.syntax_err(
18371                format!(
18372"`{name}` is a stryke builtin and cannot be redefined in `package main` (declare in a named package and call via `Pkg::{name}(...)`, or pass --compat)"
18373                ),
18374                line,
18375            ));
18376        }
18377        Ok(())
18378    }
18379
18380    /// Check if a hash name shadows a reserved stryke hash and error if so.
18381    /// Called only in non-compat mode.
18382    fn check_hash_shadows_reserved(&self, name: &str, line: usize) -> StrykeResult<()> {
18383        if Self::is_reserved_hash_name(name) {
18384            return Err(self.syntax_err(
18385                format!(
18386"`%{name}` is a stryke reserved hash and cannot be redefined (this is not Perl 5; pass --compat for Perl 5 mode)"
18387                ),
18388                line,
18389            ));
18390        }
18391        Ok(())
18392    }
18393
18394    /// Validate assignment to %hash in non-compat mode.
18395    /// Rejects: scalar, string, arrayref, hashref, coderef, undef, odd-length list.
18396    fn validate_hash_assignment(&self, value: &Expr, line: usize) -> StrykeResult<()> {
18397        match &value.kind {
18398            ExprKind::Integer(_) | ExprKind::Float(_) => {
18399                return Err(self.syntax_err(
18400                    "cannot assign scalar to hash — use %h = (key => value) or %h = %{$hashref}",
18401                    line,
18402                ));
18403            }
18404            ExprKind::String(_) | ExprKind::InterpolatedString(_) | ExprKind::Bareword(_) => {
18405                return Err(self.syntax_err(
18406                    "cannot assign string to hash — use %h = (key => value) or %h = %{$hashref}",
18407                    line,
18408                ));
18409            }
18410            ExprKind::ArrayRef(_) => {
18411                return Err(self.syntax_err(
18412                    "cannot assign arrayref to hash — use %h = @{$arrayref} for even-length list",
18413                    line,
18414                ));
18415            }
18416            ExprKind::ScalarRef(inner) => {
18417                if matches!(inner.kind, ExprKind::ArrayVar(_)) {
18418                    return Err(self.syntax_err(
18419                        "cannot assign \\@array to hash — use %h = @array for even-length list",
18420                        line,
18421                    ));
18422                }
18423                if matches!(inner.kind, ExprKind::HashVar(_)) {
18424                    return Err(self.syntax_err(
18425                        "cannot assign \\%hash to hash — use %h = %other directly",
18426                        line,
18427                    ));
18428                }
18429            }
18430            ExprKind::HashRef(_) => {
18431                return Err(self.syntax_err(
18432                    "cannot assign hashref to hash — use %h = %{$hashref} to dereference",
18433                    line,
18434                ));
18435            }
18436            ExprKind::CodeRef { .. } => {
18437                return Err(self.syntax_err("cannot assign coderef to hash", line));
18438            }
18439            ExprKind::Undef => {
18440                return Err(
18441                    self.syntax_err("cannot assign undef to hash — use %h = () to empty", line)
18442                );
18443            }
18444            ExprKind::List(items)
18445                if items.len() % 2 != 0
18446                    && !items.iter().any(|e| {
18447                        matches!(
18448                            e.kind,
18449                            ExprKind::ArrayVar(_)
18450                                | ExprKind::HashVar(_)
18451                                | ExprKind::FuncCall { .. }
18452                                | ExprKind::Deref { .. }
18453                                | ExprKind::ScalarVar(_)
18454                        )
18455                    }) =>
18456            {
18457                return Err(self.syntax_err(
18458                        format!(
18459                            "odd-length list ({} elements) in hash assignment — missing value for last key",
18460                            items.len()
18461                        ),
18462                        line,
18463                    ));
18464            }
18465            _ => {}
18466        }
18467        Ok(())
18468    }
18469
18470    /// Validate assignment to @array in non-compat mode.
18471    /// Rejects: undef (likely a mistake — use `@a = ()` to empty).
18472    /// Note: bare scalars like `@a = 2` are allowed since Perl coerces them to single-element lists.
18473    /// Note: `@a = {hashref}` is allowed as a common pattern for single-element arrays.
18474    fn validate_array_assignment(&self, value: &Expr, line: usize) -> StrykeResult<()> {
18475        if let ExprKind::Undef = &value.kind {
18476            return Err(
18477                self.syntax_err("cannot assign undef to array — use @a = () to empty", line)
18478            );
18479        }
18480        Ok(())
18481    }
18482
18483    /// Validate assignment to $scalar in non-compat mode.
18484    /// Rejects: list literals (Perl 5 silently returns last element — footgun).
18485    fn validate_scalar_assignment(&self, value: &Expr, line: usize) -> StrykeResult<()> {
18486        if let ExprKind::List(items) = &value.kind {
18487            if items.len() > 1 {
18488                return Err(self.syntax_err(
18489                    format!(
18490                        "cannot assign {}-element list to scalar — Perl 5 silently takes last element; use ($x) = (list) or $x = $list[-1]",
18491                        items.len()
18492                    ),
18493                    line,
18494                ));
18495            }
18496        }
18497        Ok(())
18498    }
18499
18500    /// Validate an assignment based on target type (in non-compat mode only).
18501    fn validate_assignment(&self, target: &Expr, value: &Expr, line: usize) -> StrykeResult<()> {
18502        if crate::compat_mode() {
18503            return Ok(());
18504        }
18505        match &target.kind {
18506            ExprKind::HashVar(_) => self.validate_hash_assignment(value, line),
18507            ExprKind::ArrayVar(_) => self.validate_array_assignment(value, line),
18508            ExprKind::ScalarVar(_) => self.validate_scalar_assignment(value, line),
18509            _ => Ok(()),
18510        }
18511    }
18512
18513    /// Parse a block OR a blockless comparison expression for sort/psort/heap.
18514    /// Blockless: `$a <=> $b` or `$a cmp $b` or any expression → wrapped as a Block.
18515    /// Also accepts a bare function name: `psort my_cmp, @list`.
18516    fn parse_block_or_bareword_cmp_block(&mut self) -> StrykeResult<Block> {
18517        if matches!(self.peek(), Token::LBrace) {
18518            return self.parse_block();
18519        }
18520        let line = self.peek_line();
18521        // Bare sub name: `psort my_cmp, @list`
18522        if let Token::Ident(ref name) = self.peek().clone() {
18523            if matches!(
18524                self.peek_at(1),
18525                Token::Comma | Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
18526            ) {
18527                let name = name.clone();
18528                self.advance();
18529                let body = Expr {
18530                    kind: ExprKind::FuncCall {
18531                        name,
18532                        args: vec![
18533                            Expr {
18534                                kind: ExprKind::ScalarVar("a".to_string()),
18535                                line,
18536                            },
18537                            Expr {
18538                                kind: ExprKind::ScalarVar("b".to_string()),
18539                                line,
18540                            },
18541                        ],
18542                    },
18543                    line,
18544                };
18545                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
18546            }
18547        }
18548        // Blockless expression: `$a <=> $b`, `$b cmp $a`, etc.
18549        let expr = self.parse_assign_expr_stop_at_pipe()?;
18550        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
18551    }
18552
18553    /// After `fan` / `fan_cap` `{ BLOCK }`, optional `, progress => EXPR` or `progress => EXPR` (no comma).
18554    fn parse_fan_optional_progress(
18555        &mut self,
18556        which: &'static str,
18557    ) -> StrykeResult<Option<Box<Expr>>> {
18558        let line = self.peek_line();
18559        if self.eat(&Token::Comma) {
18560            match self.peek() {
18561                Token::Ident(ref kw)
18562                    if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) =>
18563                {
18564                    self.advance();
18565                    self.expect(&Token::FatArrow)?;
18566                    return Ok(Some(Box::new(self.parse_assign_expr()?)));
18567                }
18568                _ => {
18569                    return Err(self.syntax_err(
18570                        format!("{which}: expected `progress => EXPR` after comma"),
18571                        line,
18572                    ));
18573                }
18574            }
18575        }
18576        if let Token::Ident(ref kw) = self.peek().clone() {
18577            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
18578                self.advance();
18579                self.expect(&Token::FatArrow)?;
18580                return Ok(Some(Box::new(self.parse_assign_expr()?)));
18581            }
18582        }
18583        Ok(None)
18584    }
18585
18586    /// Comma-separated assign expressions with optional trailing `, progress => EXPR`
18587    /// (for `pmap_chunked`, `psort`, etc.).
18588    ///
18589    /// Paren-less — individual parts parse through
18590    /// [`Self::parse_assign_expr_stop_at_pipe`] so a trailing `|>` is left for
18591    /// the enclosing pipe-forward loop (left-associative chaining).
18592    fn parse_assign_expr_list_optional_progress(&mut self) -> StrykeResult<(Expr, Option<Expr>)> {
18593        // On the RHS of `|>`, list-taking builtins may be written bare with no
18594        // operand — `@a |> uniq`, `@a |> flatten`, `foo(bar, @a |> psort)`, etc.
18595        // When the next token is a list-terminator, yield an empty placeholder
18596        // list; [`Self::pipe_forward_apply`] substitutes the piped LHS at
18597        // desugar time, so the placeholder is never evaluated.
18598        if self.in_pipe_rhs()
18599            && matches!(
18600                self.peek(),
18601                Token::Semicolon
18602                    | Token::RBrace
18603                    | Token::RParen
18604                    | Token::Eof
18605                    | Token::PipeForward
18606                    | Token::Comma
18607            )
18608        {
18609            return Ok((self.pipe_placeholder_list(self.peek_line()), None));
18610        }
18611        let mut parts = vec![self.parse_assign_expr_stop_at_pipe()?];
18612        loop {
18613            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
18614                break;
18615            }
18616            if matches!(
18617                self.peek(),
18618                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
18619            ) {
18620                break;
18621            }
18622            if self.peek_is_postfix_stmt_modifier_keyword() {
18623                break;
18624            }
18625            if let Token::Ident(ref kw) = self.peek().clone() {
18626                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
18627                    self.advance();
18628                    self.expect(&Token::FatArrow)?;
18629                    let prog = self.parse_assign_expr_stop_at_pipe()?;
18630                    return Ok((merge_expr_list(parts), Some(prog)));
18631                }
18632            }
18633            parts.push(self.parse_assign_expr_stop_at_pipe()?);
18634        }
18635        Ok((merge_expr_list(parts), None))
18636    }
18637
18638    fn parse_one_arg(&mut self) -> StrykeResult<Expr> {
18639        if matches!(self.peek(), Token::LParen) {
18640            self.advance();
18641            let expr = self.parse_expression()?;
18642            self.expect(&Token::RParen)?;
18643            Ok(expr)
18644        } else {
18645            self.parse_assign_expr_stop_at_pipe()
18646        }
18647    }
18648
18649    /// Bare argument for a Perl-5 named unary operator (`defined`, `length`,
18650    /// `abs`, `scalar`, `ref`, `keys`, `values`, etc.). Named unary precedence
18651    /// sits between shift (`<<`/`>>`) and comparison (`<`/`>`), so we parse
18652    /// only down to shift level. The surrounding `&&` / `||` / `==` / `<` /
18653    /// equality / logical / ternary stay outside the unary's argument.
18654    /// Without this `defined $x && Y` mis-parsed as `defined($x && Y)` and
18655    /// silently returned true whenever `$x` was defined — see the skip-list
18656    /// debugging write-up. Same scope rule for `length` etc.
18657    fn parse_named_unary_arg(&mut self) -> StrykeResult<Expr> {
18658        if matches!(self.peek(), Token::LParen) {
18659            self.advance();
18660            let expr = self.parse_expression()?;
18661            self.expect(&Token::RParen)?;
18662            Ok(expr)
18663        } else {
18664            self.parse_shift()
18665        }
18666    }
18667
18668    fn parse_one_arg_or_default(&mut self) -> StrykeResult<Expr> {
18669        // Treat a line boundary as a hard arg terminator: if the next
18670        // token is on a *later* line than the named-unary keyword we
18671        // just consumed, default the operand to `$_` and stop. Without
18672        // this, `my $x = uc` followed by `my $y = 5` on the next line
18673        // mis-parses by silently swallowing `my $y = 5` as the implicit
18674        // argument to `uc`. Stryke (like Perl/shell) terminates
18675        // statements at newline; continuation requires explicit `\`.
18676        // The check skips when the *next* token is itself a binary /
18677        // postfix operator that legitimately continues the expression
18678        // (handled by the existing operator stop-list below).
18679        let prev = self.prev_line();
18680        if self.peek_line() > prev {
18681            return Ok(Expr {
18682                kind: ExprKind::ScalarVar("_".into()),
18683                line: prev,
18684            });
18685        }
18686        // Default to `$_` when the next token cannot start an argument expression
18687        // because it has lower precedence than a named unary operator. Perl 5
18688        // named unary precedence sits above ternary / comparison / logical / bitwise
18689        // / assignment / list ops; everything below should terminate the implicit
18690        // argument and let the surrounding expression continue.
18691        // See `perldoc perlop` ("Named Unary Operators").
18692        if matches!(
18693            self.peek(),
18694            // Statement / list / call boundaries
18695            Token::Semicolon
18696                | Token::RBrace
18697                | Token::RParen
18698                | Token::RBracket
18699                | Token::Eof
18700                | Token::Comma
18701                | Token::FatArrow
18702                | Token::PipeForward
18703            // Ternary `? :`
18704                | Token::Question
18705                | Token::Colon
18706            // Comparison / equality (numeric + string)
18707                | Token::NumEq | Token::NumNe | Token::NumLt | Token::NumGt
18708                | Token::NumLe | Token::NumGe | Token::Spaceship
18709                | Token::StrEq | Token::StrNe | Token::StrLt | Token::StrGt
18710                | Token::StrLe | Token::StrGe | Token::StrCmp
18711            // Logical (symbolic and word forms) + defined-or
18712                | Token::LogAnd | Token::LogOr | Token::LogNot
18713                | Token::LogAndWord | Token::LogOrWord | Token::LogNotWord
18714                | Token::DefinedOr
18715            // Range (lower precedence than named unary)
18716                | Token::Range | Token::RangeExclusive
18717            // Assignment (any compound form)
18718                | Token::Assign | Token::PlusAssign | Token::MinusAssign
18719                | Token::MulAssign | Token::DivAssign | Token::ModAssign
18720                | Token::PowAssign | Token::DotAssign | Token::AndAssign
18721                | Token::OrAssign | Token::XorAssign | Token::DefinedOrAssign
18722                | Token::ShiftLeftAssign | Token::ShiftRightAssign
18723                | Token::BitAndAssign | Token::BitOrAssign
18724        ) {
18725            return Ok(Expr {
18726                kind: ExprKind::ScalarVar("_".into()),
18727                line: self.peek_line(),
18728            });
18729        }
18730        // `f()` — empty parens default to `$_`, matching Perl 5 semantics.
18731        // `perldoc -f length`: "If EXPR is omitted, returns the length of $_."
18732        // Perl accepts both `length` and `length()` as `length($_)`.
18733        if matches!(self.peek(), Token::LParen) && matches!(self.peek_at(1), Token::RParen) {
18734            let line = self.peek_line();
18735            self.advance(); // (
18736            self.advance(); // )
18737            return Ok(Expr {
18738                kind: ExprKind::ScalarVar("_".into()),
18739                line,
18740            });
18741        }
18742        // Named-unary precedence: parenless arg only goes down to shift level,
18743        // so surrounding `eq` / `==` / `?:` / `&&` / `||` stay outside. Without
18744        // this, `ref $x eq "FOO"` mis-parses as `ref ($x eq "FOO")`.
18745        // (PARITY-016 — also fixes `length $s == 3 ? "Y" : "N"` etc.)
18746        self.parse_named_unary_arg()
18747    }
18748
18749    /// Array operand for `shift` / `pop`: default `@_`, or `shift(@a)` / `shift()` (empty parens = `@_`).
18750    fn parse_one_arg_or_argv(&mut self) -> StrykeResult<Expr> {
18751        let line = self.prev_line(); // line where shift/pop keyword was
18752        if matches!(self.peek(), Token::LParen) {
18753            self.advance();
18754            if matches!(self.peek(), Token::RParen) {
18755                self.advance();
18756                return Ok(Expr {
18757                    kind: ExprKind::ArrayVar("_".into()),
18758                    line: self.peek_line(),
18759                });
18760            }
18761            let expr = self.parse_expression()?;
18762            self.expect(&Token::RParen)?;
18763            return Ok(expr);
18764        }
18765        // Implicit semicolon: if next token is on a different line, don't consume it
18766        if matches!(
18767            self.peek(),
18768            Token::Semicolon
18769                | Token::RBrace
18770                | Token::RParen
18771                | Token::Eof
18772                | Token::Comma
18773                | Token::PipeForward
18774        ) || self.peek_line() > line
18775        {
18776            Ok(Expr {
18777                kind: ExprKind::ArrayVar("_".into()),
18778                line,
18779            })
18780        } else {
18781            self.parse_assign_expr()
18782        }
18783    }
18784
18785    fn parse_builtin_args(&mut self) -> StrykeResult<Vec<Expr>> {
18786        if matches!(self.peek(), Token::LParen) {
18787            self.advance();
18788            let args = self.parse_arg_list()?;
18789            self.expect(&Token::RParen)?;
18790            Ok(args)
18791        } else if self.suppress_parenless_call > 0 && matches!(self.peek(), Token::Ident(_)) {
18792            // In thread context, don't consume barewords as arguments
18793            // so `t filesf sorted ep` parses `sorted` as a stage, not an arg to filesf
18794            Ok(vec![])
18795        } else {
18796            self.parse_list_until_terminator()
18797        }
18798    }
18799
18800    /// Check if the next token is `=>` (fat arrow). If so, the preceding bareword
18801    /// should be treated as an auto-quoted string (hash key), not a function call.
18802    /// Returns `Some(Expr::String(name))` if fat arrow follows, `None` otherwise.
18803    #[inline]
18804    fn fat_arrow_autoquote(&self, name: &str, line: usize) -> Option<Expr> {
18805        if matches!(self.peek(), Token::FatArrow) {
18806            Some(Expr {
18807                kind: ExprKind::String(name.to_string()),
18808                line,
18809            })
18810        } else {
18811            None
18812        }
18813    }
18814
18815    /// Parse a hash subscript key inside `{…}`.
18816    ///
18817    /// Perl auto-quotes a single bareword before `}`, even for keywords:
18818    /// `$h{print}`, `$r->{f}` etc. all yield the string key. Stryke also
18819    /// auto-quotes the string-comparison and word-logical operator tokens
18820    /// (`eq`, `ne`, `lt`, `gt`, `le`, `ge`, `cmp`, `and`, `or`, `not`, `x`)
18821    /// here — the lexer eagerly converts those identifiers to operator tokens,
18822    /// but inside `{…}` followed by `}` they're plainly hash keys.
18823    /// Stryke exception: topic-slot barewords (`_`, `_<`, `_0`, `_0<`, …)
18824    /// resolve to the topic value, not the literal name — `$h{_<}` ≡ `$h{$_<}`.
18825    fn parse_hash_subscript_key(&mut self) -> StrykeResult<Expr> {
18826        let line = self.peek_line();
18827        if let Token::Ident(ref k) = self.peek().clone() {
18828            if matches!(self.peek_at(1), Token::RBrace) && !Self::is_underscore_topic_slot(k) {
18829                let s = k.clone();
18830                self.advance();
18831                return Ok(Expr {
18832                    kind: ExprKind::String(s),
18833                    line,
18834                });
18835            }
18836        }
18837        if matches!(self.peek_at(1), Token::RBrace) {
18838            if let Some(s) = Self::operator_keyword_to_ident_str(self.peek()) {
18839                self.advance();
18840                return Ok(Expr {
18841                    kind: ExprKind::String(s.to_string()),
18842                    line,
18843                });
18844            }
18845        }
18846        self.parse_expression()
18847    }
18848
18849    /// `progress` introducing the optional `progress => EXPR` suffix for `glob_par` / `par_sed`.
18850    #[inline]
18851    fn peek_is_glob_par_progress_kw(&self) -> bool {
18852        matches!(self.peek(), Token::Ident(ref kw) if kw == "progress")
18853            && matches!(self.peek_at(1), Token::FatArrow)
18854    }
18855
18856    /// Pattern list for `glob_par` / `par_sed` inside `(...)`, stopping before `)` or `progress =>`.
18857    fn parse_pattern_list_until_rparen_or_progress(&mut self) -> StrykeResult<Vec<Expr>> {
18858        let mut args = Vec::new();
18859        loop {
18860            if matches!(self.peek(), Token::RParen | Token::Eof) {
18861                break;
18862            }
18863            if self.peek_is_glob_par_progress_kw() {
18864                break;
18865            }
18866            args.push(self.parse_assign_expr()?);
18867            match self.peek() {
18868                Token::RParen => break,
18869                Token::Comma => {
18870                    self.advance();
18871                    if matches!(self.peek(), Token::RParen) {
18872                        break;
18873                    }
18874                    if self.peek_is_glob_par_progress_kw() {
18875                        break;
18876                    }
18877                }
18878                _ => {
18879                    return Err(self.syntax_err(
18880                        "expected `,`, `)`, or `progress =>` after argument in `glob_par` / `par_sed`",
18881                        self.peek_line(),
18882                    ));
18883                }
18884            }
18885        }
18886        Ok(args)
18887    }
18888
18889    /// Paren-less pattern list for `glob_par` / `par_sed`, stopping before stmt end or `progress =>`.
18890    fn parse_pattern_list_glob_par_bare(&mut self) -> StrykeResult<Vec<Expr>> {
18891        let mut args = Vec::new();
18892        loop {
18893            if matches!(
18894                self.peek(),
18895                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
18896            ) {
18897                break;
18898            }
18899            if self.peek_is_postfix_stmt_modifier_keyword() {
18900                break;
18901            }
18902            if self.peek_is_glob_par_progress_kw() {
18903                break;
18904            }
18905            args.push(self.parse_assign_expr()?);
18906            if !self.eat(&Token::Comma) {
18907                break;
18908            }
18909            if self.peek_is_glob_par_progress_kw() {
18910                break;
18911            }
18912        }
18913        Ok(args)
18914    }
18915
18916    /// `glob_pat EXPR, ...` or `glob_pat(...)` plus optional `, progress => EXPR` / inner `progress =>`.
18917    fn parse_glob_par_or_par_sed_args(&mut self) -> StrykeResult<(Vec<Expr>, Option<Box<Expr>>)> {
18918        if matches!(self.peek(), Token::LParen) {
18919            self.advance();
18920            let args = self.parse_pattern_list_until_rparen_or_progress()?;
18921            let progress = if self.peek_is_glob_par_progress_kw() {
18922                self.advance();
18923                self.expect(&Token::FatArrow)?;
18924                Some(Box::new(self.parse_assign_expr()?))
18925            } else {
18926                None
18927            };
18928            self.expect(&Token::RParen)?;
18929            Ok((args, progress))
18930        } else {
18931            let args = self.parse_pattern_list_glob_par_bare()?;
18932            // Comma after the last pattern was consumed inside `parse_pattern_list_glob_par_bare`.
18933            let progress = if self.peek_is_glob_par_progress_kw() {
18934                self.advance();
18935                self.expect(&Token::FatArrow)?;
18936                Some(Box::new(self.parse_assign_expr()?))
18937            } else {
18938                None
18939            };
18940            Ok((args, progress))
18941        }
18942    }
18943
18944    pub(crate) fn parse_arg_list(&mut self) -> StrykeResult<Vec<Expr>> {
18945        let mut args = Vec::new();
18946        // Inside `(...)`, `|>` is a normal operator again (e.g. `f(2 |> g, 3)`),
18947        // so shadow any outer paren-less-arg suppression from
18948        // `no_pipe_forward_depth`. Saturating so nested mixes are safe.
18949        let saved_no_pf = self.no_pipe_forward_depth;
18950        self.no_pipe_forward_depth = 0;
18951        while !matches!(
18952            self.peek(),
18953            Token::RParen | Token::RBracket | Token::RBrace | Token::Eof
18954        ) {
18955            let arg = match self.parse_assign_expr() {
18956                Ok(e) => e,
18957                Err(err) => {
18958                    self.no_pipe_forward_depth = saved_no_pf;
18959                    return Err(err);
18960                }
18961            };
18962            args.push(arg);
18963            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
18964                break;
18965            }
18966        }
18967        self.no_pipe_forward_depth = saved_no_pf;
18968        Ok(args)
18969    }
18970
18971    /// Parse a comma-separated list of slice subscript args. Each arg may be a regular
18972    /// expression, a closed range (`1:3`, `1..3:2`), or an open-ended Python-style colon
18973    /// range (`:`, `::`, `:N`, `N:`, `::-1`, `:N:M`, `N::M`, `::M`). Open-ended forms
18974    /// produce `ExprKind::SliceRange`; closed `1:3` produces `ExprKind::Range` (legacy).
18975    ///
18976    /// `is_hash` enables fat-comma-style bareword auto-quoting for endpoints — `{a:c:1}`
18977    /// treats `a` and `c` as string keys without quoting (cannot be a function call;
18978    /// use `func():other` if you actually want to invoke).
18979    pub(crate) fn parse_slice_arg_list(&mut self, is_hash: bool) -> StrykeResult<Vec<Expr>> {
18980        let mut args = Vec::new();
18981        let saved_no_pf = self.no_pipe_forward_depth;
18982        self.no_pipe_forward_depth = 0;
18983        while !matches!(
18984            self.peek(),
18985            Token::RParen | Token::RBracket | Token::RBrace | Token::Eof
18986        ) {
18987            let arg = match self.parse_slice_arg(is_hash) {
18988                Ok(e) => e,
18989                Err(err) => {
18990                    self.no_pipe_forward_depth = saved_no_pf;
18991                    return Err(err);
18992                }
18993            };
18994            args.push(arg);
18995            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
18996                break;
18997            }
18998        }
18999        self.no_pipe_forward_depth = saved_no_pf;
19000        Ok(args)
19001    }
19002
19003    /// Parse one slice subscript argument (see [`Self::parse_slice_arg_list`]).
19004    fn parse_slice_arg(&mut self, is_hash: bool) -> StrykeResult<Expr> {
19005        let line = self.peek_line();
19006
19007        // Open-start: `:` or `::` immediately
19008        if matches!(self.peek(), Token::Colon) {
19009            self.advance();
19010            return self.finish_slice_range(None, false, is_hash, line);
19011        }
19012        if matches!(self.peek(), Token::PackageSep) {
19013            self.advance();
19014            return self.finish_slice_range(None, true, is_hash, line);
19015        }
19016
19017        // Parse FROM with `:` suppressed inside `parse_range` so it doesn't get
19018        // consumed as a colon-range there — we want to handle the colon ourselves.
19019        self.suppress_colon_range = self.suppress_colon_range.saturating_add(1);
19020        let result = self.parse_slice_endpoint(is_hash);
19021        self.suppress_colon_range = self.suppress_colon_range.saturating_sub(1);
19022        let from_expr = result?;
19023
19024        // Trailing `:` or `::` after the FROM endpoint?
19025        if matches!(self.peek(), Token::Colon) {
19026            self.advance();
19027            return self.finish_slice_range(Some(Box::new(from_expr)), false, is_hash, line);
19028        }
19029        if matches!(self.peek(), Token::PackageSep) {
19030            self.advance();
19031            return self.finish_slice_range(Some(Box::new(from_expr)), true, is_hash, line);
19032        }
19033
19034        Ok(from_expr)
19035    }
19036
19037    /// After consuming the first colon (or `::` pair), parse the rest of the slice range.
19038    /// `double` is true if we just consumed `::` — TO is implicit `None`, the next
19039    /// expression (if any) is STEP.
19040    ///
19041    /// Returns `ExprKind::Range` for fully-closed forms (legacy compatibility) and
19042    /// `ExprKind::SliceRange` whenever any endpoint is omitted (open-ended).
19043    fn finish_slice_range(
19044        &mut self,
19045        from: Option<Box<Expr>>,
19046        double: bool,
19047        is_hash: bool,
19048        line: usize,
19049    ) -> StrykeResult<Expr> {
19050        let (to, step) = if double {
19051            // `::` so TO is implicit; STEP is whatever (if anything) follows.
19052            let step_v = self.parse_slice_optional_endpoint(is_hash)?;
19053            (None, step_v)
19054        } else {
19055            // single `:` — parse TO, then optional `:STEP`.
19056            let to_v = self.parse_slice_optional_endpoint(is_hash)?;
19057            let step_v = if matches!(self.peek(), Token::Colon) {
19058                self.advance();
19059                self.parse_slice_optional_endpoint(is_hash)?
19060            } else if matches!(self.peek(), Token::PackageSep) {
19061                return Err(
19062                    self.syntax_err("Unexpected `::` after slice TO endpoint".to_string(), line)
19063                );
19064            } else {
19065                None
19066            };
19067            (to_v, step_v)
19068        };
19069
19070        // Closed form (both endpoints present) — produce a regular `Range` so the
19071        // rest of the compiler/VM keeps reusing existing range-expansion paths.
19072        if let (Some(f), Some(t)) = (from.as_ref(), to.as_ref()) {
19073            return Ok(Expr {
19074                kind: ExprKind::Range {
19075                    from: f.clone(),
19076                    to: t.clone(),
19077                    exclusive: false,
19078                    step,
19079                },
19080                line,
19081            });
19082        }
19083
19084        Ok(Expr {
19085            kind: ExprKind::SliceRange { from, to, step },
19086            line,
19087        })
19088    }
19089
19090    /// Parse an optional slice endpoint: returns `None` if the next token closes the slice
19091    /// arg (`,`, `]`, `}`, or another `:`). Otherwise parses an endpoint expression.
19092    fn parse_slice_optional_endpoint(&mut self, is_hash: bool) -> StrykeResult<Option<Box<Expr>>> {
19093        if matches!(
19094            self.peek(),
19095            Token::Colon
19096                | Token::PackageSep
19097                | Token::Comma
19098                | Token::RBracket
19099                | Token::RBrace
19100                | Token::Eof
19101        ) {
19102            return Ok(None);
19103        }
19104        self.suppress_colon_range = self.suppress_colon_range.saturating_add(1);
19105        let r = self.parse_slice_endpoint(is_hash);
19106        self.suppress_colon_range = self.suppress_colon_range.saturating_sub(1);
19107        Ok(Some(Box::new(r?)))
19108    }
19109
19110    /// Parse a single slice endpoint expression. For hash slices, a bareword `Ident`
19111    /// followed by `:`, `::`, `,`, `]`, or `}` auto-quotes (fat-comma style); otherwise
19112    /// fall through to standard expression parsing. For array slices, no auto-quote.
19113    fn parse_slice_endpoint(&mut self, is_hash: bool) -> StrykeResult<Expr> {
19114        if is_hash {
19115            if let Token::Ident(name) = self.peek().clone() {
19116                if matches!(
19117                    self.peek_at(1),
19118                    Token::Colon
19119                        | Token::PackageSep
19120                        | Token::Comma
19121                        | Token::RBracket
19122                        | Token::RBrace
19123                ) {
19124                    let line = self.peek_line();
19125                    self.advance();
19126                    return Ok(Expr {
19127                        kind: ExprKind::String(name),
19128                        line,
19129                    });
19130                }
19131            }
19132        }
19133        self.parse_assign_expr()
19134    }
19135
19136    /// Arguments for `->name` / `->SUPER::name` **without** `(...)`. Unlike `die foo + 1`
19137    /// (unary `+` on `1` passed to `foo`), Perl treats `$o->meth + 5` as infix `+` after a
19138    /// no-arg method call; we must not consume that `+` as the start of a first argument.
19139    fn parse_method_arg_list_no_paren(&mut self) -> StrykeResult<Vec<Expr>> {
19140        let mut args = Vec::new();
19141        let call_line = self.prev_line();
19142        loop {
19143            // `$g->next { ... }` — `{` starts the enclosing statement's block, not an anonymous
19144            // hash argument to `next` (paren-less method call has no args here).
19145            if args.is_empty() && matches!(self.peek(), Token::LBrace) {
19146                break;
19147            }
19148            if matches!(
19149                self.peek(),
19150                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
19151            ) {
19152                break;
19153            }
19154            if let Token::Ident(ref kw) = self.peek().clone() {
19155                if matches!(
19156                    kw.as_str(),
19157                    "if" | "unless" | "while" | "until" | "for" | "foreach"
19158                ) {
19159                    break;
19160                }
19161            }
19162            // `foo($obj->meth, $x)` — comma separates *outer* args; it is not the start of a
19163            // paren-less method argument (those use spaces: `$obj->meth $a, $b`).
19164            if args.is_empty()
19165                && (self.peek_method_arg_infix_terminator() || matches!(self.peek(), Token::Comma))
19166            {
19167                break;
19168            }
19169            // Implicit semicolon: if no args collected yet and next token is on a different
19170            // line, treat newline as statement boundary. Allows `$p->method\nnext_stmt`.
19171            if args.is_empty() && self.peek_line() > call_line {
19172                break;
19173            }
19174            args.push(self.parse_assign_expr()?);
19175            if !self.eat(&Token::Comma) {
19176                break;
19177            }
19178        }
19179        Ok(args)
19180    }
19181
19182    /// Tokens that end a paren-less method arg list when no comma-separated args yet (infix on
19183    /// the whole `->meth` expression).
19184    fn peek_method_arg_infix_terminator(&self) -> bool {
19185        matches!(
19186            self.peek(),
19187            Token::Plus
19188                | Token::Minus
19189                | Token::Star
19190                | Token::Slash
19191                | Token::Percent
19192                | Token::Power
19193                | Token::Dot
19194                | Token::X
19195                | Token::NumEq
19196                | Token::NumNe
19197                | Token::NumLt
19198                | Token::NumGt
19199                | Token::NumLe
19200                | Token::NumGe
19201                | Token::Spaceship
19202                | Token::StrEq
19203                | Token::StrNe
19204                | Token::StrLt
19205                | Token::StrGt
19206                | Token::StrLe
19207                | Token::StrGe
19208                | Token::StrCmp
19209                | Token::LogAnd
19210                | Token::LogOr
19211                | Token::LogAndWord
19212                | Token::LogOrWord
19213                | Token::DefinedOr
19214                | Token::BitAnd
19215                | Token::BitOr
19216                | Token::BitXor
19217                | Token::ShiftLeft
19218                | Token::ShiftRight
19219                | Token::Range
19220                | Token::RangeExclusive
19221                | Token::BindMatch
19222                | Token::BindNotMatch
19223                | Token::Arrow
19224                // `($a->b) ? $a->c : $a->d` — `->c` must not slurp the ternary `:` / `?`.
19225                | Token::Question
19226                | Token::Colon
19227                // Assignment operators: `$obj->field = val` is setter sugar, not method arg.
19228                | Token::Assign
19229                | Token::PlusAssign
19230                | Token::MinusAssign
19231                | Token::MulAssign
19232                | Token::DivAssign
19233                | Token::ModAssign
19234                | Token::PowAssign
19235                | Token::DotAssign
19236                | Token::AndAssign
19237                | Token::OrAssign
19238                | Token::XorAssign
19239                | Token::DefinedOrAssign
19240                | Token::ShiftLeftAssign
19241                | Token::ShiftRightAssign
19242                | Token::BitAndAssign
19243                | Token::BitOrAssign
19244        )
19245    }
19246
19247    fn parse_list_until_terminator(&mut self) -> StrykeResult<Vec<Expr>> {
19248        self.parse_list_until_terminator_inner(false)
19249    }
19250
19251    /// Variant of `parse_list_until_terminator` that allows `|>` within arguments.
19252    /// Used by print-like statements (`p`, `say`, `print`, `printf`) so that
19253    /// `p @a |> sum` parses as `p(sum(@a))` rather than `sum(p(@a))`, matching
19254    /// the behavior of `~>` thread-first macro.
19255    fn parse_list_until_terminator_allow_pipe(&mut self) -> StrykeResult<Vec<Expr>> {
19256        self.parse_list_until_terminator_inner(true)
19257    }
19258
19259    fn parse_list_until_terminator_inner(&mut self, allow_pipe: bool) -> StrykeResult<Vec<Expr>> {
19260        let mut args = Vec::new();
19261        // Line of the last consumed token (the keyword / function name that
19262        // triggered this arg parse).  Used for implicit-semicolon: if no args
19263        // have been parsed yet and the next token is on a *different* line,
19264        // treat the newline as a statement boundary and stop.
19265        let call_line = self.prev_line();
19266        loop {
19267            // When `allow_pipe` is false, `|>` terminates the list (preserving
19268            // left-associativity for chains like `@a |> head 2 |> join "-"`).
19269            // When true (print-like statements), `|>` is allowed within args.
19270            let is_terminator = if allow_pipe {
19271                matches!(
19272                    self.peek(),
19273                    Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
19274                )
19275            } else {
19276                matches!(
19277                    self.peek(),
19278                    Token::Semicolon
19279                        | Token::RBrace
19280                        | Token::RParen
19281                        | Token::Eof
19282                        | Token::PipeForward
19283                )
19284            };
19285            if is_terminator {
19286                break;
19287            }
19288            // Check for postfix modifiers — stop before `expr for LIST` / `expr if COND` etc.
19289            if let Token::Ident(ref kw) = self.peek().clone() {
19290                if matches!(
19291                    kw.as_str(),
19292                    "if" | "unless" | "while" | "until" | "for" | "foreach"
19293                ) {
19294                    break;
19295                }
19296            }
19297            // Implicit semicolons: if no args have been collected yet and the
19298            // next token is on a different line from the call keyword, treat
19299            // the newline as a statement boundary.  This prevents paren-less
19300            // calls (`say`, `print`, user subs) from greedily swallowing the
19301            // *next* statement when the author omitted a semicolon.
19302            // After a comma continuation, multi-line arg lists still work.
19303            if args.is_empty() && self.peek_line() > call_line {
19304                break;
19305            }
19306            // When `allow_pipe` is true, pipe chains are consumed within each
19307            // argument. When false, `|>` terminates the whole call list, so
19308            // individual args must not absorb a following `|>`.
19309            if allow_pipe {
19310                args.push(self.parse_assign_expr()?);
19311            } else {
19312                args.push(self.parse_assign_expr_stop_at_pipe()?);
19313            }
19314            if !self.eat(&Token::Comma) {
19315                break;
19316            }
19317        }
19318        Ok(args)
19319    }
19320
19321    /// Body of `+{ ... }` — Perl's force-hashref idiom. The opening `+` and `{`
19322    /// have already been consumed. Tries the normal `KEY => VAL, …` shape first
19323    /// (so `+{ a => 1, b => 2 }` is identical to `{ a => 1, b => 2 }`); on
19324    /// failure falls back to "single list-yielding expression treated as a
19325    /// flat key/value spread" so `+{ map { (k, v) } LIST }` works without
19326    /// the user needing a temp `my %h = ...; \%h` shuffle.
19327    fn parse_forced_hashref_body(&mut self, line: usize) -> StrykeResult<Expr> {
19328        let saved = self.pos;
19329        if let Ok(pairs) = self.try_parse_hash_ref() {
19330            return Ok(Expr {
19331                kind: ExprKind::HashRef(pairs),
19332                line,
19333            });
19334        }
19335        // Empty `+{}` is the empty hashref.
19336        self.pos = saved;
19337        if matches!(self.peek(), Token::RBrace) {
19338            self.advance();
19339            return Ok(Expr {
19340                kind: ExprKind::HashRef(vec![]),
19341                line,
19342            });
19343        }
19344        // Single expression — eval as list, flatten into key/value pairs via the
19345        // existing __HASH_SPREAD__ sentinel that `ExprKind::HashRef` already
19346        // handles in [`Interpreter::eval_expr`].
19347        let inner = self.parse_expression()?;
19348        self.expect(&Token::RBrace)?;
19349        let sentinel_key = Expr {
19350            kind: ExprKind::String("__HASH_SPREAD__".into()),
19351            line,
19352        };
19353        Ok(Expr {
19354            kind: ExprKind::HashRef(vec![(sentinel_key, inner)]),
19355            line,
19356        })
19357    }
19358
19359    fn try_parse_hash_ref(&mut self) -> StrykeResult<Vec<(Expr, Expr)>> {
19360        let mut pairs = Vec::new();
19361        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
19362            // Perl autoquotes a bareword immediately before `=>` (hash key), even for keywords like
19363            // `pos`, `bless`, `return` — see Text::Balanced `_failmsg` (`pos => $pos`).
19364            // Stryke exception: topic-slot barewords (`_`, `_<`, `_0`, `_0<`, `_!N!`, …)
19365            // resolve to the topic value, not the literal name — `{ _ => 1 }` ≡ `{ $_ => 1 }`.
19366            let line = self.peek_line();
19367            let key = if let Token::Ident(ref name) = self.peek().clone() {
19368                if matches!(self.peek_at(1), Token::FatArrow)
19369                    && !Self::is_underscore_topic_slot(name)
19370                {
19371                    self.advance();
19372                    Expr {
19373                        kind: ExprKind::String(name.clone()),
19374                        line,
19375                    }
19376                } else {
19377                    self.parse_assign_expr()?
19378                }
19379            } else {
19380                self.parse_assign_expr()?
19381            };
19382            // If the key expression is a hash/array variable and is followed by `}` or `,`
19383            // with no `=>`, treat the whole thing as a hash-from-expression construction.
19384            // This handles `{ %a }`, `{ %a, key => val }`, etc.
19385            if matches!(self.peek(), Token::RBrace | Token::Comma)
19386                && matches!(
19387                    key.kind,
19388                    ExprKind::HashVar(_)
19389                        | ExprKind::Deref {
19390                            kind: Sigil::Hash,
19391                            ..
19392                        }
19393                )
19394            {
19395                // Synthesize a pair whose key/value is spread from the hash expression.
19396                // Use a sentinel "spread" pair: key=the hash expr, value=undef.
19397                // The evaluator will flatten this.
19398                let sentinel_key = Expr {
19399                    kind: ExprKind::String("__HASH_SPREAD__".into()),
19400                    line,
19401                };
19402                pairs.push((sentinel_key, key));
19403                self.eat(&Token::Comma);
19404                continue;
19405            }
19406            // Expect => or , after key
19407            if self.eat(&Token::FatArrow) || self.eat(&Token::Comma) {
19408                let val = self.parse_assign_expr()?;
19409                pairs.push((key, val));
19410                self.eat(&Token::Comma);
19411            } else {
19412                return Err(self.syntax_err("Expected => or , in hash ref", key.line));
19413            }
19414        }
19415        self.expect(&Token::RBrace)?;
19416        Ok(pairs)
19417    }
19418
19419    /// Parse `key => val, key => val, ...` up to (but not consuming) `term`.
19420    /// Used by the `%[…]` and `%{k=>v,…}` sugar to build an inline hashref
19421    /// AST node, sidestepping the block/hashref ambiguity that `try_parse_hash_ref`
19422    /// navigates. Caller expects and consumes `term` itself.
19423    fn parse_hashref_pairs_until(&mut self, term: &Token) -> StrykeResult<Vec<(Expr, Expr)>> {
19424        let mut pairs = Vec::new();
19425        while !matches!(&self.peek(), t if std::mem::discriminant(*t) == std::mem::discriminant(term))
19426            && !matches!(self.peek(), Token::Eof)
19427        {
19428            let line = self.peek_line();
19429            let key = if let Token::Ident(ref name) = self.peek().clone() {
19430                if matches!(self.peek_at(1), Token::FatArrow)
19431                    && !Self::is_underscore_topic_slot(name)
19432                {
19433                    self.advance();
19434                    Expr {
19435                        kind: ExprKind::String(name.clone()),
19436                        line,
19437                    }
19438                } else {
19439                    self.parse_assign_expr()?
19440                }
19441            } else {
19442                self.parse_assign_expr()?
19443            };
19444            if self.eat(&Token::FatArrow) || self.eat(&Token::Comma) {
19445                let val = self.parse_assign_expr()?;
19446                pairs.push((key, val));
19447                self.eat(&Token::Comma);
19448            } else {
19449                return Err(self.syntax_err("Expected => or , in hash ref", key.line));
19450            }
19451        }
19452        Ok(pairs)
19453    }
19454
19455    /// Inside an interpolated string, after a `$name`/`${EXPR}`/`$name[i]`/`$name{k}` base
19456    /// expression, consume any chain of `->[…]`, `->{…}`, **adjacent** `[…]`, or `{…}`
19457    /// subscripts. Perl auto-implies `->` between consecutive subscripts, so
19458    /// `$matrix[1][1]` is `$matrix[1]->[1]` and `$h{a}{b}` is `$h{a}->{b}`.
19459    /// Each step wraps the current expression in an `ArrowDeref`.
19460    fn interp_chain_subscripts(
19461        &self,
19462        chars: &[char],
19463        i: &mut usize,
19464        mut base: Expr,
19465        line: usize,
19466    ) -> Expr {
19467        loop {
19468            // Optional `->` connector
19469            let (after, requires_subscript) =
19470                if *i + 1 < chars.len() && chars[*i] == '-' && chars[*i + 1] == '>' {
19471                    (*i + 2, true)
19472                } else {
19473                    (*i, false)
19474                };
19475            if after >= chars.len() {
19476                break;
19477            }
19478            match chars[after] {
19479                '[' => {
19480                    *i = after + 1;
19481                    let mut idx_str = String::new();
19482                    while *i < chars.len() && chars[*i] != ']' {
19483                        idx_str.push(chars[*i]);
19484                        *i += 1;
19485                    }
19486                    if *i < chars.len() {
19487                        *i += 1;
19488                    }
19489                    let idx_expr = if let Some(rest) = idx_str.strip_prefix('$') {
19490                        Expr {
19491                            kind: ExprKind::ScalarVar(rest.to_string()),
19492                            line,
19493                        }
19494                    } else if let Ok(n) = idx_str.parse::<i64>() {
19495                        Expr {
19496                            kind: ExprKind::Integer(n),
19497                            line,
19498                        }
19499                    } else {
19500                        Expr {
19501                            kind: ExprKind::String(idx_str),
19502                            line,
19503                        }
19504                    };
19505                    base = Expr {
19506                        kind: ExprKind::ArrowDeref {
19507                            expr: Box::new(base),
19508                            index: Box::new(idx_expr),
19509                            kind: DerefKind::Array,
19510                        },
19511                        line,
19512                    };
19513                }
19514                '{' => {
19515                    *i = after + 1;
19516                    let mut key = String::new();
19517                    let mut depth = 1usize;
19518                    while *i < chars.len() && depth > 0 {
19519                        if chars[*i] == '{' {
19520                            depth += 1;
19521                        } else if chars[*i] == '}' {
19522                            depth -= 1;
19523                            if depth == 0 {
19524                                break;
19525                            }
19526                        }
19527                        key.push(chars[*i]);
19528                        *i += 1;
19529                    }
19530                    if *i < chars.len() {
19531                        *i += 1;
19532                    }
19533                    let key_expr = if let Some(rest) = key.strip_prefix('$') {
19534                        Expr {
19535                            kind: ExprKind::ScalarVar(rest.to_string()),
19536                            line,
19537                        }
19538                    } else {
19539                        Expr {
19540                            kind: ExprKind::String(key),
19541                            line,
19542                        }
19543                    };
19544                    base = Expr {
19545                        kind: ExprKind::ArrowDeref {
19546                            expr: Box::new(base),
19547                            index: Box::new(key_expr),
19548                            kind: DerefKind::Hash,
19549                        },
19550                        line,
19551                    };
19552                }
19553                _ => {
19554                    if requires_subscript {
19555                        // `->method()` etc — not interpolated, leave for literal output.
19556                    }
19557                    break;
19558                }
19559            }
19560        }
19561        base
19562    }
19563
19564    /// Reject `$a` / `$b` references in `--no-interop` mode (lexer catches them
19565    /// outside double-quoted strings; this catches the in-string interpolation
19566    /// path which has its own parser bypassing `Token::ScalarVar`).
19567    fn no_interop_check_scalar_var_name(&self, name: &str, line: usize) -> StrykeResult<()> {
19568        if crate::no_interop_mode() && (name == "a" || name == "b") {
19569            return Err(self.syntax_err(
19570                format!(
19571                    "stryke uses `_` / `_1` (bareword in code) or `$_` / `$_1` \
19572                     (sigil inside string interpolation / when whitespace would \
19573                     change parsing) instead of `${}` (--no-interop is active)",
19574                    name
19575                ),
19576                line,
19577            ));
19578        }
19579        Ok(())
19580    }
19581
19582    fn parse_interpolated_string(&self, s: &str, line: usize) -> StrykeResult<Expr> {
19583        // Parse $var and @var inside double-quoted strings
19584        let mut parts = Vec::new();
19585        let mut literal = String::new();
19586        let chars: Vec<char> = s.chars().collect();
19587        let mut i = 0;
19588
19589        'istr: while i < chars.len() {
19590            if chars[i] == LITERAL_DOLLAR_IN_DQUOTE {
19591                literal.push('$');
19592                i += 1;
19593                continue;
19594            }
19595            if chars[i] == LITERAL_AT_IN_DQUOTE {
19596                literal.push('@');
19597                i += 1;
19598                continue;
19599            }
19600            // "\\$x" in source: one backslash in the string, then interpolate $x (Perl double-quoted string).
19601            if chars[i] == '\\' && i + 1 < chars.len() && chars[i + 1] == '$' {
19602                literal.push('\\');
19603                i += 1;
19604                // i now points at '$' — fall through to $ handling below
19605            }
19606            if chars[i] == '$' && i + 1 < chars.len() {
19607                if !literal.is_empty() {
19608                    parts.push(StringPart::Literal(std::mem::take(&mut literal)));
19609                }
19610                i += 1; // past `$`
19611                        // Perl allows whitespace between `$` and the variable name (`$ foo` → `$foo`).
19612                while i < chars.len() && chars[i].is_whitespace() {
19613                    i += 1;
19614                }
19615                if i >= chars.len() {
19616                    return Err(self.syntax_err("Final $ should be \\$ or $name", line));
19617                }
19618                // `$#name` — last index of `@name` (Perl `$#array`).
19619                if chars[i] == '#' {
19620                    i += 1;
19621                    let mut sname = String::from("#");
19622                    while i < chars.len()
19623                        && (chars[i].is_alphanumeric() || chars[i] == '_' || chars[i] == ':')
19624                    {
19625                        sname.push(chars[i]);
19626                        i += 1;
19627                    }
19628                    while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
19629                        sname.push_str("::");
19630                        i += 2;
19631                        while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
19632                            sname.push(chars[i]);
19633                            i += 1;
19634                        }
19635                    }
19636                    self.no_interop_check_scalar_var_name(&sname, line)?;
19637                    parts.push(StringPart::ScalarVar(sname));
19638                    continue;
19639                }
19640                // `$$` — process id (Perl `$$`), only when the two `$` are adjacent (no whitespace
19641                // between) and the second `$` is not followed by a word character or digit (`$$x`
19642                // / `$$_` / `$$0` are `$` + `$x` / `$_` / `$0`).
19643                if chars[i] == '$' {
19644                    let next_c = chars.get(i + 1).copied();
19645                    let is_pid = match next_c {
19646                        None => true,
19647                        Some(c)
19648                            if !c.is_ascii_digit() && !matches!(c, 'A'..='Z' | 'a'..='z' | '_') =>
19649                        {
19650                            true
19651                        }
19652                        _ => false,
19653                    };
19654                    if is_pid {
19655                        parts.push(StringPart::ScalarVar("$$".to_string()));
19656                        i += 1; // consume second `$`
19657                        continue;
19658                    }
19659                    i += 1; // skip second `$` — same as a single `$` before the identifier
19660                }
19661                if chars[i] == '{' {
19662                    // `${…}` — braced variable OR expression interpolation.
19663                    //   `${name}`              → ScalarVar(name)        (Perl standard)
19664                    //   `${$ref}` / `${\EXPR}` → deref the expression   (Perl standard)
19665                    //   `${name}[idx]` / `${name}{k}` / `${$r}[i]` …    chain after `}`
19666                    // stryke's prior `#{expr}` form remains supported elsewhere.
19667                    i += 1;
19668                    let mut inner = String::new();
19669                    let mut depth = 1usize;
19670                    while i < chars.len() && depth > 0 {
19671                        match chars[i] {
19672                            '{' => depth += 1,
19673                            '}' => {
19674                                depth -= 1;
19675                                if depth == 0 {
19676                                    break;
19677                                }
19678                            }
19679                            _ => {}
19680                        }
19681                        inner.push(chars[i]);
19682                        i += 1;
19683                    }
19684                    if i < chars.len() {
19685                        i += 1; // skip closing }
19686                    }
19687
19688                    // Distinguish "name" from "expression". If trimmed inner starts with
19689                    // `$`, `\`, or contains operator/punctuation chars, treat as Perl
19690                    // expression and emit a scalar deref. Otherwise, plain variable name.
19691                    let trimmed = inner.trim();
19692                    let is_expr = trimmed.starts_with('$')
19693                        || trimmed.starts_with('\\')
19694                        || trimmed.starts_with('@')   // `${@arr}` rare but valid
19695                        || trimmed.starts_with('%')   // `${%h}`   rare but valid
19696                        || trimmed.contains(['(', '+', '-', '*', '/', '.', '?', '&', '|']);
19697                    let mut base: Expr = if is_expr {
19698                        // Re-parse the inner content as a Perl expression. Wrap in
19699                        // `Deref { kind: Sigil::Scalar }` to dereference the resulting
19700                        // scalar reference (Perl: `${$r}` ≡ `$$r`).
19701                        match parse_expression_from_str(trimmed, "<interp>") {
19702                            Ok(e) => Expr {
19703                                kind: ExprKind::Deref {
19704                                    expr: Box::new(e),
19705                                    kind: Sigil::Scalar,
19706                                },
19707                                line,
19708                            },
19709                            Err(_) => Expr {
19710                                kind: ExprKind::ScalarVar(inner.clone()),
19711                                line,
19712                            },
19713                        }
19714                    } else {
19715                        // Treat as a plain (possibly qualified) variable name.
19716                        self.no_interop_check_scalar_var_name(&inner, line)?;
19717                        Expr {
19718                            kind: ExprKind::ScalarVar(inner),
19719                            line,
19720                        }
19721                    };
19722
19723                    // After `${…}` we may see `[idx]` / `{key}` for indexing into the
19724                    // dereferenced array/hash (`${$ar}[1]`, `${$hr}{k}`), and arrow
19725                    // chains thereafter.
19726                    base = self.interp_chain_subscripts(&chars, &mut i, base, line);
19727                    parts.push(StringPart::Expr(base));
19728                } else if chars[i] == '^' {
19729                    // `$^V`, `$^O`, … — name stored as `^V`, `^O`, … (see [`Interpreter::get_special_var`]).
19730                    let mut name = String::from("^");
19731                    i += 1;
19732                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
19733                        name.push(chars[i]);
19734                        i += 1;
19735                    }
19736                    if i < chars.len() && chars[i] == '{' {
19737                        i += 1; // skip {
19738                        let mut key = String::new();
19739                        let mut depth = 1;
19740                        while i < chars.len() && depth > 0 {
19741                            if chars[i] == '{' {
19742                                depth += 1;
19743                            } else if chars[i] == '}' {
19744                                depth -= 1;
19745                                if depth == 0 {
19746                                    break;
19747                                }
19748                            }
19749                            key.push(chars[i]);
19750                            i += 1;
19751                        }
19752                        if i < chars.len() {
19753                            i += 1;
19754                        }
19755                        let key_expr = if let Some(rest) = key.strip_prefix('$') {
19756                            Expr {
19757                                kind: ExprKind::ScalarVar(rest.to_string()),
19758                                line,
19759                            }
19760                        } else {
19761                            Expr {
19762                                kind: ExprKind::String(key),
19763                                line,
19764                            }
19765                        };
19766                        parts.push(StringPart::Expr(Expr {
19767                            kind: ExprKind::HashElement {
19768                                hash: name,
19769                                key: Box::new(key_expr),
19770                            },
19771                            line,
19772                        }));
19773                    } else if i < chars.len() && chars[i] == '[' {
19774                        i += 1;
19775                        let mut idx_str = String::new();
19776                        while i < chars.len() && chars[i] != ']' {
19777                            idx_str.push(chars[i]);
19778                            i += 1;
19779                        }
19780                        if i < chars.len() {
19781                            i += 1;
19782                        }
19783                        let idx_expr = if let Some(rest) = idx_str.strip_prefix('$') {
19784                            Expr {
19785                                kind: ExprKind::ScalarVar(rest.to_string()),
19786                                line,
19787                            }
19788                        } else if let Ok(n) = idx_str.parse::<i64>() {
19789                            Expr {
19790                                kind: ExprKind::Integer(n),
19791                                line,
19792                            }
19793                        } else {
19794                            Expr {
19795                                kind: ExprKind::String(idx_str),
19796                                line,
19797                            }
19798                        };
19799                        parts.push(StringPart::Expr(Expr {
19800                            kind: ExprKind::ArrayElement {
19801                                array: name,
19802                                index: Box::new(idx_expr),
19803                            },
19804                            line,
19805                        }));
19806                    } else {
19807                        self.no_interop_check_scalar_var_name(&name, line)?;
19808                        parts.push(StringPart::ScalarVar(name));
19809                    }
19810                } else if chars[i].is_alphabetic() || chars[i] == '_' {
19811                    let mut name = String::new();
19812                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
19813                        name.push(chars[i]);
19814                        i += 1;
19815                    }
19816                    // Package-qualified names: `$Foo::x`, `$Foo::Bar::baz`. Mirror
19817                    // the `$#Foo::a` continuation logic. Without this, `"$Foo::x"`
19818                    // captures only `Foo` and leaves `::x` as literal text — the
19819                    // interpolation reads bare `$Foo`, which is undef.
19820                    while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
19821                        name.push_str("::");
19822                        i += 2;
19823                        while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
19824                            name.push(chars[i]);
19825                            i += 1;
19826                        }
19827                    }
19828                    // `"$main::!"`, `"$main::@"`, `"$main::?"` — stryke
19829                    // implements the Perl docs faithfully ("All
19830                    // punctuation variables like $_ reside in main"):
19831                    // after the `::` chain stops, accept a single
19832                    // punctuation / digit char (or `^LETTER`) as the
19833                    // leaf so the interp sees one ScalarVar token.
19834                    // The runtime canonicalizes `main::PUNCT` →
19835                    // `PUNCT`.
19836                    //
19837                    // Disabled in `--compat`: Perl 5.42 doesn't parse
19838                    // `$main::!` as a var; the trailing punct is left
19839                    // as literal string text. Match that behavior in
19840                    // compat mode.
19841                    if name.ends_with("::") && i < chars.len() && !crate::compat_mode() {
19842                        if chars[i] == '^'
19843                            && i + 1 < chars.len()
19844                            && chars[i + 1].is_ascii_alphabetic()
19845                        {
19846                            name.push('^');
19847                            name.push(chars[i + 1]);
19848                            i += 2;
19849                        } else if "!@$&*+;',\"\\|?/<>.0123456789~%-=()[]{}".contains(chars[i]) {
19850                            name.push(chars[i]);
19851                            i += 1;
19852                        }
19853                    }
19854                    // `$_<`, `$_<<`, … — outer topic (stryke extension). Also
19855                    // `$_N<`, `$_N<<` for positional aliases. And the indexed
19856                    // shortcut `$_<N` ≡ `$_<<<...<` (N chevrons), so `"$_<3"`
19857                    // and `"$_<<<"` interpolate identically.
19858                    let is_topic_slot = name == "_"
19859                        || (name.len() > 1
19860                            && name.starts_with('_')
19861                            && name[1..].bytes().all(|b| b.is_ascii_digit()));
19862                    if is_topic_slot {
19863                        // Try indexed-ascent first: `<` immediately followed by digits.
19864                        let try_indexed = chars.get(i) == Some(&'<')
19865                            && chars.get(i + 1).is_some_and(|c| c.is_ascii_digit());
19866                        let mut handled_indexed = false;
19867                        if try_indexed {
19868                            let mut j = i + 1;
19869                            while j < chars.len() && chars[j].is_ascii_digit() {
19870                                j += 1;
19871                            }
19872                            let digits: String = chars[i + 1..j].iter().collect();
19873                            if let Ok(n) = digits.parse::<usize>() {
19874                                if n >= 1 {
19875                                    for _ in 0..n {
19876                                        name.push('<');
19877                                    }
19878                                    i = j;
19879                                    handled_indexed = true;
19880                                }
19881                            }
19882                        }
19883                        if !handled_indexed {
19884                            while i < chars.len() && chars[i] == '<' {
19885                                name.push('<');
19886                                i += 1;
19887                            }
19888                        }
19889                    }
19890                    // `--no-interop`: `$a` / `$b` are Perl-isms; reject inside
19891                    // string interpolation too. Catches both `"$a"` and `"$a[0]"`
19892                    // / `"$a{k}"` / `"$a->[0]"` because every branch below uses
19893                    // `name` to build the expression.
19894                    self.no_interop_check_scalar_var_name(&name, line)?;
19895                    // Build the base expression, then thread arrow-deref chains
19896                    // (`->[…]` / `->{…}`) onto it so things like `$ar->[2]`,
19897                    // `$href->{k}`, and chained `$x->{a}[1]->{b}` interpolate
19898                    // correctly inside double-quoted strings (Perl convention).
19899                    let mut base = if i < chars.len() && chars[i] == '{' {
19900                        // $hash{key}
19901                        i += 1; // skip {
19902                        let mut key = String::new();
19903                        let mut depth = 1;
19904                        while i < chars.len() && depth > 0 {
19905                            if chars[i] == '{' {
19906                                depth += 1;
19907                            } else if chars[i] == '}' {
19908                                depth -= 1;
19909                                if depth == 0 {
19910                                    break;
19911                                }
19912                            }
19913                            key.push(chars[i]);
19914                            i += 1;
19915                        }
19916                        if i < chars.len() {
19917                            i += 1;
19918                        } // skip }
19919                        let key_expr = if let Some(rest) = key.strip_prefix('$') {
19920                            Expr {
19921                                kind: ExprKind::ScalarVar(rest.to_string()),
19922                                line,
19923                            }
19924                        } else {
19925                            Expr {
19926                                kind: ExprKind::String(key),
19927                                line,
19928                            }
19929                        };
19930                        Expr {
19931                            kind: ExprKind::HashElement {
19932                                hash: name,
19933                                key: Box::new(key_expr),
19934                            },
19935                            line,
19936                        }
19937                    } else if i < chars.len() && chars[i] == '[' {
19938                        // $array[idx]
19939                        i += 1;
19940                        let mut idx_str = String::new();
19941                        while i < chars.len() && chars[i] != ']' {
19942                            idx_str.push(chars[i]);
19943                            i += 1;
19944                        }
19945                        if i < chars.len() {
19946                            i += 1;
19947                        }
19948                        let idx_expr = if let Some(rest) = idx_str.strip_prefix('$') {
19949                            Expr {
19950                                kind: ExprKind::ScalarVar(rest.to_string()),
19951                                line,
19952                            }
19953                        } else if let Ok(n) = idx_str.parse::<i64>() {
19954                            Expr {
19955                                kind: ExprKind::Integer(n),
19956                                line,
19957                            }
19958                        } else {
19959                            Expr {
19960                                kind: ExprKind::String(idx_str),
19961                                line,
19962                            }
19963                        };
19964                        Expr {
19965                            kind: ExprKind::ArrayElement {
19966                                array: name,
19967                                index: Box::new(idx_expr),
19968                            },
19969                            line,
19970                        }
19971                    } else {
19972                        // Bare $name — defer to the chain-extension loop below.
19973                        Expr {
19974                            kind: ExprKind::ScalarVar(name),
19975                            line,
19976                        }
19977                    };
19978
19979                    // Chain `->[…]` / `->{…}` AND adjacent `[…]` / `{…}` — Perl
19980                    // implies `->` between consecutive subscripts (`$m[1][2]`
19981                    // ≡ `$m[1]->[2]`).  See `interp_chain_subscripts`.
19982                    base = self.interp_chain_subscripts(&chars, &mut i, base, line);
19983                    parts.push(StringPart::Expr(base));
19984                } else if chars[i].is_ascii_digit() {
19985                    // $0 (program name), $1…$n (regexp captures). Perl disallows $01, $02, …
19986                    if chars[i] == '0' {
19987                        i += 1;
19988                        if i < chars.len() && chars[i].is_ascii_digit() {
19989                            return Err(self.syntax_err(
19990                                "Numeric variables with more than one digit may not start with '0'",
19991                                line,
19992                            ));
19993                        }
19994                        parts.push(StringPart::ScalarVar("0".into()));
19995                    } else {
19996                        let start = i;
19997                        while i < chars.len() && chars[i].is_ascii_digit() {
19998                            i += 1;
19999                        }
20000                        parts.push(StringPart::ScalarVar(chars[start..i].iter().collect()));
20001                    }
20002                } else {
20003                    let c = chars[i];
20004                    let probe = c.to_string();
20005                    // `&` is the regex-match special var — semantically symmetric with
20006                    // backtick (`$``) prematch and apostrophe (`$'`) postmatch which
20007                    // are already handled here. `is_special_scalar_name_for_get` doesn't
20008                    // currently list `&`/`'`/`` ` `` (those have separate runtime paths
20009                    // for set/clear under regex updates), so we add them inline.
20010                    if VMHelper::is_special_scalar_name_for_get(&probe)
20011                        || matches!(c, '\'' | '`' | '&')
20012                    {
20013                        i += 1;
20014                        // Check for hash element access: `$+{key}`, `$-{key}`, etc.
20015                        if i < chars.len() && chars[i] == '{' {
20016                            i += 1; // skip {
20017                            let mut key = String::new();
20018                            let mut depth = 1;
20019                            while i < chars.len() && depth > 0 {
20020                                if chars[i] == '{' {
20021                                    depth += 1;
20022                                } else if chars[i] == '}' {
20023                                    depth -= 1;
20024                                    if depth == 0 {
20025                                        break;
20026                                    }
20027                                }
20028                                key.push(chars[i]);
20029                                i += 1;
20030                            }
20031                            if i < chars.len() {
20032                                i += 1;
20033                            } // skip }
20034                            let key_expr = if let Some(rest) = key.strip_prefix('$') {
20035                                Expr {
20036                                    kind: ExprKind::ScalarVar(rest.to_string()),
20037                                    line,
20038                                }
20039                            } else {
20040                                Expr {
20041                                    kind: ExprKind::String(key),
20042                                    line,
20043                                }
20044                            };
20045                            let mut base = Expr {
20046                                kind: ExprKind::HashElement {
20047                                    hash: probe,
20048                                    key: Box::new(key_expr),
20049                                },
20050                                line,
20051                            };
20052                            base = self.interp_chain_subscripts(&chars, &mut i, base, line);
20053                            parts.push(StringPart::Expr(base));
20054                        } else {
20055                            // Check for arrow deref chain: `$@->{key}`, etc.
20056                            let mut base = Expr {
20057                                kind: ExprKind::ScalarVar(probe),
20058                                line,
20059                            };
20060                            base = self.interp_chain_subscripts(&chars, &mut i, base, line);
20061                            if matches!(base.kind, ExprKind::ScalarVar(_)) {
20062                                // No chain extension — use the simpler ScalarVar part
20063                                if let ExprKind::ScalarVar(name) = base.kind {
20064                                    self.no_interop_check_scalar_var_name(&name, line)?;
20065                                    parts.push(StringPart::ScalarVar(name));
20066                                }
20067                            } else {
20068                                parts.push(StringPart::Expr(base));
20069                            }
20070                        }
20071                    } else {
20072                        literal.push('$');
20073                        literal.push(c);
20074                        i += 1;
20075                    }
20076                }
20077            } else if chars[i] == '@' && i + 1 < chars.len() {
20078                let next = chars[i + 1];
20079                // `@$aref` / `@${expr}` — array dereference in interpolation (Perl `"@$r"` → elements of @$r).
20080                if next == '$' {
20081                    if !literal.is_empty() {
20082                        parts.push(StringPart::Literal(std::mem::take(&mut literal)));
20083                    }
20084                    i += 1; // past `@`
20085                    debug_assert_eq!(chars[i], '$');
20086                    i += 1; // past `$`
20087                    while i < chars.len() && chars[i].is_whitespace() {
20088                        i += 1;
20089                    }
20090                    if i >= chars.len() {
20091                        return Err(self.syntax_err(
20092                            "Expected variable or block after `@$` in double-quoted string",
20093                            line,
20094                        ));
20095                    }
20096                    let inner_expr = if chars[i] == '{' {
20097                        i += 1;
20098                        let start = i;
20099                        let mut depth = 1usize;
20100                        while i < chars.len() && depth > 0 {
20101                            match chars[i] {
20102                                '{' => depth += 1,
20103                                '}' => {
20104                                    depth -= 1;
20105                                    if depth == 0 {
20106                                        break;
20107                                    }
20108                                }
20109                                _ => {}
20110                            }
20111                            i += 1;
20112                        }
20113                        if depth != 0 {
20114                            return Err(self.syntax_err(
20115                                "Unterminated `${ ... }` after `@` in double-quoted string",
20116                                line,
20117                            ));
20118                        }
20119                        let inner: String = chars[start..i].iter().collect();
20120                        i += 1; // closing `}`
20121                        parse_expression_from_str(inner.trim(), "-e")?
20122                    } else {
20123                        let mut name = String::new();
20124                        if chars[i] == '^' {
20125                            name.push('^');
20126                            i += 1;
20127                            while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_')
20128                            {
20129                                name.push(chars[i]);
20130                                i += 1;
20131                            }
20132                        } else {
20133                            while i < chars.len()
20134                                && (chars[i].is_alphanumeric()
20135                                    || chars[i] == '_'
20136                                    || chars[i] == ':')
20137                            {
20138                                name.push(chars[i]);
20139                                i += 1;
20140                            }
20141                            while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
20142                                name.push_str("::");
20143                                i += 2;
20144                                while i < chars.len()
20145                                    && (chars[i].is_alphanumeric() || chars[i] == '_')
20146                                {
20147                                    name.push(chars[i]);
20148                                    i += 1;
20149                                }
20150                            }
20151                        }
20152                        if name.is_empty() {
20153                            return Err(self.syntax_err(
20154                                "Expected identifier after `@$` in double-quoted string",
20155                                line,
20156                            ));
20157                        }
20158                        Expr {
20159                            kind: ExprKind::ScalarVar(name),
20160                            line,
20161                        }
20162                    };
20163                    parts.push(StringPart::Expr(Expr {
20164                        kind: ExprKind::Deref {
20165                            expr: Box::new(inner_expr),
20166                            kind: Sigil::Array,
20167                        },
20168                        line,
20169                    }));
20170                    continue 'istr;
20171                }
20172                if next == '{' {
20173                    if !literal.is_empty() {
20174                        parts.push(StringPart::Literal(std::mem::take(&mut literal)));
20175                    }
20176                    i += 2; // `@{`
20177                    let start = i;
20178                    let mut depth = 1usize;
20179                    while i < chars.len() && depth > 0 {
20180                        match chars[i] {
20181                            '{' => depth += 1,
20182                            '}' => {
20183                                depth -= 1;
20184                                if depth == 0 {
20185                                    break;
20186                                }
20187                            }
20188                            _ => {}
20189                        }
20190                        i += 1;
20191                    }
20192                    if depth != 0 {
20193                        return Err(
20194                            self.syntax_err("Unterminated @{ ... } in double-quoted string", line)
20195                        );
20196                    }
20197                    let inner: String = chars[start..i].iter().collect();
20198                    i += 1; // closing `}`
20199                    let inner_expr = parse_expression_from_str(inner.trim(), "-e")?;
20200                    parts.push(StringPart::Expr(Expr {
20201                        kind: ExprKind::Deref {
20202                            expr: Box::new(inner_expr),
20203                            kind: Sigil::Array,
20204                        },
20205                        line,
20206                    }));
20207                    continue 'istr;
20208                }
20209                if !(next.is_alphabetic() || next == '_' || next == '+' || next == '-') {
20210                    literal.push(chars[i]);
20211                    i += 1;
20212                } else {
20213                    if !literal.is_empty() {
20214                        parts.push(StringPart::Literal(std::mem::take(&mut literal)));
20215                    }
20216                    i += 1;
20217                    let mut name = String::new();
20218                    if i < chars.len() && (chars[i] == '+' || chars[i] == '-') {
20219                        name.push(chars[i]);
20220                        i += 1;
20221                    } else {
20222                        while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
20223                            name.push(chars[i]);
20224                            i += 1;
20225                        }
20226                        while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
20227                            name.push_str("::");
20228                            i += 2;
20229                            while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_')
20230                            {
20231                                name.push(chars[i]);
20232                                i += 1;
20233                            }
20234                        }
20235                    }
20236                    if i < chars.len() && chars[i] == '[' {
20237                        i += 1;
20238                        let start_inner = i;
20239                        let mut depth = 1usize;
20240                        while i < chars.len() && depth > 0 {
20241                            match chars[i] {
20242                                '[' => depth += 1,
20243                                ']' => depth -= 1,
20244                                _ => {}
20245                            }
20246                            if depth == 0 {
20247                                let inner: String = chars[start_inner..i].iter().collect();
20248                                i += 1; // closing ]
20249                                let indices = parse_slice_indices_from_str(inner.trim(), "-e")?;
20250                                parts.push(StringPart::Expr(Expr {
20251                                    kind: ExprKind::ArraySlice {
20252                                        array: name.clone(),
20253                                        indices,
20254                                    },
20255                                    line,
20256                                }));
20257                                continue 'istr;
20258                            }
20259                            i += 1;
20260                        }
20261                        return Err(self.syntax_err(
20262                            "Unterminated [ in array slice inside quoted string",
20263                            line,
20264                        ));
20265                    }
20266                    parts.push(StringPart::ArrayVar(name));
20267                }
20268            } else if chars[i] == '#'
20269                && i + 1 < chars.len()
20270                && chars[i + 1] == '{'
20271                && !crate::compat_mode()
20272            {
20273                // #{expr} — Ruby-style expression interpolation (stryke extension).
20274                if !literal.is_empty() {
20275                    parts.push(StringPart::Literal(std::mem::take(&mut literal)));
20276                }
20277                i += 2; // skip `#{`
20278                let mut inner = String::new();
20279                let mut depth = 1usize;
20280                while i < chars.len() && depth > 0 {
20281                    match chars[i] {
20282                        '{' => depth += 1,
20283                        '}' => {
20284                            depth -= 1;
20285                            if depth == 0 {
20286                                break;
20287                            }
20288                        }
20289                        _ => {}
20290                    }
20291                    inner.push(chars[i]);
20292                    i += 1;
20293                }
20294                if i < chars.len() {
20295                    i += 1; // skip closing `}`
20296                }
20297                let expr = parse_block_from_str(inner.trim(), "-e", line)?;
20298                parts.push(StringPart::Expr(expr));
20299            } else {
20300                literal.push(chars[i]);
20301                i += 1;
20302            }
20303        }
20304        if !literal.is_empty() {
20305            parts.push(StringPart::Literal(literal));
20306        }
20307
20308        if parts.len() == 1 {
20309            if let StringPart::Literal(s) = &parts[0] {
20310                return Ok(Expr {
20311                    kind: ExprKind::String(s.clone()),
20312                    line,
20313                });
20314            }
20315        }
20316        if parts.is_empty() {
20317            return Ok(Expr {
20318                kind: ExprKind::String(String::new()),
20319                line,
20320            });
20321        }
20322
20323        Ok(Expr {
20324            kind: ExprKind::InterpolatedString(parts),
20325            line,
20326        })
20327    }
20328
20329    fn expr_to_overload_key(&self, e: &Expr) -> StrykeResult<String> {
20330        match &e.kind {
20331            ExprKind::String(s) => Ok(s.clone()),
20332            _ => Err(self.syntax_err(
20333                "overload key must be a string literal (e.g. '\"\"' or '+')",
20334                e.line,
20335            )),
20336        }
20337    }
20338
20339    fn expr_to_overload_sub(&mut self, e: &Expr) -> StrykeResult<String> {
20340        match &e.kind {
20341            ExprKind::String(s) => Ok(s.clone()),
20342            ExprKind::Integer(n) => Ok(n.to_string()),
20343            ExprKind::SubroutineRef(s) | ExprKind::SubroutineCodeRef(s) => Ok(s.clone()),
20344            // Anonymous sub: `use overload "+" => sub { ... };` — promote the
20345            // anon body into a synthetic top-level SubDecl so the overload
20346            // table can hold the name like the named-sub case. (PARITY-012)
20347            ExprKind::CodeRef { params, body } => {
20348                let id = self.next_overload_anon_id;
20349                self.next_overload_anon_id = self.next_overload_anon_id.saturating_add(1);
20350                let name = format!("__overload_anon_{}", id);
20351                self.pending_synthetic_subs.push(Statement {
20352                    label: None,
20353                    kind: StmtKind::SubDecl {
20354                        name: name.clone(),
20355                        params: params.clone(),
20356                        body: body.clone(),
20357                        prototype: None,
20358                    },
20359                    line: e.line,
20360                });
20361                Ok(name)
20362            }
20363            _ => Err(self.syntax_err(
20364                "overload handler must be a string literal, number (e.g. fallback => 1), or \\&subname (method in current package)",
20365                e.line,
20366            )),
20367        }
20368    }
20369}
20370
20371fn merge_expr_list(parts: Vec<Expr>) -> Expr {
20372    if parts.len() == 1 {
20373        parts.into_iter().next().unwrap()
20374    } else {
20375        let line = parts.first().map(|e| e.line).unwrap_or(0);
20376        Expr {
20377            kind: ExprKind::List(parts),
20378            line,
20379        }
20380    }
20381}
20382
20383/// Parse a single expression from `s` (e.g. contents of `@{ ... }` inside a double-quoted string).
20384pub fn parse_expression_from_str(s: &str, file: &str) -> StrykeResult<Expr> {
20385    let mut lexer = Lexer::new_with_file(s, file);
20386    let tokens = lexer.tokenize()?;
20387    let mut parser = Parser::new_with_file(tokens, file);
20388    let e = parser.parse_expression()?;
20389    if !parser.at_eof() {
20390        return Err(parser.syntax_err(
20391            "Extra tokens in embedded string expression",
20392            parser.peek_line(),
20393        ));
20394    }
20395    Ok(e)
20396}
20397
20398/// Parse a statement list from `s` and wrap as `do { ... }` (for `#{...}` interpolation).
20399pub fn parse_block_from_str(s: &str, file: &str, line: usize) -> StrykeResult<Expr> {
20400    let mut lexer = Lexer::new_with_file(s, file);
20401    let tokens = lexer.tokenize()?;
20402    let mut parser = Parser::new_with_file(tokens, file);
20403    let stmts = parser.parse_statements()?;
20404    let inner_line = stmts.first().map(|st| st.line).unwrap_or(line);
20405    let inner = Expr {
20406        kind: ExprKind::CodeRef {
20407            params: vec![],
20408            body: stmts,
20409        },
20410        line: inner_line,
20411    };
20412    Ok(Expr {
20413        kind: ExprKind::Do(Box::new(inner)),
20414        line,
20415    })
20416}
20417
20418/// Comma-separated expressions on a `format` value line (below a picture line).
20419/// Parse `[ ... ]` contents for `@a[...]` (same rules as `parse_arg_list` / comma-separated indices).
20420pub fn parse_slice_indices_from_str(s: &str, file: &str) -> StrykeResult<Vec<Expr>> {
20421    let mut lexer = Lexer::new_with_file(s, file);
20422    let tokens = lexer.tokenize()?;
20423    let mut parser = Parser::new_with_file(tokens, file);
20424    parser.parse_arg_list()
20425}
20426/// `parse_format_value_line` — see implementation.
20427pub fn parse_format_value_line(line: &str) -> StrykeResult<Vec<Expr>> {
20428    let trimmed = line.trim();
20429    if trimmed.is_empty() {
20430        return Ok(vec![]);
20431    }
20432    let mut lexer = Lexer::new(trimmed);
20433    let tokens = lexer.tokenize()?;
20434    let mut parser = Parser::new(tokens);
20435    let mut exprs = Vec::new();
20436    loop {
20437        if parser.at_eof() {
20438            break;
20439        }
20440        // Assignment-level expressions so `a, b` yields two fields (not one comma list).
20441        exprs.push(parser.parse_assign_expr()?);
20442        if parser.eat(&Token::Comma) {
20443            continue;
20444        }
20445        if !parser.at_eof() {
20446            return Err(parser.syntax_err("Extra tokens in format value line", parser.peek_line()));
20447        }
20448        break;
20449    }
20450    Ok(exprs)
20451}
20452
20453#[cfg(test)]
20454mod tests {
20455    use super::*;
20456
20457    fn parse_ok(code: &str) -> Program {
20458        let mut lexer = Lexer::new(code);
20459        let tokens = lexer.tokenize().expect("tokenize");
20460        let mut parser = Parser::new(tokens);
20461        parser.parse_program().expect("parse")
20462    }
20463
20464    fn parse_err(code: &str) -> String {
20465        let mut lexer = Lexer::new(code);
20466        let tokens = match lexer.tokenize() {
20467            Ok(t) => t,
20468            Err(e) => return e.message,
20469        };
20470        let mut parser = Parser::new(tokens);
20471        parser.parse_program().unwrap_err().message
20472    }
20473
20474    #[test]
20475    fn parse_empty_program() {
20476        let p = parse_ok("");
20477        assert!(p.statements.is_empty());
20478    }
20479
20480    #[test]
20481    fn parse_semicolons_only() {
20482        let p = parse_ok(";;");
20483        assert!(p.statements.len() <= 3);
20484    }
20485
20486    #[test]
20487    fn parse_simple_scalar_assignment() {
20488        let p = parse_ok("$x = 1");
20489        assert_eq!(p.statements.len(), 1);
20490    }
20491
20492    #[test]
20493    fn parse_simple_array_assignment() {
20494        let p = parse_ok("@arr = (1, 2, 3)");
20495        assert_eq!(p.statements.len(), 1);
20496    }
20497
20498    #[test]
20499    fn parse_simple_hash_assignment() {
20500        let p = parse_ok("%h = (a => 1, b => 2)");
20501        assert_eq!(p.statements.len(), 1);
20502    }
20503
20504    #[test]
20505    fn parse_subroutine_decl() {
20506        let p = parse_ok("fn foo { 1 }");
20507        assert_eq!(p.statements.len(), 1);
20508        match &p.statements[0].kind {
20509            StmtKind::SubDecl { name, .. } => assert_eq!(name, "foo"),
20510            _ => panic!("expected SubDecl"),
20511        }
20512    }
20513
20514    #[test]
20515    fn parse_class_method_expr_body_shorthand() {
20516        let p = parse_ok("class X { fn adg = \"\" }");
20517        match &p.statements[0].kind {
20518            StmtKind::ClassDecl { def } => {
20519                let m = def.method("adg").expect("adg method");
20520                let body = m.body.as_ref().expect("body");
20521                assert_eq!(body.len(), 1);
20522                match &body[0].kind {
20523                    StmtKind::Expression(e) => match &e.kind {
20524                        ExprKind::String(s) => assert!(s.is_empty()),
20525                        _ => panic!("expected string expr, got {:?}", e.kind),
20526                    },
20527                    _ => panic!("expected expression stmt"),
20528                }
20529            }
20530            _ => panic!("expected ClassDecl"),
20531        }
20532    }
20533
20534    #[test]
20535    fn parse_named_fn_eq_shorthand_with_sig() {
20536        let p = parse_ok("fn add_one($x) = $x + 1");
20537        match &p.statements[0].kind {
20538            StmtKind::SubDecl {
20539                name, params, body, ..
20540            } => {
20541                assert_eq!(name, "add_one");
20542                assert_eq!(params.len(), 1);
20543                assert_eq!(body.len(), 1);
20544            }
20545            _ => panic!("expected SubDecl"),
20546        }
20547    }
20548
20549    #[test]
20550    fn parse_anon_fn_eq_shorthand_with_sig() {
20551        let p = parse_ok("my $f = fn($x) = 23");
20552        match &p.statements[0].kind {
20553            StmtKind::My(decls) => {
20554                let init = decls[0].initializer.as_ref().expect("initializer");
20555                match &init.kind {
20556                    ExprKind::CodeRef { params, body } => {
20557                        assert_eq!(params.len(), 1);
20558                        assert_eq!(body.len(), 1);
20559                    }
20560                    _ => panic!("expected CodeRef"),
20561                }
20562            }
20563            _ => panic!("expected My"),
20564        }
20565    }
20566
20567    #[test]
20568    fn parse_struct_method_eq_shorthand() {
20569        let p = parse_ok("struct S { fn double($a) = $a * 2 }");
20570        match &p.statements[0].kind {
20571            StmtKind::StructDecl { def } => {
20572                assert_eq!(def.methods.len(), 1);
20573                assert_eq!(def.methods[0].name, "double");
20574                assert_eq!(def.methods[0].body.len(), 1);
20575            }
20576            _ => panic!("expected StructDecl"),
20577        }
20578    }
20579
20580    #[test]
20581    fn parse_trait_method_eq_shorthand() {
20582        let p = parse_ok("trait T { fn k = 0 }");
20583        match &p.statements[0].kind {
20584            StmtKind::TraitDecl { def } => {
20585                let m = def.method("k").expect("k");
20586                let body = m.body.as_ref().expect("default body");
20587                assert_eq!(body.len(), 1);
20588            }
20589            _ => panic!("expected TraitDecl"),
20590        }
20591    }
20592
20593    #[test]
20594    fn parse_fn_eq_shorthand_rejects_top_level_comma() {
20595        let msg = parse_err("fn z = 1, 2");
20596        assert!(
20597            msg.contains("single expression") || msg.contains("comma"),
20598            "{}",
20599            msg
20600        );
20601    }
20602
20603    #[test]
20604    fn parse_subroutine_with_prototype() {
20605        let p = parse_ok("fn foo ($$) { 1 }");
20606        assert_eq!(p.statements.len(), 1);
20607        match &p.statements[0].kind {
20608            StmtKind::SubDecl { prototype, .. } => {
20609                assert!(prototype.is_some());
20610            }
20611            _ => panic!("expected SubDecl"),
20612        }
20613    }
20614
20615    #[test]
20616    fn parse_anonymous_fn() {
20617        let p = parse_ok("my $f = fn { 1 }");
20618        assert_eq!(p.statements.len(), 1);
20619    }
20620
20621    #[test]
20622    fn parse_if_statement() {
20623        let p = parse_ok("if (1) { 2 }");
20624        assert_eq!(p.statements.len(), 1);
20625        matches!(&p.statements[0].kind, StmtKind::If { .. });
20626    }
20627
20628    #[test]
20629    fn parse_if_elsif_else() {
20630        let p = parse_ok("if (0) { 1 } elsif (1) { 2 } else { 3 }");
20631        assert_eq!(p.statements.len(), 1);
20632    }
20633
20634    #[test]
20635    fn parse_unless_statement() {
20636        let p = parse_ok("unless (0) { 1 }");
20637        assert_eq!(p.statements.len(), 1);
20638    }
20639
20640    #[test]
20641    fn parse_while_loop() {
20642        let p = parse_ok("while ($x) { $x-- }");
20643        assert_eq!(p.statements.len(), 1);
20644    }
20645
20646    #[test]
20647    fn parse_until_loop() {
20648        let p = parse_ok("until ($x) { $x++ }");
20649        assert_eq!(p.statements.len(), 1);
20650    }
20651
20652    #[test]
20653    fn parse_for_c_style() {
20654        let p = parse_ok("for (my $i=0; $i<10; $i++) { 1 }");
20655        assert_eq!(p.statements.len(), 1);
20656    }
20657
20658    #[test]
20659    fn parse_foreach_loop() {
20660        let p = parse_ok("foreach my $x (@arr) { 1 }");
20661        assert_eq!(p.statements.len(), 1);
20662    }
20663
20664    #[test]
20665    fn parse_loop_with_label() {
20666        let p = parse_ok("OUTER: for my $i (1..10) { last OUTER }");
20667        assert_eq!(p.statements.len(), 1);
20668        assert_eq!(p.statements[0].label.as_deref(), Some("OUTER"));
20669    }
20670
20671    #[test]
20672    fn parse_begin_block() {
20673        let p = parse_ok("BEGIN { 1 }");
20674        assert_eq!(p.statements.len(), 1);
20675        matches!(&p.statements[0].kind, StmtKind::Begin(_));
20676    }
20677
20678    #[test]
20679    fn parse_end_block() {
20680        let p = parse_ok("END { 1 }");
20681        assert_eq!(p.statements.len(), 1);
20682        matches!(&p.statements[0].kind, StmtKind::End(_));
20683    }
20684
20685    #[test]
20686    fn parse_package_statement() {
20687        let p = parse_ok("package Foo::Bar");
20688        assert_eq!(p.statements.len(), 1);
20689        match &p.statements[0].kind {
20690            StmtKind::Package { name } => assert_eq!(name, "Foo::Bar"),
20691            _ => panic!("expected Package"),
20692        }
20693    }
20694
20695    #[test]
20696    fn parse_use_statement() {
20697        let p = parse_ok("use strict");
20698        assert_eq!(p.statements.len(), 1);
20699    }
20700
20701    #[test]
20702    fn parse_no_statement() {
20703        let p = parse_ok("no warnings");
20704        assert_eq!(p.statements.len(), 1);
20705    }
20706
20707    #[test]
20708    fn parse_require_bareword() {
20709        let p = parse_ok("require Foo::Bar");
20710        assert_eq!(p.statements.len(), 1);
20711    }
20712
20713    #[test]
20714    fn parse_require_string() {
20715        let p = parse_ok(r#"require "foo.pl""#);
20716        assert_eq!(p.statements.len(), 1);
20717    }
20718
20719    #[test]
20720    fn parse_eval_block() {
20721        let p = parse_ok("eval { 1 }");
20722        assert_eq!(p.statements.len(), 1);
20723    }
20724
20725    #[test]
20726    fn parse_eval_string() {
20727        let p = parse_ok(r#"eval "1 + 2""#);
20728        assert_eq!(p.statements.len(), 1);
20729    }
20730
20731    #[test]
20732    fn parse_qw_word_list() {
20733        let p = parse_ok("my @a = qw(foo bar baz)");
20734        assert_eq!(p.statements.len(), 1);
20735    }
20736
20737    #[test]
20738    fn parse_q_string() {
20739        let p = parse_ok("my $s = q{hello}");
20740        assert_eq!(p.statements.len(), 1);
20741    }
20742
20743    #[test]
20744    fn parse_qq_string() {
20745        let p = parse_ok(r#"my $s = qq(hello $x)"#);
20746        assert_eq!(p.statements.len(), 1);
20747    }
20748
20749    #[test]
20750    fn parse_regex_match() {
20751        let p = parse_ok(r#"$x =~ /foo/"#);
20752        assert_eq!(p.statements.len(), 1);
20753    }
20754
20755    #[test]
20756    fn parse_regex_substitution() {
20757        let p = parse_ok(r#"$x =~ s/foo/bar/g"#);
20758        assert_eq!(p.statements.len(), 1);
20759    }
20760
20761    #[test]
20762    fn parse_transliterate() {
20763        let p = parse_ok(r#"$x =~ tr/a-z/A-Z/"#);
20764        assert_eq!(p.statements.len(), 1);
20765    }
20766
20767    #[test]
20768    fn parse_ternary_operator() {
20769        let p = parse_ok("my $x = $a ? 1 : 2");
20770        assert_eq!(p.statements.len(), 1);
20771    }
20772
20773    #[test]
20774    fn parse_arrow_method_call() {
20775        let p = parse_ok("$obj->method()");
20776        assert_eq!(p.statements.len(), 1);
20777    }
20778
20779    #[test]
20780    fn parse_arrow_deref_hash() {
20781        let p = parse_ok("$r->{key}");
20782        assert_eq!(p.statements.len(), 1);
20783    }
20784
20785    #[test]
20786    fn parse_arrow_deref_array() {
20787        let p = parse_ok("$r->[0]");
20788        assert_eq!(p.statements.len(), 1);
20789    }
20790
20791    #[test]
20792    fn parse_chained_arrow_deref() {
20793        let p = parse_ok("$r->{a}[0]{b}");
20794        assert_eq!(p.statements.len(), 1);
20795    }
20796
20797    #[test]
20798    fn parse_my_multiple_vars() {
20799        let p = parse_ok("my ($a, $b, $c) = (1, 2, 3)");
20800        assert_eq!(p.statements.len(), 1);
20801    }
20802
20803    #[test]
20804    fn parse_our_scalar() {
20805        let p = parse_ok("our $VERSION = '1.0'");
20806        assert_eq!(p.statements.len(), 1);
20807    }
20808
20809    #[test]
20810    fn parse_local_scalar() {
20811        let p = parse_ok("local $/ = undef");
20812        assert_eq!(p.statements.len(), 1);
20813    }
20814
20815    #[test]
20816    fn parse_state_variable() {
20817        let p = parse_ok("fn Test::counter { state $n = 0; $n++ }");
20818        assert_eq!(p.statements.len(), 1);
20819    }
20820
20821    #[test]
20822    fn parse_postfix_if() {
20823        let p = parse_ok("print 1 if $x");
20824        assert_eq!(p.statements.len(), 1);
20825    }
20826
20827    #[test]
20828    fn parse_postfix_unless() {
20829        let p = parse_ok("die 'error' unless $ok");
20830        assert_eq!(p.statements.len(), 1);
20831    }
20832
20833    #[test]
20834    fn parse_postfix_while() {
20835        let p = parse_ok("$x++ while $x < 10");
20836        assert_eq!(p.statements.len(), 1);
20837    }
20838
20839    #[test]
20840    fn parse_postfix_for() {
20841        let p = parse_ok("print for @arr");
20842        assert_eq!(p.statements.len(), 1);
20843    }
20844
20845    #[test]
20846    fn parse_last_next_redo() {
20847        let p = parse_ok("for (@a) { next if $_ < 0; last if $_ > 10 }");
20848        assert_eq!(p.statements.len(), 1);
20849    }
20850
20851    #[test]
20852    fn parse_return_statement() {
20853        let p = parse_ok("fn foo { return 42 }");
20854        assert_eq!(p.statements.len(), 1);
20855    }
20856
20857    #[test]
20858    fn parse_wantarray() {
20859        let p = parse_ok("fn foo { wantarray ? @a : $a }");
20860        assert_eq!(p.statements.len(), 1);
20861    }
20862
20863    #[test]
20864    fn parse_caller_builtin() {
20865        let p = parse_ok("my @c = caller");
20866        assert_eq!(p.statements.len(), 1);
20867    }
20868
20869    #[test]
20870    fn parse_ref_to_array() {
20871        let p = parse_ok("my $r = \\@arr");
20872        assert_eq!(p.statements.len(), 1);
20873    }
20874
20875    #[test]
20876    fn parse_ref_to_hash() {
20877        let p = parse_ok("my $r = \\%hash");
20878        assert_eq!(p.statements.len(), 1);
20879    }
20880
20881    #[test]
20882    fn parse_ref_to_scalar() {
20883        let p = parse_ok("my $r = \\$x");
20884        assert_eq!(p.statements.len(), 1);
20885    }
20886
20887    #[test]
20888    fn parse_deref_scalar() {
20889        let p = parse_ok("my $v = $$r");
20890        assert_eq!(p.statements.len(), 1);
20891    }
20892
20893    #[test]
20894    fn parse_deref_array() {
20895        let p = parse_ok("my @a = @$r");
20896        assert_eq!(p.statements.len(), 1);
20897    }
20898
20899    #[test]
20900    fn parse_deref_hash() {
20901        let p = parse_ok("my %h = %$r");
20902        assert_eq!(p.statements.len(), 1);
20903    }
20904
20905    #[test]
20906    fn parse_blessed_ref() {
20907        let p = parse_ok("bless $r, 'Foo'");
20908        assert_eq!(p.statements.len(), 1);
20909    }
20910
20911    #[test]
20912    fn parse_heredoc_basic() {
20913        let p = parse_ok("my $s = <<END;\nfoo\nEND");
20914        assert_eq!(p.statements.len(), 1);
20915    }
20916
20917    #[test]
20918    fn parse_heredoc_quoted() {
20919        let p = parse_ok("my $s = <<'END';\nfoo\nEND");
20920        assert_eq!(p.statements.len(), 1);
20921    }
20922
20923    #[test]
20924    fn parse_do_block() {
20925        let p = parse_ok("my $x = do { 1 + 2 }");
20926        assert_eq!(p.statements.len(), 1);
20927    }
20928
20929    #[test]
20930    fn parse_do_file() {
20931        let p = parse_ok(r#"do "foo.pl""#);
20932        assert_eq!(p.statements.len(), 1);
20933    }
20934
20935    #[test]
20936    fn parse_map_expression() {
20937        let p = parse_ok("my @b = map { $_ * 2 } @a");
20938        assert_eq!(p.statements.len(), 1);
20939    }
20940
20941    /// `on $cluster () …` must keep `()` as SOURCE (empty list), not postfix
20942    /// indirect `($cluster)()` which leaves `map` as SOURCE and breaks parsing.
20943    #[test]
20944    fn parse_dist_thread_on_scalar_empty_list_source() {
20945        let p = parse_ok("~d> on $c () map { _ * 2 }");
20946        assert_eq!(p.statements.len(), 1);
20947        let StmtKind::Expression(root) = &p.statements[0].kind else {
20948            panic!("expected Expression statement");
20949        };
20950        let ExprKind::DistReduceExpr { cluster, list, .. } = &root.kind else {
20951            panic!("expected DistReduceExpr, got {:?}", root.kind);
20952        };
20953        assert!(
20954            matches!(cluster.kind, ExprKind::ScalarVar(ref s) if s == "c"),
20955            "expected cluster $c, got {:?}",
20956            cluster.kind
20957        );
20958        assert!(
20959            matches!(list.kind, ExprKind::List(ref v) if v.is_empty()),
20960            "expected empty list source, got {:?}",
20961            list.kind
20962        );
20963    }
20964
20965    #[test]
20966    fn parse_grep_expression() {
20967        let p = parse_ok("my @b = grep { $_ > 0 } @a");
20968        assert_eq!(p.statements.len(), 1);
20969    }
20970
20971    #[test]
20972    fn parse_sort_expression() {
20973        let p = parse_ok("my @b = sort { $a <=> $b } @a");
20974        assert_eq!(p.statements.len(), 1);
20975    }
20976
20977    #[test]
20978    fn pipe_sort_does_not_swallow_next_my_decl() {
20979        // Regression: bare `|> sort` followed by `\n my $x = ...` used
20980        // to eat the next stmt as sort's argument list. After the fix,
20981        // both statements must appear in the AST.
20982        let p = parse_ok("my @s = @data |> sort\nmy $j = join(\",\", @s)");
20983        assert_eq!(
20984            p.statements.len(),
20985            2,
20986            "expected 2 stmts (sort + join decl), got {}: {:?}",
20987            p.statements.len(),
20988            p.statements
20989                .iter()
20990                .map(|s| format!("{:?}", s.kind).chars().take(60).collect::<String>())
20991                .collect::<Vec<_>>(),
20992        );
20993    }
20994
20995    #[test]
20996    fn pipe_sort_multiline_pipeline_preserves_next_decl() {
20997        // Same shape but with maps/grep stages between the source and
20998        // `sort` — mirrors the original `test_oop_inventory_threaded_pin`
20999        // bug fixture.
21000        let p = parse_ok(
21001            "my @bk = @{$inv->by_cat(\"bakery\")} |> maps { _->label() } |> sort\nmy $j = join(\"|\", @bk)",
21002        );
21003        assert_eq!(p.statements.len(), 2);
21004    }
21005
21006    #[test]
21007    fn parse_pipe_forward() {
21008        let p = parse_ok("@a |> map { $_ * 2 }");
21009        assert_eq!(p.statements.len(), 1);
21010    }
21011
21012    #[test]
21013    fn parse_expression_from_str_simple() {
21014        let e = parse_expression_from_str("$x + 1", "-e").unwrap();
21015        assert!(matches!(e.kind, ExprKind::BinOp { .. }));
21016    }
21017
21018    #[test]
21019    fn parse_expression_from_str_extra_tokens_error() {
21020        let err = parse_expression_from_str("$x; $y", "-e").unwrap_err();
21021        assert!(err.message.contains("Extra tokens"));
21022    }
21023
21024    #[test]
21025    fn parse_slice_indices_from_str_basic() {
21026        let indices = parse_slice_indices_from_str("0, 1, 2", "-e").unwrap();
21027        assert_eq!(indices.len(), 3);
21028    }
21029
21030    #[test]
21031    fn parse_format_value_line_empty() {
21032        let exprs = parse_format_value_line("").unwrap();
21033        assert!(exprs.is_empty());
21034    }
21035
21036    #[test]
21037    fn parse_format_value_line_single() {
21038        let exprs = parse_format_value_line("$x").unwrap();
21039        assert_eq!(exprs.len(), 1);
21040    }
21041
21042    #[test]
21043    fn parse_format_value_line_multiple() {
21044        let exprs = parse_format_value_line("$a, $b, $c").unwrap();
21045        assert_eq!(exprs.len(), 3);
21046    }
21047
21048    #[test]
21049    fn parse_unclosed_brace_error() {
21050        let err = parse_err("fn foo {");
21051        assert!(!err.is_empty());
21052    }
21053
21054    #[test]
21055    fn parse_unclosed_paren_error() {
21056        let err = parse_err("print (1, 2");
21057        assert!(!err.is_empty());
21058    }
21059
21060    #[test]
21061    fn parse_invalid_statement_error() {
21062        let err = parse_err("???");
21063        assert!(!err.is_empty());
21064    }
21065
21066    #[test]
21067    fn merge_expr_list_single() {
21068        let e = Expr {
21069            kind: ExprKind::Integer(1),
21070            line: 1,
21071        };
21072        let merged = merge_expr_list(vec![e.clone()]);
21073        matches!(merged.kind, ExprKind::Integer(1));
21074    }
21075
21076    #[test]
21077    fn merge_expr_list_multiple() {
21078        let e1 = Expr {
21079            kind: ExprKind::Integer(1),
21080            line: 1,
21081        };
21082        let e2 = Expr {
21083            kind: ExprKind::Integer(2),
21084            line: 1,
21085        };
21086        let merged = merge_expr_list(vec![e1, e2]);
21087        matches!(merged.kind, ExprKind::List(_));
21088    }
21089
21090    // ── --no-interop strict-mode rejections ─────────────────────────────
21091    //
21092    // `--no-interop` is the bot firewall: it rejects Perl 5 idioms the
21093    // parser would otherwise accept, forcing stryke-only spellings. Each
21094    // of these pins one rejection rule so a later refactor can't silently
21095    // accept the un-idiomatic form. We RAII the TLS flag so sibling tests
21096    // running in parallel don't see the override.
21097
21098    struct NoInteropGuard {
21099        saved: Option<bool>,
21100    }
21101    impl NoInteropGuard {
21102        fn on() -> Self {
21103            let saved = crate::no_interop_mode_tls();
21104            crate::set_no_interop_mode_tls(Some(true));
21105            Self { saved }
21106        }
21107    }
21108    impl Drop for NoInteropGuard {
21109        fn drop(&mut self) {
21110            crate::set_no_interop_mode_tls(self.saved);
21111        }
21112    }
21113
21114    #[test]
21115    fn no_interop_rejects_sub_keyword() {
21116        let _g = NoInteropGuard::on();
21117        let err = parse_err("sub foo { 1 }");
21118        assert!(
21119            err.contains("--no-interop") && err.contains("fn"),
21120            "sub rejected with fn hint: got {err:?}"
21121        );
21122    }
21123
21124    #[test]
21125    fn no_interop_rejects_say() {
21126        let _g = NoInteropGuard::on();
21127        let err = parse_err("say 1");
21128        assert!(
21129            err.contains("--no-interop") && err.contains("`p`"),
21130            "say rejected with p hint: got {err:?}"
21131        );
21132    }
21133
21134    #[test]
21135    fn no_interop_rejects_scalar_keyword() {
21136        let _g = NoInteropGuard::on();
21137        let err = parse_err("my $n = scalar @x");
21138        assert!(
21139            err.contains("--no-interop") && (err.contains("len") || err.contains("cnt")),
21140            "scalar rejected with len/cnt hint: got {err:?}"
21141        );
21142    }
21143
21144    #[test]
21145    fn no_interop_rejects_reverse() {
21146        let _g = NoInteropGuard::on();
21147        let err = parse_err("my @y = reverse @x");
21148        assert!(
21149            err.contains("--no-interop") && err.contains("rev"),
21150            "reverse rejected with rev hint: got {err:?}"
21151        );
21152    }
21153
21154    /// And the inverse — the stryke spellings (`fn`, `p`, `len`, `rev`)
21155    /// must parse cleanly under the same flag. A regression that
21156    /// accidentally rejects the canonical form is just as bad as one
21157    /// that accepts the Perl 5 form.
21158    #[test]
21159    fn no_interop_accepts_stryke_idioms() {
21160        let _g = NoInteropGuard::on();
21161        // Each of these used to be the Perl 5 form; stryke's equivalent
21162        // must parse without error.
21163        parse_ok("fn foo { 1 }");
21164        parse_ok("p 1");
21165        parse_ok("my @x = (1, 2, 3); my $n = len(@x)");
21166        parse_ok("my @x = (1, 2, 3); my @y = rev(@x)");
21167    }
21168
21169    /// `--no-interop` must NOT affect default-mode parsing. Tests run in
21170    /// parallel; the guard's Drop restores the flag, but verify the
21171    /// happy-path Perl 5 forms still parse with the flag *off* so we know
21172    /// the guard mechanics actually restore.
21173    #[test]
21174    fn default_mode_still_accepts_perl5_forms() {
21175        // No guard installed — process default (off in tests).
21176        parse_ok("sub foo { 1 }");
21177        parse_ok("say 1");
21178    }
21179
21180    /// `$a` / `$b` outside a sort or reduce block is rejected under
21181    /// `--no-interop` — stryke routes the user to `$_0` / `$_1`
21182    /// implicit-positional names which work everywhere (including in
21183    /// sort blocks). Pins the lexer arm at `parser.rs::18964` shape.
21184    #[test]
21185    fn no_interop_rejects_bare_dollar_a_dollar_b() {
21186        let _g = NoInteropGuard::on();
21187        // Bare reference outside a block context.
21188        let err = parse_err("my $x = $a + $b");
21189        assert!(
21190            err.contains("--no-interop") || err.contains("$_0") || err.contains("$_1"),
21191            "$a/$b rejected with positional hint: got {err:?}"
21192        );
21193    }
21194
21195    /// And inside a sort block too: stryke's strict mode wants
21196    /// `$_0` / `$_1` even there (per the temperature_converter and
21197    /// quicksort_no_interop examples).
21198    #[test]
21199    fn no_interop_rejects_dollar_a_inside_sort_block() {
21200        let _g = NoInteropGuard::on();
21201        let err = parse_err("my @s = sort { $a <=> $b } (3, 1, 2)");
21202        assert!(
21203            err.contains("--no-interop") || err.contains("$_0") || err.contains("$_1"),
21204            "$a in sort block rejected: got {err:?}"
21205        );
21206    }
21207
21208    /// And the inverse — `$_0` / `$_1` inside a sort block parses
21209    /// clean under `--no-interop`.
21210    #[test]
21211    fn no_interop_accepts_positional_underscore_in_sort_block() {
21212        let _g = NoInteropGuard::on();
21213        parse_ok("my @s = sort { $_0 <=> $_1 } (3, 1, 2)");
21214    }
21215
21216    // ── stryke-specific grammar pins (parse with the flag on or off) ────
21217
21218    /// Colon ranges `start:end` are stryke-canonical (not `..`).
21219    /// `1:10`, `0:N`, `-5:5` all parse.
21220    #[test]
21221    fn colon_range_parses_in_for_loop() {
21222        let _g = NoInteropGuard::on();
21223        parse_ok("for my $i (1:10) { p $i }");
21224        parse_ok("my @r = 0:99");
21225        parse_ok("my @r = -5:5");
21226    }
21227
21228    /// Postfix `if` / `unless` / `for` modifiers parse on a statement.
21229    #[test]
21230    fn postfix_statement_modifiers_parse() {
21231        let _g = NoInteropGuard::on();
21232        parse_ok("p _ for (1, 2, 3)");
21233        parse_ok("p 1 if 1");
21234        parse_ok("p 0 unless 0");
21235    }
21236
21237    /// Pipe-forward `|>` desugars at parse time — verify it accepts both
21238    /// the bare-function (`x |> f`) and the block (`x |> { _ * 2 }`) forms.
21239    #[test]
21240    fn pipe_forward_accepts_both_function_and_block_rhs() {
21241        let _g = NoInteropGuard::on();
21242        parse_ok("my $r = 1:10 |> sum");
21243        parse_ok("my @r = 1:10 |> maps { _ * 2 }");
21244        parse_ok("my @r = 1:10 |> grep { _ % 2 == 0 } |> maps { _ + 1 }");
21245    }
21246
21247    /// `_0`, `_1`, ... bareword positional params (no sigil).
21248    #[test]
21249    fn bareword_positional_underscore_n_parses_in_blocks() {
21250        let _g = NoInteropGuard::on();
21251        // `_0` / `_1` inside a sort block: the canonical strict spelling.
21252        parse_ok("my @s = sort { _0 <=> _1 } (3, 1, 2)");
21253        // And inside a maps block as the per-item topic.
21254        parse_ok("my @r = maps { _0 * 2 } (1, 2, 3)");
21255    }
21256
21257    /// Declarative types — `struct`, `enum`, `class`, `trait` — must
21258    /// parse under `--no-interop` (they're stryke extensions, not Perl 5
21259    /// shapes). Pin each via a minimal declaration.
21260    #[test]
21261    fn no_interop_accepts_struct_decl() {
21262        let _g = NoInteropGuard::on();
21263        parse_ok("struct Point { x => Int, y => Int }");
21264    }
21265
21266    #[test]
21267    fn no_interop_accepts_enum_decl() {
21268        let _g = NoInteropGuard::on();
21269        parse_ok("enum Color { Red, Green, Blue }");
21270        // Data-carrying variants use the `Variant => Type` shape, not
21271        // `Variant(Type)` — the latter is reserved for pattern
21272        // destructuring in match arms.
21273        parse_ok("enum Maybe { Just => Int, Nothing }");
21274    }
21275
21276    #[test]
21277    fn no_interop_accepts_class_decl_with_methods() {
21278        let _g = NoInteropGuard::on();
21279        parse_ok(
21280            "class Rect {\n    width: Float\n    height: Float\n\n    fn area { $self->width * $self->height }\n}",
21281        );
21282    }
21283
21284    #[test]
21285    fn no_interop_accepts_trait_decl() {
21286        let _g = NoInteropGuard::on();
21287        parse_ok("trait Greeter { fn greet; fn loudly { p \"GREET\" } }");
21288    }
21289
21290    /// Compound-assign operators — `||=` defined-or-assign, `//=` exists-
21291    /// or-assign — are stryke- and Perl-compat and should round-trip.
21292    /// These are the lazy-init idiom for hash-of-array buckets used in
21293    /// `csv_summary_no_interop.stk`.
21294    #[test]
21295    fn defined_or_assign_compound_operators_parse() {
21296        let _g = NoInteropGuard::on();
21297        parse_ok("my $h = {}; $h->{x} ||= []");
21298        parse_ok("my $v; $v //= 0");
21299    }
21300
21301    /// `+{ ... }` is the unambiguous hashref literal (vs `{ ... }` block).
21302    /// Used in the CSV demo to push rows.
21303    #[test]
21304    fn explicit_hashref_literal_parses() {
21305        let _g = NoInteropGuard::on();
21306        parse_ok("my $row = +{ region => \"north\", qty => 10 }");
21307        parse_ok("my @rows = (+{ a => 1 }, +{ a => 2 })");
21308    }
21309
21310    /// `eval { … }` block + `$@` error-variable inspection is the
21311    /// canonical exception form used in `rpn_calc_no_interop.stk`.
21312    #[test]
21313    fn eval_block_and_dollar_at_parse() {
21314        let _g = NoInteropGuard::on();
21315        parse_ok("my $r = eval { 1 + 2 }; p $@ if $@");
21316        parse_ok("eval { die \"boom\" }; p $@");
21317    }
21318
21319    /// `try { … } catch ($e) { … }` is the stryke-extension exception
21320    /// shape (Perl-5-on-steroids); must parse under `--no-interop`.
21321    #[test]
21322    fn try_catch_parses() {
21323        let _g = NoInteropGuard::on();
21324        parse_ok("try { die \"boom\" } catch ($e) { p $e }");
21325    }
21326
21327    /// File-test operators (`-d`, `-f`, `-r`, `-e`, …) are unary prefix
21328    /// ops on a filename or filehandle. Stryke inherits the Perl shape.
21329    #[test]
21330    fn file_test_operators_parse() {
21331        let _g = NoInteropGuard::on();
21332        parse_ok("p 1 if -d \"/tmp\"");
21333        parse_ok("p 2 if -f \"/etc/hosts\"");
21334        parse_ok("p 3 if -e $0");
21335        parse_ok("my $sz = -s \"/etc/hosts\"");
21336    }
21337
21338    /// `~>` (thread-first) and `~>>` (thread-last) macros — stryke's
21339    /// signature pipeline operators alongside `|>`. Pin the basic
21340    /// parsing shape.
21341    #[test]
21342    fn thread_macros_parse() {
21343        let _g = NoInteropGuard::on();
21344        parse_ok("my $r = ~> 5 +1 *2");
21345        parse_ok("my @r = ~> (1,2,3) maps { _ * 2 }");
21346    }
21347
21348    /// Hash-destructure parameter — `fn f({ a => $a, b => $b })` —
21349    /// is a stryke-extension signature shape used to unpack a hashref
21350    /// at call time.
21351    #[test]
21352    fn hash_destructure_sub_signature_parses() {
21353        let _g = NoInteropGuard::on();
21354        parse_ok("fn handle({ name => $name, qty => $qty }) { p \"$name x $qty\" }");
21355    }
21356
21357    /// Anonymous fn (`fn { ... }`) — the implicit-positional closure
21358    /// shape. Used as a first-class value: assigned, returned, passed.
21359    #[test]
21360    fn anonymous_fn_parses() {
21361        let _g = NoInteropGuard::on();
21362        parse_ok("my $f = fn { _0 * 2 }");
21363        parse_ok("my @doubled = maps { _0 * 2 } (1, 2, 3)");
21364    }
21365
21366    /// Ternary `cond ? a : b` chains for inline branching.
21367    #[test]
21368    fn ternary_and_chained_ternary_parse() {
21369        let _g = NoInteropGuard::on();
21370        parse_ok("my $r = $x > 0 ? \"pos\" : \"neg\"");
21371        parse_ok("my $r = $x > 0 ? \"pos\" : $x < 0 ? \"neg\" : \"zero\"");
21372    }
21373
21374    /// Negative indices on array slice — `@arr[-3:-1]` for the last
21375    /// three elements. Used in the parallel_primes demo.
21376    #[test]
21377    fn array_slice_with_negative_indices_parses() {
21378        let _g = NoInteropGuard::on();
21379        parse_ok("my @arr = (1,2,3,4,5); my @tail = @arr[-3:-1]");
21380        parse_ok("my @arr = (1,2,3); my @last_two = @arr[-2:]");
21381    }
21382
21383    /// `state $x` declarations (function-local persistent storage)
21384    /// used by the memoised-fib demo for the cache hash.
21385    #[test]
21386    fn state_variable_declaration_parses() {
21387        let _g = NoInteropGuard::on();
21388        // Use clearly-non-builtin fn names (`counter` / `memo` clash
21389        // with stryke builtins).
21390        parse_ok("fn my_counter { state $n = 0; $n++; $n }");
21391        parse_ok("fn my_memo($k) { state %cache; $cache{$k} //= compute($k) }");
21392    }
21393
21394    /// `our` declarations are package-globals — still legal in strict
21395    /// mode (just sub/say/scalar/reverse are rejected, not `our`).
21396    #[test]
21397    fn our_declaration_parses() {
21398        let _g = NoInteropGuard::on();
21399        parse_ok("our $VERSION = 1.0");
21400        parse_ok("our @EXPORT = (1, 2, 3)");
21401    }
21402
21403    /// Regex binding operators — `=~` (match) and `!~` (negated match).
21404    /// Used in roman_numerals_no_interop for input validation.
21405    #[test]
21406    fn regex_binding_operators_parse() {
21407        let _g = NoInteropGuard::on();
21408        parse_ok("p 1 if $s =~ /^\\d+$/");
21409        parse_ok("p 0 unless $s !~ /[A-Z]/");
21410        parse_ok("my @m = $s =~ /(\\w+)/g");
21411    }
21412
21413    /// Nested data-structure literals — hash of array of hash, the
21414    /// shape used by csv_summary_no_interop (`%by_region` is a
21415    /// hash of arrays of hashrefs).
21416    #[test]
21417    fn nested_data_structure_literals_parse() {
21418        let _g = NoInteropGuard::on();
21419        parse_ok("my %h = (a => [1, 2, 3], b => [4, 5])");
21420        parse_ok("my @rows = (+{ x => 1 }, +{ x => 2 })");
21421        parse_ok("my %grid = (cells => [+{ row => 1 }, +{ row => 2 }])");
21422    }
21423
21424    /// Anonymous fn with explicit params — `fn ($x, $y) { ... }`.
21425    /// Complements the `fn { _0 + _1 }` implicit-positional form.
21426    #[test]
21427    fn anonymous_fn_with_explicit_params_parses() {
21428        let _g = NoInteropGuard::on();
21429        parse_ok("my $add = fn ($x, $y) { $x + $y }");
21430        parse_ok("my $h = fn ($v, %opts) { p $v; p %opts }");
21431    }
21432
21433    /// `package Foo;` and `package Foo::Bar;` declarations — qualified
21434    /// namespace setup. Still legal in strict mode.
21435    #[test]
21436    fn package_declaration_parses() {
21437        let _g = NoInteropGuard::on();
21438        parse_ok("package Foo; my $x = 1");
21439        parse_ok("package Foo::Bar::Baz; our $VERSION = 0.01");
21440    }
21441
21442    /// `next` / `last` / `redo` loop-control statements (used in
21443    /// balanced_brackets / brainfuck / sieve demos to break out of
21444    /// inner loops).
21445    #[test]
21446    fn loop_control_keywords_parse() {
21447        let _g = NoInteropGuard::on();
21448        parse_ok("for my $i (1:10) { next if $i % 2; p $i }");
21449        parse_ok("while (1) { last if $done }");
21450        parse_ok("my $rerun = 0; for (1:5) { if ($rerun) { redo } }");
21451    }
21452
21453    /// Labelled loops + labelled `next` / `last`. Used when you need
21454    /// to break out of nested loops.
21455    #[test]
21456    fn labelled_loops_parse() {
21457        let _g = NoInteropGuard::on();
21458        parse_ok("OUTER: for my $i (1:10) { last OUTER if $i > 5 }");
21459        parse_ok("OUTER: for my $i (1:3) { INNER: for my $j (1:3) { next OUTER if $j > $i } }");
21460    }
21461
21462    /// String-repeat operator `x` — `\"-\" x 40` for a separator line,
21463    /// `(0) x N` for an initialized array. Both shapes appear in the
21464    /// histogram / sieve / brainfuck demos.
21465    #[test]
21466    fn string_repeat_x_operator_parses() {
21467        let _g = NoInteropGuard::on();
21468        parse_ok("my $sep = \"-\" x 40");
21469        parse_ok("my @zeros = (0) x 100");
21470        parse_ok("my $bar = \"#\" x $count");
21471    }
21472
21473    /// `chomp` / `chop` builtins — mutate-in-place string ops on the
21474    /// topic or an explicit lvalue. Used in stdin-reading scripts.
21475    #[test]
21476    fn chomp_chop_parse() {
21477        let _g = NoInteropGuard::on();
21478        parse_ok("chomp(my $line = <STDIN>)");
21479        parse_ok("my $s = \"hi\\n\"; chomp $s");
21480        parse_ok("my $t = \"hi\"; chop $t");
21481    }
21482
21483    /// Substitution operator `s/pat/repl/flags` — used by
21484    /// palindrome_no_interop to strip non-alphanumerics.
21485    #[test]
21486    fn substitution_operator_parses() {
21487        let _g = NoInteropGuard::on();
21488        parse_ok("my $s = \"abc\"; $s =~ s/b/X/");
21489        parse_ok("my $s = \"AaBb\"; $s =~ s/[a-z]//g");
21490        parse_ok("my $s = \"Hello\"; $s =~ s/(.)/\\1\\1/g");
21491    }
21492
21493    /// `sprintf` for column-aligned strings — used by every demo's
21494    /// table output.
21495    #[test]
21496    fn sprintf_parses_with_format_specs() {
21497        let _g = NoInteropGuard::on();
21498        parse_ok("my $row = sprintf(\"%-10s %5d\", \"foo\", 42)");
21499        parse_ok("p sprintf(\"%.3f ms\", 1.234)");
21500        parse_ok("p sprintf(\"%04x\", 255)");
21501    }
21502
21503    /// `<STDIN>` diamond reads — list and scalar context. Used by
21504    /// wordcount / csv_summary / anagram demos.
21505    #[test]
21506    fn diamond_stdin_reads_parse() {
21507        let _g = NoInteropGuard::on();
21508        parse_ok("my $line = <STDIN>");
21509        parse_ok("my @lines = <STDIN>");
21510        parse_ok("while (my $line = <STDIN>) { p $line }");
21511    }
21512
21513    /// Chained method calls — `$obj->foo->bar(arg)`. Used in
21514    /// build_destroy and class demos.
21515    #[test]
21516    fn chained_method_calls_parse() {
21517        let _g = NoInteropGuard::on();
21518        parse_ok("$obj->foo->bar");
21519        parse_ok("my $r = $row->{region}");
21520        parse_ok("$obj->set(1)->get");
21521    }
21522
21523    /// Hash dereference syntaxes — `%$href`, `keys %$href`,
21524    /// `$href->{key}`, `%{ expr }`. Used by set_ops_no_interop.
21525    #[test]
21526    fn hash_deref_forms_parse() {
21527        let _g = NoInteropGuard::on();
21528        parse_ok("my $h = +{a=>1}; my %copy = %$h");
21529        parse_ok("my $h = +{a=>1}; my @k = keys %$h");
21530        parse_ok("my $h = +{a=>1}; p $h->{a}");
21531        parse_ok("my $h = +{a=>1, b=>2}; p len(keys %{$h})");
21532    }
21533
21534    /// `unless` postfix on a `die` — the canonical assertion shape
21535    /// every demo uses for self-tests.
21536    #[test]
21537    fn die_unless_assertion_parses() {
21538        let _g = NoInteropGuard::on();
21539        parse_ok("die \"x must be 1\" unless 1 == 1");
21540        parse_ok("die \"empty list\" if len(@x) == 0");
21541        parse_ok("my $x = 1; die \"hi\" unless $x");
21542    }
21543
21544    /// `qw(...)` quote-words literal — the bareword-list shape used in
21545    /// the older example scripts.
21546    #[test]
21547    fn qw_literal_parses() {
21548        let _g = NoInteropGuard::on();
21549        parse_ok("my @w = qw(red green blue)");
21550        parse_ok("for my $name (qw(Alice Bob Carol)) { p $name }");
21551    }
21552
21553    /// Array slice with explicit indices — `@arr[0, 2, 4]`. Different
21554    /// from range slice `@arr[1:3]`.
21555    #[test]
21556    fn array_slice_with_explicit_indices_parses() {
21557        let _g = NoInteropGuard::on();
21558        parse_ok("my @a = (10, 20, 30, 40); my @s = @a[0, 2]");
21559        parse_ok("my @a = (10, 20, 30); my @s = @a[2, 0, 1]");
21560    }
21561
21562    /// Hash slice — `@h{'a', 'b'}` returns the list of values at keys
21563    /// 'a' and 'b'. Different sigil context than scalar hash access.
21564    #[test]
21565    fn hash_slice_with_keys_parses() {
21566        let _g = NoInteropGuard::on();
21567        parse_ok("my %h = (a=>1, b=>2, c=>3); my @v = @h{'a','c'}");
21568    }
21569
21570    /// Bitwise operators — `&`, `|`, `^`, `~`, `<<`, `>>`. Used by
21571    /// bitops_no_interop. Each binds tighter than the comparison
21572    /// operators, looser than the arithmetic ones (Perl precedence).
21573    #[test]
21574    fn bitwise_operators_parse() {
21575        let _g = NoInteropGuard::on();
21576        parse_ok("my $x = 0xAA; my $y = $x & 0x0F");
21577        parse_ok("my $x = 1; my $y = $x | 2 | 4");
21578        parse_ok("my $x = 0xFF; my $y = $x ^ 0x80");
21579        parse_ok("my $x = 0xAA; my $y = ~$x & 0xff");
21580        parse_ok("my $x = 1; my $y = $x << 4");
21581        parse_ok("my $x = 0xF0; my $y = $x >> 2");
21582    }
21583
21584    /// `0x`, `0b`, `0o` numeric literal prefixes — hex / binary /
21585    /// octal. Used freely in bitops + numeric-conversion demos.
21586    #[test]
21587    fn numeric_literal_prefixes_parse() {
21588        let _g = NoInteropGuard::on();
21589        parse_ok("my $hex = 0xCAFEBABE");
21590        parse_ok("my $bin = 0b1101");
21591        parse_ok("my $hex8 = 0xff & 0x0f");
21592    }
21593
21594    /// Negative array index — `$arr[-1]`, `$arr[-2]`. Used by
21595    /// shunting_yard (`$ops[-1]` to peek stack top).
21596    #[test]
21597    fn negative_array_index_parses() {
21598        let _g = NoInteropGuard::on();
21599        // `@a` / `@b` array names share the `$a` / `$b` reservation in
21600        // strict mode — use `@arr` instead.
21601        parse_ok("my @arr = (1, 2, 3); p $arr[-1]");
21602        parse_ok("my @stack = (10, 20, 30); p $stack[-1] if len(@stack) > 0");
21603    }
21604
21605    /// `last` / `next` / `return` as standalone statements (already
21606    /// pinned alongside `last LABEL` but not as the lone-line form).
21607    #[test]
21608    fn loop_control_standalone_parses() {
21609        let _g = NoInteropGuard::on();
21610        parse_ok("while (1) { last }");
21611        parse_ok("for my $i (1:10) { next if $i == 3 }");
21612        parse_ok("fn ret { return 42 }");
21613    }
21614
21615    /// `shift @args` / `pop @args` — common destructuring shape inside
21616    /// functions for "first arg" / "last arg" pickoff (used by morse
21617    /// to peel the mode off `@ARGV`).
21618    #[test]
21619    fn shift_pop_on_array_parses() {
21620        let _g = NoInteropGuard::on();
21621        parse_ok("my @a = (1, 2, 3); my $first = shift @a");
21622        parse_ok("my @a = (1, 2, 3); my $last  = pop @a");
21623        parse_ok("fn drop_first(@xs) { shift @xs; @xs }");
21624    }
21625
21626    /// Special internal names — `__FILE__`, `__LINE__`, `__PACKAGE__`
21627    /// — accessed by tooling and error reporting.
21628    #[test]
21629    fn special_internal_names_parse() {
21630        let _g = NoInteropGuard::on();
21631        parse_ok("p __FILE__");
21632        parse_ok("p __LINE__");
21633        parse_ok("p __PACKAGE__");
21634        parse_ok("p __FILE__ . \":\" . __LINE__");
21635    }
21636
21637    /// Nested ternary RHS with parens / chained alternatives. Used by
21638    /// the conway demo's pattern dispatch.
21639    #[test]
21640    fn deeply_nested_ternary_parses() {
21641        let _g = NoInteropGuard::on();
21642        parse_ok("my $g = ($p eq \"a\") ? 1 : ($p eq \"b\") ? 2 : ($p eq \"c\") ? 3 : 4");
21643    }
21644
21645    /// Array-of-hashref + index-into-hash subscript chain:
21646    /// `$rows[0]->{name}`, `$rows[-1]->{score}`. Used in csv_summary
21647    /// and ranking demos.
21648    #[test]
21649    fn array_of_hashref_chained_subscript_parses() {
21650        let _g = NoInteropGuard::on();
21651        parse_ok("my @rows = (+{name=>'a',sc=>1}); p $rows[0]->{name}");
21652        parse_ok("my @rows = (+{n=>10},+{n=>20}); p $rows[-1]->{n}");
21653    }
21654
21655    /// Nested `for` loops with two index variables — the 2D grid walk
21656    /// shape used in conway.
21657    #[test]
21658    fn nested_for_loops_parse() {
21659        let _g = NoInteropGuard::on();
21660        parse_ok("for my $r (0:5) { for my $c (0:5) { p \"$r,$c\" } }");
21661    }
21662
21663    /// Compound assignment `$x .= ...` (string append). Used by every
21664    /// demo that builds output strings incrementally (brainfuck,
21665    /// RLE encode, Caesar).
21666    #[test]
21667    fn dot_assign_string_append_parses() {
21668        let _g = NoInteropGuard::on();
21669        parse_ok("my $s = \"a\"; $s .= \"b\"");
21670        parse_ok("my $out = \"\"; $out .= \"x\" for (1:3)");
21671    }
21672
21673    /// `unshift @arr, $v` — push to the FRONT of an array; used by
21674    /// graph_bfs for back-tracking the path.
21675    #[test]
21676    fn unshift_parses() {
21677        let _g = NoInteropGuard::on();
21678        parse_ok("my @path = (3, 4); unshift @path, 2; unshift @path, 1");
21679        parse_ok("my @q; unshift @q, $_ for (1:5)");
21680    }
21681
21682    /// `defined` and `// 0` defined-or fallback. Used by the
21683    /// graph_bfs neighbour lookup (`$REVERSE{$ch} // 0`).
21684    #[test]
21685    fn defined_or_fallback_parses() {
21686        let _g = NoInteropGuard::on();
21687        parse_ok("my $x; my $y = $x // 0");
21688        parse_ok("my %h = (a=>1); my $v = $h{missing} // -1");
21689        parse_ok("p 1 if defined $foo");
21690    }
21691
21692    /// `for my $x (rev 0:N)` — reverse a range with the stryke `rev`
21693    /// keyword. Used by knapsack back-tracking.
21694    #[test]
21695    fn rev_over_range_parses() {
21696        let _g = NoInteropGuard::on();
21697        parse_ok("for my $i (rev 0:9) { p $i }");
21698        parse_ok("my @r = rev (1, 2, 3, 4)");
21699    }
21700
21701    /// Array deref `@$ref`, `@{$expr}`. Used freely in graph_bfs +
21702    /// knapsack to walk arrays-of-arrayrefs.
21703    #[test]
21704    fn array_deref_forms_parse() {
21705        let _g = NoInteropGuard::on();
21706        parse_ok("my $r = [1, 2, 3]; my @copy = @$r");
21707        parse_ok("my $r = [1, 2, 3]; my @copy = @{$r}");
21708        parse_ok("my @rs = ([1], [2, 3]); my @flat; push @flat, @$_ for @rs");
21709    }
21710
21711    /// Hashref via `[k]` doesn't exist — but `$ref->{k}` and `${$ref}{k}`
21712    /// are the two equivalent shapes. Pin both.
21713    #[test]
21714    fn hashref_subscript_alt_forms_parse() {
21715        let _g = NoInteropGuard::on();
21716        parse_ok("my $h = +{a=>1}; p $h->{a}");
21717        parse_ok("my $h = +{a=>1}; p ${$h}{a}");
21718    }
21719
21720    /// `abs($x)`, `int($x)`, `sqrt($x)` — common numeric builtins
21721    /// invoked as functions.
21722    #[test]
21723    fn numeric_builtins_parse() {
21724        let _g = NoInteropGuard::on();
21725        parse_ok("my $x = abs(-5)");
21726        parse_ok("my $f = int(3.7)");
21727        parse_ok("my $s = sqrt(2)");
21728        parse_ok("my $n = int($x * 100 + 0.5) / 100");
21729    }
21730
21731    /// `local $var` declaration — dynamic-scoped binding restored
21732    /// on block exit. Different from `my` (lexical) and `our`
21733    /// (package-global).
21734    #[test]
21735    fn local_declaration_parses() {
21736        let _g = NoInteropGuard::on();
21737        // Strict mode rejects `sub` — use anonymous `fn` for the scope.
21738        parse_ok("our $g = 1; (fn { local $g = 99; p $g })->()");
21739    }
21740
21741    /// `srand($seed)` and `rand($n)` — deterministic random with
21742    /// optional bound. Used by dice + histogram demos for repeatable
21743    /// CI output.
21744    #[test]
21745    fn srand_rand_parse() {
21746        let _g = NoInteropGuard::on();
21747        parse_ok("srand(42); my $r = rand(6)");
21748        parse_ok("srand(); my $r = int(rand(100))");
21749    }
21750
21751    /// `substr($s, $i, $n)` for slicing strings (2-arg + 3-arg forms).
21752    /// Used by base64 + soundex + RPN demos.
21753    #[test]
21754    fn substr_parses() {
21755        let _g = NoInteropGuard::on();
21756        parse_ok("my $s = \"hello\"; p substr($s, 0, 1)");
21757        parse_ok("my $s = \"hello\"; p substr($s, 1)");
21758        parse_ok("my $s = \"hello\"; p substr($s, -2)");
21759    }
21760
21761    /// Underscore separator in numeric literals — `1_000_000` for
21762    /// readability. Used in `pmap { ... } 1:1_000` style code.
21763    #[test]
21764    fn underscore_separators_in_numbers_parse() {
21765        let _g = NoInteropGuard::on();
21766        parse_ok("my $n = 1_000_000");
21767        parse_ok("my $r = 1_000_000 / 365");
21768        parse_ok("my $hex = 0xff_ff");
21769    }
21770
21771    /// 2D array-of-arrayref construction `[[a,b], [c,d]]` and access
21772    /// `$grid->[0]->[1]`. Used by interval_merge and dijkstra demos.
21773    #[test]
21774    fn arrayref_of_arrayref_access_parses() {
21775        let _g = NoInteropGuard::on();
21776        parse_ok("my $grid = [[1, 2], [3, 4]]");
21777        parse_ok("my $grid = [[1, 2], [3, 4]]; p $grid->[0]->[1]");
21778        parse_ok("my $grid = [[1, 2], [3, 4]]; p $grid->[1][0]");
21779    }
21780
21781    /// Mutating array element via arrow-arrow chain — `$ref->[0]->[1] = $v`.
21782    /// Used by interval_merge to extend an interval's end in place.
21783    #[test]
21784    fn arrow_chain_assignment_parses() {
21785        let _g = NoInteropGuard::on();
21786        parse_ok("my $g = [[0, 0]]; $g->[0]->[1] = 99");
21787        parse_ok("my $g = [[0, 0]]; $g->[0][1] = 99");
21788    }
21789
21790    /// Tuple destructure from arrayref element — `my ($a, $b) = @$e`.
21791    /// Pin the no-interop renames (`$a` reserved).
21792    #[test]
21793    fn tuple_destructure_from_arrayref_parses() {
21794        let _g = NoInteropGuard::on();
21795        parse_ok("my $e = [10, 20]; my ($lhs, $rhs) = @$e");
21796        parse_ok("my $e = [\"x\", 1]; my ($name, $weight) = ($e->[0], $e->[1])");
21797    }
21798
21799    /// `next unless` / `last unless` postfix on a loop body. Common
21800    /// guard shape inside the rolling-stats sliding loops.
21801    #[test]
21802    fn next_last_unless_postfix_parses() {
21803        let _g = NoInteropGuard::on();
21804        parse_ok("for my $i (1:10) { next unless $i % 2 == 0; p $i }");
21805        parse_ok("while (1) { last unless $live }");
21806    }
21807
21808    /// `keys %{ ... }` with parenthesised hash dereference — the
21809    /// shape used by deepish hash-of-hash access.
21810    #[test]
21811    fn keys_on_braced_hash_deref_parses() {
21812        let _g = NoInteropGuard::on();
21813        parse_ok("my $h = +{a=>1}; my @k = keys %{$h}");
21814        parse_ok("my $h = +{a=>+{b=>1}}; my @k = keys %{$h->{a}}");
21815    }
21816
21817    /// Namespaced fn definition — `fn Module::method($x) { ... }`.
21818    /// All UDFs in the examples/ demos use this form so future stryke
21819    /// stdlib additions don't shadow user code (or vice versa).
21820    #[test]
21821    fn namespaced_fn_decl_parses() {
21822        let _g = NoInteropGuard::on();
21823        parse_ok("fn Module::method($x) { $x * 2 }");
21824        parse_ok("fn Foo::Bar::helper { 42 }");
21825        parse_ok("fn Demo::run { p \"running\" }");
21826    }
21827
21828    /// Calling a namespaced fn — `Module::method($arg)`. Also used as
21829    /// a method on an explicit invocant in some contexts.
21830    #[test]
21831    fn namespaced_fn_call_parses() {
21832        let _g = NoInteropGuard::on();
21833        parse_ok("fn Module::add($x, $y) { $x + $y } p Module::add(2, 3)");
21834        // `caller` itself is a stryke builtin — use a namespaced caller
21835        // to avoid the clash.
21836        parse_ok("fn Foo::Bar::baz { 1 } fn Demo::main { Foo::Bar::baz() + Foo::Bar::baz() }");
21837    }
21838
21839    /// `index($haystack, $needle)` builtin — the canonical string
21840    /// substring-position lookup that KMP cross-checks against.
21841    #[test]
21842    fn index_builtin_parses() {
21843        let _g = NoInteropGuard::on();
21844        parse_ok("my $i = index(\"hello world\", \"world\")");
21845        parse_ok("my $i = index($s, $pat, 0)");
21846    }
21847
21848    /// `for my $i (rev 1:N)` — reverse iteration over a colon range.
21849    /// Already pinned in `rev_over_range_parses` but here for the
21850    /// `rev (list)` form on an array literal.
21851    #[test]
21852    fn rev_on_array_literal_parses() {
21853        let _g = NoInteropGuard::on();
21854        parse_ok("my @r = rev (1, 2, 3, 4)");
21855        parse_ok("for my $x (rev (\"a\", \"b\", \"c\")) { p $x }");
21856    }
21857
21858    /// Numeric comparison + string comparison side by side — the
21859    /// pattern used by sort blocks with secondary tie-breaking
21860    /// (numeric primary, string secondary). `$a` / `$b` are reserved
21861    /// under --no-interop; use `$_0` / `$_1` for sort comparator args.
21862    #[test]
21863    fn numeric_and_string_comparison_in_one_expr_parses() {
21864        let _g = NoInteropGuard::on();
21865        parse_ok("my $r = $_0 <=> $_1 || $name cmp $other");
21866        parse_ok("p 1 if $x == $y && $name eq \"foo\"");
21867    }
21868
21869    /// Bareword positional names — `_0`, `_1`, `_N` without sigil —
21870    /// are stryke's idiomatic spelling in code contexts. The sigil
21871    /// form (`$_0`, `$_1`) is reserved for string interpolation where
21872    /// a bareword would just be a literal substring. Pin the bareword
21873    /// shape in every common closure position.
21874    #[test]
21875    fn bareword_positional_in_sort_reduce_blocks_parses() {
21876        let _g = NoInteropGuard::on();
21877        // Sort comparator with bareword positional names.
21878        parse_ok("my @s = sort { _0 <=> _1 } (3, 1, 2)");
21879        // Reduce accumulator + element.
21880        parse_ok("my $r = (1, 2, 3) |> reduce { _0 + _1 }");
21881        // Reduce on string concat — _0 acc, _1 next.
21882        parse_ok("my $s = (\"a\", \"b\") |> reduce { _0 . _1 }");
21883    }
21884
21885    /// Bareword topic `_` inside `maps` / `grep` / `pmap` / `pgrep`
21886    /// closure bodies. Tightest form; no sigil needed.
21887    #[test]
21888    fn bareword_topic_in_maps_grep_parses() {
21889        let _g = NoInteropGuard::on();
21890        parse_ok("my @r = (1, 2, 3) |> maps { _ * 2 }");
21891        parse_ok("my @r = (1, 2, 3, 4) |> grep { _ % 2 == 0 }");
21892        parse_ok("my @r = 1:100 |> pmap { _ ** 3 }");
21893        parse_ok("my @r = 1:100 |> pgrep { _ % 7 == 0 }");
21894    }
21895
21896    /// `_` and `_1` in arithmetic expression context (no sigil).
21897    /// Inside string interpolation, only the sigil form `${_}` /
21898    /// `$_0` / `$_1` works — pin the contrast.
21899    #[test]
21900    fn bareword_vs_sigil_in_string_interp_parses() {
21901        let _g = NoInteropGuard::on();
21902        // Bareword in code: tight, idiomatic.
21903        parse_ok("my @r = (5, 10) |> maps { _ + 1 }");
21904        // Sigil in string interp: the bareword would just be the
21905        // literal characters underscore-zero, so the sigil form is
21906        // required here.
21907        parse_ok("p \"first=$_0 second=$_1\"");
21908        parse_ok("p \"got: $_\"");
21909    }
21910
21911    /// Bareword `_` topic inside a `for $_` -style postfix-for body.
21912    /// Used in Conway's life-counter — `$n += _ for @$g`.
21913    #[test]
21914    fn bareword_topic_in_postfix_for_parses() {
21915        let _g = NoInteropGuard::on();
21916        parse_ok("my $n = 0; $n += _ for (1, 2, 3, 4)");
21917        parse_ok("my %h; $h{_}++ for (\"a\", \"b\", \"a\")");
21918    }
21919
21920    /// Bareword `_` with hash-subscript on the outer side —
21921    /// `$h{_}++` is tighter than `$h{$_}++` (no sigil on key).
21922    #[test]
21923    fn bareword_topic_as_hash_key_parses() {
21924        let _g = NoInteropGuard::on();
21925        parse_ok("my %h; $h{_}++ for (\"a\", \"b\", \"a\")");
21926        parse_ok("my @arr = (1, 2, 3); my %seen; $seen{$arr[_]}++ for (0, 1, 2)");
21927    }
21928
21929    /// Hashref subscript inside a `grep` block using the bareword
21930    /// topic — the FirstU::pipe_find shape: `grep { $seen{$c[_]} == 1 }`.
21931    #[test]
21932    fn bareword_topic_inside_subscript_chain_parses() {
21933        let _g = NoInteropGuard::on();
21934        parse_ok(
21935            "my @c = (\"a\", \"b\", \"c\"); my %seen = (a => 1, b => 2, c => 1); \
21936             my @hits = grep { $seen{$c[_]} == 1 } (0, 1, 2)",
21937        );
21938    }
21939
21940    /// `ref($x)` builtin — used by flatten to discriminate arrayref
21941    /// from scalar leaves.
21942    #[test]
21943    fn ref_builtin_parses() {
21944        let _g = NoInteropGuard::on();
21945        parse_ok("my $x = [1, 2]; p ref($x)");
21946        parse_ok("my $r = +{a => 1}; p 1 if ref($r) eq \"HASH\"");
21947    }
21948
21949    /// Expression-bodied recursive `fn` — style guide rule 6. The body
21950    /// is a single ternary that re-invokes the same fn; Kadane / gcd /
21951    /// lcm demos all use this shape.
21952    #[test]
21953    fn expression_bodied_recursive_fn_parses() {
21954        let _g = NoInteropGuard::on();
21955        parse_ok("fn N::gcd = _1 == 0 ? _0 : N::gcd(_1, _0 % _1)");
21956        parse_ok("fn N::lcm = _0 * _1 / N::gcd(_0, _1)");
21957    }
21958
21959    /// Expression-bodied `fn` whose body is a `|> reduce` over the
21960    /// implicit topic — used by gcd_list / lcm_list.
21961    #[test]
21962    fn expression_bodied_pipe_reduce_parses() {
21963        let _g = NoInteropGuard::on();
21964        parse_ok(
21965            "fn N::gcd = _1 == 0 ? _0 : N::gcd(_1, _0 % _1); \
21966                  fn N::gcd_list = @{_} |> reduce { N::gcd(_0, _1) }",
21967        );
21968    }
21969
21970    /// Reduce-fold with a hashref accumulator seeded by prepending the
21971    /// init to the input — Boyer-Moore / Kadane idiom.
21972    #[test]
21973    fn reduce_fold_with_hashref_accumulator_parses() {
21974        let _g = NoInteropGuard::on();
21975        parse_ok(
21976            "my @xs = (3, 2, 3); \
21977             my $st = (+{cur => 0, best => -100}, @xs) |> reduce { \
21978                +{ cur => _1, best => _0->{best} } \
21979             }",
21980        );
21981    }
21982
21983    /// Native array-deref slicing `@$arr[$lo:$hi]` — used by
21984    /// rolling_stats to slice the input window.
21985    #[test]
21986    fn array_deref_slice_with_variable_bounds_parses() {
21987        let _g = NoInteropGuard::on();
21988        parse_ok(
21989            "my @a = (10, 20, 30, 40, 50); my $r = \\@a; \
21990             my $lo = 1; my $hi = 3; \
21991             my @s = @$r[$lo:$hi]",
21992        );
21993    }
21994
21995    /// `min(@$v[$lo:$hi])` / `max(@$v[$lo:$hi])` — rolling_stats hot
21996    /// path. Paren-less `min @$v[..]` parses wrong; pinned with parens.
21997    #[test]
21998    fn min_max_over_array_deref_slice_parses() {
21999        let _g = NoInteropGuard::on();
22000        parse_ok(
22001            "my @a = (1, 3, 2, 5); my $r = \\@a; \
22002             my $lo = 0; my $hi = 2; \
22003             my $m1 = min(@$r[$lo:$hi]); \
22004             my $m2 = max(@$r[$lo:$hi])",
22005        );
22006    }
22007
22008    /// `flat_maps` with recursive call inside the block — flatten
22009    /// demo's central idiom; verifies the recursive call inside a
22010    /// pipeline stage parses cleanly.
22011    #[test]
22012    fn flat_maps_with_recursive_call_parses() {
22013        let _g = NoInteropGuard::on();
22014        parse_ok(
22015            "fn Flat::flatten($r) = @$r |> flat_maps { \
22016                ref(_) eq \"ARRAY\" ? Flat::flatten(_) : (_) \
22017             }",
22018        );
22019    }
22020
22021    /// Inner `map { _->[$i] }` capturing outer lexical `$i` — zip's
22022    /// N-list helper. Bareword topic inside, `$i` is the lexical.
22023    #[test]
22024    fn nested_map_with_outer_lexical_capture_parses() {
22025        let _g = NoInteropGuard::on();
22026        parse_ok(
22027            "my @lists = ([1,2,3], [10,20,30]); \
22028             my @r = 0:2 |> maps { my $i = _; [map { _->[$i] } @lists] }",
22029        );
22030    }
22031
22032    /// `@{_}` deref of the topic — required when sigil-form `@$_` is
22033    /// being avoided per style guide. zip's `len(@{_})` shape.
22034    #[test]
22035    fn array_deref_of_bareword_topic_parses() {
22036        let _g = NoInteropGuard::on();
22037        parse_ok("my @lists = ([1,2,3], [10,20,30]); p min(map { len(@{_}) } @lists)");
22038    }
22039
22040    /// `~>` thread-macro stages for `glob`, `rand`, `srand` — these
22041    /// builtins have their own `ExprKind` (Glob, Rand, Srand), not the
22042    /// generic FuncCall path, so they previously fell through to the
22043    /// default arm and produced "Undefined subroutine" at runtime.
22044    #[test]
22045    fn thread_macro_accepts_glob_rand_srand_stages() {
22046        parse_ok("my @r = ~> \"/tmp/*\" glob sort");
22047        parse_ok("my $i = ~> 100 rand int");
22048        parse_ok("~> 42 srand");
22049    }
22050
22051    /// Recursive expression-bodied `fn` with path compression — the
22052    /// union-find demo idiom: `fn UF::find($uf, $x) { ... }` body
22053    /// re-invokes itself and writes the result into a hashref slot.
22054    #[test]
22055    fn recursive_fn_with_arrayref_assignment_parses() {
22056        let _g = NoInteropGuard::on();
22057        parse_ok(
22058            "fn UF::find($uf, $x) { \
22059                return $x if $uf->{parent}[$x] == $x; \
22060                $uf->{parent}[$x] = UF::find($uf, $uf->{parent}[$x]); \
22061                $uf->{parent}[$x] \
22062             }",
22063        );
22064    }
22065
22066    /// Hash-initialised with computed list inside arrayref literal —
22067    /// `[0:$n - 1]` and `[(0) x $n]` as field values. Used by UF::new.
22068    #[test]
22069    fn hashref_init_with_range_and_repeat_parses() {
22070        let _g = NoInteropGuard::on();
22071        parse_ok("fn UF::new($n) = +{ parent => [0:$n - 1], rank => [(0) x $n], count => $n }");
22072    }
22073
22074    /// Postfix-for over an arrayref-deref: `Trie::insert($t, $_) for @$words`.
22075    /// Used by Trie::from_words.
22076    #[test]
22077    fn postfix_for_arrayref_deref_parses() {
22078        let _g = NoInteropGuard::on();
22079        parse_ok("my @words = (\"a\", \"b\"); my $r = \\@words; my @out; push @out, $_ for @$r");
22080    }
22081
22082    /// Tuple-swap destructure inside a block — UF::union flips ra/rb
22083    /// for union-by-rank.
22084    #[test]
22085    fn tuple_swap_destructure_parses() {
22086        let _g = NoInteropGuard::on();
22087        parse_ok("my $ra = 1; my $rb = 2; ($ra, $rb) = ($rb, $ra)");
22088    }
22089
22090    /// 2-D array initialised with explicit row arrayrefs — the
22091    /// `--no-interop` mode rejects 2-D autoviv (`$d[$i][$j] = X`
22092    /// on un-initialized `@d`), so each row must be an arrayref
22093    /// literal first. Damerau-Levenshtein demo's matrix setup.
22094    #[test]
22095    fn explicit_2d_array_row_init_parses() {
22096        let _g = NoInteropGuard::on();
22097        parse_ok(
22098            "my @d; my $m = 3; my $n = 4; \
22099             for my $i (0:$m) { $d[$i] = [(0) x ($n + 1)] }",
22100        );
22101    }
22102
22103    /// `min()` with 3 arguments — Damerau-Levenshtein uses this for the
22104    /// deletion/insertion/substitution step.
22105    #[test]
22106    fn min_with_three_args_parses() {
22107        let _g = NoInteropGuard::on();
22108        parse_ok("my $x = min(1, 2, 3)");
22109        parse_ok("my @d; $d[0][0] = 5; my $r = min($d[0][0] + 1, $d[0][0] + 1, $d[0][0] + 0)");
22110    }
22111
22112    /// String slice with `$s[N:M]` where M is `len(...)`-based.
22113    /// Trie::count_with_prefix shape.
22114    #[test]
22115    fn string_slice_with_len_bound_parses() {
22116        let _g = NoInteropGuard::on();
22117        parse_ok(
22118            "my $w = \"apple\"; my $pre = \"app\"; \
22119             my $ok = len($w) >= len($pre) && $w[0:len($pre) - 1] eq $pre",
22120        );
22121    }
22122
22123    /// Sort comparator with `_0->[N] <=> _1->[N]` — sorting an
22124    /// array-of-arrayrefs by a positional field. Kruskal MST pattern.
22125    #[test]
22126    fn sort_block_with_arrow_deref_topic_parses() {
22127        let _g = NoInteropGuard::on();
22128        parse_ok(
22129            "my @edges = ([0,1,4], [2,3,1], [1,2,2]); \
22130             my @sorted = sort { _0->[2] <=> _1->[2] } @edges",
22131        );
22132    }
22133
22134    /// C-style `for` header with declarations and post-decrement —
22135    /// Knuth shuffle's inner loop walks high-to-low.
22136    #[test]
22137    fn cstyle_for_with_postdecrement_parses() {
22138        let _g = NoInteropGuard::on();
22139        parse_ok(
22140            "my @arr = (1, 2, 3, 4, 5); \
22141             my $n = len(@arr); \
22142             for (my $i = $n - 1; $i > 0; $i--) { p $arr[$i] }",
22143        );
22144    }
22145
22146    /// Tuple-swap on array-deref index pairs — Knuth-shuffle inner
22147    /// step swaps `$r->[$i]` and `$r->[$j]` via destructure.
22148    #[test]
22149    fn tuple_swap_arrayref_index_parses() {
22150        let _g = NoInteropGuard::on();
22151        parse_ok(
22152            "my @arr = (1, 2, 3); my $r = \\@arr; my $i = 0; my $j = 2; \
22153             ($r->[$i], $r->[$j]) = ($r->[$j], $r->[$i])",
22154        );
22155    }
22156
22157    /// Recursive backtracking — N-queens pushes a column, recurses,
22158    /// then pops. Verifies array mutation inside recursive fn calls.
22159    #[test]
22160    fn recursive_backtracking_arrayref_mutation_parses() {
22161        let _g = NoInteropGuard::on();
22162        parse_ok(
22163            "fn Q::go($n, $cols, $count_ref) { \
22164                my $r = len(@$cols); \
22165                if ($r == $n) { $$count_ref++; return } \
22166                for my $c (0:$n - 1) { \
22167                    push @$cols, $c; \
22168                    Q::go($n, $cols, $count_ref); \
22169                    pop @$cols \
22170                } \
22171             }",
22172        );
22173    }
22174
22175    /// Doubly-linked list as a hash-of-{prev,next} — LRU cache's
22176    /// node-table pattern. `$nodes->{$k}{prev}` chain.
22177    #[test]
22178    fn nested_hashref_chain_parses() {
22179        let _g = NoInteropGuard::on();
22180        parse_ok(
22181            "my $c = +{ nodes => +{ a => +{ val => 1, prev => undef, next => \"b\" } } }; \
22182             my $p = $c->{nodes}{a}{prev}; \
22183             my $n = $c->{nodes}{a}{next}",
22184        );
22185    }
22186
22187    /// `$$count_ref++` — dereference a scalar-ref and post-increment.
22188    /// Used in Queens::recur to update a shared counter.
22189    #[test]
22190    fn scalar_ref_postincrement_parses() {
22191        let _g = NoInteropGuard::on();
22192        parse_ok("my $n = 0; my $ref = \\$n; $$ref++; p $n");
22193    }
22194
22195    /// `$h{$k}` autoviv chain — Markov bigram table builds a
22196    /// hash-of-hash where the inner is created on first access.
22197    #[test]
22198    fn hash_of_hash_autoviv_increment_parses() {
22199        let _g = NoInteropGuard::on();
22200        parse_ok(
22201            "my %table; \
22202             my $prev = \"the\"; my $next = \"quick\"; \
22203             $table{$prev} //= +{}; \
22204             $table{$prev}{$next}++",
22205        );
22206    }
22207
22208    /// `for (... ; ...) { ... }` C-style with literal counter — Knuth
22209    /// shuffle's high-to-low walk and similar.
22210    #[test]
22211    fn cstyle_for_with_literal_bounds_parses() {
22212        let _g = NoInteropGuard::on();
22213        parse_ok("my $sum = 0; for (my $i = 0; $i < 10; $i++) { $sum += $i }");
22214    }
22215
22216    /// Recursive expression-bodied `fn` with ternary base case —
22217    /// Josephus closed-form. Single-letter tail segments in namespaced
22218    /// names (`J::s`, `Foo::m`, `Foo::q`, `Foo::qx`, `Foo::qr`) are
22219    /// identifiers — the `::` prefix disambiguates from the
22220    /// `s/.../.../`, `m//`, `q//`, etc. quote-like operators.
22221    #[test]
22222    fn recursive_expression_body_with_ternary_parses() {
22223        let _g = NoInteropGuard::on();
22224        parse_ok("fn J::s($n, $k) = $n == 1 ? 0 : (J::s($n - 1, $k) + $k) % $n");
22225    }
22226
22227    /// All quote-like single/two-letter operators (`s`, `m`, `q`, `qq`,
22228    /// `qx`, `qr`, `tr`, `y`) are valid namespaced identifier tails
22229    /// after `::` — they're not lexed as their regex/quote forms.
22230    #[test]
22231    fn namespaced_quote_like_tail_segments_parse() {
22232        let _g = NoInteropGuard::on();
22233        parse_ok("fn Foo::s($x) = $x + 1");
22234        parse_ok("fn Foo::m($x) = $x * 2");
22235        parse_ok("fn Foo::q($x) = $x");
22236        parse_ok("fn Foo::qq($x) = $x");
22237        parse_ok("fn Foo::qx($x) = $x");
22238        parse_ok("fn Foo::qr($x) = $x");
22239        parse_ok("fn Foo::tr($x) = $x");
22240        parse_ok("fn Foo::y($x) = $x");
22241    }
22242
22243    /// `splice @arr, $i, 1` — remove a single element from middle of
22244    /// an array. Josephus simulate uses this.
22245    #[test]
22246    fn splice_single_remove_parses() {
22247        let _g = NoInteropGuard::on();
22248        parse_ok("my @circle = 0:5; splice @circle, 2, 1");
22249    }
22250
22251    /// `atan2(0, -1)` for π — Monte Carlo's true-reference value.
22252    #[test]
22253    fn atan2_call_parses() {
22254        let _g = NoInteropGuard::on();
22255        parse_ok("fn MC::true_pi = atan2(0, -1)");
22256        parse_ok("my $pi = atan2(0, -1)");
22257    }
22258
22259    /// Flat 1-D array indexed as 2-D via `r * COLS + c` — Sudoku
22260    /// board layout. Arithmetic inside subscripts.
22261    #[test]
22262    fn flat_2d_array_indexing_parses() {
22263        let _g = NoInteropGuard::on();
22264        parse_ok(
22265            "my @board = (0) x 81; \
22266             my $r = 3; my $c = 5; \
22267             $board[$r * 9 + $c] = 7; \
22268             my $v = $board[$r * 9 + $c]",
22269        );
22270    }
22271
22272    /// `par` is callable as a top-level expression, not just an
22273    /// `~>` thread-macro stage. Prefix form: `par { BLOCK } LIST`.
22274    /// (Previously parser only accepted `par` inside thread macros,
22275    /// emitting "Undefined subroutine &par" at runtime for any other
22276    /// call site.)
22277    #[test]
22278    fn par_top_level_prefix_form_parses() {
22279        let _g = NoInteropGuard::on();
22280        parse_ok("my @r = par { _ * 2 } (1, 2, 3, 4)");
22281        parse_ok("par { p _ } @big");
22282    }
22283
22284    /// 2-D DP table with `max()` step — LCS / Levenshtein / Damerau /
22285    /// general edit-distance pattern. Validates that 3-way max +
22286    /// nested arrayref subscript chain parses cleanly.
22287    #[test]
22288    fn dp_max_step_chained_subscript_parses() {
22289        let _g = NoInteropGuard::on();
22290        parse_ok(
22291            "my @d; for my $i (0:3) { $d[$i] = [(0) x 4] } \
22292             $d[1][1] = max($d[0][1], $d[1][0]); \
22293             my $r = $d[1][1]",
22294        );
22295    }
22296
22297    /// Rolling polynomial hash arithmetic — Rabin-Karp's window update.
22298    #[test]
22299    fn rolling_hash_arithmetic_parses() {
22300        let _g = NoInteropGuard::on();
22301        parse_ok(
22302            "my $h = 0; my $base = 257; my $mod = 1000000007; my $high = 256; \
22303             my $drop = 65; my $add = 90; \
22304             $h = (($h - $drop * $high) * $base + $add) % $mod; \
22305             $h = ($h + $mod) % $mod",
22306        );
22307    }
22308
22309    /// Triple-nested loop with index expressions on a 2-D arrayref —
22310    /// Floyd-Warshall's k/i/j signature.
22311    #[test]
22312    fn triple_nested_2d_via_k_parses() {
22313        let _g = NoInteropGuard::on();
22314        parse_ok(
22315            "my @d; for my $i (0:3) { $d[$i] = [(0) x 4] } \
22316             for my $k (0:3) { for my $i (0:3) { for my $j (0:3) { \
22317                $d[$i][$j] = $d[$i][$k] + $d[$k][$j] \
22318                    if $d[$i][$k] + $d[$k][$j] < $d[$i][$j] \
22319             } } }",
22320        );
22321    }
22322
22323    /// DP fill with `@dp = ($INF) x ($amount + 1)` repeat-init.
22324    /// Coin-change shape.
22325    #[test]
22326    fn dp_array_repeat_init_parses() {
22327        let _g = NoInteropGuard::on();
22328        parse_ok("my $amount = 11; my $INF = 1e18; my @dp = ($INF) x ($amount + 1); $dp[0] = 0");
22329    }
22330
22331    /// `join("", rev split //, $s)` — palindrome check pipeline.
22332    #[test]
22333    fn rev_split_join_chain_parses() {
22334        let _g = NoInteropGuard::on();
22335        parse_ok("my $s = \"abc\"; my $r = join(\"\", rev split //, $s)");
22336    }
22337
22338    /// C-style `for` with explicit init / cond / decrement step —
22339    /// heap-sort's sift-down walks the heap children from end down.
22340    #[test]
22341    fn cstyle_for_decrement_with_arrayref_swap_parses() {
22342        let _g = NoInteropGuard::on();
22343        parse_ok(
22344            "my @arr = (1, 2, 3); my $r = \\@arr; \
22345             for (my $end = len(@arr) - 1; $end > 0; $end--) { \
22346                ($r->[0], $r->[$end]) = ($r->[$end], $r->[0]) \
22347             }",
22348        );
22349    }
22350
22351    /// `shift @q` inside a while-loop driving a BFS-style queue —
22352    /// Kahn's topological sort.
22353    #[test]
22354    fn shift_in_while_loop_parses() {
22355        let _g = NoInteropGuard::on();
22356        parse_ok(
22357            "my @q = (0, 1, 2); my @out; \
22358             while (len(@q) > 0) { my $u = shift @q; push @out, $u }",
22359        );
22360    }
22361
22362    /// Modular exponentiation by squaring — Miller-Rabin's core. While
22363    /// loop with `int($e / 2)`, modular multiply, and conditional update.
22364    #[test]
22365    fn mod_pow_squaring_loop_parses() {
22366        let _g = NoInteropGuard::on();
22367        parse_ok(
22368            "fn MR::mod_pow($base, $exp, $m) { \
22369                my $result = 1; my $bs = $base % $m; my $e = $exp; \
22370                while ($e > 0) { \
22371                    $result = ($result * $bs) % $m if $e % 2 == 1; \
22372                    $e = int($e / 2); \
22373                    $bs = ($bs * $bs) % $m \
22374                } \
22375                $result \
22376             }",
22377        );
22378    }
22379
22380    /// 2-D DP traceback: walk back from dp[n][target], conditionally
22381    /// taking or skipping each item. Subset-sum reconstruct shape.
22382    #[test]
22383    fn dp_traceback_walk_parses() {
22384        let _g = NoInteropGuard::on();
22385        parse_ok(
22386            "my @xs = (1, 2, 3); my $n = 3; my $target = 4; \
22387             my @dp; for my $i (0:$n) { $dp[$i] = [(0) x ($target + 1)] } \
22388             my @out; my $j = $target; \
22389             for (my $i = $n; $i > 0; $i--) { \
22390                my $v = $xs[$i - 1]; \
22391                if ($j >= $v && $dp[$i - 1][$j - $v] == 1) { \
22392                    unshift @out, $v; $j -= $v \
22393                } \
22394             }",
22395        );
22396    }
22397
22398    /// Binary search inside a `for` loop — LIS patience-sort variant
22399    /// using `tails` array. while-loop with `int(($lo+$hi)/2)`.
22400    #[test]
22401    fn binary_search_in_for_loop_parses() {
22402        let _g = NoInteropGuard::on();
22403        parse_ok(
22404            "my @xs = (3, 1, 4, 1, 5); my @tails; \
22405             for my $x (@xs) { \
22406                my $lo = 0; my $hi = len @tails; \
22407                while ($lo < $hi) { \
22408                    my $mid = int(($lo + $hi) / 2); \
22409                    if ($tails[$mid] < $x) { $lo = $mid + 1 } else { $hi = $mid } \
22410                } \
22411                if ($lo == len @tails) { push @tails, $x } else { $tails[$lo] = $x } \
22412             }",
22413        );
22414    }
22415
22416    /// Edge-relaxation loop with destructure + early-skip — Bellman-Ford
22417    /// shape. Tests `my ($u, $w, $cost) = @$e` inside a for-loop.
22418    #[test]
22419    fn edge_relaxation_destructure_parses() {
22420        let _g = NoInteropGuard::on();
22421        parse_ok(
22422            "my @edges = ([0, 1, 5], [1, 2, -3]); \
22423             my @dist = (0, 1e18, 1e18); my $INF = 1e18; \
22424             for my $e (@edges) { \
22425                my ($u, $w, $cost) = @$e; \
22426                next if $dist[$u] >= $INF; \
22427                $dist[$w] = $dist[$u] + $cost if $dist[$u] + $cost < $dist[$w] \
22428             }",
22429        );
22430    }
22431
22432    /// Bitwise `&` with negation: `$x & -$x` — Fenwick tree's
22433    /// lowest-set-bit isolation.
22434    #[test]
22435    fn bitwise_and_with_negation_parses() {
22436        let _g = NoInteropGuard::on();
22437        parse_ok("fn Fenwick::lsb($x) = $x & -$x");
22438        parse_ok("my $k = 12; my $lo_bit = $k & -$k");
22439    }
22440
22441    /// `@count[v] = old_v; running += c` — counting-sort cumulative
22442    /// transform. Tests serial assign-and-update inside a for loop.
22443    #[test]
22444    fn counting_sort_cumulative_loop_parses() {
22445        let _g = NoInteropGuard::on();
22446        parse_ok(
22447            "my @count = (3, 1, 2, 4); my $running = 0; \
22448             for my $v (0:3) { \
22449                my $c = $count[$v]; \
22450                $count[$v] = $running; \
22451                $running += $c \
22452             }",
22453        );
22454    }
22455
22456    /// Recursive tree walk with hashref nodes — Huffman tree traversal
22457    /// pattern. Validates that `defined $node` + `exists $node->{sym}`
22458    /// + child-recursion all parse cleanly.
22459    #[test]
22460    fn recursive_tree_walk_with_hashref_parses() {
22461        let _g = NoInteropGuard::on();
22462        parse_ok(
22463            "fn Huff::walk($node, $prefix, $codes) { \
22464                return unless defined $node; \
22465                if (exists $node->{sym}) { \
22466                    $codes->{$node->{sym}} = $prefix eq \"\" ? \"0\" : $prefix; \
22467                    return \
22468                } \
22469                Huff::walk($node->{left},  $prefix . \"0\", $codes); \
22470                Huff::walk($node->{right}, $prefix . \"1\", $codes) \
22471             }",
22472        );
22473    }
22474
22475    /// Z-array maintained-window arithmetic — three-way min via
22476    /// ternary, increment-while-match. Z-algorithm core.
22477    #[test]
22478    fn z_array_window_arithmetic_parses() {
22479        let _g = NoInteropGuard::on();
22480        parse_ok(
22481            "my @c = (\"a\", \"b\", \"a\"); my $n = 3; \
22482             my @z = (0) x $n; $z[0] = $n; \
22483             my $l = 0; my $r = 0; my $i = 1; \
22484             if ($i < $r) { \
22485                my $inside = $r - $i < $z[$i - $l] ? $r - $i : $z[$i - $l]; \
22486                $z[$i] = $inside \
22487             } \
22488             while ($i + $z[$i] < $n && $c[$z[$i]] eq $c[$i + $z[$i]]) { $z[$i]++ }",
22489        );
22490    }
22491
22492    /// BFS expansion with parent-linked hashref nodes — A* /
22493    /// general pathfinding shape. Inner `for` over neighbor offsets.
22494    #[test]
22495    fn bfs_with_parent_link_node_parses() {
22496        let _g = NoInteropGuard::on();
22497        parse_ok(
22498            "my @open = (+{ r => 0, c => 0, g => 0, parent => undef }); \
22499             while (len(@open) > 0) { \
22500                my $cur = shift @open; \
22501                for my $d ([-1, 0], [1, 0], [0, -1], [0, 1]) { \
22502                    my ($dr, $dc) = @$d; \
22503                    push @open, +{ \
22504                        r => $cur->{r} + $dr, c => $cur->{c} + $dc, \
22505                        g => $cur->{g} + 1, parent => $cur \
22506                    } \
22507                } \
22508                last \
22509             }",
22510        );
22511    }
22512
22513    /// Cross-product sort comparator with collinear tiebreak —
22514    /// Graham scan's polar sort.
22515    #[test]
22516    fn cross_product_sort_comparator_parses() {
22517        let _g = NoInteropGuard::on();
22518        parse_ok(
22519            "fn Hull::cross($p1, $p2, $p3) = \
22520                ($p2->[0] - $p1->[0]) * ($p3->[1] - $p1->[1]) - \
22521                ($p2->[1] - $p1->[1]) * ($p3->[0] - $p1->[0]); \
22522             my $pivot = [0, 0]; my @pts = ([1, 1], [2, 0]); \
22523             my @sorted = sort { \
22524                my $c = Hull::cross($pivot, _0, _1); \
22525                $c == 0 ? 0 : ($c < 0 ? 1 : -1) \
22526             } @pts",
22527        );
22528    }
22529
22530    /// Lomuto partition + recursive bisection — quickselect's loop.
22531    /// Inner loop with `$i++` + swap on each match.
22532    #[test]
22533    fn lomuto_partition_loop_parses() {
22534        let _g = NoInteropGuard::on();
22535        parse_ok(
22536            "fn QS::partition($arr, $lo, $hi) { \
22537                my $pivot = $arr->[$hi]; \
22538                my $i = $lo - 1; \
22539                for my $j ($lo:$hi - 1) { \
22540                    if ($arr->[$j] <= $pivot) { \
22541                        $i++; \
22542                        ($arr->[$i], $arr->[$j]) = ($arr->[$j], $arr->[$i]) \
22543                    } \
22544                } \
22545                ($arr->[$i + 1], $arr->[$hi]) = ($arr->[$hi], $arr->[$i + 1]); \
22546                $i + 1 \
22547             }",
22548        );
22549    }
22550
22551    /// `do { x } while (cond)` shape with diff-then-gcd — Pollard rho's
22552    /// tortoise-and-hare loop. Tests absolute-difference via ternary.
22553    #[test]
22554    fn tortoise_hare_diff_loop_parses() {
22555        let _g = NoInteropGuard::on();
22556        parse_ok(
22557            "my $x = 2; my $y = 2; my $n = 35; my $d = 1; \
22558             while ($d == 1) { \
22559                $x = ($x * $x + 1) % $n; \
22560                $y = ($y * $y + 1) % $n; \
22561                $y = ($y * $y + 1) % $n; \
22562                my $diff = $x > $y ? $x - $y : $y - $x; \
22563                $d = $diff \
22564             }",
22565        );
22566    }
22567
22568    /// Recursive ext_gcd returning a 3-tuple via arrayref destructure.
22569    /// Modular-inverse pattern.
22570    #[test]
22571    fn ext_gcd_recursive_destructure_parses() {
22572        let _g = NoInteropGuard::on();
22573        parse_ok(
22574            "fn Mod::ext_gcd($va, $vb) { \
22575                return [$va, 1, 0] if $vb == 0; \
22576                my $r = Mod::ext_gcd($vb, $va % $vb); \
22577                my ($g, $x1, $y1) = @$r; \
22578                [$g, $y1, $x1 - int($va / $vb) * $y1] \
22579             }",
22580        );
22581    }
22582
22583    /// Convolution-recurrence DP — Catalan number computation.
22584    /// Inner accumulator with mult on each iteration.
22585    #[test]
22586    fn convolution_recurrence_dp_parses() {
22587        let _g = NoInteropGuard::on();
22588        parse_ok(
22589            "my @c = (1); my $n = 5; \
22590             for my $i (1:$n) { \
22591                my $sum = 0; \
22592                for my $j (0:$i - 1) { $sum += $c[$j] * $c[$i - 1 - $j] } \
22593                push @c, $sum \
22594             }",
22595        );
22596    }
22597
22598    /// Tarjan SCC state: shared mutable hashref carries the entire
22599    /// algorithm's bookkeeping (`index`, `idx_of`, `low_of`,
22600    /// `on_stack`, `stack`, `sccs`) — passed by reference to the
22601    /// recursive worker.
22602    #[test]
22603    fn tarjan_scc_shared_state_parses() {
22604        let _g = NoInteropGuard::on();
22605        parse_ok(
22606            "fn SCC::strong_connect($s, $v) { \
22607                $s->{idx_of}{$v} = $s->{index}; \
22608                $s->{low_of}{$v} = $s->{index}; \
22609                $s->{index}++; \
22610                push @{$s->{stack}}, $v; \
22611                $s->{on_stack}{$v} = 1; \
22612                for my $w (@{$s->{adj}{$v}}) { \
22613                    if (!exists $s->{idx_of}{$w}) { \
22614                        SCC::strong_connect($s, $w); \
22615                        $s->{low_of}{$v} = $s->{low_of}{$w} if $s->{low_of}{$w} < $s->{low_of}{$v} \
22616                    } \
22617                } \
22618             }",
22619        );
22620    }
22621
22622    /// Heap's algorithm permutation generator — recursive in-place
22623    /// swap with parity-conditional pivot choice.
22624    #[test]
22625    fn heaps_algorithm_recursive_parses() {
22626        let _g = NoInteropGuard::on();
22627        parse_ok(
22628            "fn Perm::heaps_inner($arr, $n, $out) { \
22629                if ($n == 1) { my @snap = @$arr; push @$out, \\@snap; return } \
22630                for my $i (0:$n - 1) { \
22631                    Perm::heaps_inner($arr, $n - 1, $out); \
22632                    if ($n % 2 == 0) { \
22633                        ($arr->[$i], $arr->[$n - 1]) = ($arr->[$n - 1], $arr->[$i]) \
22634                    } else { \
22635                        ($arr->[0], $arr->[$n - 1]) = ($arr->[$n - 1], $arr->[0]) \
22636                    } \
22637                } \
22638             }",
22639        );
22640    }
22641
22642    /// `format` is a Perl FORMAT-declaration keyword. The lexer must
22643    /// NOT eat `format` when it appears as a hash key
22644    /// (`$h{format}`, `{format => ...}`), a method name
22645    /// (`$obj->format`), a namespaced tail (`Foo::format`), or a
22646    /// list/expr item with terminator follow-up. Previously
22647    /// `$opts{format} = "csv"` triggered "Expected '=' after format
22648    /// name" because the lexer greedily entered format-decl mode.
22649    #[test]
22650    fn format_as_hash_key_parses() {
22651        parse_ok("my %opts; $opts{format} = \"csv\"");
22652        parse_ok("my %opts = (format => \"csv\", level => 9)");
22653        parse_ok("my $h = +{ format => \"csv\" }");
22654        parse_ok("my @keys = ($h->{format}, $h->{level})");
22655    }
22656
22657    /// `format` after `->` is a method name, not the format keyword.
22658    #[test]
22659    fn format_as_method_call_parses() {
22660        parse_ok("class Foo { val: Str; fn format($self) { \"x\" } } my $f = Foo(val => \"y\"); my $s = $f->format()");
22661    }
22662
22663    /// `format` after `::` is a namespaced fn name tail.
22664    #[test]
22665    fn format_as_namespaced_tail_parses() {
22666        parse_ok("fn Foo::format($x) = $x . \"!\"");
22667        parse_ok("fn Foo::format($x) = $x . \"!\"; my $r = Foo::format(\"hi\")");
22668    }
22669
22670    /// Compound-assign on hash arrow-deref leaves the new value on the
22671    /// stack (uses `SetArrowHashKeep`, not `SetArrowHash`). Previously
22672    /// the no-keep variant left nothing for the statement-level Pop,
22673    /// which then ate a slot from the CALLER's stack frame — corrupting
22674    /// `dec($h) + dec($h) + dec($h)`-style multi-call expressions.
22675    /// See tests/suite/hashref_assignment_pin.rs for runtime pins.
22676    #[test]
22677    fn arrow_hash_compound_assign_parses_all_ops() {
22678        let _g = NoInteropGuard::on();
22679        parse_ok("my $h = +{n=>10}; $h->{n} -= 1");
22680        parse_ok("my $h = +{n=>10}; $h->{n} += 1");
22681        parse_ok("my $h = +{n=>10}; $h->{n} *= 2");
22682        parse_ok("my $h = +{n=>10}; $h->{n} /= 2");
22683        parse_ok("my $h = +{n=>10}; $h->{n} %= 3");
22684        parse_ok("my $h = +{n=>\"x\"}; $h->{n} .= \"y\"");
22685    }
22686
22687    /// The compound-assign yields the NEW value as its expression
22688    /// result, same as plain `$h->{k} = v` does. Validated at parse
22689    /// time by accepting the use-as-rvalue shape `my $v = $h->{n} -= 1`.
22690    #[test]
22691    fn arrow_hash_compound_assign_value_chains_parses() {
22692        let _g = NoInteropGuard::on();
22693        parse_ok("my $h = +{n=>10}; my $v = $h->{n} -= 1");
22694        parse_ok("my $h = +{n=>10}; my @list = ($h->{n} += 5, $h->{n} += 5)");
22695        parse_ok("my $h = +{n=>10}; my $double = ($h->{n} -= 1) * 2");
22696    }
22697}