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
82pub struct Parser {
83    tokens: Vec<(Token, usize)>,
84    pos: usize,
85    /// Monotonic slot id for `rate_limit(...)` sliding-window state in the interpreter.
86    next_rate_limit_slot: u32,
87    /// When > 0, `expr` `(` is not parsed as [`ExprKind::IndirectCall`] — e.g. `sort $k (1)` must
88    /// treat `(1)` as the sort list, not `$k(1)`.
89    suppress_indirect_paren_call: u32,
90    /// When > 0, the current expression is being parsed as the RHS of `|>`
91    /// (pipe-forward). Builtins that normally require a list/string/second arg
92    /// (`map`, `grep`, `sort`, `join`, `reverse` / `reversed`, `split`, …) may accept a
93    /// placeholder when this flag is set, because [`Self::pipe_forward_apply`]
94    /// will substitute the piped value in afterwards.
95    pipe_rhs_depth: u32,
96    /// When > 0 we are parsing inside a `{ … }` block (function body, `map`/`grep`,
97    /// `for`, `if`, anonymous coderef, etc.). Inside any block, bare `_` is a topic
98    /// reference (`$_[0]`/`$_`), so `my $i = _` means "capture the topic" and must
99    /// NOT be auto-wrapped as an implicit zero-arg coderef. Only at the true top
100    /// level (depth 0 — module scope) is `_` unbound, allowing `my $f = _ * 2` to
101    /// parse as `my $f = fn { _ * 2 }`. Bumped in [`Self::parse_block`].
102    block_depth: u32,
103    /// When > 0, [`Self::parse_pipe_forward`] will **not** consume a trailing `|>`
104    /// and leaves it for an outer parser instead. Bumped while parsing paren-less
105    /// arg lists (`parse_list_until_terminator`, paren-less method args, `map`/`grep`
106    /// LIST, …) so `@a |> head 2 |> join "-"` chains left-associatively as
107    /// `(@a |> head 2) |> join "-"` instead of `head` swallowing the outer `|>`
108    /// as part of its first arg. Reset to 0 on entry to any parenthesized
109    /// arg list (`parse_arg_list`) so `head(2 |> foo, 3)` still works.
110    no_pipe_forward_depth: u32,
111    /// When > 0, `{` after a scalar / scalar deref is not `%hash{key}` / `->{}`, so
112    /// `if let` / `while let` scrutinees can be followed by `{ ... }`.
113    suppress_scalar_hash_brace: u32,
114    /// Counter for `while let` / similar desugar temps (`$__while_let_0`, …).
115    next_desugar_tmp: u32,
116    /// Source path for [`StrykeError`] (matches lexer / `parse_with_file`).
117    error_file: String,
118    /// User-declared sub names (for allowing UDF to shadow stryke extensions in compat mode).
119    declared_subs: std::collections::HashSet<String>,
120    /// When > 0, `parse_named_expr` will not consume following barewords as paren-less
121    /// function arguments. Used by thread macro to prevent `t Color::Red p` from
122    /// interpreting `p` as an argument to the enum constructor instead of a stage.
123    suppress_parenless_call: u32,
124    /// Pre-built input expression for the next `parse_thread_macro_inner`
125    /// call. Used by `~p>` continuation parsing (`||>` / `|then|`) to
126    /// thread the par_reduce result into a normal `~>` continuation
127    /// without re-parsing a source expression.
128    pending_thread_input: Option<Expr>,
129    /// When > 0, `parse_multiplication` will not consume `Token::Slash` as division.
130    /// Used by thread macro so `/pattern/` is left for the stage parser to handle.
131    suppress_slash_as_div: u32,
132    /// When > 0, the lexer should not interpret `m/`, `s/`, etc. as regex-starters.
133    /// Used by thread macro to prevent `/m/` from being misparsed.
134    pub suppress_m_regex: u32,
135    /// When > 0, `parse_range` will not consume `:` as the short-form range operator.
136    /// Bumped while parsing the then-branch of a ternary `? :` so `a ? b : c` doesn't
137    /// misparse `b : c` as a range.
138    suppress_colon_range: u32,
139    /// Counter (depth-tracked like [`Self::suppress_colon_range`]) that
140    /// disables `~` as a range separator. Used inside paired `~...~` char-
141    /// index/slice subscripts so the closing `~` doesn't get eaten as a
142    /// range op. `:` range is still allowed inside (e.g. `$_~1:3~` is a
143    /// slice with a `:` range as the index).
144    suppress_tilde_range: u32,
145    /// When true, `pipe_forward_apply` uses thread-last semantics (append to args)
146    /// instead of thread-first (prepend). Set by `->>` thread macro.
147    thread_last_mode: bool,
148    /// When true, we're parsing a module (via `use`/`require`), not user code.
149    /// Modules are allowed to shadow builtins; user code is not (unless `--compat`).
150    pub parsing_module: bool,
151    /// `self.pos` immediately after consuming a paren-list close (`(EXPR)`,
152    /// `(EXPR, …)`, `()`) or `qw(…)` in `parse_primary`. The `x` operator
153    /// reads this at parse time to distinguish `(LIST) x N` (list repetition)
154    /// from `EXPR x N` (scalar string repetition). The compare is exact: any
155    /// postfix consumption (`->method()`, `[idx]`, …) advances `self.pos`
156    /// past this checkpoint, so list-repeat fires only when `x` is the very
157    /// next token after the closing paren.
158    list_construct_close_pos: Option<usize>,
159    /// Synthetic SubDecl statements queued by anonymous-sub overload handlers
160    /// (`use overload "+" => sub { ... }`) — drained at the end of
161    /// [`Self::parse_program`] and prepended to the top-level statements so
162    /// the package-qualified synthetic name resolves at runtime. (PARITY-012)
163    pending_synthetic_subs: Vec<Statement>,
164    /// Counter for unique anonymous-overload-handler names.
165    next_overload_anon_id: u32,
166    /// Token-vector indices where the lexer emitted a *bare* positional alias
167    /// (`_`, `_0`, `_1`, …) — i.e. without a leading `$` sigil. Populated by
168    /// [`crate::lexer::Lexer::tokenize`]. Consulted by [`Self::parse_my_our_local`]
169    /// to auto-wrap an RHS expression that contains free positional aliases
170    /// into an implicit zero-arg coderef, so `my $f = _ * 2` ≡
171    /// `my $f = fn { _ * 2 }`.
172    pub bare_positional_indices: std::collections::HashSet<usize>,
173    /// Current package context — updated by `parse_package`. Defaults to
174    /// `"main"`. Used by [`Self::check_udf_shadows_builtin`] to allow
175    /// `fn name(...)` inside `package Foo` to shadow stryke builtins
176    /// (the bare `name` becomes `Foo::name`, so the builtin remains
177    /// reachable via the unqualified call from outside the package).
178    current_package: String,
179}
180
181impl Parser {
182    pub fn new(tokens: Vec<(Token, usize)>) -> Self {
183        Self::new_with_file(tokens, "-e")
184    }
185
186    pub fn new_with_file(tokens: Vec<(Token, usize)>, file: impl Into<String>) -> Self {
187        Self {
188            tokens,
189            pos: 0,
190            next_rate_limit_slot: 0,
191            suppress_indirect_paren_call: 0,
192            pipe_rhs_depth: 0,
193            no_pipe_forward_depth: 0,
194            suppress_scalar_hash_brace: 0,
195            next_desugar_tmp: 0,
196            error_file: file.into(),
197            declared_subs: std::collections::HashSet::new(),
198            suppress_parenless_call: 0,
199            pending_thread_input: None,
200            suppress_slash_as_div: 0,
201            suppress_m_regex: 0,
202            suppress_colon_range: 0,
203            suppress_tilde_range: 0,
204            thread_last_mode: false,
205            pending_synthetic_subs: Vec::new(),
206            next_overload_anon_id: 0,
207            parsing_module: false,
208            list_construct_close_pos: None,
209            bare_positional_indices: std::collections::HashSet::new(),
210            block_depth: 0,
211            current_package: "main".to_string(),
212        }
213    }
214
215    fn alloc_desugar_tmp(&mut self) -> u32 {
216        let n = self.next_desugar_tmp;
217        self.next_desugar_tmp = self.next_desugar_tmp.saturating_add(1);
218        n
219    }
220
221    /// True when we are currently parsing the RHS of a `|>` pipe-forward.
222    /// Used by builtins (`map`, `grep`, `sort`, `join`, …) to supply a
223    /// placeholder list instead of erroring on a missing operand.
224    #[inline]
225    fn in_pipe_rhs(&self) -> bool {
226        self.pipe_rhs_depth > 0
227    }
228
229    /// List-slurping builtin: the operand is entirely the LHS of `|>` (no following list tokens).
230    /// A newline after the builtin name also terminates the pipe stage (implicit semicolon).
231    fn pipe_supplies_slurped_list_operand(&self) -> bool {
232        self.in_pipe_rhs()
233            && (matches!(
234                self.peek(),
235                Token::Semicolon
236                    | Token::RBrace
237                    | Token::RParen
238                    | Token::Eof
239                    | Token::Comma
240                    | Token::PipeForward
241            ) || self.peek_line() > self.prev_line())
242    }
243
244    /// Empty placeholder list used as a stand-in for the list operand of
245    /// list-taking builtins when they appear on the RHS of `|>`.
246    /// [`Self::pipe_forward_apply`] rewrites this slot with the actual piped
247    /// value at desugar time, so the placeholder is never evaluated.
248    #[inline]
249    fn pipe_placeholder_list(&self, line: usize) -> Expr {
250        Expr {
251            kind: ExprKind::List(vec![]),
252            line,
253        }
254    }
255
256    /// List builtins that take `{ BLOCK }, LIST` and accept the threaded list at
257    /// `args[1]` via [`Self::pipe_forward_apply`]. Used by both the pipe-forward
258    /// dispatcher and `parse_thread_stage_with_block` so `~> @a NAME { ... }` and
259    /// `@a |> NAME { ... }` route through the same substitution.
260    fn is_block_then_list_pipe_builtin(name: &str) -> bool {
261        matches!(
262            name,
263            "pfirst"
264                | "pany"
265                | "any"
266                | "all"
267                | "none"
268                | "first"
269                | "find_index"
270                | "firstidx"
271                | "first_index"
272                | "take_while"
273                | "drop_while"
274                | "skip_while"
275                | "reject"
276                | "grepv"
277                | "tap"
278                | "peek"
279                | "group_by"
280                | "chunk_by"
281                | "partition"
282                | "min_by"
283                | "max_by"
284                | "zip_with"
285                | "count_by"
286        )
287    }
288
289    /// Lift a `Bareword("f")` to `FuncCall { f, [$_] }`.
290    ///
291    /// stryke extension contexts (map/grep/fore expression forms, pipe-forward)
292    /// call this so that `map sha512, @list` invokes `sha512($_)` for each
293    /// element instead of stringifying the bareword.  Non-bareword expressions
294    /// pass through unchanged.
295    ///
296    /// Also injects `$_` into known builtins that were parsed with zero
297    /// arguments (e.g. `fore unlink`, `map stat`) so they operate on the
298    /// topic variable instead of being no-ops.
299    fn lift_bareword_to_topic_call(expr: Expr) -> Expr {
300        let line = expr.line;
301        let topic = || Expr {
302            kind: ExprKind::ScalarVar("_".into()),
303            line,
304        };
305        match expr.kind {
306            ExprKind::Bareword(ref name) => Expr {
307                kind: ExprKind::FuncCall {
308                    name: name.clone(),
309                    args: vec![topic()],
310                },
311                line,
312            },
313            // Builtins that take Vec<Expr> args — inject $_ when empty.
314            ExprKind::Unlink(ref args) if args.is_empty() => Expr {
315                kind: ExprKind::Unlink(vec![topic()]),
316                line,
317            },
318            ExprKind::Chmod(ref args) if args.is_empty() => Expr {
319                kind: ExprKind::Chmod(vec![topic()]),
320                line,
321            },
322            // Builtins that take Box<Expr> — inject $_ when arg is implicit.
323            ExprKind::Stat(_) => expr,
324            ExprKind::Lstat(_) => expr,
325            ExprKind::Readlink(_) => expr,
326            // rev with empty list should use $_
327            ExprKind::Rev(ref inner) => {
328                if matches!(inner.kind, ExprKind::List(ref v) if v.is_empty()) {
329                    Expr {
330                        kind: ExprKind::Rev(Box::new(topic())),
331                        line,
332                    }
333                } else {
334                    expr
335                }
336            }
337            _ => expr,
338        }
339    }
340
341    /// `parse_assign_expr` with `no_pipe_forward_depth` bumped for the
342    /// duration, so any trailing `|>` is left to the enclosing parser instead
343    /// of being absorbed into this sub-expression. Used by paren-less arg
344    /// parsers (`parse_list_until_terminator`, `chunked`/`windowed` paren-less,
345    /// paren-less method args, …) so `@a |> head 2 |> join "-"` chains
346    /// left-associatively instead of letting `head`'s first arg swallow the
347    /// outer `|>`. The counter is restored on both success and error paths.
348    fn parse_assign_expr_stop_at_pipe(&mut self) -> StrykeResult<Expr> {
349        self.no_pipe_forward_depth = self.no_pipe_forward_depth.saturating_add(1);
350        let r = self.parse_assign_expr();
351        self.no_pipe_forward_depth = self.no_pipe_forward_depth.saturating_sub(1);
352        r
353    }
354
355    fn syntax_err(&self, message: impl Into<String>, line: usize) -> StrykeError {
356        StrykeError::new(ErrorKind::Syntax, message, line, self.error_file.clone())
357    }
358
359    /// Coderef-in-block-position helper for tier-2 list builtins (`any`,
360    /// `all`, `none`, `first`, `take_while`, …). Returns `Some([f, list])`
361    /// when the next tokens look like `$f [,] LIST` (or `$f` alone in
362    /// pipe-RHS); `None` when the caller should fall through to the block
363    /// form. The first arg is any coderef-shaped expression — runtime
364    /// checks `as_code_ref()` and dispatches.
365    fn try_parse_coderef_listop_args(&mut self, line: usize) -> StrykeResult<Option<Vec<Expr>>> {
366        if !matches!(self.peek(), Token::ScalarVar(_) | Token::Backslash) {
367            return Ok(None);
368        }
369        let f = self.parse_assign_expr_stop_at_pipe()?;
370        let _ = self.eat(&Token::Comma);
371        let list = if self.in_pipe_rhs()
372            && matches!(
373                self.peek(),
374                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
375            ) {
376            self.pipe_placeholder_list(line)
377        } else {
378            self.parse_expression()?
379        };
380        Ok(Some(vec![f, list]))
381    }
382
383    fn alloc_rate_limit_slot(&mut self) -> u32 {
384        let s = self.next_rate_limit_slot;
385        self.next_rate_limit_slot = self.next_rate_limit_slot.saturating_add(1);
386        s
387    }
388
389    fn peek(&self) -> &Token {
390        self.tokens
391            .get(self.pos)
392            .map(|(t, _)| t)
393            .unwrap_or(&Token::Eof)
394    }
395
396    fn peek_line(&self) -> usize {
397        self.tokens.get(self.pos).map(|(_, l)| *l).unwrap_or(0)
398    }
399
400    fn peek_at(&self, offset: usize) -> &Token {
401        self.tokens
402            .get(self.pos + offset)
403            .map(|(t, _)| t)
404            .unwrap_or(&Token::Eof)
405    }
406
407    fn advance(&mut self) -> (Token, usize) {
408        let tok = self
409            .tokens
410            .get(self.pos)
411            .cloned()
412            .unwrap_or((Token::Eof, 0));
413        self.pos += 1;
414        tok
415    }
416
417    /// Line number of the most recently consumed token (the token at `pos - 1`).
418    fn prev_line(&self) -> usize {
419        if self.pos > 0 {
420            self.tokens.get(self.pos - 1).map(|(_, l)| *l).unwrap_or(0)
421        } else {
422            0
423        }
424    }
425
426    /// Check if `{ ... }` starting at current position looks like a hashref rather than a block.
427    /// Heuristics (assuming current token is `{`):
428    /// - `{ bareword =>` → hashref
429    /// - `{ "string" =>` → hashref
430    /// - `{ $var =>` → hashref
431    /// - `{ 0 =>` → hashref (numeric key)
432    /// - `{ %hash }` or `{ %hash, ...}` → hashref (spread)
433    /// - `{ }` (empty) → hashref
434    fn looks_like_hashref(&self) -> bool {
435        debug_assert!(matches!(self.peek(), Token::LBrace));
436        let tok1 = self.peek_at(1);
437        let tok2 = self.peek_at(2);
438        match tok1 {
439            Token::RBrace => true,
440            Token::Ident(_)
441            | Token::SingleString(_)
442            | Token::DoubleString(_)
443            | Token::ScalarVar(_)
444            | Token::Integer(_) => matches!(tok2, Token::FatArrow),
445            Token::HashVar(_) => matches!(tok2, Token::RBrace | Token::Comma),
446            _ => false,
447        }
448    }
449
450    fn expect(&mut self, expected: &Token) -> StrykeResult<usize> {
451        let (tok, line) = self.advance();
452        if std::mem::discriminant(&tok) == std::mem::discriminant(expected) {
453            Ok(line)
454        } else {
455            Err(self.syntax_err(format!("Expected {:?}, got {:?}", expected, tok), line))
456        }
457    }
458
459    fn eat(&mut self, expected: &Token) -> bool {
460        if std::mem::discriminant(self.peek()) == std::mem::discriminant(expected) {
461            self.advance();
462            true
463        } else {
464            false
465        }
466    }
467
468    fn at_eof(&self) -> bool {
469        matches!(self.peek(), Token::Eof)
470    }
471
472    /// True when a file test (`-d`, `-f`, …) may omit its operand and use `$_` (Perl filetest default).
473    fn filetest_allows_implicit_topic(tok: &Token) -> bool {
474        matches!(
475            tok,
476            Token::RParen
477                | Token::Semicolon
478                | Token::Comma
479                | Token::RBrace
480                | Token::Eof
481                | Token::LogAnd
482                | Token::LogOr
483                | Token::LogAndWord
484                | Token::LogOrWord
485                | Token::PipeForward
486        )
487    }
488
489    /// True when the next token is a statement-starting keyword on a *different*
490    /// line from `stmt_line`.  Used by `parse_use` / `parse_no` to stop parsing
491    /// import lists when semicolons are omitted (stryke extension).
492    fn next_is_new_stmt_keyword(&self, stmt_line: usize) -> bool {
493        // Semicolons-optional is a stryke extension; in compat mode, require them.
494        if crate::compat_mode() {
495            return false;
496        }
497        if self.peek_line() == stmt_line {
498            return false;
499        }
500        matches!(
501            self.peek(),
502            Token::Ident(ref kw) if matches!(kw.as_str(),
503                "use" | "no" | "my" | "our" | "local" | "sub" | "struct" | "enum"
504                | "if" | "unless" | "while" | "until" | "for" | "foreach"
505                | "return" | "last" | "next" | "redo" | "package" | "require"
506                | "BEGIN" | "END" | "UNITCHECK" | "frozen" | "const" | "typed"
507                // stryke-specific declaration keywords that start a new
508                // statement on a fresh line. Without these, a bare `use
509                // strict` / `use warnings` followed by `fn foo { ... }`
510                // on the next line swallows `foo` as an import argument.
511                | "fn" | "class" | "abstract" | "final" | "trait"
512                | "state" | "mysync" | "oursync"
513            )
514        )
515    }
516
517    /// True when the next token is on a different line from `stmt_line` and could
518    /// start a new statement. More permissive than `next_is_new_stmt_keyword` —
519    /// includes sigil-prefixed variables like `$var`, `@arr`, `%hash`.
520    fn next_is_new_statement_start(&self, stmt_line: usize) -> bool {
521        if crate::compat_mode() {
522            return false;
523        }
524        if self.peek_line() == stmt_line {
525            return false;
526        }
527        matches!(
528            self.peek(),
529            Token::ScalarVar(_)
530                | Token::DerefScalarVar(_)
531                | Token::ArrayVar(_)
532                | Token::HashVar(_)
533                | Token::LBrace
534        ) || self.next_is_new_stmt_keyword(stmt_line)
535    }
536
537    // ── Top level ──
538
539    pub fn parse_program(&mut self) -> StrykeResult<Program> {
540        let mut statements = self.parse_statements()?;
541        // Prepend any synthetic SubDecl stubs queued by anonymous overload
542        // handlers so the package-qualified synthetic names resolve when the
543        // overload table is consulted at runtime. (PARITY-012)
544        if !self.pending_synthetic_subs.is_empty() {
545            let synthetics = std::mem::take(&mut self.pending_synthetic_subs);
546            let mut combined = Vec::with_capacity(synthetics.len() + statements.len());
547            combined.extend(synthetics);
548            combined.append(&mut statements);
549            statements = combined;
550        }
551        Ok(Program { statements })
552    }
553
554    /// Parse statements until EOF. Used by parse_program and parse_block_from_str.
555    pub fn parse_statements(&mut self) -> StrykeResult<Vec<Statement>> {
556        let mut statements = Vec::new();
557        while !self.at_eof() {
558            if matches!(self.peek(), Token::Semicolon) {
559                let line = self.peek_line();
560                self.advance();
561                statements.push(Statement {
562                    label: None,
563                    kind: StmtKind::Empty,
564                    line,
565                });
566                continue;
567            }
568            statements.push(self.parse_statement()?);
569        }
570        Ok(statements)
571    }
572
573    // ── Statements ──
574
575    fn parse_statement(&mut self) -> StrykeResult<Statement> {
576        let line = self.peek_line();
577
578        // Statement label `FOO:` / `boot:` / `BAR_BAZ:` (not `Foo::` — that is `Ident` + `::`).
579        // Uppercase-only was too strict: XSLoader.pm uses `boot:` before `my $xs = ...`.
580        let label = match self.peek().clone() {
581            Token::Ident(_) => {
582                if matches!(self.peek_at(1), Token::Colon)
583                    && !matches!(self.peek_at(2), Token::Colon)
584                {
585                    let (tok, _) = self.advance();
586                    let l = match tok {
587                        Token::Ident(l) => l,
588                        _ => unreachable!(),
589                    };
590                    self.advance(); // ':'
591                    Some(l)
592                } else {
593                    None
594                }
595            }
596            _ => None,
597        };
598
599        let mut stmt = match self.peek().clone() {
600            Token::FormatDecl { .. } => {
601                let tok_line = self.peek_line();
602                let (tok, _) = self.advance();
603                match tok {
604                    Token::FormatDecl { name, lines } => Statement {
605                        label: label.clone(),
606                        kind: StmtKind::FormatDecl { name, lines },
607                        line: tok_line,
608                    },
609                    _ => unreachable!(),
610                }
611            }
612            Token::Ident(ref kw) => match kw.as_str() {
613                "if" => self.parse_if()?,
614                "unless" => self.parse_unless()?,
615                "while" => {
616                    let mut s = self.parse_while()?;
617                    if let StmtKind::While {
618                        label: ref mut lbl, ..
619                    } = s.kind
620                    {
621                        *lbl = label.clone();
622                    }
623                    s
624                }
625                "until" => {
626                    let mut s = self.parse_until()?;
627                    if let StmtKind::Until {
628                        label: ref mut lbl, ..
629                    } = s.kind
630                    {
631                        *lbl = label.clone();
632                    }
633                    s
634                }
635                "for" => {
636                    let mut s = self.parse_for_or_foreach()?;
637                    match s.kind {
638                        StmtKind::For {
639                            label: ref mut lbl, ..
640                        }
641                        | StmtKind::Foreach {
642                            label: ref mut lbl, ..
643                        } => *lbl = label.clone(),
644                        _ => {}
645                    }
646                    s
647                }
648                "foreach" => {
649                    let mut s = self.parse_foreach()?;
650                    if let StmtKind::Foreach {
651                        label: ref mut lbl, ..
652                    } = s.kind
653                    {
654                        *lbl = label.clone();
655                    }
656                    s
657                }
658                "sub" => {
659                    if crate::no_interop_mode() {
660                        return Err(self.syntax_err(
661                            "stryke uses `fn` instead of `sub` (--no-interop is active)",
662                            self.peek_line(),
663                        ));
664                    }
665                    self.parse_sub_decl(true)?
666                }
667                "fn" => self.parse_sub_decl(false)?,
668                "struct" => {
669                    if crate::compat_mode() {
670                        return Err(self.syntax_err(
671                            "`struct` is a stryke extension (disabled by --compat)",
672                            self.peek_line(),
673                        ));
674                    }
675                    self.parse_struct_decl()?
676                }
677                "enum" => {
678                    if crate::compat_mode() {
679                        return Err(self.syntax_err(
680                            "`enum` is a stryke extension (disabled by --compat)",
681                            self.peek_line(),
682                        ));
683                    }
684                    self.parse_enum_decl()?
685                }
686                "class" => {
687                    if crate::compat_mode() {
688                        // TODO: parse Perl 5.38 class syntax with :isa()
689                        return Err(self.syntax_err(
690                            "Perl 5.38 `class` syntax not yet implemented in --compat mode",
691                            self.peek_line(),
692                        ));
693                    }
694                    self.parse_class_decl(false, false)?
695                }
696                "abstract" => {
697                    self.advance(); // abstract
698                    if !matches!(self.peek(), Token::Ident(ref s) if s == "class") {
699                        return Err(self.syntax_err(
700                            "`abstract` must be followed by `class`",
701                            self.peek_line(),
702                        ));
703                    }
704                    self.parse_class_decl(true, false)?
705                }
706                "final" => {
707                    self.advance(); // final
708                    if !matches!(self.peek(), Token::Ident(ref s) if s == "class") {
709                        return Err(self
710                            .syntax_err("`final` must be followed by `class`", self.peek_line()));
711                    }
712                    self.parse_class_decl(false, true)?
713                }
714                "trait" => {
715                    if crate::compat_mode() {
716                        return Err(self.syntax_err(
717                            "`trait` is a stryke extension (disabled by --compat)",
718                            self.peek_line(),
719                        ));
720                    }
721                    self.parse_trait_decl()?
722                }
723                "my" => self.parse_my_our_local("my", false)?,
724                "state" => self.parse_my_our_local("state", false)?,
725                "mysync" => {
726                    if crate::compat_mode() {
727                        return Err(self.syntax_err(
728                            "`mysync` is a stryke extension (disabled by --compat)",
729                            self.peek_line(),
730                        ));
731                    }
732                    self.parse_my_our_local("mysync", false)?
733                }
734                "oursync" => {
735                    if crate::compat_mode() {
736                        return Err(self.syntax_err(
737                            "`oursync` is a stryke extension (disabled by --compat)",
738                            self.peek_line(),
739                        ));
740                    }
741                    self.parse_my_our_local("oursync", false)?
742                }
743                "frozen" | "const" => {
744                    let leading = kw.as_str().to_string();
745                    if crate::compat_mode() {
746                        return Err(self.syntax_err(
747                            format!("`{leading}` is a stryke extension (disabled by --compat)"),
748                            self.peek_line(),
749                        ));
750                    }
751                    // `frozen my $x = val;` / `const my $x = val;` — the
752                    // two spellings are interchangeable (`const` is the
753                    // more-familiar name for new users). Expects `my`
754                    // to follow.
755                    self.advance(); // consume "frozen"/"const"
756                    if let Token::Ident(ref kw) = self.peek().clone() {
757                        if kw == "my" {
758                            // Accept type annotations the same way `typed
759                            // my $x : Int` does — `const`/`frozen` is
760                            // orthogonal to typing, and `: Type` after a
761                            // name is unambiguous in either form.
762                            let mut stmt = self.parse_my_our_local("my", true)?;
763                            if let StmtKind::My(ref mut decls) = stmt.kind {
764                                for decl in decls.iter_mut() {
765                                    decl.frozen = true;
766                                }
767                            }
768                            stmt
769                        } else {
770                            return Err(self.syntax_err(
771                                format!("Expected 'my' after '{leading}'"),
772                                self.peek_line(),
773                            ));
774                        }
775                    } else {
776                        return Err(self.syntax_err(
777                            format!("Expected 'my' after '{leading}'"),
778                            self.peek_line(),
779                        ));
780                    }
781                }
782                "typed" => {
783                    if crate::compat_mode() {
784                        return Err(self.syntax_err(
785                            "`typed` is a stryke extension (disabled by --compat)",
786                            self.peek_line(),
787                        ));
788                    }
789                    self.advance();
790                    if let Token::Ident(ref kw) = self.peek().clone() {
791                        if kw == "my" {
792                            self.parse_my_our_local("my", true)?
793                        } else {
794                            return Err(
795                                self.syntax_err("Expected 'my' after 'typed'", self.peek_line())
796                            );
797                        }
798                    } else {
799                        return Err(
800                            self.syntax_err("Expected 'my' after 'typed'", self.peek_line())
801                        );
802                    }
803                }
804                "our" => self.parse_my_our_local("our", false)?,
805                "local" => self.parse_my_our_local("local", false)?,
806                "package" => self.parse_package()?,
807                "use" => self.parse_use()?,
808                "no" => self.parse_no()?,
809                "return" => self.parse_return()?,
810                "last" => {
811                    self.advance();
812                    let lbl = self.try_take_loop_label();
813                    let stmt = Statement {
814                        label: None,
815                        kind: StmtKind::Last(lbl.or(label.clone())),
816                        line,
817                    };
818                    self.parse_stmt_postfix_modifier(stmt)?
819                }
820                "next" => {
821                    self.advance();
822                    let lbl = self.try_take_loop_label();
823                    let stmt = Statement {
824                        label: None,
825                        kind: StmtKind::Next(lbl.or(label.clone())),
826                        line,
827                    };
828                    self.parse_stmt_postfix_modifier(stmt)?
829                }
830                "redo" => {
831                    self.advance();
832                    let lbl = self.try_take_loop_label();
833                    let stmt = Statement {
834                        label: None,
835                        kind: StmtKind::Redo(lbl.or(label.clone())),
836                        line,
837                    };
838                    self.parse_stmt_postfix_modifier(stmt)?
839                }
840                "BEGIN" => {
841                    self.advance();
842                    let block = self.parse_block()?;
843                    Statement {
844                        label: None,
845                        kind: StmtKind::Begin(block),
846                        line,
847                    }
848                }
849                "END" => {
850                    self.advance();
851                    let block = self.parse_block()?;
852                    Statement {
853                        label: None,
854                        kind: StmtKind::End(block),
855                        line,
856                    }
857                }
858                "UNITCHECK" => {
859                    self.advance();
860                    let block = self.parse_block()?;
861                    Statement {
862                        label: None,
863                        kind: StmtKind::UnitCheck(block),
864                        line,
865                    }
866                }
867                "CHECK" => {
868                    self.advance();
869                    let block = self.parse_block()?;
870                    Statement {
871                        label: None,
872                        kind: StmtKind::Check(block),
873                        line,
874                    }
875                }
876                "INIT" => {
877                    self.advance();
878                    let block = self.parse_block()?;
879                    Statement {
880                        label: None,
881                        kind: StmtKind::Init(block),
882                        line,
883                    }
884                }
885                "goto" => {
886                    self.advance();
887                    let target = self.parse_expression()?;
888                    let stmt = Statement {
889                        label: None,
890                        kind: StmtKind::Goto {
891                            target: Box::new(target),
892                        },
893                        line,
894                    };
895                    // `goto $l if COND;` / `goto &$cr if defined &$cr;` (XSLoader.pm)
896                    self.parse_stmt_postfix_modifier(stmt)?
897                }
898                "continue" => {
899                    self.advance();
900                    let block = self.parse_block()?;
901                    Statement {
902                        label: None,
903                        kind: StmtKind::Continue(block),
904                        line,
905                    }
906                }
907                "before"
908                    if matches!(
909                        self.peek_at(1),
910                        Token::SingleString(_) | Token::DoubleString(_)
911                    ) =>
912                {
913                    self.parse_advice_decl(crate::ast::AdviceKind::Before)?
914                }
915                "after"
916                    if matches!(
917                        self.peek_at(1),
918                        Token::SingleString(_) | Token::DoubleString(_)
919                    ) =>
920                {
921                    self.parse_advice_decl(crate::ast::AdviceKind::After)?
922                }
923                "around"
924                    if matches!(
925                        self.peek_at(1),
926                        Token::SingleString(_) | Token::DoubleString(_)
927                    ) =>
928                {
929                    self.parse_advice_decl(crate::ast::AdviceKind::Around)?
930                }
931                "try" => self.parse_try_catch()?,
932                "defer" => self.parse_defer_stmt()?,
933                "tie" => self.parse_tie_stmt()?,
934                "given" => self.parse_given()?,
935                "when" => self.parse_when_stmt()?,
936                "default" => self.parse_default_stmt()?,
937                "eval_timeout" => self.parse_eval_timeout()?,
938                "do" => {
939                    if matches!(self.peek_at(1), Token::LBrace) {
940                        self.advance();
941                        let body = self.parse_block()?;
942                        if let Token::Ident(ref w) = self.peek().clone() {
943                            if w == "while" {
944                                self.advance();
945                                self.expect(&Token::LParen)?;
946                                let mut condition = self.parse_expression()?;
947                                Self::mark_match_scalar_g_for_boolean_condition(&mut condition);
948                                self.expect(&Token::RParen)?;
949                                self.eat(&Token::Semicolon);
950                                Statement {
951                                    label: label.clone(),
952                                    kind: StmtKind::DoWhile { body, condition },
953                                    line,
954                                }
955                            } else {
956                                let inner_line = body.first().map(|s| s.line).unwrap_or(line);
957                                let inner = Expr {
958                                    kind: ExprKind::CodeRef {
959                                        params: vec![],
960                                        body,
961                                    },
962                                    line: inner_line,
963                                };
964                                let expr = Expr {
965                                    kind: ExprKind::Do(Box::new(inner)),
966                                    line,
967                                };
968                                let stmt = Statement {
969                                    label: label.clone(),
970                                    kind: StmtKind::Expression(expr),
971                                    line,
972                                };
973                                // `do { } if EXPR` / `do { } unless EXPR` — postfix modifier, not a new `if (` statement.
974                                self.parse_stmt_postfix_modifier(stmt)?
975                            }
976                        } else {
977                            let inner_line = body.first().map(|s| s.line).unwrap_or(line);
978                            let inner = Expr {
979                                kind: ExprKind::CodeRef {
980                                    params: vec![],
981                                    body,
982                                },
983                                line: inner_line,
984                            };
985                            let expr = Expr {
986                                kind: ExprKind::Do(Box::new(inner)),
987                                line,
988                            };
989                            let stmt = Statement {
990                                label: label.clone(),
991                                kind: StmtKind::Expression(expr),
992                                line,
993                            };
994                            self.parse_stmt_postfix_modifier(stmt)?
995                        }
996                    } else {
997                        if let Some(expr) = self.try_parse_bareword_stmt_call() {
998                            let stmt = self.maybe_postfix_modifier(expr)?;
999                            self.parse_stmt_postfix_modifier(stmt)?
1000                        } else {
1001                            let expr = self.parse_expression()?;
1002                            let stmt = self.maybe_postfix_modifier(expr)?;
1003                            self.parse_stmt_postfix_modifier(stmt)?
1004                        }
1005                    }
1006                }
1007                _ => {
1008                    // `foo;` or `{ foo }` — bareword statement is a zero-arg call (topic `$_` at runtime).
1009                    if let Some(expr) = self.try_parse_bareword_stmt_call() {
1010                        let stmt = self.maybe_postfix_modifier(expr)?;
1011                        self.parse_stmt_postfix_modifier(stmt)?
1012                    } else {
1013                        let expr = self.parse_expression()?;
1014                        let stmt = self.maybe_postfix_modifier(expr)?;
1015                        self.parse_stmt_postfix_modifier(stmt)?
1016                    }
1017                }
1018            },
1019            Token::LBrace => {
1020                // Disambiguate hashref `{ k => v }` from block `{ stmt; stmt }`.
1021                // If it looks like a hashref, parse as expression; otherwise parse as block.
1022                if self.looks_like_hashref() {
1023                    let expr = self.parse_expression()?;
1024                    let stmt = self.maybe_postfix_modifier(expr)?;
1025                    self.parse_stmt_postfix_modifier(stmt)?
1026                } else {
1027                    let block = self.parse_block()?;
1028                    let stmt = Statement {
1029                        label: None,
1030                        kind: StmtKind::Block(block),
1031                        line,
1032                    };
1033                    // `{ … } if EXPR` / `{ … } unless EXPR` — same postfix rule as `do { } if …` (not `if (`).
1034                    self.parse_stmt_postfix_modifier(stmt)?
1035                }
1036            }
1037            _ => {
1038                let expr = self.parse_expression()?;
1039                let stmt = self.maybe_postfix_modifier(expr)?;
1040                self.parse_stmt_postfix_modifier(stmt)?
1041            }
1042        };
1043
1044        stmt.label = label;
1045        Ok(stmt)
1046    }
1047
1048    /// Consume an immediately-following loop label after `next`/`last`/`redo`.
1049    /// Matches identifiers that look like Perl loop labels — uppercase letters,
1050    /// digits, or `_`, and must start with a non-digit. Anything else
1051    /// (lowercase function names, `if`, `unless`, `(EXPR`, …) is left for the
1052    /// `EXPR`-form / postfix-modifier paths.
1053    fn try_take_loop_label(&mut self) -> Option<String> {
1054        let Token::Ident(s) = self.peek() else {
1055            return None;
1056        };
1057        let mut chars = s.chars();
1058        let first = chars.next()?;
1059        if !(first.is_ascii_uppercase() || first == '_') {
1060            return None;
1061        }
1062        let ok = s
1063            .chars()
1064            .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_');
1065        if !ok {
1066            return None;
1067        }
1068        let (Token::Ident(l), _) = self.advance() else {
1069            unreachable!()
1070        };
1071        Some(l)
1072    }
1073
1074    /// Handle postfix if/unless on statement-level keywords like last/next.
1075    fn parse_stmt_postfix_modifier(&mut self, stmt: Statement) -> StrykeResult<Statement> {
1076        let line = stmt.line;
1077        // Implicit semicolon: a modifier keyword on a new line is a new
1078        // statement, not a postfix modifier.  This prevents semicolon-less
1079        // code like `my $x = "val"\nif ($x) { ... }` from being mis-parsed
1080        // as `my $x = "val" if ($x) { ... }`.
1081        if self.peek_line() > self.prev_line() {
1082            self.eat(&Token::Semicolon);
1083            return Ok(stmt);
1084        }
1085        if let Token::Ident(ref kw) = self.peek().clone() {
1086            match kw.as_str() {
1087                "if" => {
1088                    self.advance();
1089                    let mut cond = self.parse_expression()?;
1090                    Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
1091                    self.eat(&Token::Semicolon);
1092                    return Ok(Statement {
1093                        label: None,
1094                        kind: StmtKind::If {
1095                            condition: cond,
1096                            body: vec![stmt],
1097                            elsifs: vec![],
1098                            else_block: None,
1099                        },
1100                        line,
1101                    });
1102                }
1103                "unless" => {
1104                    self.advance();
1105                    let mut cond = self.parse_expression()?;
1106                    Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
1107                    self.eat(&Token::Semicolon);
1108                    return Ok(Statement {
1109                        label: None,
1110                        kind: StmtKind::Unless {
1111                            condition: cond,
1112                            body: vec![stmt],
1113                            else_block: None,
1114                        },
1115                        line,
1116                    });
1117                }
1118                "while" | "until" | "for" | "foreach" => {
1119                    // `do { } for @a` / `{ } while COND` — same postfix forms as [`maybe_postfix_modifier`],
1120                    // not a new `for (` / `while (` statement (which would require `(` after `for`).
1121                    if let Some(expr) = Self::stmt_into_postfix_body_expr(stmt) {
1122                        let out = self.maybe_postfix_modifier(expr)?;
1123                        self.eat(&Token::Semicolon);
1124                        return Ok(out);
1125                    }
1126                    return Err(self.syntax_err(
1127                        format!("postfix `{}` is not supported on this statement form", kw),
1128                        self.peek_line(),
1129                    ));
1130                }
1131                // `{ } pmap @a` / `{ } pflat_map @a` / `{ } pfor @a` / `do { } …` — same shapes as prefix forms.
1132                "pmap" | "pflat_map" | "pgrep" | "pfor" | "preduce" | "pcache" | "par" => {
1133                    let line = stmt.line;
1134                    let block = self.stmt_into_parallel_block(stmt)?;
1135                    let which = kw.as_str();
1136                    self.advance();
1137                    self.eat(&Token::Comma);
1138                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
1139                    self.eat(&Token::Semicolon);
1140                    let list = Box::new(list);
1141                    let progress = progress.map(Box::new);
1142                    let kind = match which {
1143                        "pmap" => ExprKind::PMapExpr {
1144                            block,
1145                            list,
1146                            progress,
1147                            flat_outputs: false,
1148                            on_cluster: None,
1149                            stream: false,
1150                        },
1151                        "pflat_map" => ExprKind::PMapExpr {
1152                            block,
1153                            list,
1154                            progress,
1155                            flat_outputs: true,
1156                            on_cluster: None,
1157                            stream: false,
1158                        },
1159                        "pgrep" => ExprKind::PGrepExpr {
1160                            block,
1161                            list,
1162                            progress,
1163                            stream: false,
1164                        },
1165                        "pfor" => ExprKind::PForExpr {
1166                            block,
1167                            list,
1168                            progress,
1169                        },
1170                        "preduce" => ExprKind::PReduceExpr {
1171                            block,
1172                            list,
1173                            progress,
1174                        },
1175                        "pcache" => ExprKind::PcacheExpr {
1176                            block,
1177                            list,
1178                            progress,
1179                        },
1180                        "par" => ExprKind::ParExpr { block, list },
1181                        _ => unreachable!(),
1182                    };
1183                    return Ok(Statement {
1184                        label: None,
1185                        kind: StmtKind::Expression(Expr { kind, line }),
1186                        line,
1187                    });
1188                }
1189                _ => {}
1190            }
1191        }
1192        self.eat(&Token::Semicolon);
1193        Ok(stmt)
1194    }
1195
1196    /// Block body for postfix `pmap` / `pfor` / … — bare `{ }`, `do { }`, or any expression
1197    /// statement (wrapped as a one-line block, e.g. `` `cmd` pfor @a ``).
1198    fn stmt_into_parallel_block(&self, stmt: Statement) -> StrykeResult<Block> {
1199        let line = stmt.line;
1200        match stmt.kind {
1201            StmtKind::Block(block) => Ok(block),
1202            StmtKind::Expression(expr) => {
1203                if let ExprKind::Do(ref inner) = expr.kind {
1204                    if let ExprKind::CodeRef { ref body, .. } = inner.kind {
1205                        return Ok(body.clone());
1206                    }
1207                }
1208                Ok(vec![Statement {
1209                    label: None,
1210                    kind: StmtKind::Expression(expr),
1211                    line,
1212                }])
1213            }
1214            _ => Err(self.syntax_err(
1215                "postfix parallel op expects `do { }`, a bare `{ }` block, or an expression statement",
1216                line,
1217            )),
1218        }
1219    }
1220
1221    /// `StmtKind::Expression` or a bare block (`StmtKind::Block`) as an [`Expr`] for postfix
1222    /// `while` / `until` / `for` / `foreach` (mirrors `do { }` → [`ExprKind::Do`]\([`ExprKind::CodeRef`]\)).
1223    fn stmt_into_postfix_body_expr(stmt: Statement) -> Option<Expr> {
1224        match stmt.kind {
1225            StmtKind::Expression(expr) => Some(expr),
1226            StmtKind::Block(block) => {
1227                let line = stmt.line;
1228                let inner = Expr {
1229                    kind: ExprKind::CodeRef {
1230                        params: vec![],
1231                        body: block,
1232                    },
1233                    line,
1234                };
1235                Some(Expr {
1236                    kind: ExprKind::Do(Box::new(inner)),
1237                    line,
1238                })
1239            }
1240            _ => None,
1241        }
1242    }
1243
1244    /// Statement-modifier keywords that must not be consumed as part of a comma-separated list
1245    /// (same set as [`parse_list_until_terminator`]).
1246    fn peek_is_postfix_stmt_modifier_keyword(&self) -> bool {
1247        matches!(
1248            self.peek(),
1249            Token::Ident(ref kw)
1250                if matches!(
1251                    kw.as_str(),
1252                    "if" | "unless" | "while" | "until" | "for" | "foreach"
1253                )
1254        )
1255    }
1256
1257    /// Token classes whose precedence sits below a Perl-style named unary
1258    /// operator. When one of these is the next token after a unary keyword
1259    /// (`length`, `len`, `cnt`, …), the keyword takes no explicit argument
1260    /// and the surrounding expression continues. Mirrors the `parse_one_arg_or_default`
1261    /// boundary set; kept as a separate predicate so other parse paths can
1262    /// reuse it without committing to default-to-`$_` semantics.
1263    fn peek_is_named_unary_terminator(&self) -> bool {
1264        matches!(
1265            self.peek(),
1266            Token::Semicolon
1267                | Token::RBrace
1268                | Token::RParen
1269                | Token::RBracket
1270                | Token::Eof
1271                | Token::Comma
1272                | Token::FatArrow
1273                | Token::PipeForward
1274                | Token::Question
1275                | Token::Colon
1276                | Token::NumEq
1277                | Token::NumNe
1278                | Token::NumLt
1279                | Token::NumGt
1280                | Token::NumLe
1281                | Token::NumGe
1282                | Token::Spaceship
1283                | Token::StrEq
1284                | Token::StrNe
1285                | Token::StrLt
1286                | Token::StrGt
1287                | Token::StrLe
1288                | Token::StrGe
1289                | Token::StrCmp
1290                | Token::LogAnd
1291                | Token::LogOr
1292                | Token::LogAndWord
1293                | Token::LogOrWord
1294                | Token::DefinedOr
1295                | Token::Range
1296                | Token::RangeExclusive
1297                | Token::Assign
1298                | Token::PlusAssign
1299                | Token::MinusAssign
1300                | Token::MulAssign
1301                | Token::DivAssign
1302                | Token::ModAssign
1303                | Token::PowAssign
1304                | Token::DotAssign
1305                | Token::AndAssign
1306                | Token::OrAssign
1307                | Token::XorAssign
1308                | Token::DefinedOrAssign
1309                | Token::ShiftLeftAssign
1310                | Token::ShiftRightAssign
1311                | Token::BitAndAssign
1312                | Token::BitOrAssign
1313        )
1314    }
1315
1316    fn maybe_postfix_modifier(&mut self, expr: Expr) -> StrykeResult<Statement> {
1317        let line = expr.line;
1318        // Implicit semicolon: modifier keyword on a new line starts a new statement.
1319        if self.peek_line() > self.prev_line() {
1320            return Ok(Statement {
1321                label: None,
1322                kind: StmtKind::Expression(expr),
1323                line,
1324            });
1325        }
1326        match self.peek() {
1327            Token::Ident(ref kw) => match kw.as_str() {
1328                "if" => {
1329                    self.advance();
1330                    let cond = self.parse_expression()?;
1331                    Ok(Statement {
1332                        label: None,
1333                        kind: StmtKind::Expression(Expr {
1334                            kind: ExprKind::PostfixIf {
1335                                expr: Box::new(expr),
1336                                condition: Box::new(cond),
1337                            },
1338                            line,
1339                        }),
1340                        line,
1341                    })
1342                }
1343                "unless" => {
1344                    self.advance();
1345                    let cond = self.parse_expression()?;
1346                    Ok(Statement {
1347                        label: None,
1348                        kind: StmtKind::Expression(Expr {
1349                            kind: ExprKind::PostfixUnless {
1350                                expr: Box::new(expr),
1351                                condition: Box::new(cond),
1352                            },
1353                            line,
1354                        }),
1355                        line,
1356                    })
1357                }
1358                "while" => {
1359                    self.advance();
1360                    let cond = self.parse_expression()?;
1361                    Ok(Statement {
1362                        label: None,
1363                        kind: StmtKind::Expression(Expr {
1364                            kind: ExprKind::PostfixWhile {
1365                                expr: Box::new(expr),
1366                                condition: Box::new(cond),
1367                            },
1368                            line,
1369                        }),
1370                        line,
1371                    })
1372                }
1373                "until" => {
1374                    self.advance();
1375                    let cond = self.parse_expression()?;
1376                    Ok(Statement {
1377                        label: None,
1378                        kind: StmtKind::Expression(Expr {
1379                            kind: ExprKind::PostfixUntil {
1380                                expr: Box::new(expr),
1381                                condition: Box::new(cond),
1382                            },
1383                            line,
1384                        }),
1385                        line,
1386                    })
1387                }
1388                "for" | "foreach" => {
1389                    self.advance();
1390                    let list = self.parse_expression()?;
1391                    Ok(Statement {
1392                        label: None,
1393                        kind: StmtKind::Expression(Expr {
1394                            kind: ExprKind::PostfixForeach {
1395                                expr: Box::new(expr),
1396                                list: Box::new(list),
1397                            },
1398                            line,
1399                        }),
1400                        line,
1401                    })
1402                }
1403                _ => Ok(Statement {
1404                    label: None,
1405                    kind: StmtKind::Expression(expr),
1406                    line,
1407                }),
1408            },
1409            _ => Ok(Statement {
1410                label: None,
1411                kind: StmtKind::Expression(expr),
1412                line,
1413            }),
1414        }
1415    }
1416
1417    /// `name;` or `name}` — a bare identifier statement is a sub call with no explicit args (`$_` implied).
1418    fn try_parse_bareword_stmt_call(&mut self) -> Option<Expr> {
1419        let saved = self.pos;
1420        let line = self.peek_line();
1421        let mut name = match self.peek() {
1422            Token::Ident(n) => n.clone(),
1423            _ => return None,
1424        };
1425        // Names that begin `parse_named_expr` (builtins / `undef` / …) must use that path, not a sub call.
1426        if name.starts_with('\x00') || !Self::bareword_stmt_may_be_sub(&name) {
1427            return None;
1428        }
1429        self.advance();
1430        while self.eat(&Token::PackageSep) {
1431            match self.advance() {
1432                (Token::Ident(part), _) => {
1433                    name = format!("{}::{}", name, part);
1434                }
1435                _ => {
1436                    self.pos = saved;
1437                    return None;
1438                }
1439            }
1440        }
1441        match self.peek() {
1442            Token::Semicolon | Token::RBrace => Some(Expr {
1443                kind: ExprKind::FuncCall { name, args: vec![] },
1444                line,
1445            }),
1446            _ => {
1447                self.pos = saved;
1448                None
1449            }
1450        }
1451    }
1452
1453    /// Map an operator-keyword token (the lexer converts `eq`, `ne`, …, `and`,
1454    /// `or`, `not`, `x` to dedicated tokens) back to its identifier spelling.
1455    /// Used in hash-key contexts where the bareword form is the user's intent.
1456    pub(crate) fn operator_keyword_to_ident_str(tok: &Token) -> Option<&'static str> {
1457        Some(match tok {
1458            Token::StrEq => "eq",
1459            Token::StrNe => "ne",
1460            Token::StrLt => "lt",
1461            Token::StrGt => "gt",
1462            Token::StrLe => "le",
1463            Token::StrGe => "ge",
1464            Token::StrCmp => "cmp",
1465            Token::LogAndWord => "and",
1466            Token::LogOrWord => "or",
1467            Token::LogNotWord => "not",
1468            Token::X => "x",
1469            _ => return None,
1470        })
1471    }
1472
1473    /// Bare names that resolve to the topic-slot scalar matrix:
1474    /// `_`, `_0`, `_1`, …, `_N`, plus `_<+`, `_N<+` for the 4-deep outer chain.
1475    /// These must NOT be treated as zero-arg sub calls — they're scalar var refs.
1476    pub(crate) fn is_underscore_topic_slot(name: &str) -> bool {
1477        if name == "_" {
1478            return true;
1479        }
1480        if !name.starts_with('_') || name.len() < 2 {
1481            return false;
1482        }
1483        let bytes = name.as_bytes();
1484        let mut i = 1;
1485        // Optional digit run (positional slot index).
1486        while i < bytes.len() && bytes[i].is_ascii_digit() {
1487            i += 1;
1488        }
1489        // Then any number of `<` chevrons (runtime cap at 5; lexer accepts more).
1490        let chevrons_start = i;
1491        while i < bytes.len() && bytes[i] == b'<' {
1492            i += 1;
1493        }
1494        // Must be one of: `_`, `_N`, `_<+`, `_N<+`. No other trailing chars.
1495        i == bytes.len() && (i > 1 || chevrons_start > 1)
1496    }
1497
1498    /// Bareword names that map to Perl special variables / filehandles /
1499    /// compile-time tokens. A user-defined sub with any of these names
1500    /// would shadow the special variable's expression-position usage and
1501    /// produce silently-broken code. Reject at parse time with a
1502    /// foot-gun error message.
1503    ///
1504    /// Sigil-form spellings (`$@`, `$!`, `@ARGV`, `%ENV`, etc.) are caught
1505    /// separately via the `parse_sub_decl` catch-all branch — those don't
1506    /// even lex as `Token::Ident` so they hit a different code path.
1507    pub(crate) fn is_reserved_special_var_name(name: &str) -> bool {
1508        matches!(
1509            name,
1510            // Standard filehandles (Perl: STDIN, STDOUT, STDERR, ARGV, …)
1511            "STDIN" | "STDOUT" | "STDERR" | "ARGV" | "ARGVOUT" | "DATA"
1512            // Package globals, normally accessed via sigils (@ARGV, %ENV,
1513            // @INC, %SIG, @ISA, %ENV, etc.) — bareword shadow is a foot-gun.
1514            // NOTE: `AUTOLOAD` is intentionally NOT in this list — `fn
1515            // AUTOLOAD { ... }` is the legitimate Perl idiom for handling
1516            // missing-method dispatch. The runtime sets `$AUTOLOAD` to the
1517            // missing sub's qualified name before invoking the user's
1518            // AUTOLOAD sub. Adding it here would break that mechanism.
1519            | "ENV" | "INC" | "SIG" | "ISA"
1520            | "EXPORT" | "EXPORT_OK" | "EXPORT_TAGS"
1521            | "VERSION"
1522            // Compile-time tokens (resolve to constants at parse time).
1523            | "__FILE__" | "__LINE__" | "__PACKAGE__" | "__SUB__"
1524            | "__DATA__" | "__END__"
1525        )
1526    }
1527
1528    /// Identifiers that start a [`parse_named_expr`] arm (builtins / special forms), not a bare sub call.
1529    fn bareword_stmt_may_be_sub(name: &str) -> bool {
1530        // Topic-slot scalar names (`_`, `_N`, `_<+`, `_N<+`) are scalar
1531        // variables, not zero-arg sub calls. Without this guard, the
1532        // statement-position parser would emit `Op::Call("_0", 0)` and fail
1533        // at runtime with "Undefined subroutine &_0".
1534        if Self::is_underscore_topic_slot(name) {
1535            return false;
1536        }
1537        !matches!(
1538            name,
1539            "__FILE__"
1540                | "__LINE__"
1541                | "__PACKAGE__"
1542                | "__SUB__"
1543                | "abs"
1544                | "async"
1545                | "spawn"
1546                | "atan2"
1547                | "await"
1548                | "barrier"
1549                | "bless"
1550                | "caller"
1551                | "capture"
1552                | "cat"
1553                | "chdir"
1554                | "chmod"
1555                | "chomp"
1556                | "chop"
1557                | "chr"
1558                | "chown"
1559                | "closedir"
1560                | "close"
1561                | "collect"
1562                | "cos"
1563                | "crypt"
1564                | "defined"
1565                | "dec"
1566                | "delete"
1567                | "die"
1568                | "deque"
1569                | "do"
1570                | "each"
1571                | "eof"
1572                | "fore"
1573                | "eval"
1574                | "exec"
1575                | "exists"
1576                | "exit"
1577                | "exp"
1578                | "fan"
1579                | "fan_cap"
1580                | "fc"
1581                | "fetch_url"
1582                | "d"
1583                | "dirs"
1584                | "dr"
1585                | "f"
1586                | "fi"
1587                | "files"
1588                | "filesf"
1589                | "filter"
1590                | "fr"
1591                | "getcwd"
1592                | "glob_par"
1593                | "par_sed"
1594                | "glob"
1595                | "grep"
1596                | "greps"
1597                | "heap"
1598                | "hex"
1599                | "inc"
1600                | "index"
1601                | "int"
1602                | "join"
1603                | "keys"
1604                | "lcfirst"
1605                | "lc"
1606                | "length"
1607                | "link"
1608                | "log"
1609                | "lstat"
1610                | "map"
1611                | "flat_map"
1612                | "maps"
1613                | "flat_maps"
1614                | "flatten"
1615                | "frequencies"
1616                | "freq"
1617                | "pfrequencies"
1618                | "pfreq"
1619                | "interleave"
1620                | "ddump"
1621                | "stringify"
1622                | "str"
1623                | "s"
1624                | "input"
1625                | "lines"
1626                | "words"
1627                | "chars"
1628                | "digits"
1629                | "letters"
1630                | "letters_uc"
1631                | "letters_lc"
1632                | "punctuation"
1633                | "sentences"
1634                | "paragraphs"
1635                | "sections"
1636                | "numbers"
1637                | "graphemes"
1638                | "columns"
1639                | "trim"
1640                | "avg"
1641                | "top"
1642                | "pager"
1643                | "pg"
1644                | "less"
1645                | "count_by"
1646                | "to_file"
1647                | "to_json"
1648                | "to_csv"
1649                | "grep_v"
1650                | "select_keys"
1651                | "pluck"
1652                | "clamp"
1653                | "normalize"
1654                | "stddev"
1655                | "squared"
1656                | "square"
1657                | "cubed"
1658                | "cube"
1659                | "expt"
1660                | "pow"
1661                | "pw"
1662                | "snake_case"
1663                | "camel_case"
1664                | "kebab_case"
1665                | "to_toml"
1666                | "to_yaml"
1667                | "to_xml"
1668                | "to_html"
1669                | "to_markdown"
1670                | "xopen"
1671                | "clip"
1672                | "paste"
1673                | "to_table"
1674                | "sparkline"
1675                | "bar_chart"
1676                | "flame"
1677                | "set"
1678                | "list_count"
1679                | "list_size"
1680                | "count"
1681                | "size"
1682                | "cnt"
1683                | "len"
1684                | "all"
1685                | "any"
1686                | "none"
1687                | "take_while"
1688                | "drop_while"
1689                | "skip_while"
1690                | "skip"
1691                | "first_or"
1692                | "tap"
1693                | "peek"
1694                | "partition"
1695                | "min_by"
1696                | "max_by"
1697                | "zip_with"
1698                | "group_by"
1699                | "chunk_by"
1700                | "with_index"
1701                | "puniq"
1702                | "pfirst"
1703                | "pany"
1704                | "uniq"
1705                | "distinct"
1706                | "shuffle"
1707                | "shuffled"
1708                | "chunked"
1709                | "windowed"
1710                | "match"
1711                | "mkdir"
1712                | "every"
1713                | "gen"
1714                | "oct"
1715                | "open"
1716                | "p"
1717                | "opendir"
1718                | "ord"
1719                | "par"
1720                | "par_lines"
1721                | "par_walk"
1722                | "pipe"
1723                | "pipes"
1724                | "block_devices"
1725                | "char_devices"
1726                | "exe"
1727                | "executables"
1728                | "rate_limit"
1729                | "retry"
1730                | "pcache"
1731                | "pchannel"
1732                | "pfor"
1733                | "pgrep"
1734                | "pgreps"
1735                | "pipeline"
1736                | "pmap_chunked"
1737                | "pmap_reduce"
1738                | "par_reduce"
1739                | "pmap_on"
1740                | "pflat_map_on"
1741                | "pmap"
1742                | "pmaps"
1743                | "pflat_map"
1744                | "pflat_maps"
1745                | "pop"
1746                | "pos"
1747                | "ppool"
1748                | "preduce_init"
1749                | "preduce"
1750                | "pselect"
1751                | "printf"
1752                | "print"
1753                | "pr"
1754                | "psort"
1755                | "push"
1756                | "pwatch"
1757                | "rand"
1758                | "readdir"
1759                | "readlink"
1760                | "reduce"
1761                | "fold"
1762                | "inject"
1763                | "first"
1764                | "detect"
1765                | "find"
1766                | "find_all"
1767                | "find_index"
1768                | "firstidx"
1769                | "first_index"
1770                | "ref"
1771                | "rename"
1772                | "require"
1773                | "rev"
1774                | "reverse"
1775                | "reversed"
1776                | "rewinddir"
1777                | "rindex"
1778                | "rmdir"
1779                | "rm"
1780                | "say"
1781                | "scalar"
1782                | "seekdir"
1783                | "shift"
1784                | "sin"
1785                | "slurp"
1786                | "sockets"
1787                | "sort"
1788                | "splice"
1789                | "splice_last"
1790                | "splice1"
1791                | "spl_last"
1792                | "split"
1793                | "sprintf"
1794                | "sqrt"
1795                | "srand"
1796                | "stat"
1797                | "study"
1798                | "substr"
1799                | "symlink"
1800                | "sym_links"
1801                | "system"
1802                | "telldir"
1803                | "timer"
1804                | "trace"
1805                | "ucfirst"
1806                | "uc"
1807                | "undef"
1808                | "umask"
1809                | "unlink"
1810                | "unshift"
1811                | "utime"
1812                | "values"
1813                | "wantarray"
1814                | "warn"
1815                | "watch"
1816                | "yield"
1817                | "sub"
1818        )
1819    }
1820
1821    fn parse_block(&mut self) -> StrykeResult<Block> {
1822        self.expect(&Token::LBrace)?;
1823        // Statements inside a block are NOT pipe RHS - reset depth so nested `~>`
1824        // parses its own input instead of using `$_[0]` placeholder.
1825        let saved_pipe_rhs_depth = self.pipe_rhs_depth;
1826        self.pipe_rhs_depth = 0;
1827        self.block_depth += 1;
1828        let mut stmts = Vec::new();
1829        // `{ |$a, $b| body }` — Ruby-style block params.
1830        // Desugars to `my $a = $_` (1 param), `my $a = $a; my $b = $b` (2 — sort/reduce),
1831        // or `my $p = $_N` for positional N≥3.
1832        if let Some(param_stmts) = self.try_parse_block_params()? {
1833            stmts.extend(param_stmts);
1834        }
1835        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
1836            if self.eat(&Token::Semicolon) {
1837                continue;
1838            }
1839            stmts.push(self.parse_statement()?);
1840        }
1841        self.expect(&Token::RBrace)?;
1842        self.pipe_rhs_depth = saved_pipe_rhs_depth;
1843        self.block_depth -= 1;
1844        Self::default_topic_for_sole_bareword(&mut stmts);
1845        Ok(stmts)
1846    }
1847
1848    /// Try to parse `|$var1, $var2, ...|` at the start of a block.
1849    /// Returns `None` if the leading `|` is not block-param syntax.
1850    /// When successful, returns `my $var = <implicit>` assignment statements
1851    /// that alias the block's positional arguments.
1852    fn try_parse_block_params(&mut self) -> StrykeResult<Option<Vec<Statement>>> {
1853        if !matches!(self.peek(), Token::BitOr) {
1854            return Ok(None);
1855        }
1856        // Lookahead: `| $scalar [, $scalar]* |` — verify before consuming.
1857        let mut i = 1; // skip the opening `|`
1858        loop {
1859            match self.peek_at(i) {
1860                Token::ScalarVar(_) => i += 1,
1861                _ => return Ok(None), // not `|$var...|`
1862            }
1863            match self.peek_at(i) {
1864                Token::BitOr => break,  // closing `|`
1865                Token::Comma => i += 1, // more params
1866                _ => return Ok(None),   // not block params
1867            }
1868        }
1869        // Confirmed — consume and build assignments.
1870        let line = self.peek_line();
1871        self.advance(); // eat opening `|`
1872        let mut names = Vec::new();
1873        loop {
1874            if let Token::ScalarVar(ref name) = self.peek().clone() {
1875                names.push(name.clone());
1876                self.advance();
1877            }
1878            if self.eat(&Token::BitOr) {
1879                break;
1880            }
1881            self.expect(&Token::Comma)?;
1882        }
1883        // Generate `my $name = <source>` for each param.
1884        // 1 param  → source is `$_` (map/grep/each/for topic)
1885        // 2 params → sources are `$a`, `$b` (sort/reduce)
1886        // N params → sources are `$_`, `$_1`, `$_2`, … (positional)
1887        let sources: Vec<&str> = match names.len() {
1888            1 => vec!["_"],
1889            2 => vec!["a", "b"],
1890            n => {
1891                // Can't return borrowed from a generated vec, handle below.
1892                let _ = n;
1893                vec![] // sentinel — handled in the else branch
1894            }
1895        };
1896        let mut stmts = Vec::with_capacity(names.len());
1897        if !sources.is_empty() {
1898            for (name, src) in names.iter().zip(sources.iter()) {
1899                stmts.push(Statement {
1900                    label: None,
1901                    kind: StmtKind::My(vec![VarDecl {
1902                        sigil: Sigil::Scalar,
1903                        name: name.clone(),
1904                        initializer: Some(Expr {
1905                            kind: ExprKind::ScalarVar(src.to_string()),
1906                            line,
1907                        }),
1908                        frozen: false,
1909                        type_annotation: None,
1910                        list_context: false,
1911                    }]),
1912                    line,
1913                });
1914            }
1915        } else {
1916            // N≥3: positional `$_`, `$_1`, `$_2`, …
1917            for (idx, name) in names.iter().enumerate() {
1918                let src = if idx == 0 {
1919                    "_".to_string()
1920                } else {
1921                    format!("_{idx}")
1922                };
1923                stmts.push(Statement {
1924                    label: None,
1925                    kind: StmtKind::My(vec![VarDecl {
1926                        sigil: Sigil::Scalar,
1927                        name: name.clone(),
1928                        initializer: Some(Expr {
1929                            kind: ExprKind::ScalarVar(src),
1930                            line,
1931                        }),
1932                        frozen: false,
1933                        type_annotation: None,
1934                        list_context: false,
1935                    }]),
1936                    line,
1937                });
1938            }
1939        }
1940        Ok(Some(stmts))
1941    }
1942
1943    /// Block shorthand: when the body is literally one bare builtin call
1944    /// (`{ uc }`, `{ basename }`, `{ to_json }`), inject `$_` as its first
1945    /// argument so `map { basename }` == `map { basename($_) }` uniformly.
1946    ///
1947    /// Without this, the ExprKind-modeled core names (`uc`/`lc`/`length`/…)
1948    /// default to `$_` via their own parse arms, but generic `FuncCall`-
1949    /// dispatched builtins (`basename`/`to_json`/`tj`/`bn`) are called with
1950    /// empty args and return the wrong value. This rewrite levels the
1951    /// playing field at parse time — no per-builtin handling needed.
1952    ///
1953    /// Narrow by design: fires only when the block has *exactly one*
1954    /// expression statement whose sole content is a known-bareword call
1955    /// with zero args. Multi-statement blocks and blocks with any other
1956    /// content are untouched.
1957    fn default_topic_for_sole_bareword(stmts: &mut [Statement]) {
1958        let [only] = stmts else { return };
1959        let StmtKind::Expression(ref mut expr) = only.kind else {
1960            return;
1961        };
1962        let topic_line = expr.line;
1963        let topic_arg = || Expr {
1964            kind: ExprKind::ScalarVar("_".to_string()),
1965            line: topic_line,
1966        };
1967        match expr.kind {
1968            // Zero-arg FuncCall whose name is a known builtin → inject `$_`.
1969            ExprKind::FuncCall {
1970                ref name,
1971                ref mut args,
1972            } if args.is_empty()
1973                && (Self::is_known_bareword(name) || Self::is_try_builtin_name(name)) =>
1974            {
1975                args.push(topic_arg());
1976            }
1977            // Lone bareword (the parser sometimes keeps a bareword as a
1978            // `Bareword` node instead of a zero-arg `FuncCall` —
1979            // e.g. `{ to_json }`, `{ ddump }`). Promote to a call.
1980            ExprKind::Bareword(ref name)
1981                if (Self::is_known_bareword(name) || Self::is_try_builtin_name(name)) =>
1982            {
1983                let n = name.clone();
1984                expr.kind = ExprKind::FuncCall {
1985                    name: n,
1986                    args: vec![topic_arg()],
1987                };
1988            }
1989            _ => {}
1990        }
1991    }
1992
1993    /// `defer { BLOCK }` — register a block to run when the current scope exits.
1994    /// Desugars to a `defer__internal(fn { BLOCK })` function call that the compiler
1995    /// handles specially by emitting Op::DeferBlock.
1996    fn parse_defer_stmt(&mut self) -> StrykeResult<Statement> {
1997        let line = self.peek_line();
1998        self.advance(); // defer
1999        let body = self.parse_block()?;
2000        self.eat(&Token::Semicolon);
2001        // Desugar: defer { BLOCK } → defer__internal(fn { BLOCK })
2002        let coderef = Expr {
2003            kind: ExprKind::CodeRef {
2004                params: vec![],
2005                body,
2006            },
2007            line,
2008        };
2009        Ok(Statement {
2010            label: None,
2011            kind: StmtKind::Expression(Expr {
2012                kind: ExprKind::FuncCall {
2013                    name: "defer__internal".to_string(),
2014                    args: vec![coderef],
2015                },
2016                line,
2017            }),
2018            line,
2019        })
2020    }
2021
2022    /// `try { } catch ($err) { }` with optional `finally { }`
2023    fn parse_try_catch(&mut self) -> StrykeResult<Statement> {
2024        let line = self.peek_line();
2025        self.advance(); // try
2026        let try_block = self.parse_block()?;
2027        match self.peek() {
2028            Token::Ident(ref k) if k == "catch" => {
2029                self.advance();
2030            }
2031            _ => {
2032                return Err(self.syntax_err("expected 'catch' after try block", self.peek_line()));
2033            }
2034        }
2035        self.expect(&Token::LParen)?;
2036        let catch_var = self.parse_scalar_var_name()?;
2037        self.expect(&Token::RParen)?;
2038        let catch_block = self.parse_block()?;
2039        let finally_block = match self.peek() {
2040            Token::Ident(ref k) if k == "finally" => {
2041                self.advance();
2042                Some(self.parse_block()?)
2043            }
2044            _ => None,
2045        };
2046        self.eat(&Token::Semicolon);
2047        Ok(Statement {
2048            label: None,
2049            kind: StmtKind::TryCatch {
2050                try_block,
2051                catch_var,
2052                catch_block,
2053                finally_block,
2054            },
2055            line,
2056        })
2057    }
2058
2059    /// `thread EXPR stage1 stage2 ...` — Clojure-style threading macro.
2060    /// Desugars to `EXPR |> stage1 |> stage2 |> ...`
2061    ///
2062    /// When `thread_last` is true (`->>` syntax), injects as last arg instead of first.
2063    ///
2064    /// When invoked as the RHS of `|>` (e.g. `LHS |> t s1 s2 ...`), the init
2065    /// is not parsed from tokens — using `parse_unary()` there lets the first
2066    /// bareword greedily consume the next token as its arg, which misparses
2067    /// `t inc pow($_, 2) p` as init=`inc(pow(…))` + stage=`p` instead of three
2068    /// separate stages. Instead, seed init with `$_[0]`, run every remaining
2069    /// token through the stage loop, and wrap the resulting chain in a
2070    /// `CodeRef`. The outer `pipe_forward_apply` then calls it with `lhs` as
2071    /// `$_[0]`, giving `LHS |> t s1 s2 s3` == `LHS |> s1 |> s2 |> s3`.
2072    fn parse_thread_macro(&mut self, _line: usize, thread_last: bool) -> StrykeResult<Expr> {
2073        self.parse_thread_macro_inner(_line, thread_last, None)
2074    }
2075
2076    /// Shared core for `~>` / `~>>` / `~s>` / `~s>>`. When
2077    /// `parallel_collector` is `Some` (streaming-mode entry from `~s>` /
2078    /// `~s>>`), after each stage is parsed we push the (just-built) stage
2079    /// expression into the collector and reset `result` to `$_` so the
2080    /// next stage parses against a fresh topic. The collector ends up
2081    /// with one Expr per stage where each stage's input is `$_`, ready
2082    /// to be wrapped as a `fn { ... }` closure for the per-item
2083    /// streaming runtime (`_thread_par_run`).
2084    fn parse_thread_macro_inner(
2085        &mut self,
2086        _line: usize,
2087        thread_last: bool,
2088        mut parallel_collector: Option<&mut Vec<Expr>>,
2089    ) -> StrykeResult<Expr> {
2090        // Set thread-last mode for pipe_forward_apply calls within this macro
2091        let saved_thread_last = self.thread_last_mode;
2092        self.thread_last_mode = thread_last;
2093
2094        let pipe_rhs_wrap = self.in_pipe_rhs();
2095        // `pending_thread_input` (set by `~p>` continuation parsing after
2096        // `||>` / `|then|`) supplies a pre-built input expression so we
2097        // skip parsing a source.
2098        let mut result = if let Some(pre) = self.pending_thread_input.take() {
2099            pre
2100        } else if pipe_rhs_wrap {
2101            Expr {
2102                kind: ExprKind::ArrayElement {
2103                    array: "_".to_string(),
2104                    index: Box::new(Expr {
2105                        kind: ExprKind::Integer(0),
2106                        line: _line,
2107                    }),
2108                },
2109                line: _line,
2110            }
2111        } else {
2112            // Suppress paren-less function calls so `t Color::Red p` parses
2113            // the enum variant without consuming `p` as an argument.
2114            self.suppress_parenless_call = self.suppress_parenless_call.saturating_add(1);
2115            let expr = self.parse_thread_input();
2116            self.suppress_parenless_call = self.suppress_parenless_call.saturating_sub(1);
2117            expr?
2118        };
2119        // Capture the source expression for parallel mode BEFORE any stage
2120        // is parsed, then reset `result` to `$_` so the first stage's parse
2121        // reads the topic instead of the source.
2122        let source_for_par = if parallel_collector.is_some() {
2123            let src = std::mem::replace(
2124                &mut result,
2125                Expr {
2126                    kind: ExprKind::ScalarVar("_".into()),
2127                    line: _line,
2128                },
2129            );
2130            Some(src)
2131        } else {
2132            None
2133        };
2134
2135        // Track line where the last stage ended (initially the input expression's line).
2136        let mut last_stage_end_line = self.prev_line();
2137
2138        // Parse stages until we hit a statement terminator
2139        loop {
2140            // Newline termination: if the next token is on a different line than where
2141            // the previous stage ended, the thread macro terminates. This allows
2142            // `~> @arr map { $_ * 2 }` on one line followed by `my @b = ...` on the next
2143            // without requiring a semicolon.
2144            if self.peek_line() > last_stage_end_line {
2145                break;
2146            }
2147
2148            // Check for terminators - |> ends thread and allows piping the result.
2149            // Variables ($x, @x, %x) and declaration keywords (my, our, local, state)
2150            // cannot be stages, so they implicitly terminate the thread macro.
2151            match self.peek() {
2152                Token::Semicolon
2153                | Token::RBrace
2154                | Token::RParen
2155                | Token::RBracket
2156                | Token::PipeForward
2157                | Token::Eof
2158                | Token::ScalarVar(_)
2159                | Token::ArrayVar(_)
2160                | Token::HashVar(_)
2161                | Token::Comma => break,
2162                // `||>` (LogOr + NumGt): chunk-parallel → sequential boundary
2163                // for `~p>` macros. Other thread macros never see this in
2164                // practice; if it appears, terminate the macro and let the
2165                // outer parser handle it.
2166                Token::LogOr if matches!(self.peek_at(1), Token::NumGt) => break,
2167                // `|then|` (BitOr + Ident("then") + BitOr): same boundary.
2168                Token::BitOr
2169                    if matches!(self.peek_at(1), Token::Ident(ref n) if n == "then")
2170                        && matches!(self.peek_at(2), Token::BitOr) =>
2171                {
2172                    break
2173                }
2174                Token::Ident(ref kw)
2175                    if matches!(
2176                        kw.as_str(),
2177                        "my" | "our"
2178                            | "local"
2179                            | "state"
2180                            | "if"
2181                            | "unless"
2182                            | "while"
2183                            | "until"
2184                            | "for"
2185                            | "foreach"
2186                            | "return"
2187                            | "last"
2188                            | "next"
2189                            | "redo"
2190                    ) =>
2191                {
2192                    break
2193                }
2194                _ => {}
2195            }
2196
2197            let stage_line = self.peek_line();
2198
2199            // Parse a stage and apply it to result via pipe
2200            match self.peek().clone() {
2201                // `>{ block }` — standalone anonymous block (sugar for fn { })
2202                Token::ArrowBrace => {
2203                    self.advance(); // consume `>{`
2204                    let mut stmts = Vec::new();
2205                    while !matches!(self.peek(), Token::RBrace | Token::Eof) {
2206                        if self.eat(&Token::Semicolon) {
2207                            continue;
2208                        }
2209                        stmts.push(self.parse_statement()?);
2210                    }
2211                    self.expect(&Token::RBrace)?;
2212                    let code_ref = Expr {
2213                        kind: ExprKind::CodeRef {
2214                            params: vec![],
2215                            body: stmts,
2216                        },
2217                        line: stage_line,
2218                    };
2219                    result = self.pipe_forward_apply(result, code_ref, stage_line)?;
2220                }
2221                // `sub { block }` — blocked in no-interop mode
2222                Token::Ident(ref name) if name == "sub" => {
2223                    if crate::no_interop_mode() {
2224                        return Err(self.syntax_err(
2225                            "stryke uses `fn {}` instead of `sub {}` (--no-interop)",
2226                            stage_line,
2227                        ));
2228                    }
2229                    self.advance(); // consume `sub`
2230                    let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
2231                    let body = self.parse_block()?;
2232                    let code_ref = Expr {
2233                        kind: ExprKind::CodeRef { params, body },
2234                        line: stage_line,
2235                    };
2236                    result = self.pipe_forward_apply(result, code_ref, stage_line)?;
2237                }
2238                // `fn { block }` — stryke anonymous function
2239                Token::Ident(ref name) if name == "fn" => {
2240                    self.advance(); // consume `fn`
2241                    let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
2242                    self.parse_sub_attributes()?;
2243                    let body = self.parse_fn_eq_body_or_block(false)?;
2244                    let code_ref = Expr {
2245                        kind: ExprKind::CodeRef { params, body },
2246                        line: stage_line,
2247                    };
2248                    result = self.pipe_forward_apply(result, code_ref, stage_line)?;
2249                }
2250                // `ident` possibly followed by block (or namespaced like `Foo::Bar::func`)
2251                Token::Ident(ref name) => {
2252                    let mut func_name = name.clone();
2253                    self.advance();
2254
2255                    // Collect namespaced function name (e.g., Rosetta::Stack::push)
2256                    while matches!(self.peek(), Token::PackageSep) {
2257                        self.advance(); // consume `::`
2258                        if let Token::Ident(ref part) = self.peek().clone() {
2259                            func_name.push_str("::");
2260                            func_name.push_str(part);
2261                            self.advance();
2262                        } else {
2263                            return Err(self.syntax_err(
2264                                format!(
2265                                    "Expected identifier after `::` in thread stage, got {:?}",
2266                                    self.peek()
2267                                ),
2268                                stage_line,
2269                            ));
2270                        }
2271                    }
2272
2273                    // Handle s/// and tr/// encoded tokens
2274                    if func_name.starts_with('\x00') {
2275                        let parts: Vec<&str> = func_name.split('\x00').collect();
2276                        if parts.len() >= 4 && parts[1] == "s" {
2277                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
2278                            let stage = Expr {
2279                                kind: ExprKind::Substitution {
2280                                    expr: Box::new(result.clone()),
2281                                    pattern: parts[2].to_string(),
2282                                    replacement: parts[3].to_string(),
2283                                    flags: format!("{}r", parts.get(4).unwrap_or(&"")),
2284                                    delim,
2285                                },
2286                                line: stage_line,
2287                            };
2288                            result = stage;
2289                            last_stage_end_line = self.prev_line();
2290                            continue;
2291                        }
2292                        if parts.len() >= 4 && parts[1] == "tr" {
2293                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
2294                            let stage = Expr {
2295                                kind: ExprKind::Transliterate {
2296                                    expr: Box::new(result.clone()),
2297                                    from: parts[2].to_string(),
2298                                    to: parts[3].to_string(),
2299                                    flags: format!("{}r", parts.get(4).unwrap_or(&"")),
2300                                    delim,
2301                                },
2302                                line: stage_line,
2303                            };
2304                            result = stage;
2305                            last_stage_end_line = self.prev_line();
2306                            continue;
2307                        }
2308                        return Err(
2309                            self.syntax_err("Unexpected encoded token in thread", stage_line)
2310                        );
2311                    }
2312
2313                    // `map +{ ... }` — hashref expression form (not a code block).
2314                    // The `+` disambiguates: `+{` is always a hashref constructor.
2315                    // Desugars to `MapExprComma` so pipe_forward_apply threads the
2316                    // list correctly: `t LIST map +{k => $_}` → `map +{k => $_}, LIST`.
2317                    if matches!(self.peek(), Token::Plus)
2318                        && matches!(self.peek_at(1), Token::LBrace)
2319                    {
2320                        self.advance(); // consume `+`
2321                        self.expect(&Token::LBrace)?;
2322                        // try_parse_hash_ref consumes the closing `}`
2323                        let pairs = self.try_parse_hash_ref()?;
2324                        let hashref_expr = Expr {
2325                            kind: ExprKind::HashRef(pairs),
2326                            line: stage_line,
2327                        };
2328                        let flatten_array_refs =
2329                            matches!(func_name.as_str(), "flat_map" | "flat_maps");
2330                        let stream = matches!(func_name.as_str(), "maps" | "flat_maps");
2331                        // Placeholder list — pipe_forward_apply replaces it with `result`.
2332                        let placeholder = Expr {
2333                            kind: ExprKind::Undef,
2334                            line: stage_line,
2335                        };
2336                        let map_node = Expr {
2337                            kind: ExprKind::MapExprComma {
2338                                expr: Box::new(hashref_expr),
2339                                list: Box::new(placeholder),
2340                                flatten_array_refs,
2341                                stream,
2342                            },
2343                            line: stage_line,
2344                        };
2345                        result = self.pipe_forward_apply(result, map_node, stage_line)?;
2346                    // `pmap_chunked CHUNK_SIZE { BLOCK }` — parallel chunked map
2347                    } else if func_name == "pmap_chunked" {
2348                        let chunk_size = self.parse_assign_expr()?;
2349                        let block = self.parse_block_or_bareword_block()?;
2350                        let placeholder = self.pipe_placeholder_list(stage_line);
2351                        let stage = Expr {
2352                            kind: ExprKind::PMapChunkedExpr {
2353                                chunk_size: Box::new(chunk_size),
2354                                block,
2355                                list: Box::new(placeholder),
2356                                progress: None,
2357                            },
2358                            line: stage_line,
2359                        };
2360                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2361                    // `preduce_init INIT { BLOCK }` — parallel reduce with init value
2362                    } else if func_name == "preduce_init" {
2363                        let init = self.parse_assign_expr()?;
2364                        let block = self.parse_block_or_bareword_block()?;
2365                        let placeholder = self.pipe_placeholder_list(stage_line);
2366                        let stage = Expr {
2367                            kind: ExprKind::PReduceInitExpr {
2368                                init: Box::new(init),
2369                                block,
2370                                list: Box::new(placeholder),
2371                                progress: None,
2372                            },
2373                            line: stage_line,
2374                        };
2375                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2376                    // `pmap_reduce { MAP } { REDUCE }` — parallel map-reduce
2377                    } else if func_name == "pmap_reduce" {
2378                        let map_block = self.parse_block_or_bareword_block()?;
2379                        let reduce_block = if matches!(self.peek(), Token::LBrace) {
2380                            self.parse_block()?
2381                        } else {
2382                            self.expect(&Token::Comma)?;
2383                            self.parse_block_or_bareword_cmp_block()?
2384                        };
2385                        let placeholder = self.pipe_placeholder_list(stage_line);
2386                        let stage = Expr {
2387                            kind: ExprKind::PMapReduceExpr {
2388                                map_block,
2389                                reduce_block,
2390                                list: Box::new(placeholder),
2391                                progress: None,
2392                            },
2393                            line: stage_line,
2394                        };
2395                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2396                    // `par_reduce { extract } [ { merge } ]` — chunk-extract-merge.
2397                    // First block runs per chunk in parallel; optional second
2398                    // block reduces pairwise across chunks (omit for auto-merge
2399                    // by result type).
2400                    } else if func_name == "par_reduce" {
2401                        let extract_block = self.parse_block_or_bareword_block()?;
2402                        let reduce_block = if matches!(self.peek(), Token::LBrace) {
2403                            Some(self.parse_block()?)
2404                        } else {
2405                            None
2406                        };
2407                        let placeholder = self.pipe_placeholder_list(stage_line);
2408                        let stage = Expr {
2409                            kind: ExprKind::ParReduceExpr {
2410                                extract_block,
2411                                reduce_block,
2412                                list: Box::new(placeholder),
2413                            },
2414                            line: stage_line,
2415                        };
2416                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2417                    // `pmap_on $cluster { BLOCK }` — parallel map dispatched to a remote
2418                    // cluster. Mirrors the `pmap_chunked` thread-stage shape; the cluster
2419                    // expression is parsed before the block, the threaded list slots in
2420                    // as the placeholder.
2421                    } else if func_name == "pmap_on" || func_name == "pflat_map_on" {
2422                        // Suppress `$cluster { ... }` auto-arrow (`$h->{...}`) so the
2423                        // brace opens the block, not a hash subscript.
2424                        self.suppress_scalar_hash_brace =
2425                            self.suppress_scalar_hash_brace.saturating_add(1);
2426                        let cluster = self.parse_assign_expr();
2427                        self.suppress_scalar_hash_brace =
2428                            self.suppress_scalar_hash_brace.saturating_sub(1);
2429                        let cluster = cluster?;
2430                        // Optional comma between cluster and block (matches the
2431                        // canonical `pmap_on $c, { BLOCK } @list` form in the LSP docs).
2432                        self.eat(&Token::Comma);
2433                        let block = self.parse_block_or_bareword_block()?;
2434                        let placeholder = self.pipe_placeholder_list(stage_line);
2435                        let stage = Expr {
2436                            kind: ExprKind::PMapExpr {
2437                                block,
2438                                list: Box::new(placeholder),
2439                                progress: None,
2440                                flat_outputs: func_name == "pflat_map_on",
2441                                on_cluster: Some(Box::new(cluster)),
2442                                stream: false,
2443                            },
2444                            line: stage_line,
2445                        };
2446                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2447                    // Check if followed by a block (like `filter { }`, `sort { }`, `map { }`)
2448                    } else if matches!(self.peek(), Token::LBrace) {
2449                        // Parse as a block-taking builtin
2450                        self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_add(1);
2451                        let stage = self.parse_thread_stage_with_block(&func_name, stage_line)?;
2452                        self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_sub(1);
2453                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2454                    } else if matches!(self.peek(), Token::LParen) {
2455                        // Special handling for join(sep) and split(pattern) in thread context.
2456                        // These take the threaded list/string as their data argument, not as $_.
2457                        if func_name == "join" {
2458                            self.advance(); // consume `(`
2459                            let separator = self.parse_assign_expr()?;
2460                            self.expect(&Token::RParen)?;
2461                            let placeholder = self.pipe_placeholder_list(stage_line);
2462                            let stage = Expr {
2463                                kind: ExprKind::JoinExpr {
2464                                    separator: Box::new(separator),
2465                                    list: Box::new(placeholder),
2466                                },
2467                                line: stage_line,
2468                            };
2469                            result = self.pipe_forward_apply(result, stage, stage_line)?;
2470                        } else if func_name == "split" {
2471                            self.advance(); // consume `(`
2472                            let pattern = self.parse_assign_expr()?;
2473                            let limit = if self.eat(&Token::Comma) {
2474                                Some(Box::new(self.parse_assign_expr()?))
2475                            } else {
2476                                None
2477                            };
2478                            self.expect(&Token::RParen)?;
2479                            let placeholder = Expr {
2480                                kind: ExprKind::ScalarVar("_".to_string()),
2481                                line: stage_line,
2482                            };
2483                            let stage = Expr {
2484                                kind: ExprKind::SplitExpr {
2485                                    pattern: Box::new(pattern),
2486                                    string: Box::new(placeholder),
2487                                    limit,
2488                                },
2489                                line: stage_line,
2490                            };
2491                            result = self.pipe_forward_apply(result, stage, stage_line)?;
2492                        } else {
2493                            // `name($_-bearing-args)` — parse explicit args, require at
2494                            // least one `$_` placeholder, then wrap as a `>{...}` block
2495                            // so the threaded value binds to `$_` at any position.
2496                            // Examples:
2497                            //   t 10 add2($_, 5) p      → add2(10, 5)
2498                            //   t 10 sub2(20, $_) p     → sub2(20, 10)
2499                            //   t 10 add3($_, 5, 10) p  → add3(10, 5, 10)
2500                            // To pass the threaded value as a sole arg, use bare form:
2501                            //   t 10 add2 p   (not `add2()`)
2502                            self.advance(); // consume `(`
2503                            let mut call_args = Vec::new();
2504                            while !matches!(self.peek(), Token::RParen | Token::Eof) {
2505                                call_args.push(self.parse_assign_expr()?);
2506                                if !self.eat(&Token::Comma) {
2507                                    break;
2508                                }
2509                            }
2510                            self.expect(&Token::RParen)?;
2511                            // If no `$_` placeholder, auto-inject threaded value.
2512                            // Thread-first: `t data to_file("/tmp/o.html")` → `to_file($_, "/tmp/o.html")`
2513                            // Thread-last: `->> data to_file("/tmp/o.html")` → `to_file("/tmp/o.html", $_)`
2514                            if !call_args.iter().any(Self::expr_contains_topic_var) {
2515                                let topic = Expr {
2516                                    kind: ExprKind::ScalarVar("_".to_string()),
2517                                    line: stage_line,
2518                                };
2519                                if self.thread_last_mode {
2520                                    call_args.push(topic);
2521                                } else {
2522                                    call_args.insert(0, topic);
2523                                }
2524                            }
2525                            let call_expr = Expr {
2526                                kind: ExprKind::FuncCall {
2527                                    name: func_name.clone(),
2528                                    args: call_args,
2529                                },
2530                                line: stage_line,
2531                            };
2532                            let code_ref = Expr {
2533                                kind: ExprKind::CodeRef {
2534                                    params: vec![],
2535                                    body: vec![Statement {
2536                                        label: None,
2537                                        kind: StmtKind::Expression(call_expr),
2538                                        line: stage_line,
2539                                    }],
2540                                },
2541                                line: stage_line,
2542                            };
2543                            result = self.pipe_forward_apply(result, code_ref, stage_line)?;
2544                        }
2545                    } else {
2546                        // Bare function name — handle unary builtins specially
2547                        result = self.thread_apply_bare_func(&func_name, result, stage_line)?;
2548                    }
2549                }
2550                // `/pattern/flags` — grep filter (desugar to `grep { /pattern/flags }`)
2551                Token::Regex(ref pattern, ref flags, delim) => {
2552                    let pattern = pattern.clone();
2553                    let flags = flags.clone();
2554                    self.advance();
2555                    result =
2556                        self.thread_regex_grep_stage(result, pattern, flags, delim, stage_line);
2557                }
2558                // Handle `/` that was lexed as Slash (division) because it followed a term.
2559                // In thread stage context, `/pattern/` should be a regex filter.
2560                Token::Slash => {
2561                    self.advance(); // consume opening /
2562
2563                    // Special case: if next token is Ident("m") or similar followed by Regex,
2564                    // the lexer interpreted `/m/` as `/ m/pattern/` where `m/` started a new regex.
2565                    // We need to handle this: the pattern is just "m" (or whatever the ident is).
2566                    if let Token::Ident(ref ident_s) = self.peek().clone() {
2567                        if matches!(ident_s.as_str(), "m" | "s" | "tr" | "y" | "qr")
2568                            && matches!(self.peek_at(1), Token::Regex(..))
2569                        {
2570                            // The `m` (or s/tr/y/qr) is our pattern, the Regex token was misparsed
2571                            self.advance(); // consume the ident
2572                                            // The Token::Regex after it was a misparsed `m/...` - we need to
2573                                            // extract what would have been the closing `/` situation.
2574                                            // Actually, the lexer consumed everything. Let's just use the ident
2575                                            // as the pattern and expect a closing slash.
2576                            if let Token::Regex(ref misparsed_pattern, ref misparsed_flags, _) =
2577                                self.peek().clone()
2578                            {
2579                                // The misparsed regex ate our closing `/`.
2580                                // For `/m/`, lexer saw `m/` and parsed until next `/`, finding nothing or wrong content.
2581                                // Actually for `/m/ less`, after Slash, lexer sees `m`, then `/`,
2582                                // interprets as m// regex start, reads until next `/` (none) -> error.
2583                                // So we shouldn't reach here if there was an error.
2584                                // But if lexer succeeded parsing `m/ less/` as regex, we'd have wrong pattern.
2585                                // This is getting complicated. Let me try a different approach.
2586                                // Just consume the Regex token and issue a warning? No, let's reconstruct.
2587                                // Skip for now and fall through to manual parsing.
2588                                let _ = (misparsed_pattern, misparsed_flags);
2589                            }
2590                        }
2591                    }
2592
2593                    // Manually parse the regex pattern from tokens until we hit another Slash
2594                    let mut pattern = String::new();
2595                    loop {
2596                        match self.peek().clone() {
2597                            Token::Slash => {
2598                                self.advance(); // consume closing /
2599                                break;
2600                            }
2601                            Token::Eof | Token::Semicolon | Token::Newline => {
2602                                return Err(self
2603                                    .syntax_err("Unterminated regex in thread stage", stage_line));
2604                            }
2605                            // Handle case where lexer misparsed m/pattern/ as Ident("m") + Regex
2606                            Token::Regex(ref inner_pattern, ref inner_flags, delim) => {
2607                                // This means `/m/` was lexed as Slash, then `m/` started a regex.
2608                                // The Regex token contains whatever was between the inner `m/` and closing `/`.
2609                                // For `/m/ less`, lexer would fail earlier. For `/m/i`, it might work weirdly.
2610                                // The safest: if we see a Regex token here and pattern is empty or just "m"/"s"/etc,
2611                                // treat the previous ident as the whole pattern and this Regex as misparsed.
2612                                // Actually, let's just prepend the ident we may have seen and use empty pattern.
2613                                // This is a lexer bug workaround.
2614                                if pattern.is_empty()
2615                                    || matches!(pattern.as_str(), "m" | "s" | "tr" | "y" | "qr")
2616                                {
2617                                    // The whole thing was probably `/X/` where X is m/s/tr/y/qr
2618                                    // and lexer misparsed. The Regex token is garbage.
2619                                    // Just use the ident as pattern and ignore this Regex.
2620                                    // But we already advanced past the ident...
2621                                    // This is messy. Let me try a cleaner approach.
2622                                    let _ = (inner_pattern, inner_flags, delim);
2623                                }
2624                                // For now, error out - this case is too complex
2625                                return Err(self.syntax_err(
2626                                    "Complex regex in thread stage - use m/pattern/ syntax instead",
2627                                    stage_line,
2628                                ));
2629                            }
2630                            Token::Ident(ref s) => {
2631                                pattern.push_str(s);
2632                                self.advance();
2633                            }
2634                            Token::Integer(n) => {
2635                                pattern.push_str(&n.to_string());
2636                                self.advance();
2637                            }
2638                            Token::ScalarVar(ref v) => {
2639                                pattern.push('$');
2640                                pattern.push_str(v);
2641                                self.advance();
2642                            }
2643                            Token::Dot => {
2644                                pattern.push('.');
2645                                self.advance();
2646                            }
2647                            Token::Star => {
2648                                pattern.push('*');
2649                                self.advance();
2650                            }
2651                            Token::Plus => {
2652                                pattern.push('+');
2653                                self.advance();
2654                            }
2655                            Token::Question => {
2656                                pattern.push('?');
2657                                self.advance();
2658                            }
2659                            Token::LParen => {
2660                                pattern.push('(');
2661                                self.advance();
2662                            }
2663                            Token::RParen => {
2664                                pattern.push(')');
2665                                self.advance();
2666                            }
2667                            Token::LBracket => {
2668                                pattern.push('[');
2669                                self.advance();
2670                            }
2671                            Token::RBracket => {
2672                                pattern.push(']');
2673                                self.advance();
2674                            }
2675                            Token::Backslash => {
2676                                pattern.push('\\');
2677                                self.advance();
2678                            }
2679                            Token::BitOr => {
2680                                pattern.push('|');
2681                                self.advance();
2682                            }
2683                            Token::Power => {
2684                                pattern.push_str("**");
2685                                self.advance();
2686                            }
2687                            Token::BitXor => {
2688                                pattern.push('^');
2689                                self.advance();
2690                            }
2691                            Token::Minus => {
2692                                pattern.push('-');
2693                                self.advance();
2694                            }
2695                            _ => {
2696                                return Err(self.syntax_err(
2697                                    format!("Unexpected token in regex pattern: {:?}", self.peek()),
2698                                    stage_line,
2699                                ));
2700                            }
2701                        }
2702                    }
2703                    // Parse optional flags (sequence of letters after closing /)
2704                    // Be careful: single letters like 'e' could be regex flags OR thread
2705                    // stages like `fore`/`e`. If followed by `{`, it's a stage, not a flag.
2706                    let mut flags = String::new();
2707                    if let Token::Ident(ref s) = self.peek().clone() {
2708                        let is_flag_only =
2709                            s.chars().all(|c| "gimsxecor".contains(c)) && s.len() <= 6;
2710                        let followed_by_brace = matches!(self.peek_at(1), Token::LBrace);
2711                        if is_flag_only && !followed_by_brace {
2712                            flags.push_str(s);
2713                            self.advance();
2714                        }
2715                    }
2716                    result = self.thread_regex_grep_stage(result, pattern, flags, '/', stage_line);
2717                }
2718                tok => {
2719                    return Err(self.syntax_err(
2720                        format!(
2721                            "thread: expected stage (ident, fn {{}}, s///, tr///, or /re/), got {:?}",
2722                            tok
2723                        ),
2724                        stage_line,
2725                    ));
2726                }
2727            };
2728            last_stage_end_line = self.prev_line();
2729            // Parallel mode: each iteration of the loop has produced a
2730            // stage expression where `$_` is the input. Push it into the
2731            // collector and reset `result` to `$_` so the next stage
2732            // parses against a fresh topic.
2733            if let Some(stages) = parallel_collector.as_mut() {
2734                let stage_body = std::mem::replace(
2735                    &mut result,
2736                    Expr {
2737                        kind: ExprKind::ScalarVar("_".into()),
2738                        line: stage_line,
2739                    },
2740                );
2741                stages.push(stage_body);
2742            }
2743        }
2744
2745        // Restore thread-last mode
2746        self.thread_last_mode = saved_thread_last;
2747
2748        // Parallel mode: lower to `_thread_par_run(source_expr, [stage_closures], thread_last)`.
2749        // The runtime treats the source value as a list-of-items and feeds
2750        // each item into stage 1 via a bounded channel. Each stage runs in
2751        // its own worker; stages are wrapped as `fn { body }` closures so
2752        // the runtime sets `$_` to the current item before invoking.
2753        if let Some(stages) = parallel_collector {
2754            let source_expr = source_for_par.unwrap_or(result);
2755            if stages.is_empty() {
2756                return Err(self.syntax_err(
2757                    "~p> / ~p>> require at least one stage after the source",
2758                    _line,
2759                ));
2760            }
2761            // Wrap each stage body in `[ ... ]` (an ArrayRef) so list-returning
2762            // ops like `map`/`grep` propagate their full output instead of
2763            // collapsing to a scalar count. The runtime worker peels one
2764            // level of array-ref via `map_flatten_outputs(true)` so each
2765            // element flows downstream as its own item.
2766            let stage_closures: Vec<Expr> = stages
2767                .drain(..)
2768                .map(|body| {
2769                    let body_line = body.line;
2770                    let wrapped = Expr {
2771                        kind: ExprKind::ArrayRef(vec![body]),
2772                        line: body_line,
2773                    };
2774                    Expr {
2775                        kind: ExprKind::CodeRef {
2776                            params: vec![],
2777                            body: vec![Statement {
2778                                label: None,
2779                                kind: StmtKind::Expression(wrapped),
2780                                line: body_line,
2781                            }],
2782                        },
2783                        line: body_line,
2784                    }
2785                })
2786                .collect();
2787            let stages_arr = Expr {
2788                kind: ExprKind::ArrayRef(stage_closures),
2789                line: _line,
2790            };
2791            let thread_last_flag = Expr {
2792                kind: ExprKind::Integer(if thread_last { 1 } else { 0 }),
2793                line: _line,
2794            };
2795            // Argument order: stages, thread_last, source... — source
2796            // is LAST so its list expansion (`(1,2,3)`, `@a`, ranges)
2797            // lands in the variadic tail. Pre-fix the source was first
2798            // and any list source flattened across the slot, breaking
2799            // the `args.len() == 3` invariant in `_thread_par_run` and
2800            // hitting "expected 3 args" for `~s> (1,2,3) sum` etc.
2801            return Ok(Expr {
2802                kind: ExprKind::FuncCall {
2803                    name: "_thread_par_run".into(),
2804                    args: vec![stages_arr, thread_last_flag, source_expr],
2805                },
2806                line: _line,
2807            });
2808        }
2809
2810        if pipe_rhs_wrap {
2811            // Wrap as `fn { …stages threaded from $_[0]… }` so the outer
2812            // `pipe_forward_apply` can invoke it with `lhs` as the arg.
2813            let body_line = result.line;
2814            return Ok(Expr {
2815                kind: ExprKind::CodeRef {
2816                    params: vec![],
2817                    body: vec![Statement {
2818                        label: None,
2819                        kind: StmtKind::Expression(result),
2820                        line: body_line,
2821                    }],
2822                },
2823                line: _line,
2824            });
2825        }
2826        Ok(result)
2827    }
2828
2829    /// Build a grep filter stage from a regex pattern for the thread macro.
2830    fn thread_regex_grep_stage(
2831        &self,
2832        list: Expr,
2833        pattern: String,
2834        flags: String,
2835        delim: char,
2836        line: usize,
2837    ) -> Expr {
2838        let topic = Expr {
2839            kind: ExprKind::ScalarVar("_".to_string()),
2840            line,
2841        };
2842        let match_expr = Expr {
2843            kind: ExprKind::Match {
2844                expr: Box::new(topic),
2845                pattern,
2846                flags,
2847                scalar_g: false,
2848                delim,
2849            },
2850            line,
2851        };
2852        let block = vec![Statement {
2853            label: None,
2854            kind: StmtKind::Expression(match_expr),
2855            line,
2856        }];
2857        Expr {
2858            kind: ExprKind::GrepExpr {
2859                block,
2860                list: Box::new(list),
2861                keyword: crate::ast::GrepBuiltinKeyword::Grep,
2862            },
2863            line,
2864        }
2865    }
2866
2867    /// Check whether an expression contains a `$_` reference anywhere in its sub-tree.
2868    /// Used by the thread macro to validate `name(args)` call-stages: the threaded
2869    /// value is bound to `$_` via a wrapping CodeRef, so at least one `$_` placeholder
2870    /// must appear in the args, otherwise the threaded value is silently dropped.
2871    ///
2872    /// Implementation uses Rust's `Debug` to serialize the entire sub-tree once and
2873    /// scan for the canonical `ScalarVar("_")` representation. This avoids a
2874    /// per-variant walker that would need to be updated whenever new `ExprKind`
2875    /// variants are added (and would silently miss any it forgot to handle).
2876    /// Parse-time perf is non-critical and the AST is small at this scope.
2877    fn expr_contains_topic_var(e: &Expr) -> bool {
2878        format!("{:?}", e).contains("ScalarVar(\"_\")")
2879    }
2880
2881    /// Walk tokens in `[rhs_start, rhs_end)` looking for a *free* bare
2882    /// topic-slot index (one in `self.bare_positional_indices` at
2883    /// brace-depth 0 within the RHS). Any `_` inside `{ ... }` is
2884    /// considered bound by whatever defines that block (closure,
2885    /// hash literal, map/grep/sort/match arm) and doesn't count.
2886    ///
2887    /// Special case: when the RHS *starts* with a thread-macro intro
2888    /// token (`~>`, `->>`, `~>>`, `~p>`, `~s>`, `~d>` and `-Last`
2889    /// variants), the macro itself binds `_` for all stage expressions
2890    /// — only the immediate input expression after the arrow can
2891    /// trigger the wrap. `->> 10 div(_, 2)` is eager (input = `10`,
2892    /// `_` is the threaded placeholder), but `~> _ uc` wraps (input
2893    /// is the free bare `_`).
2894    ///
2895    /// Drives the implicit-coderef sugar in `parse_my_our_local`.
2896    fn rhs_has_free_bare_topic_slot(&self, rhs_start: usize, rhs_end: usize) -> bool {
2897        let end = rhs_end.min(self.tokens.len());
2898        if rhs_start < end && Self::is_thread_arrow(&self.tokens[rhs_start].0) {
2899            // Only the input expression (first token after the arrow)
2900            // can trigger the wrap; everything else is a stage and
2901            // its bare `_` is the threaded placeholder.
2902            let input = rhs_start + 1;
2903            return input < end && self.bare_positional_indices.contains(&input);
2904        }
2905        let mut brace_depth = 0i32;
2906        for i in rhs_start..end {
2907            if brace_depth == 0 && self.bare_positional_indices.contains(&i) {
2908                return true;
2909            }
2910            match &self.tokens[i].0 {
2911                Token::LBrace | Token::ArrowBrace => brace_depth += 1,
2912                Token::RBrace => brace_depth -= 1,
2913                _ => {}
2914            }
2915        }
2916        false
2917    }
2918
2919    fn is_thread_arrow(tok: &Token) -> bool {
2920        matches!(
2921            tok,
2922            Token::ThreadArrow
2923                | Token::ThreadArrowLast
2924                | Token::ThreadArrowStream
2925                | Token::ThreadArrowStreamLast
2926                | Token::ThreadArrowPar
2927                | Token::ThreadArrowParLast
2928                | Token::ThreadArrowDist
2929                | Token::ThreadArrowDistLast
2930        )
2931    }
2932
2933    /// Apply a bare function name in thread context, handling unary builtins specially.
2934    fn thread_apply_bare_func(&self, name: &str, arg: Expr, line: usize) -> StrykeResult<Expr> {
2935        let kind = match name {
2936            // String functions
2937            "uc" => ExprKind::Uc(Box::new(arg)),
2938            "lc" => ExprKind::Lc(Box::new(arg)),
2939            "ucfirst" | "ufc" => ExprKind::Ucfirst(Box::new(arg)),
2940            "lcfirst" | "lfc" => ExprKind::Lcfirst(Box::new(arg)),
2941            "fc" => ExprKind::Fc(Box::new(arg)),
2942            "chomp" => ExprKind::Chomp(Box::new(arg)),
2943            "chop" => ExprKind::Chop(Box::new(arg)),
2944            "length" => ExprKind::Length(Box::new(arg)),
2945            "len" | "cnt" => ExprKind::FuncCall {
2946                name: "count".to_string(),
2947                args: vec![arg],
2948            },
2949            "quotemeta" | "qm" => ExprKind::FuncCall {
2950                name: "quotemeta".to_string(),
2951                args: vec![arg],
2952            },
2953            // Numeric functions
2954            "abs" => ExprKind::Abs(Box::new(arg)),
2955            "int" => ExprKind::Int(Box::new(arg)),
2956            "sqrt" | "sq" => ExprKind::Sqrt(Box::new(arg)),
2957            "sin" => ExprKind::Sin(Box::new(arg)),
2958            "cos" => ExprKind::Cos(Box::new(arg)),
2959            "exp" => ExprKind::Exp(Box::new(arg)),
2960            "log" => ExprKind::Log(Box::new(arg)),
2961            "hex" => ExprKind::Hex(Box::new(arg)),
2962            "oct" => ExprKind::Oct(Box::new(arg)),
2963            "chr" => ExprKind::Chr(Box::new(arg)),
2964            "ord" => ExprKind::Ord(Box::new(arg)),
2965            "rand" => ExprKind::Rand(Some(Box::new(arg))),
2966            "srand" => ExprKind::Srand(Some(Box::new(arg))),
2967            // Type/ref functions
2968            "defined" | "def" => ExprKind::Defined(Box::new(arg)),
2969            "ref" => ExprKind::Ref(Box::new(arg)),
2970            "scalar" => {
2971                if crate::no_interop_mode() {
2972                    return Err(self.syntax_err(
2973                        "stryke uses `len` (also `cnt` / `count`) instead of `scalar` (--no-interop)",
2974                        line,
2975                    ));
2976                }
2977                ExprKind::ScalarContext(Box::new(arg))
2978            }
2979            // Array/hash functions
2980            "keys" => ExprKind::Keys(Box::new(arg)),
2981            "values" => ExprKind::Values(Box::new(arg)),
2982            "each" => ExprKind::Each(Box::new(arg)),
2983            "pop" => ExprKind::Pop(Box::new(arg)),
2984            "shift" => ExprKind::Shift(Box::new(arg)),
2985            "reverse" => {
2986                if crate::no_interop_mode() {
2987                    return Err(self.syntax_err(
2988                        "stryke uses `rev` instead of `reverse` (--no-interop)",
2989                        line,
2990                    ));
2991                }
2992                ExprKind::ReverseExpr(Box::new(arg))
2993            }
2994            "reversed" | "rv" | "rev" => ExprKind::Rev(Box::new(arg)),
2995            "sort" | "so" => ExprKind::SortExpr {
2996                cmp: None,
2997                list: Box::new(arg),
2998            },
2999            "psort" => ExprKind::PSortExpr {
3000                cmp: None,
3001                list: Box::new(arg),
3002                progress: None,
3003            },
3004            "uniq" | "distinct" | "uq" => ExprKind::FuncCall {
3005                name: "uniq".to_string(),
3006                args: vec![arg],
3007            },
3008            "trim" | "tm" => ExprKind::FuncCall {
3009                name: "trim".to_string(),
3010                args: vec![arg],
3011            },
3012            "flatten" | "fl" => ExprKind::FuncCall {
3013                name: "flatten".to_string(),
3014                args: vec![arg],
3015            },
3016            "compact" | "cpt" => ExprKind::FuncCall {
3017                name: "compact".to_string(),
3018                args: vec![arg],
3019            },
3020            "shuffle" | "shuf" => ExprKind::FuncCall {
3021                name: "shuffle".to_string(),
3022                args: vec![arg],
3023            },
3024            "frequencies" | "freq" | "frq" => ExprKind::FuncCall {
3025                name: "frequencies".to_string(),
3026                args: vec![arg],
3027            },
3028            "pfrequencies" | "pfreq" | "pfrq" => ExprKind::FuncCall {
3029                name: "pfrequencies".to_string(),
3030                args: vec![arg],
3031            },
3032            "dedup" | "dup" => ExprKind::FuncCall {
3033                name: "dedup".to_string(),
3034                args: vec![arg],
3035            },
3036            "enumerate" | "en" => ExprKind::FuncCall {
3037                name: "enumerate".to_string(),
3038                args: vec![arg],
3039            },
3040            "lines" | "ln" => ExprKind::FuncCall {
3041                name: "lines".to_string(),
3042                args: vec![arg],
3043            },
3044            "words" | "wd" => ExprKind::FuncCall {
3045                name: "words".to_string(),
3046                args: vec![arg],
3047            },
3048            "chars" | "ch" => ExprKind::FuncCall {
3049                name: "chars".to_string(),
3050                args: vec![arg],
3051            },
3052            "digits" | "dg" => ExprKind::FuncCall {
3053                name: "digits".to_string(),
3054                args: vec![arg],
3055            },
3056            "letters" | "lts" => ExprKind::FuncCall {
3057                name: "letters".to_string(),
3058                args: vec![arg],
3059            },
3060            "letters_uc" => ExprKind::FuncCall {
3061                name: "letters_uc".to_string(),
3062                args: vec![arg],
3063            },
3064            "letters_lc" => ExprKind::FuncCall {
3065                name: "letters_lc".to_string(),
3066                args: vec![arg],
3067            },
3068            "punctuation" | "punct" => ExprKind::FuncCall {
3069                name: "punctuation".to_string(),
3070                args: vec![arg],
3071            },
3072            "sentences" | "sents" => ExprKind::FuncCall {
3073                name: "sentences".to_string(),
3074                args: vec![arg],
3075            },
3076            "paragraphs" | "paras" => ExprKind::FuncCall {
3077                name: "paragraphs".to_string(),
3078                args: vec![arg],
3079            },
3080            "sections" | "sects" => ExprKind::FuncCall {
3081                name: "sections".to_string(),
3082                args: vec![arg],
3083            },
3084            "numbers" | "nums" => ExprKind::FuncCall {
3085                name: "numbers".to_string(),
3086                args: vec![arg],
3087            },
3088            "graphemes" | "grs" => ExprKind::FuncCall {
3089                name: "graphemes".to_string(),
3090                args: vec![arg],
3091            },
3092            "columns" | "cols" => ExprKind::FuncCall {
3093                name: "columns".to_string(),
3094                args: vec![arg],
3095            },
3096            // File functions
3097            "slurp" | "sl" => ExprKind::Slurp(Box::new(arg)),
3098            "glob" => ExprKind::Glob(vec![arg]),
3099            "chdir" => ExprKind::Chdir(Box::new(arg)),
3100            "stat" => ExprKind::Stat(Box::new(arg)),
3101            "lstat" => ExprKind::Lstat(Box::new(arg)),
3102            "readlink" => ExprKind::Readlink(Box::new(arg)),
3103            "readdir" => ExprKind::Readdir(Box::new(arg)),
3104            "close" => ExprKind::Close(Box::new(arg)),
3105            "basename" | "bn" => ExprKind::FuncCall {
3106                name: "basename".to_string(),
3107                args: vec![arg],
3108            },
3109            "dirname" | "dn" => ExprKind::FuncCall {
3110                name: "dirname".to_string(),
3111                args: vec![arg],
3112            },
3113            "realpath" | "rp" => ExprKind::FuncCall {
3114                name: "realpath".to_string(),
3115                args: vec![arg],
3116            },
3117            "which" | "wh" => ExprKind::FuncCall {
3118                name: "which".to_string(),
3119                args: vec![arg],
3120            },
3121            // Other
3122            "eval" => ExprKind::Eval(Box::new(arg)),
3123            "require" => ExprKind::Require(Box::new(arg)),
3124            "study" => ExprKind::Study(Box::new(arg)),
3125            // Case conversion
3126            "snake_case" | "sc" => ExprKind::FuncCall {
3127                name: "snake_case".to_string(),
3128                args: vec![arg],
3129            },
3130            "camel_case" | "cc" => ExprKind::FuncCall {
3131                name: "camel_case".to_string(),
3132                args: vec![arg],
3133            },
3134            "kebab_case" | "kc" => ExprKind::FuncCall {
3135                name: "kebab_case".to_string(),
3136                args: vec![arg],
3137            },
3138            // Serialization
3139            "to_json" | "tj" => ExprKind::FuncCall {
3140                name: "to_json".to_string(),
3141                args: vec![arg],
3142            },
3143            "to_yaml" | "ty" => ExprKind::FuncCall {
3144                name: "to_yaml".to_string(),
3145                args: vec![arg],
3146            },
3147            "to_toml" | "tt" => ExprKind::FuncCall {
3148                name: "to_toml".to_string(),
3149                args: vec![arg],
3150            },
3151            "to_csv" | "tc" => ExprKind::FuncCall {
3152                name: "to_csv".to_string(),
3153                args: vec![arg],
3154            },
3155            "to_xml" | "tx" => ExprKind::FuncCall {
3156                name: "to_xml".to_string(),
3157                args: vec![arg],
3158            },
3159            "to_html" | "th" => ExprKind::FuncCall {
3160                name: "to_html".to_string(),
3161                args: vec![arg],
3162            },
3163            "to_markdown" | "to_md" | "tmd" => ExprKind::FuncCall {
3164                name: "to_markdown".to_string(),
3165                args: vec![arg],
3166            },
3167            "xopen" | "xo" => ExprKind::FuncCall {
3168                name: "xopen".to_string(),
3169                args: vec![arg],
3170            },
3171            "clip" | "clipboard" | "pbcopy" => ExprKind::FuncCall {
3172                name: "clip".to_string(),
3173                args: vec![arg],
3174            },
3175            "to_table" | "table" | "tbl" => ExprKind::FuncCall {
3176                name: "to_table".to_string(),
3177                args: vec![arg],
3178            },
3179            "sparkline" | "spark" => ExprKind::FuncCall {
3180                name: "sparkline".to_string(),
3181                args: vec![arg],
3182            },
3183            "bar_chart" | "bars" => ExprKind::FuncCall {
3184                name: "bar_chart".to_string(),
3185                args: vec![arg],
3186            },
3187            "flame" | "flamechart" => ExprKind::FuncCall {
3188                name: "flame".to_string(),
3189                args: vec![arg],
3190            },
3191            "ddump" | "dd" => ExprKind::FuncCall {
3192                name: "ddump".to_string(),
3193                args: vec![arg],
3194            },
3195            "say" => {
3196                if crate::no_interop_mode() {
3197                    return Err(
3198                        self.syntax_err("stryke uses `p` instead of `say` (--no-interop)", line)
3199                    );
3200                }
3201                ExprKind::Say {
3202                    handle: None,
3203                    args: vec![arg],
3204                }
3205            }
3206            "p" => ExprKind::Say {
3207                handle: None,
3208                args: vec![arg],
3209            },
3210            "print" => ExprKind::Print {
3211                handle: None,
3212                args: vec![arg],
3213            },
3214            "warn" => ExprKind::Warn(vec![arg]),
3215            "die" => ExprKind::Die(vec![arg]),
3216            "stringify" | "str" => ExprKind::FuncCall {
3217                name: "stringify".to_string(),
3218                args: vec![arg],
3219            },
3220            "json_decode" | "jd" => ExprKind::FuncCall {
3221                name: "json_decode".to_string(),
3222                args: vec![arg],
3223            },
3224            "yaml_decode" | "yd" => ExprKind::FuncCall {
3225                name: "yaml_decode".to_string(),
3226                args: vec![arg],
3227            },
3228            "toml_decode" | "td" => ExprKind::FuncCall {
3229                name: "toml_decode".to_string(),
3230                args: vec![arg],
3231            },
3232            "xml_decode" | "xd" => ExprKind::FuncCall {
3233                name: "xml_decode".to_string(),
3234                args: vec![arg],
3235            },
3236            "json_encode" | "je" => ExprKind::FuncCall {
3237                name: "json_encode".to_string(),
3238                args: vec![arg],
3239            },
3240            "yaml_encode" | "ye" => ExprKind::FuncCall {
3241                name: "yaml_encode".to_string(),
3242                args: vec![arg],
3243            },
3244            "toml_encode" | "te" => ExprKind::FuncCall {
3245                name: "toml_encode".to_string(),
3246                args: vec![arg],
3247            },
3248            "xml_encode" | "xe" => ExprKind::FuncCall {
3249                name: "xml_encode".to_string(),
3250                args: vec![arg],
3251            },
3252            // Encoding
3253            "base64_encode" | "b64e" => ExprKind::FuncCall {
3254                name: "base64_encode".to_string(),
3255                args: vec![arg],
3256            },
3257            "base64_decode" | "b64d" => ExprKind::FuncCall {
3258                name: "base64_decode".to_string(),
3259                args: vec![arg],
3260            },
3261            "hex_encode" | "hxe" => ExprKind::FuncCall {
3262                name: "hex_encode".to_string(),
3263                args: vec![arg],
3264            },
3265            "hex_decode" | "hxd" => ExprKind::FuncCall {
3266                name: "hex_decode".to_string(),
3267                args: vec![arg],
3268            },
3269            "url_encode" | "uri_escape" | "ue" => ExprKind::FuncCall {
3270                name: "url_encode".to_string(),
3271                args: vec![arg],
3272            },
3273            "url_decode" | "uri_unescape" | "ud" => ExprKind::FuncCall {
3274                name: "url_decode".to_string(),
3275                args: vec![arg],
3276            },
3277            "gzip" | "gz" => ExprKind::FuncCall {
3278                name: "gzip".to_string(),
3279                args: vec![arg],
3280            },
3281            "gunzip" | "ugz" => ExprKind::FuncCall {
3282                name: "gunzip".to_string(),
3283                args: vec![arg],
3284            },
3285            "zstd" | "zst" => ExprKind::FuncCall {
3286                name: "zstd".to_string(),
3287                args: vec![arg],
3288            },
3289            "zstd_decode" | "uzst" => ExprKind::FuncCall {
3290                name: "zstd_decode".to_string(),
3291                args: vec![arg],
3292            },
3293            // Crypto
3294            "sha256" | "s256" => ExprKind::FuncCall {
3295                name: "sha256".to_string(),
3296                args: vec![arg],
3297            },
3298            "sha1" | "s1" => ExprKind::FuncCall {
3299                name: "sha1".to_string(),
3300                args: vec![arg],
3301            },
3302            "md5" | "m5" => ExprKind::FuncCall {
3303                name: "md5".to_string(),
3304                args: vec![arg],
3305            },
3306            "uuid" | "uid" => ExprKind::FuncCall {
3307                name: "uuid".to_string(),
3308                args: vec![arg],
3309            },
3310            // Datetime
3311            "datetime_utc" | "utc" => ExprKind::FuncCall {
3312                name: "datetime_utc".to_string(),
3313                args: vec![arg],
3314            },
3315            // Bare `e` / `fore` / `ep` in thread context: foreach element, say it.
3316            // `t @list e` == `@list |> e p` == `@list |> ep` == foreach (@list) { say }
3317            "e" | "fore" | "ep" => ExprKind::ForEachExpr {
3318                block: vec![Statement {
3319                    label: None,
3320                    kind: StmtKind::Expression(Expr {
3321                        kind: ExprKind::Say {
3322                            handle: None,
3323                            args: vec![Expr {
3324                                kind: ExprKind::ScalarVar("_".into()),
3325                                line,
3326                            }],
3327                        },
3328                        line,
3329                    }),
3330                    line,
3331                }],
3332                list: Box::new(arg),
3333            },
3334            // Default: generic function call
3335            _ => ExprKind::FuncCall {
3336                name: name.to_string(),
3337                args: vec![arg],
3338            },
3339        };
3340        Ok(Expr { kind, line })
3341    }
3342
3343    /// Parse a thread stage that has a block: `map { }`, `filter { }`, `sort { }`, etc.
3344    /// In thread context, we only parse the block - the list comes from the piped result.
3345    fn parse_thread_stage_with_block(&mut self, name: &str, line: usize) -> StrykeResult<Expr> {
3346        let block = self.parse_block()?;
3347        // Use a placeholder for the list - pipe_forward_apply will replace it
3348        let placeholder = self.pipe_placeholder_list(line);
3349
3350        match name {
3351            "map" | "flat_map" | "maps" | "flat_maps" => {
3352                let flatten_array_refs = matches!(name, "flat_map" | "flat_maps");
3353                let stream = matches!(name, "maps" | "flat_maps");
3354                Ok(Expr {
3355                    kind: ExprKind::MapExpr {
3356                        block,
3357                        list: Box::new(placeholder),
3358                        flatten_array_refs,
3359                        stream,
3360                    },
3361                    line,
3362                })
3363            }
3364            "grep" | "greps" | "filter" | "fi" | "find_all" | "gr" => {
3365                let keyword = match name {
3366                    "grep" | "gr" => crate::ast::GrepBuiltinKeyword::Grep,
3367                    "greps" => crate::ast::GrepBuiltinKeyword::Greps,
3368                    "filter" | "fi" => crate::ast::GrepBuiltinKeyword::Filter,
3369                    "find_all" => crate::ast::GrepBuiltinKeyword::FindAll,
3370                    _ => unreachable!(),
3371                };
3372                Ok(Expr {
3373                    kind: ExprKind::GrepExpr {
3374                        block,
3375                        list: Box::new(placeholder),
3376                        keyword,
3377                    },
3378                    line,
3379                })
3380            }
3381            "sort" | "so" => Ok(Expr {
3382                kind: ExprKind::SortExpr {
3383                    cmp: Some(SortComparator::Block(block)),
3384                    list: Box::new(placeholder),
3385                },
3386                line,
3387            }),
3388            "reduce" | "rd" => Ok(Expr {
3389                kind: ExprKind::ReduceExpr {
3390                    block,
3391                    list: Box::new(placeholder),
3392                },
3393                line,
3394            }),
3395            "fore" | "e" | "ep" => Ok(Expr {
3396                kind: ExprKind::ForEachExpr {
3397                    block,
3398                    list: Box::new(placeholder),
3399                },
3400                line,
3401            }),
3402            "par" => Ok(Expr {
3403                kind: ExprKind::ParExpr {
3404                    block,
3405                    list: Box::new(placeholder),
3406                },
3407                line,
3408            }),
3409            "pmap" | "pflat_map" | "pmaps" | "pflat_maps" => Ok(Expr {
3410                kind: ExprKind::PMapExpr {
3411                    block,
3412                    list: Box::new(placeholder),
3413                    progress: None,
3414                    flat_outputs: name == "pflat_map" || name == "pflat_maps",
3415                    on_cluster: None,
3416                    stream: name == "pmaps" || name == "pflat_maps",
3417                },
3418                line,
3419            }),
3420            "pgrep" | "pgreps" => Ok(Expr {
3421                kind: ExprKind::PGrepExpr {
3422                    block,
3423                    list: Box::new(placeholder),
3424                    progress: None,
3425                    stream: name == "pgreps",
3426                },
3427                line,
3428            }),
3429            "pfor" => Ok(Expr {
3430                kind: ExprKind::PForExpr {
3431                    block,
3432                    list: Box::new(placeholder),
3433                    progress: None,
3434                },
3435                line,
3436            }),
3437            "preduce" => Ok(Expr {
3438                kind: ExprKind::PReduceExpr {
3439                    block,
3440                    list: Box::new(placeholder),
3441                    progress: None,
3442                },
3443                line,
3444            }),
3445            "pcache" => Ok(Expr {
3446                kind: ExprKind::PcacheExpr {
3447                    block,
3448                    list: Box::new(placeholder),
3449                    progress: None,
3450                },
3451                line,
3452            }),
3453            "psort" => Ok(Expr {
3454                kind: ExprKind::PSortExpr {
3455                    cmp: Some(block),
3456                    list: Box::new(placeholder),
3457                    progress: None,
3458                },
3459                line,
3460            }),
3461            _ => {
3462                // Generic: parse block and treat as FuncCall with code ref arg.
3463                // Block-then-list pipe builtins (`pfirst`, `any`, `take_while`, etc.)
3464                // need the threaded list slot pre-allocated at args[1] so
3465                // `pipe_forward_apply` can substitute the lhs there (parser.rs:5823).
3466                // For everything else, the generic pipe-forward arm prepends or
3467                // appends the lhs based on `thread_last_mode`.
3468                let code_ref = Expr {
3469                    kind: ExprKind::CodeRef {
3470                        params: vec![],
3471                        body: block,
3472                    },
3473                    line,
3474                };
3475                let args = if Self::is_block_then_list_pipe_builtin(name) {
3476                    vec![code_ref, placeholder]
3477                } else {
3478                    vec![code_ref]
3479                };
3480                Ok(Expr {
3481                    kind: ExprKind::FuncCall {
3482                        name: name.to_string(),
3483                        args,
3484                    },
3485                    line,
3486                })
3487            }
3488        }
3489    }
3490
3491    /// `tie %hash | tie @arr | tie $x , 'Class', ...args`
3492    fn parse_tie_stmt(&mut self) -> StrykeResult<Statement> {
3493        let line = self.peek_line();
3494        self.advance(); // tie
3495                        // `tie my $x, Class` and `tie our $x, Class` — common Perl idiom.
3496                        // Desugar by emitting an implicit `my $x` (or `our $x`) declaration
3497                        // before the tie. The tie target then references the just-declared
3498                        // variable. Without this, `tie my $x, Class, ARGS` errors with
3499                        // "tie expects $scalar, @array, or %hash, got Ident(\"my\")".
3500        let mut implicit_decl: Option<Statement> = None;
3501        if let Token::Ident(kw) = self.peek().clone() {
3502            if matches!(kw.as_str(), "my" | "our") {
3503                let kw_line = self.peek_line();
3504                self.advance(); // my / our
3505                                // Read the variable being declared (must be Scalar/Array/Hash).
3506                let (decl_sigil, decl_name) = match self.peek().clone() {
3507                    Token::ScalarVar(s) => (Sigil::Scalar, s),
3508                    Token::ArrayVar(a) => (Sigil::Array, a),
3509                    Token::HashVar(h) => (Sigil::Hash, h),
3510                    tok => {
3511                        return Err(self.syntax_err(
3512                            format!("expected variable after `tie {}`, got {:?}", kw, tok),
3513                            self.peek_line(),
3514                        ));
3515                    }
3516                };
3517                let decls = vec![VarDecl {
3518                    sigil: decl_sigil,
3519                    name: decl_name.clone(),
3520                    initializer: None,
3521                    frozen: false,
3522                    type_annotation: None,
3523                    list_context: false,
3524                }];
3525                implicit_decl = Some(Statement {
3526                    label: None,
3527                    kind: if kw == "my" {
3528                        StmtKind::My(decls)
3529                    } else {
3530                        StmtKind::Our(decls)
3531                    },
3532                    line: kw_line,
3533                });
3534                // Don't advance past the variable token here — fall through
3535                // to the existing match below so `target` is built from the
3536                // same token (the ScalarVar/ArrayVar/HashVar path will
3537                // advance and capture the name).
3538            }
3539        }
3540        let target = match self.peek().clone() {
3541            Token::HashVar(h) => {
3542                self.advance();
3543                TieTarget::Hash(h)
3544            }
3545            Token::ArrayVar(a) => {
3546                self.advance();
3547                TieTarget::Array(a)
3548            }
3549            Token::ScalarVar(s) => {
3550                self.advance();
3551                TieTarget::Scalar(s)
3552            }
3553            tok => {
3554                return Err(self.syntax_err(
3555                    format!("tie expects $scalar, @array, or %hash, got {:?}", tok),
3556                    self.peek_line(),
3557                ));
3558            }
3559        };
3560        self.expect(&Token::Comma)?;
3561        let class = self.parse_assign_expr()?;
3562        let mut args = Vec::new();
3563        while self.eat(&Token::Comma) {
3564            if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof) {
3565                break;
3566            }
3567            args.push(self.parse_assign_expr()?);
3568        }
3569        self.eat(&Token::Semicolon);
3570        let tie_stmt = Statement {
3571            label: None,
3572            kind: StmtKind::Tie {
3573                target,
3574                class,
3575                args,
3576            },
3577            line,
3578        };
3579        if let Some(decl) = implicit_decl {
3580            // Wrap the implicit `my $x` + tie in a `StmtGroup` so they live
3581            // in the same lexical block (the parser desugar is invisible to
3582            // callers; `StmtGroup` runs statements in order without a frame
3583            // push).
3584            Ok(Statement {
3585                label: None,
3586                kind: StmtKind::StmtGroup(vec![decl, tie_stmt]),
3587                line,
3588            })
3589        } else {
3590            Ok(tie_stmt)
3591        }
3592    }
3593
3594    /// `given (EXPR) { ... }`
3595    fn parse_given(&mut self) -> StrykeResult<Statement> {
3596        let line = self.peek_line();
3597        self.advance();
3598        self.expect(&Token::LParen)?;
3599        let topic = self.parse_expression()?;
3600        self.expect(&Token::RParen)?;
3601        let body = self.parse_block()?;
3602        self.eat(&Token::Semicolon);
3603        Ok(Statement {
3604            label: None,
3605            kind: StmtKind::Given { topic, body },
3606            line,
3607        })
3608    }
3609
3610    /// `when (COND) { ... }` — only meaningful inside `given`
3611    fn parse_when_stmt(&mut self) -> StrykeResult<Statement> {
3612        let line = self.peek_line();
3613        self.advance();
3614        self.expect(&Token::LParen)?;
3615        let cond = self.parse_expression()?;
3616        self.expect(&Token::RParen)?;
3617        let body = self.parse_block()?;
3618        self.eat(&Token::Semicolon);
3619        Ok(Statement {
3620            label: None,
3621            kind: StmtKind::When { cond, body },
3622            line,
3623        })
3624    }
3625
3626    /// `default { ... }` — only meaningful inside `given`
3627    fn parse_default_stmt(&mut self) -> StrykeResult<Statement> {
3628        let line = self.peek_line();
3629        self.advance();
3630        let body = self.parse_block()?;
3631        self.eat(&Token::Semicolon);
3632        Ok(Statement {
3633            label: None,
3634            kind: StmtKind::DefaultCase { body },
3635            line,
3636        })
3637    }
3638
3639    /// `cond { EXPR => RESULT, ..., default => RESULT }`
3640    ///
3641    /// Desugars to an if/elsif/else chain at parse time.
3642    /// Each arm is `condition => { body }` or `condition => expr`.
3643    /// `default => ...` becomes the else branch.
3644    fn parse_cond_expr(&mut self, line: usize) -> StrykeResult<Expr> {
3645        self.expect(&Token::LBrace)?;
3646
3647        let mut arms: Vec<(Expr, Block)> = Vec::new();
3648        let mut else_block: Option<Block> = None;
3649
3650        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
3651            let arm_line = self.peek_line();
3652
3653            // Check for `default =>`
3654            let is_default = matches!(self.peek(), Token::Ident(ref s) if s == "default")
3655                && matches!(self.peek_at(1), Token::FatArrow);
3656
3657            if is_default {
3658                self.advance(); // consume `default`
3659                self.advance(); // consume `=>`
3660                let body = if matches!(self.peek(), Token::LBrace) {
3661                    self.parse_block()?
3662                } else {
3663                    let expr = self.parse_assign_expr()?;
3664                    vec![Statement {
3665                        label: None,
3666                        kind: StmtKind::Expression(expr),
3667                        line: arm_line,
3668                    }]
3669                };
3670                else_block = Some(body);
3671                self.eat(&Token::Comma);
3672                break; // default must be last
3673            }
3674
3675            // Parse condition expression (stop before `=>`)
3676            let condition = self.parse_assign_expr()?;
3677            self.expect(&Token::FatArrow)?;
3678
3679            let body = if matches!(self.peek(), Token::LBrace) {
3680                self.parse_block()?
3681            } else {
3682                let expr = self.parse_assign_expr()?;
3683                vec![Statement {
3684                    label: None,
3685                    kind: StmtKind::Expression(expr),
3686                    line: arm_line,
3687                }]
3688            };
3689
3690            arms.push((condition, body));
3691            self.eat(&Token::Comma);
3692        }
3693
3694        self.expect(&Token::RBrace)?;
3695
3696        if arms.is_empty() {
3697            return Err(self.syntax_err("cond requires at least one condition arm", line));
3698        }
3699
3700        // Build if/elsif/else chain from the arms.
3701        let (first_cond, first_body) = arms.remove(0);
3702        let elsifs: Vec<(Expr, Block)> = arms;
3703
3704        // Wrap in a do-block so `cond { ... }` is an expression.
3705        let if_stmt = Statement {
3706            label: None,
3707            kind: StmtKind::If {
3708                condition: first_cond,
3709                body: first_body,
3710                elsifs,
3711                else_block,
3712            },
3713            line,
3714        };
3715        let inner = Expr {
3716            kind: ExprKind::CodeRef {
3717                params: vec![],
3718                body: vec![if_stmt],
3719            },
3720            line,
3721        };
3722        Ok(Expr {
3723            kind: ExprKind::Do(Box::new(inner)),
3724            line,
3725        })
3726    }
3727
3728    /// `match (EXPR) { PATTERN => EXPR, ... }`
3729    fn parse_algebraic_match_expr(&mut self, line: usize) -> StrykeResult<Expr> {
3730        self.expect(&Token::LParen)?;
3731        let subject = self.parse_expression()?;
3732        self.expect(&Token::RParen)?;
3733        self.expect(&Token::LBrace)?;
3734        let mut arms = Vec::new();
3735        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
3736            if self.eat(&Token::Semicolon) {
3737                continue;
3738            }
3739            let pattern = self.parse_match_pattern()?;
3740            let guard = if matches!(self.peek(), Token::Ident(ref s) if s == "if") {
3741                self.advance();
3742                // Use assign-level parsing so `=>` after the guard is not consumed as a comma/fat-comma
3743                // separator (see [`Self::parse_comma_expr`]).
3744                Some(Box::new(self.parse_assign_expr()?))
3745            } else {
3746                None
3747            };
3748            self.expect(&Token::FatArrow)?;
3749            // Use assign-level parsing so commas separate arms, not `List` elements.
3750            let body = self.parse_assign_expr()?;
3751            arms.push(MatchArm {
3752                pattern,
3753                guard,
3754                body,
3755            });
3756            self.eat(&Token::Comma);
3757        }
3758        self.expect(&Token::RBrace)?;
3759        Ok(Expr {
3760            kind: ExprKind::AlgebraicMatch {
3761                subject: Box::new(subject),
3762                arms,
3763            },
3764            line,
3765        })
3766    }
3767
3768    fn parse_match_pattern(&mut self) -> StrykeResult<MatchPattern> {
3769        match self.peek().clone() {
3770            Token::Regex(pattern, flags, _delim) => {
3771                self.advance();
3772                Ok(MatchPattern::Regex { pattern, flags })
3773            }
3774            Token::Ident(ref s) if s == "_" => {
3775                self.advance();
3776                Ok(MatchPattern::Any)
3777            }
3778            Token::Ident(ref s) if s == "Some" => {
3779                self.advance();
3780                self.expect(&Token::LParen)?;
3781                let name = self.parse_scalar_var_name()?;
3782                self.expect(&Token::RParen)?;
3783                Ok(MatchPattern::OptionSome(name))
3784            }
3785            Token::LBracket => self.parse_match_array_pattern(),
3786            Token::LBrace => self.parse_match_hash_pattern(),
3787            Token::LParen => {
3788                self.advance();
3789                let e = self.parse_expression()?;
3790                self.expect(&Token::RParen)?;
3791                Ok(MatchPattern::Value(Box::new(e)))
3792            }
3793            _ => {
3794                let e = self.parse_assign_expr()?;
3795                Ok(MatchPattern::Value(Box::new(e)))
3796            }
3797        }
3798    }
3799
3800    /// Contents of `[ ... ]` for algebraic array patterns and `sub ($a, [ ... ])` signatures.
3801    fn parse_match_array_elems_until_rbracket(&mut self) -> StrykeResult<Vec<MatchArrayElem>> {
3802        let mut elems = Vec::new();
3803        if self.eat(&Token::RBracket) {
3804            return Ok(vec![]);
3805        }
3806        loop {
3807            if matches!(self.peek(), Token::Star) {
3808                self.advance();
3809                elems.push(MatchArrayElem::Rest);
3810                self.eat(&Token::Comma);
3811                if !matches!(self.peek(), Token::RBracket) {
3812                    return Err(self.syntax_err(
3813                        "`*` must be the last element in an array match pattern",
3814                        self.peek_line(),
3815                    ));
3816                }
3817                self.expect(&Token::RBracket)?;
3818                return Ok(elems);
3819            }
3820            if let Token::ArrayVar(name) = self.peek().clone() {
3821                self.advance();
3822                elems.push(MatchArrayElem::RestBind(name));
3823                self.eat(&Token::Comma);
3824                if !matches!(self.peek(), Token::RBracket) {
3825                    return Err(self.syntax_err(
3826                        "`@name` rest bind must be the last element in an array match pattern",
3827                        self.peek_line(),
3828                    ));
3829                }
3830                self.expect(&Token::RBracket)?;
3831                return Ok(elems);
3832            }
3833            if let Token::ScalarVar(name) = self.peek().clone() {
3834                self.advance();
3835                elems.push(MatchArrayElem::CaptureScalar(name));
3836                if self.eat(&Token::Comma) {
3837                    if matches!(self.peek(), Token::RBracket) {
3838                        break;
3839                    }
3840                    continue;
3841                }
3842                break;
3843            }
3844            let e = self.parse_assign_expr()?;
3845            elems.push(MatchArrayElem::Expr(e));
3846            if self.eat(&Token::Comma) {
3847                if matches!(self.peek(), Token::RBracket) {
3848                    break;
3849                }
3850                continue;
3851            }
3852            break;
3853        }
3854        self.expect(&Token::RBracket)?;
3855        Ok(elems)
3856    }
3857
3858    fn parse_match_array_pattern(&mut self) -> StrykeResult<MatchPattern> {
3859        self.expect(&Token::LBracket)?;
3860        let elems = self.parse_match_array_elems_until_rbracket()?;
3861        Ok(MatchPattern::Array(elems))
3862    }
3863
3864    fn parse_match_hash_pattern(&mut self) -> StrykeResult<MatchPattern> {
3865        self.expect(&Token::LBrace)?;
3866        let mut pairs = Vec::new();
3867        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
3868            if self.eat(&Token::Semicolon) {
3869                continue;
3870            }
3871            let key = self.parse_assign_expr()?;
3872            self.expect(&Token::FatArrow)?;
3873            match self.advance().0 {
3874                Token::Ident(ref s) if s == "_" => {
3875                    pairs.push(MatchHashPair::KeyOnly { key });
3876                }
3877                Token::ScalarVar(name) => {
3878                    pairs.push(MatchHashPair::Capture { key, name });
3879                }
3880                tok => {
3881                    return Err(self.syntax_err(
3882                        format!(
3883                            "hash match pattern must bind with `=> $name` or `=> _`, got {:?}",
3884                            tok
3885                        ),
3886                        self.peek_line(),
3887                    ));
3888                }
3889            }
3890            self.eat(&Token::Comma);
3891        }
3892        self.expect(&Token::RBrace)?;
3893        Ok(MatchPattern::Hash(pairs))
3894    }
3895
3896    /// `eval_timeout SECS { ... }`
3897    fn parse_eval_timeout(&mut self) -> StrykeResult<Statement> {
3898        let line = self.peek_line();
3899        self.advance();
3900        let timeout = self.parse_postfix()?;
3901        let body = self.parse_block_or_bareword_block_no_args()?;
3902        self.eat(&Token::Semicolon);
3903        Ok(Statement {
3904            label: None,
3905            kind: StmtKind::EvalTimeout { timeout, body },
3906            line,
3907        })
3908    }
3909
3910    fn mark_match_scalar_g_for_boolean_condition(cond: &mut Expr) {
3911        match &mut cond.kind {
3912            ExprKind::Match {
3913                flags, scalar_g, ..
3914            } if flags.contains('g') => {
3915                *scalar_g = true;
3916            }
3917            ExprKind::UnaryOp {
3918                op: UnaryOp::LogNot,
3919                expr,
3920            } => {
3921                if let ExprKind::Match {
3922                    flags, scalar_g, ..
3923                } = &mut expr.kind
3924                {
3925                    if flags.contains('g') {
3926                        *scalar_g = true;
3927                    }
3928                }
3929            }
3930            _ => {}
3931        }
3932    }
3933
3934    fn parse_if(&mut self) -> StrykeResult<Statement> {
3935        let line = self.peek_line();
3936        self.advance(); // 'if'
3937        if matches!(self.peek(), Token::Ident(ref s) if s == "let") {
3938            if crate::compat_mode() {
3939                return Err(self.syntax_err(
3940                    "`if let` is a stryke extension (disabled by --compat)",
3941                    line,
3942                ));
3943            }
3944            return self.parse_if_let(line);
3945        }
3946        self.expect(&Token::LParen)?;
3947        let mut cond = self.parse_expression()?;
3948        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
3949        self.expect(&Token::RParen)?;
3950        let body = self.parse_block()?;
3951
3952        let mut elsifs = Vec::new();
3953        let mut else_block = None;
3954
3955        loop {
3956            if let Token::Ident(ref kw) = self.peek().clone() {
3957                if kw == "elsif" {
3958                    self.advance();
3959                    self.expect(&Token::LParen)?;
3960                    let mut c = self.parse_expression()?;
3961                    Self::mark_match_scalar_g_for_boolean_condition(&mut c);
3962                    self.expect(&Token::RParen)?;
3963                    let b = self.parse_block()?;
3964                    elsifs.push((c, b));
3965                    continue;
3966                }
3967                if kw == "else" {
3968                    self.advance();
3969                    else_block = Some(self.parse_block()?);
3970                }
3971            }
3972            break;
3973        }
3974
3975        Ok(Statement {
3976            label: None,
3977            kind: StmtKind::If {
3978                condition: cond,
3979                body,
3980                elsifs,
3981                else_block,
3982            },
3983            line,
3984        })
3985    }
3986
3987    /// `if let PAT = EXPR { ... } [ else { ... } ]` — desugars to [`ExprKind::AlgebraicMatch`].
3988    fn parse_if_let(&mut self, line: usize) -> StrykeResult<Statement> {
3989        self.advance(); // `let`
3990        let pattern = self.parse_match_pattern()?;
3991        self.expect(&Token::Assign)?;
3992        // Use assign-level parsing so a following `{ ... }` is the `if let` body, not an anon hash.
3993        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_add(1);
3994        let rhs = self.parse_assign_expr();
3995        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_sub(1);
3996        let rhs = rhs?;
3997        let then_block = self.parse_block()?;
3998        let else_block_opt = match self.peek().clone() {
3999            Token::Ident(ref kw) if kw == "else" => {
4000                self.advance();
4001                Some(self.parse_block()?)
4002            }
4003            Token::Ident(ref kw) if kw == "elsif" => {
4004                return Err(self.syntax_err(
4005                    "`if let` does not support `elsif`; use `else { }` or a full `match`",
4006                    self.peek_line(),
4007                ));
4008            }
4009            _ => None,
4010        };
4011        let then_expr = Self::expr_do_anon_block(then_block, line);
4012        let else_expr = if let Some(eb) = else_block_opt {
4013            Self::expr_do_anon_block(eb, line)
4014        } else {
4015            Expr {
4016                kind: ExprKind::Undef,
4017                line,
4018            }
4019        };
4020        let arms = vec![
4021            MatchArm {
4022                pattern,
4023                guard: None,
4024                body: then_expr,
4025            },
4026            MatchArm {
4027                pattern: MatchPattern::Any,
4028                guard: None,
4029                body: else_expr,
4030            },
4031        ];
4032        Ok(Statement {
4033            label: None,
4034            kind: StmtKind::Expression(Expr {
4035                kind: ExprKind::AlgebraicMatch {
4036                    subject: Box::new(rhs),
4037                    arms,
4038                },
4039                line,
4040            }),
4041            line,
4042        })
4043    }
4044
4045    fn expr_do_anon_block(block: Block, outer_line: usize) -> Expr {
4046        let inner_line = block.first().map(|s| s.line).unwrap_or(outer_line);
4047        Expr {
4048            kind: ExprKind::Do(Box::new(Expr {
4049                kind: ExprKind::CodeRef {
4050                    params: vec![],
4051                    body: block,
4052                },
4053                line: inner_line,
4054            })),
4055            line: outer_line,
4056        }
4057    }
4058
4059    fn parse_unless(&mut self) -> StrykeResult<Statement> {
4060        let line = self.peek_line();
4061        self.advance(); // 'unless'
4062        self.expect(&Token::LParen)?;
4063        let mut cond = self.parse_expression()?;
4064        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
4065        self.expect(&Token::RParen)?;
4066        let body = self.parse_block()?;
4067        let else_block = if let Token::Ident(ref kw) = self.peek().clone() {
4068            if kw == "else" {
4069                self.advance();
4070                Some(self.parse_block()?)
4071            } else {
4072                None
4073            }
4074        } else {
4075            None
4076        };
4077        Ok(Statement {
4078            label: None,
4079            kind: StmtKind::Unless {
4080                condition: cond,
4081                body,
4082                else_block,
4083            },
4084            line,
4085        })
4086    }
4087
4088    fn parse_while(&mut self) -> StrykeResult<Statement> {
4089        let line = self.peek_line();
4090        self.advance(); // 'while'
4091        if matches!(self.peek(), Token::Ident(ref s) if s == "let") {
4092            if crate::compat_mode() {
4093                return Err(self.syntax_err(
4094                    "`while let` is a stryke extension (disabled by --compat)",
4095                    line,
4096                ));
4097            }
4098            return self.parse_while_let(line);
4099        }
4100        self.expect(&Token::LParen)?;
4101        let mut cond = self.parse_expression()?;
4102        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
4103        self.expect(&Token::RParen)?;
4104        let body = self.parse_block()?;
4105        let continue_block = self.parse_optional_continue_block()?;
4106        Ok(Statement {
4107            label: None,
4108            kind: StmtKind::While {
4109                condition: cond,
4110                body,
4111                label: None,
4112                continue_block,
4113            },
4114            line,
4115        })
4116    }
4117
4118    /// `while let PAT = EXPR { ... }` — desugars to a `match` that returns 0/1 plus `unless ($tmp) { last }`
4119    /// so bytecode does not run `last` inside a tree-assisted [`Op::AlgebraicMatch`] arm.
4120    fn parse_while_let(&mut self, line: usize) -> StrykeResult<Statement> {
4121        self.advance(); // `let`
4122        let pattern = self.parse_match_pattern()?;
4123        self.expect(&Token::Assign)?;
4124        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_add(1);
4125        let rhs = self.parse_assign_expr();
4126        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_sub(1);
4127        let rhs = rhs?;
4128        let mut user_body = self.parse_block()?;
4129        let continue_block = self.parse_optional_continue_block()?;
4130        user_body.push(Statement::new(
4131            StmtKind::Expression(Expr {
4132                kind: ExprKind::Integer(1),
4133                line,
4134            }),
4135            line,
4136        ));
4137        let tmp = format!("__while_let_{}", self.alloc_desugar_tmp());
4138        let match_expr = Expr {
4139            kind: ExprKind::AlgebraicMatch {
4140                subject: Box::new(rhs),
4141                arms: vec![
4142                    MatchArm {
4143                        pattern,
4144                        guard: None,
4145                        body: Self::expr_do_anon_block(user_body, line),
4146                    },
4147                    MatchArm {
4148                        pattern: MatchPattern::Any,
4149                        guard: None,
4150                        body: Expr {
4151                            kind: ExprKind::Integer(0),
4152                            line,
4153                        },
4154                    },
4155                ],
4156            },
4157            line,
4158        };
4159        let my_stmt = Statement::new(
4160            StmtKind::My(vec![VarDecl {
4161                sigil: Sigil::Scalar,
4162                name: tmp.clone(),
4163                initializer: Some(match_expr),
4164                frozen: false,
4165                type_annotation: None,
4166                list_context: false,
4167            }]),
4168            line,
4169        );
4170        let unless_last = Statement::new(
4171            StmtKind::Unless {
4172                condition: Expr {
4173                    kind: ExprKind::ScalarVar(tmp),
4174                    line,
4175                },
4176                body: vec![Statement::new(StmtKind::Last(None), line)],
4177                else_block: None,
4178            },
4179            line,
4180        );
4181        Ok(Statement::new(
4182            StmtKind::While {
4183                condition: Expr {
4184                    kind: ExprKind::Integer(1),
4185                    line,
4186                },
4187                body: vec![my_stmt, unless_last],
4188                label: None,
4189                continue_block,
4190            },
4191            line,
4192        ))
4193    }
4194
4195    fn parse_until(&mut self) -> StrykeResult<Statement> {
4196        let line = self.peek_line();
4197        self.advance(); // 'until'
4198        self.expect(&Token::LParen)?;
4199        let mut cond = self.parse_expression()?;
4200        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
4201        self.expect(&Token::RParen)?;
4202        let body = self.parse_block()?;
4203        let continue_block = self.parse_optional_continue_block()?;
4204        Ok(Statement {
4205            label: None,
4206            kind: StmtKind::Until {
4207                condition: cond,
4208                body,
4209                label: None,
4210                continue_block,
4211            },
4212            line,
4213        })
4214    }
4215
4216    /// `continue { ... }` after a loop body (optional).
4217    fn parse_optional_continue_block(&mut self) -> StrykeResult<Option<Block>> {
4218        if let Token::Ident(ref kw) = self.peek().clone() {
4219            if kw == "continue" {
4220                self.advance();
4221                return Ok(Some(self.parse_block()?));
4222            }
4223        }
4224        Ok(None)
4225    }
4226
4227    fn parse_for_or_foreach(&mut self) -> StrykeResult<Statement> {
4228        let line = self.peek_line();
4229        self.advance(); // 'for'
4230
4231        // Peek to determine if C-style for or foreach
4232        // C-style: for (init; cond; step)
4233        // foreach-style: for $var (list) or for (list)
4234        match self.peek() {
4235            Token::LParen => {
4236                // Check if next after ( is a semicolon or an assignment — C-style
4237                // Or if it's a list — foreach-style
4238                // Heuristic: if the token after ( is 'my' or '$' followed by
4239                // content that contains ';', it's C-style.
4240                let saved = self.pos;
4241                self.advance(); // consume (
4242                                // Look for semicolon at paren depth 0
4243                let mut depth = 1;
4244                let mut has_semi = false;
4245                let mut scan = self.pos;
4246                while scan < self.tokens.len() {
4247                    match &self.tokens[scan].0 {
4248                        Token::LParen => depth += 1,
4249                        Token::RParen => {
4250                            depth -= 1;
4251                            if depth == 0 {
4252                                break;
4253                            }
4254                        }
4255                        Token::Semicolon if depth == 1 => {
4256                            has_semi = true;
4257                            break;
4258                        }
4259                        _ => {}
4260                    }
4261                    scan += 1;
4262                }
4263                self.pos = saved;
4264
4265                if has_semi {
4266                    self.parse_c_style_for(line)
4267                } else {
4268                    // foreach without explicit var — uses $_
4269                    self.expect(&Token::LParen)?;
4270                    let list = self.parse_expression()?;
4271                    self.expect(&Token::RParen)?;
4272                    let body = self.parse_block()?;
4273                    let continue_block = self.parse_optional_continue_block()?;
4274                    Ok(Statement {
4275                        label: None,
4276                        kind: StmtKind::Foreach {
4277                            var: "_".to_string(),
4278                            list,
4279                            body,
4280                            label: None,
4281                            continue_block,
4282                        },
4283                        line,
4284                    })
4285                }
4286            }
4287            Token::Ident(ref kw) if kw == "my" => {
4288                self.advance(); // 'my'
4289                let var = self.parse_scalar_var_name()?;
4290                self.expect(&Token::LParen)?;
4291                let list = self.parse_expression()?;
4292                self.expect(&Token::RParen)?;
4293                let body = self.parse_block()?;
4294                let continue_block = self.parse_optional_continue_block()?;
4295                Ok(Statement {
4296                    label: None,
4297                    kind: StmtKind::Foreach {
4298                        var,
4299                        list,
4300                        body,
4301                        label: None,
4302                        continue_block,
4303                    },
4304                    line,
4305                })
4306            }
4307            Token::ScalarVar(_) => {
4308                let var = self.parse_scalar_var_name()?;
4309                self.expect(&Token::LParen)?;
4310                let list = self.parse_expression()?;
4311                self.expect(&Token::RParen)?;
4312                let body = self.parse_block()?;
4313                let continue_block = self.parse_optional_continue_block()?;
4314                Ok(Statement {
4315                    label: None,
4316                    kind: StmtKind::Foreach {
4317                        var,
4318                        list,
4319                        body,
4320                        label: None,
4321                        continue_block,
4322                    },
4323                    line,
4324                })
4325            }
4326            _ => self.parse_c_style_for(line),
4327        }
4328    }
4329
4330    fn parse_c_style_for(&mut self, line: usize) -> StrykeResult<Statement> {
4331        self.expect(&Token::LParen)?;
4332        let init = if self.eat(&Token::Semicolon) {
4333            None
4334        } else {
4335            let s = self.parse_statement()?;
4336            self.eat(&Token::Semicolon);
4337            Some(Box::new(s))
4338        };
4339        let mut condition = if matches!(self.peek(), Token::Semicolon) {
4340            None
4341        } else {
4342            Some(self.parse_expression()?)
4343        };
4344        if let Some(ref mut c) = condition {
4345            Self::mark_match_scalar_g_for_boolean_condition(c);
4346        }
4347        self.expect(&Token::Semicolon)?;
4348        let step = if matches!(self.peek(), Token::RParen) {
4349            None
4350        } else {
4351            Some(self.parse_expression()?)
4352        };
4353        self.expect(&Token::RParen)?;
4354        let body = self.parse_block()?;
4355        let continue_block = self.parse_optional_continue_block()?;
4356        Ok(Statement {
4357            label: None,
4358            kind: StmtKind::For {
4359                init,
4360                condition,
4361                step,
4362                body,
4363                label: None,
4364                continue_block,
4365            },
4366            line,
4367        })
4368    }
4369
4370    fn parse_foreach(&mut self) -> StrykeResult<Statement> {
4371        let line = self.peek_line();
4372        self.advance(); // 'foreach'
4373        let var = match self.peek() {
4374            Token::Ident(ref kw) if kw == "my" => {
4375                self.advance();
4376                self.parse_scalar_var_name()?
4377            }
4378            Token::ScalarVar(_) => self.parse_scalar_var_name()?,
4379            _ => "_".to_string(),
4380        };
4381        self.expect(&Token::LParen)?;
4382        let list = self.parse_expression()?;
4383        self.expect(&Token::RParen)?;
4384        let body = self.parse_block()?;
4385        let continue_block = self.parse_optional_continue_block()?;
4386        Ok(Statement {
4387            label: None,
4388            kind: StmtKind::Foreach {
4389                var,
4390                list,
4391                body,
4392                label: None,
4393                continue_block,
4394            },
4395            line,
4396        })
4397    }
4398
4399    fn parse_scalar_var_name(&mut self) -> StrykeResult<String> {
4400        match self.advance() {
4401            (Token::ScalarVar(name), _) => Ok(name),
4402            (tok, line) => {
4403                Err(self.syntax_err(format!("Expected scalar variable, got {:?}", tok), line))
4404            }
4405        }
4406    }
4407
4408    /// After `(` was consumed: Perl5 prototype characters until `)` (or `$)` + `{`).
4409    fn parse_legacy_sub_prototype_tail(&mut self) -> StrykeResult<String> {
4410        let mut s = String::new();
4411        loop {
4412            match self.peek().clone() {
4413                Token::RParen => {
4414                    self.advance();
4415                    break;
4416                }
4417                Token::Eof => {
4418                    return Err(self.syntax_err(
4419                        "Unterminated sub prototype (expected ')' before end of input)",
4420                        self.peek_line(),
4421                    ));
4422                }
4423                Token::ScalarVar(v) if v == ")" => {
4424                    // Lexer merges `$` + `)` into one token (`$)`). In `sub name ($) {`, the
4425                    // closing `)` of the prototype is not a separate `RParen` — next is `{`.
4426                    self.advance();
4427                    s.push('$');
4428                    if matches!(self.peek(), Token::LBrace) {
4429                        break;
4430                    }
4431                }
4432                Token::Ident(i) => {
4433                    let i = i.clone();
4434                    self.advance();
4435                    s.push_str(&i);
4436                }
4437                Token::Semicolon => {
4438                    self.advance();
4439                    s.push(';');
4440                }
4441                Token::LParen => {
4442                    self.advance();
4443                    s.push('(');
4444                }
4445                Token::LBracket => {
4446                    self.advance();
4447                    s.push('[');
4448                }
4449                Token::RBracket => {
4450                    self.advance();
4451                    s.push(']');
4452                }
4453                Token::Backslash => {
4454                    self.advance();
4455                    s.push('\\');
4456                }
4457                Token::Comma => {
4458                    self.advance();
4459                    s.push(',');
4460                }
4461                Token::ScalarVar(v) => {
4462                    let v = v.clone();
4463                    self.advance();
4464                    s.push('$');
4465                    s.push_str(&v);
4466                }
4467                Token::ArrayVar(v) => {
4468                    let v = v.clone();
4469                    self.advance();
4470                    s.push('@');
4471                    s.push_str(&v);
4472                }
4473                // Bare `@` / `%` in prototypes (e.g. Try::Tiny's `sub try (&;@)`).
4474                Token::ArrayAt => {
4475                    self.advance();
4476                    s.push('@');
4477                }
4478                Token::HashVar(v) => {
4479                    let v = v.clone();
4480                    self.advance();
4481                    s.push('%');
4482                    s.push_str(&v);
4483                }
4484                Token::HashPercent => {
4485                    self.advance();
4486                    s.push('%');
4487                }
4488                Token::Plus => {
4489                    self.advance();
4490                    s.push('+');
4491                }
4492                Token::Minus => {
4493                    self.advance();
4494                    s.push('-');
4495                }
4496                Token::BitAnd => {
4497                    self.advance();
4498                    s.push('&');
4499                }
4500                tok => {
4501                    return Err(self.syntax_err(
4502                        format!("Unexpected token in sub prototype: {:?}", tok),
4503                        self.peek_line(),
4504                    ));
4505                }
4506            }
4507        }
4508        Ok(s)
4509    }
4510
4511    fn sub_signature_list_starts_here(&self) -> bool {
4512        match self.peek() {
4513            Token::LBrace | Token::LBracket => true,
4514            Token::ScalarVar(name) if name != "$$" && name != ")" => true,
4515            Token::ArrayVar(_) | Token::HashVar(_) => true,
4516            _ => false,
4517        }
4518    }
4519
4520    fn parse_sub_signature_hash_key(&mut self) -> StrykeResult<String> {
4521        let (tok, line) = self.advance();
4522        match tok {
4523            Token::Ident(i) => Ok(i),
4524            Token::SingleString(s) | Token::DoubleString(s) => Ok(s),
4525            tok => Err(self.syntax_err(
4526                format!(
4527                    "sub signature: expected hash key (identifier or string), got {:?}",
4528                    tok
4529                ),
4530                line,
4531            )),
4532        }
4533    }
4534
4535    fn parse_sub_signature_param_list(&mut self) -> StrykeResult<Vec<SubSigParam>> {
4536        let mut params = Vec::new();
4537        loop {
4538            if matches!(self.peek(), Token::RParen) {
4539                break;
4540            }
4541            match self.peek().clone() {
4542                Token::ScalarVar(name) => {
4543                    if name == "$$" || name == ")" {
4544                        return Err(self.syntax_err(
4545                            format!(
4546                                "`{name}` cannot start a stryke sub signature (use legacy prototype `($$)` etc.)"
4547                            ),
4548                            self.peek_line(),
4549                        ));
4550                    }
4551                    self.advance();
4552                    let ty = if self.eat(&Token::Colon) {
4553                        match self.peek() {
4554                            Token::Ident(ref tname) => {
4555                                let tname = tname.clone();
4556                                self.advance();
4557                                Some(match tname.as_str() {
4558                                    "Int" => PerlTypeName::Int,
4559                                    "Str" => PerlTypeName::Str,
4560                                    "Float" => PerlTypeName::Float,
4561                                    "Bool" => PerlTypeName::Bool,
4562                                    "Array" => PerlTypeName::Array,
4563                                    "Hash" => PerlTypeName::Hash,
4564                                    "Ref" => PerlTypeName::Ref,
4565                                    "Any" => PerlTypeName::Any,
4566                                    _ => PerlTypeName::Struct(tname),
4567                                })
4568                            }
4569                            _ => {
4570                                return Err(self.syntax_err(
4571                                    "expected type name after `:` in sub signature",
4572                                    self.peek_line(),
4573                                ));
4574                            }
4575                        }
4576                    } else {
4577                        None
4578                    };
4579                    // Check for default value: `$x = expr`
4580                    let default = if self.eat(&Token::Assign) {
4581                        Some(Box::new(self.parse_ternary()?))
4582                    } else {
4583                        None
4584                    };
4585                    params.push(SubSigParam::Scalar(name, ty, default));
4586                }
4587                Token::ArrayVar(name) => {
4588                    self.advance();
4589                    let default = if self.eat(&Token::Assign) {
4590                        Some(Box::new(self.parse_ternary()?))
4591                    } else {
4592                        None
4593                    };
4594                    params.push(SubSigParam::Array(name, default));
4595                }
4596                Token::HashVar(name) => {
4597                    self.advance();
4598                    let default = if self.eat(&Token::Assign) {
4599                        Some(Box::new(self.parse_ternary()?))
4600                    } else {
4601                        None
4602                    };
4603                    params.push(SubSigParam::Hash(name, default));
4604                }
4605                Token::LBracket => {
4606                    self.advance();
4607                    let elems = self.parse_match_array_elems_until_rbracket()?;
4608                    params.push(SubSigParam::ArrayDestruct(elems));
4609                }
4610                Token::LBrace => {
4611                    self.advance();
4612                    let mut pairs = Vec::new();
4613                    loop {
4614                        if matches!(self.peek(), Token::RBrace | Token::Eof) {
4615                            break;
4616                        }
4617                        if self.eat(&Token::Comma) {
4618                            continue;
4619                        }
4620                        let key = self.parse_sub_signature_hash_key()?;
4621                        self.expect(&Token::FatArrow)?;
4622                        let bind = self.parse_scalar_var_name()?;
4623                        pairs.push((key, bind));
4624                        self.eat(&Token::Comma);
4625                    }
4626                    self.expect(&Token::RBrace)?;
4627                    params.push(SubSigParam::HashDestruct(pairs));
4628                }
4629                tok => {
4630                    return Err(self.syntax_err(
4631                        format!(
4632                            "expected `$name`, `[ ... ]`, or `{{ ... }}` in sub signature, got {:?}",
4633                            tok
4634                        ),
4635                        self.peek_line(),
4636                    ));
4637                }
4638            }
4639            match self.peek() {
4640                Token::Comma => {
4641                    self.advance();
4642                    if matches!(self.peek(), Token::RParen) {
4643                        return Err(self.syntax_err(
4644                            "trailing `,` before `)` in sub signature",
4645                            self.peek_line(),
4646                        ));
4647                    }
4648                }
4649                Token::RParen => break,
4650                _ => {
4651                    return Err(self.syntax_err(
4652                        format!(
4653                            "expected `,` or `)` after sub signature parameter, got {:?}",
4654                            self.peek()
4655                        ),
4656                        self.peek_line(),
4657                    ));
4658                }
4659            }
4660        }
4661        Ok(params)
4662    }
4663
4664    /// Optional `sub` parens: either a Perl 5 prototype string or a stryke **`$name` / `{ k => $v }`** signature.
4665    fn parse_sub_sig_or_prototype_opt(
4666        &mut self,
4667    ) -> StrykeResult<(Vec<SubSigParam>, Option<String>)> {
4668        if !matches!(self.peek(), Token::LParen) {
4669            return Ok((vec![], None));
4670        }
4671        self.advance();
4672        if matches!(self.peek(), Token::RParen) {
4673            self.advance();
4674            return Ok((vec![], Some(String::new())));
4675        }
4676        if self.sub_signature_list_starts_here() {
4677            let params = self.parse_sub_signature_param_list()?;
4678            self.expect(&Token::RParen)?;
4679            return Ok((params, None));
4680        }
4681        let proto = self.parse_legacy_sub_prototype_tail()?;
4682        Ok((vec![], Some(proto)))
4683    }
4684
4685    /// Optional subroutine attributes after name/prototype: `sub foo : lvalue { }`, `sub : ATTR(ARGS) { }`.
4686    fn parse_sub_attributes(&mut self) -> StrykeResult<()> {
4687        while self.eat(&Token::Colon) {
4688            match self.advance() {
4689                (Token::Ident(_), _) => {}
4690                (tok, line) => {
4691                    return Err(self.syntax_err(
4692                        format!("Expected attribute name after `:`, got {:?}", tok),
4693                        line,
4694                    ));
4695                }
4696            }
4697            if self.eat(&Token::LParen) {
4698                let mut depth = 1usize;
4699                while depth > 0 {
4700                    match self.advance().0 {
4701                        Token::LParen => depth += 1,
4702                        Token::RParen => {
4703                            depth -= 1;
4704                        }
4705                        Token::Eof => {
4706                            return Err(self.syntax_err(
4707                                "Unterminated sub attribute argument list",
4708                                self.peek_line(),
4709                            ));
4710                        }
4711                        _ => {}
4712                    }
4713                }
4714            }
4715        }
4716        Ok(())
4717    }
4718
4719    /// After `fn` + optional `(SIG)` + attrs: stryke-only `= EXPR` (one assign-level expression;
4720    /// no top-level `,` after the expression). Returns `None` if the next token is not `=`.
4721    fn try_parse_fn_assign_shorthand_body(&mut self) -> StrykeResult<Option<Block>> {
4722        if !self.eat(&Token::Assign) {
4723            return Ok(None);
4724        }
4725        let expr = self.parse_assign_expr()?;
4726        if matches!(self.peek(), Token::Comma) {
4727            return Err(self.syntax_err(
4728                "`fn ... =` allows only a single expression; use `fn ... { ... }` for multiple statements",
4729                self.peek_line(),
4730            ));
4731        }
4732        let eline = expr.line;
4733        self.eat(&Token::Semicolon);
4734        let mut body = vec![Statement {
4735            label: None,
4736            kind: StmtKind::Expression(expr),
4737            line: eline,
4738        }];
4739        Self::default_topic_for_sole_bareword(&mut body);
4740        Ok(Some(body))
4741    }
4742
4743    /// After `fn` + optional `(SIG)` + attrs: `{ ... }` or stryke-only `= EXPR` (see
4744    /// [`Self::try_parse_fn_assign_shorthand_body`]). `sub` always requires `{ ... }`.
4745    fn parse_fn_eq_body_or_block(&mut self, is_sub_keyword: bool) -> StrykeResult<Block> {
4746        if !is_sub_keyword {
4747            if let Some(block) = self.try_parse_fn_assign_shorthand_body()? {
4748                return Ok(block);
4749            }
4750        }
4751        self.parse_block()
4752    }
4753
4754    fn parse_sub_decl(&mut self, is_sub_keyword: bool) -> StrykeResult<Statement> {
4755        let line = self.peek_line();
4756        self.advance(); // 'sub' or 'fn'
4757        match self.peek().clone() {
4758            Token::Ident(_) => {
4759                let name = self.parse_package_qualified_identifier()?;
4760                // Topic-slot barewords (`_`, `_<`, `_<<`, `_<<<`, `_<<<<`,
4761                // `_0`, `_1`, …, `_N`, plus `_N<+` chain forms) are scalar
4762                // refs to the current/positional/outer topic. A user-defined
4763                // sub with any of these names — bare or package-qualified —
4764                // would shadow the topic in expression position and silently
4765                // break every `_`-aware builtin (`map { _ }`, `say _`,
4766                // `lc _`, …). Reject ALL forms at parse time, including
4767                // `Foo::_`, `Pkg::_0`, `My::Module::_<<<<`.
4768                let bare = name.rsplit("::").next().unwrap_or(&name);
4769                if Self::is_underscore_topic_slot(bare) {
4770                    return Err(self.syntax_err(
4771                        format!(
4772                            "`fn {}` would shadow the topic-slot scalar; pick a different name",
4773                            name
4774                        ),
4775                        line,
4776                    ));
4777                }
4778                if Self::is_reserved_special_var_name(bare) {
4779                    return Err(self.syntax_err(
4780                        format!(
4781                            "`fn {}` would shadow a Perl special variable / filehandle / compile-time token; pick a different name",
4782                            name
4783                        ),
4784                        line,
4785                    ));
4786                }
4787                // Allow shadowing builtins:
4788                // - In compat mode (full Perl 5)
4789                // - When parsing a module (imports should work)
4790                // Block shadowing:
4791                // - In user code (default mode, not parsing module)
4792                // - Always in no-interop mode
4793                let allow_shadow =
4794                    crate::compat_mode() || (self.parsing_module && !crate::no_interop_mode());
4795                if !allow_shadow {
4796                    self.check_udf_shadows_builtin(&name, line)?;
4797                }
4798                self.declared_subs.insert(name.clone());
4799                let (params, prototype) = self.parse_sub_sig_or_prototype_opt()?;
4800                self.parse_sub_attributes()?;
4801                let body = self.parse_fn_eq_body_or_block(is_sub_keyword)?;
4802                Ok(Statement {
4803                    label: None,
4804                    kind: StmtKind::SubDecl {
4805                        name,
4806                        params,
4807                        body,
4808                        prototype,
4809                    },
4810                    line,
4811                })
4812            }
4813            Token::LParen | Token::LBrace | Token::Colon => {
4814                // In no-interop mode, `sub {}` anonymous is not allowed — must use `fn {}`
4815                if is_sub_keyword && crate::no_interop_mode() {
4816                    return Err(self.syntax_err(
4817                        "stryke uses `fn {}` instead of `sub {}` (--no-interop)",
4818                        line,
4819                    ));
4820                }
4821                // Statement-level anonymous sub: `fn { }`, `sub () { }`, `sub :lvalue { }`
4822                let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
4823                self.parse_sub_attributes()?;
4824                let body = self.parse_fn_eq_body_or_block(is_sub_keyword)?;
4825                Ok(Statement {
4826                    label: None,
4827                    kind: StmtKind::Expression(Expr {
4828                        kind: ExprKind::CodeRef { params, body },
4829                        line,
4830                    }),
4831                    line,
4832                })
4833            }
4834            tok => {
4835                // Sigil-form topic-slot names (`fn $_`, `fn $_<`, `fn $_0`,
4836                // `fn @_`, `fn %_`, …) are also rejected with the same
4837                // foot-gun message as the bareword form. Without this branch
4838                // the user gets a confusing generic "Expected sub name" error.
4839                let topic_name = match &tok {
4840                    Token::ScalarVar(n) | Token::ArrayVar(n) | Token::HashVar(n)
4841                        if Self::is_underscore_topic_slot(n) =>
4842                    {
4843                        Some((
4844                            match &tok {
4845                                Token::ScalarVar(_) => '$',
4846                                Token::ArrayVar(_) => '@',
4847                                Token::HashVar(_) => '%',
4848                                _ => unreachable!(),
4849                            },
4850                            n.clone(),
4851                        ))
4852                    }
4853                    _ => None,
4854                };
4855                if let Some((sigil, n)) = topic_name {
4856                    return Err(self.syntax_err(
4857                        format!(
4858                            "`fn {}{}` would shadow the topic-slot scalar; pick a different name",
4859                            sigil, n
4860                        ),
4861                        self.peek_line(),
4862                    ));
4863                }
4864                // Sigil-form Perl special variables / globals — same foot-gun.
4865                // Catches `fn $@`, `fn $!`, `fn $/`, `fn $\\`, `fn $,`, `fn $;`,
4866                // `fn $"`, `fn $.`, `fn $0`, `fn $$`, `fn $?`, `fn $1`-`$9`,
4867                // `fn $^I`, `fn @ARGV`, `fn @INC`, `fn %ENV`, `fn %SIG`, etc.
4868                let special_var = match &tok {
4869                    Token::ScalarVar(n) | Token::ArrayVar(n) | Token::HashVar(n) => Some((
4870                        match &tok {
4871                            Token::ScalarVar(_) => '$',
4872                            Token::ArrayVar(_) => '@',
4873                            Token::HashVar(_) => '%',
4874                            _ => unreachable!(),
4875                        },
4876                        n.clone(),
4877                    )),
4878                    _ => None,
4879                };
4880                if let Some((sigil, n)) = special_var {
4881                    return Err(self.syntax_err(
4882                        format!(
4883                            "`fn {}{}` would shadow a Perl special variable / global; pick a different name",
4884                            sigil, n
4885                        ),
4886                        self.peek_line(),
4887                    ));
4888                }
4889                // After `fn`, `%` lexes as `Token::Percent` (modulo) rather
4890                // than a hash sigil — but `fn %ENV { }`, `fn %SIG { }`,
4891                // `fn %_ { }`, etc. all reach here. Emit the same foot-gun
4892                // message as the sigil-form catch above.
4893                if matches!(tok, Token::Percent) {
4894                    return Err(self.syntax_err(
4895                        "`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 { ... }`",
4896                        self.peek_line(),
4897                    ));
4898                }
4899                Err(self.syntax_err(
4900                    format!("Expected sub name, `(`, `{{`, or `:`, got {:?}", tok),
4901                    self.peek_line(),
4902                ))
4903            }
4904        }
4905    }
4906
4907    /// `before|after|around "<glob>" { ... }` — register AOP advice.
4908    /// The pattern is a glob (`*`, `?`) matched against the called sub's bare name.
4909    fn parse_advice_decl(&mut self, kind: crate::ast::AdviceKind) -> StrykeResult<Statement> {
4910        let line = self.peek_line();
4911        self.advance(); // before/after/around
4912        let pattern = match self.advance() {
4913            (Token::SingleString(s), _) | (Token::DoubleString(s), _) => s,
4914            (tok, err_line) => {
4915                return Err(self.syntax_err(
4916                    format!(
4917                        "Expected string-literal pattern after `{}`, got {:?}",
4918                        match kind {
4919                            crate::ast::AdviceKind::Before => "before",
4920                            crate::ast::AdviceKind::After => "after",
4921                            crate::ast::AdviceKind::Around => "around",
4922                        },
4923                        tok
4924                    ),
4925                    err_line,
4926                ));
4927            }
4928        };
4929        let body = self.parse_block()?;
4930        Ok(Statement {
4931            label: None,
4932            kind: StmtKind::AdviceDecl {
4933                kind,
4934                pattern,
4935                body,
4936            },
4937            line,
4938        })
4939    }
4940
4941    /// `struct Name { field => Type, ... ; fn method { } }`
4942    fn parse_struct_decl(&mut self) -> StrykeResult<Statement> {
4943        let line = self.peek_line();
4944        self.advance(); // struct
4945        let raw_name = self.parse_package_qualified_identifier().map_err(|_| {
4946            self.syntax_err(
4947                format!("Expected struct name, got {:?}", self.peek()),
4948                self.peek_line(),
4949            )
4950        })?;
4951        let name = if raw_name.contains("::") || self.current_package == "main" {
4952            raw_name
4953        } else {
4954            format!("{}::{}", self.current_package, raw_name)
4955        };
4956        self.expect(&Token::LBrace)?;
4957        let mut fields = Vec::new();
4958        let mut methods = Vec::new();
4959        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
4960            // Check for method definition: `fn name { }` or `fn name { }`
4961            let is_method = match self.peek() {
4962                Token::Ident(s) => s == "fn" || s == "sub",
4963                _ => false,
4964            };
4965            if is_method {
4966                let is_sub_keyword = matches!(self.peek(), Token::Ident(ref s) if s == "sub");
4967                self.advance(); // fn/sub
4968                let method_name = match self.advance() {
4969                    (Token::Ident(n), _) => n,
4970                    (tok, err_line) => {
4971                        return Err(self
4972                            .syntax_err(format!("Expected method name, got {:?}", tok), err_line))
4973                    }
4974                };
4975                // Parse optional signature: `($self, $arg: Type, ...)`
4976                let params = if self.eat(&Token::LParen) {
4977                    let p = self.parse_sub_signature_param_list()?;
4978                    self.expect(&Token::RParen)?;
4979                    p
4980                } else {
4981                    Vec::new()
4982                };
4983                let body = if is_sub_keyword {
4984                    self.parse_block()?
4985                } else {
4986                    self.parse_fn_eq_body_or_block(false)?
4987                };
4988                methods.push(crate::ast::StructMethod {
4989                    name: method_name,
4990                    params,
4991                    body,
4992                });
4993                // Optional trailing comma/semicolon after method
4994                self.eat(&Token::Comma);
4995                self.eat(&Token::Semicolon);
4996                continue;
4997            }
4998
4999            let field_name = match self.advance() {
5000                (Token::Ident(n), _) => n,
5001                (tok, err_line) => {
5002                    return Err(
5003                        self.syntax_err(format!("Expected field name, got {:?}", tok), err_line)
5004                    )
5005                }
5006            };
5007            // Support three forms:
5008            //   - `field => Type`   (Perl-style fat-comma)
5009            //   - `field: Type`     (Rust/class-style colon)
5010            //   - bare `field`      (implies Any type)
5011            let ty = if self.eat(&Token::FatArrow) || self.eat(&Token::Colon) {
5012                self.parse_type_name()?
5013            } else {
5014                crate::ast::PerlTypeName::Any
5015            };
5016            let default = if self.eat(&Token::Assign) {
5017                // Use parse_ternary to avoid consuming commas (next field separator)
5018                Some(self.parse_ternary()?)
5019            } else {
5020                None
5021            };
5022            fields.push(StructField {
5023                name: field_name,
5024                ty,
5025                default,
5026            });
5027            if !self.eat(&Token::Comma) {
5028                // Also allow semicolons as field separators
5029                self.eat(&Token::Semicolon);
5030            }
5031        }
5032        self.expect(&Token::RBrace)?;
5033        self.eat(&Token::Semicolon);
5034        Ok(Statement {
5035            label: None,
5036            kind: StmtKind::StructDecl {
5037                def: StructDef {
5038                    name,
5039                    fields,
5040                    methods,
5041                },
5042            },
5043            line,
5044        })
5045    }
5046
5047    /// `enum Name { Variant1, Variant2 => Type, ... }`
5048    fn parse_enum_decl(&mut self) -> StrykeResult<Statement> {
5049        let line = self.peek_line();
5050        self.advance(); // enum
5051        let raw_name = self.parse_package_qualified_identifier().map_err(|_| {
5052            self.syntax_err(
5053                format!("Expected enum name, got {:?}", self.peek()),
5054                self.peek_line(),
5055            )
5056        })?;
5057        let name = if raw_name.contains("::") || self.current_package == "main" {
5058            raw_name
5059        } else {
5060            format!("{}::{}", self.current_package, raw_name)
5061        };
5062        self.expect(&Token::LBrace)?;
5063        let mut variants = Vec::new();
5064        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
5065            let variant_name = match self.advance() {
5066                (Token::Ident(n), _) => n,
5067                (tok, err_line) => {
5068                    return Err(
5069                        self.syntax_err(format!("Expected variant name, got {:?}", tok), err_line)
5070                    )
5071                }
5072            };
5073            let ty = if self.eat(&Token::FatArrow) {
5074                Some(self.parse_type_name()?)
5075            } else {
5076                None
5077            };
5078            variants.push(EnumVariant {
5079                name: variant_name,
5080                ty,
5081            });
5082            if !self.eat(&Token::Comma) {
5083                self.eat(&Token::Semicolon);
5084            }
5085        }
5086        self.expect(&Token::RBrace)?;
5087        self.eat(&Token::Semicolon);
5088        Ok(Statement {
5089            label: None,
5090            kind: StmtKind::EnumDecl {
5091                def: EnumDef { name, variants },
5092            },
5093            line,
5094        })
5095    }
5096
5097    /// `[abstract|final] class Name extends Parent impl Trait { fields; methods }`
5098    fn parse_class_decl(&mut self, is_abstract: bool, is_final: bool) -> StrykeResult<Statement> {
5099        use crate::ast::{ClassDef, ClassField, ClassMethod, ClassStaticField, Visibility};
5100        let line = self.peek_line();
5101        self.advance(); // class
5102        let raw_name = self.parse_package_qualified_identifier().map_err(|_| {
5103            self.syntax_err(
5104                format!("Expected class name, got {:?}", self.peek()),
5105                self.peek_line(),
5106            )
5107        })?;
5108        // Bare `class Point` inside `package Geo` registers as `Geo::Point`,
5109        // matching the unqualified-fn rule. Already-qualified names pass
5110        // through unchanged, and `main` keeps the bare spelling so
5111        // existing test code that calls `Point->new(...)` still resolves.
5112        let name = if raw_name.contains("::") || self.current_package == "main" {
5113            raw_name
5114        } else {
5115            format!("{}::{}", self.current_package, raw_name)
5116        };
5117
5118        // Parse `extends Parent1, Parent2` (each may be namespaced: `Foo::Base`)
5119        let mut extends = Vec::new();
5120        if matches!(self.peek(), Token::Ident(ref s) if s == "extends") {
5121            self.advance(); // extends
5122            loop {
5123                let parent = self.parse_package_qualified_identifier().map_err(|_| {
5124                    self.syntax_err(
5125                        format!(
5126                            "Expected parent class name after `extends`, got {:?}",
5127                            self.peek()
5128                        ),
5129                        self.peek_line(),
5130                    )
5131                })?;
5132                extends.push(parent);
5133                if !self.eat(&Token::Comma) {
5134                    break;
5135                }
5136            }
5137        }
5138
5139        // Parse `impl Trait1, Trait2` (each may be namespaced: `Foo::Trait`)
5140        let mut implements = Vec::new();
5141        if matches!(self.peek(), Token::Ident(ref s) if s == "impl") {
5142            self.advance(); // impl
5143            loop {
5144                let trait_name = self.parse_package_qualified_identifier().map_err(|_| {
5145                    self.syntax_err(
5146                        format!("Expected trait name after `impl`, got {:?}", self.peek()),
5147                        self.peek_line(),
5148                    )
5149                })?;
5150                implements.push(trait_name);
5151                if !self.eat(&Token::Comma) {
5152                    break;
5153                }
5154            }
5155        }
5156
5157        self.expect(&Token::LBrace)?;
5158        let mut fields = Vec::new();
5159        let mut methods = Vec::new();
5160        let mut static_fields = Vec::new();
5161
5162        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
5163            // Check for visibility modifier
5164            let visibility = match self.peek() {
5165                Token::Ident(ref s) if s == "pub" => {
5166                    self.advance();
5167                    Visibility::Public
5168                }
5169                Token::Ident(ref s) if s == "priv" => {
5170                    self.advance();
5171                    Visibility::Private
5172                }
5173                Token::Ident(ref s) if s == "prot" => {
5174                    self.advance();
5175                    Visibility::Protected
5176                }
5177                _ => Visibility::Public, // default public
5178            };
5179
5180            // Check for static field: `static name: Type = default`
5181            if matches!(self.peek(), Token::Ident(ref s) if s == "static") {
5182                self.advance(); // static
5183
5184                // Could be a static method (`static fn`) or static field
5185                if matches!(self.peek(), Token::Ident(ref s) if s == "fn" || s == "sub") {
5186                    // static fn is same as fn Self.name — handled below but not here
5187                    return Err(self.syntax_err(
5188                        "use `fn Self.name` for static methods, not `static fn`",
5189                        self.peek_line(),
5190                    ));
5191                }
5192
5193                let field_name = match self.advance() {
5194                    (Token::Ident(n), _) => n,
5195                    (tok, err_line) => {
5196                        return Err(self.syntax_err(
5197                            format!("Expected static field name, got {:?}", tok),
5198                            err_line,
5199                        ))
5200                    }
5201                };
5202
5203                let ty = if self.eat(&Token::Colon) {
5204                    self.parse_type_name()?
5205                } else {
5206                    crate::ast::PerlTypeName::Any
5207                };
5208
5209                let default = if self.eat(&Token::Assign) {
5210                    Some(self.parse_ternary()?)
5211                } else {
5212                    None
5213                };
5214
5215                static_fields.push(ClassStaticField {
5216                    name: field_name,
5217                    ty,
5218                    visibility,
5219                    default,
5220                });
5221
5222                if !self.eat(&Token::Comma) {
5223                    self.eat(&Token::Semicolon);
5224                }
5225                continue;
5226            }
5227
5228            // Check for `final` modifier before fn
5229            let method_is_final = matches!(self.peek(), Token::Ident(ref s) if s == "final");
5230            if method_is_final {
5231                self.advance(); // final
5232            }
5233
5234            // Check for method: `fn name` or `fn Self.name` (static)
5235            let is_method = matches!(self.peek(), Token::Ident(ref s) if s == "fn" || s == "sub");
5236            if is_method {
5237                self.advance(); // fn/sub
5238
5239                // Check for static method: `fn Self.name`
5240                let is_static = matches!(self.peek(), Token::Ident(ref s) if s == "Self");
5241                if is_static {
5242                    self.advance(); // Self
5243                    self.expect(&Token::Dot)?;
5244                }
5245
5246                let method_name = match self.advance() {
5247                    (Token::Ident(n), _) => n,
5248                    (tok, err_line) => {
5249                        return Err(self
5250                            .syntax_err(format!("Expected method name, got {:?}", tok), err_line))
5251                    }
5252                };
5253
5254                // Parse optional signature
5255                let params = if self.eat(&Token::LParen) {
5256                    let p = self.parse_sub_signature_param_list()?;
5257                    self.expect(&Token::RParen)?;
5258                    p
5259                } else {
5260                    Vec::new()
5261                };
5262
5263                // Body: `{ ... }`, or `= expr` (same rules as top-level `fn`), or omitted (abstract)
5264                let body = if let Some(b) = self.try_parse_fn_assign_shorthand_body()? {
5265                    Some(b)
5266                } else if matches!(self.peek(), Token::LBrace) {
5267                    Some(self.parse_block()?)
5268                } else {
5269                    None
5270                };
5271
5272                methods.push(ClassMethod {
5273                    name: method_name,
5274                    params,
5275                    body,
5276                    visibility,
5277                    is_static,
5278                    is_final: method_is_final,
5279                });
5280                self.eat(&Token::Comma);
5281                self.eat(&Token::Semicolon);
5282                continue;
5283            } else if method_is_final {
5284                return Err(self.syntax_err("`final` must be followed by `fn`", self.peek_line()));
5285            }
5286
5287            // Parse field: `name: Type = default`
5288            let field_name = match self.advance() {
5289                (Token::Ident(n), _) => n,
5290                (tok, err_line) => {
5291                    return Err(
5292                        self.syntax_err(format!("Expected field name, got {:?}", tok), err_line)
5293                    )
5294                }
5295            };
5296
5297            // Type via colon (`name: Type`) OR fat-comma (`name => Type`).
5298            // The Perl-flavored struct-style fat-comma is accepted on
5299            // classes for symmetry with struct fields.
5300            let ty = if self.eat(&Token::Colon) || self.eat(&Token::FatArrow) {
5301                self.parse_type_name()?
5302            } else {
5303                crate::ast::PerlTypeName::Any
5304            };
5305
5306            // Default value after `=`
5307            let default = if self.eat(&Token::Assign) {
5308                Some(self.parse_ternary()?)
5309            } else {
5310                None
5311            };
5312
5313            fields.push(ClassField {
5314                name: field_name,
5315                ty,
5316                visibility,
5317                default,
5318            });
5319
5320            if !self.eat(&Token::Comma) {
5321                self.eat(&Token::Semicolon);
5322            }
5323        }
5324
5325        self.expect(&Token::RBrace)?;
5326        self.eat(&Token::Semicolon);
5327
5328        Ok(Statement {
5329            label: None,
5330            kind: StmtKind::ClassDecl {
5331                def: ClassDef {
5332                    name,
5333                    is_abstract,
5334                    is_final,
5335                    extends,
5336                    implements,
5337                    fields,
5338                    methods,
5339                    static_fields,
5340                },
5341            },
5342            line,
5343        })
5344    }
5345
5346    /// `trait Name { fn required; fn with_default { } }`
5347    fn parse_trait_decl(&mut self) -> StrykeResult<Statement> {
5348        use crate::ast::{ClassMethod, TraitDef, Visibility};
5349        let line = self.peek_line();
5350        self.advance(); // trait
5351        let raw_name = self.parse_package_qualified_identifier().map_err(|_| {
5352            self.syntax_err(
5353                format!("Expected trait name, got {:?}", self.peek()),
5354                self.peek_line(),
5355            )
5356        })?;
5357        let name = if raw_name.contains("::") || self.current_package == "main" {
5358            raw_name
5359        } else {
5360            format!("{}::{}", self.current_package, raw_name)
5361        };
5362
5363        self.expect(&Token::LBrace)?;
5364        let mut methods = Vec::new();
5365
5366        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
5367            // Optional visibility
5368            let visibility = match self.peek() {
5369                Token::Ident(ref s) if s == "pub" => {
5370                    self.advance();
5371                    Visibility::Public
5372                }
5373                Token::Ident(ref s) if s == "priv" => {
5374                    self.advance();
5375                    Visibility::Private
5376                }
5377                Token::Ident(ref s) if s == "prot" => {
5378                    self.advance();
5379                    Visibility::Protected
5380                }
5381                _ => Visibility::Public,
5382            };
5383
5384            // Expect `fn` or `sub`
5385            if !matches!(self.peek(), Token::Ident(ref s) if s == "fn" || s == "sub") {
5386                return Err(self.syntax_err("Expected `fn` in trait definition", self.peek_line()));
5387            }
5388            self.advance(); // fn/sub
5389
5390            let method_name = match self.advance() {
5391                (Token::Ident(n), _) => n,
5392                (tok, err_line) => {
5393                    return Err(
5394                        self.syntax_err(format!("Expected method name, got {:?}", tok), err_line)
5395                    )
5396                }
5397            };
5398
5399            // Optional signature
5400            let params = if self.eat(&Token::LParen) {
5401                let p = self.parse_sub_signature_param_list()?;
5402                self.expect(&Token::RParen)?;
5403                p
5404            } else {
5405                Vec::new()
5406            };
5407
5408            // Body: `{ ... }`, `= expr`, or omitted (required method)
5409            let body = if let Some(b) = self.try_parse_fn_assign_shorthand_body()? {
5410                Some(b)
5411            } else if matches!(self.peek(), Token::LBrace) {
5412                Some(self.parse_block()?)
5413            } else {
5414                None
5415            };
5416
5417            methods.push(ClassMethod {
5418                name: method_name,
5419                params,
5420                body,
5421                visibility,
5422                is_static: false,
5423                is_final: false,
5424            });
5425
5426            self.eat(&Token::Comma);
5427            self.eat(&Token::Semicolon);
5428        }
5429
5430        self.expect(&Token::RBrace)?;
5431        self.eat(&Token::Semicolon);
5432
5433        Ok(Statement {
5434            label: None,
5435            kind: StmtKind::TraitDecl {
5436                def: TraitDef { name, methods },
5437            },
5438            line,
5439        })
5440    }
5441
5442    fn local_simple_target_to_var_decl(target: &Expr) -> Option<VarDecl> {
5443        match &target.kind {
5444            ExprKind::ScalarVar(name) => Some(VarDecl {
5445                sigil: Sigil::Scalar,
5446                name: name.clone(),
5447                initializer: None,
5448                frozen: false,
5449                type_annotation: None,
5450                list_context: false,
5451            }),
5452            ExprKind::ArrayVar(name) => Some(VarDecl {
5453                sigil: Sigil::Array,
5454                name: name.clone(),
5455                initializer: None,
5456                frozen: false,
5457                type_annotation: None,
5458                list_context: false,
5459            }),
5460            ExprKind::HashVar(name) => Some(VarDecl {
5461                sigil: Sigil::Hash,
5462                name: name.clone(),
5463                initializer: None,
5464                frozen: false,
5465                type_annotation: None,
5466                list_context: false,
5467            }),
5468            ExprKind::Typeglob(name) => Some(VarDecl {
5469                sigil: Sigil::Typeglob,
5470                name: name.clone(),
5471                initializer: None,
5472                frozen: false,
5473                type_annotation: None,
5474                list_context: false,
5475            }),
5476            _ => None,
5477        }
5478    }
5479
5480    fn parse_decl_array_destructure(
5481        &mut self,
5482        keyword: &str,
5483        line: usize,
5484    ) -> StrykeResult<Statement> {
5485        self.expect(&Token::LBracket)?;
5486        let elems = self.parse_match_array_elems_until_rbracket()?;
5487        self.expect(&Token::Assign)?;
5488        self.suppress_scalar_hash_brace += 1;
5489        let rhs = self.parse_expression()?;
5490        self.suppress_scalar_hash_brace -= 1;
5491        let stmt = self.desugar_array_destructure(keyword, line, elems, rhs)?;
5492        self.parse_stmt_postfix_modifier(stmt)
5493    }
5494
5495    fn parse_decl_hash_destructure(
5496        &mut self,
5497        keyword: &str,
5498        line: usize,
5499    ) -> StrykeResult<Statement> {
5500        let MatchPattern::Hash(pairs) = self.parse_match_hash_pattern()? else {
5501            unreachable!("parse_match_hash_pattern returns Hash");
5502        };
5503        self.expect(&Token::Assign)?;
5504        self.suppress_scalar_hash_brace += 1;
5505        let rhs = self.parse_expression()?;
5506        self.suppress_scalar_hash_brace -= 1;
5507        let stmt = self.desugar_hash_destructure(keyword, line, pairs, rhs)?;
5508        self.parse_stmt_postfix_modifier(stmt)
5509    }
5510
5511    fn desugar_array_destructure(
5512        &mut self,
5513        keyword: &str,
5514        line: usize,
5515        elems: Vec<MatchArrayElem>,
5516        rhs: Expr,
5517    ) -> StrykeResult<Statement> {
5518        let tmp = format!("__stryke_ds_{}", self.alloc_desugar_tmp());
5519        let mut stmts: Vec<Statement> = Vec::new();
5520        stmts.push(destructure_stmt_from_var_decls(
5521            keyword,
5522            vec![VarDecl {
5523                sigil: Sigil::Scalar,
5524                name: tmp.clone(),
5525                initializer: Some(rhs),
5526                frozen: false,
5527                type_annotation: None,
5528                list_context: false,
5529            }],
5530            line,
5531        ));
5532
5533        let has_rest = elems
5534            .iter()
5535            .any(|e| matches!(e, MatchArrayElem::Rest | MatchArrayElem::RestBind(_)));
5536        let fixed_slots = elems
5537            .iter()
5538            .filter(|e| {
5539                matches!(
5540                    e,
5541                    MatchArrayElem::CaptureScalar(_) | MatchArrayElem::Expr(_)
5542                )
5543            })
5544            .count();
5545        if !has_rest {
5546            let cond = Expr {
5547                kind: ExprKind::BinOp {
5548                    left: Box::new(destructure_expr_array_len(&tmp, line)),
5549                    op: BinOp::NumEq,
5550                    right: Box::new(Expr {
5551                        kind: ExprKind::Integer(fixed_slots as i64),
5552                        line,
5553                    }),
5554                },
5555                line,
5556            };
5557            stmts.push(destructure_stmt_unless_die(
5558                line,
5559                cond,
5560                "array destructure: length mismatch",
5561            ));
5562        }
5563
5564        let mut idx: i64 = 0;
5565        for elem in elems {
5566            match elem {
5567                MatchArrayElem::Rest => break,
5568                MatchArrayElem::RestBind(name) => {
5569                    let list_source = Expr {
5570                        kind: ExprKind::Deref {
5571                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
5572                            kind: Sigil::Array,
5573                        },
5574                        line,
5575                    };
5576                    let last_ix = Expr {
5577                        kind: ExprKind::BinOp {
5578                            left: Box::new(destructure_expr_array_len(&tmp, line)),
5579                            op: BinOp::Sub,
5580                            right: Box::new(Expr {
5581                                kind: ExprKind::Integer(1),
5582                                line,
5583                            }),
5584                        },
5585                        line,
5586                    };
5587                    let range = Expr {
5588                        kind: ExprKind::Range {
5589                            from: Box::new(Expr {
5590                                kind: ExprKind::Integer(idx),
5591                                line,
5592                            }),
5593                            to: Box::new(last_ix),
5594                            exclusive: false,
5595                            step: None,
5596                        },
5597                        line,
5598                    };
5599                    let slice = Expr {
5600                        kind: ExprKind::AnonymousListSlice {
5601                            source: Box::new(list_source),
5602                            indices: vec![range],
5603                        },
5604                        line,
5605                    };
5606                    stmts.push(destructure_stmt_from_var_decls(
5607                        keyword,
5608                        vec![VarDecl {
5609                            sigil: Sigil::Array,
5610                            name,
5611                            initializer: Some(slice),
5612                            frozen: false,
5613                            type_annotation: None,
5614                            list_context: false,
5615                        }],
5616                        line,
5617                    ));
5618                    break;
5619                }
5620                MatchArrayElem::CaptureScalar(name) => {
5621                    let arrow = Expr {
5622                        kind: ExprKind::ArrowDeref {
5623                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
5624                            index: Box::new(Expr {
5625                                kind: ExprKind::Integer(idx),
5626                                line,
5627                            }),
5628                            kind: DerefKind::Array,
5629                        },
5630                        line,
5631                    };
5632                    stmts.push(destructure_stmt_from_var_decls(
5633                        keyword,
5634                        vec![VarDecl {
5635                            sigil: Sigil::Scalar,
5636                            name,
5637                            initializer: Some(arrow),
5638                            frozen: false,
5639                            list_context: false,
5640                            type_annotation: None,
5641                        }],
5642                        line,
5643                    ));
5644                    idx += 1;
5645                }
5646                MatchArrayElem::Expr(e) => {
5647                    let elem_subj = Expr {
5648                        kind: ExprKind::ArrowDeref {
5649                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
5650                            index: Box::new(Expr {
5651                                kind: ExprKind::Integer(idx),
5652                                line,
5653                            }),
5654                            kind: DerefKind::Array,
5655                        },
5656                        line,
5657                    };
5658                    let match_expr = Expr {
5659                        kind: ExprKind::AlgebraicMatch {
5660                            subject: Box::new(elem_subj),
5661                            arms: vec![
5662                                MatchArm {
5663                                    pattern: MatchPattern::Value(Box::new(e.clone())),
5664                                    guard: None,
5665                                    body: Expr {
5666                                        kind: ExprKind::Integer(0),
5667                                        line,
5668                                    },
5669                                },
5670                                MatchArm {
5671                                    pattern: MatchPattern::Any,
5672                                    guard: None,
5673                                    body: Expr {
5674                                        kind: ExprKind::Die(vec![Expr {
5675                                            kind: ExprKind::String(
5676                                                "array destructure: element pattern mismatch"
5677                                                    .to_string(),
5678                                            ),
5679                                            line,
5680                                        }]),
5681                                        line,
5682                                    },
5683                                },
5684                            ],
5685                        },
5686                        line,
5687                    };
5688                    stmts.push(Statement {
5689                        label: None,
5690                        kind: StmtKind::Expression(match_expr),
5691                        line,
5692                    });
5693                    idx += 1;
5694                }
5695            }
5696        }
5697
5698        Ok(Statement {
5699            label: None,
5700            kind: StmtKind::StmtGroup(stmts),
5701            line,
5702        })
5703    }
5704
5705    fn desugar_hash_destructure(
5706        &mut self,
5707        keyword: &str,
5708        line: usize,
5709        pairs: Vec<MatchHashPair>,
5710        rhs: Expr,
5711    ) -> StrykeResult<Statement> {
5712        let tmp = format!("__stryke_ds_{}", self.alloc_desugar_tmp());
5713        let mut stmts: Vec<Statement> = Vec::new();
5714        stmts.push(destructure_stmt_from_var_decls(
5715            keyword,
5716            vec![VarDecl {
5717                sigil: Sigil::Scalar,
5718                name: tmp.clone(),
5719                initializer: Some(rhs),
5720                frozen: false,
5721                type_annotation: None,
5722                list_context: false,
5723            }],
5724            line,
5725        ));
5726
5727        for pair in pairs {
5728            match pair {
5729                MatchHashPair::KeyOnly { key } => {
5730                    let exists_op = Expr {
5731                        kind: ExprKind::Exists(Box::new(Expr {
5732                            kind: ExprKind::ArrowDeref {
5733                                expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
5734                                index: Box::new(key),
5735                                kind: DerefKind::Hash,
5736                            },
5737                            line,
5738                        })),
5739                        line,
5740                    };
5741                    stmts.push(destructure_stmt_unless_die(
5742                        line,
5743                        exists_op,
5744                        "hash destructure: missing required key",
5745                    ));
5746                }
5747                MatchHashPair::Capture { key, name } => {
5748                    let init = Expr {
5749                        kind: ExprKind::ArrowDeref {
5750                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
5751                            index: Box::new(key),
5752                            kind: DerefKind::Hash,
5753                        },
5754                        line,
5755                    };
5756                    stmts.push(destructure_stmt_from_var_decls(
5757                        keyword,
5758                        vec![VarDecl {
5759                            sigil: Sigil::Scalar,
5760                            name,
5761                            initializer: Some(init),
5762                            frozen: false,
5763                            type_annotation: None,
5764                            list_context: false,
5765                        }],
5766                        line,
5767                    ));
5768                }
5769            }
5770        }
5771
5772        Ok(Statement {
5773            label: None,
5774            kind: StmtKind::StmtGroup(stmts),
5775            line,
5776        })
5777    }
5778
5779    fn parse_my_our_local(
5780        &mut self,
5781        keyword: &str,
5782        allow_type_annotation: bool,
5783    ) -> StrykeResult<Statement> {
5784        let line = self.peek_line();
5785        self.advance(); // 'my'/'our'/'local'
5786
5787        if keyword == "local"
5788            && !matches!(self.peek(), Token::LParen | Token::LBracket | Token::LBrace)
5789        {
5790            let target = self.parse_postfix()?;
5791            let mut initializer: Option<Expr> = None;
5792            if self.eat(&Token::Assign) {
5793                initializer = Some(self.parse_expression()?);
5794            } else if matches!(
5795                self.peek(),
5796                Token::OrAssign | Token::DefinedOrAssign | Token::AndAssign
5797            ) {
5798                if matches!(&target.kind, ExprKind::Typeglob(_)) {
5799                    return Err(self.syntax_err(
5800                        "compound assignment on typeglob declaration is not supported",
5801                        self.peek_line(),
5802                    ));
5803                }
5804                let op = match self.peek().clone() {
5805                    Token::OrAssign => BinOp::LogOr,
5806                    Token::DefinedOrAssign => BinOp::DefinedOr,
5807                    Token::AndAssign => BinOp::LogAnd,
5808                    _ => unreachable!(),
5809                };
5810                self.advance();
5811                let rhs = self.parse_assign_expr()?;
5812                let tgt_line = target.line;
5813                initializer = Some(Expr {
5814                    kind: ExprKind::CompoundAssign {
5815                        target: Box::new(target.clone()),
5816                        op,
5817                        value: Box::new(rhs),
5818                    },
5819                    line: tgt_line,
5820                });
5821            }
5822
5823            let kind = if let Some(mut decl) = Self::local_simple_target_to_var_decl(&target) {
5824                decl.initializer = initializer;
5825                StmtKind::Local(vec![decl])
5826            } else {
5827                StmtKind::LocalExpr {
5828                    target,
5829                    initializer,
5830                }
5831            };
5832            let stmt = Statement {
5833                label: None,
5834                kind,
5835                line,
5836            };
5837            return self.parse_stmt_postfix_modifier(stmt);
5838        }
5839
5840        if matches!(self.peek(), Token::LBracket) {
5841            return self.parse_decl_array_destructure(keyword, line);
5842        }
5843        if matches!(self.peek(), Token::LBrace) {
5844            return self.parse_decl_hash_destructure(keyword, line);
5845        }
5846
5847        let mut decls = Vec::new();
5848        let used_parens = self.eat(&Token::LParen);
5849
5850        if used_parens {
5851            // my ($a, @b, %c)
5852            while !matches!(self.peek(), Token::RParen | Token::Eof) {
5853                let decl = self.parse_var_decl(allow_type_annotation)?;
5854                decls.push(decl);
5855                if !self.eat(&Token::Comma) {
5856                    break;
5857                }
5858            }
5859            self.expect(&Token::RParen)?;
5860        } else {
5861            decls.push(self.parse_var_decl(allow_type_annotation)?);
5862        }
5863        // my ($x) = @a  → list context on the scalar (gets first element, not count)
5864        if used_parens {
5865            for decl in &mut decls {
5866                decl.list_context = true;
5867            }
5868        }
5869
5870        // Optional initializer: my $x = expr — plus `our @EXPORT = our @EXPORT_OK = qw(...)` (Try::Tiny).
5871        if self.eat(&Token::Assign) {
5872            if keyword == "our" && decls.len() == 1 {
5873                while matches!(self.peek(), Token::Ident(ref i) if i == "our") {
5874                    self.advance();
5875                    decls.push(self.parse_var_decl(allow_type_annotation)?);
5876                    if !self.eat(&Token::Assign) {
5877                        return Err(self.syntax_err(
5878                            "expected `=` after `our` in chained our-declaration",
5879                            self.peek_line(),
5880                        ));
5881                    }
5882                }
5883            }
5884            let rhs_start_pos = self.pos;
5885            let mut val = self.parse_expression()?;
5886            let rhs_end_pos = self.pos;
5887            // Stryke implicit-coderef sugar: when the RHS contains a
5888            // *free* bare topic-slot reference (`_`, `_0`, `_1`, `_<`,
5889            // `_<<`, etc. — no `$` sigil), auto-wrap the RHS in
5890            // `fn { ... }`. Forces consistent coderef semantics across
5891            // every topic-using form:
5892            //   `my $sq  = _ * _`                     → CODE ref
5893            //   `my $up  = uc _`                      → CODE ref
5894            //   `my $rev = ~> _ >{...} rev join("")`  → CODE ref
5895            // To compute eagerly using the current topic, use the
5896            // explicit `$_` / `$_N` / `$_<` sigil-prefixed forms —
5897            // those keep Perl semantics and never auto-wrap.
5898            //
5899            // "Free" = at brace-depth 0 within the RHS token stream.
5900            // Any `_` inside `{ ... }` (closure body, hash literal,
5901            // map/grep/sort/match block) is bound to whatever defines
5902            // that block and doesn't trigger the wrap, so
5903            //   `my $r = call(fn { _ < 4 })`   — `_` is inner-fn's
5904            //   `my $h = { k => _ }`           — `_` is hash value at depth 1
5905            //   `my $kind = match { _ => x }`  — `_` is wildcard pattern
5906            // all stay eager.
5907            if !crate::compat_mode()
5908                && self.block_depth == 0
5909                && decls.len() == 1
5910                && matches!(decls[0].sigil, Sigil::Scalar)
5911                && !matches!(
5912                    val.kind,
5913                    ExprKind::CodeRef { .. }
5914                        | ExprKind::SubroutineRef(_)
5915                        | ExprKind::SubroutineCodeRef(_)
5916                        | ExprKind::DynamicSubCodeRef(_)
5917                )
5918                && self.rhs_has_free_bare_topic_slot(rhs_start_pos, rhs_end_pos)
5919            {
5920                let val_line = val.line;
5921                val = Expr {
5922                    kind: ExprKind::CodeRef {
5923                        params: Vec::new(),
5924                        body: vec![Statement {
5925                            label: None,
5926                            kind: StmtKind::Expression(val),
5927                            line: val_line,
5928                        }],
5929                    },
5930                    line: val_line,
5931                };
5932            }
5933            // Validate assignment for single variable declarations (not destructuring)
5934            // `my ($a, $b) = (1, 2)` is destructuring, not scalar-from-list
5935            if !crate::compat_mode() && decls.len() == 1 {
5936                let decl = &decls[0];
5937                let target_kind = match decl.sigil {
5938                    Sigil::Scalar => ExprKind::ScalarVar(decl.name.clone()),
5939                    Sigil::Array => ExprKind::ArrayVar(decl.name.clone()),
5940                    Sigil::Hash => ExprKind::HashVar(decl.name.clone()),
5941                    Sigil::Typeglob => {
5942                        // Skip validation for typeglob
5943                        if decls.len() == 1 {
5944                            decls[0].initializer = Some(val);
5945                        } else {
5946                            for d in &mut decls {
5947                                d.initializer = Some(val.clone());
5948                            }
5949                        }
5950                        return Ok(Statement {
5951                            label: None,
5952                            kind: match keyword {
5953                                "my" => StmtKind::My(decls),
5954                                "mysync" => StmtKind::MySync(decls),
5955                                "our" => StmtKind::Our(decls),
5956                                "oursync" => StmtKind::OurSync(decls),
5957                                "local" => StmtKind::Local(decls),
5958                                "state" => StmtKind::State(decls),
5959                                _ => unreachable!(),
5960                            },
5961                            line,
5962                        });
5963                    }
5964                };
5965                let target = Expr {
5966                    kind: target_kind,
5967                    line,
5968                };
5969                self.validate_assignment(&target, &val, line)?;
5970            }
5971            if decls.len() == 1 {
5972                decls[0].initializer = Some(val);
5973            } else {
5974                for decl in &mut decls {
5975                    decl.initializer = Some(val.clone());
5976                }
5977            }
5978        } else if decls.len() == 1 {
5979            // `our $Verbose ||= 0` (Exporter.pm) — compound assign on a single decl
5980            let op = match self.peek().clone() {
5981                Token::OrAssign => Some(BinOp::LogOr),
5982                Token::DefinedOrAssign => Some(BinOp::DefinedOr),
5983                Token::AndAssign => Some(BinOp::LogAnd),
5984                _ => None,
5985            };
5986            if let Some(op) = op {
5987                let d = &decls[0];
5988                if matches!(d.sigil, Sigil::Typeglob) {
5989                    return Err(self.syntax_err(
5990                        "compound assignment on typeglob declaration is not supported",
5991                        self.peek_line(),
5992                    ));
5993                }
5994                self.advance();
5995                let rhs = self.parse_assign_expr()?;
5996                let target = Expr {
5997                    kind: match d.sigil {
5998                        Sigil::Scalar => ExprKind::ScalarVar(d.name.clone()),
5999                        Sigil::Array => ExprKind::ArrayVar(d.name.clone()),
6000                        Sigil::Hash => ExprKind::HashVar(d.name.clone()),
6001                        Sigil::Typeglob => unreachable!(),
6002                    },
6003                    line,
6004                };
6005                decls[0].initializer = Some(Expr {
6006                    kind: ExprKind::CompoundAssign {
6007                        target: Box::new(target),
6008                        op,
6009                        value: Box::new(rhs),
6010                    },
6011                    line,
6012                });
6013            }
6014        }
6015
6016        let kind = match keyword {
6017            "my" => StmtKind::My(decls),
6018            "mysync" => StmtKind::MySync(decls),
6019            "our" => StmtKind::Our(decls),
6020            "oursync" => StmtKind::OurSync(decls),
6021            "local" => StmtKind::Local(decls),
6022            "state" => StmtKind::State(decls),
6023            _ => unreachable!(),
6024        };
6025        let stmt = Statement {
6026            label: None,
6027            kind,
6028            line,
6029        };
6030        // `my $x = 1 if $y;` — statement modifier applies to the whole declaration (Perl).
6031        self.parse_stmt_postfix_modifier(stmt)
6032    }
6033
6034    fn parse_var_decl(&mut self, allow_type_annotation: bool) -> StrykeResult<VarDecl> {
6035        let mut decl = match self.advance() {
6036            (Token::ScalarVar(name), _) => VarDecl {
6037                sigil: Sigil::Scalar,
6038                name,
6039                initializer: None,
6040                frozen: false,
6041                type_annotation: None,
6042                list_context: false,
6043            },
6044            (Token::ArrayVar(name), _) => VarDecl {
6045                sigil: Sigil::Array,
6046                name,
6047                initializer: None,
6048                frozen: false,
6049                type_annotation: None,
6050                list_context: false,
6051            },
6052            (Token::HashVar(name), line) => {
6053                if !crate::compat_mode() {
6054                    self.check_hash_shadows_reserved(&name, line)?;
6055                }
6056                VarDecl {
6057                    sigil: Sigil::Hash,
6058                    name,
6059                    initializer: None,
6060                    frozen: false,
6061                    type_annotation: None,
6062                    list_context: false,
6063                }
6064            }
6065            (Token::Star, _line) => {
6066                let name = match self.advance() {
6067                    (Token::Ident(n), _) => n,
6068                    (tok, l) => {
6069                        return Err(self
6070                            .syntax_err(format!("Expected identifier after *, got {:?}", tok), l));
6071                    }
6072                };
6073                VarDecl {
6074                    sigil: Sigil::Typeglob,
6075                    name,
6076                    initializer: None,
6077                    frozen: false,
6078                    type_annotation: None,
6079                    list_context: false,
6080                }
6081            }
6082            // `my ($a, undef, $c) = (1, 2, 3)` — Perl idiom for discarding a
6083            // slot in a list assignment. The interpreter treats `undef`-named
6084            // scalar decls as throwaway: declared into a unique sink so the
6085            // distribute-to-decls loop advances past the slot.
6086            (Token::Ident(ref kw), _) if kw == "undef" => VarDecl {
6087                sigil: Sigil::Scalar,
6088                // Synthesize a name that user code cannot reference. Each
6089                // sink slot in a list-assign gets its own unique name so the
6090                // declarations don't collide.
6091                name: format!("__undef_sink_{}", self.pos),
6092                initializer: None,
6093                frozen: false,
6094                type_annotation: None,
6095                list_context: false,
6096            },
6097            (tok, line) => {
6098                return Err(self.syntax_err(
6099                    format!("Expected variable in declaration, got {:?}", tok),
6100                    line,
6101                ));
6102            }
6103        };
6104        if allow_type_annotation && self.eat(&Token::Colon) {
6105            let ty = self.parse_type_name()?;
6106            if decl.sigil != Sigil::Scalar {
6107                return Err(self.syntax_err(
6108                    "`: Type` is only valid for scalar declarations (typed my $name : Int)",
6109                    self.peek_line(),
6110                ));
6111            }
6112            decl.type_annotation = Some(ty);
6113        }
6114        Ok(decl)
6115    }
6116
6117    fn parse_type_name(&mut self) -> StrykeResult<PerlTypeName> {
6118        match self.advance() {
6119            (Token::Ident(name), _) => match name.as_str() {
6120                "Int" => Ok(PerlTypeName::Int),
6121                "Str" => Ok(PerlTypeName::Str),
6122                "Float" => Ok(PerlTypeName::Float),
6123                "Bool" => Ok(PerlTypeName::Bool),
6124                "Array" => Ok(PerlTypeName::Array),
6125                "Hash" => Ok(PerlTypeName::Hash),
6126                "Ref" => Ok(PerlTypeName::Ref),
6127                "Any" => Ok(PerlTypeName::Any),
6128                _ => Ok(PerlTypeName::Struct(name)),
6129            },
6130            (tok, err_line) => Err(self.syntax_err(
6131                format!("Expected type name after `:`, got {:?}", tok),
6132                err_line,
6133            )),
6134        }
6135    }
6136
6137    fn parse_package(&mut self) -> StrykeResult<Statement> {
6138        let line = self.peek_line();
6139        self.advance(); // 'package'
6140        let name = match self.advance() {
6141            (Token::Ident(n), _) => n,
6142            (tok, line) => {
6143                return Err(self.syntax_err(format!("Expected package name, got {:?}", tok), line))
6144            }
6145        };
6146        // Handle Foo::Bar
6147        let mut full_name = name;
6148        while self.eat(&Token::PackageSep) {
6149            if let (Token::Ident(part), _) = self.advance() {
6150                full_name = format!("{}::{}", full_name, part);
6151            }
6152        }
6153        self.eat(&Token::Semicolon);
6154        // Track the active package so subsequent `fn name(...)` decls can be
6155        // recognised as `Pkg::name` for shadow-of-builtin checks.
6156        self.current_package = full_name.clone();
6157        Ok(Statement {
6158            label: None,
6159            kind: StmtKind::Package { name: full_name },
6160            line,
6161        })
6162    }
6163
6164    fn parse_use(&mut self) -> StrykeResult<Statement> {
6165        let line = self.peek_line();
6166        self.advance(); // 'use'
6167        let (tok, tok_line) = self.advance();
6168        match tok {
6169            Token::Float(v) => {
6170                self.eat(&Token::Semicolon);
6171                Ok(Statement {
6172                    label: None,
6173                    kind: StmtKind::UsePerlVersion { version: v },
6174                    line,
6175                })
6176            }
6177            Token::Integer(n) => {
6178                if matches!(self.peek(), Token::Semicolon | Token::Eof) {
6179                    self.eat(&Token::Semicolon);
6180                    Ok(Statement {
6181                        label: None,
6182                        kind: StmtKind::UsePerlVersion { version: n as f64 },
6183                        line,
6184                    })
6185                } else {
6186                    Err(self.syntax_err(
6187                        format!("Expected ';' after use VERSION (got {:?})", self.peek()),
6188                        line,
6189                    ))
6190                }
6191            }
6192            Token::Ident(n) => {
6193                let mut full_name = n;
6194                while self.eat(&Token::PackageSep) {
6195                    if let (Token::Ident(part), _) = self.advance() {
6196                        full_name = format!("{}::{}", full_name, part);
6197                    }
6198                }
6199                if full_name == "overload" {
6200                    let mut pairs = Vec::new();
6201                    let mut parse_overload_pairs = |this: &mut Self| -> StrykeResult<()> {
6202                        loop {
6203                            if matches!(this.peek(), Token::RParen | Token::Semicolon | Token::Eof)
6204                            {
6205                                break;
6206                            }
6207                            let key_e = this.parse_assign_expr()?;
6208                            this.expect(&Token::FatArrow)?;
6209                            let val_e = this.parse_assign_expr()?;
6210                            let key = this.expr_to_overload_key(&key_e)?;
6211                            let val = this.expr_to_overload_sub(&val_e)?;
6212                            pairs.push((key, val));
6213                            if !this.eat(&Token::Comma) {
6214                                break;
6215                            }
6216                        }
6217                        Ok(())
6218                    };
6219                    if self.eat(&Token::LParen) {
6220                        // `use overload ();` — common in JSON::PP and other modules.
6221                        parse_overload_pairs(self)?;
6222                        self.expect(&Token::RParen)?;
6223                    } else if !matches!(self.peek(), Token::Semicolon | Token::Eof) {
6224                        parse_overload_pairs(self)?;
6225                    }
6226                    self.eat(&Token::Semicolon);
6227                    return Ok(Statement {
6228                        label: None,
6229                        kind: StmtKind::UseOverload { pairs },
6230                        line,
6231                    });
6232                }
6233                let mut imports = Vec::new();
6234                // Imports must start on the SAME LINE as `use Module`.
6235                // Without this, a bare `use K8s` followed by `p "…"`
6236                // on the next line silently swallowed the `p` call as
6237                // an import expression — failing later with the
6238                // confusing "pragma import must be a compile-time
6239                // string" error pointing at the next-line statement.
6240                // The legitimate multi-line form uses `,` to continue.
6241                let on_same_line = self.peek_line() == tok_line;
6242                if on_same_line
6243                    && !matches!(self.peek(), Token::Semicolon | Token::Eof)
6244                    && !self.next_is_new_statement_start(tok_line)
6245                {
6246                    loop {
6247                        if matches!(self.peek(), Token::Semicolon | Token::Eof) {
6248                            break;
6249                        }
6250                        imports.push(self.parse_expression()?);
6251                        if !self.eat(&Token::Comma) {
6252                            break;
6253                        }
6254                    }
6255                }
6256                self.eat(&Token::Semicolon);
6257                Ok(Statement {
6258                    label: None,
6259                    kind: StmtKind::Use {
6260                        module: full_name,
6261                        imports,
6262                    },
6263                    line,
6264                })
6265            }
6266            other => Err(self.syntax_err(
6267                format!("Expected module name or version after use, got {:?}", other),
6268                tok_line,
6269            )),
6270        }
6271    }
6272
6273    fn parse_no(&mut self) -> StrykeResult<Statement> {
6274        let line = self.peek_line();
6275        self.advance(); // 'no'
6276        let module = match self.advance() {
6277            (Token::Ident(n), tok_line) => (n, tok_line),
6278            (tok, line) => {
6279                return Err(self.syntax_err(
6280                    format!("Expected module name after no, got {:?}", tok),
6281                    line,
6282                ))
6283            }
6284        };
6285        let (module_name, tok_line) = module;
6286        let mut full_name = module_name;
6287        while self.eat(&Token::PackageSep) {
6288            if let (Token::Ident(part), _) = self.advance() {
6289                full_name = format!("{}::{}", full_name, part);
6290            }
6291        }
6292        let mut imports = Vec::new();
6293        if !matches!(self.peek(), Token::Semicolon | Token::Eof)
6294            && !self.next_is_new_statement_start(tok_line)
6295        {
6296            loop {
6297                if matches!(self.peek(), Token::Semicolon | Token::Eof) {
6298                    break;
6299                }
6300                imports.push(self.parse_expression()?);
6301                if !self.eat(&Token::Comma) {
6302                    break;
6303                }
6304            }
6305        }
6306        self.eat(&Token::Semicolon);
6307        Ok(Statement {
6308            label: None,
6309            kind: StmtKind::No {
6310                module: full_name,
6311                imports,
6312            },
6313            line,
6314        })
6315    }
6316
6317    fn parse_return(&mut self) -> StrykeResult<Statement> {
6318        let line = self.peek_line();
6319        self.advance(); // 'return'
6320                        // No-value return: terminator tokens AND any postfix statement-modifier
6321                        // keyword (`if`/`unless`/`while`/`until`/`for`/`foreach`). Without this
6322                        // the postfix-modifier check below never fires for valueless returns —
6323                        // `parse_assign_expr` would see `if` and look it up as a sub call,
6324                        // producing the misleading "Undefined subroutine &if" error.
6325        let val = if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof)
6326            || self.peek_is_postfix_stmt_modifier_keyword()
6327        {
6328            None
6329        } else {
6330            // Parse the operand as a comma-list — Perl's `return` is a
6331            // list-operator, so `return 1, 2, 3` returns the list (1, 2, 3).
6332            // (BUG-010) Stay below pipe-forward and stop at postfix
6333            // statement-modifier keywords like `if` / `unless`.
6334            let first = self.parse_assign_expr()?;
6335            if matches!(self.peek(), Token::Comma | Token::FatArrow) {
6336                let mut items = vec![first];
6337                while self.eat(&Token::Comma) || self.eat(&Token::FatArrow) {
6338                    if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof)
6339                        || self.peek_is_postfix_stmt_modifier_keyword()
6340                    {
6341                        break;
6342                    }
6343                    items.push(self.parse_assign_expr()?);
6344                }
6345                let line = items.first().map(|e| e.line).unwrap_or(line);
6346                Some(Expr {
6347                    kind: ExprKind::List(items),
6348                    line,
6349                })
6350            } else {
6351                Some(first)
6352            }
6353        };
6354        // Check for postfix modifiers on return
6355        let stmt = Statement {
6356            label: None,
6357            kind: StmtKind::Return(val),
6358            line,
6359        };
6360        if let Token::Ident(ref kw) = self.peek().clone() {
6361            match kw.as_str() {
6362                "if" => {
6363                    self.advance();
6364                    let cond = self.parse_expression()?;
6365                    self.eat(&Token::Semicolon);
6366                    return Ok(Statement {
6367                        label: None,
6368                        kind: StmtKind::If {
6369                            condition: cond,
6370                            body: vec![stmt],
6371                            elsifs: vec![],
6372                            else_block: None,
6373                        },
6374                        line,
6375                    });
6376                }
6377                "unless" => {
6378                    self.advance();
6379                    let cond = self.parse_expression()?;
6380                    self.eat(&Token::Semicolon);
6381                    return Ok(Statement {
6382                        label: None,
6383                        kind: StmtKind::Unless {
6384                            condition: cond,
6385                            body: vec![stmt],
6386                            else_block: None,
6387                        },
6388                        line,
6389                    });
6390                }
6391                _ => {}
6392            }
6393        }
6394        self.eat(&Token::Semicolon);
6395        Ok(stmt)
6396    }
6397
6398    // ── Expressions (Pratt / precedence climbing) ──
6399
6400    fn parse_expression(&mut self) -> StrykeResult<Expr> {
6401        self.parse_comma_expr()
6402    }
6403
6404    fn parse_comma_expr(&mut self) -> StrykeResult<Expr> {
6405        // Word-op precedence (or/and/not) sits ABOVE assignment in Perl —
6406        // `EXPR or $err = $@` parses as `EXPR or ($err = $@)`, NOT
6407        // `(EXPR or $err) = $@`. Entering through `parse_or_word` here
6408        // (instead of `parse_assign_expr` directly) gives `or`/`and`/`not`
6409        // looser binding than `=`, matching `perlop`. The deeper chain
6410        // (`parse_not_word → parse_assign_expr → parse_ternary → … →
6411        // parse_log_or → …`) handles tighter operators normally.
6412        let expr = self.parse_or_word()?;
6413        let mut exprs = vec![expr];
6414        while self.eat(&Token::Comma) || self.eat(&Token::FatArrow) {
6415            if matches!(
6416                self.peek(),
6417                Token::RParen | Token::RBracket | Token::RBrace | Token::Semicolon | Token::Eof
6418            ) {
6419                break; // trailing comma
6420            }
6421            exprs.push(self.parse_or_word()?);
6422        }
6423        if exprs.len() == 1 {
6424            return Ok(exprs.pop().unwrap());
6425        }
6426        let line = exprs[0].line;
6427        Ok(Expr {
6428            kind: ExprKind::List(exprs),
6429            line,
6430        })
6431    }
6432
6433    fn parse_assign_expr(&mut self) -> StrykeResult<Expr> {
6434        let expr = self.parse_ternary()?;
6435        let line = expr.line;
6436
6437        match self.peek().clone() {
6438            Token::Assign => {
6439                self.advance();
6440                let right = self.parse_assign_expr()?;
6441                // Desugar `$obj->field = value` into `$obj->field(value)` (setter call)
6442                if let ExprKind::MethodCall { ref args, .. } = expr.kind {
6443                    if args.is_empty() {
6444                        // Destructure again to take ownership
6445                        let ExprKind::MethodCall {
6446                            object,
6447                            method,
6448                            super_call,
6449                            ..
6450                        } = expr.kind
6451                        else {
6452                            unreachable!()
6453                        };
6454                        return Ok(Expr {
6455                            kind: ExprKind::MethodCall {
6456                                object,
6457                                method,
6458                                args: vec![right],
6459                                super_call,
6460                            },
6461                            line,
6462                        });
6463                    }
6464                }
6465                self.validate_assignment(&expr, &right, line)?;
6466                Ok(Expr {
6467                    kind: ExprKind::Assign {
6468                        target: Box::new(expr),
6469                        value: Box::new(right),
6470                    },
6471                    line,
6472                })
6473            }
6474            Token::PlusAssign => {
6475                self.advance();
6476                let r = self.parse_assign_expr()?;
6477                Ok(Expr {
6478                    kind: ExprKind::CompoundAssign {
6479                        target: Box::new(expr),
6480                        op: BinOp::Add,
6481                        value: Box::new(r),
6482                    },
6483                    line,
6484                })
6485            }
6486            Token::MinusAssign => {
6487                self.advance();
6488                let r = self.parse_assign_expr()?;
6489                Ok(Expr {
6490                    kind: ExprKind::CompoundAssign {
6491                        target: Box::new(expr),
6492                        op: BinOp::Sub,
6493                        value: Box::new(r),
6494                    },
6495                    line,
6496                })
6497            }
6498            Token::MulAssign => {
6499                self.advance();
6500                let r = self.parse_assign_expr()?;
6501                Ok(Expr {
6502                    kind: ExprKind::CompoundAssign {
6503                        target: Box::new(expr),
6504                        op: BinOp::Mul,
6505                        value: Box::new(r),
6506                    },
6507                    line,
6508                })
6509            }
6510            Token::DivAssign => {
6511                self.advance();
6512                let r = self.parse_assign_expr()?;
6513                Ok(Expr {
6514                    kind: ExprKind::CompoundAssign {
6515                        target: Box::new(expr),
6516                        op: BinOp::Div,
6517                        value: Box::new(r),
6518                    },
6519                    line,
6520                })
6521            }
6522            Token::ModAssign => {
6523                self.advance();
6524                let r = self.parse_assign_expr()?;
6525                Ok(Expr {
6526                    kind: ExprKind::CompoundAssign {
6527                        target: Box::new(expr),
6528                        op: BinOp::Mod,
6529                        value: Box::new(r),
6530                    },
6531                    line,
6532                })
6533            }
6534            Token::PowAssign => {
6535                self.advance();
6536                let r = self.parse_assign_expr()?;
6537                Ok(Expr {
6538                    kind: ExprKind::CompoundAssign {
6539                        target: Box::new(expr),
6540                        op: BinOp::Pow,
6541                        value: Box::new(r),
6542                    },
6543                    line,
6544                })
6545            }
6546            Token::XAssign => {
6547                // `$s x= N` has no matching `BinOp::Repeat`; desugar to
6548                // `$s = $s x N` so we can reuse the existing `ExprKind::Repeat`
6549                // evaluator (scalar-repeat path; list-repeat fires only when
6550                // the LHS is a syntactic list literal).
6551                self.advance();
6552                let r = self.parse_assign_expr()?;
6553                let lhs_for_repeat = expr.clone();
6554                Ok(Expr {
6555                    kind: ExprKind::Assign {
6556                        target: Box::new(expr),
6557                        value: Box::new(Expr {
6558                            kind: ExprKind::Repeat {
6559                                expr: Box::new(lhs_for_repeat),
6560                                count: Box::new(r),
6561                                list_repeat: false,
6562                            },
6563                            line,
6564                        }),
6565                    },
6566                    line,
6567                })
6568            }
6569            Token::DotAssign => {
6570                self.advance();
6571                let r = self.parse_assign_expr()?;
6572                Ok(Expr {
6573                    kind: ExprKind::CompoundAssign {
6574                        target: Box::new(expr),
6575                        op: BinOp::Concat,
6576                        value: Box::new(r),
6577                    },
6578                    line,
6579                })
6580            }
6581            Token::BitAndAssign => {
6582                self.advance();
6583                let r = self.parse_assign_expr()?;
6584                Ok(Expr {
6585                    kind: ExprKind::CompoundAssign {
6586                        target: Box::new(expr),
6587                        op: BinOp::BitAnd,
6588                        value: Box::new(r),
6589                    },
6590                    line,
6591                })
6592            }
6593            Token::BitOrAssign => {
6594                self.advance();
6595                let r = self.parse_assign_expr()?;
6596                Ok(Expr {
6597                    kind: ExprKind::CompoundAssign {
6598                        target: Box::new(expr),
6599                        op: BinOp::BitOr,
6600                        value: Box::new(r),
6601                    },
6602                    line,
6603                })
6604            }
6605            Token::XorAssign => {
6606                self.advance();
6607                let r = self.parse_assign_expr()?;
6608                Ok(Expr {
6609                    kind: ExprKind::CompoundAssign {
6610                        target: Box::new(expr),
6611                        op: BinOp::BitXor,
6612                        value: Box::new(r),
6613                    },
6614                    line,
6615                })
6616            }
6617            Token::ShiftLeftAssign => {
6618                self.advance();
6619                let r = self.parse_assign_expr()?;
6620                Ok(Expr {
6621                    kind: ExprKind::CompoundAssign {
6622                        target: Box::new(expr),
6623                        op: BinOp::ShiftLeft,
6624                        value: Box::new(r),
6625                    },
6626                    line,
6627                })
6628            }
6629            Token::ShiftRightAssign => {
6630                self.advance();
6631                let r = self.parse_assign_expr()?;
6632                Ok(Expr {
6633                    kind: ExprKind::CompoundAssign {
6634                        target: Box::new(expr),
6635                        op: BinOp::ShiftRight,
6636                        value: Box::new(r),
6637                    },
6638                    line,
6639                })
6640            }
6641            Token::OrAssign => {
6642                self.advance();
6643                let r = self.parse_assign_expr()?;
6644                Ok(Expr {
6645                    kind: ExprKind::CompoundAssign {
6646                        target: Box::new(expr),
6647                        op: BinOp::LogOr,
6648                        value: Box::new(r),
6649                    },
6650                    line,
6651                })
6652            }
6653            Token::DefinedOrAssign => {
6654                self.advance();
6655                let r = self.parse_assign_expr()?;
6656                Ok(Expr {
6657                    kind: ExprKind::CompoundAssign {
6658                        target: Box::new(expr),
6659                        op: BinOp::DefinedOr,
6660                        value: Box::new(r),
6661                    },
6662                    line,
6663                })
6664            }
6665            Token::AndAssign => {
6666                self.advance();
6667                let r = self.parse_assign_expr()?;
6668                Ok(Expr {
6669                    kind: ExprKind::CompoundAssign {
6670                        target: Box::new(expr),
6671                        op: BinOp::LogAnd,
6672                        value: Box::new(r),
6673                    },
6674                    line,
6675                })
6676            }
6677            _ => Ok(expr),
6678        }
6679    }
6680
6681    fn parse_ternary(&mut self) -> StrykeResult<Expr> {
6682        let expr = self.parse_pipe_forward()?;
6683        if self.eat(&Token::Question) {
6684            let line = expr.line;
6685            self.suppress_colon_range = self.suppress_colon_range.saturating_add(1);
6686            let then_expr = self.parse_assign_expr();
6687            self.suppress_colon_range = self.suppress_colon_range.saturating_sub(1);
6688            let then_expr = then_expr?;
6689            self.expect(&Token::Colon)?;
6690            let else_expr = self.parse_assign_expr()?;
6691            return Ok(Expr {
6692                kind: ExprKind::Ternary {
6693                    condition: Box::new(expr),
6694                    then_expr: Box::new(then_expr),
6695                    else_expr: Box::new(else_expr),
6696                },
6697                line,
6698            });
6699        }
6700        Ok(expr)
6701    }
6702
6703    /// `EXPR |> CALL` — pipe-forward (F#/Elixir). Left-associative; the LHS is threaded
6704    /// in as the **first argument** of the RHS call at parse time (pure AST rewrite,
6705    /// no runtime cost). `x |> f(a, b)` → `f(x, a, b)`; `x |> f` → `f(x)`; chain
6706    /// `x |> f |> g(2)` → `g(f(x), 2)`. Precedence sits between `?:` and `||`, so
6707    /// `x + 1 |> f || y` parses as `f(x + 1) || y`.
6708    fn parse_pipe_forward(&mut self) -> StrykeResult<Expr> {
6709        // After moving word-ops (or/and/not) above the assignment level,
6710        // pipe_forward must descend into `parse_range` (which itself
6711        // descends into `parse_log_or`) — calling `parse_or_word` here
6712        // would re-introduce `or` at a wrong place in the precedence chain
6713        // (it now sits above `parse_comma_expr`). We skip past `parse_range`
6714        // rather than `parse_log_or` so `..` stays reachable.
6715        let mut left = self.parse_range()?;
6716        // Inside a paren-less arg list, `|>` is a hard terminator for the
6717        // enclosing call — leave it for the outer `parse_pipe_forward` loop
6718        // so `qw(…) |> head 2 |> join "-"` chains left-to-right as
6719        // `(qw(…) |> head 2) |> join "-"` instead of `head` swallowing the
6720        // outer `|>` via its first-arg `parse_assign_expr`.
6721        if self.no_pipe_forward_depth > 0 {
6722            return Ok(left);
6723        }
6724        while matches!(self.peek(), Token::PipeForward) {
6725            if crate::compat_mode() {
6726                return Err(self.syntax_err(
6727                    "pipe-forward operator `|>` is a stryke extension (disabled by --compat)",
6728                    left.line,
6729                ));
6730            }
6731            let line = left.line;
6732            self.advance();
6733            // Set pipe-RHS context so list-taking builtins (`map`, `grep`,
6734            // `join`, …) accept a placeholder in place of their list operand.
6735            self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_add(1);
6736            // RHS of `|>` parses at the same precedence as the LHS — see
6737            // the comment at the top of `parse_pipe_forward` for why this
6738            // descends into `parse_range` instead of `parse_or_word`.
6739            let right_result = self.parse_range();
6740            self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_sub(1);
6741            let right = right_result?;
6742            left = self.pipe_forward_apply(left, right, line)?;
6743        }
6744        Ok(left)
6745    }
6746
6747    /// Desugar `lhs |> rhs`: thread `lhs` into the call that `rhs` represents as
6748    /// its **first** argument (Elixir / R / proposed-JS convention).
6749    ///
6750    /// The strategy depends on the shape of `rhs`:
6751    /// - Generic calls (`FuncCall`, `MethodCall`, `IndirectCall`) and variadic
6752    ///   builtins (`Print`, `Say`, `Printf`, `Die`, `Warn`, `Sprintf`, `System`,
6753    ///   `Exec`, `Unlink`, `Chmod`, `Chown`, `Glob`, …) — **prepend** `lhs` to
6754    ///   the args list. So `URL |> json_jq ".[]"` → `json_jq(URL, ".[]")`,
6755    ///   matching the `(data, filter)` signature the builtin expects.
6756    /// - Unary-style builtins (`Length`, `Abs`, `Lc`, `Uc`, `Defined`, `Ref`,
6757    ///   `Keys`, `Values`, `Pop`, `Shift`, …) — **replace** the sole operand with
6758    ///   `lhs` (these parse a single default `$_` when called without an arg, so
6759    ///   piping overrides that default; first-arg and last-arg are identical).
6760    /// - List-taking higher-order forms (`map`, `flat_map`, `grep`, `sort`, `join`, `reduce`, `fold`,
6761    ///   `pmap`, `pflat_map`, `pgrep`, `pfor`, …) — **replace** the `list` field with `lhs`, so
6762    ///   `@arr |> map { $_ * 2 }` becomes `map { $_ * 2 } @arr`.
6763    /// - `Bareword("f")` — lift to `FuncCall { f, [lhs] }`.
6764    /// - Scalar / deref / coderef expressions — wrap in `IndirectCall` with `lhs`
6765    ///   as the sole argument.
6766    /// - Ambiguous forms (binary ops, ternaries, literals, lists) — parse error,
6767    ///   since silently calling a non-callable at runtime would be worse.
6768    fn pipe_forward_apply(&self, lhs: Expr, rhs: Expr, line: usize) -> StrykeResult<Expr> {
6769        let Expr { kind, line: rline } = rhs;
6770        let new_kind = match kind {
6771            // ── Generic / user-defined calls ───────────────────────────────────
6772            ExprKind::FuncCall { name, mut args } => {
6773                // Stryke builtins are unprefixed; `CORE::` callers route back to the
6774                // bare-name pipe-forward dispatch below.
6775                let dispatch_name: &str = name.strip_prefix("CORE::").unwrap_or(name.as_str());
6776                match dispatch_name {
6777                    "puniq" | "uniq" | "distinct" | "flatten" | "set" | "list_count"
6778                    | "list_size" | "count" | "size" | "cnt" | "len" | "with_index" | "shuffle"
6779                    | "shuffled" | "frequencies" | "freq" | "pfrequencies" | "pfreq"
6780                    | "interleave" | "ddump" | "stringify" | "str" | "lines" | "words"
6781                    | "chars" | "digits" | "letters" | "letters_uc" | "letters_lc"
6782                    | "punctuation" | "numbers" | "graphemes" | "columns" | "sentences"
6783                    | "paragraphs" | "sections" | "trim" | "avg" | "to_json" | "to_csv"
6784                    | "to_toml" | "to_yaml" | "to_xml" | "to_html" | "from_json" | "from_csv"
6785                    | "from_toml" | "from_yaml" | "from_xml" | "to_markdown" | "to_table"
6786                    | "xopen" | "clip" | "sparkline" | "bar_chart" | "flame" | "stddev"
6787                    | "squared" | "sq" | "square" | "cubed" | "cb" | "cube" | "normalize"
6788                    | "snake_case" | "camel_case" | "kebab_case" => {
6789                        if args.is_empty() {
6790                            args.push(lhs);
6791                        } else {
6792                            args[0] = lhs;
6793                        }
6794                    }
6795                    "chunked" | "windowed" => {
6796                        if args.is_empty() {
6797                            return Err(self.syntax_err(
6798                                "|>: chunked(N) / windowed(N) needs size — e.g. `@a |> windowed(2)`",
6799                                line,
6800                            ));
6801                        }
6802                        args.insert(0, lhs);
6803                    }
6804                    "reduce" | "fold" => {
6805                        args.push(lhs);
6806                    }
6807                    "grep_v" | "pluck" | "tee" | "nth" | "chunk" => {
6808                        // data |> grep_v "pattern" → grep_v("pattern", data...)
6809                        // data |> pluck "key" → pluck("key", data...)
6810                        // data |> tee "file" → tee("file", data...)
6811                        // data |> nth N → nth(N, data...)
6812                        // data |> chunk N → chunk(N, data...)
6813                        args.push(lhs);
6814                    }
6815                    "enumerate" | "dedup" => {
6816                        // data |> enumerate → enumerate(data)
6817                        // data |> dedup → dedup(data)
6818                        args.insert(0, lhs);
6819                    }
6820                    "clamp" => {
6821                        // data |> clamp MIN, MAX → clamp(MIN, MAX, data...)
6822                        args.push(lhs);
6823                    }
6824                    n if Self::is_block_then_list_pipe_builtin(n) => {
6825                        if args.len() < 2 {
6826                            return Err(self.syntax_err(
6827                                format!(
6828                                    "|>: `{name}` needs {{ BLOCK }}, LIST so the list can receive the pipe"
6829                                ),
6830                                line,
6831                            ));
6832                        }
6833                        args[1] = lhs;
6834                    }
6835                    "take" | "head" | "tail" | "drop" => {
6836                        if args.is_empty() {
6837                            return Err(self.syntax_err(
6838                                "|>: `{name}` needs N last — e.g. `@a |> take(3)` for `take(@a, 3)`",
6839                                line,
6840                            ));
6841                        }
6842                        // `LIST |> take N` → `take(LIST, N)` (prepend piped list before trailing count)
6843                        args.insert(0, lhs);
6844                    }
6845                    _ => {
6846                        if self.thread_last_mode {
6847                            args.push(lhs);
6848                        } else {
6849                            args.insert(0, lhs);
6850                        }
6851                    }
6852                }
6853                ExprKind::FuncCall { name, args }
6854            }
6855            ExprKind::MethodCall {
6856                object,
6857                method,
6858                mut args,
6859                super_call,
6860            } => {
6861                if self.thread_last_mode {
6862                    args.push(lhs);
6863                } else {
6864                    args.insert(0, lhs);
6865                }
6866                ExprKind::MethodCall {
6867                    object,
6868                    method,
6869                    args,
6870                    super_call,
6871                }
6872            }
6873            ExprKind::IndirectCall {
6874                target,
6875                mut args,
6876                ampersand,
6877                pass_caller_arglist: _,
6878            } => {
6879                if self.thread_last_mode {
6880                    args.push(lhs);
6881                } else {
6882                    args.insert(0, lhs);
6883                }
6884                ExprKind::IndirectCall {
6885                    target,
6886                    args,
6887                    ampersand,
6888                    // Prepending an explicit first arg means this is no longer
6889                    // "pass the caller's @_" — that form is only bare `&$cr`.
6890                    pass_caller_arglist: false,
6891                }
6892            }
6893
6894            // ── Print-like / diagnostic ops (variadic) ─────────────────────────
6895            ExprKind::Print { handle, mut args } => {
6896                if self.thread_last_mode {
6897                    args.push(lhs);
6898                } else {
6899                    args.insert(0, lhs);
6900                }
6901                ExprKind::Print { handle, args }
6902            }
6903            ExprKind::Say { handle, mut args } => {
6904                if self.thread_last_mode {
6905                    args.push(lhs);
6906                } else {
6907                    args.insert(0, lhs);
6908                }
6909                ExprKind::Say { handle, args }
6910            }
6911            ExprKind::Printf { handle, mut args } => {
6912                if self.thread_last_mode {
6913                    args.push(lhs);
6914                } else {
6915                    args.insert(0, lhs);
6916                }
6917                ExprKind::Printf { handle, args }
6918            }
6919            ExprKind::Die(mut args) => {
6920                if self.thread_last_mode {
6921                    args.push(lhs);
6922                } else {
6923                    args.insert(0, lhs);
6924                }
6925                ExprKind::Die(args)
6926            }
6927            ExprKind::Warn(mut args) => {
6928                if self.thread_last_mode {
6929                    args.push(lhs);
6930                } else {
6931                    args.insert(0, lhs);
6932                }
6933                ExprKind::Warn(args)
6934            }
6935
6936            // ── Sprintf: first-arg pipe threads lhs into the `format` slot ─────
6937            //   `"n=%d" |> sprintf(42)` → `sprintf("n=%d", 42)` is awkward,
6938            //   but piping the format string is the rarer case. Prepending
6939            //   to the values list gives `sprintf(format, lhs, ...args)` for
6940            //   the common `$n |> sprintf "count=%d"` case.
6941            ExprKind::Sprintf { format, mut args } => {
6942                if self.thread_last_mode {
6943                    args.push(lhs);
6944                } else {
6945                    args.insert(0, lhs);
6946                }
6947                ExprKind::Sprintf { format, args }
6948            }
6949
6950            // ── System / exec / globbing / filesystem variadics ────────────────
6951            ExprKind::System(mut args) => {
6952                if self.thread_last_mode {
6953                    args.push(lhs);
6954                } else {
6955                    args.insert(0, lhs);
6956                }
6957                ExprKind::System(args)
6958            }
6959            ExprKind::Exec(mut args) => {
6960                if self.thread_last_mode {
6961                    args.push(lhs);
6962                } else {
6963                    args.insert(0, lhs);
6964                }
6965                ExprKind::Exec(args)
6966            }
6967            ExprKind::Unlink(mut args) => {
6968                if self.thread_last_mode {
6969                    args.push(lhs);
6970                } else {
6971                    args.insert(0, lhs);
6972                }
6973                ExprKind::Unlink(args)
6974            }
6975            ExprKind::Chmod(mut args) => {
6976                if self.thread_last_mode {
6977                    args.push(lhs);
6978                } else {
6979                    args.insert(0, lhs);
6980                }
6981                ExprKind::Chmod(args)
6982            }
6983            ExprKind::Chown(mut args) => {
6984                if self.thread_last_mode {
6985                    args.push(lhs);
6986                } else {
6987                    args.insert(0, lhs);
6988                }
6989                ExprKind::Chown(args)
6990            }
6991            ExprKind::Glob(mut args) => {
6992                if self.thread_last_mode {
6993                    args.push(lhs);
6994                } else {
6995                    args.insert(0, lhs);
6996                }
6997                ExprKind::Glob(args)
6998            }
6999            ExprKind::Files(mut args) => {
7000                if self.thread_last_mode {
7001                    args.push(lhs);
7002                } else {
7003                    args.insert(0, lhs);
7004                }
7005                ExprKind::Files(args)
7006            }
7007            ExprKind::Filesf(mut args) => {
7008                if self.thread_last_mode {
7009                    args.push(lhs);
7010                } else {
7011                    args.insert(0, lhs);
7012                }
7013                ExprKind::Filesf(args)
7014            }
7015            ExprKind::FilesfRecursive(mut args) => {
7016                if self.thread_last_mode {
7017                    args.push(lhs);
7018                } else {
7019                    args.insert(0, lhs);
7020                }
7021                ExprKind::FilesfRecursive(args)
7022            }
7023            ExprKind::Dirs(mut args) => {
7024                if self.thread_last_mode {
7025                    args.push(lhs);
7026                } else {
7027                    args.insert(0, lhs);
7028                }
7029                ExprKind::Dirs(args)
7030            }
7031            ExprKind::DirsRecursive(mut args) => {
7032                if self.thread_last_mode {
7033                    args.push(lhs);
7034                } else {
7035                    args.insert(0, lhs);
7036                }
7037                ExprKind::DirsRecursive(args)
7038            }
7039            ExprKind::SymLinks(mut args) => {
7040                if self.thread_last_mode {
7041                    args.push(lhs);
7042                } else {
7043                    args.insert(0, lhs);
7044                }
7045                ExprKind::SymLinks(args)
7046            }
7047            ExprKind::Sockets(mut args) => {
7048                if self.thread_last_mode {
7049                    args.push(lhs);
7050                } else {
7051                    args.insert(0, lhs);
7052                }
7053                ExprKind::Sockets(args)
7054            }
7055            ExprKind::Pipes(mut args) => {
7056                if self.thread_last_mode {
7057                    args.push(lhs);
7058                } else {
7059                    args.insert(0, lhs);
7060                }
7061                ExprKind::Pipes(args)
7062            }
7063            ExprKind::BlockDevices(mut args) => {
7064                if self.thread_last_mode {
7065                    args.push(lhs);
7066                } else {
7067                    args.insert(0, lhs);
7068                }
7069                ExprKind::BlockDevices(args)
7070            }
7071            ExprKind::CharDevices(mut args) => {
7072                if self.thread_last_mode {
7073                    args.push(lhs);
7074                } else {
7075                    args.insert(0, lhs);
7076                }
7077                ExprKind::CharDevices(args)
7078            }
7079            ExprKind::GlobPar { mut args, progress } => {
7080                if self.thread_last_mode {
7081                    args.push(lhs);
7082                } else {
7083                    args.insert(0, lhs);
7084                }
7085                ExprKind::GlobPar { args, progress }
7086            }
7087            ExprKind::ParSed { mut args, progress } => {
7088                if self.thread_last_mode {
7089                    args.push(lhs);
7090                } else {
7091                    args.insert(0, lhs);
7092                }
7093                ExprKind::ParSed { args, progress }
7094            }
7095
7096            // ── Unary-style builtins: replace the lone operand with `lhs` ──────
7097            ExprKind::Length(_) => ExprKind::Length(Box::new(lhs)),
7098            ExprKind::Abs(_) => ExprKind::Abs(Box::new(lhs)),
7099            ExprKind::Int(_) => ExprKind::Int(Box::new(lhs)),
7100            ExprKind::Sqrt(_) => ExprKind::Sqrt(Box::new(lhs)),
7101            ExprKind::Sin(_) => ExprKind::Sin(Box::new(lhs)),
7102            ExprKind::Cos(_) => ExprKind::Cos(Box::new(lhs)),
7103            ExprKind::Exp(_) => ExprKind::Exp(Box::new(lhs)),
7104            ExprKind::Log(_) => ExprKind::Log(Box::new(lhs)),
7105            ExprKind::Hex(_) => ExprKind::Hex(Box::new(lhs)),
7106            ExprKind::Oct(_) => ExprKind::Oct(Box::new(lhs)),
7107            ExprKind::Lc(_) => ExprKind::Lc(Box::new(lhs)),
7108            ExprKind::Uc(_) => ExprKind::Uc(Box::new(lhs)),
7109            ExprKind::Lcfirst(_) => ExprKind::Lcfirst(Box::new(lhs)),
7110            ExprKind::Ucfirst(_) => ExprKind::Ucfirst(Box::new(lhs)),
7111            ExprKind::Fc(_) => ExprKind::Fc(Box::new(lhs)),
7112            ExprKind::Chr(_) => ExprKind::Chr(Box::new(lhs)),
7113            ExprKind::Ord(_) => ExprKind::Ord(Box::new(lhs)),
7114            ExprKind::Chomp(_) => ExprKind::Chomp(Box::new(lhs)),
7115            ExprKind::Chop(_) => ExprKind::Chop(Box::new(lhs)),
7116            ExprKind::Defined(_) => ExprKind::Defined(Box::new(lhs)),
7117            ExprKind::Ref(_) => ExprKind::Ref(Box::new(lhs)),
7118            ExprKind::ScalarContext(_) => ExprKind::ScalarContext(Box::new(lhs)),
7119            ExprKind::Keys(_) => ExprKind::Keys(Box::new(lhs)),
7120            ExprKind::Values(_) => ExprKind::Values(Box::new(lhs)),
7121            ExprKind::Each(_) => ExprKind::Each(Box::new(lhs)),
7122            ExprKind::Pop(_) => ExprKind::Pop(Box::new(lhs)),
7123            ExprKind::Shift(_) => ExprKind::Shift(Box::new(lhs)),
7124            ExprKind::Delete(_) => ExprKind::Delete(Box::new(lhs)),
7125            ExprKind::Exists(_) => ExprKind::Exists(Box::new(lhs)),
7126            ExprKind::ReverseExpr(_) => ExprKind::ReverseExpr(Box::new(lhs)),
7127            ExprKind::Rev(_) => ExprKind::Rev(Box::new(lhs)),
7128            ExprKind::Slurp(_) => ExprKind::Slurp(Box::new(lhs)),
7129            ExprKind::Capture(_) => ExprKind::Capture(Box::new(lhs)),
7130            ExprKind::Qx(_) => ExprKind::Qx(Box::new(lhs)),
7131            ExprKind::FetchUrl(_) => ExprKind::FetchUrl(Box::new(lhs)),
7132            ExprKind::Close(_) => ExprKind::Close(Box::new(lhs)),
7133            ExprKind::Chdir(_) => ExprKind::Chdir(Box::new(lhs)),
7134            ExprKind::Readdir(_) => ExprKind::Readdir(Box::new(lhs)),
7135            ExprKind::Closedir(_) => ExprKind::Closedir(Box::new(lhs)),
7136            ExprKind::Rewinddir(_) => ExprKind::Rewinddir(Box::new(lhs)),
7137            ExprKind::Telldir(_) => ExprKind::Telldir(Box::new(lhs)),
7138            ExprKind::Stat(_) => ExprKind::Stat(Box::new(lhs)),
7139            ExprKind::Lstat(_) => ExprKind::Lstat(Box::new(lhs)),
7140            ExprKind::Readlink(_) => ExprKind::Readlink(Box::new(lhs)),
7141            ExprKind::Study(_) => ExprKind::Study(Box::new(lhs)),
7142            ExprKind::Await(_) => ExprKind::Await(Box::new(lhs)),
7143            ExprKind::Eval(_) => ExprKind::Eval(Box::new(lhs)),
7144            ExprKind::Rand(_) => ExprKind::Rand(Some(Box::new(lhs))),
7145            ExprKind::Srand(_) => ExprKind::Srand(Some(Box::new(lhs))),
7146            ExprKind::Pos(_) => ExprKind::Pos(Some(Box::new(lhs))),
7147            ExprKind::Exit(_) => ExprKind::Exit(Some(Box::new(lhs))),
7148
7149            // ── Higher-order / list-taking forms: replace the `list` slot ──────
7150            ExprKind::MapExpr {
7151                block,
7152                list: _,
7153                flatten_array_refs,
7154                stream,
7155            } => ExprKind::MapExpr {
7156                block,
7157                list: Box::new(lhs),
7158                flatten_array_refs,
7159                stream,
7160            },
7161            ExprKind::MapExprComma {
7162                expr,
7163                list: _,
7164                flatten_array_refs,
7165                stream,
7166            } => ExprKind::MapExprComma {
7167                expr,
7168                list: Box::new(lhs),
7169                flatten_array_refs,
7170                stream,
7171            },
7172            ExprKind::GrepExpr {
7173                block,
7174                list: _,
7175                keyword,
7176            } => ExprKind::GrepExpr {
7177                block,
7178                list: Box::new(lhs),
7179                keyword,
7180            },
7181            ExprKind::GrepExprComma {
7182                expr,
7183                list: _,
7184                keyword,
7185            } => ExprKind::GrepExprComma {
7186                expr,
7187                list: Box::new(lhs),
7188                keyword,
7189            },
7190            ExprKind::ForEachExpr { block, list: _ } => ExprKind::ForEachExpr {
7191                block,
7192                list: Box::new(lhs),
7193            },
7194            ExprKind::SortExpr { cmp, list: _ } => ExprKind::SortExpr {
7195                cmp,
7196                list: Box::new(lhs),
7197            },
7198            ExprKind::JoinExpr { separator, list: _ } => ExprKind::JoinExpr {
7199                separator,
7200                list: Box::new(lhs),
7201            },
7202            ExprKind::ReduceExpr { block, list: _ } => ExprKind::ReduceExpr {
7203                block,
7204                list: Box::new(lhs),
7205            },
7206            ExprKind::PMapExpr {
7207                block,
7208                list: _,
7209                progress,
7210                flat_outputs,
7211                on_cluster,
7212                stream,
7213            } => ExprKind::PMapExpr {
7214                block,
7215                list: Box::new(lhs),
7216                progress,
7217                flat_outputs,
7218                on_cluster,
7219                stream,
7220            },
7221            ExprKind::ParExpr { block, list: _ } => ExprKind::ParExpr {
7222                block,
7223                list: Box::new(lhs),
7224            },
7225            ExprKind::ParReduceExpr {
7226                extract_block,
7227                reduce_block,
7228                list: _,
7229            } => ExprKind::ParReduceExpr {
7230                extract_block,
7231                reduce_block,
7232                list: Box::new(lhs),
7233            },
7234            ExprKind::PMapChunkedExpr {
7235                chunk_size,
7236                block,
7237                list: _,
7238                progress,
7239            } => ExprKind::PMapChunkedExpr {
7240                chunk_size,
7241                block,
7242                list: Box::new(lhs),
7243                progress,
7244            },
7245            ExprKind::PGrepExpr {
7246                block,
7247                list: _,
7248                progress,
7249                stream,
7250            } => ExprKind::PGrepExpr {
7251                block,
7252                list: Box::new(lhs),
7253                progress,
7254                stream,
7255            },
7256            ExprKind::PForExpr {
7257                block,
7258                list: _,
7259                progress,
7260            } => ExprKind::PForExpr {
7261                block,
7262                list: Box::new(lhs),
7263                progress,
7264            },
7265            ExprKind::PSortExpr {
7266                cmp,
7267                list: _,
7268                progress,
7269            } => ExprKind::PSortExpr {
7270                cmp,
7271                list: Box::new(lhs),
7272                progress,
7273            },
7274            ExprKind::PReduceExpr {
7275                block,
7276                list: _,
7277                progress,
7278            } => ExprKind::PReduceExpr {
7279                block,
7280                list: Box::new(lhs),
7281                progress,
7282            },
7283            ExprKind::PcacheExpr {
7284                block,
7285                list: _,
7286                progress,
7287            } => ExprKind::PcacheExpr {
7288                block,
7289                list: Box::new(lhs),
7290                progress,
7291            },
7292            ExprKind::PReduceInitExpr {
7293                init,
7294                block,
7295                list: _,
7296                progress,
7297            } => ExprKind::PReduceInitExpr {
7298                init,
7299                block,
7300                list: Box::new(lhs),
7301                progress,
7302            },
7303            ExprKind::PMapReduceExpr {
7304                map_block,
7305                reduce_block,
7306                list: _,
7307                progress,
7308            } => ExprKind::PMapReduceExpr {
7309                map_block,
7310                reduce_block,
7311                list: Box::new(lhs),
7312                progress,
7313            },
7314
7315            // ── Push / unshift: first arg is the array, so pipe the LHS
7316            //     into the **values** list — `"x" |> push(@arr)` → `push @arr, "x"`
7317            //     is unchanged, but `@arr |> push "x"` is unnatural; use push
7318            //     directly for that.
7319            ExprKind::Push { array, mut values } => {
7320                values.insert(0, lhs);
7321                ExprKind::Push { array, values }
7322            }
7323            ExprKind::Unshift { array, mut values } => {
7324                values.insert(0, lhs);
7325                ExprKind::Unshift { array, values }
7326            }
7327
7328            // ── Split: pipe the subject string — `$line |> split /,/` ─────────
7329            ExprKind::SplitExpr {
7330                pattern,
7331                string: _,
7332                limit,
7333            } => ExprKind::SplitExpr {
7334                pattern,
7335                string: Box::new(lhs),
7336                limit,
7337            },
7338
7339            // ── Regex ops: pipe the subject — `$str |> s/\n//g` ────────────────
7340            //    Auto-inject `r` flag so the substitution returns the modified
7341            //    string instead of the match count (non-destructive / Perl /r).
7342            ExprKind::Substitution {
7343                pattern,
7344                replacement,
7345                mut flags,
7346                expr: _,
7347                delim,
7348            } => {
7349                if !flags.contains('r') {
7350                    flags.push('r');
7351                }
7352                ExprKind::Substitution {
7353                    expr: Box::new(lhs),
7354                    pattern,
7355                    replacement,
7356                    flags,
7357                    delim,
7358                }
7359            }
7360            ExprKind::Transliterate {
7361                from,
7362                to,
7363                mut flags,
7364                expr: _,
7365                delim,
7366            } => {
7367                if !flags.contains('r') {
7368                    flags.push('r');
7369                }
7370                ExprKind::Transliterate {
7371                    expr: Box::new(lhs),
7372                    from,
7373                    to,
7374                    flags,
7375                    delim,
7376                }
7377            }
7378            ExprKind::Match {
7379                pattern,
7380                flags,
7381                scalar_g,
7382                expr: _,
7383                delim,
7384            } => ExprKind::Match {
7385                expr: Box::new(lhs),
7386                pattern,
7387                flags,
7388                scalar_g,
7389                delim,
7390            },
7391            // Bare `/regex/` (no explicit `m`): promote to Match on piped LHS
7392            ExprKind::Regex(pattern, flags) => ExprKind::Match {
7393                expr: Box::new(lhs),
7394                pattern,
7395                flags,
7396                scalar_g: false,
7397                delim: '/',
7398            },
7399
7400            // ── Bareword function name → plain unary call ──────────────────────
7401            ExprKind::Bareword(name) => match name.as_str() {
7402                "reverse" => {
7403                    if crate::no_interop_mode() {
7404                        return Err(self.syntax_err(
7405                            "stryke uses `rev` instead of `reverse` (--no-interop)",
7406                            line,
7407                        ));
7408                    }
7409                    ExprKind::ReverseExpr(Box::new(lhs))
7410                }
7411                "rv" | "reversed" | "rev" => ExprKind::Rev(Box::new(lhs)),
7412                "uq" | "uniq" | "distinct" => ExprKind::FuncCall {
7413                    name: "uniq".to_string(),
7414                    args: vec![lhs],
7415                },
7416                "fl" | "flatten" => ExprKind::FuncCall {
7417                    name: "flatten".to_string(),
7418                    args: vec![lhs],
7419                },
7420                _ => ExprKind::FuncCall {
7421                    name,
7422                    args: vec![lhs],
7423                },
7424            },
7425
7426            // ── Callable scalars / coderefs / derefs → IndirectCall ────────────
7427            kind @ (ExprKind::ScalarVar(_)
7428            | ExprKind::ArrayElement { .. }
7429            | ExprKind::HashElement { .. }
7430            | ExprKind::Deref { .. }
7431            | ExprKind::ArrowDeref { .. }
7432            | ExprKind::CodeRef { .. }
7433            | ExprKind::SubroutineRef(_)
7434            | ExprKind::SubroutineCodeRef(_)
7435            | ExprKind::DynamicSubCodeRef(_)) => ExprKind::IndirectCall {
7436                target: Box::new(Expr { kind, line: rline }),
7437                args: vec![lhs],
7438                ampersand: false,
7439                pass_caller_arglist: false,
7440            },
7441
7442            // `LHS |> >{ BLOCK }` — the `>{}` form is parsed everywhere as `Do(CodeRef)` (IIFE).
7443            // On the RHS of `|>` we want pipe-apply semantics instead: unwrap the Do and invoke
7444            // the inner coderef with `lhs` as `$_[0]`, matching `LHS |> fn { ... }`.
7445            ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. }) => {
7446                ExprKind::IndirectCall {
7447                    target: inner,
7448                    args: vec![lhs],
7449                    ampersand: false,
7450                    pass_caller_arglist: false,
7451                }
7452            }
7453
7454            other => {
7455                return Err(self.syntax_err(
7456                    format!(
7457                        "right-hand side of `|>` must be a call, builtin, or coderef \
7458                         expression (got {})",
7459                        Self::expr_kind_name(&other)
7460                    ),
7461                    line,
7462                ));
7463            }
7464        };
7465        Ok(Expr {
7466            kind: new_kind,
7467            line,
7468        })
7469    }
7470
7471    /// Short label for an `ExprKind` (used in `|>` error messages).
7472    fn expr_kind_name(kind: &ExprKind) -> &'static str {
7473        match kind {
7474            ExprKind::Integer(_) | ExprKind::Float(_) => "numeric literal",
7475            ExprKind::String(_) | ExprKind::InterpolatedString(_) => "string literal",
7476            ExprKind::BinOp { .. } => "binary expression",
7477            ExprKind::UnaryOp { .. } => "unary expression",
7478            ExprKind::Ternary { .. } => "ternary expression",
7479            ExprKind::Assign { .. } | ExprKind::CompoundAssign { .. } => "assignment",
7480            ExprKind::List(_) => "list expression",
7481            ExprKind::Range { .. } => "range expression",
7482            _ => "expression",
7483        }
7484    }
7485
7486    // or / not (lowest precedence word operators)
7487    fn parse_or_word(&mut self) -> StrykeResult<Expr> {
7488        let mut left = self.parse_and_word()?;
7489        while matches!(self.peek(), Token::LogOrWord) {
7490            let line = left.line;
7491            self.advance();
7492            let right = self.parse_and_word()?;
7493            left = Expr {
7494                kind: ExprKind::BinOp {
7495                    left: Box::new(left),
7496                    op: BinOp::LogOrWord,
7497                    right: Box::new(right),
7498                },
7499                line,
7500            };
7501        }
7502        Ok(left)
7503    }
7504
7505    fn parse_and_word(&mut self) -> StrykeResult<Expr> {
7506        let mut left = self.parse_not_word()?;
7507        while matches!(self.peek(), Token::LogAndWord) {
7508            let line = left.line;
7509            self.advance();
7510            let right = self.parse_not_word()?;
7511            left = Expr {
7512                kind: ExprKind::BinOp {
7513                    left: Box::new(left),
7514                    op: BinOp::LogAndWord,
7515                    right: Box::new(right),
7516                },
7517                line,
7518            };
7519        }
7520        Ok(left)
7521    }
7522
7523    fn parse_not_word(&mut self) -> StrykeResult<Expr> {
7524        if matches!(self.peek(), Token::LogNotWord) {
7525            let line = self.peek_line();
7526            self.advance();
7527            let expr = self.parse_not_word()?;
7528            return Ok(Expr {
7529                kind: ExprKind::UnaryOp {
7530                    op: UnaryOp::LogNotWord,
7531                    expr: Box::new(expr),
7532                },
7533                line,
7534            });
7535        }
7536        // Descend into assignment level — `not` sits ABOVE `=` in Perl
7537        // precedence, so `not $x = 5` parses as `not ($x = 5)`.
7538        self.parse_assign_expr()
7539    }
7540
7541    fn parse_log_or(&mut self) -> StrykeResult<Expr> {
7542        let mut left = self.parse_log_and()?;
7543        loop {
7544            let op = match self.peek() {
7545                Token::LogOr => BinOp::LogOr,
7546                Token::DefinedOr => BinOp::DefinedOr,
7547                _ => break,
7548            };
7549            let line = left.line;
7550            self.advance();
7551            let right = self.parse_log_and()?;
7552            left = Expr {
7553                kind: ExprKind::BinOp {
7554                    left: Box::new(left),
7555                    op,
7556                    right: Box::new(right),
7557                },
7558                line,
7559            };
7560        }
7561        Ok(left)
7562    }
7563
7564    fn parse_log_and(&mut self) -> StrykeResult<Expr> {
7565        let mut left = self.parse_bit_or()?;
7566        while matches!(self.peek(), Token::LogAnd) {
7567            let line = left.line;
7568            self.advance();
7569            let right = self.parse_bit_or()?;
7570            left = Expr {
7571                kind: ExprKind::BinOp {
7572                    left: Box::new(left),
7573                    op: BinOp::LogAnd,
7574                    right: Box::new(right),
7575                },
7576                line,
7577            };
7578        }
7579        Ok(left)
7580    }
7581
7582    fn parse_bit_or(&mut self) -> StrykeResult<Expr> {
7583        let mut left = self.parse_bit_xor()?;
7584        while matches!(self.peek(), Token::BitOr) {
7585            let line = left.line;
7586            self.advance();
7587            let right = self.parse_bit_xor()?;
7588            left = Expr {
7589                kind: ExprKind::BinOp {
7590                    left: Box::new(left),
7591                    op: BinOp::BitOr,
7592                    right: Box::new(right),
7593                },
7594                line,
7595            };
7596        }
7597        Ok(left)
7598    }
7599
7600    fn parse_bit_xor(&mut self) -> StrykeResult<Expr> {
7601        let mut left = self.parse_bit_and()?;
7602        while matches!(self.peek(), Token::BitXor) {
7603            let line = left.line;
7604            self.advance();
7605            let right = self.parse_bit_and()?;
7606            left = Expr {
7607                kind: ExprKind::BinOp {
7608                    left: Box::new(left),
7609                    op: BinOp::BitXor,
7610                    right: Box::new(right),
7611                },
7612                line,
7613            };
7614        }
7615        Ok(left)
7616    }
7617
7618    fn parse_bit_and(&mut self) -> StrykeResult<Expr> {
7619        let mut left = self.parse_equality()?;
7620        while matches!(self.peek(), Token::BitAnd) {
7621            let line = left.line;
7622            self.advance();
7623            let right = self.parse_equality()?;
7624            left = Expr {
7625                kind: ExprKind::BinOp {
7626                    left: Box::new(left),
7627                    op: BinOp::BitAnd,
7628                    right: Box::new(right),
7629                },
7630                line,
7631            };
7632        }
7633        Ok(left)
7634    }
7635
7636    fn parse_equality(&mut self) -> StrykeResult<Expr> {
7637        let mut left = self.parse_comparison()?;
7638        loop {
7639            let op = match self.peek() {
7640                Token::NumEq => BinOp::NumEq,
7641                Token::NumNe => BinOp::NumNe,
7642                Token::StrEq => BinOp::StrEq,
7643                Token::StrNe => BinOp::StrNe,
7644                Token::Spaceship => BinOp::Spaceship,
7645                Token::StrCmp => BinOp::StrCmp,
7646                _ => break,
7647            };
7648            let line = left.line;
7649            self.advance();
7650            let right = self.parse_comparison()?;
7651            left = Expr {
7652                kind: ExprKind::BinOp {
7653                    left: Box::new(left),
7654                    op,
7655                    right: Box::new(right),
7656                },
7657                line,
7658            };
7659        }
7660        Ok(left)
7661    }
7662
7663    fn parse_comparison(&mut self) -> StrykeResult<Expr> {
7664        let left = self.parse_shift()?;
7665        let first_op = match self.peek() {
7666            Token::NumLt => BinOp::NumLt,
7667            Token::NumGt => BinOp::NumGt,
7668            Token::NumLe => BinOp::NumLe,
7669            Token::NumGe => BinOp::NumGe,
7670            Token::StrLt => BinOp::StrLt,
7671            Token::StrGt => BinOp::StrGt,
7672            Token::StrLe => BinOp::StrLe,
7673            Token::StrGe => BinOp::StrGe,
7674            _ => return Ok(left),
7675        };
7676        let line = left.line;
7677        self.advance();
7678        let middle = self.parse_shift()?;
7679
7680        let second_op = match self.peek() {
7681            Token::NumLt => Some(BinOp::NumLt),
7682            Token::NumGt => Some(BinOp::NumGt),
7683            Token::NumLe => Some(BinOp::NumLe),
7684            Token::NumGe => Some(BinOp::NumGe),
7685            Token::StrLt => Some(BinOp::StrLt),
7686            Token::StrGt => Some(BinOp::StrGt),
7687            Token::StrLe => Some(BinOp::StrLe),
7688            Token::StrGe => Some(BinOp::StrGe),
7689            _ => None,
7690        };
7691
7692        if second_op.is_none() {
7693            return Ok(Expr {
7694                kind: ExprKind::BinOp {
7695                    left: Box::new(left),
7696                    op: first_op,
7697                    right: Box::new(middle),
7698                },
7699                line,
7700            });
7701        }
7702
7703        // Chained comparison: `a < b < c` → `(a < b) && (b < c)`
7704        // Collect all operands and operators for chains like `1 < x < 10 < y`
7705        let mut operands = vec![left, middle];
7706        let mut ops = vec![first_op];
7707
7708        loop {
7709            let op = match self.peek() {
7710                Token::NumLt => BinOp::NumLt,
7711                Token::NumGt => BinOp::NumGt,
7712                Token::NumLe => BinOp::NumLe,
7713                Token::NumGe => BinOp::NumGe,
7714                Token::StrLt => BinOp::StrLt,
7715                Token::StrGt => BinOp::StrGt,
7716                Token::StrLe => BinOp::StrLe,
7717                Token::StrGe => BinOp::StrGe,
7718                _ => break,
7719            };
7720            self.advance();
7721            ops.push(op);
7722            operands.push(self.parse_shift()?);
7723        }
7724
7725        // Build `(a op0 b) && (b op1 c) && (c op2 d) && ...`
7726        let mut result = Expr {
7727            kind: ExprKind::BinOp {
7728                left: Box::new(operands[0].clone()),
7729                op: ops[0],
7730                right: Box::new(operands[1].clone()),
7731            },
7732            line,
7733        };
7734
7735        for i in 1..ops.len() {
7736            let cmp = Expr {
7737                kind: ExprKind::BinOp {
7738                    left: Box::new(operands[i].clone()),
7739                    op: ops[i],
7740                    right: Box::new(operands[i + 1].clone()),
7741                },
7742                line,
7743            };
7744            result = Expr {
7745                kind: ExprKind::BinOp {
7746                    left: Box::new(result),
7747                    op: BinOp::LogAnd,
7748                    right: Box::new(cmp),
7749                },
7750                line,
7751            };
7752        }
7753
7754        Ok(result)
7755    }
7756
7757    fn parse_shift(&mut self) -> StrykeResult<Expr> {
7758        let mut left = self.parse_addition()?;
7759        loop {
7760            let op = match self.peek() {
7761                Token::ShiftLeft => BinOp::ShiftLeft,
7762                Token::ShiftRight => BinOp::ShiftRight,
7763                _ => break,
7764            };
7765            let line = left.line;
7766            self.advance();
7767            let right = self.parse_addition()?;
7768            left = Expr {
7769                kind: ExprKind::BinOp {
7770                    left: Box::new(left),
7771                    op,
7772                    right: Box::new(right),
7773                },
7774                line,
7775            };
7776        }
7777        Ok(left)
7778    }
7779
7780    fn parse_addition(&mut self) -> StrykeResult<Expr> {
7781        let mut left = self.parse_multiplication()?;
7782        loop {
7783            // Implicit semicolon: `-` or `+` on a new line is a unary operator on
7784            // the next statement, not a binary operator continuing this expression.
7785            let op = match self.peek() {
7786                Token::Plus if self.peek_line() == self.prev_line() => BinOp::Add,
7787                Token::Minus if self.peek_line() == self.prev_line() => BinOp::Sub,
7788                Token::Dot => BinOp::Concat,
7789                _ => break,
7790            };
7791            let line = left.line;
7792            self.advance();
7793            let right = self.parse_multiplication()?;
7794            left = Expr {
7795                kind: ExprKind::BinOp {
7796                    left: Box::new(left),
7797                    op,
7798                    right: Box::new(right),
7799                },
7800                line,
7801            };
7802        }
7803        Ok(left)
7804    }
7805
7806    fn parse_multiplication(&mut self) -> StrykeResult<Expr> {
7807        let mut left = self.parse_regex_bind()?;
7808        loop {
7809            let op = match self.peek() {
7810                Token::Star => BinOp::Mul,
7811                Token::Slash if self.suppress_slash_as_div == 0 => BinOp::Div,
7812                // Implicit semicolon: `%` on a new line is a hash dereference or hash
7813                // sigil for the next statement, not modulo operator on this expression.
7814                Token::Percent if self.peek_line() == self.prev_line() => BinOp::Mod,
7815                Token::X => {
7816                    let line = left.line;
7817                    // List-repeat fires when the LHS was just closed by a
7818                    // list-constructor paren (`(EXPR)`, `(LIST)`, `()`) or
7819                    // `qw(...)`. `parse_primary` records the post-close
7820                    // position; an exact match against `self.pos` here means
7821                    // no postfix consumed any tokens between the close and
7822                    // the `x`, so the LHS is intrinsically a list construct.
7823                    let list_repeat = self.list_construct_close_pos == Some(self.pos);
7824                    self.advance();
7825                    let right = self.parse_regex_bind()?;
7826                    left = Expr {
7827                        kind: ExprKind::Repeat {
7828                            expr: Box::new(left),
7829                            count: Box::new(right),
7830                            list_repeat,
7831                        },
7832                        line,
7833                    };
7834                    continue;
7835                }
7836                _ => break,
7837            };
7838            let line = left.line;
7839            self.advance();
7840            let right = self.parse_regex_bind()?;
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_regex_bind(&mut self) -> StrykeResult<Expr> {
7854        let left = self.parse_unary()?;
7855        match self.peek() {
7856            Token::BindMatch => {
7857                let line = left.line;
7858                self.advance();
7859                match self.peek().clone() {
7860                    Token::Regex(pattern, flags, delim) => {
7861                        self.advance();
7862                        Ok(Expr {
7863                            kind: ExprKind::Match {
7864                                expr: Box::new(left),
7865                                pattern,
7866                                flags,
7867                                scalar_g: false,
7868                                delim,
7869                            },
7870                            line,
7871                        })
7872                    }
7873                    Token::Ident(ref s) if s.starts_with('\x00') => {
7874                        let (Token::Ident(encoded), _) = self.advance() else {
7875                            unreachable!()
7876                        };
7877                        let parts: Vec<&str> = encoded.split('\x00').collect();
7878                        if parts.len() >= 4 && parts[1] == "s" {
7879                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
7880                            Ok(Expr {
7881                                kind: ExprKind::Substitution {
7882                                    expr: Box::new(left),
7883                                    pattern: parts[2].to_string(),
7884                                    replacement: parts[3].to_string(),
7885                                    flags: parts.get(4).unwrap_or(&"").to_string(),
7886                                    delim,
7887                                },
7888                                line,
7889                            })
7890                        } else if parts.len() >= 4 && parts[1] == "tr" {
7891                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
7892                            Ok(Expr {
7893                                kind: ExprKind::Transliterate {
7894                                    expr: Box::new(left),
7895                                    from: parts[2].to_string(),
7896                                    to: parts[3].to_string(),
7897                                    flags: parts.get(4).unwrap_or(&"").to_string(),
7898                                    delim,
7899                                },
7900                                line,
7901                            })
7902                        } else {
7903                            Err(self.syntax_err("Invalid regex binding", line))
7904                        }
7905                    }
7906                    _ => {
7907                        let rhs = self.parse_unary()?;
7908                        Ok(Expr {
7909                            kind: ExprKind::BinOp {
7910                                left: Box::new(left),
7911                                op: BinOp::BindMatch,
7912                                right: Box::new(rhs),
7913                            },
7914                            line,
7915                        })
7916                    }
7917                }
7918            }
7919            Token::BindNotMatch => {
7920                let line = left.line;
7921                self.advance();
7922                match self.peek().clone() {
7923                    Token::Regex(pattern, flags, delim) => {
7924                        self.advance();
7925                        Ok(Expr {
7926                            kind: ExprKind::UnaryOp {
7927                                op: UnaryOp::LogNot,
7928                                expr: Box::new(Expr {
7929                                    kind: ExprKind::Match {
7930                                        expr: Box::new(left),
7931                                        pattern,
7932                                        flags,
7933                                        scalar_g: false,
7934                                        delim,
7935                                    },
7936                                    line,
7937                                }),
7938                            },
7939                            line,
7940                        })
7941                    }
7942                    Token::Ident(ref s) if s.starts_with('\x00') => {
7943                        let (Token::Ident(encoded), _) = self.advance() else {
7944                            unreachable!()
7945                        };
7946                        let parts: Vec<&str> = encoded.split('\x00').collect();
7947                        if parts.len() >= 4 && parts[1] == "s" {
7948                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
7949                            Ok(Expr {
7950                                kind: ExprKind::UnaryOp {
7951                                    op: UnaryOp::LogNot,
7952                                    expr: Box::new(Expr {
7953                                        kind: ExprKind::Substitution {
7954                                            expr: Box::new(left),
7955                                            pattern: parts[2].to_string(),
7956                                            replacement: parts[3].to_string(),
7957                                            flags: parts.get(4).unwrap_or(&"").to_string(),
7958                                            delim,
7959                                        },
7960                                        line,
7961                                    }),
7962                                },
7963                                line,
7964                            })
7965                        } else if parts.len() >= 4 && parts[1] == "tr" {
7966                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
7967                            Ok(Expr {
7968                                kind: ExprKind::UnaryOp {
7969                                    op: UnaryOp::LogNot,
7970                                    expr: Box::new(Expr {
7971                                        kind: ExprKind::Transliterate {
7972                                            expr: Box::new(left),
7973                                            from: parts[2].to_string(),
7974                                            to: parts[3].to_string(),
7975                                            flags: parts.get(4).unwrap_or(&"").to_string(),
7976                                            delim,
7977                                        },
7978                                        line,
7979                                    }),
7980                                },
7981                                line,
7982                            })
7983                        } else {
7984                            Err(self.syntax_err("Invalid regex binding after !~", line))
7985                        }
7986                    }
7987                    _ => {
7988                        let rhs = self.parse_unary()?;
7989                        Ok(Expr {
7990                            kind: ExprKind::BinOp {
7991                                left: Box::new(left),
7992                                op: BinOp::BindNotMatch,
7993                                right: Box::new(rhs),
7994                            },
7995                            line,
7996                        })
7997                    }
7998                }
7999            }
8000            _ => Ok(left),
8001        }
8002    }
8003
8004    /// Parse thread macro input. Like `parse_range` but suppresses `/` as division
8005    /// so that `/pattern/` is left for the thread stage parser to handle as regex filter.
8006    fn parse_thread_input(&mut self) -> StrykeResult<Expr> {
8007        self.suppress_slash_as_div = self.suppress_slash_as_div.saturating_add(1);
8008        let result = self.parse_range();
8009        self.suppress_slash_as_div = self.suppress_slash_as_div.saturating_sub(1);
8010        result
8011    }
8012
8013    /// Parse `~p>` / `~p>>` parallel-chunk thread-macros. Equivalent to
8014    /// `par_reduce { stage1 |> stage2 |> ... } SOURCE`, with optional
8015    /// `||>` or `|then|` mid-pipeline boundary that switches to a normal
8016    /// `~>` / `~>>` continuation operating on the auto-merged result.
8017    fn parse_thread_macro_chunk_par(
8018        &mut self,
8019        line: usize,
8020        thread_last: bool,
8021    ) -> StrykeResult<Expr> {
8022        // Source: same parsing rules as `~>`.
8023        self.suppress_parenless_call = self.suppress_parenless_call.saturating_add(1);
8024        let source_expr = self.parse_thread_input();
8025        self.suppress_parenless_call = self.suppress_parenless_call.saturating_sub(1);
8026        let source_expr = source_expr?;
8027
8028        // Per-chunk stage chain: stages operate on `@_` (the chunk elements)
8029        // which the par_reduce runtime binds as the argument array. Use
8030        // `pending_thread_input` to seed the stage chain with `@_`.
8031        self.pending_thread_input = Some(Expr {
8032            kind: ExprKind::ArrayVar("_".into()),
8033            line,
8034        });
8035        let chunk_chain = self.parse_thread_macro_inner(line, thread_last, None);
8036        self.pending_thread_input = None;
8037        let chunk_chain = chunk_chain?;
8038
8039        // `parse_thread_macro_inner` (under pipe_rhs_depth > 0) wraps its
8040        // result as `fn { ... stages applied to $_[0] ... }`. Unwrap to
8041        // get the bare Block (`Vec<Statement>`) for the `par_reduce`
8042        // extract slot.
8043        let extract_block: Block = match chunk_chain.kind {
8044            ExprKind::CodeRef { params: _, body } => body,
8045            _ => vec![Statement {
8046                label: None,
8047                kind: StmtKind::Expression(chunk_chain),
8048                line,
8049            }],
8050        };
8051
8052        let par_reduce = Expr {
8053            kind: ExprKind::ParReduceExpr {
8054                extract_block,
8055                reduce_block: None,
8056                list: Box::new(source_expr),
8057            },
8058            line,
8059        };
8060
8061        // Check for `||>` / `|then|` boundary; if present, parse the
8062        // continuation as a normal `~>` / `~>>` thread macro with the
8063        // par_reduce result as its input.
8064        if self.eat_chunk_par_split_boundary() {
8065            return self.parse_thread_macro_continuation(par_reduce, line, thread_last);
8066        }
8067        Ok(par_reduce)
8068    }
8069
8070    /// Parse `~d>` / `~d>>` distributed thread-macros. Same chunk-block
8071    /// semantics as `~p>` (stages operate on `@_`) but chunks ship to a
8072    /// `RemoteCluster` via the existing `cluster::run_cluster` dispatcher.
8073    /// Syntax: `~d> on EXPR SOURCE stage1 stage2 ...`. The `on EXPR` slot
8074    /// is required; without it the operator falls through to a syntax
8075    /// error (no implicit default-cluster in v1).
8076    fn parse_thread_macro_dist(&mut self, line: usize, thread_last: bool) -> StrykeResult<Expr> {
8077        // Required `on EXPR` — the cluster operand.
8078        let on_ok = matches!(self.peek(), Token::Ident(ref s) if s == "on");
8079        if !on_ok {
8080            return Err(
8081                self.syntax_err("~d>: expected `on <cluster-expr>` after the operator", line)
8082            );
8083        }
8084        self.advance(); // consume `on`
8085                        // Parse cluster expr — same parse-rules as a thread-macro input
8086                        // (avoid pulling stages into the cluster expression).
8087        self.suppress_parenless_call = self.suppress_parenless_call.saturating_add(1);
8088        // Without this, `on $cluster () map { … }` parses `()` as a postfix
8089        // indirect call on `$cluster`, stealing the empty list meant as SOURCE.
8090        // Zero-arg cluster from a scalar sub: `on ($factory())` or `on $f->()`.
8091        self.suppress_indirect_paren_call = self.suppress_indirect_paren_call.saturating_add(1);
8092        let cluster_expr = self.parse_thread_input();
8093        self.suppress_indirect_paren_call = self.suppress_indirect_paren_call.saturating_sub(1);
8094        self.suppress_parenless_call = self.suppress_parenless_call.saturating_sub(1);
8095        let cluster_expr = cluster_expr?;
8096
8097        // Source list: same rules as `~p>` source.
8098        self.suppress_parenless_call = self.suppress_parenless_call.saturating_add(1);
8099        let source_expr = self.parse_thread_input();
8100        self.suppress_parenless_call = self.suppress_parenless_call.saturating_sub(1);
8101        let source_expr = source_expr?;
8102
8103        // Stage chain seeded with `@_` — matches `~p>` chunk-block
8104        // semantics. The VM-side eval prepends `@_ = $_;` to the shipped
8105        // block source so the remote agent's `set_topic(chunk_flat_array)`
8106        // is reflected into `@_` before user stages run.
8107        self.pending_thread_input = Some(Expr {
8108            kind: ExprKind::ArrayVar("_".into()),
8109            line,
8110        });
8111        let chunk_chain = self.parse_thread_macro_inner(line, thread_last, None);
8112        self.pending_thread_input = None;
8113        let chunk_chain = chunk_chain?;
8114
8115        let extract_block: Block = match chunk_chain.kind {
8116            ExprKind::CodeRef { params: _, body } => body,
8117            _ => vec![Statement {
8118                label: None,
8119                kind: StmtKind::Expression(chunk_chain),
8120                line,
8121            }],
8122        };
8123
8124        let dist_reduce = Expr {
8125            kind: ExprKind::DistReduceExpr {
8126                cluster: Box::new(cluster_expr),
8127                extract_block,
8128                list: Box::new(source_expr),
8129            },
8130            line,
8131        };
8132
8133        // `||>` / `|then|` boundary continuation, same as `~p>`.
8134        if self.eat_chunk_par_split_boundary() {
8135            return self.parse_thread_macro_continuation(dist_reduce, line, thread_last);
8136        }
8137        Ok(dist_reduce)
8138    }
8139
8140    /// Parse a `~>` / `~>>` continuation after a `||>` / `|then|`
8141    /// chunk-parallel-to-sequential boundary. Reuses
8142    /// `parse_thread_macro_inner` with `result_init: Some(prior)` so the
8143    /// stage loop threads from the par_reduce result instead of parsing
8144    /// a fresh source expression.
8145    fn parse_thread_macro_continuation(
8146        &mut self,
8147        prior: Expr,
8148        line: usize,
8149        thread_last: bool,
8150    ) -> StrykeResult<Expr> {
8151        self.pending_thread_input = Some(prior);
8152        let res = self.parse_thread_macro_inner(line, thread_last, None);
8153        self.pending_thread_input = None;
8154        res
8155    }
8156
8157    /// Try to consume `||>` (LogOr followed by `>`) or `|then|`
8158    /// (`Pipe Ident("then") Pipe`) as the chunk-parallel → sequential
8159    /// switch marker. Returns true if a boundary was consumed.
8160    fn eat_chunk_par_split_boundary(&mut self) -> bool {
8161        // `||>` = `LogOr` token (already merged in lex) followed by `>`.
8162        if matches!(self.peek(), Token::LogOr) && matches!(self.peek_at(1), Token::NumGt) {
8163            self.advance(); // ||
8164            self.advance(); // >
8165            return true;
8166        }
8167        // `|then|` = `BitOr` + `Ident("then")` + `BitOr`.
8168        if matches!(self.peek(), Token::BitOr) {
8169            if let Token::Ident(name) = self.peek_at(1).clone() {
8170                if name == "then" && matches!(self.peek_at(2), Token::BitOr) {
8171                    self.advance(); // |
8172                    self.advance(); // then
8173                    self.advance(); // |
8174                    return true;
8175                }
8176            }
8177        }
8178        false
8179    }
8180
8181    /// Perl `..` / `...` operator — precedence sits between `?:` and `||` (`perlop`), so
8182    /// `$x .. $x + 3` parses as `$x .. ($x + 3)` and `1..$n||5` parses as `1..($n||5)`. Both
8183    /// operands recurse through `parse_log_or`, which in turn walks down through all tighter
8184    /// operators (additive, multiplicative, regex bind, unary). Non-associative: the right
8185    /// operand is a single `parse_log_or` so `1..5..10` is a parse error in Perl, but we accept
8186    /// it greedily (left-associated) because the lexer already forbids `..` after a range RHS.
8187    fn parse_range(&mut self) -> StrykeResult<Expr> {
8188        let left = self.parse_log_or()?;
8189        let line = left.line;
8190        // `1..10` (traditional inclusive) / `1...10` (exclusive) / `1:10`
8191        // (short form) / `1~10` (universal short form). The `~` separator
8192        // works for every range type and is the only viable separator for
8193        // IPv6 since IPv6 already uses `:` internally; `:` would collide.
8194        // It also dodges `!`'s collision with the `_!N!` paired char-index
8195        // syntax. Single-`~` (vs `!!!` triple) keeps the surface simple.
8196        let (exclusive, _colon_style) = if self.eat(&Token::RangeExclusive) {
8197            (true, false)
8198        } else if self.eat(&Token::Range) {
8199            (false, false)
8200        } else if self.suppress_colon_range == 0 && self.eat(&Token::Colon) {
8201            // `1:10` short form — only valid for numeric ranges, not ternary
8202            // Lookahead: must be followed by something that looks like a range endpoint
8203            (false, true)
8204        } else if self.suppress_tilde_range == 0 && self.eat(&Token::BitNot) {
8205            (false, true)
8206        } else {
8207            return Ok(left);
8208        };
8209        let right = self.parse_log_or()?;
8210        // Optional step: `1..100:2` / `1:100:2` / `IPV6~IPV6~STEP`. `~` is
8211        // gated by `suppress_tilde_range` so paired char-index (`$x~5~`)
8212        // doesn't get its closing delimiter eaten as a range op.
8213        let step = if self.eat(&Token::Colon)
8214            || (self.suppress_tilde_range == 0 && self.eat(&Token::BitNot))
8215        {
8216            Some(Box::new(self.parse_unary()?))
8217        } else {
8218            None
8219        };
8220        Ok(Expr {
8221            kind: ExprKind::Range {
8222                from: Box::new(left),
8223                to: Box::new(right),
8224                exclusive,
8225                step,
8226            },
8227            line,
8228        })
8229    }
8230
8231    /// `name` or `Foo::Bar::baz` — used after `sub`, unary `&`, etc.
8232    fn parse_package_qualified_identifier(&mut self) -> StrykeResult<String> {
8233        let mut name = match self.advance() {
8234            (Token::Ident(n), _) => n,
8235            (tok, l) => {
8236                return Err(self.syntax_err(format!("Expected identifier, got {:?}", tok), l));
8237            }
8238        };
8239        while self.eat(&Token::PackageSep) {
8240            match self.advance() {
8241                (Token::Ident(part), _) => {
8242                    name.push_str("::");
8243                    name.push_str(&part);
8244                }
8245                // Topic-slot scalars (`_`, `_<<<<`, `_3`, etc.) lex as
8246                // `Token::ScalarVar` per the lexer's reservation. Accept
8247                // them as the trailing segment of a package-qualified
8248                // name so callers (e.g. `parse_sub_decl`) can reject the
8249                // full name with a friendly "would shadow topic-slot"
8250                // message rather than a generic "Expected identifier
8251                // after `::`" lexer-level error.
8252                (Token::ScalarVar(part), _) if Self::is_underscore_topic_slot(&part) => {
8253                    name.push_str("::");
8254                    name.push_str(&part);
8255                }
8256                (tok, l) => {
8257                    return Err(self
8258                        .syntax_err(format!("Expected identifier after `::`, got {:?}", tok), l));
8259                }
8260            }
8261        }
8262        Ok(name)
8263    }
8264
8265    /// After consuming unary `&`: `name` or `Foo::Bar::baz` (Perl `&foo` / `&Foo::bar`).
8266    fn parse_qualified_subroutine_name(&mut self) -> StrykeResult<String> {
8267        self.parse_package_qualified_identifier()
8268    }
8269
8270    fn parse_unary(&mut self) -> StrykeResult<Expr> {
8271        let line = self.peek_line();
8272        match self.peek().clone() {
8273            Token::Minus => {
8274                self.advance();
8275                let expr = self.parse_power()?;
8276                Ok(Expr {
8277                    kind: ExprKind::UnaryOp {
8278                        op: UnaryOp::Negate,
8279                        expr: Box::new(expr),
8280                    },
8281                    line,
8282                })
8283            }
8284            // Unary `+EXPR` — Perl uses this to disambiguate barewords in hash subscripts (`$h{+Foo}`)
8285            // and for scalar context; treat as a no-op on the parsed operand.
8286            // Special case: `+{ ... }` forces hashref interpretation (Perl idiom),
8287            // even when the body is a list-yielding expression like `+{ map { ... } @arr }`.
8288            // Without this, `{ map { ... } @arr }` falls back to block/CodeRef parsing
8289            // because the body doesn't fit `KEY => VAL` shape.
8290            Token::Plus => {
8291                self.advance();
8292                if matches!(self.peek(), Token::LBrace) {
8293                    let line = self.peek_line();
8294                    self.advance(); // consume {
8295                    return self.parse_forced_hashref_body(line);
8296                }
8297                self.parse_unary()
8298            }
8299            Token::LogNot => {
8300                self.advance();
8301                let expr = self.parse_unary()?;
8302                Ok(Expr {
8303                    kind: ExprKind::UnaryOp {
8304                        op: UnaryOp::LogNot,
8305                        expr: Box::new(expr),
8306                    },
8307                    line,
8308                })
8309            }
8310            Token::BitNot => {
8311                self.advance();
8312                let expr = self.parse_unary()?;
8313                Ok(Expr {
8314                    kind: ExprKind::UnaryOp {
8315                        op: UnaryOp::BitNot,
8316                        expr: Box::new(expr),
8317                    },
8318                    line,
8319                })
8320            }
8321            Token::Increment => {
8322                self.advance();
8323                let expr = self.parse_postfix()?;
8324                Ok(Expr {
8325                    kind: ExprKind::UnaryOp {
8326                        op: UnaryOp::PreIncrement,
8327                        expr: Box::new(expr),
8328                    },
8329                    line,
8330                })
8331            }
8332            Token::Decrement => {
8333                self.advance();
8334                let expr = self.parse_postfix()?;
8335                Ok(Expr {
8336                    kind: ExprKind::UnaryOp {
8337                        op: UnaryOp::PreDecrement,
8338                        expr: Box::new(expr),
8339                    },
8340                    line,
8341                })
8342            }
8343            Token::BitAnd => {
8344                // Unary `&name` / `&Pkg::name` (call / coderef); binary `&` is in `parse_bit_and`.
8345                // `&$coderef(...)` — call sub whose ref is in a scalar (core `B.pm` / `&$recurse($sym)`).
8346                self.advance();
8347                if matches!(self.peek(), Token::LBrace) {
8348                    self.advance();
8349                    let inner = self.parse_expression()?;
8350                    self.expect(&Token::RBrace)?;
8351                    return Ok(Expr {
8352                        kind: ExprKind::DynamicSubCodeRef(Box::new(inner)),
8353                        line,
8354                    });
8355                }
8356                if matches!(self.peek(), Token::Ident(_)) {
8357                    let name = self.parse_qualified_subroutine_name()?;
8358                    return Ok(Expr {
8359                        kind: ExprKind::SubroutineRef(name),
8360                        line,
8361                    });
8362                }
8363                let target = self.parse_primary()?;
8364                if matches!(self.peek(), Token::LParen) {
8365                    self.advance();
8366                    let args = self.parse_arg_list()?;
8367                    self.expect(&Token::RParen)?;
8368                    return Ok(Expr {
8369                        kind: ExprKind::IndirectCall {
8370                            target: Box::new(target),
8371                            args,
8372                            ampersand: true,
8373                            pass_caller_arglist: false,
8374                        },
8375                        line,
8376                    });
8377                }
8378                // `&$coderef` / `&{expr}` with no `(...)` — call with caller's @_ (Perl `&$sub`).
8379                Ok(Expr {
8380                    kind: ExprKind::IndirectCall {
8381                        target: Box::new(target),
8382                        args: vec![],
8383                        ampersand: true,
8384                        pass_caller_arglist: true,
8385                    },
8386                    line,
8387                })
8388            }
8389            Token::Backslash => {
8390                self.advance();
8391                let expr = self.parse_unary()?;
8392                if let ExprKind::SubroutineRef(name) = expr.kind {
8393                    return Ok(Expr {
8394                        kind: ExprKind::SubroutineCodeRef(name),
8395                        line,
8396                    });
8397                }
8398                if matches!(expr.kind, ExprKind::DynamicSubCodeRef(_)) {
8399                    return Ok(expr);
8400                }
8401                // `\` uses `ScalarRef`; array/hash vars and `\@{...}` lower to binding or alias refs.
8402                Ok(Expr {
8403                    kind: ExprKind::ScalarRef(Box::new(expr)),
8404                    line,
8405                })
8406            }
8407            Token::FileTest(op) => {
8408                self.advance();
8409                // Perl: `-d` with no operand uses `$_` (e.g. `if (-d)` inside `for` / `while read`).
8410                let expr = if Self::filetest_allows_implicit_topic(self.peek()) {
8411                    Expr {
8412                        kind: ExprKind::ScalarVar("_".into()),
8413                        line: self.peek_line(),
8414                    }
8415                } else {
8416                    self.parse_unary()?
8417                };
8418                Ok(Expr {
8419                    kind: ExprKind::FileTest {
8420                        op,
8421                        expr: Box::new(expr),
8422                    },
8423                    line,
8424                })
8425            }
8426            _ => self.parse_power(),
8427        }
8428    }
8429
8430    fn parse_power(&mut self) -> StrykeResult<Expr> {
8431        let left = self.parse_postfix()?;
8432        if matches!(self.peek(), Token::Power) {
8433            let line = left.line;
8434            self.advance();
8435            let right = self.parse_unary()?; // right-associative
8436            return Ok(Expr {
8437                kind: ExprKind::BinOp {
8438                    left: Box::new(left),
8439                    op: BinOp::Pow,
8440                    right: Box::new(right),
8441                },
8442                line,
8443            });
8444        }
8445        Ok(left)
8446    }
8447
8448    fn parse_postfix(&mut self) -> StrykeResult<Expr> {
8449        let mut expr = self.parse_primary()?;
8450        loop {
8451            match self.peek().clone() {
8452                Token::Increment => {
8453                    // Implicit semicolon: `++` on a new line is a prefix operator
8454                    // on the next statement, not postfix on the previous expression.
8455                    if self.peek_line() > self.prev_line() {
8456                        break;
8457                    }
8458                    let line = expr.line;
8459                    self.advance();
8460                    expr = Expr {
8461                        kind: ExprKind::PostfixOp {
8462                            expr: Box::new(expr),
8463                            op: PostfixOp::Increment,
8464                        },
8465                        line,
8466                    };
8467                }
8468                Token::Decrement => {
8469                    // Implicit semicolon: `--` on a new line is a prefix operator
8470                    // on the next statement, not postfix on the previous expression.
8471                    if self.peek_line() > self.prev_line() {
8472                        break;
8473                    }
8474                    let line = expr.line;
8475                    self.advance();
8476                    expr = Expr {
8477                        kind: ExprKind::PostfixOp {
8478                            expr: Box::new(expr),
8479                            op: PostfixOp::Decrement,
8480                        },
8481                        line,
8482                    };
8483                }
8484                Token::LParen => {
8485                    if self.suppress_indirect_paren_call > 0 {
8486                        break;
8487                    }
8488                    // Implicit semicolon: `(` on a new line after an expression
8489                    // is a new statement, not a postfix code-ref call.
8490                    // e.g.  `my $x = $ENV{"KEY"}\n($y =~ s/.../.../)`
8491                    if self.peek_line() > self.prev_line() {
8492                        break;
8493                    }
8494                    let line = expr.line;
8495                    self.advance();
8496                    let args = self.parse_arg_list()?;
8497                    self.expect(&Token::RParen)?;
8498                    expr = Expr {
8499                        kind: ExprKind::IndirectCall {
8500                            target: Box::new(expr),
8501                            args,
8502                            ampersand: false,
8503                            pass_caller_arglist: false,
8504                        },
8505                        line,
8506                    };
8507                }
8508                Token::Arrow => {
8509                    let line = expr.line;
8510                    self.advance();
8511                    match self.peek().clone() {
8512                        Token::LBracket => {
8513                            self.advance();
8514                            let index = self.parse_expression()?;
8515                            self.expect(&Token::RBracket)?;
8516                            expr = Expr {
8517                                kind: ExprKind::ArrowDeref {
8518                                    expr: Box::new(expr),
8519                                    index: Box::new(index),
8520                                    kind: DerefKind::Array,
8521                                },
8522                                line,
8523                            };
8524                        }
8525                        Token::LBrace => {
8526                            self.advance();
8527                            let key = self.parse_hash_subscript_key()?;
8528                            self.expect(&Token::RBrace)?;
8529                            expr = Expr {
8530                                kind: ExprKind::ArrowDeref {
8531                                    expr: Box::new(expr),
8532                                    index: Box::new(key),
8533                                    kind: DerefKind::Hash,
8534                                },
8535                                line,
8536                            };
8537                        }
8538                        Token::LParen => {
8539                            self.advance();
8540                            let args = self.parse_arg_list()?;
8541                            self.expect(&Token::RParen)?;
8542                            expr = Expr {
8543                                kind: ExprKind::ArrowDeref {
8544                                    expr: Box::new(expr),
8545                                    index: Box::new(Expr {
8546                                        kind: ExprKind::List(args),
8547                                        line,
8548                                    }),
8549                                    kind: DerefKind::Call,
8550                                },
8551                                line,
8552                            };
8553                        }
8554                        Token::Ident(method) => {
8555                            self.advance();
8556                            if method == "SUPER" {
8557                                self.expect(&Token::PackageSep)?;
8558                                let real_method = match self.advance() {
8559                                    (Token::Ident(n), _) => n,
8560                                    (tok, l) => {
8561                                        return Err(self.syntax_err(
8562                                            format!(
8563                                                "Expected method name after SUPER::, got {:?}",
8564                                                tok
8565                                            ),
8566                                            l,
8567                                        ));
8568                                    }
8569                                };
8570                                let args = if self.eat(&Token::LParen) {
8571                                    let a = self.parse_arg_list()?;
8572                                    self.expect(&Token::RParen)?;
8573                                    a
8574                                } else {
8575                                    self.parse_method_arg_list_no_paren()?
8576                                };
8577                                expr = Expr {
8578                                    kind: ExprKind::MethodCall {
8579                                        object: Box::new(expr),
8580                                        method: real_method,
8581                                        args,
8582                                        super_call: true,
8583                                    },
8584                                    line,
8585                                };
8586                            } else {
8587                                let mut method_name = method;
8588                                while self.eat(&Token::PackageSep) {
8589                                    match self.advance() {
8590                                        (Token::Ident(part), _) => {
8591                                            method_name.push_str("::");
8592                                            method_name.push_str(&part);
8593                                        }
8594                                        (tok, l) => {
8595                                            return Err(self.syntax_err(
8596                                                format!(
8597                                                    "Expected identifier after :: in method name, got {:?}",
8598                                                    tok
8599                                                ),
8600                                                l,
8601                                            ));
8602                                        }
8603                                    }
8604                                }
8605                                let args = if self.eat(&Token::LParen) {
8606                                    let a = self.parse_arg_list()?;
8607                                    self.expect(&Token::RParen)?;
8608                                    a
8609                                } else {
8610                                    self.parse_method_arg_list_no_paren()?
8611                                };
8612                                expr = Expr {
8613                                    kind: ExprKind::MethodCall {
8614                                        object: Box::new(expr),
8615                                        method: method_name,
8616                                        args,
8617                                        super_call: false,
8618                                    },
8619                                    line,
8620                                };
8621                            }
8622                        }
8623                        // Postfix dereference (Perl 5.20+, default 5.24+):
8624                        //   `$ref->@*`         — full array      ≡ `@{$ref}`
8625                        //   `$ref->@[i,j]`     — array slice     ≡ `@{$ref}[i,j]`
8626                        //   `$ref->@{k,l}`     — hash slice (vals) ≡ `@{$ref}{k,l}`
8627                        //   `$ref->%*`         — full hash       ≡ `%{$ref}`
8628                        Token::ArrayAt => {
8629                            self.advance(); // consume `@`
8630                            match self.peek().clone() {
8631                                Token::Star => {
8632                                    self.advance();
8633                                    expr = Expr {
8634                                        kind: ExprKind::Deref {
8635                                            expr: Box::new(expr),
8636                                            kind: Sigil::Array,
8637                                        },
8638                                        line,
8639                                    };
8640                                }
8641                                Token::LBracket => {
8642                                    self.advance();
8643                                    let indices = self.parse_slice_arg_list(false)?;
8644                                    self.expect(&Token::RBracket)?;
8645                                    let source = Expr {
8646                                        kind: ExprKind::Deref {
8647                                            expr: Box::new(expr),
8648                                            kind: Sigil::Array,
8649                                        },
8650                                        line,
8651                                    };
8652                                    expr = Expr {
8653                                        kind: ExprKind::AnonymousListSlice {
8654                                            source: Box::new(source),
8655                                            indices,
8656                                        },
8657                                        line,
8658                                    };
8659                                }
8660                                Token::LBrace => {
8661                                    self.advance();
8662                                    let keys = self.parse_slice_arg_list(true)?;
8663                                    self.expect(&Token::RBrace)?;
8664                                    expr = Expr {
8665                                        kind: ExprKind::HashSliceDeref {
8666                                            container: Box::new(expr),
8667                                            keys,
8668                                        },
8669                                        line,
8670                                    };
8671                                }
8672                                tok => {
8673                                    return Err(self.syntax_err(
8674                                        format!(
8675                                            "Expected `*`, `[…]`, or `{{…}}` after `->@`, got {:?}",
8676                                            tok
8677                                        ),
8678                                        line,
8679                                    ));
8680                                }
8681                            }
8682                        }
8683                        Token::HashPercent => {
8684                            self.advance(); // consume `%`
8685                            match self.peek().clone() {
8686                                Token::Star => {
8687                                    self.advance();
8688                                    expr = Expr {
8689                                        kind: ExprKind::Deref {
8690                                            expr: Box::new(expr),
8691                                            kind: Sigil::Hash,
8692                                        },
8693                                        line,
8694                                    };
8695                                }
8696                                tok => {
8697                                    return Err(self.syntax_err(
8698                                        format!("Expected `*` after `->%`, got {:?}", tok),
8699                                        line,
8700                                    ));
8701                                }
8702                            }
8703                        }
8704                        // `x` is lexed as `Token::X` (repeat op); after `->` it is a method name.
8705                        Token::X => {
8706                            self.advance();
8707                            let args = if self.eat(&Token::LParen) {
8708                                let a = self.parse_arg_list()?;
8709                                self.expect(&Token::RParen)?;
8710                                a
8711                            } else {
8712                                self.parse_method_arg_list_no_paren()?
8713                            };
8714                            expr = Expr {
8715                                kind: ExprKind::MethodCall {
8716                                    object: Box::new(expr),
8717                                    method: "x".to_string(),
8718                                    args,
8719                                    super_call: false,
8720                                },
8721                                line,
8722                            };
8723                        }
8724                        _ => break,
8725                    }
8726                }
8727                Token::LBracket => {
8728                    // Implicit semicolon: `[` on a new line is a new statement (array literal),
8729                    // not an array subscript on the preceding expression.
8730                    if self.peek_line() > self.prev_line() {
8731                        break;
8732                    }
8733                    // `$a[i]` — or chained `$r->{k}[i]` / `$a[1][2]` — or list slice `(sort ...)[0]`.
8734                    let line = expr.line;
8735                    if matches!(expr.kind, ExprKind::ScalarVar(_)) {
8736                        if let ExprKind::ScalarVar(ref name) = expr.kind {
8737                            let name = name.clone();
8738                            self.advance();
8739                            // Parse full expression to handle comma operator correctly:
8740                            // `$a[1, 2]` evaluates comma expr (returns last value = 2)
8741                            let index = self.parse_expression()?;
8742                            self.expect(&Token::RBracket)?;
8743                            expr = Expr {
8744                                kind: ExprKind::ArrayElement {
8745                                    array: name,
8746                                    index: Box::new(index),
8747                                },
8748                                line,
8749                            };
8750                        }
8751                    } else if postfix_lbracket_is_arrow_container(&expr) {
8752                        self.advance();
8753                        let indices = self.parse_arg_list()?;
8754                        self.expect(&Token::RBracket)?;
8755                        expr = Expr {
8756                            kind: ExprKind::ArrowDeref {
8757                                expr: Box::new(expr),
8758                                index: Box::new(Expr {
8759                                    kind: ExprKind::List(indices),
8760                                    line,
8761                                }),
8762                                kind: DerefKind::Array,
8763                            },
8764                            line,
8765                        };
8766                    } else {
8767                        self.advance();
8768                        let indices = self.parse_arg_list()?;
8769                        self.expect(&Token::RBracket)?;
8770                        expr = Expr {
8771                            kind: ExprKind::AnonymousListSlice {
8772                                source: Box::new(expr),
8773                                indices,
8774                            },
8775                            line,
8776                        };
8777                    }
8778                }
8779                Token::LBrace => {
8780                    if self.suppress_scalar_hash_brace > 0 {
8781                        break;
8782                    }
8783                    // Implicit semicolon: `{` on a new line is a new statement (block/hashref),
8784                    // not a hash subscript on the preceding expression.
8785                    if self.peek_line() > self.prev_line() {
8786                        break;
8787                    }
8788                    // `$h{k}`, or chained `$h{k2}{k3}` / `$r->{a}{b}` / `$a[0]{k}` — second+ `{…}` is
8789                    // hash subscript on the scalar value (same as `-> { … }` without extra `->`).
8790                    let line = expr.line;
8791                    let is_scalar_named_hash = matches!(expr.kind, ExprKind::ScalarVar(_));
8792                    let is_chainable_hash_subscript = is_scalar_named_hash
8793                        || matches!(
8794                            expr.kind,
8795                            ExprKind::HashElement { .. }
8796                                | ExprKind::ArrayElement { .. }
8797                                | ExprKind::ArrowDeref { .. }
8798                                | ExprKind::Deref {
8799                                    kind: Sigil::Scalar,
8800                                    ..
8801                                }
8802                        );
8803                    if !is_chainable_hash_subscript {
8804                        break;
8805                    }
8806                    self.advance();
8807                    let key = self.parse_hash_subscript_key()?;
8808                    self.expect(&Token::RBrace)?;
8809                    expr = if is_scalar_named_hash {
8810                        if let ExprKind::ScalarVar(ref name) = expr.kind {
8811                            let name = name.clone();
8812                            // Perl: `$_ { k }` means `$_->{k}` (implicit arrow), not the `%_` stash hash.
8813                            if name == "_" {
8814                                Expr {
8815                                    kind: ExprKind::ArrowDeref {
8816                                        expr: Box::new(Expr {
8817                                            kind: ExprKind::ScalarVar("_".into()),
8818                                            line,
8819                                        }),
8820                                        index: Box::new(key),
8821                                        kind: DerefKind::Hash,
8822                                    },
8823                                    line,
8824                                }
8825                            } else {
8826                                Expr {
8827                                    kind: ExprKind::HashElement {
8828                                        hash: name,
8829                                        key: Box::new(key),
8830                                    },
8831                                    line,
8832                                }
8833                            }
8834                        } else {
8835                            unreachable!("is_scalar_named_hash implies ScalarVar");
8836                        }
8837                    } else {
8838                        Expr {
8839                            kind: ExprKind::ArrowDeref {
8840                                expr: Box::new(expr),
8841                                index: Box::new(key),
8842                                kind: DerefKind::Hash,
8843                            },
8844                            line,
8845                        }
8846                    };
8847                }
8848                Token::LogNot | Token::BitNot => {
8849                    // Stryke universal string-subscript sugar — paired `!…!`
8850                    // OR paired `~…~`: `$VAR!N!`, `$VAR~N~`, `$VAR!1:5:2!`,
8851                    // `_!N!`, `_~from:to:step~`. Returns substring of the
8852                    // scalar (Unicode chars).  Distinct from `[N]` which has
8853                    // Perl's `@VAR[N]` / `$_[N]` semantics. Both forms work on
8854                    // any scalar (named or topic) without colliding: `!` and
8855                    // `~` after a value have no current postfix meaning (`!=`
8856                    // / `!~` are pre-merged binary tokens; `~` is prefix-only
8857                    // bit-not). The opening and closing delimiter must match.
8858                    //
8859                    // Implementation: rewrite to ArrayElement with a
8860                    // synthetic name `__topicstr__$NAME`. The interpreter
8861                    // and VM strip the prefix and dispatch to char-of-string
8862                    // (and slice-of-string for Range indices).
8863                    if !matches!(expr.kind, ExprKind::ScalarVar(_)) {
8864                        break;
8865                    }
8866                    if self.peek_line() > self.prev_line() {
8867                        break;
8868                    }
8869                    let opener = self.peek().clone();
8870                    let line = expr.line;
8871                    let name = if let ExprKind::ScalarVar(ref n) = expr.kind {
8872                        n.clone()
8873                    } else {
8874                        unreachable!()
8875                    };
8876                    self.advance(); // consume opening `!` or `~`
8877                                    // Suppress `~` as a range separator while parsing the
8878                                    // paired index — `$_~5~` would otherwise consume the
8879                                    // closing `~` as a range op. `:` is still allowed so
8880                                    // `$_~1:3~` (slice with `:` range index) keeps working.
8881                    self.suppress_tilde_range = self.suppress_tilde_range.saturating_add(1);
8882                    let index_result = self.parse_expression();
8883                    self.suppress_tilde_range = self.suppress_tilde_range.saturating_sub(1);
8884                    let index = index_result?;
8885                    let close_match = matches!(
8886                        (&opener, self.peek()),
8887                        (Token::LogNot, Token::LogNot) | (Token::BitNot, Token::BitNot)
8888                    );
8889                    if !close_match {
8890                        let want = if matches!(opener, Token::LogNot) {
8891                            "!"
8892                        } else {
8893                            "~"
8894                        };
8895                        return Err(self.syntax_err(
8896                            format!("expected closing `{}` for string subscript", want),
8897                            self.peek_line(),
8898                        ));
8899                    }
8900                    self.advance(); // consume closing delimiter
8901                    expr = Expr {
8902                        kind: ExprKind::ArrayElement {
8903                            array: format!("__topicstr__{}", name),
8904                            index: Box::new(index),
8905                        },
8906                        line,
8907                    };
8908                }
8909                _ => break,
8910            }
8911        }
8912        Ok(expr)
8913    }
8914
8915    fn parse_primary(&mut self) -> StrykeResult<Expr> {
8916        let line = self.peek_line();
8917        // `my $x = …` (or `our` / `state` / `local`) used inside an expression —
8918        // typically `if (my $x = …)` / `while (my $line = <FH>)`.  Returns the
8919        // assigned value(s); has the side effect of declaring the variable in
8920        // the current scope.  See `ExprKind::MyExpr`.
8921        if let Token::Ident(ref kw) = self.peek().clone() {
8922            if matches!(kw.as_str(), "my" | "our" | "state" | "local") {
8923                let kw_owned = kw.clone();
8924                // Parse exactly like the statement form via `parse_my_our_local`,
8925                // then unwrap the resulting `StmtKind::*` back into a list of
8926                // `VarDecl`s for the expression node.  This re-uses the full
8927                // syntax (typed sigs, list destructuring, type annotations).
8928                let saved_pos = self.pos;
8929                let stmt = self.parse_my_our_local(&kw_owned, false)?;
8930                let decls = match stmt.kind {
8931                    StmtKind::My(d)
8932                    | StmtKind::Our(d)
8933                    | StmtKind::State(d)
8934                    | StmtKind::Local(d) => d,
8935                    _ => {
8936                        // `local *FOO = …` / non-decl forms — fall back to the
8937                        // statement parser (already advanced); restore position
8938                        // and let the surrounding code handle it as a statement
8939                        // by erroring loudly here.
8940                        self.pos = saved_pos;
8941                        return Err(self.syntax_err(
8942                            "`my`/`our`/`local` in expression must declare variables",
8943                            line,
8944                        ));
8945                    }
8946                };
8947                return Ok(Expr {
8948                    kind: ExprKind::MyExpr {
8949                        keyword: kw_owned,
8950                        decls,
8951                    },
8952                    line,
8953                });
8954            }
8955        }
8956        match self.peek().clone() {
8957            Token::Integer(n) => {
8958                self.advance();
8959                Ok(Expr {
8960                    kind: ExprKind::Integer(n),
8961                    line,
8962                })
8963            }
8964            Token::Float(f) => {
8965                self.advance();
8966                Ok(Expr {
8967                    kind: ExprKind::Float(f),
8968                    line,
8969                })
8970            }
8971            // `>{ BLOCK }` — IIFE block expression (immediately-invoked anonymous sub).
8972            // Valid in any expression position; evaluates the block and yields its last value.
8973            // In thread-macro stage position (`EXPR |>` already consumed by the stage loop in
8974            // `parse_thread_macro`), the explicit branch at ~1417 wins and the block is
8975            // instead pipe-applied as a coderef — that path is never reached from here.
8976            Token::ArrowBrace => {
8977                self.advance();
8978                let mut stmts = Vec::new();
8979                while !matches!(self.peek(), Token::RBrace | Token::Eof) {
8980                    if self.eat(&Token::Semicolon) {
8981                        continue;
8982                    }
8983                    stmts.push(self.parse_statement()?);
8984                }
8985                self.expect(&Token::RBrace)?;
8986                let inner_line = stmts.first().map(|s| s.line).unwrap_or(line);
8987                let inner = Expr {
8988                    kind: ExprKind::CodeRef {
8989                        params: vec![],
8990                        body: stmts,
8991                    },
8992                    line: inner_line,
8993                };
8994                Ok(Expr {
8995                    kind: ExprKind::Do(Box::new(inner)),
8996                    line,
8997                })
8998            }
8999            Token::Star => {
9000                self.advance();
9001                if matches!(self.peek(), Token::LBrace) {
9002                    self.advance();
9003                    let inner = self.parse_expression()?;
9004                    self.expect(&Token::RBrace)?;
9005                    return Ok(Expr {
9006                        kind: ExprKind::Deref {
9007                            expr: Box::new(inner),
9008                            kind: Sigil::Typeglob,
9009                        },
9010                        line,
9011                    });
9012                }
9013                // `*$_{$k}`, `*${expr}`, `*$foo` — typeglob from a sigil expression (Perl 5 `*$globref`).
9014                if matches!(
9015                    self.peek(),
9016                    Token::ScalarVar(_)
9017                        | Token::ArrayVar(_)
9018                        | Token::HashVar(_)
9019                        | Token::DerefScalarVar(_)
9020                        | Token::HashPercent
9021                ) {
9022                    let inner = self.parse_postfix()?;
9023                    return Ok(Expr {
9024                        kind: ExprKind::TypeglobExpr(Box::new(inner)),
9025                        line,
9026                    });
9027                }
9028                // `x` tokenizes as `Token::X` (repeat op) — still a valid package/typeglob name.
9029                let mut full_name = match self.advance() {
9030                    (Token::Ident(n), _) => n,
9031                    (Token::X, _) => "x".to_string(),
9032                    (tok, l) => {
9033                        return Err(self
9034                            .syntax_err(format!("Expected identifier after *, got {:?}", tok), l));
9035                    }
9036                };
9037                while self.eat(&Token::PackageSep) {
9038                    match self.advance() {
9039                        (Token::Ident(part), _) => {
9040                            full_name = format!("{}::{}", full_name, part);
9041                        }
9042                        (Token::X, _) => {
9043                            full_name = format!("{}::x", full_name);
9044                        }
9045                        (tok, l) => {
9046                            return Err(self.syntax_err(
9047                                format!("Expected identifier after :: in typeglob, got {:?}", tok),
9048                                l,
9049                            ));
9050                        }
9051                    }
9052                }
9053                Ok(Expr {
9054                    kind: ExprKind::Typeglob(full_name),
9055                    line,
9056                })
9057            }
9058            Token::SingleString(s) => {
9059                self.advance();
9060                Ok(Expr {
9061                    kind: ExprKind::String(s),
9062                    line,
9063                })
9064            }
9065            Token::DoubleString(s) => {
9066                self.advance();
9067                self.parse_interpolated_string(&s, line)
9068            }
9069            Token::BacktickString(s) => {
9070                self.advance();
9071                let inner = self.parse_interpolated_string(&s, line)?;
9072                Ok(Expr {
9073                    kind: ExprKind::Qx(Box::new(inner)),
9074                    line,
9075                })
9076            }
9077            Token::HereDoc(_, body, interpolate) => {
9078                self.advance();
9079                if interpolate {
9080                    self.parse_interpolated_string(&body, line)
9081                } else {
9082                    Ok(Expr {
9083                        kind: ExprKind::String(body),
9084                        line,
9085                    })
9086                }
9087            }
9088            Token::Regex(pattern, flags, _delim) => {
9089                self.advance();
9090                Ok(Expr {
9091                    kind: ExprKind::Regex(pattern, flags),
9092                    line,
9093                })
9094            }
9095            Token::QW(words) => {
9096                self.advance();
9097                // `qw(a b c) x N` is list-repeat in Perl even without explicit
9098                // outer parens — `qw(...)` is itself a list constructor.
9099                self.list_construct_close_pos = Some(self.pos);
9100                Ok(Expr {
9101                    kind: ExprKind::QW(words),
9102                    line,
9103                })
9104            }
9105            Token::DerefScalarVar(name) => {
9106                self.advance();
9107                Ok(Expr {
9108                    kind: ExprKind::Deref {
9109                        expr: Box::new(Expr {
9110                            kind: ExprKind::ScalarVar(name),
9111                            line,
9112                        }),
9113                        kind: Sigil::Scalar,
9114                    },
9115                    line,
9116                })
9117            }
9118            Token::ScalarVar(name) => {
9119                self.advance();
9120                Ok(Expr {
9121                    kind: ExprKind::ScalarVar(name),
9122                    line,
9123                })
9124            }
9125            Token::ArrayVar(name) => {
9126                self.advance();
9127                // Check for slice: @arr[...] (array slice) or @hash{...} (hash slice)
9128                match self.peek() {
9129                    Token::LBracket => {
9130                        self.advance();
9131                        let indices = self.parse_slice_arg_list(false)?;
9132                        self.expect(&Token::RBracket)?;
9133                        Ok(Expr {
9134                            kind: ExprKind::ArraySlice {
9135                                array: name,
9136                                indices,
9137                            },
9138                            line,
9139                        })
9140                    }
9141                    Token::LBrace if self.suppress_scalar_hash_brace == 0 => {
9142                        self.advance();
9143                        let keys = self.parse_slice_arg_list(true)?;
9144                        self.expect(&Token::RBrace)?;
9145                        Ok(Expr {
9146                            kind: ExprKind::HashSlice { hash: name, keys },
9147                            line,
9148                        })
9149                    }
9150                    _ => Ok(Expr {
9151                        kind: ExprKind::ArrayVar(name),
9152                        line,
9153                    }),
9154                }
9155            }
9156            Token::HashVar(name) => {
9157                self.advance();
9158                // `%h{KEYS}` — Perl 5.20+ key-value slice. Parser-level
9159                // disambiguation: `%h` immediately followed by `{` is a kv-
9160                // slice; `%h` alone (or followed by `=`, list ops, etc.) is
9161                // the bare hash. (BUG-008)
9162                if matches!(self.peek(), Token::LBrace) && self.suppress_scalar_hash_brace == 0 {
9163                    self.advance(); // {
9164                    let keys = self.parse_slice_arg_list(true)?;
9165                    self.expect(&Token::RBrace)?;
9166                    return Ok(Expr {
9167                        kind: ExprKind::HashKvSlice { hash: name, keys },
9168                        line,
9169                    });
9170                }
9171                Ok(Expr {
9172                    kind: ExprKind::HashVar(name),
9173                    line,
9174                })
9175            }
9176            Token::HashPercent => {
9177                // `%$href` — hash ref deref; `%{ $expr }` — symbolic / braced form
9178                self.advance();
9179                if matches!(self.peek(), Token::ScalarVar(_)) {
9180                    let n = match self.advance() {
9181                        (Token::ScalarVar(n), _) => n,
9182                        (tok, l) => {
9183                            return Err(self.syntax_err(
9184                                format!("Expected scalar variable after %%, got {:?}", tok),
9185                                l,
9186                            ));
9187                        }
9188                    };
9189                    return Ok(Expr {
9190                        kind: ExprKind::Deref {
9191                            expr: Box::new(Expr {
9192                                kind: ExprKind::ScalarVar(n),
9193                                line,
9194                            }),
9195                            kind: Sigil::Hash,
9196                        },
9197                        line,
9198                    });
9199                }
9200                // `%[a => 1, b => 2]` — sugar for `%{+{a=>1,b=>2}}`: dereference an
9201                // anonymous hashref inline, using `[...]` as the delimiter to avoid
9202                // the block-vs-hashref ambiguity that `%{a=>1}` has in real Perl.
9203                // Real Perl errors on `%[...]` syntactically, so no compat risk.
9204                if matches!(self.peek(), Token::LBracket) {
9205                    self.advance();
9206                    let pairs = self.parse_hashref_pairs_until(&Token::RBracket)?;
9207                    self.expect(&Token::RBracket)?;
9208                    let href = Expr {
9209                        kind: ExprKind::HashRef(pairs),
9210                        line,
9211                    };
9212                    return Ok(Expr {
9213                        kind: ExprKind::Deref {
9214                            expr: Box::new(href),
9215                            kind: Sigil::Hash,
9216                        },
9217                        line,
9218                    });
9219                }
9220                self.expect(&Token::LBrace)?;
9221                // Peek to disambiguate `%{ $ref }` (deref a hashref expression) from
9222                // `%{ k => v }` (inline hash literal). Real Perl's block-vs-hashref
9223                // heuristic is famously unreliable — when the first non-whitespace
9224                // token is an ident/string followed by `=>`, treat the whole thing
9225                // as a hashref literal to make `%{a=>1,b=>2}` work reliably.
9226                let looks_like_pair = matches!(
9227                    self.peek(),
9228                    Token::Ident(_) | Token::SingleString(_) | Token::DoubleString(_)
9229                ) && matches!(self.peek_at(1), Token::FatArrow);
9230                let inner = if looks_like_pair {
9231                    let pairs = self.parse_hashref_pairs_until(&Token::RBrace)?;
9232                    Expr {
9233                        kind: ExprKind::HashRef(pairs),
9234                        line,
9235                    }
9236                } else {
9237                    self.parse_expression()?
9238                };
9239                self.expect(&Token::RBrace)?;
9240                Ok(Expr {
9241                    kind: ExprKind::Deref {
9242                        expr: Box::new(inner),
9243                        kind: Sigil::Hash,
9244                    },
9245                    line,
9246                })
9247            }
9248            Token::ArrayAt => {
9249                self.advance();
9250                // `@{ $expr }` / `@{ "Pkg::NAME" }` — symbolic array (e.g. `@{"$pkg\::EXPORT"}` in Exporter.pm)
9251                if matches!(self.peek(), Token::LBrace) {
9252                    self.advance();
9253                    let inner = self.parse_expression()?;
9254                    self.expect(&Token::RBrace)?;
9255                    // `@{$href}{k1,k2}` — hash slice through a hashref using
9256                    // the curly-brace deref form. Mirrors the `@$href{KEYS}`
9257                    // path (BUG-091/BUG-217). Likewise `@{$aref}[i,j]` is the
9258                    // array-slice-through-arrayref form.
9259                    if matches!(self.peek(), Token::LBrace) {
9260                        self.advance();
9261                        let keys = self.parse_slice_arg_list(true)?;
9262                        self.expect(&Token::RBrace)?;
9263                        return Ok(Expr {
9264                            kind: ExprKind::HashSliceDeref {
9265                                container: Box::new(inner),
9266                                keys,
9267                            },
9268                            line,
9269                        });
9270                    }
9271                    if matches!(self.peek(), Token::LBracket) {
9272                        self.advance();
9273                        let indices = self.parse_slice_arg_list(false)?;
9274                        self.expect(&Token::RBracket)?;
9275                        let source = Expr {
9276                            kind: ExprKind::Deref {
9277                                expr: Box::new(inner),
9278                                kind: Sigil::Array,
9279                            },
9280                            line,
9281                        };
9282                        return Ok(Expr {
9283                            kind: ExprKind::AnonymousListSlice {
9284                                source: Box::new(source),
9285                                indices,
9286                            },
9287                            line,
9288                        });
9289                    }
9290                    return Ok(Expr {
9291                        kind: ExprKind::Deref {
9292                            expr: Box::new(inner),
9293                            kind: Sigil::Array,
9294                        },
9295                        line,
9296                    });
9297                }
9298                // `@[a, b, c]` — sugar for `@{[a, b, c]}`: dereference an
9299                // anonymous arrayref inline. Real Perl rejects `@[...]` at
9300                // the parser level, so this extension has no compat risk.
9301                if matches!(self.peek(), Token::LBracket) {
9302                    self.advance();
9303                    let mut elems = Vec::new();
9304                    if !matches!(self.peek(), Token::RBracket) {
9305                        elems.push(self.parse_assign_expr()?);
9306                        while self.eat(&Token::Comma) {
9307                            if matches!(self.peek(), Token::RBracket) {
9308                                break;
9309                            }
9310                            elems.push(self.parse_assign_expr()?);
9311                        }
9312                    }
9313                    self.expect(&Token::RBracket)?;
9314                    let aref = Expr {
9315                        kind: ExprKind::ArrayRef(elems),
9316                        line,
9317                    };
9318                    return Ok(Expr {
9319                        kind: ExprKind::Deref {
9320                            expr: Box::new(aref),
9321                            kind: Sigil::Array,
9322                        },
9323                        line,
9324                    });
9325                }
9326                // `@$arr` — array dereference; `@$h{k1,k2}` — hash slice via hashref
9327                let container = match self.peek().clone() {
9328                    Token::ScalarVar(n) => {
9329                        self.advance();
9330                        Expr {
9331                            kind: ExprKind::ScalarVar(n),
9332                            line,
9333                        }
9334                    }
9335                    _ => {
9336                        return Err(self.syntax_err(
9337                            "Expected `$name`, `{`, or `[` after `@` (e.g. `@$aref`, `@{expr}`, `@[1,2,3]`, or `@$href{keys}`)",
9338                            line,
9339                        ));
9340                    }
9341                };
9342                if matches!(self.peek(), Token::LBrace) {
9343                    self.advance();
9344                    let keys = self.parse_slice_arg_list(true)?;
9345                    self.expect(&Token::RBrace)?;
9346                    return Ok(Expr {
9347                        kind: ExprKind::HashSliceDeref {
9348                            container: Box::new(container),
9349                            keys,
9350                        },
9351                        line,
9352                    });
9353                }
9354                Ok(Expr {
9355                    kind: ExprKind::Deref {
9356                        expr: Box::new(container),
9357                        kind: Sigil::Array,
9358                    },
9359                    line,
9360                })
9361            }
9362            Token::LParen => {
9363                self.advance();
9364                if matches!(self.peek(), Token::RParen) {
9365                    self.advance();
9366                    // Empty `() x 3` is a no-op list repeat — record the close
9367                    // position so `Token::X` knows the LHS was a list literal.
9368                    self.list_construct_close_pos = Some(self.pos);
9369                    return Ok(Expr {
9370                        kind: ExprKind::List(vec![]),
9371                        line,
9372                    });
9373                }
9374                // Inside parens, pipe-forward is allowed even if we're in a
9375                // paren-less arg context. Save and restore no_pipe_forward_depth.
9376                let saved_no_pipe = self.no_pipe_forward_depth;
9377                self.no_pipe_forward_depth = 0;
9378                // Thread-macro `on` may set `suppress_indirect_paren_call` so
9379                // `on $c ()` does not steal `()`; inside explicit `(...)` use
9380                // normal postfix-`(` rules (`on ($factory())`).
9381                let saved_indirect = self.suppress_indirect_paren_call;
9382                self.suppress_indirect_paren_call = 0;
9383                let expr = self.parse_expression();
9384                self.no_pipe_forward_depth = saved_no_pipe;
9385                self.suppress_indirect_paren_call = saved_indirect;
9386                let expr = expr?;
9387                self.expect(&Token::RParen)?;
9388                // Mark this paren as a list-constructor for the `x` operator
9389                // (parse_multiplication compares `self.pos` at the X token to
9390                // this checkpoint). Function-call parens (`f(args)`) don't
9391                // reach this branch; they're parsed by the call machinery.
9392                self.list_construct_close_pos = Some(self.pos);
9393                Ok(expr)
9394            }
9395            Token::LBracket => {
9396                self.advance();
9397                let elems = self.parse_arg_list()?;
9398                self.expect(&Token::RBracket)?;
9399                Ok(Expr {
9400                    kind: ExprKind::ArrayRef(elems),
9401                    line,
9402                })
9403            }
9404            Token::LBrace => {
9405                // Could be hash ref or block — disambiguate
9406                self.advance();
9407                // Try to parse as hash ref: { key => val, ... }
9408                let saved = self.pos;
9409                match self.try_parse_hash_ref() {
9410                    Ok(pairs) => Ok(Expr {
9411                        kind: ExprKind::HashRef(pairs),
9412                        line,
9413                    }),
9414                    Err(_) => {
9415                        self.pos = saved;
9416                        // Parse as block, wrap in code ref
9417                        let mut stmts = Vec::new();
9418                        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
9419                            if self.eat(&Token::Semicolon) {
9420                                continue;
9421                            }
9422                            stmts.push(self.parse_statement()?);
9423                        }
9424                        self.expect(&Token::RBrace)?;
9425                        Ok(Expr {
9426                            kind: ExprKind::CodeRef {
9427                                params: vec![],
9428                                body: stmts,
9429                            },
9430                            line,
9431                        })
9432                    }
9433                }
9434            }
9435            Token::Diamond => {
9436                self.advance();
9437                Ok(Expr {
9438                    kind: ExprKind::ReadLine(None),
9439                    line,
9440                })
9441            }
9442            Token::ReadLine(handle) => {
9443                self.advance();
9444                Ok(Expr {
9445                    kind: ExprKind::ReadLine(Some(handle)),
9446                    line,
9447                })
9448            }
9449
9450            // Named functions / builtins
9451            Token::ThreadArrow => {
9452                self.advance();
9453                self.parse_thread_macro(line, false)
9454            }
9455            Token::ThreadArrowLast => {
9456                self.advance();
9457                self.parse_thread_macro(line, true)
9458            }
9459            Token::ThreadArrowStream => {
9460                self.advance();
9461                let mut stages = Vec::new();
9462                self.parse_thread_macro_inner(line, false, Some(&mut stages))
9463            }
9464            Token::ThreadArrowStreamLast => {
9465                self.advance();
9466                let mut stages = Vec::new();
9467                self.parse_thread_macro_inner(line, true, Some(&mut stages))
9468            }
9469            Token::ThreadArrowPar => {
9470                self.advance();
9471                self.parse_thread_macro_chunk_par(line, false)
9472            }
9473            Token::ThreadArrowParLast => {
9474                self.advance();
9475                self.parse_thread_macro_chunk_par(line, true)
9476            }
9477            Token::ThreadArrowDist => {
9478                self.advance();
9479                self.parse_thread_macro_dist(line, false)
9480            }
9481            Token::ThreadArrowDistLast => {
9482                self.advance();
9483                self.parse_thread_macro_dist(line, true)
9484            }
9485            Token::Ident(ref name) => {
9486                let name = name.clone();
9487                // Handle s///
9488                if name.starts_with('\x00') {
9489                    self.advance();
9490                    let parts: Vec<&str> = name.split('\x00').collect();
9491                    if parts.len() >= 4 && parts[1] == "s" {
9492                        let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
9493                        return Ok(Expr {
9494                            kind: ExprKind::Substitution {
9495                                expr: Box::new(Expr {
9496                                    kind: ExprKind::ScalarVar("_".into()),
9497                                    line,
9498                                }),
9499                                pattern: parts[2].to_string(),
9500                                replacement: parts[3].to_string(),
9501                                flags: parts.get(4).unwrap_or(&"").to_string(),
9502                                delim,
9503                            },
9504                            line,
9505                        });
9506                    }
9507                    if parts.len() >= 4 && parts[1] == "tr" {
9508                        let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
9509                        return Ok(Expr {
9510                            kind: ExprKind::Transliterate {
9511                                expr: Box::new(Expr {
9512                                    kind: ExprKind::ScalarVar("_".into()),
9513                                    line,
9514                                }),
9515                                from: parts[2].to_string(),
9516                                to: parts[3].to_string(),
9517                                flags: parts.get(4).unwrap_or(&"").to_string(),
9518                                delim,
9519                            },
9520                            line,
9521                        });
9522                    }
9523                    return Err(self.syntax_err("Unexpected encoded token", line));
9524                }
9525                self.parse_named_expr(name)
9526            }
9527
9528            // `%name` when lexer emitted `Token::Percent` (due to preceding term context)
9529            // instead of `Token::HashVar`. This happens after `t` (thread macro) etc.
9530            Token::Percent => {
9531                self.advance();
9532                match self.peek().clone() {
9533                    Token::Ident(name) => {
9534                        self.advance();
9535                        Ok(Expr {
9536                            kind: ExprKind::HashVar(name),
9537                            line,
9538                        })
9539                    }
9540                    Token::ScalarVar(n) => {
9541                        self.advance();
9542                        Ok(Expr {
9543                            kind: ExprKind::Deref {
9544                                expr: Box::new(Expr {
9545                                    kind: ExprKind::ScalarVar(n),
9546                                    line,
9547                                }),
9548                                kind: Sigil::Hash,
9549                            },
9550                            line,
9551                        })
9552                    }
9553                    Token::LBrace => {
9554                        self.advance();
9555                        let looks_like_pair = matches!(
9556                            self.peek(),
9557                            Token::Ident(_) | Token::SingleString(_) | Token::DoubleString(_)
9558                        ) && matches!(self.peek_at(1), Token::FatArrow);
9559                        let inner = if looks_like_pair {
9560                            let pairs = self.parse_hashref_pairs_until(&Token::RBrace)?;
9561                            Expr {
9562                                kind: ExprKind::HashRef(pairs),
9563                                line,
9564                            }
9565                        } else {
9566                            self.parse_expression()?
9567                        };
9568                        self.expect(&Token::RBrace)?;
9569                        Ok(Expr {
9570                            kind: ExprKind::Deref {
9571                                expr: Box::new(inner),
9572                                kind: Sigil::Hash,
9573                            },
9574                            line,
9575                        })
9576                    }
9577                    Token::LBracket => {
9578                        self.advance();
9579                        let pairs = self.parse_hashref_pairs_until(&Token::RBracket)?;
9580                        self.expect(&Token::RBracket)?;
9581                        let href = Expr {
9582                            kind: ExprKind::HashRef(pairs),
9583                            line,
9584                        };
9585                        Ok(Expr {
9586                            kind: ExprKind::Deref {
9587                                expr: Box::new(href),
9588                                kind: Sigil::Hash,
9589                            },
9590                            line,
9591                        })
9592                    }
9593                    tok => Err(self.syntax_err(
9594                        format!(
9595                            "Expected identifier, `$`, `{{`, or `[` after `%`, got {:?}",
9596                            tok
9597                        ),
9598                        line,
9599                    )),
9600                }
9601            }
9602
9603            tok => Err(self.syntax_err(format!("Unexpected token {:?}", tok), line)),
9604        }
9605    }
9606
9607    fn parse_named_expr(&mut self, mut name: String) -> StrykeResult<Expr> {
9608        let line = self.peek_line();
9609        self.advance(); // consume the ident
9610        while self.eat(&Token::PackageSep) {
9611            match self.advance() {
9612                (Token::Ident(part), _) => {
9613                    name = format!("{}::{}", name, part);
9614                }
9615                (tok, err_line) => {
9616                    return Err(self.syntax_err(
9617                        format!("Expected identifier after `::`, got {:?}", tok),
9618                        err_line,
9619                    ));
9620                }
9621            }
9622        }
9623
9624        // Fat-arrow auto-quoting: ANY bareword (including keywords/builtins)
9625        // before `=>` is treated as a string key, matching Perl 5 semantics.
9626        // e.g. `(print => 1, pr => "x", sort => 3)` are all valid hash pairs.
9627        // Stryke exception: topic-slot barewords (`_`, `_<`, `_0`, `_0<`, …) are
9628        // scalar references to the topic / positional / outer-topic chain — they
9629        // must evaluate as the topic value, not the literal name.
9630        if matches!(self.peek(), Token::FatArrow) && !Self::is_underscore_topic_slot(&name) {
9631            return Ok(Expr {
9632                kind: ExprKind::String(name),
9633                line,
9634            });
9635        }
9636
9637        if crate::compat_mode() {
9638            if let Some(ext) = Self::stryke_extension_name(&name) {
9639                if !self.declared_subs.contains(&name) {
9640                    return Err(self.syntax_err(
9641                        format!("`{ext}` is a stryke extension (disabled by --compat)"),
9642                        line,
9643                    ));
9644                }
9645            }
9646        }
9647
9648        // `CORE::length(...)` etc. — strip the explicit core-dispatch prefix so
9649        // the keyword arms below match the bare name and produce the same
9650        // `ExprKind::Length` / `ExprKind::Print` / etc. as the unprefixed form.
9651        // Matches Perl 5's `CORE::` namespace, which routes back to the
9652        // built-in implementation regardless of any same-named user sub.
9653        // (PARITY-011)
9654        if let Some(rest) = name.strip_prefix("CORE::") {
9655            name = rest.to_string();
9656        }
9657
9658        match name.as_str() {
9659            "__FILE__" => Ok(Expr {
9660                kind: ExprKind::MagicConst(MagicConstKind::File),
9661                line,
9662            }),
9663            "__LINE__" => Ok(Expr {
9664                kind: ExprKind::MagicConst(MagicConstKind::Line),
9665                line,
9666            }),
9667            "__SUB__" => Ok(Expr {
9668                kind: ExprKind::MagicConst(MagicConstKind::Sub),
9669                line,
9670            }),
9671            // `__PACKAGE__` is a compile-time constant set to the currently
9672            // active package, so a sub body in `package Demo::P1` keeps
9673            // returning `"Demo::P1"` regardless of the caller's package
9674            // (Perl 5 documented behavior).
9675            "__PACKAGE__" => Ok(Expr {
9676                kind: ExprKind::String(self.current_package.clone()),
9677                line,
9678            }),
9679            "stdin" => Ok(Expr {
9680                kind: ExprKind::FuncCall {
9681                    name: "stdin".into(),
9682                    args: vec![],
9683                },
9684                line,
9685            }),
9686            "range" => {
9687                let args = self.parse_builtin_args()?;
9688                Ok(Expr {
9689                    kind: ExprKind::FuncCall {
9690                        name: "range".into(),
9691                        args,
9692                    },
9693                    line,
9694                })
9695            }
9696            "print" | "pr" => self.parse_print_like(|h, a| ExprKind::Print { handle: h, args: a }),
9697            "say" => {
9698                if crate::no_interop_mode() {
9699                    return Err(
9700                        self.syntax_err("stryke uses `p` instead of `say` (--no-interop)", line)
9701                    );
9702                }
9703                self.parse_print_like(|h, a| ExprKind::Say { handle: h, args: a })
9704            }
9705            "p" => self.parse_print_like(|h, a| ExprKind::Say { handle: h, args: a }),
9706            "printf" => self.parse_print_like(|h, a| ExprKind::Printf { handle: h, args: a }),
9707            "die" => {
9708                let args = self.parse_list_until_terminator()?;
9709                Ok(Expr {
9710                    kind: ExprKind::Die(args),
9711                    line,
9712                })
9713            }
9714            "warn" => {
9715                let args = self.parse_list_until_terminator()?;
9716                Ok(Expr {
9717                    kind: ExprKind::Warn(args),
9718                    line,
9719                })
9720            }
9721            // `croak` / `confess` — `Carp` builtins available without `use Carp`
9722            // (matches the doc claim in `lsp.rs:1243`). For now both desugar to
9723            // `die` — TODO: croak should report caller's file/line, confess
9724            // should append a full stack trace.
9725            "croak" | "confess" => {
9726                let args = self.parse_list_until_terminator()?;
9727                Ok(Expr {
9728                    kind: ExprKind::Die(args),
9729                    line,
9730                })
9731            }
9732            // `carp` / `cluck` — `Carp` warning siblings of `croak`/`confess`.
9733            "carp" | "cluck" => {
9734                let args = self.parse_list_until_terminator()?;
9735                Ok(Expr {
9736                    kind: ExprKind::Warn(args),
9737                    line,
9738                })
9739            }
9740            "chomp" => {
9741                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9742                    return Ok(e);
9743                }
9744                let a = self.parse_one_arg_or_default()?;
9745                Ok(Expr {
9746                    kind: ExprKind::Chomp(Box::new(a)),
9747                    line,
9748                })
9749            }
9750            "chop" => {
9751                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9752                    return Ok(e);
9753                }
9754                let a = self.parse_one_arg_or_default()?;
9755                Ok(Expr {
9756                    kind: ExprKind::Chop(Box::new(a)),
9757                    line,
9758                })
9759            }
9760            "length" => {
9761                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9762                    return Ok(e);
9763                }
9764                let a = self.parse_one_arg_or_default()?;
9765                Ok(Expr {
9766                    kind: ExprKind::Length(Box::new(a)),
9767                    line,
9768                })
9769            }
9770            "defined" => {
9771                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9772                    return Ok(e);
9773                }
9774                // Named-unary precedence: `defined X && Y` is `(defined X) && Y`,
9775                // not `defined(X && Y)`. The default `parse_one_arg_or_default`
9776                // path is greedy (calls `parse_assign_expr_stop_at_pipe`), which
9777                // would let `&&` bind into the argument and silently make
9778                // `defined $h{k} && $h{k} > 0`-style guards always-true when the
9779                // hash element existed. `parse_named_unary_arg` stops at shift
9780                // level so logical operators stay outside.
9781                let a = if matches!(
9782                    self.peek(),
9783                    Token::Semicolon
9784                        | Token::RBrace
9785                        | Token::RParen
9786                        | Token::RBracket
9787                        | Token::Eof
9788                        | Token::Comma
9789                        | Token::FatArrow
9790                        | Token::PipeForward
9791                        | Token::Question
9792                        | Token::Colon
9793                        | Token::NumEq
9794                        | Token::NumNe
9795                        | Token::NumLt
9796                        | Token::NumGt
9797                        | Token::NumLe
9798                        | Token::NumGe
9799                        | Token::Spaceship
9800                        | Token::StrEq
9801                        | Token::StrNe
9802                        | Token::StrLt
9803                        | Token::StrGt
9804                        | Token::StrLe
9805                        | Token::StrGe
9806                        | Token::StrCmp
9807                        | Token::LogAnd
9808                        | Token::LogOr
9809                        | Token::LogNot
9810                        | Token::LogAndWord
9811                        | Token::LogOrWord
9812                        | Token::LogNotWord
9813                        | Token::DefinedOr
9814                        | Token::Range
9815                        | Token::RangeExclusive
9816                        | Token::Assign
9817                        | Token::PlusAssign
9818                        | Token::MinusAssign
9819                        | Token::MulAssign
9820                        | Token::DivAssign
9821                        | Token::ModAssign
9822                        | Token::PowAssign
9823                        | Token::DotAssign
9824                        | Token::AndAssign
9825                        | Token::OrAssign
9826                        | Token::XorAssign
9827                        | Token::DefinedOrAssign
9828                        | Token::ShiftLeftAssign
9829                        | Token::ShiftRightAssign
9830                        | Token::BitAndAssign
9831                        | Token::BitOrAssign
9832                ) {
9833                    Expr {
9834                        kind: ExprKind::ScalarVar("_".into()),
9835                        line: self.peek_line(),
9836                    }
9837                } else if matches!(self.peek(), Token::LParen)
9838                    && matches!(self.peek_at(1), Token::RParen)
9839                {
9840                    let pl = self.peek_line();
9841                    self.advance();
9842                    self.advance();
9843                    Expr {
9844                        kind: ExprKind::ScalarVar("_".into()),
9845                        line: pl,
9846                    }
9847                } else {
9848                    self.parse_named_unary_arg()?
9849                };
9850                Ok(Expr {
9851                    kind: ExprKind::Defined(Box::new(a)),
9852                    line,
9853                })
9854            }
9855            "ref" => {
9856                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9857                    return Ok(e);
9858                }
9859                let a = self.parse_one_arg_or_default()?;
9860                Ok(Expr {
9861                    kind: ExprKind::Ref(Box::new(a)),
9862                    line,
9863                })
9864            }
9865            "undef" => {
9866                // `undef $var` sets `$var` to undef — but a variable on a new line
9867                // is a separate statement (implicit semicolon), not an argument.
9868                if self.peek_line() == self.prev_line()
9869                    && matches!(
9870                        self.peek(),
9871                        Token::ScalarVar(_) | Token::ArrayVar(_) | Token::HashVar(_)
9872                    )
9873                {
9874                    let target = self.parse_primary()?;
9875                    return Ok(Expr {
9876                        kind: ExprKind::Assign {
9877                            target: Box::new(target),
9878                            value: Box::new(Expr {
9879                                kind: ExprKind::Undef,
9880                                line,
9881                            }),
9882                        },
9883                        line,
9884                    });
9885                }
9886                Ok(Expr {
9887                    kind: ExprKind::Undef,
9888                    line,
9889                })
9890            }
9891            "scalar" => {
9892                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9893                    return Ok(e);
9894                }
9895                if crate::no_interop_mode() {
9896                    return Err(self.syntax_err(
9897                        "stryke uses `len` (also `cnt` / `count`) instead of `scalar` (--no-interop)",
9898                        line,
9899                    ));
9900                }
9901                let a = self.parse_one_arg_or_default()?;
9902                Ok(Expr {
9903                    kind: ExprKind::ScalarContext(Box::new(a)),
9904                    line,
9905                })
9906            }
9907            "abs" => {
9908                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9909                    return Ok(e);
9910                }
9911                let a = self.parse_one_arg_or_default()?;
9912                Ok(Expr {
9913                    kind: ExprKind::Abs(Box::new(a)),
9914                    line,
9915                })
9916            }
9917            // stryke unary numeric extensions — treat like `abs` so a bare
9918            // identifier in `map { inc }` / `for (…) { p inc }` becomes a
9919            // call with implicit `$_` rather than falling through to the
9920            // generic `Bareword` arm (which stringifies to `"inc"`).
9921            "inc" | "dec" => {
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::FuncCall {
9928                        name,
9929                        args: vec![a],
9930                    },
9931                    line,
9932                })
9933            }
9934            "int" => {
9935                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9936                    return Ok(e);
9937                }
9938                let a = self.parse_one_arg_or_default()?;
9939                Ok(Expr {
9940                    kind: ExprKind::Int(Box::new(a)),
9941                    line,
9942                })
9943            }
9944            "sqrt" => {
9945                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9946                    return Ok(e);
9947                }
9948                let a = self.parse_one_arg_or_default()?;
9949                Ok(Expr {
9950                    kind: ExprKind::Sqrt(Box::new(a)),
9951                    line,
9952                })
9953            }
9954            "sin" => {
9955                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9956                    return Ok(e);
9957                }
9958                let a = self.parse_one_arg_or_default()?;
9959                Ok(Expr {
9960                    kind: ExprKind::Sin(Box::new(a)),
9961                    line,
9962                })
9963            }
9964            "cos" => {
9965                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9966                    return Ok(e);
9967                }
9968                let a = self.parse_one_arg_or_default()?;
9969                Ok(Expr {
9970                    kind: ExprKind::Cos(Box::new(a)),
9971                    line,
9972                })
9973            }
9974            "atan2" => {
9975                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9976                    return Ok(e);
9977                }
9978                let args = self.parse_builtin_args()?;
9979                if args.len() != 2 {
9980                    return Err(self.syntax_err("atan2 requires two arguments", line));
9981                }
9982                Ok(Expr {
9983                    kind: ExprKind::Atan2 {
9984                        y: Box::new(args[0].clone()),
9985                        x: Box::new(args[1].clone()),
9986                    },
9987                    line,
9988                })
9989            }
9990            "exp" => {
9991                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
9992                    return Ok(e);
9993                }
9994                let a = self.parse_one_arg_or_default()?;
9995                Ok(Expr {
9996                    kind: ExprKind::Exp(Box::new(a)),
9997                    line,
9998                })
9999            }
10000            "log" => {
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::Log(Box::new(a)),
10007                    line,
10008                })
10009            }
10010            "input" => {
10011                let args = if matches!(
10012                    self.peek(),
10013                    Token::Semicolon
10014                        | Token::RBrace
10015                        | Token::RParen
10016                        | Token::Eof
10017                        | Token::Comma
10018                        | Token::PipeForward
10019                ) {
10020                    vec![]
10021                } else if matches!(self.peek(), Token::LParen) {
10022                    self.advance();
10023                    if matches!(self.peek(), Token::RParen) {
10024                        self.advance();
10025                        vec![]
10026                    } else {
10027                        let a = self.parse_expression()?;
10028                        self.expect(&Token::RParen)?;
10029                        vec![a]
10030                    }
10031                } else {
10032                    let a = self.parse_one_arg()?;
10033                    vec![a]
10034                };
10035                Ok(Expr {
10036                    kind: ExprKind::FuncCall {
10037                        name: "input".to_string(),
10038                        args,
10039                    },
10040                    line,
10041                })
10042            }
10043            "rand" => {
10044                if matches!(
10045                    self.peek(),
10046                    Token::Semicolon
10047                        | Token::RBrace
10048                        | Token::RParen
10049                        | Token::Eof
10050                        | Token::Comma
10051                        | Token::PipeForward
10052                ) {
10053                    Ok(Expr {
10054                        kind: ExprKind::Rand(None),
10055                        line,
10056                    })
10057                } else if matches!(self.peek(), Token::LParen) {
10058                    self.advance();
10059                    if matches!(self.peek(), Token::RParen) {
10060                        self.advance();
10061                        Ok(Expr {
10062                            kind: ExprKind::Rand(None),
10063                            line,
10064                        })
10065                    } else {
10066                        let a = self.parse_expression()?;
10067                        self.expect(&Token::RParen)?;
10068                        Ok(Expr {
10069                            kind: ExprKind::Rand(Some(Box::new(a))),
10070                            line,
10071                        })
10072                    }
10073                } else {
10074                    let a = self.parse_one_arg()?;
10075                    Ok(Expr {
10076                        kind: ExprKind::Rand(Some(Box::new(a))),
10077                        line,
10078                    })
10079                }
10080            }
10081            "srand" => {
10082                if matches!(
10083                    self.peek(),
10084                    Token::Semicolon
10085                        | Token::RBrace
10086                        | Token::RParen
10087                        | Token::Eof
10088                        | Token::Comma
10089                        | Token::PipeForward
10090                ) {
10091                    Ok(Expr {
10092                        kind: ExprKind::Srand(None),
10093                        line,
10094                    })
10095                } else if matches!(self.peek(), Token::LParen) {
10096                    self.advance();
10097                    if matches!(self.peek(), Token::RParen) {
10098                        self.advance();
10099                        Ok(Expr {
10100                            kind: ExprKind::Srand(None),
10101                            line,
10102                        })
10103                    } else {
10104                        let a = self.parse_expression()?;
10105                        self.expect(&Token::RParen)?;
10106                        Ok(Expr {
10107                            kind: ExprKind::Srand(Some(Box::new(a))),
10108                            line,
10109                        })
10110                    }
10111                } else {
10112                    let a = self.parse_one_arg()?;
10113                    Ok(Expr {
10114                        kind: ExprKind::Srand(Some(Box::new(a))),
10115                        line,
10116                    })
10117                }
10118            }
10119            "hex" => {
10120                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10121                    return Ok(e);
10122                }
10123                let a = self.parse_one_arg_or_default()?;
10124                Ok(Expr {
10125                    kind: ExprKind::Hex(Box::new(a)),
10126                    line,
10127                })
10128            }
10129            "oct" => {
10130                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10131                    return Ok(e);
10132                }
10133                let a = self.parse_one_arg_or_default()?;
10134                Ok(Expr {
10135                    kind: ExprKind::Oct(Box::new(a)),
10136                    line,
10137                })
10138            }
10139            "chr" => {
10140                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10141                    return Ok(e);
10142                }
10143                let a = self.parse_one_arg_or_default()?;
10144                Ok(Expr {
10145                    kind: ExprKind::Chr(Box::new(a)),
10146                    line,
10147                })
10148            }
10149            "ord" => {
10150                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10151                    return Ok(e);
10152                }
10153                let a = self.parse_one_arg_or_default()?;
10154                Ok(Expr {
10155                    kind: ExprKind::Ord(Box::new(a)),
10156                    line,
10157                })
10158            }
10159            "lc" => {
10160                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10161                    return Ok(e);
10162                }
10163                let a = self.parse_one_arg_or_default()?;
10164                Ok(Expr {
10165                    kind: ExprKind::Lc(Box::new(a)),
10166                    line,
10167                })
10168            }
10169            "uc" => {
10170                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10171                    return Ok(e);
10172                }
10173                let a = self.parse_one_arg_or_default()?;
10174                Ok(Expr {
10175                    kind: ExprKind::Uc(Box::new(a)),
10176                    line,
10177                })
10178            }
10179            "lcfirst" => {
10180                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10181                    return Ok(e);
10182                }
10183                let a = self.parse_one_arg_or_default()?;
10184                Ok(Expr {
10185                    kind: ExprKind::Lcfirst(Box::new(a)),
10186                    line,
10187                })
10188            }
10189            "ucfirst" => {
10190                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10191                    return Ok(e);
10192                }
10193                let a = self.parse_one_arg_or_default()?;
10194                Ok(Expr {
10195                    kind: ExprKind::Ucfirst(Box::new(a)),
10196                    line,
10197                })
10198            }
10199            "fc" => {
10200                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10201                    return Ok(e);
10202                }
10203                let a = self.parse_one_arg_or_default()?;
10204                Ok(Expr {
10205                    kind: ExprKind::Fc(Box::new(a)),
10206                    line,
10207                })
10208            }
10209            "crypt" => {
10210                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10211                    return Ok(e);
10212                }
10213                let args = self.parse_builtin_args()?;
10214                if args.len() != 2 {
10215                    return Err(self.syntax_err("crypt requires two arguments", line));
10216                }
10217                Ok(Expr {
10218                    kind: ExprKind::Crypt {
10219                        plaintext: Box::new(args[0].clone()),
10220                        salt: Box::new(args[1].clone()),
10221                    },
10222                    line,
10223                })
10224            }
10225            "pos" => {
10226                if matches!(
10227                    self.peek(),
10228                    Token::Semicolon
10229                        | Token::RBrace
10230                        | Token::RParen
10231                        | Token::Eof
10232                        | Token::Comma
10233                        | Token::PipeForward
10234                ) {
10235                    Ok(Expr {
10236                        kind: ExprKind::Pos(None),
10237                        line,
10238                    })
10239                } else if matches!(self.peek(), Token::Assign) {
10240                    // Perl: `pos = EXPR` is `pos($_) = EXPR` (Text::Balanced `_eb_delims`).
10241                    self.advance();
10242                    let rhs = self.parse_assign_expr()?;
10243                    Ok(Expr {
10244                        kind: ExprKind::Assign {
10245                            target: Box::new(Expr {
10246                                kind: ExprKind::Pos(Some(Box::new(Expr {
10247                                    kind: ExprKind::ScalarVar("_".into()),
10248                                    line,
10249                                }))),
10250                                line,
10251                            }),
10252                            value: Box::new(rhs),
10253                        },
10254                        line,
10255                    })
10256                } else if matches!(self.peek(), Token::LParen) {
10257                    self.advance();
10258                    if matches!(self.peek(), Token::RParen) {
10259                        self.advance();
10260                        Ok(Expr {
10261                            kind: ExprKind::Pos(None),
10262                            line,
10263                        })
10264                    } else {
10265                        let a = self.parse_expression()?;
10266                        self.expect(&Token::RParen)?;
10267                        Ok(Expr {
10268                            kind: ExprKind::Pos(Some(Box::new(a))),
10269                            line,
10270                        })
10271                    }
10272                } else {
10273                    let saved = self.pos;
10274                    let subj = self.parse_unary()?;
10275                    if matches!(self.peek(), Token::Assign) {
10276                        self.advance();
10277                        let rhs = self.parse_assign_expr()?;
10278                        Ok(Expr {
10279                            kind: ExprKind::Assign {
10280                                target: Box::new(Expr {
10281                                    kind: ExprKind::Pos(Some(Box::new(subj))),
10282                                    line,
10283                                }),
10284                                value: Box::new(rhs),
10285                            },
10286                            line,
10287                        })
10288                    } else {
10289                        self.pos = saved;
10290                        let a = self.parse_one_arg()?;
10291                        Ok(Expr {
10292                            kind: ExprKind::Pos(Some(Box::new(a))),
10293                            line,
10294                        })
10295                    }
10296                }
10297            }
10298            "study" => {
10299                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10300                    return Ok(e);
10301                }
10302                let a = self.parse_one_arg_or_default()?;
10303                Ok(Expr {
10304                    kind: ExprKind::Study(Box::new(a)),
10305                    line,
10306                })
10307            }
10308            "push" => {
10309                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10310                    return Ok(e);
10311                }
10312                let args = self.parse_builtin_args()?;
10313                let (first, rest) = args
10314                    .split_first()
10315                    .ok_or_else(|| self.syntax_err("push requires arguments", line))?;
10316                // Perl 5.24+ rejects `push SCALAR, ...` at parse time. Reject any
10317                // first arg that is unambiguously a scalar (literal scalar var or
10318                // numeric/string literal). Array refs (`@$x`), bindings, slices,
10319                // and `our @a` style remain permitted.
10320                if matches!(
10321                    first.kind,
10322                    ExprKind::ScalarVar(_)
10323                        | ExprKind::Integer(_)
10324                        | ExprKind::Float(_)
10325                        | ExprKind::String(_)
10326                ) {
10327                    return Err(self
10328                        .syntax_err("Experimental push on scalar is now forbidden", line)
10329                        .with_near("at EOF"));
10330                }
10331                Ok(Expr {
10332                    kind: ExprKind::Push {
10333                        array: Box::new(first.clone()),
10334                        values: rest.to_vec(),
10335                    },
10336                    line,
10337                })
10338            }
10339            "pop" => {
10340                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10341                    return Ok(e);
10342                }
10343                let a = self.parse_one_arg_or_argv()?;
10344                Ok(Expr {
10345                    kind: ExprKind::Pop(Box::new(a)),
10346                    line,
10347                })
10348            }
10349            "shift" => {
10350                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10351                    return Ok(e);
10352                }
10353                let a = self.parse_one_arg_or_argv()?;
10354                Ok(Expr {
10355                    kind: ExprKind::Shift(Box::new(a)),
10356                    line,
10357                })
10358            }
10359            "unshift" => {
10360                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10361                    return Ok(e);
10362                }
10363                let args = self.parse_builtin_args()?;
10364                let (first, rest) = args
10365                    .split_first()
10366                    .ok_or_else(|| self.syntax_err("unshift requires arguments", line))?;
10367                Ok(Expr {
10368                    kind: ExprKind::Unshift {
10369                        array: Box::new(first.clone()),
10370                        values: rest.to_vec(),
10371                    },
10372                    line,
10373                })
10374            }
10375            "splice" => {
10376                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10377                    return Ok(e);
10378                }
10379                let args = self.parse_builtin_args()?;
10380                let mut iter = args.into_iter();
10381                let array = Box::new(
10382                    iter.next()
10383                        .ok_or_else(|| self.syntax_err("splice requires arguments", line))?,
10384                );
10385                let offset = iter.next().map(Box::new);
10386                let length = iter.next().map(Box::new);
10387                let replacement: Vec<Expr> = iter.collect();
10388                Ok(Expr {
10389                    kind: ExprKind::Splice {
10390                        array,
10391                        offset,
10392                        length,
10393                        replacement,
10394                    },
10395                    line,
10396                })
10397            }
10398            // `splice_last(@a, off[, n])` is the stryke spelling of Perl's
10399            // `scalar splice(@a, off, n)` — returns the LAST removed element
10400            // (or undef if nothing was removed). Desugars to `tail(splice(...))`
10401            // so the array is still mutated in place.
10402            "splice_last" | "splice1" | "spl_last" => {
10403                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10404                    return Ok(e);
10405                }
10406                let args = self.parse_builtin_args()?;
10407                let mut iter = args.into_iter();
10408                let array = Box::new(
10409                    iter.next()
10410                        .ok_or_else(|| self.syntax_err("splice_last requires arguments", line))?,
10411                );
10412                let offset = iter.next().map(Box::new);
10413                let length = iter.next().map(Box::new);
10414                let replacement: Vec<Expr> = iter.collect();
10415                let splice_expr = Expr {
10416                    kind: ExprKind::Splice {
10417                        array,
10418                        offset,
10419                        length,
10420                        replacement,
10421                    },
10422                    line,
10423                };
10424                Ok(Expr {
10425                    kind: ExprKind::FuncCall {
10426                        name: "tail".to_string(),
10427                        args: vec![splice_expr],
10428                    },
10429                    line,
10430                })
10431            }
10432            "delete" => {
10433                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10434                    return Ok(e);
10435                }
10436                let a = self.parse_postfix()?;
10437                Ok(Expr {
10438                    kind: ExprKind::Delete(Box::new(a)),
10439                    line,
10440                })
10441            }
10442            "exists" => {
10443                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10444                    return Ok(e);
10445                }
10446                // `parse_postfix` starts at `parse_primary` which doesn't
10447                // accept the leading `&` of `&subname` — call `parse_unary`
10448                // instead so `exists &main::myf` parses the same as
10449                // `defined &main::myf` already does.
10450                let a = self.parse_unary()?;
10451                Ok(Expr {
10452                    kind: ExprKind::Exists(Box::new(a)),
10453                    line,
10454                })
10455            }
10456            "keys" => {
10457                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10458                    return Ok(e);
10459                }
10460                let a = self.parse_one_arg_or_default()?;
10461                Ok(Expr {
10462                    kind: ExprKind::Keys(Box::new(a)),
10463                    line,
10464                })
10465            }
10466            "values" => {
10467                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10468                    return Ok(e);
10469                }
10470                let a = self.parse_one_arg_or_default()?;
10471                Ok(Expr {
10472                    kind: ExprKind::Values(Box::new(a)),
10473                    line,
10474                })
10475            }
10476            "each" => {
10477                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10478                    return Ok(e);
10479                }
10480                let a = self.parse_one_arg_or_default()?;
10481                Ok(Expr {
10482                    kind: ExprKind::Each(Box::new(a)),
10483                    line,
10484                })
10485            }
10486            "fore" | "e" | "ep" => {
10487                // `fore { BLOCK } LIST` / `ep` — forEach expression (pipe-forward friendly)
10488                if matches!(self.peek(), Token::LBrace) {
10489                    let (block, list) = self.parse_block_list()?;
10490                    Ok(Expr {
10491                        kind: ExprKind::ForEachExpr {
10492                            block,
10493                            list: Box::new(list),
10494                        },
10495                        line,
10496                    })
10497                } else if self.in_pipe_rhs() {
10498                    // `|> ep` — bare ep at end of pipe: default to `say $_`
10499                    // `|> fore say` / `|> e say` — blockless pipe form: wrap EXPR into a synthetic block
10500                    let is_terminal = matches!(
10501                        self.peek(),
10502                        Token::Semicolon
10503                            | Token::RParen
10504                            | Token::Eof
10505                            | Token::PipeForward
10506                            | Token::RBrace
10507                    );
10508                    let block = if name == "ep" && is_terminal {
10509                        vec![Statement {
10510                            label: None,
10511                            kind: StmtKind::Expression(Expr {
10512                                kind: ExprKind::Say {
10513                                    handle: None,
10514                                    args: vec![Expr {
10515                                        kind: ExprKind::ScalarVar("_".into()),
10516                                        line,
10517                                    }],
10518                                },
10519                                line,
10520                            }),
10521                            line,
10522                        }]
10523                    } else {
10524                        let expr = self.parse_assign_expr_stop_at_pipe()?;
10525                        let expr = Self::lift_bareword_to_topic_call(expr);
10526                        vec![Statement {
10527                            label: None,
10528                            kind: StmtKind::Expression(expr),
10529                            line,
10530                        }]
10531                    };
10532                    let list = self.pipe_placeholder_list(line);
10533                    Ok(Expr {
10534                        kind: ExprKind::ForEachExpr {
10535                            block,
10536                            list: Box::new(list),
10537                        },
10538                        line,
10539                    })
10540                } else {
10541                    // Two surface forms share this branch:
10542                    //   `fore EXPR, LIST` — comma form (explicit per-item EXPR + list)
10543                    //   `ep LIST`         — list-only form: print each item with `say $_`
10544                    // We disambiguate by peeking after the first parsed expression:
10545                    // if the next token is a comma we're in the EXPR-then-LIST form;
10546                    // otherwise the first parse *was* the LIST and we default the
10547                    // block to `say $_` (only for `ep` — `fore`/`e` keep their
10548                    // explicit-expression contract).
10549                    let expr = self.parse_assign_expr()?;
10550                    let expr = Self::lift_bareword_to_topic_call(expr);
10551                    if !matches!(self.peek(), Token::Comma) && name == "ep" {
10552                        let block = vec![Statement {
10553                            label: None,
10554                            kind: StmtKind::Expression(Expr {
10555                                kind: ExprKind::Say {
10556                                    handle: None,
10557                                    args: vec![Expr {
10558                                        kind: ExprKind::ScalarVar("_".into()),
10559                                        line,
10560                                    }],
10561                                },
10562                                line,
10563                            }),
10564                            line,
10565                        }];
10566                        return Ok(Expr {
10567                            kind: ExprKind::ForEachExpr {
10568                                block,
10569                                list: Box::new(expr),
10570                            },
10571                            line,
10572                        });
10573                    }
10574                    self.expect(&Token::Comma)?;
10575                    let list_parts = self.parse_list_until_terminator()?;
10576                    let list_expr = if list_parts.len() == 1 {
10577                        list_parts.into_iter().next().unwrap()
10578                    } else {
10579                        Expr {
10580                            kind: ExprKind::List(list_parts),
10581                            line,
10582                        }
10583                    };
10584                    let block = vec![Statement {
10585                        label: None,
10586                        kind: StmtKind::Expression(expr),
10587                        line,
10588                    }];
10589                    Ok(Expr {
10590                        kind: ExprKind::ForEachExpr {
10591                            block,
10592                            list: Box::new(list_expr),
10593                        },
10594                        line,
10595                    })
10596                }
10597            }
10598            "rev" => {
10599                // `rev` — context-aware reverse: string in scalar, list in list context.
10600                // List-operator precedence (so `rev 1..3` parses as `rev(1..3)`, not
10601                // `(rev 1)..3`). Defaults to $_ when no argument given.
10602                // Only use pipe placeholder when directly in pipe RHS (not inside a block).
10603                // RBrace means we're inside a block like `map { rev }` - use $_ default.
10604                let prev = self.prev_line();
10605                let a = if self.in_pipe_rhs()
10606                    && (matches!(
10607                        self.peek(),
10608                        Token::Semicolon | Token::RParen | Token::Eof | Token::PipeForward
10609                    ) || self.peek_line() > prev)
10610                {
10611                    self.pipe_placeholder_list(line)
10612                } else if self.peek_line() > prev {
10613                    // Newline boundary: argument is on a later line —
10614                    // default to `$_` so the next statement parses as
10615                    // its own thing instead of being slurped as the
10616                    // implicit operand. (Same rule as
10617                    // `parse_one_arg_or_default`.)
10618                    Expr {
10619                        kind: ExprKind::ScalarVar("_".into()),
10620                        line: prev,
10621                    }
10622                } else if matches!(
10623                    self.peek(),
10624                    Token::Semicolon
10625                        | Token::RBrace
10626                        | Token::RParen
10627                        | Token::RBracket
10628                        | Token::Eof
10629                        | Token::Comma
10630                        | Token::FatArrow
10631                        | Token::PipeForward
10632                ) {
10633                    Expr {
10634                        kind: ExprKind::ScalarVar("_".into()),
10635                        line: self.peek_line(),
10636                    }
10637                } else if matches!(self.peek(), Token::LParen)
10638                    && matches!(self.peek_at(1), Token::RParen)
10639                {
10640                    // `rev()` — empty parens default to `$_` (matches Perl's
10641                    // `length()` / `uc()` etc. and the `|> rev()` pipe form).
10642                    let pl = self.peek_line();
10643                    self.advance(); // (
10644                    self.advance(); // )
10645                    Expr {
10646                        kind: ExprKind::ScalarVar("_".into()),
10647                        line: pl,
10648                    }
10649                } else {
10650                    self.parse_one_arg()?
10651                };
10652                Ok(Expr {
10653                    kind: ExprKind::Rev(Box::new(a)),
10654                    line,
10655                })
10656            }
10657            "reverse" => {
10658                if crate::no_interop_mode() {
10659                    return Err(self.syntax_err(
10660                        "stryke uses `rev` instead of `reverse` (--no-interop)",
10661                        line,
10662                    ));
10663                }
10664                // On the RHS of `|>`, the operand is supplied by the piped LHS.
10665                let a = if self.in_pipe_rhs()
10666                    && matches!(
10667                        self.peek(),
10668                        Token::Semicolon
10669                            | Token::RBrace
10670                            | Token::RParen
10671                            | Token::Eof
10672                            | Token::PipeForward
10673                    ) {
10674                    self.pipe_placeholder_list(line)
10675                } else if matches!(self.peek(), Token::LParen)
10676                    && matches!(self.peek_at(1), Token::RParen)
10677                {
10678                    // `reverse()` — Perl-style empty list call returns the empty list.
10679                    self.advance();
10680                    self.advance();
10681                    Expr {
10682                        kind: ExprKind::List(Vec::new()),
10683                        line,
10684                    }
10685                } else {
10686                    self.parse_one_arg()?
10687                };
10688                Ok(Expr {
10689                    kind: ExprKind::ReverseExpr(Box::new(a)),
10690                    line,
10691                })
10692            }
10693            "reversed" | "rv" => {
10694                // On the RHS of `|>`, the operand is supplied by the piped LHS.
10695                let a = if self.in_pipe_rhs()
10696                    && matches!(
10697                        self.peek(),
10698                        Token::Semicolon
10699                            | Token::RBrace
10700                            | Token::RParen
10701                            | Token::Eof
10702                            | Token::PipeForward
10703                    ) {
10704                    self.pipe_placeholder_list(line)
10705                } else {
10706                    self.parse_one_arg()?
10707                };
10708                Ok(Expr {
10709                    kind: ExprKind::Rev(Box::new(a)),
10710                    line,
10711                })
10712            }
10713            "join" => {
10714                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10715                    return Ok(e);
10716                }
10717                let args = self.parse_builtin_args()?;
10718                if args.is_empty() {
10719                    return Err(self.syntax_err("join requires separator and list", line));
10720                }
10721                // `@list |> join(",")` — list slot is filled by the piped LHS.
10722                if args.len() < 2 && !self.in_pipe_rhs() {
10723                    return Err(self.syntax_err("join requires separator and list", line));
10724                }
10725                Ok(Expr {
10726                    kind: ExprKind::JoinExpr {
10727                        separator: Box::new(args[0].clone()),
10728                        list: Box::new(Expr {
10729                            kind: ExprKind::List(args[1..].to_vec()),
10730                            line,
10731                        }),
10732                    },
10733                    line,
10734                })
10735            }
10736            "split" => {
10737                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10738                    return Ok(e);
10739                }
10740                let args = self.parse_builtin_args()?;
10741                let pattern = args.first().cloned().unwrap_or(Expr {
10742                    kind: ExprKind::String(" ".into()),
10743                    line,
10744                });
10745                let string = args.get(1).cloned().unwrap_or(Expr {
10746                    kind: ExprKind::ScalarVar("_".into()),
10747                    line,
10748                });
10749                let limit = args.get(2).cloned().map(Box::new);
10750                Ok(Expr {
10751                    kind: ExprKind::SplitExpr {
10752                        pattern: Box::new(pattern),
10753                        string: Box::new(string),
10754                        limit,
10755                    },
10756                    line,
10757                })
10758            }
10759            "substr" => {
10760                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10761                    return Ok(e);
10762                }
10763                let args = self.parse_builtin_args()?;
10764                Ok(Expr {
10765                    kind: ExprKind::Substr {
10766                        string: Box::new(args[0].clone()),
10767                        offset: Box::new(args[1].clone()),
10768                        length: args.get(2).cloned().map(Box::new),
10769                        replacement: args.get(3).cloned().map(Box::new),
10770                    },
10771                    line,
10772                })
10773            }
10774            "index" => {
10775                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10776                    return Ok(e);
10777                }
10778                let args = self.parse_builtin_args()?;
10779                Ok(Expr {
10780                    kind: ExprKind::Index {
10781                        string: Box::new(args[0].clone()),
10782                        substr: Box::new(args[1].clone()),
10783                        position: args.get(2).cloned().map(Box::new),
10784                    },
10785                    line,
10786                })
10787            }
10788            "rindex" => {
10789                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10790                    return Ok(e);
10791                }
10792                let args = self.parse_builtin_args()?;
10793                Ok(Expr {
10794                    kind: ExprKind::Rindex {
10795                        string: Box::new(args[0].clone()),
10796                        substr: Box::new(args[1].clone()),
10797                        position: args.get(2).cloned().map(Box::new),
10798                    },
10799                    line,
10800                })
10801            }
10802            "sprintf" => {
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 (first, rest) = args
10808                    .split_first()
10809                    .ok_or_else(|| self.syntax_err("sprintf requires format", line))?;
10810                Ok(Expr {
10811                    kind: ExprKind::Sprintf {
10812                        format: Box::new(first.clone()),
10813                        args: rest.to_vec(),
10814                    },
10815                    line,
10816                })
10817            }
10818            "map" | "flat_map" | "maps" | "flat_maps" => {
10819                let flatten_array_refs = matches!(name.as_str(), "flat_map" | "flat_maps");
10820                let stream = matches!(name.as_str(), "maps" | "flat_maps");
10821                if matches!(self.peek(), Token::LBrace) {
10822                    let (block, list) = self.parse_block_list()?;
10823                    Ok(Expr {
10824                        kind: ExprKind::MapExpr {
10825                            block,
10826                            list: Box::new(list),
10827                            flatten_array_refs,
10828                            stream,
10829                        },
10830                        line,
10831                    })
10832                } else {
10833                    let expr = self.parse_assign_expr_stop_at_pipe()?;
10834                    // Lift bareword to FuncCall($_) so `map sha512, @list`
10835                    // calls sha512($_) for each element instead of stringifying.
10836                    let expr = Self::lift_bareword_to_topic_call(expr);
10837                    let list_expr = if self.pipe_supplies_slurped_list_operand() {
10838                        self.pipe_placeholder_list(line)
10839                    } else {
10840                        self.expect(&Token::Comma)?;
10841                        let list_parts = self.parse_list_until_terminator()?;
10842                        if list_parts.len() == 1 {
10843                            list_parts.into_iter().next().unwrap()
10844                        } else {
10845                            Expr {
10846                                kind: ExprKind::List(list_parts),
10847                                line,
10848                            }
10849                        }
10850                    };
10851                    Ok(Expr {
10852                        kind: ExprKind::MapExprComma {
10853                            expr: Box::new(expr),
10854                            list: Box::new(list_expr),
10855                            flatten_array_refs,
10856                            stream,
10857                        },
10858                        line,
10859                    })
10860                }
10861            }
10862            "cond" => {
10863                if crate::compat_mode() {
10864                    return Err(self
10865                        .syntax_err("`cond` is a stryke extension (disabled by --compat)", line));
10866                }
10867                self.parse_cond_expr(line)
10868            }
10869            "match" => {
10870                if crate::compat_mode() {
10871                    return Err(self.syntax_err(
10872                        "algebraic `match` is a stryke extension (disabled by --compat)",
10873                        line,
10874                    ));
10875                }
10876                self.parse_algebraic_match_expr(line)
10877            }
10878            "grep" | "greps" | "filter" | "fi" | "find_all" => {
10879                let keyword = match name.as_str() {
10880                    "grep" => crate::ast::GrepBuiltinKeyword::Grep,
10881                    "greps" => crate::ast::GrepBuiltinKeyword::Greps,
10882                    "filter" | "fi" => crate::ast::GrepBuiltinKeyword::Filter,
10883                    "find_all" => crate::ast::GrepBuiltinKeyword::FindAll,
10884                    _ => unreachable!(),
10885                };
10886                if matches!(self.peek(), Token::LBrace) {
10887                    let (block, list) = self.parse_block_list()?;
10888                    Ok(Expr {
10889                        kind: ExprKind::GrepExpr {
10890                            block,
10891                            list: Box::new(list),
10892                            keyword,
10893                        },
10894                        line,
10895                    })
10896                } else {
10897                    let expr = self.parse_assign_expr_stop_at_pipe()?;
10898                    if self.pipe_supplies_slurped_list_operand() {
10899                        // Pipe-RHS blockless form: `|> grep EXPR`
10900                        // For literals, desugar to `$_ eq/== EXPR` so
10901                        // `|> filter 't'` keeps only elements equal to 't'.
10902                        // For regexes, desugar to `$_ =~ EXPR`.
10903                        let list = self.pipe_placeholder_list(line);
10904                        let topic = Expr {
10905                            kind: ExprKind::ScalarVar("_".into()),
10906                            line,
10907                        };
10908                        let test = match &expr.kind {
10909                            ExprKind::Integer(_) | ExprKind::Float(_) => Expr {
10910                                kind: ExprKind::BinOp {
10911                                    op: BinOp::NumEq,
10912                                    left: Box::new(topic),
10913                                    right: Box::new(expr),
10914                                },
10915                                line,
10916                            },
10917                            ExprKind::String(_) | ExprKind::InterpolatedString(_) => Expr {
10918                                kind: ExprKind::BinOp {
10919                                    op: BinOp::StrEq,
10920                                    left: Box::new(topic),
10921                                    right: Box::new(expr),
10922                                },
10923                                line,
10924                            },
10925                            ExprKind::Regex { .. } => Expr {
10926                                kind: ExprKind::BinOp {
10927                                    op: BinOp::BindMatch,
10928                                    left: Box::new(topic),
10929                                    right: Box::new(expr),
10930                                },
10931                                line,
10932                            },
10933                            _ => {
10934                                // Non-literal (e.g. `defined`, scalar coderef var,
10935                                // hash slot): lift barewords to topic-call, then
10936                                // route through GrepExprComma so the runtime
10937                                // coderef-dispatch in Op::GrepWithExpr handles
10938                                // both truthiness AND coderef-call uniformly.
10939                                let expr = Self::lift_bareword_to_topic_call(expr);
10940                                return Ok(Expr {
10941                                    kind: ExprKind::GrepExprComma {
10942                                        expr: Box::new(expr),
10943                                        list: Box::new(list),
10944                                        keyword,
10945                                    },
10946                                    line,
10947                                });
10948                            }
10949                        };
10950                        let block = vec![Statement {
10951                            label: None,
10952                            kind: StmtKind::Expression(test),
10953                            line,
10954                        }];
10955                        Ok(Expr {
10956                            kind: ExprKind::GrepExpr {
10957                                block,
10958                                list: Box::new(list),
10959                                keyword,
10960                            },
10961                            line,
10962                        })
10963                    } else {
10964                        let expr = Self::lift_bareword_to_topic_call(expr);
10965                        self.expect(&Token::Comma)?;
10966                        let list_parts = self.parse_list_until_terminator()?;
10967                        let list_expr = if list_parts.len() == 1 {
10968                            list_parts.into_iter().next().unwrap()
10969                        } else {
10970                            Expr {
10971                                kind: ExprKind::List(list_parts),
10972                                line,
10973                            }
10974                        };
10975                        Ok(Expr {
10976                            kind: ExprKind::GrepExprComma {
10977                                expr: Box::new(expr),
10978                                list: Box::new(list_expr),
10979                                keyword,
10980                            },
10981                            line,
10982                        })
10983                    }
10984                }
10985            }
10986            "sort" => {
10987                use crate::ast::SortComparator;
10988                if matches!(self.peek(), Token::LBrace) {
10989                    let block = self.parse_block()?;
10990                    let block_end_line = self.prev_line();
10991                    let _ = self.eat(&Token::Comma);
10992                    let list = if self.in_pipe_rhs()
10993                        && (matches!(
10994                            self.peek(),
10995                            Token::Semicolon
10996                                | Token::RBrace
10997                                | Token::RParen
10998                                | Token::Eof
10999                                | Token::PipeForward
11000                        ) || self.peek_line() > block_end_line)
11001                    {
11002                        self.pipe_placeholder_list(line)
11003                    } else {
11004                        self.parse_expression()?
11005                    };
11006                    Ok(Expr {
11007                        kind: ExprKind::SortExpr {
11008                            cmp: Some(SortComparator::Block(block)),
11009                            list: Box::new(list),
11010                        },
11011                        line,
11012                    })
11013                } else if matches!(self.peek(), Token::ScalarVar(ref v) if v == "a" || v == "b") {
11014                    // Blockless comparator: `sort $a <=> $b, @list`
11015                    let block = self.parse_block_or_bareword_cmp_block()?;
11016                    let _ = self.eat(&Token::Comma);
11017                    let list = if self.in_pipe_rhs()
11018                        && matches!(
11019                            self.peek(),
11020                            Token::Semicolon
11021                                | Token::RBrace
11022                                | Token::RParen
11023                                | Token::Eof
11024                                | Token::PipeForward
11025                        ) {
11026                        self.pipe_placeholder_list(line)
11027                    } else {
11028                        self.parse_expression()?
11029                    };
11030                    Ok(Expr {
11031                        kind: ExprKind::SortExpr {
11032                            cmp: Some(SortComparator::Block(block)),
11033                            list: Box::new(list),
11034                        },
11035                        line,
11036                    })
11037                } else if matches!(self.peek(), Token::ScalarVar(_)) {
11038                    // `sort $coderef (LIST)` — comparator is first; list often parenthesized.
11039                    // Pipe-RHS form `|> sort $coderef` uses placeholder LHS as the list.
11040                    self.suppress_indirect_paren_call =
11041                        self.suppress_indirect_paren_call.saturating_add(1);
11042                    let code = self.parse_assign_expr()?;
11043                    self.suppress_indirect_paren_call =
11044                        self.suppress_indirect_paren_call.saturating_sub(1);
11045                    let _ = self.eat(&Token::Comma);
11046                    let list = if self.in_pipe_rhs()
11047                        && matches!(
11048                            self.peek(),
11049                            Token::Semicolon
11050                                | Token::RBrace
11051                                | Token::RParen
11052                                | Token::Eof
11053                                | Token::PipeForward
11054                        ) {
11055                        self.pipe_placeholder_list(line)
11056                    } else if matches!(self.peek(), Token::LParen) {
11057                        self.advance();
11058                        let e = self.parse_expression()?;
11059                        self.expect(&Token::RParen)?;
11060                        e
11061                    } else {
11062                        self.parse_expression()?
11063                    };
11064                    Ok(Expr {
11065                        kind: ExprKind::SortExpr {
11066                            cmp: Some(SortComparator::Code(Box::new(code))),
11067                            list: Box::new(list),
11068                        },
11069                        line,
11070                    })
11071                } else if matches!(self.peek(), Token::Ident(ref name) if !Self::is_known_bareword(name))
11072                {
11073                    // Blockless comparator via bare sub name: `sort my_cmp @list`
11074                    let block = self.parse_block_or_bareword_cmp_block()?;
11075                    let _ = self.eat(&Token::Comma);
11076                    let list = if self.in_pipe_rhs()
11077                        && matches!(
11078                            self.peek(),
11079                            Token::Semicolon
11080                                | Token::RBrace
11081                                | Token::RParen
11082                                | Token::Eof
11083                                | Token::PipeForward
11084                        ) {
11085                        self.pipe_placeholder_list(line)
11086                    } else {
11087                        self.parse_expression()?
11088                    };
11089                    Ok(Expr {
11090                        kind: ExprKind::SortExpr {
11091                            cmp: Some(SortComparator::Block(block)),
11092                            list: Box::new(list),
11093                        },
11094                        line,
11095                    })
11096                } else {
11097                    // Bare `sort` with no comparator and no list: only allowed
11098                    // as the RHS of `|>`, where the list comes from the LHS.
11099                    // Treat a newline as an implicit pipeline terminator —
11100                    // `@a |> sort\nmy $x = ...` must NOT swallow the next
11101                    // `my` stmt as sort's argument list.
11102                    let list = if self.in_pipe_rhs()
11103                        && (matches!(
11104                            self.peek(),
11105                            Token::Semicolon
11106                                | Token::RBrace
11107                                | Token::RParen
11108                                | Token::Eof
11109                                | Token::PipeForward
11110                        ) || self.peek_line() > line)
11111                    {
11112                        self.pipe_placeholder_list(line)
11113                    } else {
11114                        self.parse_expression()?
11115                    };
11116                    Ok(Expr {
11117                        kind: ExprKind::SortExpr {
11118                            cmp: None,
11119                            list: Box::new(list),
11120                        },
11121                        line,
11122                    })
11123                }
11124            }
11125            "reduce" | "fold" | "inject" => {
11126                let (block, list) = self.parse_block_list()?;
11127                Ok(Expr {
11128                    kind: ExprKind::ReduceExpr {
11129                        block,
11130                        list: Box::new(list),
11131                    },
11132                    line,
11133                })
11134            }
11135            // Parallel extensions
11136            "pmap" => {
11137                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11138                Ok(Expr {
11139                    kind: ExprKind::PMapExpr {
11140                        block,
11141                        list: Box::new(list),
11142                        progress: progress.map(Box::new),
11143                        flat_outputs: false,
11144                        on_cluster: None,
11145                        stream: false,
11146                    },
11147                    line,
11148                })
11149            }
11150            "pmap_on" => {
11151                let (cluster, block, list, progress) =
11152                    self.parse_cluster_block_then_list_optional_progress()?;
11153                Ok(Expr {
11154                    kind: ExprKind::PMapExpr {
11155                        block,
11156                        list: Box::new(list),
11157                        progress: progress.map(Box::new),
11158                        flat_outputs: false,
11159                        on_cluster: Some(Box::new(cluster)),
11160                        stream: false,
11161                    },
11162                    line,
11163                })
11164            }
11165            "pflat_map" => {
11166                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11167                Ok(Expr {
11168                    kind: ExprKind::PMapExpr {
11169                        block,
11170                        list: Box::new(list),
11171                        progress: progress.map(Box::new),
11172                        flat_outputs: true,
11173                        on_cluster: None,
11174                        stream: false,
11175                    },
11176                    line,
11177                })
11178            }
11179            "pflat_map_on" => {
11180                let (cluster, block, list, progress) =
11181                    self.parse_cluster_block_then_list_optional_progress()?;
11182                Ok(Expr {
11183                    kind: ExprKind::PMapExpr {
11184                        block,
11185                        list: Box::new(list),
11186                        progress: progress.map(Box::new),
11187                        flat_outputs: true,
11188                        on_cluster: Some(Box::new(cluster)),
11189                        stream: false,
11190                    },
11191                    line,
11192                })
11193            }
11194            "pmaps" => {
11195                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11196                Ok(Expr {
11197                    kind: ExprKind::PMapExpr {
11198                        block,
11199                        list: Box::new(list),
11200                        progress: progress.map(Box::new),
11201                        flat_outputs: false,
11202                        on_cluster: None,
11203                        stream: true,
11204                    },
11205                    line,
11206                })
11207            }
11208            "pflat_maps" => {
11209                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11210                Ok(Expr {
11211                    kind: ExprKind::PMapExpr {
11212                        block,
11213                        list: Box::new(list),
11214                        progress: progress.map(Box::new),
11215                        flat_outputs: true,
11216                        on_cluster: None,
11217                        stream: true,
11218                    },
11219                    line,
11220                })
11221            }
11222            "pgreps" => {
11223                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11224                Ok(Expr {
11225                    kind: ExprKind::PGrepExpr {
11226                        block,
11227                        list: Box::new(list),
11228                        progress: progress.map(Box::new),
11229                        stream: true,
11230                    },
11231                    line,
11232                })
11233            }
11234            "pmap_chunked" => {
11235                let chunk_size = self.parse_assign_expr()?;
11236                let block = self.parse_block_or_bareword_block()?;
11237                self.eat(&Token::Comma);
11238                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
11239                Ok(Expr {
11240                    kind: ExprKind::PMapChunkedExpr {
11241                        chunk_size: Box::new(chunk_size),
11242                        block,
11243                        list: Box::new(list),
11244                        progress: progress.map(Box::new),
11245                    },
11246                    line,
11247                })
11248            }
11249            "pgrep" => {
11250                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11251                Ok(Expr {
11252                    kind: ExprKind::PGrepExpr {
11253                        block,
11254                        list: Box::new(list),
11255                        progress: progress.map(Box::new),
11256                        stream: false,
11257                    },
11258                    line,
11259                })
11260            }
11261            "pfor" => {
11262                if matches!(self.peek(), Token::LParen) {
11263                    self.expect(&Token::LParen)?;
11264                    let list = self.parse_expression()?;
11265                    self.expect(&Token::RParen)?;
11266                    let block = self.parse_block()?;
11267                    Ok(Expr {
11268                        kind: ExprKind::PForExpr {
11269                            block,
11270                            list: Box::new(list),
11271                            progress: None,
11272                        },
11273                        line,
11274                    })
11275                } else {
11276                    let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11277                    Ok(Expr {
11278                        kind: ExprKind::PForExpr {
11279                            block,
11280                            list: Box::new(list),
11281                            progress: progress.map(Box::new),
11282                        },
11283                        line,
11284                    })
11285                }
11286            }
11287            // `par { BLOCK } LIST` — generic parallel-chunk evaluator.
11288            // Splits LIST into chunks (UTF-8-aligned for strings,
11289            // element-aligned for arrays), runs BLOCK on each chunk in
11290            // parallel with `_` bound to the chunk, flattens results.
11291            // Available as a top-level expression, not just an `~>` stage.
11292            "par" => {
11293                let (block, list, _progress) = self.parse_block_then_list_optional_progress()?;
11294                Ok(Expr {
11295                    kind: ExprKind::ParExpr {
11296                        block,
11297                        list: Box::new(list),
11298                    },
11299                    line,
11300                })
11301            }
11302            "par_lines" | "par_walk" => {
11303                let args = self.parse_builtin_args()?;
11304                if args.len() < 2 {
11305                    return Err(
11306                        self.syntax_err(format!("{} requires at least two arguments", name), line)
11307                    );
11308                }
11309
11310                if name == "par_lines" {
11311                    Ok(Expr {
11312                        kind: ExprKind::ParLinesExpr {
11313                            path: Box::new(args[0].clone()),
11314                            callback: Box::new(args[1].clone()),
11315                            progress: None,
11316                        },
11317                        line,
11318                    })
11319                } else {
11320                    Ok(Expr {
11321                        kind: ExprKind::ParWalkExpr {
11322                            path: Box::new(args[0].clone()),
11323                            callback: Box::new(args[1].clone()),
11324                            progress: None,
11325                        },
11326                        line,
11327                    })
11328                }
11329            }
11330            "pwatch" | "watch" => {
11331                let args = self.parse_builtin_args()?;
11332                if args.len() < 2 {
11333                    return Err(
11334                        self.syntax_err(format!("{} requires at least two arguments", name), line)
11335                    );
11336                }
11337                Ok(Expr {
11338                    kind: ExprKind::PwatchExpr {
11339                        path: Box::new(args[0].clone()),
11340                        callback: Box::new(args[1].clone()),
11341                    },
11342                    line,
11343                })
11344            }
11345            "fan" => {
11346                // fan { BLOCK }            — no count, block body
11347                // fan COUNT { BLOCK }      — count + block body
11348                // fan EXPR;                — no count, blockless body (wrap EXPR as block)
11349                // fan COUNT EXPR;          — count + blockless body
11350                // Optional: `, progress => EXPR` or `progress => EXPR` (no comma before progress)
11351                let (count, block) = self.parse_fan_count_and_block(line)?;
11352                let progress = self.parse_fan_optional_progress("fan")?;
11353                Ok(Expr {
11354                    kind: ExprKind::FanExpr {
11355                        count,
11356                        block,
11357                        progress,
11358                        capture: false,
11359                    },
11360                    line,
11361                })
11362            }
11363            "fan_cap" => {
11364                let (count, block) = self.parse_fan_count_and_block(line)?;
11365                let progress = self.parse_fan_optional_progress("fan_cap")?;
11366                Ok(Expr {
11367                    kind: ExprKind::FanExpr {
11368                        count,
11369                        block,
11370                        progress,
11371                        capture: true,
11372                    },
11373                    line,
11374                })
11375            }
11376            "async" => {
11377                if !matches!(self.peek(), Token::LBrace) {
11378                    return Err(self.syntax_err("async must be followed by { BLOCK }", line));
11379                }
11380                let block = self.parse_block()?;
11381                Ok(Expr {
11382                    kind: ExprKind::AsyncBlock { body: block },
11383                    line,
11384                })
11385            }
11386            "spawn" => {
11387                if !matches!(self.peek(), Token::LBrace) {
11388                    return Err(self.syntax_err("spawn must be followed by { BLOCK }", line));
11389                }
11390                let block = self.parse_block()?;
11391                Ok(Expr {
11392                    kind: ExprKind::SpawnBlock { body: block },
11393                    line,
11394                })
11395            }
11396            "trace" => {
11397                if !matches!(self.peek(), Token::LBrace) {
11398                    return Err(self.syntax_err("trace must be followed by { BLOCK }", line));
11399                }
11400                let block = self.parse_block()?;
11401                Ok(Expr {
11402                    kind: ExprKind::Trace { body: block },
11403                    line,
11404                })
11405            }
11406            "timer" => {
11407                let block = self.parse_block_or_bareword_block_no_args()?;
11408                Ok(Expr {
11409                    kind: ExprKind::Timer { body: block },
11410                    line,
11411                })
11412            }
11413            "bench" => {
11414                let block = self.parse_block_or_bareword_block_no_args()?;
11415                let times = Box::new(self.parse_expression()?);
11416                Ok(Expr {
11417                    kind: ExprKind::Bench { body: block, times },
11418                    line,
11419                })
11420            }
11421            "spinner" => {
11422                // `spinner "msg" { BLOCK }` or `spinner { BLOCK }`
11423                let (message, body) = if matches!(self.peek(), Token::LBrace) {
11424                    let body = self.parse_block()?;
11425                    (
11426                        Box::new(Expr {
11427                            kind: ExprKind::String("working".to_string()),
11428                            line,
11429                        }),
11430                        body,
11431                    )
11432                } else {
11433                    let msg = self.parse_assign_expr()?;
11434                    let body = self.parse_block()?;
11435                    (Box::new(msg), body)
11436                };
11437                Ok(Expr {
11438                    kind: ExprKind::Spinner { message, body },
11439                    line,
11440                })
11441            }
11442            "thread" | "t" => {
11443                // `thread EXPR stage1 stage2 ...` — threading macro (thread-first)
11444                // `t` is a short alias for `thread`
11445                // Each stage is either:
11446                //   - `ident` — bare function call
11447                //   - `ident { block }` — function with block arg
11448                //   - `ident arg1 arg2 { block }` — function with args and optional block
11449                //   - `fn { block }` — standalone anonymous block
11450                //   - `>{ block }` — shorthand for standalone anonymous block
11451                // Desugars to: EXPR |> stage1 |> stage2 |> ...
11452                self.parse_thread_macro(line, false)
11453            }
11454            "retry" => {
11455                // `retry { BLOCK }` or `retry BAREWORD` — bareword becomes zero-arg call.
11456                // An optional comma before `times` is allowed in both forms.
11457                let body = if matches!(self.peek(), Token::LBrace) {
11458                    self.parse_block()?
11459                } else {
11460                    let bw_line = self.peek_line();
11461                    let Token::Ident(ref name) = self.peek().clone() else {
11462                        return Err(self
11463                            .syntax_err("retry: expected block or bareword function name", line));
11464                    };
11465                    let name = name.clone();
11466                    self.advance();
11467                    vec![Statement::new(
11468                        StmtKind::Expression(Expr {
11469                            kind: ExprKind::FuncCall { name, args: vec![] },
11470                            line: bw_line,
11471                        }),
11472                        bw_line,
11473                    )]
11474                };
11475                self.eat(&Token::Comma);
11476                match self.peek() {
11477                    Token::Ident(ref s) if s == "times" => {
11478                        self.advance();
11479                    }
11480                    _ => {
11481                        return Err(self.syntax_err("retry: expected `times =>` after block", line));
11482                    }
11483                }
11484                self.expect(&Token::FatArrow)?;
11485                let times = Box::new(self.parse_assign_expr()?);
11486                let mut backoff = RetryBackoff::None;
11487                if self.eat(&Token::Comma) {
11488                    match self.peek() {
11489                        Token::Ident(ref s) if s == "backoff" => {
11490                            self.advance();
11491                        }
11492                        _ => {
11493                            return Err(
11494                                self.syntax_err("retry: expected `backoff =>` after comma", line)
11495                            );
11496                        }
11497                    }
11498                    self.expect(&Token::FatArrow)?;
11499                    let Token::Ident(mode) = self.peek().clone() else {
11500                        return Err(self.syntax_err(
11501                            "retry: expected backoff mode (none, linear, exponential)",
11502                            line,
11503                        ));
11504                    };
11505                    backoff = match mode.as_str() {
11506                        "none" => RetryBackoff::None,
11507                        "linear" => RetryBackoff::Linear,
11508                        "exponential" => RetryBackoff::Exponential,
11509                        _ => {
11510                            return Err(
11511                                self.syntax_err(format!("retry: invalid backoff `{mode}`"), line)
11512                            );
11513                        }
11514                    };
11515                    self.advance();
11516                }
11517                Ok(Expr {
11518                    kind: ExprKind::RetryBlock {
11519                        body,
11520                        times,
11521                        backoff,
11522                    },
11523                    line,
11524                })
11525            }
11526            "rate_limit" => {
11527                self.expect(&Token::LParen)?;
11528                let max = Box::new(self.parse_assign_expr()?);
11529                self.expect(&Token::Comma)?;
11530                let window = Box::new(self.parse_assign_expr()?);
11531                self.expect(&Token::RParen)?;
11532                let body = self.parse_block_or_bareword_block_no_args()?;
11533                let slot = self.alloc_rate_limit_slot();
11534                Ok(Expr {
11535                    kind: ExprKind::RateLimitBlock {
11536                        slot,
11537                        max,
11538                        window,
11539                        body,
11540                    },
11541                    line,
11542                })
11543            }
11544            "every" => {
11545                // `every("500ms") { BLOCK }` or `every "500ms" BODY` — parens optional.
11546                // Body consumes `|>` (every is an infinite loop, not a pipeable source).
11547                let has_paren = self.eat(&Token::LParen);
11548                let interval = Box::new(self.parse_assign_expr()?);
11549                if has_paren {
11550                    self.expect(&Token::RParen)?;
11551                }
11552                let body = if matches!(self.peek(), Token::LBrace) {
11553                    self.parse_block()?
11554                } else {
11555                    let bline = self.peek_line();
11556                    let expr = self.parse_assign_expr()?;
11557                    vec![Statement::new(StmtKind::Expression(expr), bline)]
11558                };
11559                Ok(Expr {
11560                    kind: ExprKind::EveryBlock { interval, body },
11561                    line,
11562                })
11563            }
11564            "gen" => {
11565                if !matches!(self.peek(), Token::LBrace) {
11566                    return Err(self.syntax_err("gen must be followed by { BLOCK }", line));
11567                }
11568                let body = self.parse_block()?;
11569                Ok(Expr {
11570                    kind: ExprKind::GenBlock { body },
11571                    line,
11572                })
11573            }
11574            "yield" => {
11575                let e = self.parse_assign_expr()?;
11576                Ok(Expr {
11577                    kind: ExprKind::Yield(Box::new(e)),
11578                    line,
11579                })
11580            }
11581            "await" => {
11582                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11583                    return Ok(e);
11584                }
11585                // `await` defaults to `$_` so `map { await } @tasks` works
11586                // (Perl-style topic-defaulting unary).
11587                let a = self.parse_one_arg_or_default()?;
11588                Ok(Expr {
11589                    kind: ExprKind::Await(Box::new(a)),
11590                    line,
11591                })
11592            }
11593            "slurp" | "cat" | "c" => {
11594                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11595                    return Ok(e);
11596                }
11597                let a = self.parse_one_arg_or_default()?;
11598                Ok(Expr {
11599                    kind: ExprKind::Slurp(Box::new(a)),
11600                    line,
11601                })
11602            }
11603            "capture" => {
11604                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11605                    return Ok(e);
11606                }
11607                let a = self.parse_one_arg()?;
11608                Ok(Expr {
11609                    kind: ExprKind::Capture(Box::new(a)),
11610                    line,
11611                })
11612            }
11613            "fetch_url" => {
11614                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11615                    return Ok(e);
11616                }
11617                let a = self.parse_one_arg()?;
11618                Ok(Expr {
11619                    kind: ExprKind::FetchUrl(Box::new(a)),
11620                    line,
11621                })
11622            }
11623            "pchannel" => {
11624                let capacity = if self.eat(&Token::LParen) {
11625                    if matches!(self.peek(), Token::RParen) {
11626                        self.advance();
11627                        None
11628                    } else {
11629                        let e = self.parse_expression()?;
11630                        self.expect(&Token::RParen)?;
11631                        Some(Box::new(e))
11632                    }
11633                } else {
11634                    None
11635                };
11636                Ok(Expr {
11637                    kind: ExprKind::Pchannel { capacity },
11638                    line,
11639                })
11640            }
11641            "psort" => {
11642                if matches!(self.peek(), Token::LBrace)
11643                    || matches!(self.peek(), Token::ScalarVar(ref v) if v == "a" || v == "b")
11644                    || matches!(self.peek(), Token::Ident(ref name) if !Self::is_known_bareword(name))
11645                {
11646                    let block = self.parse_block_or_bareword_cmp_block()?;
11647                    // Mirror `sort`'s pipe-RHS handling — after the block,
11648                    // a newline (or any standard terminator token) inside a
11649                    // `|> psort { ... }` chain means the list comes from the
11650                    // pipe LHS, not from continued parsing into the next
11651                    // statement. Without this check `(@list) |> psort {
11652                    // _0 <=> _1 }\nmy $n = ...` silently swallowed `my $n =
11653                    // ...` as the list operand.
11654                    let block_end_line = self.prev_line();
11655                    self.eat(&Token::Comma);
11656                    let use_placeholder = self.in_pipe_rhs()
11657                        && (matches!(
11658                            self.peek(),
11659                            Token::Semicolon
11660                                | Token::RBrace
11661                                | Token::RParen
11662                                | Token::Eof
11663                                | Token::PipeForward
11664                        ) || self.peek_line() > block_end_line);
11665                    let (list, progress) = if use_placeholder {
11666                        (self.pipe_placeholder_list(line), None)
11667                    } else {
11668                        self.parse_assign_expr_list_optional_progress()?
11669                    };
11670                    Ok(Expr {
11671                        kind: ExprKind::PSortExpr {
11672                            cmp: Some(block),
11673                            list: Box::new(list),
11674                            progress: progress.map(Box::new),
11675                        },
11676                        line,
11677                    })
11678                } else {
11679                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
11680                    Ok(Expr {
11681                        kind: ExprKind::PSortExpr {
11682                            cmp: None,
11683                            list: Box::new(list),
11684                            progress: progress.map(Box::new),
11685                        },
11686                        line,
11687                    })
11688                }
11689            }
11690            "preduce" => {
11691                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11692                Ok(Expr {
11693                    kind: ExprKind::PReduceExpr {
11694                        block,
11695                        list: Box::new(list),
11696                        progress: progress.map(Box::new),
11697                    },
11698                    line,
11699                })
11700            }
11701            "preduce_init" => {
11702                let (init, block, list, progress) =
11703                    self.parse_init_block_then_list_optional_progress()?;
11704                Ok(Expr {
11705                    kind: ExprKind::PReduceInitExpr {
11706                        init: Box::new(init),
11707                        block,
11708                        list: Box::new(list),
11709                        progress: progress.map(Box::new),
11710                    },
11711                    line,
11712                })
11713            }
11714            "pmap_reduce" => {
11715                let map_block = self.parse_block_or_bareword_block()?;
11716                // After the map block, expect either a `{ REDUCE }` block, or
11717                // after an eaten comma, a blockless reduce expr (`$a + $b`).
11718                let reduce_block = if matches!(self.peek(), Token::LBrace) {
11719                    self.parse_block()?
11720                } else {
11721                    // comma separates blockless map from blockless reduce
11722                    self.expect(&Token::Comma)?;
11723                    self.parse_block_or_bareword_cmp_block()?
11724                };
11725                self.eat(&Token::Comma);
11726                let line = self.peek_line();
11727                if let Token::Ident(ref kw) = self.peek().clone() {
11728                    if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
11729                        self.advance();
11730                        self.expect(&Token::FatArrow)?;
11731                        let prog = self.parse_assign_expr()?;
11732                        return Ok(Expr {
11733                            kind: ExprKind::PMapReduceExpr {
11734                                map_block,
11735                                reduce_block,
11736                                list: Box::new(Expr {
11737                                    kind: ExprKind::List(vec![]),
11738                                    line,
11739                                }),
11740                                progress: Some(Box::new(prog)),
11741                            },
11742                            line,
11743                        });
11744                    }
11745                }
11746                if matches!(
11747                    self.peek(),
11748                    Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
11749                ) {
11750                    return Ok(Expr {
11751                        kind: ExprKind::PMapReduceExpr {
11752                            map_block,
11753                            reduce_block,
11754                            list: Box::new(Expr {
11755                                kind: ExprKind::List(vec![]),
11756                                line,
11757                            }),
11758                            progress: None,
11759                        },
11760                        line,
11761                    });
11762                }
11763                let mut parts = vec![self.parse_assign_expr()?];
11764                loop {
11765                    if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
11766                        break;
11767                    }
11768                    if matches!(
11769                        self.peek(),
11770                        Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
11771                    ) {
11772                        break;
11773                    }
11774                    if let Token::Ident(ref kw) = self.peek().clone() {
11775                        if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
11776                            self.advance();
11777                            self.expect(&Token::FatArrow)?;
11778                            let prog = self.parse_assign_expr()?;
11779                            return Ok(Expr {
11780                                kind: ExprKind::PMapReduceExpr {
11781                                    map_block,
11782                                    reduce_block,
11783                                    list: Box::new(merge_expr_list(parts)),
11784                                    progress: Some(Box::new(prog)),
11785                                },
11786                                line,
11787                            });
11788                        }
11789                    }
11790                    parts.push(self.parse_assign_expr()?);
11791                }
11792                Ok(Expr {
11793                    kind: ExprKind::PMapReduceExpr {
11794                        map_block,
11795                        reduce_block,
11796                        list: Box::new(merge_expr_list(parts)),
11797                        progress: None,
11798                    },
11799                    line,
11800                })
11801            }
11802            "puniq" => {
11803                if self.pipe_supplies_slurped_list_operand() {
11804                    return Ok(Expr {
11805                        kind: ExprKind::FuncCall {
11806                            name: "puniq".to_string(),
11807                            args: vec![],
11808                        },
11809                        line,
11810                    });
11811                }
11812                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
11813                let mut args = vec![list];
11814                if let Some(p) = progress {
11815                    args.push(p);
11816                }
11817                Ok(Expr {
11818                    kind: ExprKind::FuncCall {
11819                        name: "puniq".to_string(),
11820                        args,
11821                    },
11822                    line,
11823                })
11824            }
11825            "pfirst" => {
11826                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11827                let cr = Expr {
11828                    kind: ExprKind::CodeRef {
11829                        params: vec![],
11830                        body: block,
11831                    },
11832                    line,
11833                };
11834                let mut args = vec![cr, list];
11835                if let Some(p) = progress {
11836                    args.push(p);
11837                }
11838                Ok(Expr {
11839                    kind: ExprKind::FuncCall {
11840                        name: "pfirst".to_string(),
11841                        args,
11842                    },
11843                    line,
11844                })
11845            }
11846            "pany" => {
11847                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11848                let cr = Expr {
11849                    kind: ExprKind::CodeRef {
11850                        params: vec![],
11851                        body: block,
11852                    },
11853                    line,
11854                };
11855                let mut args = vec![cr, list];
11856                if let Some(p) = progress {
11857                    args.push(p);
11858                }
11859                Ok(Expr {
11860                    kind: ExprKind::FuncCall {
11861                        name: "pany".to_string(),
11862                        args,
11863                    },
11864                    line,
11865                })
11866            }
11867            "uniq" | "distinct" => {
11868                if self.pipe_supplies_slurped_list_operand() {
11869                    return Ok(Expr {
11870                        kind: ExprKind::FuncCall {
11871                            name: name.clone(),
11872                            args: vec![],
11873                        },
11874                        line,
11875                    });
11876                }
11877                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
11878                if progress.is_some() {
11879                    return Err(self.syntax_err(
11880                        "`progress =>` is not supported for uniq (use puniq for parallel + progress)",
11881                        line,
11882                    ));
11883                }
11884                Ok(Expr {
11885                    kind: ExprKind::FuncCall {
11886                        name: name.clone(),
11887                        args: vec![list],
11888                    },
11889                    line,
11890                })
11891            }
11892            "flatten" => {
11893                if self.pipe_supplies_slurped_list_operand() {
11894                    return Ok(Expr {
11895                        kind: ExprKind::FuncCall {
11896                            name: "flatten".to_string(),
11897                            args: vec![],
11898                        },
11899                        line,
11900                    });
11901                }
11902                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
11903                if progress.is_some() {
11904                    return Err(self.syntax_err("`progress =>` is not supported for flatten", line));
11905                }
11906                Ok(Expr {
11907                    kind: ExprKind::FuncCall {
11908                        name: "flatten".to_string(),
11909                        args: vec![list],
11910                    },
11911                    line,
11912                })
11913            }
11914            "set" => {
11915                if self.pipe_supplies_slurped_list_operand() {
11916                    return Ok(Expr {
11917                        kind: ExprKind::FuncCall {
11918                            name: "set".to_string(),
11919                            args: vec![],
11920                        },
11921                        line,
11922                    });
11923                }
11924                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
11925                if progress.is_some() {
11926                    return Err(self.syntax_err("`progress =>` is not supported for set", line));
11927                }
11928                Ok(Expr {
11929                    kind: ExprKind::FuncCall {
11930                        name: "set".to_string(),
11931                        args: vec![list],
11932                    },
11933                    line,
11934                })
11935            }
11936            // `size` is the file-size builtin (Perl `-s`), not a list-count alias.
11937            // Defaults to `$_` when no arg is given, like `length`. See
11938            // `builtin_file_size` in builtins.rs for the runtime behavior.
11939            "size" => {
11940                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11941                    return Ok(e);
11942                }
11943                if self.pipe_supplies_slurped_list_operand() {
11944                    return Ok(Expr {
11945                        kind: ExprKind::FuncCall {
11946                            name: "size".to_string(),
11947                            args: vec![],
11948                        },
11949                        line,
11950                    });
11951                }
11952                let a = self.parse_one_arg_or_default()?;
11953                Ok(Expr {
11954                    kind: ExprKind::FuncCall {
11955                        name: "size".to_string(),
11956                        args: vec![a],
11957                    },
11958                    line,
11959                })
11960            }
11961            "list_count" | "list_size" | "count" | "len" | "cnt" => {
11962                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11963                    return Ok(e);
11964                }
11965                if self.pipe_supplies_slurped_list_operand() {
11966                    return Ok(Expr {
11967                        kind: ExprKind::FuncCall {
11968                            name: name.clone(),
11969                            args: vec![],
11970                        },
11971                        line,
11972                    });
11973                }
11974                // `len(EXPR)` / `cnt(EXPR)` / `count(EXPR)` with a tight `(` —
11975                // the parens are function-call syntax, not a parenthesized
11976                // list: stop the argument at `)` so `len(@a) % 2 == 1` is
11977                // `(len(@a)) % 2 == 1`, not `len(@a % 2 == 1)`. Empty parens
11978                // `len()` collapse to a zero-arg call (use the piped operand
11979                // or `$_`). Bare `len` followed by a low-precedence operator
11980                // (`==`, `&&`, `?`, …) also defaults to a zero-arg call so
11981                // `{ len == 0 }` works as a block predicate on the topic.
11982                // Bare `len EXPR` (no parens, e.g. `len @arr`) goes through
11983                // the greedy list-arg parser; this means `len @a + len @b`
11984                // is `len(@a + len(@b))` (returning the length of the sum
11985                // string), not `(len @a) + (len @b)`. Use explicit parens
11986                // when combining `len` with `+`, `-`, comparisons, etc.
11987                let args = if matches!(self.peek(), Token::LParen) {
11988                    self.advance();
11989                    if matches!(self.peek(), Token::RParen) {
11990                        self.advance();
11991                        Vec::new()
11992                    } else {
11993                        let inner = self.parse_expression()?;
11994                        self.expect(&Token::RParen)?;
11995                        vec![inner]
11996                    }
11997                } else if self.peek_is_named_unary_terminator() {
11998                    Vec::new()
11999                } else {
12000                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
12001                    if progress.is_some() {
12002                        return Err(self.syntax_err(
12003                            "`progress =>` is not supported for list_count / list_size / count / cnt",
12004                            line,
12005                        ));
12006                    }
12007                    vec![list]
12008                };
12009                Ok(Expr {
12010                    kind: ExprKind::FuncCall {
12011                        name: name.clone(),
12012                        args,
12013                    },
12014                    line,
12015                })
12016            }
12017            "shuffle" | "shuffled" => {
12018                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12019                    return Ok(e);
12020                }
12021                if self.pipe_supplies_slurped_list_operand() {
12022                    return Ok(Expr {
12023                        kind: ExprKind::FuncCall {
12024                            name: "shuffle".to_string(),
12025                            args: vec![],
12026                        },
12027                        line,
12028                    });
12029                }
12030                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
12031                if progress.is_some() {
12032                    return Err(self.syntax_err("`progress =>` is not supported for shuffle", line));
12033                }
12034                Ok(Expr {
12035                    kind: ExprKind::FuncCall {
12036                        name: "shuffle".to_string(),
12037                        args: vec![list],
12038                    },
12039                    line,
12040                })
12041            }
12042            "chunked" => {
12043                let mut parts = Vec::new();
12044                if self.eat(&Token::LParen) {
12045                    if !matches!(self.peek(), Token::RParen) {
12046                        parts.push(self.parse_assign_expr()?);
12047                        while self.eat(&Token::Comma) {
12048                            if matches!(self.peek(), Token::RParen) {
12049                                break;
12050                            }
12051                            parts.push(self.parse_assign_expr()?);
12052                        }
12053                    }
12054                    self.expect(&Token::RParen)?;
12055                } else {
12056                    // Paren-less `chunked N`: `|>` is a hard terminator, not
12057                    // an operator inside the arg (see
12058                    // `parse_assign_expr_stop_at_pipe`).
12059                    parts.push(self.parse_assign_expr_stop_at_pipe()?);
12060                    loop {
12061                        if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
12062                            break;
12063                        }
12064                        if matches!(
12065                            self.peek(),
12066                            Token::Semicolon
12067                                | Token::RBrace
12068                                | Token::RParen
12069                                | Token::Eof
12070                                | Token::PipeForward
12071                        ) {
12072                            break;
12073                        }
12074                        if self.peek_is_postfix_stmt_modifier_keyword() {
12075                            break;
12076                        }
12077                        parts.push(self.parse_assign_expr_stop_at_pipe()?);
12078                    }
12079                }
12080                if parts.len() == 1 {
12081                    let n = parts.pop().unwrap();
12082                    return Ok(Expr {
12083                        kind: ExprKind::FuncCall {
12084                            name: "chunked".to_string(),
12085                            args: vec![n],
12086                        },
12087                        line,
12088                    });
12089                }
12090                if parts.is_empty() {
12091                    return Ok(Expr {
12092                        kind: ExprKind::FuncCall {
12093                            name: "chunked".to_string(),
12094                            args: parts,
12095                        },
12096                        line,
12097                    });
12098                }
12099                if parts.len() == 2 {
12100                    let n = parts.pop().unwrap();
12101                    let list = parts.pop().unwrap();
12102                    return Ok(Expr {
12103                        kind: ExprKind::FuncCall {
12104                            name: "chunked".to_string(),
12105                            args: vec![list, n],
12106                        },
12107                        line,
12108                    });
12109                }
12110                Err(self.syntax_err(
12111                    "chunked: use LIST |> chunked(N) or chunked((1,2,3), 2)",
12112                    line,
12113                ))
12114            }
12115            "windowed" => {
12116                let mut parts = Vec::new();
12117                if self.eat(&Token::LParen) {
12118                    if !matches!(self.peek(), Token::RParen) {
12119                        parts.push(self.parse_assign_expr()?);
12120                        while self.eat(&Token::Comma) {
12121                            if matches!(self.peek(), Token::RParen) {
12122                                break;
12123                            }
12124                            parts.push(self.parse_assign_expr()?);
12125                        }
12126                    }
12127                    self.expect(&Token::RParen)?;
12128                } else {
12129                    // Paren-less `windowed N`: same `|>`-terminator rule as
12130                    // `chunked` above.
12131                    parts.push(self.parse_assign_expr_stop_at_pipe()?);
12132                    loop {
12133                        if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
12134                            break;
12135                        }
12136                        if matches!(
12137                            self.peek(),
12138                            Token::Semicolon
12139                                | Token::RBrace
12140                                | Token::RParen
12141                                | Token::Eof
12142                                | Token::PipeForward
12143                        ) {
12144                            break;
12145                        }
12146                        if self.peek_is_postfix_stmt_modifier_keyword() {
12147                            break;
12148                        }
12149                        parts.push(self.parse_assign_expr_stop_at_pipe()?);
12150                    }
12151                }
12152                if parts.len() == 1 {
12153                    let n = parts.pop().unwrap();
12154                    return Ok(Expr {
12155                        kind: ExprKind::FuncCall {
12156                            name: "windowed".to_string(),
12157                            args: vec![n],
12158                        },
12159                        line,
12160                    });
12161                }
12162                if parts.is_empty() {
12163                    return Ok(Expr {
12164                        kind: ExprKind::FuncCall {
12165                            name: "windowed".to_string(),
12166                            args: parts,
12167                        },
12168                        line,
12169                    });
12170                }
12171                if parts.len() == 2 {
12172                    let n = parts.pop().unwrap();
12173                    let list = parts.pop().unwrap();
12174                    return Ok(Expr {
12175                        kind: ExprKind::FuncCall {
12176                            name: "windowed".to_string(),
12177                            args: vec![list, n],
12178                        },
12179                        line,
12180                    });
12181                }
12182                Err(self.syntax_err(
12183                    "windowed: use LIST |> windowed(N) or windowed((1,2,3), 2)",
12184                    line,
12185                ))
12186            }
12187            "any" | "all" | "none" => {
12188                // `any(CODEREF, LIST)` with parens — parse as normal call.
12189                if matches!(self.peek(), Token::LParen) {
12190                    self.advance();
12191                    let args = self.parse_arg_list()?;
12192                    self.expect(&Token::RParen)?;
12193                    return Ok(Expr {
12194                        kind: ExprKind::FuncCall {
12195                            name: name.clone(),
12196                            args,
12197                        },
12198                        line,
12199                    });
12200                }
12201                // Coderef-in-block-position: `any $f LIST` / `any $f, LIST` /
12202                // `LIST |> any $f`. Same shape as the block form but uses a
12203                // value expression where `{ BLOCK }` would go.
12204                if let Some(args) = self.try_parse_coderef_listop_args(line)? {
12205                    return Ok(Expr {
12206                        kind: ExprKind::FuncCall {
12207                            name: name.clone(),
12208                            args,
12209                        },
12210                        line,
12211                    });
12212                }
12213                // `any BLOCK LIST` without parens.
12214                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
12215                if progress.is_some() {
12216                    return Err(self.syntax_err(
12217                        "`progress =>` is not supported for any/all/none (use pany for parallel + progress)",
12218                        line,
12219                    ));
12220                }
12221                let cr = Expr {
12222                    kind: ExprKind::CodeRef {
12223                        params: vec![],
12224                        body: block,
12225                    },
12226                    line,
12227                };
12228                Ok(Expr {
12229                    kind: ExprKind::FuncCall {
12230                        name: name.clone(),
12231                        args: vec![cr, list],
12232                    },
12233                    line,
12234                })
12235            }
12236            // Ruby `detect` / `find` — same as `first` (first element matching block).
12237            "first" | "detect" | "find" | "find_index" | "firstidx" | "first_index" => {
12238                let canonical =
12239                    if matches!(name.as_str(), "find_index" | "firstidx" | "first_index") {
12240                        "find_index"
12241                    } else {
12242                        "first"
12243                    };
12244                // `first(CODEREF, LIST)` with parens — parse as normal call.
12245                if matches!(self.peek(), Token::LParen) {
12246                    self.advance();
12247                    let args = self.parse_arg_list()?;
12248                    self.expect(&Token::RParen)?;
12249                    return Ok(Expr {
12250                        kind: ExprKind::FuncCall {
12251                            name: canonical.to_string(),
12252                            args,
12253                        },
12254                        line,
12255                    });
12256                }
12257                // Coderef-in-block-position: `first $f LIST` / `LIST |> first $f`.
12258                if let Some(args) = self.try_parse_coderef_listop_args(line)? {
12259                    return Ok(Expr {
12260                        kind: ExprKind::FuncCall {
12261                            name: canonical.to_string(),
12262                            args,
12263                        },
12264                        line,
12265                    });
12266                }
12267                // `first BLOCK LIST` without parens.
12268                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
12269                if progress.is_some() {
12270                    return Err(self.syntax_err(
12271                        "`progress =>` is not supported for first/detect/find/find_index (use pfirst for parallel + progress)",
12272                        line,
12273                    ));
12274                }
12275                let cr = Expr {
12276                    kind: ExprKind::CodeRef {
12277                        params: vec![],
12278                        body: block,
12279                    },
12280                    line,
12281                };
12282                Ok(Expr {
12283                    kind: ExprKind::FuncCall {
12284                        name: canonical.to_string(),
12285                        args: vec![cr, list],
12286                    },
12287                    line,
12288                })
12289            }
12290            "take_while" | "drop_while" | "skip_while" | "reject" | "grepv" | "tap" | "peek"
12291            | "partition" | "min_by" | "max_by" | "zip_with" | "count_by" => {
12292                // Coderef-in-block-position: `take_while $f LIST` etc.
12293                if let Some(args) = self.try_parse_coderef_listop_args(line)? {
12294                    return Ok(Expr {
12295                        kind: ExprKind::FuncCall {
12296                            name: name.to_string(),
12297                            args,
12298                        },
12299                        line,
12300                    });
12301                }
12302                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
12303                if progress.is_some() {
12304                    return Err(
12305                        self.syntax_err(format!("`progress =>` is not supported for {name}"), line)
12306                    );
12307                }
12308                let cr = Expr {
12309                    kind: ExprKind::CodeRef {
12310                        params: vec![],
12311                        body: block,
12312                    },
12313                    line,
12314                };
12315                Ok(Expr {
12316                    kind: ExprKind::FuncCall {
12317                        name: name.to_string(),
12318                        args: vec![cr, list],
12319                    },
12320                    line,
12321                })
12322            }
12323            "group_by" | "chunk_by" => {
12324                if matches!(self.peek(), Token::LBrace) {
12325                    let (block, list) = self.parse_block_list()?;
12326                    let cr = Expr {
12327                        kind: ExprKind::CodeRef {
12328                            params: vec![],
12329                            body: block,
12330                        },
12331                        line,
12332                    };
12333                    Ok(Expr {
12334                        kind: ExprKind::FuncCall {
12335                            name: name.to_string(),
12336                            args: vec![cr, list],
12337                        },
12338                        line,
12339                    })
12340                } else {
12341                    let key_expr = self.parse_assign_expr()?;
12342                    self.expect(&Token::Comma)?;
12343                    let list_parts = self.parse_list_until_terminator()?;
12344                    let list_expr = if list_parts.len() == 1 {
12345                        list_parts.into_iter().next().unwrap()
12346                    } else {
12347                        Expr {
12348                            kind: ExprKind::List(list_parts),
12349                            line,
12350                        }
12351                    };
12352                    Ok(Expr {
12353                        kind: ExprKind::FuncCall {
12354                            name: name.to_string(),
12355                            args: vec![key_expr, list_expr],
12356                        },
12357                        line,
12358                    })
12359                }
12360            }
12361            "with_index" => {
12362                if self.pipe_supplies_slurped_list_operand() {
12363                    return Ok(Expr {
12364                        kind: ExprKind::FuncCall {
12365                            name: "with_index".to_string(),
12366                            args: vec![],
12367                        },
12368                        line,
12369                    });
12370                }
12371                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
12372                if progress.is_some() {
12373                    return Err(
12374                        self.syntax_err("`progress =>` is not supported for with_index", line)
12375                    );
12376                }
12377                Ok(Expr {
12378                    kind: ExprKind::FuncCall {
12379                        name: "with_index".to_string(),
12380                        args: vec![list],
12381                    },
12382                    line,
12383                })
12384            }
12385            "pcache" => {
12386                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
12387                Ok(Expr {
12388                    kind: ExprKind::PcacheExpr {
12389                        block,
12390                        list: Box::new(list),
12391                        progress: progress.map(Box::new),
12392                    },
12393                    line,
12394                })
12395            }
12396            "pselect" => {
12397                let paren = self.eat(&Token::LParen);
12398                let (receivers, timeout) = self.parse_comma_expr_list_with_timeout_tail(paren)?;
12399                if paren {
12400                    self.expect(&Token::RParen)?;
12401                }
12402                if receivers.is_empty() {
12403                    return Err(self.syntax_err("pselect needs at least one receiver", line));
12404                }
12405                Ok(Expr {
12406                    kind: ExprKind::PselectExpr {
12407                        receivers,
12408                        timeout: timeout.map(Box::new),
12409                    },
12410                    line,
12411                })
12412            }
12413            "open" => {
12414                let paren = matches!(self.peek(), Token::LParen);
12415                if paren {
12416                    self.advance();
12417                }
12418                if matches!(self.peek(), Token::Ident(ref s) if s == "my") {
12419                    self.advance();
12420                    let name = self.parse_scalar_var_name()?;
12421                    self.expect(&Token::Comma)?;
12422                    let mode = self.parse_assign_expr()?;
12423                    let file = if self.eat(&Token::Comma) {
12424                        Some(self.parse_assign_expr()?)
12425                    } else {
12426                        None
12427                    };
12428                    if paren {
12429                        self.expect(&Token::RParen)?;
12430                    }
12431                    Ok(Expr {
12432                        kind: ExprKind::Open {
12433                            handle: Box::new(Expr {
12434                                kind: ExprKind::OpenMyHandle { name },
12435                                line,
12436                            }),
12437                            mode: Box::new(mode),
12438                            file: file.map(Box::new),
12439                        },
12440                        line,
12441                    })
12442                } else {
12443                    // Perl convention: `open FH, "<", path` — an all-uppercase
12444                    // (or `_`) bareword in the filehandle slot is a literal
12445                    // handle name, never a constant / sub / bareword
12446                    // expression. Without this special-case, registered
12447                    // constants like `PI` / `TAU` / `E` would override the
12448                    // documented Perl idiom and the handle would register
12449                    // under the constant's numeric value.
12450                    let handle_lit = self.take_bareword_filehandle();
12451                    if handle_lit.is_some() {
12452                        // Consume the comma after the bareword filehandle so
12453                        // the arg parser starts at the mode expression.
12454                        self.expect(&Token::Comma)?;
12455                    }
12456                    let args = if paren {
12457                        self.parse_arg_list()?
12458                    } else {
12459                        self.parse_list_until_terminator()?
12460                    };
12461                    if paren {
12462                        self.expect(&Token::RParen)?;
12463                    }
12464                    let total = handle_lit.is_some() as usize + args.len();
12465                    if total < 2 {
12466                        return Err(self.syntax_err("open requires at least 2 arguments", line));
12467                    }
12468                    let (handle_expr, mode_expr, file_expr) = match handle_lit {
12469                        Some(name) => {
12470                            let h = Expr {
12471                                kind: ExprKind::String(name),
12472                                line,
12473                            };
12474                            (h, args[0].clone(), args.get(1).cloned())
12475                        }
12476                        None => (args[0].clone(), args[1].clone(), args.get(2).cloned()),
12477                    };
12478                    Ok(Expr {
12479                        kind: ExprKind::Open {
12480                            handle: Box::new(handle_expr),
12481                            mode: Box::new(mode_expr),
12482                            file: file_expr.map(Box::new),
12483                        },
12484                        line,
12485                    })
12486                }
12487            }
12488            "close" => {
12489                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12490                    return Ok(e);
12491                }
12492                // `close FH` — bareword filehandle slot takes a literal name.
12493                let a = self
12494                    .take_bareword_filehandle_arg(line)
12495                    .map(Ok)
12496                    .unwrap_or_else(|| self.parse_one_arg_or_default())?;
12497                Ok(Expr {
12498                    kind: ExprKind::Close(Box::new(a)),
12499                    line,
12500                })
12501            }
12502            "opendir" => {
12503                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12504                    return Ok(e);
12505                }
12506                let args = self.parse_builtin_args()?;
12507                if args.len() != 2 {
12508                    return Err(self.syntax_err("opendir requires two arguments", line));
12509                }
12510                Ok(Expr {
12511                    kind: ExprKind::Opendir {
12512                        handle: Box::new(args[0].clone()),
12513                        path: Box::new(args[1].clone()),
12514                    },
12515                    line,
12516                })
12517            }
12518            "readdir" => {
12519                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12520                    return Ok(e);
12521                }
12522                let a = self.parse_one_arg()?;
12523                Ok(Expr {
12524                    kind: ExprKind::Readdir(Box::new(a)),
12525                    line,
12526                })
12527            }
12528            "closedir" => {
12529                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12530                    return Ok(e);
12531                }
12532                let a = self.parse_one_arg()?;
12533                Ok(Expr {
12534                    kind: ExprKind::Closedir(Box::new(a)),
12535                    line,
12536                })
12537            }
12538            "rewinddir" => {
12539                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12540                    return Ok(e);
12541                }
12542                let a = self.parse_one_arg()?;
12543                Ok(Expr {
12544                    kind: ExprKind::Rewinddir(Box::new(a)),
12545                    line,
12546                })
12547            }
12548            "telldir" => {
12549                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12550                    return Ok(e);
12551                }
12552                let a = self.parse_one_arg()?;
12553                Ok(Expr {
12554                    kind: ExprKind::Telldir(Box::new(a)),
12555                    line,
12556                })
12557            }
12558            "seekdir" => {
12559                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12560                    return Ok(e);
12561                }
12562                let args = self.parse_builtin_args()?;
12563                if args.len() != 2 {
12564                    return Err(self.syntax_err("seekdir requires two arguments", line));
12565                }
12566                Ok(Expr {
12567                    kind: ExprKind::Seekdir {
12568                        handle: Box::new(args[0].clone()),
12569                        position: Box::new(args[1].clone()),
12570                    },
12571                    line,
12572                })
12573            }
12574            "eof" => {
12575                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12576                    return Ok(e);
12577                }
12578                // `eof FH` — bareword filehandle slot (no parens) takes a
12579                // literal name. `eof(FH)` / `eof($fh)` / `eof("FH")` keep
12580                // their general-expression handling.
12581                if let Some(a) = self.take_bareword_filehandle_arg(line) {
12582                    return Ok(Expr {
12583                        kind: ExprKind::Eof(Some(Box::new(a))),
12584                        line,
12585                    });
12586                }
12587                if matches!(self.peek(), Token::LParen) {
12588                    self.advance();
12589                    if matches!(self.peek(), Token::RParen) {
12590                        self.advance();
12591                        Ok(Expr {
12592                            kind: ExprKind::Eof(None),
12593                            line,
12594                        })
12595                    } else {
12596                        // Inside the parens, bareword still wins as a handle.
12597                        let a = self
12598                            .take_bareword_filehandle_arg(line)
12599                            .map(Ok)
12600                            .unwrap_or_else(|| self.parse_expression())?;
12601                        self.expect(&Token::RParen)?;
12602                        Ok(Expr {
12603                            kind: ExprKind::Eof(Some(Box::new(a))),
12604                            line,
12605                        })
12606                    }
12607                } else {
12608                    Ok(Expr {
12609                        kind: ExprKind::Eof(None),
12610                        line,
12611                    })
12612                }
12613            }
12614            "system" => {
12615                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12616                    return Ok(e);
12617                }
12618                let args = self.parse_builtin_args()?;
12619                Ok(Expr {
12620                    kind: ExprKind::System(args),
12621                    line,
12622                })
12623            }
12624            "exec" => {
12625                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12626                    return Ok(e);
12627                }
12628                let args = self.parse_builtin_args()?;
12629                Ok(Expr {
12630                    kind: ExprKind::Exec(args),
12631                    line,
12632                })
12633            }
12634            "eval" => {
12635                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12636                    return Ok(e);
12637                }
12638                let a = if matches!(self.peek(), Token::LBrace) {
12639                    let block = self.parse_block()?;
12640                    Expr {
12641                        kind: ExprKind::CodeRef {
12642                            params: vec![],
12643                            body: block,
12644                        },
12645                        line,
12646                    }
12647                } else {
12648                    self.parse_one_arg_or_default()?
12649                };
12650                Ok(Expr {
12651                    kind: ExprKind::Eval(Box::new(a)),
12652                    line,
12653                })
12654            }
12655            "do" => {
12656                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12657                    return Ok(e);
12658                }
12659                let a = self.parse_one_arg()?;
12660                Ok(Expr {
12661                    kind: ExprKind::Do(Box::new(a)),
12662                    line,
12663                })
12664            }
12665            "require" => {
12666                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12667                    return Ok(e);
12668                }
12669                let a = self.parse_one_arg()?;
12670                Ok(Expr {
12671                    kind: ExprKind::Require(Box::new(a)),
12672                    line,
12673                })
12674            }
12675            "exit" => {
12676                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12677                    return Ok(e);
12678                }
12679                if matches!(
12680                    self.peek(),
12681                    Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
12682                ) {
12683                    Ok(Expr {
12684                        kind: ExprKind::Exit(None),
12685                        line,
12686                    })
12687                } else {
12688                    let a = self.parse_one_arg()?;
12689                    Ok(Expr {
12690                        kind: ExprKind::Exit(Some(Box::new(a))),
12691                        line,
12692                    })
12693                }
12694            }
12695            "chdir" => {
12696                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12697                    return Ok(e);
12698                }
12699                let a = self.parse_one_arg_or_default()?;
12700                Ok(Expr {
12701                    kind: ExprKind::Chdir(Box::new(a)),
12702                    line,
12703                })
12704            }
12705            "mkdir" => {
12706                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12707                    return Ok(e);
12708                }
12709                let args = self.parse_builtin_args()?;
12710                Ok(Expr {
12711                    kind: ExprKind::Mkdir {
12712                        path: Box::new(args[0].clone()),
12713                        mode: args.get(1).cloned().map(Box::new),
12714                    },
12715                    line,
12716                })
12717            }
12718            "unlink" | "rm" => {
12719                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12720                    return Ok(e);
12721                }
12722                let args = self.parse_builtin_args()?;
12723                Ok(Expr {
12724                    kind: ExprKind::Unlink(args),
12725                    line,
12726                })
12727            }
12728            "rename" => {
12729                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12730                    return Ok(e);
12731                }
12732                let args = self.parse_builtin_args()?;
12733                if args.len() != 2 {
12734                    return Err(self.syntax_err("rename requires two arguments", line));
12735                }
12736                Ok(Expr {
12737                    kind: ExprKind::Rename {
12738                        old: Box::new(args[0].clone()),
12739                        new: Box::new(args[1].clone()),
12740                    },
12741                    line,
12742                })
12743            }
12744            "chmod" => {
12745                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12746                    return Ok(e);
12747                }
12748                let args = self.parse_builtin_args()?;
12749                if args.len() < 2 {
12750                    return Err(self.syntax_err("chmod requires mode and at least one file", line));
12751                }
12752                Ok(Expr {
12753                    kind: ExprKind::Chmod(args),
12754                    line,
12755                })
12756            }
12757            "chown" => {
12758                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12759                    return Ok(e);
12760                }
12761                let args = self.parse_builtin_args()?;
12762                if args.len() < 3 {
12763                    return Err(
12764                        self.syntax_err("chown requires uid, gid, and at least one file", line)
12765                    );
12766                }
12767                Ok(Expr {
12768                    kind: ExprKind::Chown(args),
12769                    line,
12770                })
12771            }
12772            "stat" => {
12773                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12774                    return Ok(e);
12775                }
12776                let args = self.parse_builtin_args()?;
12777                let arg = if args.len() == 1 {
12778                    args[0].clone()
12779                } else if args.is_empty() {
12780                    Expr {
12781                        kind: ExprKind::ScalarVar("_".into()),
12782                        line,
12783                    }
12784                } else {
12785                    return Err(self.syntax_err("stat requires zero or one argument", line));
12786                };
12787                Ok(Expr {
12788                    kind: ExprKind::Stat(Box::new(arg)),
12789                    line,
12790                })
12791            }
12792            "lstat" => {
12793                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12794                    return Ok(e);
12795                }
12796                let args = self.parse_builtin_args()?;
12797                let arg = if args.len() == 1 {
12798                    args[0].clone()
12799                } else if args.is_empty() {
12800                    Expr {
12801                        kind: ExprKind::ScalarVar("_".into()),
12802                        line,
12803                    }
12804                } else {
12805                    return Err(self.syntax_err("lstat requires zero or one argument", line));
12806                };
12807                Ok(Expr {
12808                    kind: ExprKind::Lstat(Box::new(arg)),
12809                    line,
12810                })
12811            }
12812            "link" => {
12813                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12814                    return Ok(e);
12815                }
12816                let args = self.parse_builtin_args()?;
12817                if args.len() != 2 {
12818                    return Err(self.syntax_err("link requires two arguments", line));
12819                }
12820                Ok(Expr {
12821                    kind: ExprKind::Link {
12822                        old: Box::new(args[0].clone()),
12823                        new: Box::new(args[1].clone()),
12824                    },
12825                    line,
12826                })
12827            }
12828            "symlink" => {
12829                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12830                    return Ok(e);
12831                }
12832                let args = self.parse_builtin_args()?;
12833                if args.len() != 2 {
12834                    return Err(self.syntax_err("symlink requires two arguments", line));
12835                }
12836                Ok(Expr {
12837                    kind: ExprKind::Symlink {
12838                        old: Box::new(args[0].clone()),
12839                        new: Box::new(args[1].clone()),
12840                    },
12841                    line,
12842                })
12843            }
12844            "readlink" => {
12845                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12846                    return Ok(e);
12847                }
12848                let args = self.parse_builtin_args()?;
12849                let arg = if args.len() == 1 {
12850                    args[0].clone()
12851                } else if args.is_empty() {
12852                    Expr {
12853                        kind: ExprKind::ScalarVar("_".into()),
12854                        line,
12855                    }
12856                } else {
12857                    return Err(self.syntax_err("readlink requires zero or one argument", line));
12858                };
12859                Ok(Expr {
12860                    kind: ExprKind::Readlink(Box::new(arg)),
12861                    line,
12862                })
12863            }
12864            "files" => {
12865                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12866                    return Ok(e);
12867                }
12868                let args = self.parse_builtin_args()?;
12869                Ok(Expr {
12870                    kind: ExprKind::Files(args),
12871                    line,
12872                })
12873            }
12874            "filesf" | "f" => {
12875                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12876                    return Ok(e);
12877                }
12878                let args = self.parse_builtin_args()?;
12879                Ok(Expr {
12880                    kind: ExprKind::Filesf(args),
12881                    line,
12882                })
12883            }
12884            "fr" => {
12885                let args = self.parse_builtin_args()?;
12886                Ok(Expr {
12887                    kind: ExprKind::FilesfRecursive(args),
12888                    line,
12889                })
12890            }
12891            "dirs" | "d" => {
12892                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12893                    return Ok(e);
12894                }
12895                let args = self.parse_builtin_args()?;
12896                Ok(Expr {
12897                    kind: ExprKind::Dirs(args),
12898                    line,
12899                })
12900            }
12901            "dr" => {
12902                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12903                    return Ok(e);
12904                }
12905                let args = self.parse_builtin_args()?;
12906                Ok(Expr {
12907                    kind: ExprKind::DirsRecursive(args),
12908                    line,
12909                })
12910            }
12911            "sym_links" => {
12912                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12913                    return Ok(e);
12914                }
12915                let args = self.parse_builtin_args()?;
12916                Ok(Expr {
12917                    kind: ExprKind::SymLinks(args),
12918                    line,
12919                })
12920            }
12921            "sockets" => {
12922                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12923                    return Ok(e);
12924                }
12925                let args = self.parse_builtin_args()?;
12926                Ok(Expr {
12927                    kind: ExprKind::Sockets(args),
12928                    line,
12929                })
12930            }
12931            "pipes" => {
12932                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12933                    return Ok(e);
12934                }
12935                let args = self.parse_builtin_args()?;
12936                Ok(Expr {
12937                    kind: ExprKind::Pipes(args),
12938                    line,
12939                })
12940            }
12941            "block_devices" => {
12942                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12943                    return Ok(e);
12944                }
12945                let args = self.parse_builtin_args()?;
12946                Ok(Expr {
12947                    kind: ExprKind::BlockDevices(args),
12948                    line,
12949                })
12950            }
12951            "char_devices" => {
12952                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12953                    return Ok(e);
12954                }
12955                let args = self.parse_builtin_args()?;
12956                Ok(Expr {
12957                    kind: ExprKind::CharDevices(args),
12958                    line,
12959                })
12960            }
12961            "exe" | "executables" => {
12962                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12963                    return Ok(e);
12964                }
12965                let args = self.parse_builtin_args()?;
12966                Ok(Expr {
12967                    kind: ExprKind::Executables(args),
12968                    line,
12969                })
12970            }
12971            "glob" => {
12972                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12973                    return Ok(e);
12974                }
12975                let args = self.parse_builtin_args()?;
12976                Ok(Expr {
12977                    kind: ExprKind::Glob(args),
12978                    line,
12979                })
12980            }
12981            "glob_par" => {
12982                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12983                    return Ok(e);
12984                }
12985                let (args, progress) = self.parse_glob_par_or_par_sed_args()?;
12986                Ok(Expr {
12987                    kind: ExprKind::GlobPar { args, progress },
12988                    line,
12989                })
12990            }
12991            "par_sed" => {
12992                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12993                    return Ok(e);
12994                }
12995                let (args, progress) = self.parse_glob_par_or_par_sed_args()?;
12996                Ok(Expr {
12997                    kind: ExprKind::ParSed { args, progress },
12998                    line,
12999                })
13000            }
13001            "bless" => {
13002                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13003                    return Ok(e);
13004                }
13005                let args = self.parse_builtin_args()?;
13006                Ok(Expr {
13007                    kind: ExprKind::Bless {
13008                        ref_expr: Box::new(args[0].clone()),
13009                        class: args.get(1).cloned().map(Box::new),
13010                    },
13011                    line,
13012                })
13013            }
13014            "caller" => {
13015                if matches!(self.peek(), Token::LParen) {
13016                    self.advance();
13017                    if matches!(self.peek(), Token::RParen) {
13018                        self.advance();
13019                        Ok(Expr {
13020                            kind: ExprKind::Caller(None),
13021                            line,
13022                        })
13023                    } else {
13024                        let a = self.parse_expression()?;
13025                        self.expect(&Token::RParen)?;
13026                        Ok(Expr {
13027                            kind: ExprKind::Caller(Some(Box::new(a))),
13028                            line,
13029                        })
13030                    }
13031                } else {
13032                    Ok(Expr {
13033                        kind: ExprKind::Caller(None),
13034                        line,
13035                    })
13036                }
13037            }
13038            "wantarray" => {
13039                if crate::no_interop_mode() {
13040                    return Err(self.syntax_err(
13041                        "stryke `wantarray` is rejected under --no-interop — \
13042                         use explicit return-shape (`@result` vs `$scalar`) \
13043                         or pass a flag arg instead of context-sniffing",
13044                        line,
13045                    ));
13046                }
13047                if matches!(self.peek(), Token::LParen) {
13048                    self.advance();
13049                    self.expect(&Token::RParen)?;
13050                }
13051                Ok(Expr {
13052                    kind: ExprKind::Wantarray,
13053                    line,
13054                })
13055            }
13056            "sub" => {
13057                // In no-interop mode, `sub {}` is not valid — must use `fn {}`
13058                if crate::no_interop_mode() {
13059                    return Err(self.syntax_err(
13060                        "stryke uses `fn {}` instead of `sub {}` (--no-interop)",
13061                        line,
13062                    ));
13063                }
13064                // Anonymous sub — optional prototype `sub () { }` (e.g. Carp.pm `*X = sub () { 1 }`)
13065                let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
13066                let body = self.parse_block()?;
13067                Ok(Expr {
13068                    kind: ExprKind::CodeRef { params, body },
13069                    line,
13070                })
13071            }
13072            "fn" => {
13073                // Anonymous fn — stryke syntax for anonymous subroutines
13074                let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
13075                self.parse_sub_attributes()?;
13076                let body = self.parse_fn_eq_body_or_block(false)?;
13077                Ok(Expr {
13078                    kind: ExprKind::CodeRef { params, body },
13079                    line,
13080                })
13081            }
13082            _ => {
13083                // Generic function call
13084                // Check for fat arrow (bareword string in hash) — except for
13085                // topic-slot barewords (`_`, `_<`, `_0`, `_0<`, …), which must
13086                // resolve to the topic value, not the literal name.
13087                if matches!(self.peek(), Token::FatArrow) && !Self::is_underscore_topic_slot(&name)
13088                {
13089                    return Ok(Expr {
13090                        kind: ExprKind::String(name),
13091                        line,
13092                    });
13093                }
13094                // Bare `_` in expression position → topic variable `$_`.
13095                // Allows concise blocks: `map { _ * 2 }`, `fi { _ > 5 }`.
13096                // Also handles the outer-topic chain: `_<`, `_<<`, `_<<<`,
13097                // `_<<<<` for 1..4 frames up — and the positional matrix:
13098                // `_0<<<<`, `_1<<<<`, `_N<<<<` (N positionals × 5 levels).
13099                // `_0` is canonically aliased to `_` at every level (see
13100                // `Scope::set_closure_args`).
13101                //
13102                // Stryke string-index sugar: `_[N]` (bareword, no sigil) is
13103                // an alias for `_!N!` — char-of-topic substring. The sigil
13104                // form `$_[N]` keeps Perl's `@_`-access semantics (first
13105                // positional arg). We dispatch here, before the generic
13106                // ArrayElement path, so the AST for `_[N]` carries the
13107                // synthetic `__topicstr__$NAME` flag the interpreter / VM
13108                // strip and route to char-of-string.
13109                if Self::is_underscore_topic_slot(&name) {
13110                    if matches!(self.peek(), Token::LBracket) && self.peek_line() == line {
13111                        self.advance(); // [
13112                        let index = self.parse_expression()?;
13113                        self.expect(&Token::RBracket)?;
13114                        return Ok(Expr {
13115                            kind: ExprKind::ArrayElement {
13116                                array: format!("__topicstr__{}", name),
13117                                index: Box::new(index),
13118                            },
13119                            line,
13120                        });
13121                    }
13122                    return Ok(Expr {
13123                        kind: ExprKind::ScalarVar(name.clone()),
13124                        line,
13125                    });
13126                }
13127                // Function call with optional parens
13128                if matches!(self.peek(), Token::LParen) {
13129                    self.advance();
13130                    let args = self.parse_arg_list()?;
13131                    self.expect(&Token::RParen)?;
13132                    Ok(Expr {
13133                        kind: ExprKind::FuncCall { name, args },
13134                        line,
13135                    })
13136                } else if self.peek().is_term_start()
13137                    && !(matches!(self.peek(), Token::Ident(ref kw) if kw == "sub")
13138                        && matches!(self.peek_at(1), Token::Ident(_)))
13139                    && !(self.suppress_parenless_call > 0 && matches!(self.peek(), Token::Ident(_)))
13140                    && !(matches!(self.peek(), Token::LBrace)
13141                        && self.peek_line() > self.prev_line())
13142                    && !(matches!(self.peek(), Token::BitNot)
13143                        && self.suppress_tilde_range == 0
13144                        && matches!(
13145                            self.peek_at(1),
13146                            Token::Ident(_) | Token::Integer(_) | Token::Float(_)
13147                        ))
13148                {
13149                    // Perl allows func arg without parens
13150                    // Guard: `sub <name> { }` is a named sub declaration (new
13151                    // statement), not an argument to the preceding call.
13152                    // Guard: suppress_parenless_call > 0 with Ident prevents consuming
13153                    // barewords (used by thread macro so `t Color::Red p` treats
13154                    // `p` as a stage, not an argument to the enum variant), but
13155                    // still allows `{` for struct/hash literals like `t Foo { x => 1 } p`.
13156                    // Guard: `{` on a new line is a new statement (hashref/block),
13157                    // not an argument to the preceding bareword call.
13158                    // Guard: `~Ident` / `~Integer` / `~Float` after a bareword is
13159                    // the universal-tilde range separator (`I~M~5`, `Mon~Fri`,
13160                    // `Jan~Dec~2`), not unary BitNot of an arg. Bail to Bareword
13161                    // so the outer `parse_range` consumes `~` as the range op.
13162                    let args = self.parse_list_until_terminator()?;
13163                    Ok(Expr {
13164                        kind: ExprKind::FuncCall { name, args },
13165                        line,
13166                    })
13167                } else {
13168                    // No parens, no visible arguments — emit a Bareword.
13169                    // At runtime, Bareword tries sub resolution first (zero-arg
13170                    // call) and falls back to a string value.  stryke extension
13171                    // contexts (pipe-forward, map/fore) lift Bareword → FuncCall
13172                    // with `$_` injection separately.
13173                    Ok(Expr {
13174                        kind: ExprKind::Bareword(name),
13175                        line,
13176                    })
13177                }
13178            }
13179        }
13180    }
13181
13182    /// `open FH, ...` / `close FH` / `eof FH` — Perl's convention is that an
13183    /// all-uppercase (letters / digits / `_`, starting with a letter or `_`)
13184    /// bareword in the filehandle slot is a literal handle name, never a
13185    /// constant or sub call. This shadows registered constants like `PI`,
13186    /// `TAU`, `E` and the rare uppercase-letter filehandles (`H`, `O`, …)
13187    /// that would otherwise route through the bareword resolver.
13188    ///
13189    /// Returns `Some(name)` when the next token is such a bareword and the
13190    /// token after it is one of the accepted terminators (any of `accept`,
13191    /// or — when `accept` is empty — any of `,`, `;`, `)`, `}`, `|>`, Eof).
13192    /// Otherwise returns `None` and leaves the cursor untouched.
13193    fn take_bareword_filehandle_if(&mut self, accept: &[Token]) -> Option<String> {
13194        let Token::Ident(h) = self.peek().clone() else {
13195            return None;
13196        };
13197        let mut chars = h.chars();
13198        let first = chars.next()?;
13199        if !(first.is_ascii_uppercase() || first == '_') {
13200            return None;
13201        }
13202        if !h
13203            .chars()
13204            .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')
13205        {
13206            return None;
13207        }
13208        let next = self.peek_at(1);
13209        let ok = if accept.is_empty() {
13210            matches!(
13211                next,
13212                Token::Comma
13213                    | Token::Semicolon
13214                    | Token::RParen
13215                    | Token::RBrace
13216                    | Token::Eof
13217                    | Token::PipeForward
13218            )
13219        } else {
13220            accept
13221                .iter()
13222                .any(|t| std::mem::discriminant(t) == std::mem::discriminant(next))
13223        };
13224        if !ok {
13225            return None;
13226        }
13227        self.advance();
13228        Some(h)
13229    }
13230
13231    /// `open FH, …` — bareword filehandle followed by a comma.
13232    fn take_bareword_filehandle(&mut self) -> Option<String> {
13233        self.take_bareword_filehandle_if(&[Token::Comma])
13234    }
13235
13236    /// `close FH` / `eof FH` — bareword filehandle followed by a statement
13237    /// terminator. Returns a `String` expression to splice into the arg
13238    /// slot, or `None` if the next token isn't a literal filehandle.
13239    fn take_bareword_filehandle_arg(&mut self, line: usize) -> Option<Expr> {
13240        self.take_bareword_filehandle_if(&[]).map(|name| Expr {
13241            kind: ExprKind::String(name),
13242            line,
13243        })
13244    }
13245
13246    fn parse_print_like(
13247        &mut self,
13248        make: impl FnOnce(Option<String>, Vec<Expr>) -> ExprKind,
13249    ) -> StrykeResult<Expr> {
13250        let line = self.peek_line();
13251        // Check for filehandle: print STDERR "msg"  /  print $fh "msg"
13252        let handle = if let Token::Ident(ref h) = self.peek().clone() {
13253            if h.chars().all(|c| c.is_uppercase() || c == '_')
13254                && !matches!(self.peek(), Token::LParen)
13255            {
13256                let h = h.clone();
13257                let saved = self.pos;
13258                self.advance();
13259                // Verify next token is a term start (not operator).
13260                // Guard: `~Ident` / `~Integer` / `~Float` is a universal-tilde
13261                // range separator (`p I~M~5`, `p Mon~Fri`), not unary BitNot of
13262                // an arg. Bail filehandle detection so the bareword `I` flows
13263                // into the regular expression path where `parse_range` consumes
13264                // `~` as the range op.
13265                let is_tilde_range_after = matches!(self.peek(), Token::BitNot)
13266                    && self.suppress_tilde_range == 0
13267                    && matches!(
13268                        self.peek_at(1),
13269                        Token::Ident(_) | Token::Integer(_) | Token::Float(_)
13270                    );
13271                if !is_tilde_range_after
13272                    && (self.peek().is_term_start()
13273                        || matches!(
13274                            self.peek(),
13275                            Token::DoubleString(_)
13276                                | Token::BacktickString(_)
13277                                | Token::SingleString(_)
13278                        ))
13279                {
13280                    Some(h)
13281                } else {
13282                    self.pos = saved;
13283                    None
13284                }
13285            } else {
13286                None
13287            }
13288        } else if let Token::ScalarVar(ref v) = self.peek().clone() {
13289            // `print $fh "msg"` — scalar variable as indirect filehandle.
13290            // Treat as handle when the next token (after $var) is a term-start or
13291            // string literal *without* a preceding comma/operator, matching Perl's
13292            // indirect-object heuristic.
13293            // Exclude `$_` — it's virtually always the topic variable, not a handle.
13294            // Exclude `[` and `{` — those are array/hash subscripts on the variable
13295            // itself (`print $F[0]`, `print $h{k}`), not separate print arguments.
13296            // Exclude statement modifiers (`if`/`unless`/`while`/`until`/`for`/`foreach`)
13297            // — `print $_ if COND` prints `$_` to STDOUT, not to a handle named `$_`.
13298            // Exclude tokens on a later line — a newline ends the print statement
13299            // in stryke, so `p $j\nmy $k = …` must not absorb the following `my`.
13300            let v = v.clone();
13301            if v == "_" {
13302                None
13303            } else {
13304                let saved = self.pos;
13305                let var_line = self.peek_line();
13306                self.advance();
13307                let next = self.peek().clone();
13308                let next_line = self.peek_line();
13309                let is_stmt_modifier = matches!(&next, Token::Ident(kw)
13310                    if matches!(kw.as_str(), "if" | "unless" | "while" | "until" | "for" | "foreach"));
13311                if !is_stmt_modifier
13312                    && next_line == var_line
13313                    && !matches!(next, Token::LBracket | Token::LBrace)
13314                    && (next.is_term_start()
13315                        || matches!(
13316                            next,
13317                            Token::DoubleString(_)
13318                                | Token::BacktickString(_)
13319                                | Token::SingleString(_)
13320                        ))
13321                {
13322                    // Next token looks like a print argument — $var is the handle.
13323                    Some(format!("${v}"))
13324                } else {
13325                    self.pos = saved;
13326                    None
13327                }
13328            }
13329        } else {
13330            None
13331        };
13332        // `print()` / `say()` / `printf()` — empty parens default to `$_`,
13333        // matching Perl 5: `perldoc -f print` / `-f say` say "If no arguments
13334        // are given, prints $_." (Same convention as the topic-default unary
13335        // builtins handled in `parse_one_arg_or_default`.)
13336        // Use `parse_list_until_terminator_allow_pipe` so that `p @a |> sum`
13337        // parses as `p(sum(@a))`, matching `~>` thread-first behavior.
13338        let args =
13339            if matches!(self.peek(), Token::LParen) && matches!(self.peek_at(1), Token::RParen) {
13340                let line_topic = self.peek_line();
13341                self.advance(); // (
13342                self.advance(); // )
13343                vec![Expr {
13344                    kind: ExprKind::ScalarVar("_".into()),
13345                    line: line_topic,
13346                }]
13347            } else {
13348                self.parse_list_until_terminator_allow_pipe()?
13349            };
13350        Ok(Expr {
13351            kind: make(handle, args),
13352            line,
13353        })
13354    }
13355
13356    fn parse_block_list(&mut self) -> StrykeResult<(Block, Expr)> {
13357        let block = self.parse_block()?;
13358        let block_end_line = self.prev_line();
13359        self.eat(&Token::Comma);
13360        // On the RHS of `|>`, the list operand is supplied by the piped LHS
13361        // and will be substituted at desugar time — accept a placeholder when
13362        // we're at a terminator here or on a new line (implicit semicolon).
13363        if self.in_pipe_rhs()
13364            && (matches!(
13365                self.peek(),
13366                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
13367            ) || self.peek_line() > block_end_line)
13368        {
13369            let line = self.peek_line();
13370            return Ok((block, self.pipe_placeholder_list(line)));
13371        }
13372        let list = self.parse_expression()?;
13373        Ok((block, list))
13374    }
13375
13376    /// Comma-separated expressions with optional trailing `timeout => SECS` (for `pselect`).
13377    /// When `paren` is true, stops at `)` as well as normal terminators.
13378    fn parse_comma_expr_list_with_timeout_tail(
13379        &mut self,
13380        paren: bool,
13381    ) -> StrykeResult<(Vec<Expr>, Option<Expr>)> {
13382        let mut parts = vec![self.parse_assign_expr()?];
13383        loop {
13384            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
13385                break;
13386            }
13387            if paren && matches!(self.peek(), Token::RParen) {
13388                break;
13389            }
13390            if matches!(
13391                self.peek(),
13392                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
13393            ) {
13394                break;
13395            }
13396            if self.peek_is_postfix_stmt_modifier_keyword() {
13397                break;
13398            }
13399            if let Token::Ident(ref kw) = self.peek().clone() {
13400                if kw == "timeout" && matches!(self.peek_at(1), Token::FatArrow) {
13401                    self.advance();
13402                    self.expect(&Token::FatArrow)?;
13403                    let t = self.parse_assign_expr()?;
13404                    return Ok((parts, Some(t)));
13405                }
13406            }
13407            parts.push(self.parse_assign_expr()?);
13408        }
13409        Ok((parts, None))
13410    }
13411
13412    /// `preduce_init EXPR, BLOCK, LIST` with optional `, progress => EXPR`.
13413    fn parse_init_block_then_list_optional_progress(
13414        &mut self,
13415    ) -> StrykeResult<(Expr, Block, Expr, Option<Expr>)> {
13416        let init = self.parse_assign_expr()?;
13417        self.expect(&Token::Comma)?;
13418        let block = self.parse_block_or_bareword_block()?;
13419        self.eat(&Token::Comma);
13420        let line = self.peek_line();
13421        if let Token::Ident(ref kw) = self.peek().clone() {
13422            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
13423                self.advance();
13424                self.expect(&Token::FatArrow)?;
13425                let prog = self.parse_assign_expr()?;
13426                return Ok((
13427                    init,
13428                    block,
13429                    Expr {
13430                        kind: ExprKind::List(vec![]),
13431                        line,
13432                    },
13433                    Some(prog),
13434                ));
13435            }
13436        }
13437        if matches!(
13438            self.peek(),
13439            Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
13440        ) {
13441            return Ok((
13442                init,
13443                block,
13444                Expr {
13445                    kind: ExprKind::List(vec![]),
13446                    line,
13447                },
13448                None,
13449            ));
13450        }
13451        let mut parts = vec![self.parse_assign_expr()?];
13452        loop {
13453            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
13454                break;
13455            }
13456            if matches!(
13457                self.peek(),
13458                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
13459            ) {
13460                break;
13461            }
13462            if self.peek_is_postfix_stmt_modifier_keyword() {
13463                break;
13464            }
13465            if let Token::Ident(ref kw) = self.peek().clone() {
13466                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
13467                    self.advance();
13468                    self.expect(&Token::FatArrow)?;
13469                    let prog = self.parse_assign_expr()?;
13470                    return Ok((init, block, merge_expr_list(parts), Some(prog)));
13471                }
13472            }
13473            parts.push(self.parse_assign_expr()?);
13474        }
13475        Ok((init, block, merge_expr_list(parts), None))
13476    }
13477
13478    /// `pmap_on CLUSTER { BLOCK } LIST [, progress => EXPR]` — cluster expr, then same tail as [`Self::parse_block_then_list_optional_progress`].
13479    fn parse_cluster_block_then_list_optional_progress(
13480        &mut self,
13481    ) -> StrykeResult<(Expr, Block, Expr, Option<Expr>)> {
13482        // `pmap_on $c { BLOCK } @list` — suppress `$c { ... }` hash-subscript
13483        // auto-arrow so the brace opens the BLOCK, not a `$c->{...}` deref.
13484        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_add(1);
13485        let cluster = self.parse_assign_expr();
13486        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_sub(1);
13487        let cluster = cluster?;
13488        // Accept the canonical `pmap_on $c, { BLOCK } @list` LSP-doc form too.
13489        self.eat(&Token::Comma);
13490        let block = self.parse_block_or_bareword_block()?;
13491        let block_end_line = self.prev_line();
13492        self.eat(&Token::Comma);
13493        let line = self.peek_line();
13494        if let Token::Ident(ref kw) = self.peek().clone() {
13495            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
13496                self.advance();
13497                self.expect(&Token::FatArrow)?;
13498                let prog = self.parse_assign_expr_stop_at_pipe()?;
13499                return Ok((
13500                    cluster,
13501                    block,
13502                    Expr {
13503                        kind: ExprKind::List(vec![]),
13504                        line,
13505                    },
13506                    Some(prog),
13507                ));
13508            }
13509        }
13510        let empty_list_ok = matches!(
13511            self.peek(),
13512            Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
13513        ) || (self.in_pipe_rhs()
13514            && (matches!(self.peek(), Token::Comma) || self.peek_line() > block_end_line));
13515        if empty_list_ok {
13516            return Ok((
13517                cluster,
13518                block,
13519                Expr {
13520                    kind: ExprKind::List(vec![]),
13521                    line,
13522                },
13523                None,
13524            ));
13525        }
13526        let mut parts = vec![self.parse_assign_expr_stop_at_pipe()?];
13527        loop {
13528            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
13529                break;
13530            }
13531            if matches!(
13532                self.peek(),
13533                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
13534            ) {
13535                break;
13536            }
13537            if self.peek_is_postfix_stmt_modifier_keyword() {
13538                break;
13539            }
13540            if let Token::Ident(ref kw) = self.peek().clone() {
13541                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
13542                    self.advance();
13543                    self.expect(&Token::FatArrow)?;
13544                    let prog = self.parse_assign_expr_stop_at_pipe()?;
13545                    return Ok((cluster, block, merge_expr_list(parts), Some(prog)));
13546                }
13547            }
13548            parts.push(self.parse_assign_expr_stop_at_pipe()?);
13549        }
13550        Ok((cluster, block, merge_expr_list(parts), None))
13551    }
13552
13553    /// Like [`parse_block_list`] but supports a trailing `, progress => EXPR`
13554    /// (`pmap`, `pgrep`, `preduce`, `pfor`, `pcache`, `psort`, …).
13555    ///
13556    /// Always invoked for paren-less trailing forms (`pmap { … } LIST`,
13557    /// `pmap { … } LIST, progress => EXPR`), so `|>` must terminate the whole
13558    /// stage — individual list parts and the progress value parse through
13559    /// [`Self::parse_assign_expr_stop_at_pipe`] to keep pipe-forward
13560    /// left-associative in `@a |> pmap { $_ * 2 }, progress => 0 |> join ','`.
13561    fn parse_block_then_list_optional_progress(
13562        &mut self,
13563    ) -> StrykeResult<(Block, Expr, Option<Expr>)> {
13564        let block = self.parse_block_or_bareword_block()?;
13565        let block_end_line = self.prev_line();
13566        self.eat(&Token::Comma);
13567        let line = self.peek_line();
13568        if let Token::Ident(ref kw) = self.peek().clone() {
13569            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
13570                self.advance();
13571                self.expect(&Token::FatArrow)?;
13572                let prog = self.parse_assign_expr_stop_at_pipe()?;
13573                return Ok((
13574                    block,
13575                    Expr {
13576                        kind: ExprKind::List(vec![]),
13577                        line,
13578                    },
13579                    Some(prog),
13580                ));
13581            }
13582        }
13583        // An empty list operand is allowed when the next token terminates the
13584        // enclosing context. Inside a pipe-forward RHS, a trailing `,` also
13585        // counts — `foo(bar, @a |> pmap { $_ * 2 }, baz)`. `|>` is also a
13586        // terminator — left-associative chaining leaves the outer `|>` for
13587        // the enclosing pipe-forward loop. A newline after the block also
13588        // terminates in pipe-RHS — the LHS supplies the list, so we must NOT
13589        // greedily eat the next statement (matches `parse_block_list`).
13590        let empty_list_ok = matches!(
13591            self.peek(),
13592            Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
13593        ) || (self.in_pipe_rhs()
13594            && (matches!(self.peek(), Token::Comma) || self.peek_line() > block_end_line));
13595        if empty_list_ok {
13596            return Ok((
13597                block,
13598                Expr {
13599                    kind: ExprKind::List(vec![]),
13600                    line,
13601                },
13602                None,
13603            ));
13604        }
13605        let mut parts = vec![self.parse_assign_expr_stop_at_pipe()?];
13606        loop {
13607            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
13608                break;
13609            }
13610            if matches!(
13611                self.peek(),
13612                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
13613            ) {
13614                break;
13615            }
13616            if self.peek_is_postfix_stmt_modifier_keyword() {
13617                break;
13618            }
13619            if let Token::Ident(ref kw) = self.peek().clone() {
13620                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
13621                    self.advance();
13622                    self.expect(&Token::FatArrow)?;
13623                    let prog = self.parse_assign_expr_stop_at_pipe()?;
13624                    return Ok((block, merge_expr_list(parts), Some(prog)));
13625                }
13626            }
13627            parts.push(self.parse_assign_expr_stop_at_pipe()?);
13628        }
13629        Ok((block, merge_expr_list(parts), None))
13630    }
13631
13632    /// Parse fan/fan_cap arguments: optional count + block or blockless expression.
13633    fn parse_fan_count_and_block(
13634        &mut self,
13635        line: usize,
13636    ) -> StrykeResult<(Option<Box<Expr>>, Block)> {
13637        // `fan { BLOCK }` — no count
13638        if matches!(self.peek(), Token::LBrace) {
13639            let block = self.parse_block()?;
13640            return Ok((None, block));
13641        }
13642        let saved = self.pos;
13643        // Not a brace — first expr could be count or body
13644        let first = self.parse_postfix()?;
13645        if matches!(self.peek(), Token::LBrace) {
13646            // `fan COUNT { BLOCK }`
13647            let block = self.parse_block()?;
13648            Ok((Some(Box::new(first)), block))
13649        } else if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof)
13650            || (matches!(self.peek(), Token::Comma)
13651                && matches!(self.peek_at(1), Token::Ident(ref kw) if kw == "progress"))
13652        {
13653            // `fan EXPR;` — no count, first is the body
13654            let block = self.bareword_to_no_arg_block(first);
13655            Ok((None, block))
13656        } else if matches!(first.kind, ExprKind::Integer(_)) {
13657            // `fan COUNT EXPR` or `fan COUNT, EXPR` — integer count + body
13658            self.eat(&Token::Comma);
13659            let body = self.parse_fan_blockless_body(line)?;
13660            Ok((Some(Box::new(first)), body))
13661        } else {
13662            // Non-integer first (e.g. `$_`) followed by binary op (e.g. `* $_`)
13663            // — backtrack and re-parse as a full body expression.
13664            self.pos = saved;
13665            let body = self.parse_fan_blockless_body(line)?;
13666            Ok((None, body))
13667        }
13668    }
13669
13670    /// Parse a blockless fan/fan_cap body as a full expression (not just postfix).
13671    fn parse_fan_blockless_body(&mut self, line: usize) -> StrykeResult<Block> {
13672        if matches!(self.peek(), Token::LBrace) {
13673            return self.parse_block();
13674        }
13675        // Check for bareword (zero-arg sub call) terminated by ; } EOF , or pipe
13676        if let Token::Ident(ref name) = self.peek().clone() {
13677            if matches!(
13678                self.peek_at(1),
13679                Token::Comma | Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
13680            ) {
13681                let name = name.clone();
13682                self.advance();
13683                let body = Expr {
13684                    kind: ExprKind::FuncCall { name, args: vec![] },
13685                    line,
13686                };
13687                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
13688            }
13689        }
13690        // Full expression (handles `$_ * $_`, `$_ + 1`, etc.)
13691        let expr = self.parse_assign_expr_stop_at_pipe()?;
13692        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
13693    }
13694
13695    /// Wrap a parsed expression as a single-statement block, converting bare
13696    /// identifiers to zero-arg calls (`work` → `work()`).
13697    fn bareword_to_no_arg_block(&self, expr: Expr) -> Block {
13698        let line = expr.line;
13699        let body = match &expr.kind {
13700            ExprKind::Bareword(name) => Expr {
13701                kind: ExprKind::FuncCall {
13702                    name: name.clone(),
13703                    args: vec![],
13704                },
13705                line,
13706            },
13707            _ => expr,
13708        };
13709        vec![Statement::new(StmtKind::Expression(body), line)]
13710    }
13711
13712    /// Parse either a `{ BLOCK }` or a bare expression and wrap it as a synthetic block.
13713    ///
13714    /// When the next token is `{`, delegates to [`Self::parse_block`].
13715    /// Otherwise parses a single postfix expression and wraps it as a call
13716    /// with `$_` as argument (for barewords) or a plain expression statement:
13717    ///
13718    /// - Bareword `foo` → `{ foo($_) }`
13719    /// - Other expr     → `{ EXPR }`
13720    fn parse_block_or_bareword_block(&mut self) -> StrykeResult<Block> {
13721        if matches!(self.peek(), Token::LBrace) {
13722            return self.parse_block();
13723        }
13724        let line = self.peek_line();
13725        // A lone identifier followed by a list-terminator is a bare sub name:
13726        // `pmap double, @list` → block is `{ double($_) }`, rest is list.
13727        if let Token::Ident(ref name) = self.peek().clone() {
13728            if matches!(
13729                self.peek_at(1),
13730                Token::Comma | Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
13731            ) {
13732                let name = name.clone();
13733                self.advance();
13734                let body = Expr {
13735                    kind: ExprKind::FuncCall {
13736                        name,
13737                        args: vec![Expr {
13738                            kind: ExprKind::ScalarVar("_".to_string()),
13739                            line,
13740                        }],
13741                    },
13742                    line,
13743                };
13744                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
13745            }
13746        }
13747        // Not a simple bareword — parse as expression (e.g. `$_ * 2`, `uc $_`)
13748        let expr = self.parse_assign_expr_stop_at_pipe()?;
13749        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
13750    }
13751
13752    /// Like [`parse_block_or_bareword_block`] but for fan/timer/bench where the
13753    /// bare function takes no args (body runs stand-alone, not per-element).
13754    /// Only consumes a single bareword identifier — does NOT let `parse_primary`
13755    /// greedily swallow subsequent tokens as function arguments.
13756    fn parse_block_or_bareword_block_no_args(&mut self) -> StrykeResult<Block> {
13757        if matches!(self.peek(), Token::LBrace) {
13758            return self.parse_block();
13759        }
13760        let line = self.peek_line();
13761        if let Token::Ident(ref name) = self.peek().clone() {
13762            if matches!(
13763                self.peek_at(1),
13764                Token::Comma
13765                    | Token::Semicolon
13766                    | Token::RBrace
13767                    | Token::Eof
13768                    | Token::PipeForward
13769                    | Token::Integer(_)
13770            ) {
13771                let name = name.clone();
13772                self.advance();
13773                let body = Expr {
13774                    kind: ExprKind::FuncCall { name, args: vec![] },
13775                    line,
13776                };
13777                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
13778            }
13779        }
13780        let expr = self.parse_postfix()?;
13781        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
13782    }
13783
13784    /// Returns true if `name` is a Perl keyword/builtin that should NOT be
13785    /// treated as a bare sub name (e.g. inside `sort`).
13786    /// True for any bareword the parser treats as a known builtin / keyword —
13787    /// Perl 5 core *or* a stryke extension. Used to suppress "call as user
13788    /// sub" interpretations (e.g. `sort my_cmp @list` only treats `my_cmp`
13789    /// as a comparator name if it *isn't* a known bareword). Previously named
13790    /// `is_perl_keyword`, which was misleading.
13791    fn is_known_bareword(name: &str) -> bool {
13792        Self::is_perl5_core(name) || Self::stryke_extension_name(name).is_some()
13793    }
13794
13795    /// True iff `name` appears as any spelling (primary *or* alias) in a
13796    /// `try_builtin` match arm. Picks up the ~300 aliases that don't show
13797    /// up in the parser-level keyword lists but are still callable at
13798    /// runtime — so `map { tj }` can default to `tj($_)` the same way
13799    /// `map { to_json }` does.
13800    fn is_try_builtin_name(name: &str) -> bool {
13801        crate::builtins::BUILTIN_ARMS
13802            .iter()
13803            .any(|arm| arm.contains(&name))
13804    }
13805
13806    /// True iff `name` is a Perl 5 core keyword/builtin (as shipped in stock
13807    /// `perl`). Extensions (`pmap`, `fan`, `timer`, …) are *not* included
13808    /// here — those live in `stryke_extension_name`. `%stryke::perl_compats`
13809    /// is derived from this list by `build.rs`.
13810    fn is_perl5_core(name: &str) -> bool {
13811        matches!(
13812            name,
13813            // ── array / list ────────────────────────────────────────────
13814            "map" | "grep" | "sort" | "reverse" | "join" | "split"
13815            | "push" | "pop" | "shift" | "unshift" | "splice"
13816            | "splice_last" | "splice1" | "spl_last"
13817            | "pack" | "unpack"
13818            | "unpack_first" | "unpack1" | "up1"
13819            // ── hash ────────────────────────────────────────────────────
13820            | "keys" | "values" | "each"
13821            // ── string ──────────────────────────────────────────────────
13822            | "chomp" | "chop" | "chr" | "ord" | "hex" | "oct"
13823            | "lc" | "uc" | "lcfirst" | "ucfirst"
13824            | "length" | "substr" | "index" | "rindex"
13825            | "sprintf" | "printf" | "print" | "say"
13826            | "pos" | "quotemeta" | "study"
13827            // ── numeric ─────────────────────────────────────────────────
13828            | "abs" | "int" | "sqrt" | "sin" | "cos" | "atan2"
13829            | "exp" | "log" | "rand" | "srand"
13830            // ── time ────────────────────────────────────────────────────
13831            | "time" | "localtime" | "gmtime"
13832            // ── type / reflection ───────────────────────────────────────
13833            | "defined" | "undef" | "ref" | "scalar" | "wantarray"
13834            | "caller" | "delete" | "exists" | "bless" | "prototype"
13835            | "tie" | "untie" | "tied"
13836            // ── io ──────────────────────────────────────────────────────
13837            | "open" | "close" | "read" | "readline" | "write" | "seek" | "tell"
13838            | "eof" | "binmode" | "getc" | "fileno" | "truncate"
13839            | "format" | "formline" | "select" | "vec"
13840            | "sysopen" | "sysread" | "sysseek" | "syswrite"
13841            // ── filesystem ──────────────────────────────────────────────
13842            | "stat" | "lstat" | "rename" | "unlink" | "utime"
13843            | "mkdir" | "rmdir" | "chdir" | "chmod" | "chown"
13844            | "glob" | "opendir" | "readdir" | "closedir"
13845            | "link" | "readlink" | "symlink"
13846            // ── ipc ─────────────────────────────────────────────────────
13847            | "fcntl" | "flock" | "ioctl" | "pipe" | "dbmopen" | "dbmclose"
13848            // ── sysv ipc ────────────────────────────────────────────────
13849            | "msgctl" | "msgget" | "msgrcv" | "msgsnd"
13850            | "semctl" | "semget" | "semop"
13851            | "shmctl" | "shmget" | "shmread" | "shmwrite"
13852            // ── process / system ────────────────────────────────────────
13853            | "system" | "exec" | "exit" | "die" | "warn" | "dump"
13854            | "fork" | "wait" | "waitpid" | "kill" | "syscall" | "alarm" | "sleep"
13855            | "chroot" | "times" | "umask" | "reset"
13856            | "getpgrp" | "setpgrp" | "getppid"
13857            | "getpriority" | "setpriority"
13858            // ── socket ──────────────────────────────────────────────────
13859            | "socket" | "socketpair" | "connect" | "listen" | "accept" | "shutdown"
13860            | "send" | "recv" | "bind" | "setsockopt" | "getsockopt"
13861            | "getpeername" | "getsockname"
13862            // ── posix metadata ──────────────────────────────────────────
13863            | "getpwnam" | "getpwuid" | "getpwent" | "setpwent"
13864            | "getgrnam" | "getgrgid" | "getgrent" | "setgrent"
13865            | "getlogin"
13866            | "gethostbyname" | "gethostbyaddr" | "gethostent"
13867            | "getnetbyname" | "getnetent"
13868            | "getprotobyname" | "getprotoent"
13869            | "getservbyname" | "getservent"
13870            | "sethostent" | "setnetent" | "setprotoent" | "setservent"
13871            | "endpwent" | "endgrent"
13872            | "endhostent" | "endnetent" | "endprotoent" | "endservent"
13873            // ── control flow ────────────────────────────────────────────
13874            | "return" | "do" | "eval" | "require"
13875            | "my" | "our" | "local" | "use" | "no"
13876            | "sub" | "if" | "unless" | "while" | "until"
13877            | "for" | "foreach" | "last" | "next" | "redo" | "goto"
13878            | "not" | "and" | "or"
13879            // ── quoting ─────────────────────────────────────────────────
13880            | "qw" | "qq" | "q"
13881            // ── phase blocks ────────────────────────────────────────────
13882            | "BEGIN" | "END"
13883        )
13884    }
13885
13886    /// If `name` is a stryke-only extension keyword/builtin, return it; else `None`.
13887    /// Used by `--compat` to reject extensions at parse time.
13888    fn stryke_extension_name(name: &str) -> Option<&str> {
13889        match name {
13890            // ── numerical stability + modern IDs ───────────────────────────
13891            | "ulid" | "is_ulid" | "ulid_timestamp"
13892            | "kahan_sum" | "welford_mean" | "welford_variance"
13893            | "welford_stddev" | "welford_pop_variance"
13894            // ── Shell-like REPL (Tier S) ───────────────────────────────────
13895            | "clear" | "cls" | "whoami" | "groups"
13896            | "pushd" | "popd" | "dir_stack"
13897            | "history" | "repl_alias" | "repl_unalias" | "set_alias" | "unset_alias"
13898            | "term_size" | "term_width" | "term_height"
13899            | "set_title" | "beep" | "ring_bell" | "man" | "manpage"
13900            | "run" | "exec_script" | "source" | "src"
13901            // ── Shell-like REPL (Tier A) ───────────────────────────────────
13902            | "rm" | "mktemp" | "mktempdir" | "whereis"
13903            | "nice" | "renice"
13904            | "tree" | "comm" | "column" | "xargs"
13905            | "openurl" | "xdg_open"
13906            | "curl_get" | "curl_post"
13907            | "iconv" | "strftime"
13908            | "tac" | "rev_lines"
13909            | "tty_raw" | "tty_cooked"
13910            // ── probabilistic data structures ──────────────────────────────
13911            | "bloom_filter" | "bloom_add" | "bloom_contains" | "bloom_len"
13912            | "bloom_clear" | "bloom_merge" | "bloom_fpr" | "bloom_bits"
13913            | "bloom_serialize" | "bloom_deserialize"
13914            | "hll" | "hyperloglog" | "hll_add" | "hll_count" | "hll_merge"
13915            | "hll_clear" | "hll_precision" | "hll_serialize" | "hll_deserialize"
13916            | "cms" | "count_min_sketch" | "cms_add" | "cms_count" | "cms_query"
13917            | "cms_merge" | "cms_clear" | "cms_serialize" | "cms_deserialize"
13918            | "topk" | "top_k_sketch" | "topk_add" | "topk_heavies" | "topk_count"
13919            | "topk_size" | "topk_merge" | "topk_clear"
13920            | "topk_serialize" | "topk_deserialize"
13921            | "t_digest" | "tdg" | "tdigest" | "td_add" | "td_quantile" | "td_count"
13922            | "td_min" | "td_max" | "td_sum" | "td_mean" | "td_merge" | "td_clear"
13923            | "td_serialize" | "td_deserialize"
13924            | "roaring" | "roaring_bitmap" | "rbm" | "rb_add" | "rb_remove" | "rb_contains"
13925            | "rb_len" | "rb_min" | "rb_max" | "rb_to_array" | "rb_rank"
13926            | "rb_or" | "rb_and" | "rb_xor" | "rb_andnot" | "rb_clear"
13927            | "rb_serialize" | "rb_deserialize"
13928            // ── Rate limiters / hash ring / LSH / trees / diff ────────────
13929            | "token_bucket" | "leaky_bucket" | "rl_try_take" | "rl_available"
13930            | "hash_ring" | "consistent_hash" | "hr_add" | "hr_remove" | "hr_get" | "hr_nodes"
13931            | "simhash" | "sh_add" | "sh_digest" | "sh_similarity"
13932            | "minhash" | "mh_add" | "mh_jaccard" | "mh_merge"
13933            | "interval_tree" | "it_insert" | "it_query_point" | "it_query_range"
13934            | "it_remove" | "it_len"
13935            | "bk_tree" | "bk_insert" | "bk_query" | "bk_len"
13936            | "rope" | "rope_insert" | "rope_delete" | "rope_substring"
13937            | "rope_to_string" | "rope_len"
13938            | "myers_diff" | "patience_diff"
13939            // ── rkyv KV store ──────────────────────────────────────────────
13940            | "kv_open" | "kv_new" | "kv_put" | "kv_set" | "kv_get"
13941            | "kv_del" | "kv_delete" | "kv_remove" | "kv_exists" | "kv_has"
13942            | "kv_keys" | "kv_scan" | "kv_len" | "kv_count" | "kv_size"
13943            | "kv_commit" | "kv_flush" | "kv_batch" | "kv_close"
13944            | "kv_stats" | "kv_info"
13945            // ── aop ────────────────────────────────────────────────────────
13946            | "proceed" | "intercept_list" | "intercept_remove" | "intercept_clear"
13947            // ── parallel ────────────────────────────────────────────────────
13948            | "pmap" | "pmap_on" | "pflat_map" | "pflat_map_on" | "pmap_chunked"
13949            | "pgrep" | "pfor" | "psort" | "preduce" | "preduce_init" | "pmap_reduce"
13950            | "pcache" | "pchannel" | "pselect" | "puniq" | "pfirst" | "pany"
13951            | "fan" | "fan_cap" | "par_lines" | "par_walk" | "par_sed"
13952            | "par_find_files" | "par_line_count" | "pwatch" | "par_pipeline_stream"
13953            | "glob_par" | "ppool" | "barrier" | "pipeline" | "cluster"
13954            | "pmaps" | "pflat_maps" | "pgreps"
13955            // ── functional / iterator ───────────────────────────────────────
13956            | "fore" | "e" | "ep" | "flat_map" | "flat_maps" | "maps" | "filter" | "fi" | "find_all" | "reduce" | "fold"
13957            | "inject" | "collect" | "uniq" | "distinct" | "any" | "all" | "none"
13958            | "first" | "detect" | "find" | "find_index" | "firstidx" | "first_index"
13959            | "compact" | "concat" | "chain" | "reject" | "grepv" | "flatten" | "set"
13960            | "min_by" | "max_by" | "sort_by" | "tally"
13961            | "each_with_index" | "count" | "cnt" |"len" | "group_by" | "chunk_by"
13962            | "zip" | "chunk" | "chunked" | "sliding_window" | "windowed"
13963            | "enumerate" | "with_index" | "shuffle" | "shuffled"| "heap"
13964            | "take_while" | "drop_while" | "skip_while" | "tap" | "peek" | "partition"
13965            | "zip_with" | "count_by" | "skip" | "first_or"
13966            // ── cli / argv ──────────────────────────────────────────────────
13967            | "getopts"
13968            // ── pipeline / string helpers ───────────────────────────────────
13969            | "input" | "lines" | "words" | "chars" | "cindex" | "crindex"
13970            | "digits" | "letters" | "letters_uc" | "letters_lc"
13971            | "punctuation" | "punct"
13972            | "sentences" | "sents"
13973            | "paragraphs" | "paras" | "sections" | "sects"
13974            | "numbers" | "nums" | "graphemes" | "grs" | "columns" | "cols"
13975            | "trim" | "avg" | "stddev"
13976            | "squared" | "sq" | "square" | "cubed" | "cb" | "cube" | "expt" | "pow" | "pw"
13977            | "normalize" | "snake_case" | "camel_case" | "kebab_case"
13978            | "frequencies" | "freq" | "pfrequencies" | "pfreq"
13979            | "interleave" | "ddump" | "stringify" | "str" | "top"
13980            | "to_json" | "to_csv" | "to_toml" | "to_yaml" | "to_xml"
13981            | "to_html" | "to_markdown" | "to_table" | "xopen"
13982            | "from_json" | "from_csv" | "from_toml" | "from_yaml" | "from_xml"
13983            | "clip" | "clipboard" | "paste" | "pbcopy" | "pbpaste" | "preview"
13984            | "sparkline" | "spark" | "bar_chart" | "bars" | "flame" | "flamechart"
13985            | "histo" | "gauge" | "spinner" | "spinner_start" | "spinner_stop"
13986            | "to_hash" | "to_set"
13987            | "to_file" | "read_lines" | "append_file" | "write_json" | "read_json"
13988            | "tempfile" | "tempdir" | "list_count" | "list_size" | "size"
13989            | "clamp" | "grep_v" | "select_keys" | "pluck" | "glob_match" | "which_all"
13990            | "dedup" | "nth" | "tail" | "take" | "drop" | "tee" | "range"
13991            | "inc" | "dec" | "elapsed"
13992            // ── filesystem extensions ───────────────────────────────────────
13993            | "files" | "filesf" | "f" | "fr" | "dirs" | "d" | "dr" | "sym_links"
13994            | "sockets" | "pipes" | "block_devices" | "char_devices" | "exe" | "executables"
13995            | "basename" | "dirname" | "fileparse" | "realpath" | "canonpath"
13996            | "copy" | "cp" | "move" | "spurt" | "spit" | "read_bytes" | "which"
13997            | "getcwd" | "cd" | "ls" | "touch" | "gethostname" | "uname"
13998            | "file" | "xxd"
13999            // ── data / network ──────────────────────────────────────────────
14000            | "csv_read" | "csv_write" | "dataframe" | "sqlite"
14001            | "fetch" | "fetch_json" | "fetch_async" | "fetch_async_json"
14002            | "par_fetch" | "par_csv_read" | "par_pipeline"
14003            | "json_encode" | "json_decode" | "json_jq"
14004            | "http_request" | "serve" | "ssh"
14005            | "html_parse" | "css_select" | "xml_parse" | "xpath"
14006            | "smtp_send"
14007            | "net_interfaces" | "net_ipv4" | "net_ipv6" | "net_mac"
14008            | "net_public_ip" | "net_dns" | "net_reverse_dns"
14009            | "net_ping" | "net_port_open" | "net_ports_scan"
14010            | "net_latency" | "net_download" | "net_headers"
14011            | "net_dns_servers" | "net_gateway" | "net_whois" | "net_hostname"
14012            // ── git ─────────────────────────────────────────────────────────
14013            | "git_log" | "git_status" | "git_diff" | "git_branches"
14014            | "git_tags" | "git_blame" | "git_authors" | "git_files"
14015            | "git_show" | "git_root"
14016            // ── github / gh REST API ─────────────────────────────────────────
14017            | "gh_get" | "gh_user" | "gh_org" | "gh_followers" | "gh_following"
14018            | "gh_repo" | "gh_repos" | "gh_org_repos" | "gh_starred"
14019            | "gh_gists" | "gh_gist"
14020            | "gh_issues" | "gh_prs" | "gh_commits" | "gh_branches"
14021            | "gh_tags" | "gh_releases" | "gh_contributors" | "gh_forks"
14022            | "gh_stargazers" | "gh_topics" | "gh_languages"
14023            | "gh_readme" | "gh_workflows" | "gh_runs"
14024            | "gh_search_repos" | "gh_search_users" | "gh_search_code" | "gh_search_issues"
14025            | "gh_rate_limit" | "gh_meta" | "gh_emojis" | "gh_zen"
14026            // ── audio / media ───────────────────────────────────────────────
14027            | "audio_convert" | "audio_info" | "id3_read" | "id3_write"
14028            // ── pdf ─────────────────────────────────────────────────────────
14029            | "to_pdf" | "pdf_text" | "pdf_pages"
14030            // ── serialization (stryke-only encoders) ────────────────────────
14031            | "toml_encode" | "toml_decode"
14032            | "yaml_encode" | "yaml_decode"
14033            | "xml_encode" | "xml_decode"
14034            // ── crypto / encoding ───────────────────────────────────────────
14035            | "md5" | "sha1" | "sha224" | "sha256" | "sha384" | "sha512"
14036            | "sha3_256" | "s3_256" | "sha3_512" | "s3_512"
14037            | "shake128" | "shake256"
14038            | "hmac_sha256" | "hmac_sha1" | "hmac_sha384" | "hmac_sha512" | "hmac_md5"
14039            | "uuid" | "crc32"
14040            | "blake2b" | "b2b" | "blake2s" | "b2s" | "blake3" | "b3"
14041            | "ripemd160" | "rmd160" | "md4"
14042            | "xxh32" | "xxhash32" | "xxh64" | "xxhash64" | "xxh3" | "xxhash3" | "xxh3_128" | "xxhash3_128"
14043            | "murmur3" | "murmur3_32" | "murmur3_128"
14044            | "siphash" | "siphash_keyed"
14045            | "hkdf_sha256" | "hkdf" | "hkdf_sha512"
14046            | "poly1305" | "poly1305_mac"
14047            | "base32_encode" | "b32e" | "base32_decode" | "b32d"
14048            | "base58_encode" | "b58e" | "base58_decode" | "b58d"
14049            | "totp" | "totp_generate" | "totp_verify" | "hotp" | "hotp_generate"
14050            | "aes_cbc_encrypt" | "aes_cbc_enc" | "aes_cbc_decrypt" | "aes_cbc_dec"
14051            | "blowfish_encrypt" | "bf_enc" | "blowfish_decrypt" | "bf_dec"
14052            | "des3_encrypt" | "3des_enc" | "tdes_enc" | "des3_decrypt" | "3des_dec" | "tdes_dec"
14053            | "twofish_encrypt" | "tf_enc" | "twofish_decrypt" | "tf_dec"
14054            | "camellia_encrypt" | "cam_enc" | "camellia_decrypt" | "cam_dec"
14055            | "cast5_encrypt" | "cast5_enc" | "cast5_decrypt" | "cast5_dec"
14056            | "salsa20" | "salsa20_encrypt" | "salsa20_decrypt"
14057            | "xsalsa20" | "xsalsa20_encrypt" | "xsalsa20_decrypt"
14058            | "secretbox" | "secretbox_seal" | "secretbox_open"
14059            | "nacl_box_keygen" | "box_keygen" | "nacl_box" | "nacl_box_seal" | "box_seal"
14060            | "nacl_box_open" | "box_open"
14061            | "qr_ascii" | "qr" | "qr_png" | "qr_svg"
14062            | "barcode_code128" | "code128" | "barcode_code39" | "code39"
14063            | "barcode_ean13" | "ean13" | "barcode_svg"
14064            | "argon2_hash" | "argon2" | "argon2_verify"
14065            | "bcrypt_hash" | "bcrypt" | "bcrypt_verify"
14066            | "scrypt_hash" | "scrypt" | "scrypt_verify"
14067            | "pbkdf2" | "pbkdf2_derive"
14068            | "random_bytes" | "randbytes" | "random_bytes_hex" | "randhex"
14069            | "aes_encrypt" | "aes_enc" | "aes_decrypt" | "aes_dec"
14070            | "chacha_encrypt" | "chacha_enc" | "chacha_decrypt" | "chacha_dec"
14071            | "rsa_keygen" | "rsa_encrypt" | "rsa_enc" | "rsa_decrypt" | "rsa_dec"
14072            | "rsa_encrypt_pkcs1" | "rsa_decrypt_pkcs1" | "rsa_sign" | "rsa_verify"
14073            | "ecdsa_p256_keygen" | "p256_keygen" | "ecdsa_p256_sign" | "p256_sign"
14074            | "ecdsa_p256_verify" | "p256_verify"
14075            | "ecdsa_p384_keygen" | "p384_keygen" | "ecdsa_p384_sign" | "p384_sign"
14076            | "ecdsa_p384_verify" | "p384_verify"
14077            | "ecdsa_secp256k1_keygen" | "secp256k1_keygen"
14078            | "ecdsa_secp256k1_sign" | "secp256k1_sign"
14079            | "ecdsa_secp256k1_verify" | "secp256k1_verify"
14080            | "ecdh_p256" | "p256_dh" | "ecdh_p384" | "p384_dh"
14081            | "ed25519_keygen" | "ed_keygen" | "ed25519_sign" | "ed_sign"
14082            | "ed25519_verify" | "ed_verify"
14083            | "x25519_keygen" | "x_keygen" | "x25519_dh" | "x_dh"
14084            | "base64_encode" | "base64_decode"
14085            | "hex_encode" | "hex_decode"
14086            | "url_encode" | "url_decode"
14087            | "gzip" | "gunzip" | "gz" | "ugz" | "zstd" | "zstd_decode" | "zst" | "uzst"
14088            | "brotli" | "br" | "brotli_decode" | "ubr"
14089            | "xz" | "lzma" | "xz_decode" | "unxz" | "unlzma"
14090            | "bzip2" | "bz2" | "bzip2_decode" | "bunzip2" | "ubz2"
14091            | "lz4" | "lz4_decode" | "unlz4"
14092            | "snappy" | "snp" | "snappy_decode" | "unsnappy"
14093            | "lzw" | "lzw_decode" | "unlzw"
14094            | "tar_create" | "tar" | "tar_extract" | "untar" | "tar_list"
14095            | "tar_gz_create" | "tgz" | "tar_gz_extract" | "untgz"
14096            | "zip_create" | "zip_archive" | "zip_extract" | "unzip_archive" | "zip_list"
14097            // ── special math functions ────────────────────────────────────────
14098            | "erf" | "erfc" | "gamma" | "tgamma" | "lgamma" | "ln_gamma"
14099            | "digamma" | "psi" | "beta_fn" | "lbeta" | "ln_beta"
14100            | "betainc" | "beta_reg" | "gammainc" | "gamma_li"
14101            | "gammaincc" | "gamma_ui" | "gammainc_reg" | "gamma_lr"
14102            | "gammaincc_reg" | "gamma_ur"
14103            // ── date / time ─────────────────────────────────────────────────
14104            | "datetime_utc" | "datetime_now_tz" | "now"
14105            | "datetime_format_tz" | "datetime_add_seconds"
14106            | "datetime_from_epoch"
14107            | "datetime_parse_rfc3339" | "datetime_parse_local"
14108            | "datetime_strftime"
14109            | "dateseq" | "dategrep" | "dateround" | "datesort"
14110            // ── jwt ─────────────────────────────────────────────────────────
14111            | "jwt_encode" | "jwt_decode" | "jwt_decode_unsafe"
14112            // ── logging ─────────────────────────────────────────────────────
14113            | "log_info" | "log_warn" | "log_error"
14114            | "log_debug" | "log_trace" | "log_json" | "log_level"
14115            // ── concurrency / timing ────────────────────────────────────────
14116            | "async" | "spawn" | "trace" | "timer" | "bench"
14117            | "eval_timeout" | "retry" | "rate_limit" | "every"
14118            | "gen" | "watch"
14119            // ── caching ────────────────────────────────────────────────────────
14120            | "cache_clear" | "cache_exists" | "cache_stats" | "cacheview"
14121            // ── testing framework ────────────────────────────────────────────
14122            | "assert_eq" | "assert_ne" | "assert_ok" | "assert_err"
14123            | "assert_true" | "assert_false"
14124            | "assert_gt" | "assert_lt" | "assert_ge" | "assert_le"
14125            | "assert_match" | "assert_contains" | "assert_near" | "assert_dies"
14126            | "test_run" | "run_tests" | "test_skip" | "skip_test" | "skip_assert"
14127            // ── system info ─────────────────────────────────────────────────
14128            | "mounts" | "du" | "du_tree" | "process_list"
14129            | "thread_count" | "pool_info" | "par_bench"
14130            | "perfview" | "pfv"
14131            | "docs" | "help" | "h"
14132            | "banner"
14133            // ── network / ip / cidr ─────────────────────────────────────────
14134            | "ip_parse" | "ip_is_valid" | "ip_version" | "ip_family"
14135            | "ip_to_int" | "int_to_ip" | "ip_to_bytes" | "bytes_to_ip"
14136            | "ip_to_bits" | "bits_to_ip"
14137            | "ip_is_private" | "ip_is_loopback" | "ip_is_multicast"
14138            | "ip_is_link_local" | "ip_is_unspecified" | "ip_is_global"
14139            | "ip_is_documentation" | "ip_is_benchmarking" | "ip_is_shared"
14140            | "ip_is_reserved" | "ip_is_broadcast"
14141            | "ip_canonical" | "ip_reverse" | "ip_arpa"
14142            | "ip_compare" | "ip_sort" | "ip_random"
14143            | "ipv4_parse" | "ipv4_is_valid" | "ipv4_classful_class"
14144            | "ipv6_parse" | "ipv6_is_valid" | "ipv6_canonical"
14145            | "ipv6_expand" | "ipv6_compress" | "ipv6_strip_zone" | "ipv6_zone_id"
14146            | "ipv6_link_local" | "ipv6_unique_local" | "ipv6_solicited_node"
14147            | "ipv6_eui64_addr" | "ipv6_link_local_from_mac"
14148            | "ipv4_to_ipv6_mapped" | "ipv4_to_ipv6_6to4" | "ipv6_to_ipv4_compat"
14149            | "ipv6_is_6to4" | "ipv6_6to4_extract"
14150            | "ipv6_is_teredo" | "ipv6_teredo_extract"
14151            | "ipv6_is_isatap" | "ipv6_isatap_extract"
14152            | "cidr_parse" | "cidr_valid_subnet" | "cidr_format"
14153            | "cidr_prefix_len" | "cidr_class"
14154            | "cidr_network" | "cidr_broadcast" | "cidr_netmask"
14155            | "cidr_hostmask" | "cidr_wildcard"
14156            | "cidr_to_netmask" | "netmask_to_prefix"
14157            | "cidr_first_host" | "cidr_last_host" | "cidr_num_hosts"
14158            | "cidr_size" | "cidr_hosts" | "cidr_iterate"
14159            | "cidr_contains" | "ip_in_cidr" | "ip_in_subnet"
14160            | "cidr_subnet" | "cidr_supernet" | "cidr_subnets" | "cidr_split"
14161            | "cidr_overlaps" | "cidr_aggregate" | "cidr_summarize"
14162            | "cidr_intersection" | "cidr_difference" | "cidr_union"
14163            | "cidr_minimum_covering" | "cidr_is_aggregable"
14164            | "cidr_next" | "cidr_prev" | "cidr_distance"
14165            | "cidr_random_ip" | "ip_random_in_cidr"
14166            | "cidr_compare" | "cidr_sort"
14167            | "mac_parse" | "mac_is_valid" | "mac_normalize" | "mac_format"
14168            | "mac_to_int" | "int_to_mac" | "mac_to_bytes" | "bytes_to_mac"
14169            | "mac_oui" | "mac_vendor_lookup" | "mac_lookup_vendor"
14170            | "mac_is_unicast" | "mac_is_multicast" | "mac_is_broadcast"
14171            | "mac_is_locally_administered" | "mac_is_universally_administered"
14172            | "mac_random" | "mac_random_local" | "mac_compare"
14173            | "eui48_to_eui64" | "eui64_to_eui48" | "eui64_from_mac"
14174            | "port_name" | "port_is_well_known" | "port_is_assigned"
14175            | "port_is_registered" | "port_is_ephemeral" | "port_is_dynamic"
14176            | "port_to_service" | "port_service_lookup"
14177            | "port_parse_range" | "port_random_ephemeral"
14178            | "ws_handshake_key" | "ws_handshake_accept"
14179            | "ws_mask" | "ws_unmask" | "ws_frame_encode" | "ws_frame_decode"
14180            | "ws_close_frame"
14181            | "cookie_parse" | "cookie_format"
14182            | "cookie_jar_new" | "cookie_jar_add" | "cookie_jar_get"
14183            | "cookie_is_session" | "cookie_is_expired"
14184            | "cookie_domain_matches" | "cookie_path_matches"
14185            | "cookie_set_max_age"
14186            | "http_method_is_idempotent" | "http_method_is_safe"
14187            | "http_method_has_body"
14188            | "http_status_class"
14189            | "http_status_is_informational" | "http_status_is_success"
14190            | "http_status_is_redirect" | "http_status_is_client_error"
14191            | "http_status_is_server_error"
14192            | "http_status_text" | "http_date_parse" | "http_date_format"
14193            | "mime_type_for_extension" | "mime_extension_for_type"
14194            | "mime_is_text" | "mime_is_image" | "mime_is_audio"
14195            | "mime_is_video" | "mime_is_application"
14196            | "bandwidth_format" | "bandwidth_parse"
14197            | "latency_ms" | "packet_loss" | "jitter_ms"
14198            | "rtt_min" | "rtt_max" | "rtt_avg"
14199            // ── validation / input checks ──
14200            | "is_alpha_only" | "is_alphanumeric_only" | "is_numeric_only"
14201            | "is_ascii_only" | "is_printable_ascii" | "is_utf8"
14202            | "is_lowercase" | "is_uppercase" | "is_titlecase"
14203            | "is_palindrome_str"
14204            | "is_hex" | "is_octal" | "is_binary" | "is_base32"
14205            | "is_md5_hash" | "is_sha1_hash" | "is_sha256_hash"
14206            | "is_ipv6" | "is_cidr" | "is_mac"
14207            | "is_url_http" | "is_url_https"
14208            | "is_uuid_v4" | "is_uuid_v7"
14209            | "is_jwt" | "is_email_strict"
14210            | "luhn_digit" | "is_imei" | "is_imsi"
14211            | "is_vin" | "vin_decode"
14212            | "is_ean13" | "is_upc"
14213            | "is_isbn" | "isbn10_to_isbn13" | "isbn13_to_isbn10"
14214            | "iban_format" | "iban_country" | "is_bic" | "is_swift"
14215            | "is_phone" | "is_phone_e164"
14216            | "is_zip_us" | "is_zip_plus4" | "is_postal_code" | "is_ssn_us"
14217            | "semver_compare" | "semver_satisfies"
14218            | "semver_increment_major" | "semver_increment_minor" | "semver_increment_patch"
14219            // ── math / number theory extras ──
14220            | "extended_gcd" | "modinverse" | "modpow" | "modular_sqrt"
14221            | "stirling_1" | "stirling_2" | "catalan_number" | "lucas_n"
14222            | "prime_count_below" | "divisor_count" | "divisor_sum" | "sigma_divisors"
14223            | "sum_digits" | "product_digits" | "collatz_steps"
14224            | "hyperoperation" | "busy_beaver"
14225            | "quadratic_residue" | "is_quadratic_residue"
14226            | "discrete_log" | "order_modulo" | "square_free"
14227            | "perfect_number" | "abundant" | "deficient"
14228            // ── random / sampling extras ──
14229            | "random_bernoulli" | "random_normal" | "random_lognormal"
14230            | "random_exponential" | "random_poisson" | "random_gamma" | "random_beta"
14231            | "random_alphanumeric" | "random_alphabetic" | "random_password"
14232            | "random_choices_weighted"
14233            | "sample_weighted_unique" | "reservoir_sample_weighted"
14234            | "seeded_rng" | "save_random_state" | "restore_random_state"
14235            // ── complex / geom / color / trig ──
14236            | "complex_new" | "complex_real" | "complex_imag"
14237            | "complex_polar" | "complex_from_polar"
14238            | "complex_magnitude" | "complex_abs" | "complex_phase" | "complex_angle"
14239            | "complex_conjugate"
14240            | "complex_add" | "complex_sub" | "complex_mul" | "complex_div"
14241            | "complex_pow" | "complex_sqrt" | "complex_exp" | "complex_log"
14242            | "complex_sin" | "complex_cos" | "complex_tan"
14243            | "complex_sinh" | "complex_cosh" | "complex_tanh"
14244            | "complex_equal"
14245            | "point_angle"
14246            | "line_intersect" | "line_segment_intersect" | "line_distance_point"
14247            | "polygon_signed_area" | "polygon_orientation" | "polygon_reverse"
14248            | "polygon_contains_point" | "polygon_convex"
14249            | "polygon_simplify_dp" | "polygon_convex_hull_2d"
14250            | "triangle_area" | "triangle_centroid"
14251            | "triangle_circumcircle" | "triangle_incircle"
14252            | "triangle_contains_point"
14253            | "circle_circumference" | "circle_area"
14254            | "circle_intersects_line" | "circle_intersects_circle"
14255            | "rect_area" | "rect_perimeter" | "rect_intersect"
14256            | "rect_contains_point" | "rect_union"
14257            | "ellipse_area"
14258            | "sphere_surface_area" | "cylinder_surface_area"
14259            | "cone_surface_area" | "torus_surface_area"
14260            | "srgb_to_rgb" | "rgb_to_srgb"
14261            | "rgb_to_p3" | "p3_to_rgb"
14262            | "rgb_to_adobe_rgb" | "adobe_rgb_to_rgb"
14263            | "xyz_d65_to_d50" | "xyz_d50_to_d65"
14264            | "gamma_apply" | "gamma_remove"
14265            | "white_point_d65" | "white_point_d50"
14266            | "color_temperature_to_rgb" | "rgb_to_color_temperature"
14267            | "chromatic_adaptation"
14268            | "color_interpolate_rgb" | "color_interpolate_hsl"
14269            | "color_interpolate_lab" | "color_interpolate_oklab"
14270            | "color_blend_screen"
14271            | "atan2_deg" | "atan2_quadrant"
14272            | "polar_to_cartesian" | "cartesian_to_polar"
14273            | "spherical_to_cartesian" | "cartesian_to_spherical"
14274            | "cylindrical_to_cartesian" | "cartesian_to_cylindrical"
14275            | "versine_fn"
14276            // ── iterator + string-distance extras ──
14277            | "triples" | "n_tuples" | "peekable" | "runs" | "unique_by"
14278            | "multipeek" | "lookahead_n"
14279            | "sliding_average" | "sliding_sum" | "sliding_max" | "sliding_min"
14280            | "top_n_by" | "bottom_n_by" | "all_equal" | "take_n_random"
14281            | "unzip3" | "roundrobin" | "mode_iter" | "distinct_sample"
14282            | "ranked_choice" | "boyer_moore_majority"
14283            | "quickselect_nth" | "quickselect_median"
14284            | "top_k_min_heap" | "bottom_k_max_heap"
14285            | "unique_consecutive" | "exclude" | "exclude_first" | "exclude_last"
14286            | "weave_n" | "pad_left_n" | "pad_right_n"
14287            | "collect_into_string" | "collect_into_hashset" | "collect_into_btreeset"
14288            | "collect_into_hashmap" | "collect_into_btreemap"
14289            | "foldl1_iter" | "foldr1_iter"
14290            | "sort_by_cached_key"
14291            | "position_max" | "position_min" | "position_max_by" | "position_min_by"
14292            | "group_map"
14293            | "levenshtein_normalized" | "ratcliff_obershelp" | "match_rating"
14294            | "str_lcs" | "str_lcs_length" | "str_longest_common_substring"
14295            | "str_kmp" | "str_boyer_moore" | "str_rabin_karp"
14296            | "str_aho_corasick" | "str_z_array" | "str_suffix_array"
14297            | "str_rotations" | "str_compress_rle" | "str_decompress_rle"
14298            | "str_huffman_encode" | "str_huffman_decode"
14299            | "str_compress_lzss" | "str_decompress_lzss"
14300            | "str_isogram" | "fold_case"
14301            // ── extras ──
14302            | "bignum_new" | "bignum_from_str" | "bignum_to_str" | "bignum_to_int"
14303            | "bignum_add" | "bignum_sub" | "bignum_mul" | "bignum_div" | "bignum_mod"
14304            | "bignum_pow" | "bignum_modpow" | "bignum_gcd" | "bignum_lcm"
14305            | "bignum_factorial" | "bignum_sqrt" | "bignum_bit_length"
14306            | "bignum_set_bit" | "bignum_clear_bit" | "bignum_test_bit"
14307            | "bignum_and" | "bignum_or" | "bignum_xor" | "bignum_not"
14308            | "bignum_shl" | "bignum_shr" | "bignum_compare"
14309            | "bignum_negate" | "bignum_abs" | "bignum_sign"
14310            | "bignum_is_zero" | "bignum_is_negative" | "bignum_is_prime"
14311            | "bignum_random"
14312            | "gravity_constant" | "physics_apply_force" | "physics_apply_impulse"
14313            | "physics_collide_aabb" | "physics_collide_sphere"
14314            | "physics_raycast" | "physics_step"
14315            | "particle_emit" | "particle_update"
14316            | "vector2_new" | "vector2_add" | "vector2_sub" | "vector2_scale"
14317            | "vector2_dot" | "vector2_cross" | "vector2_length"
14318            | "vector2_normalize" | "vector2_distance" | "vector2_rotate"
14319            | "quaternion_new" | "quaternion_from_axis_angle"
14320            | "quaternion_multiply" | "quaternion_normalize" | "quaternion_to_matrix"
14321            | "freq_to_note" | "note_to_freq" | "midi_note_to_name"
14322            | "chord_notes" | "scale_notes" | "transpose_note"
14323            | "window_tukey" | "zero_crossing_rate" | "peak_db"
14324            | "audio_normalize" | "audio_fade_in" | "audio_fade_out"
14325            | "audio_to_mono" | "audio_to_stereo"
14326            | "biquad_lowpass" | "biquad_highpass" | "biquad_bandpass" | "biquad_notch"
14327            | "oscillator_sine" | "oscillator_square"
14328            | "oscillator_sawtooth" | "oscillator_triangle"
14329            | "adsr_envelope" | "ar_envelope" | "crossfade"
14330            | "fade_curve_linear" | "fade_curve_logarithmic" | "fade_curve_exponential"
14331            | "bbox_contains" | "bbox_union" | "bbox_intersect"
14332            | "bbox_center" | "bbox_area"
14333            | "mercator_unproject" | "geohash_precision"
14334            // ── extras ──
14335            | "jq_get" | "jq_set" | "jq_delete" | "jq_select"
14336            | "jq_keys_at" | "jq_values_at" | "jq_length_at"
14337            | "jq_type" | "jq_has" | "jq_paths" | "jq_leaf_paths"
14338            | "jq_walk" | "jq_map_values" | "jq_filter"
14339            | "jq_to_entries" | "jq_from_entries" | "jq_with_entries"
14340            | "jq_recurse" | "jq_min_by" | "jq_max_by"
14341            | "jq_sort_by" | "jq_group_by" | "jq_unique_by"
14342            | "jq_any" | "jq_all" | "jq_flatten"
14343            | "jq_index" | "jq_indices" | "jq_first" | "jq_last"
14344            | "jq_split_at" | "jq_chunks" | "jq_zip" | "jq_combinations"
14345            | "json_diff" | "json_patch" | "json_merge_patch"
14346            | "json_pointer_resolve" | "json_pointer_set"
14347 | "html_to_text" | "html_pretty" | "html_minify"
14348            | "html_sanitize" | "html_strip_tags" | "html_strip_scripts" | "html_strip_styles"
14349            | "html_extract_links" | "html_extract_images" | "html_extract_text"
14350            | "html_extract_meta" | "html_extract_title"
14351            | "html_extract_headings" | "html_extract_tables"
14352            | "html_inner_text" | "html_canonical_url"
14353            | "html_meta_charset" | "html_meta_keywords" | "html_meta_description"
14354            | "html_meta_og" | "html_meta_twitter"
14355            | "html_to_markdown" | "markdown_to_html" | "markdown_render"
14356 | "xml_pretty" | "xml_minify"
14357            | "xml_namespace" | "xml_text" | "xml_attrs"
14358            | "xml_children_by_tag" | "xml_root"
14359            | "xpath_select_one" | "xpath_attribute" | "xpath_text"
14360            | "xml_to_json" | "json_to_xml" | "xml_canonicalize"
14361            | "css_parse" | "css_minify" | "css_pretty"
14362            | "css_selector_parse" | "css_rule_extract" | "css_specificity"
14363            | "css_var_resolve" | "css_property_set" | "css_property_get"
14364            | "css_url_extract" | "css_import_extract" | "css_font_extract"
14365            | "selector_to_xpath" | "xpath_to_selector"
14366            // ── extras ──
14367            | "http_status_continue" | "http_status_switching_protocols"
14368            | "http_status_ok" | "http_status_created" | "http_status_accepted"
14369            | "http_status_no_content" | "http_status_partial_content"
14370            | "http_status_multiple_choices" | "http_status_moved_permanently"
14371            | "http_status_found" | "http_status_see_other" | "http_status_not_modified"
14372            | "http_status_temporary_redirect" | "http_status_permanent_redirect"
14373            | "http_status_bad_request" | "http_status_unauthorized"
14374            | "http_status_payment_required" | "http_status_forbidden"
14375            | "http_status_not_found" | "http_status_method_not_allowed"
14376            | "http_status_not_acceptable" | "http_status_conflict" | "http_status_gone"
14377            | "http_status_length_required" | "http_status_precondition_failed"
14378            | "http_status_payload_too_large" | "http_status_uri_too_long"
14379            | "http_status_unsupported_media_type" | "http_status_range_not_satisfiable"
14380            | "http_status_expectation_failed" | "http_status_im_a_teapot"
14381            | "http_status_unprocessable_entity" | "http_status_too_many_requests"
14382            | "http_status_internal_server_error" | "http_status_not_implemented"
14383            | "http_status_bad_gateway" | "http_status_service_unavailable"
14384            | "http_status_gateway_timeout" | "http_status_http_version_not_supported"
14385            | "http_method_get" | "http_method_post" | "http_method_put"
14386            | "http_method_delete" | "http_method_patch" | "http_method_head"
14387            | "http_method_options" | "http_method_trace" | "http_method_connect"
14388            | "dbeta" | "qbeta" | "rbeta" | "dcauchy" | "qcauchy" | "rcauchy"
14389            | "dexp" | "qexp" | "rexp" | "dgamma" | "qgamma" | "rgamma"
14390            | "dlnorm" | "qlnorm" | "rlnorm" | "dlogis" | "qlogis" | "rlogis"
14391            | "dpois" | "qpois" | "rpois" | "dweibull" | "qweibull" | "rweibull"
14392            | "qnorm" | "rnorm" | "qunif" | "runif"
14393            | "qbinom" | "rbinom" | "qgeom" | "rgeom" | "qhyper" | "rhyper"
14394            | "qchisq" | "rchisq" | "qf" | "rf" | "qt" | "rt"
14395            // ── extras ──
14396            | "currency_format" | "currency_parse" | "currency_round"
14397            | "currency_split_thousands" | "currency_code_to_symbol"
14398            | "currency_symbol_to_code" | "currency_convert" | "currency_rate"
14399            | "currency_iso_4217" | "currency_decimal_places"
14400            | "money_add" | "money_sub" | "money_mul" | "money_div" | "money_compare"
14401            | "tokenize_simple" | "tokenize_word" | "tokenize_subword"
14402            | "tokenize_bpe" | "tokenize_sentencepiece" | "embed_text"
14403            | "cosine_similarity" | "euclidean_distance" | "manhattan_distance"
14404            | "dot_product" | "normalize_vector"
14405            | "vector_add" | "vector_sub" | "vector_scale" | "vector_mean"
14406            | "top_k_indices" | "softmax" | "sigmoid" | "log_softmax" | "cross_entropy"
14407            | "path_canonical" | "path_relative_to" | "path_components"
14408            | "path_filename" | "path_stem" | "path_extension"
14409            | "path_join_many" | "path_with_extension" | "path_with_filename"
14410            | "path_is_subdirectory" | "path_common_ancestor" | "path_strip_prefix"
14411            | "path_glob_match_regex"
14412            | "file_mime" | "file_kind" | "file_attr_get" | "file_attr_set"
14413            | "xattr_get" | "xattr_set" | "xattr_list"
14414            | "file_chmod_string" | "file_chmod_octal" | "file_locked"
14415            | "file_acl_get" | "file_acl_set"
14416            | "locale_parse" | "locale_format" | "locale_language"
14417            | "locale_region" | "locale_script" | "locale_variant" | "locale_canonical"
14418            | "bcp47_parse" | "bcp47_format" | "bcp47_validate"
14419            | "language_tag_match" | "language_tag_subtags"
14420            | "locale_likely_subtags" | "locale_minimize" | "locale_collation"
14421            | "locale_calendar" | "locale_currency"
14422            | "locale_number_format" | "locale_date_format" | "locale_time_format"
14423            | "locale_decimal_separator" | "locale_group_separator"
14424            | "locale_first_day_of_week" | "locale_measurement_system"
14425            | "country_code_alpha2" | "country_code_alpha3" | "country_code_numeric"
14426            | "country_name" | "country_phone_prefix" | "country_currency"
14427            | "country_languages"
14428            | "language_iso_639_1" | "language_iso_639_2" | "language_iso_639_3"
14429            | "language_name"
14430            | "channel_unbounded" | "channel_bounded" | "channel_sync"
14431            | "channel_send_timeout" | "channel_recv_timeout"
14432            | "channel_try_recv" | "channel_try_send"
14433            | "channel_drain" | "channel_close" | "channel_is_closed"
14434            | "broadcast_channel_new" | "broadcast_channel_subscribe"
14435            | "broadcast_channel_publish"
14436            | "mpsc_new" | "mpmc_new" | "spmc_new" | "oneshot_new"
14437            // ── mutex + counting semaphore ─────────────────────────────────
14438            | "mutex" | "mutex_lock" | "mutex_unlock" | "mutex_try_lock" | "mutex_is_locked"
14439            | "semaphore" | "sem"
14440            | "semaphore_acquire" | "sem_acquire"
14441            | "semaphore_release" | "sem_release"
14442            | "semaphore_try_acquire" | "sem_try_acquire"
14443            | "semaphore_permits" | "sem_permits"
14444            | "semaphore_limit" | "sem_limit"
14445            // ── stress testing ──────────────────────────────────────────────
14446            | "stress_cpu" | "scpu" | "stress_mem" | "smem"
14447            | "stress_io" | "sio" | "stress_test" | "st"
14448            | "heat" | "fire" | "fire_and_forget" | "pin"
14449            // ── I/O extensions ──────────────────────────────────────────────
14450            | "slurp" | "cat" | "c" | "capture" | "pager" | "pg" | "less"
14451            | "stdin"
14452            // ── internal ────────────────────────────────────────────────────
14453            | "__stryke_rust_compile"
14454            | "vec_set_value"
14455            // ── short aliases ───────────────────────────────────────────────
14456            | "p" | "rev"
14457            // ── trivial numeric / predicate builtins ────────────────────────
14458            | "even" | "odd" | "zero" | "nonzero"
14459            | "positive" | "pos_n" | "negative" | "neg_n"
14460            | "sign" | "negate" | "double" | "triple" | "half"
14461            | "identity" | "id"
14462            | "round" | "floor" | "ceil" | "ceiling" | "trunc" | "truncn"
14463            | "gcd" | "lcm" | "min2" | "max2"
14464            | "log2" | "log10" | "hypot"
14465            | "rad_to_deg" | "r2d" | "deg_to_rad" | "d2r"
14466            | "pow2" | "abs_diff"
14467            | "factorial" | "fact" | "fibonacci" | "fib"
14468            | "is_prime" | "is_square" | "is_power_of_two" | "is_pow2"
14469            | "cbrt" | "exp2" | "percent" | "pct" | "inverse"
14470            | "median" | "mode_val" | "variance"
14471            // ── trivial string ops ──────────────────────────────────────────
14472            | "is_empty" | "is_blank" | "is_numeric"
14473            | "is_upper" | "is_lower" | "is_alpha" | "is_digit" | "is_alnum"
14474            | "is_space" | "is_whitespace"
14475            | "starts_with" | "sw" | "ends_with" | "ew" | "contains"
14476            | "capitalize" | "cap" | "swap_case" | "repeat"
14477            | "title_case" | "title" | "squish"
14478            | "pad_left" | "lpad" | "pad_right" | "rpad" | "center"
14479            | "truncate_at" | "shorten" | "reverse_str" | "rev_str"
14480            | "char_count" | "word_count" | "wc" | "line_count" | "lc_lines"
14481            // ── trivial type predicates ─────────────────────────────────────
14482            | "is_array" | "is_arrayref" | "is_hash" | "is_hashref"
14483            | "is_code" | "is_coderef" | "is_ref"
14484            | "is_undef" | "is_defined" | "is_def"
14485            | "is_string" | "is_str" | "is_int" | "is_integer" | "is_float"
14486            // ── hash helpers ────────────────────────────────────────────────
14487            | "invert" | "merge_hash"
14488            | "hash_map_values" | "hash_filter_keys" | "hash_filter_values"
14489            | "has_key" | "hk" | "has_any_key" | "has_all_keys"
14490            // ── boolean combinators ─────────────────────────────────────────
14491            | "both" | "either" | "neither" | "xor_bool" | "bool_to_int" | "b2i"
14492            // ── collection helpers (trivial) ────────────────────────────────
14493            | "riffle" | "intersperse" | "every_nth"
14494            | "drop_n" | "take_n" | "rotate" | "swap_pairs"
14495            // ── base conversion ─────────────────────────────────────────────
14496            | "to_bin" | "bin_of" | "to_hex" | "hex_of" | "to_oct" | "oct_of"
14497            | "from_bin" | "from_hex" | "from_oct" | "to_base" | "from_base"
14498            | "bits_count" | "popcount" | "leading_zeros" | "lz"
14499            | "trailing_zeros" | "tz" | "bit_length" | "bitlen"
14500            // ── bit ops ─────────────────────────────────────────────────────
14501            | "bit_and" | "bit_or" | "bit_xor" | "bit_not"
14502            | "shift_left" | "shl" | "shift_right" | "shr"
14503            | "bit_set" | "bit_clear" | "bit_toggle" | "bit_test"
14504            // ── unit conversions: temperature ───────────────────────────────
14505            | "c_to_f" | "f_to_c" | "c_to_k" | "k_to_c" | "f_to_k" | "k_to_f"
14506            // ── unit conversions: distance ──────────────────────────────────
14507            | "miles_to_km" | "km_to_miles" | "miles_to_m" | "m_to_miles"
14508            | "feet_to_m" | "m_to_feet" | "inches_to_cm" | "cm_to_inches"
14509            | "yards_to_m" | "m_to_yards"
14510            // ── unit conversions: mass ──────────────────────────────────────
14511            | "kg_to_lbs" | "lbs_to_kg" | "g_to_oz" | "oz_to_g"
14512            | "stone_to_kg" | "kg_to_stone"
14513            // ── unit conversions: digital ───────────────────────────────────
14514            | "bytes_to_kb" | "b_to_kb" | "kb_to_bytes" | "kb_to_b"
14515            | "bytes_to_mb" | "mb_to_bytes" | "bytes_to_gb" | "gb_to_bytes"
14516            | "kb_to_mb" | "mb_to_gb"
14517            | "bits_to_bytes" | "bytes_to_bits"
14518            // ── unit conversions: time ──────────────────────────────────────
14519            | "seconds_to_minutes" | "s_to_m" | "minutes_to_seconds" | "m_to_s"
14520            | "seconds_to_hours" | "hours_to_seconds"
14521            | "seconds_to_days" | "days_to_seconds"
14522            | "minutes_to_hours" | "hours_to_minutes"
14523            | "hours_to_days" | "days_to_hours"
14524            // ── date helpers ────────────────────────────────────────────────
14525            | "is_leap_year" | "is_leap" | "days_in_month"
14526            | "month_name" | "month_short"
14527            | "weekday_name" | "weekday_short" | "quarter_of"
14528            // ── now / timestamp ─────────────────────────────────────────────
14529            | "now_ms" | "now_us" | "now_ns"
14530            | "unix_epoch" | "epoch" | "unix_epoch_ms" | "epoch_ms"
14531            // ── color / ANSI ────────────────────────────────────────────────
14532            | "rgb_to_hex" | "hex_to_rgb"
14533            | "ansi_red" | "ansi_green" | "ansi_yellow" | "ansi_blue"
14534            | "ansi_magenta" | "ansi_cyan" | "ansi_white" | "ansi_black"
14535            | "ansi_bold" | "ansi_dim" | "ansi_underline" | "ansi_reverse"
14536            | "strip_ansi"
14537            | "red" | "green" | "yellow" | "blue" | "magenta" | "purple" | "cyan"
14538            | "white" | "black" | "bold" | "dim" | "italic" | "underline"
14539            | "strikethrough" | "ansi_off" | "off" | "gray" | "grey"
14540            | "bright_red" | "bright_green" | "bright_yellow" | "bright_blue"
14541            | "bright_magenta" | "bright_cyan" | "bright_white"
14542            | "bg_red" | "bg_green" | "bg_yellow" | "bg_blue"
14543            | "bg_magenta" | "bg_cyan" | "bg_white" | "bg_black"
14544            | "red_bold" | "bold_red" | "green_bold" | "bold_green"
14545            | "yellow_bold" | "bold_yellow" | "blue_bold" | "bold_blue"
14546            | "magenta_bold" | "bold_magenta" | "cyan_bold" | "bold_cyan"
14547            | "white_bold" | "bold_white"
14548            | "blink" | "rapid_blink" | "hidden" | "overline"
14549            | "bg_bright_red" | "bg_bright_green" | "bg_bright_yellow" | "bg_bright_blue"
14550            | "bg_bright_magenta" | "bg_bright_cyan" | "bg_bright_white"
14551            | "rgb" | "bg_rgb" | "color256" | "c256" | "bg_color256" | "bg_c256"
14552            // ── network / validation ────────────────────────────────────────
14553            | "ipv4_to_int" | "int_to_ipv4"
14554            | "is_valid_ipv4" | "is_valid_ipv6" | "is_valid_email" | "is_valid_url"
14555            // ── path helpers ────────────────────────────────────────────────
14556            | "path_ext" | "path_parent" | "path_join" | "path_split"
14557            | "strip_prefix" | "strip_suffix" | "ensure_prefix" | "ensure_suffix"
14558            // ── functional primitives ───────────────────────────────────────
14559            | "const_fn" | "always_true" | "always_false"
14560            | "flip_args" | "first_arg" | "second_arg" | "last_arg"
14561            // ── more list helpers ───────────────────────────────────────────
14562            | "count_eq" | "count_ne" | "all_eq"
14563            | "all_distinct" | "all_unique" | "has_duplicates"
14564            | "sum_of" | "product_of" | "max_of" | "min_of" | "range_of"
14565            // ── string quote / escape ───────────────────────────────────────
14566            | "quote" | "single_quote" | "unquote"
14567            | "extract_between" | "ellipsis"
14568            // ── random ──────────────────────────────────────────────────────
14569            | "coin_flip" | "dice_roll"
14570            | "random_int" | "random_float" | "random_bool"
14571            | "random_choice" | "random_between"
14572            | "random_string" | "random_alpha" | "random_digit"
14573            // ── symbol table ────────────────────────────────────────────────
14574            | "refresh_stashes"
14575            // ── system introspection ────────────────────────────────────────
14576            | "os_name" | "os_arch" | "num_cpus"
14577            | "pid" | "ppid" | "uid" | "gid"
14578            | "username" | "home_dir" | "temp_dir"
14579            | "mem_total" | "mem_free" | "mem_used"
14580            | "swap_total" | "swap_free" | "swap_used"
14581            | "disk_total" | "disk_free" | "disk_avail" | "disk_used"
14582            | "load_avg" | "sys_uptime" | "page_size"
14583            | "os_version" | "os_family" | "endianness" | "pointer_width"
14584            | "proc_mem" | "rss"
14585            // ── collection more ─────────────────────────────────────────────
14586            | "transpose" | "unzip"
14587            | "run_length_encode" | "rle" | "run_length_decode" | "rld"
14588            | "sliding_pairs" | "consecutive_eq" | "flatten_deep"
14589            // ── trig / math ───────────────────────────────────────
14590            | "tan" | "asin" | "acos" | "atan"
14591            | "sinh" | "cosh" | "tanh" | "asinh" | "acosh" | "atanh"
14592            | "sqr" | "cube_fn"
14593            | "mod_op" | "ceil_div" | "floor_div"
14594            | "is_finite" | "is_infinite" | "is_inf" | "is_nan"
14595            | "degrees" | "radians"
14596            | "min_abs" | "max_abs"
14597            | "saturate" | "sat01" | "wrap_around"
14598            // ── string ────────────────────────────────────────────
14599            | "rot13" | "rot47" | "caesar_shift" | "reverse_words"
14600            | "count_vowels" | "count_consonants" | "is_vowel" | "is_consonant"
14601            | "first_word" | "last_word"
14602            | "left_str" | "head_str" | "right_str" | "tail_str" | "mid_str"
14603            | "lowercase" | "uppercase"
14604            | "pascal_case" | "pc_case"
14605            | "constant_case" | "upper_snake" | "dot_case" | "path_case"
14606            | "is_palindrome" | "hamming_distance"
14607            | "longest_common_prefix" | "lcp"
14608            | "ascii_ord" | "ascii_chr" | "count_char" | "indexes_of"
14609            | "replace_first" | "replace_all_str"
14610            | "contains_any" | "contains_all"
14611            | "starts_with_any" | "ends_with_any"
14612            // ── predicates ────────────────────────────────────────
14613            | "is_pair" | "is_triple"
14614            | "is_sorted" | "is_asc" | "is_sorted_desc" | "is_desc"
14615            | "is_empty_arr" | "is_empty_hash"
14616            | "is_subset" | "is_superset" | "is_permutation"
14617            // ── collection ────────────────────────────────────────
14618            | "first_eq" | "last_eq"
14619            | "index_of" | "last_index_of" | "positions_of"
14620            | "batch" | "binary_search" | "bsearch" | "linear_search" | "lsearch"
14621            | "distinct_count" | "longest" | "shortest"
14622            | "array_union" | "list_union"
14623            | "array_intersection" | "list_intersection"
14624            | "array_difference" | "list_difference"
14625            | "symmetric_diff" | "group_of_n" | "chunk_n"
14626            | "repeat_list" | "cycle_n" | "random_sample" | "sample_n"
14627            // ── hash ops ──────────────────────────────────────────
14628            | "pick_keys" | "pick" | "omit_keys" | "omit"
14629            | "map_keys_fn" | "map_values_fn"
14630            | "hash_size" | "hash_from_pairs" | "pairs_from_hash"
14631            | "hash_eq" | "keys_sorted" | "values_sorted" | "remove_keys"
14632            // ── date ──────────────────────────────────────────────
14633            | "today" | "yesterday" | "tomorrow" | "is_weekend" | "is_weekday"
14634            // ── json helpers ────────────────────────────────────────────────
14635            | "json_pretty" | "json_minify" | "escape_json" | "json_escape"
14636            // ── process / env ───────────────────────────────────────────────
14637            | "cmd_exists" | "env_get" | "env_has" | "env_keys"
14638            | "argc" | "script_name"
14639            | "has_stdin_tty" | "has_stdout_tty" | "has_stderr_tty"
14640            // ── id helpers ──────────────────────────────────────────────────
14641            | "uuid_v4" | "nanoid" | "short_id" | "is_uuid" | "token"
14642            // ── url / email parts ───────────────────────────────────────────
14643            | "email_domain" | "email_local"
14644            | "url_host" | "url_path" | "url_query" | "url_scheme"
14645            // ── file stat / path ────────────────────────────────────────────
14646            | "file_size" | "fsize" | "file_mtime" | "mtime"
14647            | "file_atime" | "atime" | "file_ctime" | "ctime"
14648            | "is_symlink" | "is_readable" | "is_writable" | "is_executable"
14649            | "path_is_abs" | "path_is_rel"
14650            // ── stats / sort / array / format / cmp / regex / time conv / volume / force ──
14651            | "min_max" | "percentile" | "harmonic_mean" | "geometric_mean" | "zscore"
14652            | "sorted" | "sorted_desc" | "sorted_nums" | "sorted_by_length"
14653            | "reverse_list" | "list_reverse"
14654            | "without" | "without_nth" | "take_last" | "drop_last"
14655            | "pairwise" | "zipmap"
14656            | "format_bytes" | "human_bytes"
14657            | "format_duration" | "human_duration"
14658            | "format_number" | "group_number"
14659            | "format_percent" | "pad_number"
14660            | "spaceship" | "cmp_num" | "cmp_str"
14661            | "compare_versions" | "version_cmp"
14662            | "hash_insert" | "hash_update" | "hash_delete"
14663            | "matches_regex" | "re_match"
14664            | "count_regex_matches" | "regex_extract"
14665            | "regex_split_str" | "regex_replace_str"
14666            | "shuffle_chars" | "random_char" | "nth_word"
14667            | "head_lines" | "tail_lines" | "count_substring"
14668            | "is_valid_hex" | "hex_upper" | "hex_lower"
14669            | "ms_to_s" | "s_to_ms" | "ms_to_ns" | "ns_to_ms"
14670            | "us_to_ns" | "ns_to_us"
14671            | "liters_to_gallons" | "gallons_to_liters"
14672            | "liters_to_ml" | "ml_to_liters"
14673            | "cups_to_ml" | "ml_to_cups"
14674            | "newtons_to_lbf" | "lbf_to_newtons"
14675            | "joules_to_cal" | "cal_to_joules"
14676            | "watts_to_hp" | "hp_to_watts"
14677            | "pascals_to_psi" | "psi_to_pascals"
14678            | "bar_to_pascals" | "pascals_to_bar"
14679            // ── algebraic match ─────────────────────────────────────────────
14680            | "match"
14681            // ── clojure stdlib (only names not matched above) ─────────────────
14682            | "fst" | "rest" | "rst" | "second" | "snd"
14683            | "last_clj" | "lastc" | "butlast" | "bl"
14684            | "ffirst" | "ffs" | "fnext" | "fne" | "nfirst" | "nfs" | "nnext" | "nne"
14685            | "cons" | "conj"
14686            | "peek_clj" | "pkc" | "pop_clj" | "popc"
14687            | "some" | "not_any" | "not_every"
14688            | "comp" | "compose" | "partial" | "constantly" | "complement" | "compl"
14689            | "fnil" | "juxt"
14690            | "memoize" | "memo" | "curry" | "once"
14691            | "deep_clone" | "dclone" | "deep_merge" | "dmerge" | "deep_equal" | "deq"
14692            | "iterate" | "iter" | "repeatedly" | "rptd" | "cycle" | "cyc"
14693            | "mapcat" | "mcat" | "keep" | "kp" | "remove_clj" | "remc"
14694            | "reductions" | "rdcs"
14695            | "partition_by" | "pby" | "partition_all" | "pall"
14696            | "split_at" | "spat" | "split_with" | "spw"
14697            | "assoc" | "dissoc" | "get_in" | "gin" | "assoc_in" | "ain" | "update_in" | "uin"
14698            | "into" | "empty_clj" | "empc" | "seq" | "vec_clj" | "vecc"
14699            | "apply" | "appl"
14700            // ── python/ruby stdlib ───────────────────────────────────────────
14701            | "divmod" | "dm" | "accumulate" | "accum" | "starmap" | "smap"
14702            | "zip_longest" | "zipl" | "zip_fill" | "zipf" | "combinations" | "comb" | "permutations" | "perm"
14703            | "cartesian_product" | "cprod" | "compress" | "cmpr" | "filterfalse" | "falf"
14704            | "islice" | "isl" | "chain_from" | "chfr" | "pairwise_iter" | "pwi"
14705            | "tee_iter" | "teei" | "groupby_iter" | "gbi"
14706            | "each_slice" | "eslice" | "each_cons" | "econs"
14707            | "one" | "none_match" | "nonem"
14708            | "find_index_fn" | "fidx" | "rindex_fn" | "ridx"
14709            | "minmax" | "mmx" | "minmax_by" | "mmxb"
14710            | "dig" | "values_at" | "vat" | "fetch_val" | "fv" | "slice_arr" | "sla"
14711            | "transform_keys" | "tkeys" | "transform_values" | "tvals"
14712            | "sum_by" | "sumb" | "uniq_by" | "uqb"
14713            | "flat_map_fn" | "fmf" | "then_fn" | "thfn" | "times_fn" | "timf"
14714            | "step" | "upto" | "downto"
14715            // ── javascript array/object methods ─────────────────────────────
14716            | "find_last" | "fndl" | "find_last_index" | "fndli"
14717            | "at_index" | "ati" | "replace_at" | "repa"
14718            | "to_sorted" | "tsrt" | "to_reversed" | "trev" | "to_spliced" | "tspl"
14719            | "flat_depth" | "fltd" | "fill_arr" | "filla" | "includes_val" | "incv"
14720            | "object_keys" | "okeys" | "object_values" | "ovals"
14721            | "object_entries" | "oents" | "object_from_entries" | "ofents"
14722            // ── haskell list functions ──────────────────────────────────────
14723            | "span_fn" | "spanf" | "break_fn" | "brkf" | "group_runs" | "gruns"
14724            | "nub" | "sort_on" | "srton"
14725            | "intersperse_val" | "isp" | "intercalate" | "ical"
14726            | "replicate_val" | "repv" | "elem_of" | "elof" | "not_elem" | "ntelm"
14727            | "lookup_assoc" | "lkpa" | "scanl" | "scanr" | "unfoldr" | "unfr"
14728            // ── rust iterator methods ───────────────────────────────────────
14729            | "find_map" | "fndm" | "filter_map" | "fltm" | "fold_right" | "fldr"
14730            | "partition_either" | "peith" | "try_fold" | "tfld"
14731            | "map_while" | "mapw" | "inspect" | "insp"
14732            // ── ruby enumerable extras ──────────────────────────────────────
14733            | "tally_by" | "talb" | "sole" | "chunk_while" | "chkw" | "count_while" | "cntw"
14734            // ── go/general functional utilities ─────────────────────────────
14735            | "insert_at" | "insa" | "delete_at" | "dela" | "update_at" | "upda"
14736            | "split_on" | "spon" | "words_from" | "wfrm" | "unwords" | "unwds"
14737            | "lines_from" | "lfrm" | "unlines" | "unlns"
14738            | "window_n" | "winn" | "adjacent_pairs" | "adjp"
14739            | "zip_all" | "zall" | "unzip_pairs" | "uzp"
14740            | "interpose" | "ipos" | "partition_n" | "partn"
14741            | "map_indexed" | "mapi" | "reduce_indexed" | "redi" | "filter_indexed" | "flti"
14742            | "group_by_fn" | "gbf" | "index_by" | "idxb" | "associate" | "assoc_fn"
14743            // ── additional missing stdlib functions ─────────────────────────
14744            | "combinations_rep" | "combrep" | "inits" | "tails" | "subsequences" | "subseqs"
14745            | "nub_by" | "nubb" | "slice_when" | "slcw" | "slice_before" | "slcb" | "slice_after" | "slca"
14746            | "each_with_object" | "ewo" | "reduce_right" | "redr"
14747            | "is_sorted_by" | "issrtb" | "intersperse_with" | "ispw"
14748            | "running_reduce" | "runred" | "windowed_circular" | "wincirc"
14749            | "distinct_by" | "distb" | "average" | "mean" | "copy_within" | "cpyw"
14750            | "and_list" | "andl" | "or_list" | "orl" | "concat_map" | "cmap"
14751            | "elem_index" | "elidx" | "elem_indices" | "elidxs" | "find_indices" | "fndidxs"
14752            | "delete_first" | "delfst" | "delete_by" | "delby" | "insert_sorted" | "inssrt"
14753            | "union_list" | "unionl" | "intersect_list" | "intl"
14754            | "maximum_by" | "maxby" | "minimum_by" | "minby" | "batched" | "btch"
14755            // ── Extended stdlib: Text Processing ─────────────────────────────
14756            | "match_all" | "mall" | "capture_groups" | "capg" | "is_match" | "ism"
14757            | "split_regex" | "splre" | "replace_regex" | "replre"
14758            | "is_ascii" | "isasc" | "to_ascii" | "toasc"
14759            | "char_at" | "chat" | "code_point_at" | "cpat" | "from_code_point" | "fcp"
14760            | "normalize_spaces" | "nrmsp" | "remove_whitespace" | "rmws"
14761            | "pluralize" | "plur" | "ordinalize" | "ordn"
14762            | "parse_int" | "pint" | "parse_float" | "pflt" | "parse_bool" | "pbool"
14763            | "levenshtein" | "lev" | "soundex" | "sdx" | "similarity" | "sim"
14764            | "common_prefix" | "cpfx" | "common_suffix" | "csfx"
14765            | "wrap_text" | "wrpt" | "dedent" | "ddt" | "indent" | "idt"
14766            // ── Extended stdlib: Advanced Numeric ────────────────────────────
14767            | "lerp" | "inv_lerp" | "ilerp" | "smoothstep" | "smst" | "remap"
14768 | "dotp" | "cross_product" | "crossp"
14769            | "matrix_mul" | "matmul" | "mm"
14770            | "magnitude" | "mag" | "normalize_vec" | "nrmv"
14771            | "distance" | "dist" | "mdist"
14772            | "covariance" | "cov" | "correlation" | "corr"
14773            | "iqr" | "quantile" | "qntl" | "quantiles" | "qntls"
14774            | "lsp_completion_words" | "lsp_words"
14775            | "doctor" | "health"
14776            | "clamp_int" | "clpi"
14777            | "in_range" | "inrng" | "wrap_range" | "wrprng"
14778            | "sum_squares" | "sumsq" | "rms" | "cumsum" | "csum" | "cumprod" | "cprod_acc" | "diff"
14779            // ── Extended stdlib: Date/Time ───────────────────────────────────
14780            | "add_days" | "addd" | "add_hours" | "addh" | "add_minutes" | "addm"
14781            | "diff_days" | "diffd" | "diff_hours" | "diffh"
14782            | "start_of_day" | "sod" | "end_of_day" | "eod"
14783            | "start_of_hour" | "soh" | "start_of_minute" | "som"
14784            // ── Extended stdlib: Encoding/Hashing ────────────────────────────
14785            | "urle" | "urld"
14786            | "html_encode" | "htmle" | "html_decode" | "htmld"
14787            | "adler32" | "adl32" | "fnv1a" | "djb2"
14788            // ── Extended stdlib: Validation ──────────────────────────────────
14789            | "is_credit_card" | "iscc" | "is_isbn10" | "isbn10" | "is_isbn13" | "isbn13"
14790            | "is_iban" | "isiban" | "is_hex_str" | "ishex" | "is_binary_str" | "isbin"
14791            | "is_octal_str" | "isoct" | "is_json" | "isjson" | "is_base64" | "isb64"
14792            | "is_semver" | "issv" | "is_slug" | "isslug" | "slugify" | "slug"
14793            // ── Extended stdlib: Collection Advanced ─────────────────────────
14794            | "mode_stat" | "mstat" | "sampn" | "weighted_sample" | "wsamp"
14795            | "shuffle_arr" | "shuf" | "argmax" | "amax" | "argmin" | "amin"
14796            | "argsort" | "asrt" | "rank" | "rnk" | "dense_rank" | "drnk"
14797            | "partition_point" | "ppt" | "lower_bound" | "lbound"
14798            | "upper_bound" | "ubound" | "equal_range" | "eqrng"
14799            // ── Extended stdlib: Matrix Operations ───────────────────────────
14800            | "matrix_add" | "madd" | "matrix_sub" | "msub" | "matrix_mult" | "mmult"
14801            | "matrix_scalar" | "mscal" | "matrix_identity" | "mident"
14802            | "matrix_zeros" | "mzeros" | "matrix_ones" | "mones"
14803            | "matrix_diag" | "mdiag" | "matrix_trace" | "mtrace"
14804            | "matrix_row" | "mrow" | "matrix_col" | "mcol"
14805            | "matrix_shape" | "mshape" | "matrix_det" | "mdet"
14806            | "matrix_scale" | "mat_scale" | "diagonal" | "diag"
14807            // ── Extended stdlib: Graph Algorithms ────────────────────────────
14808            | "topological_sort" | "toposort" | "bfs_traverse" | "bfs"
14809            | "dfs_traverse" | "dfs" | "shortest_path_bfs" | "spbfs"
14810            | "connected_components_graph" | "ccgraph"
14811            | "has_cycle_graph" | "hascyc" | "is_bipartite_graph" | "isbip"
14812            // ── Extended stdlib: Data Validation ─────────────────────────────
14813            | "is_ipv4_addr" | "isip4" | "is_ipv6_addr" | "isip6"
14814            | "is_mac_addr" | "ismac" | "is_port_num" | "isport"
14815            | "is_hostname_valid" | "ishost"
14816            | "is_iso_date" | "isisodt" | "is_iso_time" | "isisotm"
14817            | "is_iso_datetime" | "isisodtm"
14818            | "is_phone_num" | "isphone" | "is_us_zip" | "iszip"
14819            // ── Extended stdlib: String Utilities Novel ──────────────────────
14820            | "word_wrap_text" | "wwrap" | "center_text" | "ctxt"
14821            | "ljust_text" | "ljt" | "rjust_text" | "rjt" | "zfill_num" | "zfill"
14822            | "remove_all_str" | "rmall" | "replace_n_times" | "repln"
14823            | "find_all_indices" | "fndalli"
14824            | "text_between" | "txbtwn" | "text_before" | "txbef" | "text_after" | "txaft"
14825            | "text_before_last" | "txbefl" | "text_after_last" | "txaftl"
14826            // ── Extended stdlib: Math Novel ──────────────────────────────────
14827            | "is_even_num" | "iseven" | "is_odd_num" | "isodd"
14828            | "is_positive_num" | "ispos" | "is_negative_num" | "isneg"
14829            | "is_zero_num" | "iszero" | "is_whole_num" | "iswhole"
14830            | "log_with_base" | "logb" | "nth_root_of" | "nroot"
14831            | "frac_part" | "fracp" | "reciprocal_of" | "recip"
14832            | "copy_sign" | "cpsgn" | "fused_mul_add" | "fmadd"
14833            | "floor_mod" | "fmod" | "floor_div_op" | "fdivop"
14834            | "signum_of" | "sgnum" | "midpoint_of" | "midpt"
14835            // ── Extended stdlib: Array Analysis ──────────────────────
14836            | "longest_run" | "lrun" | "longest_increasing" | "linc"
14837            | "longest_decreasing" | "ldec" | "max_sum_subarray" | "maxsub"
14838            | "majority_element" | "majority" | "kth_largest" | "kthl"
14839            | "kth_smallest" | "kths" | "count_inversions" | "cinv"
14840            | "is_monotonic" | "ismono" | "equilibrium_index" | "eqidx"
14841            // ── Extended stdlib: Set Operations ──────────────────────
14842            | "jaccard_index" | "jaccard" | "dice_coefficient" | "dicecoef"
14843            | "overlap_coefficient" | "overlapcoef"
14844            | "power_set" | "powerset" | "cartesian_power" | "cartpow"
14845            // ── Extended stdlib: Advanced String ─────────────────────
14846            | "is_isogram" | "isiso" | "is_heterogram" | "ishet"
14847            | "hamdist" | "jaro_similarity" | "jarosim"
14848            | "longest_common_substring" | "lcsub"
14849            | "longest_common_subsequence" | "lcseq"
14850            | "count_words" | "wcount" | "count_lines" | "lcount"
14851            | "count_chars" | "ccount" | "count_bytes" | "bcount"
14852            // ── Extended stdlib: More Math ───────────────────────────
14853            | "binomial" | "binom" | "catalan" | "catn" | "pascal_row" | "pascrow"
14854            | "is_coprime" | "iscopr" | "euler_totient" | "etot"
14855            | "mobius" | "mob" | "is_squarefree" | "issqfr"
14856            | "digital_root" | "digroot" | "is_narcissistic" | "isnarc"
14857            | "is_harshad" | "isharsh" | "is_kaprekar" | "iskap"
14858            // ── Extended stdlib: Date/Time Additional ────────────────
14859            | "day_of_year" | "doy" | "week_of_year" | "woy"
14860            | "days_in_month_fn" | "daysinmo" | "is_valid_date" | "isvdate"
14861            | "age_in_years" | "ageyrs"
14862            // ── functional combinators ──────────────────────────────────────
14863
14864            | "when_true" | "when_false" | "if_else" | "clamp_fn"
14865            | "attempt" | "try_fn" | "safe_div" | "safe_mod" | "safe_sqrt" | "safe_log"
14866            | "juxt2" | "juxt3" | "tap_val" | "debug_val" | "converge"
14867            | "iterate_n" | "unfold" | "arity_of" | "is_callable"
14868            | "coalesce" | "default_to" | "fallback"
14869            | "apply_list" | "zip_apply" | "scan"
14870            | "keep_if" | "reject_if" | "group_consecutive"
14871            | "after_n" | "before_n" | "clamp_list" | "normalize_list"
14872
14873            // ── matrix / linear algebra ─────────────────────────────────────
14874
14875
14876            | "matrix_multiply" | "mat_mul"
14877            | "identity_matrix" | "eye" | "zeros_matrix" | "zeros" | "ones_matrix" | "ones"
14878
14879
14880
14881            | "vec_normalize" | "unit_vec" | "vec_add" | "vec_sub" | "vec_scale"
14882            | "linspace" | "arange"
14883            // ── more regex ──────────────────────────────────────────────────
14884            | "re_test" | "re_find_all" | "re_groups" | "re_escape"
14885            | "re_split_limit" | "glob_to_regex" | "is_regex_valid"
14886            // ── more process / system ───────────────────────────────────────
14887            | "cwd" | "pwd_str" | "cpu_count" | "is_root" | "uptime_secs"
14888            | "env_pairs" | "env_set" | "env_remove" | "hostname_str" | "is_tty" | "signal_name"
14889            // ── data structure helpers ───────────────────────────────────────
14890            | "stack_new" | "queue_new" | "lru_new"
14891            | "counter" | "counter_most_common" | "defaultdict" | "ordered_set"
14892            | "bitset_new" | "bitset_set" | "bitset_test" | "bitset_clear"
14893            // ── trivial numeric helpers ─────────────────────────────
14894            | "abs_ceil" | "abs_each" | "abs_floor" | "ceil_each" | "dec_each"
14895            | "double_each" | "floor_each" | "half_each" | "inc_each" | "length_each"
14896            | "negate_each" | "not_each" | "offset_each" | "reverse_each" | "round_each"
14897            | "scale_each" | "sqrt_each" | "square_each" | "to_float_each" | "to_int_each"
14898            | "trim_each" | "type_each" | "upcase_each" | "downcase_each" | "bool_each"
14899            // ── math / physics constants ──────────────────────────────────────
14900            | "avogadro" | "boltzmann" | "golden_ratio" | "gravity" | "ln10" | "ln2"
14901            | "planck" | "speed_of_light" | "sqrt2"
14902            // ── physics formulas ──────────────────────────────────────────────
14903            | "bmi_calc" | "compound_interest" | "dew_point" | "discount_amount"
14904            | "force_mass_acc" | "freq_wavelength" | "future_value" | "haversine"
14905            | "heat_index" | "kinetic_energy" | "margin_price" | "markup_price"
14906            | "mortgage_payment" | "ohms_law_i" | "ohms_law_r" | "ohms_law_v"
14907            | "potential_energy" | "present_value" | "simple_interest" | "speed_distance_time"
14908            | "tax_amount" | "tip_amount" | "wavelength_freq" | "wind_chill"
14909            // ── math functions ────────────────────────────────────────────────
14910            | "angle_between_deg" | "approx_eq" | "chebyshev_distance" | "copysign"
14911 | "cube_root" | "entropy" | "float_bits" | "fma"
14912            | "int_bits" | "jaccard_similarity" | "log_base" | "mae" | "mse" | "nth_root"
14913            | "r_squared" | "reciprocal" | "relu" | "rmse" | "rotate_point" | "round_to"
14914 | "signum" | "square_root"
14915            // ── sequences ─────────────────────────────────────────────────────
14916            | "cubes_seq" | "fibonacci_seq" | "powers_of_seq" | "primes_seq"
14917            | "squares_seq" | "triangular_seq"
14918            // ── string helpers ──────────────────────────────────────
14919            | "alternate_case" | "angle_bracket" | "bracket" | "byte_length"
14920            | "bytes_to_hex_str" | "camel_words" | "char_length" | "chars_to_string"
14921            | "chomp_str" | "chop_str" | "filter_chars" | "from_csv_line" | "hex_to_bytes"
14922            | "insert_str" | "intersperse_char" | "ljust" | "map_chars" | "mirror_string"
14923            | "normalize_whitespace" | "only_alnum" | "only_alpha" | "only_ascii"
14924            | "only_digits" | "parenthesize" | "remove_str" | "repeat_string" | "rjust"
14925            | "sentence_case" | "string_count" | "string_sort" | "string_to_chars"
14926            | "string_unique_chars" | "substring" | "to_csv_line" | "trim_left" | "trim_right"
14927            | "xor_strings"
14928            // ── list helpers ─────────────────────────────────────────
14929            | "adjacent_difference" | "append_elem" | "consecutive_pairs" | "contains_elem"
14930            | "count_elem" | "drop_every" | "duplicate_count" | "elem_at" | "find_first"
14931            | "first_elem" | "flatten_once" | "fold_left" | "from_digits" | "from_pairs"
14932            | "group_by_size" | "hash_from_list"
14933            | "hash_merge_deep" | "hash_to_list" | "hash_zip" | "head_n" | "histogram_bins"
14934            | "index_of_elem" | "init_list" | "interleave_lists" | "last_elem" | "least_common"
14935            | "list_compact" | "list_eq" | "list_flatten_deep" | "max_list" | "mean_list"
14936            | "min_list" | "mode_list" | "most_common" | "partition_two" | "prefix_sums"
14937            | "prepend" | "product_list" | "remove_at" | "remove_elem" | "remove_first_elem"
14938            | "repeat_elem" | "running_max" | "running_min" | "sample_one" | "scan_left"
14939            | "second_elem" | "span" | "suffix_sums" | "sum_list" | "tail_n" | "take_every"
14940            | "third_elem" | "to_array" | "to_pairs" | "trimmed_mean" | "unique_count_of"
14941            | "wrap_index" | "digits_of"
14942            // ── predicates ──────────────────────────────────────────
14943            | "all_match" | "any_match" | "is_between" | "is_blank_or_nil" | "is_divisible_by"
14944            | "is_email" | "is_even" | "is_falsy" | "is_fibonacci" | "is_hex_color"
14945            | "is_in_range" | "is_ipv4" | "is_multiple_of" | "is_negative" | "is_nil"
14946            | "is_nonzero" | "is_odd" | "is_perfect_square" | "is_positive" | "is_power_of"
14947            | "is_prefix" | "is_present" | "is_strictly_decreasing" | "is_strictly_increasing"
14948            | "is_suffix" | "is_triangular" | "is_truthy" | "is_url" | "is_whole" | "is_zero"
14949            // ── counters ────────────────────────────────────────────
14950            | "count_digits" | "count_letters" | "count_lower" | "count_match"
14951            | "count_punctuation" | "count_spaces" | "count_upper" | "defined_count"
14952            | "empty_count" | "falsy_count" | "nonempty_count" | "numeric_count"
14953            | "truthy_count" | "undef_count"
14954            // ── conversion / utility ────────────────────────────────
14955            | "assert_type" | "between" | "clamp_each" | "die_if" | "die_unless"
14956            | "join_colons" | "join_commas" | "join_dashes" | "join_dots" | "join_lines"
14957            | "join_pipes" | "join_slashes" | "join_spaces" | "join_tabs" | "measure"
14958            | "max_float" | "min_float" | "noop_val" | "nop" | "pass" | "pred" | "succ"
14959            | "tap_debug" | "to_bool" | "to_float" | "to_int" | "to_string" | "void"
14960            | "range_exclusive" | "range_inclusive"
14961            // ── math / numeric extras ─────────────────────────────────────────
14962            | "aliquot_sum" | "autocorrelation" | "bell_number" | "cagr" | "coeff_of_variation"
14963            | "collatz_length" | "collatz_sequence" | "convolution"
14964            | "depreciation_double" | "depreciation_linear" | "discount" | "divisors"
14965            | "epsilon" | "euler_number" | "exponential_moving_average"
14966            | "f64_max" | "f64_min" | "fft_magnitude" | "goldbach" | "i64_max" | "i64_min"
14967            | "kurtosis" | "linear_regression" | "look_and_say" | "lucas" | "luhn_check"
14968            | "mean_absolute_error" | "mean_squared_error" | "median_absolute_deviation"
14969            | "minkowski_distance" | "moving_average" | "multinomial" | "neg_inf" | "npv"
14970            | "num_divisors" | "partition_number" | "pascals_triangle" | "skewness"
14971            | "standard_error" | "subfactorial" | "sum_divisors" | "totient_sum"
14972            | "tribonacci" | "weighted_mean" | "winsorize"
14973            // ── statistics (extended) ─────────────────────────────────────────
14974            | "chi_square_stat" | "describe" | "five_number_summary"
14975            | "gini" | "gini_coefficient" | "lorenz_curve" | "outliers_iqr"
14976            | "percentile_rank" | "quartiles" | "sample_stddev" | "sample_variance"
14977            | "spearman_correlation" | "t_test_one_sample" | "t_test_two_sample"
14978            | "z_score" | "z_scores"
14979            // ── number theory / primes ──────────────────────────────────────────
14980            | "abundant_numbers" | "deficient_numbers" | "is_abundant" | "is_deficient"
14981            | "is_pentagonal" | "is_perfect" | "is_smith" | "next_prime" | "nth_prime"
14982            | "pentagonal_number" | "perfect_numbers" | "prev_prime" | "prime_factors"
14983            | "prime_pi" | "primes_up_to" | "triangular_number" | "twin_primes"
14984            // ── geometry / physics ──────────────────────────────────────────────
14985            | "area_circle" | "area_ellipse" | "area_rectangle" | "area_trapezoid" | "area_triangle"
14986            | "bearing" | "circumference" | "cone_volume" | "cylinder_volume" | "heron_area"
14987            | "midpoint" | "perimeter_rectangle" | "perimeter_triangle" | "point_distance"
14988            | "polygon_area" | "slope" | "sphere_surface" | "sphere_volume" | "triangle_hypotenuse"
14989            // ── geometry (extended) ───────────────────────────────────────────
14990            | "angle_between" | "arc_length" | "bounding_box" | "centroid"
14991            | "circle_from_three_points" | "circ3" | "convex_hull" | "ellipse_perimeter" | "ellper"
14992            | "frustum_volume" | "haversine_distance" | "line_intersection"
14993            | "point_in_polygon" | "pip" | "polygon_perimeter" | "polyper" | "pyramid_volume"
14994            | "reflect_point" | "scale_point" | "sector_area"
14995            | "torus_surface" | "torus_volume" | "translate_point"
14996            | "vector_angle" | "vector_cross" | "vector_dot" | "vector_magnitude" | "vector_normalize"
14997            // ── constants ───────────────────────────────────────────────────────
14998            | "avogadro_number" | "boltzmann_constant" | "electron_mass" | "elementary_charge"
14999            | "gravitational_constant" | "phi" | "pi" | "PI" | "planck_constant"
15000            | "proton_mass" | "sol" | "tau" | "TAU" | "E"
15001            // ── finance ─────────────────────────────────────────────────────────
15002            | "bac_estimate" | "bmi" | "break_even" | "margin" | "markup" | "roi" | "tax" | "tip"
15003            // ── finance (extended) ────────────────────────────────────────────
15004            | "amortization_schedule" | "black_scholes_call" | "black_scholes_put"
15005            | "bond_price" | "bond_yield" | "capm" | "continuous_compound" | "ccomp"
15006            | "discounted_payback" | "duration" | "irr"
15007            | "max_drawdown" | "mdd" | "modified_duration" | "mod_dur" | "nper" | "num_periods" | "payback_period"
15008            | "pmt" | "pv" | "rule_of_72" | "sharpe_ratio" | "sortino_ratio"
15009            | "wacc" | "xirr"
15010            // ── string processing extras ──────────────────────────────────────
15011            | "acronym" | "atbash" | "bigrams" | "camel_to_snake" | "char_frequencies"
15012            | "chunk_string" | "collapse_whitespace" | "dedent_text" | "indent_text"
15013            | "initials" | "leetspeak" | "mask_string" | "ngrams" | "pig_latin"
15014            | "remove_consonants" | "remove_vowels" | "reverse_each_word" | "snake_to_camel"
15015            | "sort_words" | "string_distance" | "string_multiply" | "strip_html"
15016            | "trigrams" | "unique_words" | "word_frequencies" | "zalgo"
15017            // ── encoding / phonetics ────────────────────────────────────────────
15018            | "braille_encode" | "double_metaphone" | "metaphone" | "morse_decode"
15019            | "morse_encode" | "nato_phonetic" | "phonetic_digit" | "subscript" | "superscript"
15020            | "to_emoji_num"
15021            // ── roman numerals ──────────────────────────────────────────────────
15022            | "int_to_roman" | "roman_add" | "roman_numeral_list" | "roman_to_int"
15023            // ── base / gray code ────────────────────────────────────────────────
15024            | "base_convert" | "binary_to_gray" | "gray_code_sequence" | "gray_to_binary"
15025            // ── color operations ────────────────────────────────────────────────
15026            | "ansi_256" | "ansi_truecolor" | "color_blend" | "color_complement"
15027            | "color_darken" | "color_distance" | "color_grayscale" | "color_invert"
15028            | "color_lighten" | "hsl_to_rgb" | "hsv_to_rgb" | "random_color"
15029            | "rgb_to_hsl" | "rgb_to_hsv"
15030            // ── matrix operations extras ──────────────────────────────────────
15031            | "matrix_flatten" | "matrix_from_rows" | "matrix_hadamard" | "matrix_inverse"
15032            | "matrix_map" | "matrix_max" | "matrix_min" | "matrix_power" | "matrix_sum"
15033            | "matrix_transpose"
15034            // ── array / list operations extras ────────────────────────────────
15035            | "binary_insert" | "bucket" | "clamp_array" | "group_consecutive_by"
15036            | "histogram" | "merge_sorted" | "next_permutation" | "normalize_array"
15037            | "normalize_range" | "peak_detect" | "range_compress" | "range_expand"
15038            | "reservoir_sample" | "run_length_decode_str" | "run_length_encode_str"
15039            | "zero_crossings"
15040            // ── DSP / signal (extended) ───────────────────────────────────────
15041            | "apply_window" | "bandpass_filter" | "cross_correlation" | "dft"
15042            | "downsample" | "decimate" | "energy" | "envelope" | "hilbert_env" | "highpass_filter" | "idft"
15043            | "lowpass_filter" | "median_filter" | "normalize_signal" | "phase_spectrum"
15044            | "power_spectrum" | "psd" | "resample" | "spectral_centroid" | "spectrogram" | "stft" | "upsample" | "interpolate"
15045            | "window_blackman" | "window_hamming" | "window_hann" | "window_kaiser"
15046            // ── validation predicates extras ──────────────────────────────────
15047            | "is_anagram" | "is_balanced_parens" | "is_control" | "is_numeric_string"
15048            | "is_pangram" | "is_printable" | "is_valid_cidr" | "is_valid_cron"
15049            | "is_valid_hex_color" | "is_valid_latitude" | "is_valid_longitude" | "is_valid_mime"
15050            // ── algorithms / puzzles ────────────────────────────────────────────
15051            | "eval_rpn" | "fizzbuzz" | "game_of_life_step" | "mandelbrot_char"
15052            | "sierpinski" | "tower_of_hanoi" | "truth_table"
15053            // ── misc / utility ──────────────────────────────────────────────────
15054            | "byte_size" | "degrees_to_compass" | "to_string_val" | "type_of"
15055            // ── math formulas ───────────────────────────────────────────────────
15056            | "quadratic_roots" | "quadratic_discriminant" | "arithmetic_series"
15057            | "geometric_series" | "stirling_approx"
15058            | "double_factorial" | "rising_factorial" | "falling_factorial"
15059            | "gamma_approx" | "erf_approx" | "normal_pdf" | "normal_cdf"
15060            | "poisson_pmf" | "exponential_pdf" | "inverse_lerp"
15061            | "map_range"
15062            // ── physics formulas ────────────────────────────────────────────────
15063            | "momentum" | "impulse" | "work" | "power_phys" | "torque" | "angular_velocity"
15064            | "centripetal_force" | "escape_velocity" | "orbital_velocity" | "orbital_period"
15065            | "gravitational_force" | "coulomb_force" | "electric_field" | "capacitance"
15066            | "capacitor_energy" | "inductor_energy" | "resonant_frequency"
15067            | "rc_time_constant" | "rl_time_constant" | "impedance_rlc"
15068            | "relativistic_mass" | "lorentz_factor" | "time_dilation" | "length_contraction"
15069            | "relativistic_energy" | "rest_energy" | "de_broglie_wavelength"
15070            | "photon_energy" | "photon_energy_wavelength" | "schwarzschild_radius"
15071            | "stefan_boltzmann" | "wien_displacement" | "ideal_gas_pressure" | "ideal_gas_volume"
15072            | "projectile_range" | "projectile_max_height" | "projectile_time"
15073            | "spring_force" | "spring_energy" | "pendulum_period" | "doppler_frequency"
15074            | "decibel_ratio" | "snells_law" | "brewster_angle" | "critical_angle"
15075            | "lens_power" | "thin_lens" | "magnification_lens"
15076            // ── math constants ──────────────────────────────────────────────────
15077            | "euler_mascheroni" | "apery_constant" | "feigenbaum_delta" | "feigenbaum_alpha"
15078            | "catalan_constant" | "khinchin_constant" | "glaisher_constant"
15079            | "plastic_number" | "silver_ratio" | "supergolden_ratio"
15080            // ── physics constants ───────────────────────────────────────────────
15081            | "vacuum_permittivity" | "vacuum_permeability" | "coulomb_constant"
15082            | "fine_structure_constant" | "rydberg_constant" | "bohr_radius"
15083            | "bohr_magneton" | "nuclear_magneton" | "stefan_boltzmann_constant"
15084            | "wien_constant" | "gas_constant" | "faraday_constant" | "neutron_mass"
15085            | "atomic_mass_unit" | "earth_mass" | "earth_radius" | "sun_mass" | "sun_radius"
15086            | "astronomical_unit" | "light_year" | "parsec" | "hubble_constant"
15087            | "planck_length" | "planck_time" | "planck_mass" | "planck_temperature"
15088            // ── linear algebra (extended) ──────────────────────────────────
15089            | "matrix_solve" | "msolve" | "solve"
15090            | "matrix_lu" | "mlu" | "matrix_qr" | "mqr"
15091            | "matrix_eigenvalues" | "meig" | "eigenvalues" | "eig"
15092            | "matrix_norm" | "mnorm" | "matrix_cond" | "mcond" | "cond"
15093            | "matrix_pinv" | "mpinv" | "pinv"
15094            | "matrix_cholesky" | "mchol" | "cholesky"
15095            | "matrix_det_general" | "mdetg" | "det"
15096            // ── statistics tests (extended) ────────────────────────────────
15097            | "welch_ttest" | "welcht" | "paired_ttest" | "pairedt"
15098            | "cohen_d" | "cohend" | "anova_oneway" | "anova" | "anova1"
15099            | "spearman_corr" | "rho" | "kendall_tau" | "kendall" | "ktau"
15100            | "confidence_interval" | "ci"
15101            // ── distributions (extended) ──────────────────────────────────
15102            | "beta_pdf" | "betapdf" | "gamma_pdf" | "gammapdf"
15103            | "chi2_pdf" | "chi2pdf" | "chi_squared_pdf"
15104            | "t_pdf" | "tpdf" | "student_pdf"
15105            | "f_pdf" | "fpdf" | "fisher_pdf"
15106            | "lognormal_pdf" | "lnormpdf" | "weibull_pdf" | "weibpdf"
15107            | "cauchy_pdf" | "cauchypdf" | "laplace_pdf" | "laplacepdf"
15108            | "pareto_pdf" | "paretopdf"
15109            // ── interpolation & curve fitting ─────────────────────────────
15110            | "lagrange_interp" | "lagrange" | "linterp"
15111            | "cubic_spline" | "cspline" | "spline"
15112            | "poly_eval" | "polyval" | "polynomial_fit" | "polyfit"
15113            // ── numerical integration & differentiation ───────────────────
15114            | "trapz" | "trapezoid" | "simpson" | "simps"
15115            | "numerical_diff" | "numdiff" | "diff_array"
15116            | "cumtrapz" | "cumulative_trapz"
15117            // ── optimization / root finding ────────────────────────────────
15118            | "bisection" | "bisect" | "newton_method" | "newton" | "newton_raphson"
15119            | "golden_section" | "golden" | "gss"
15120            // ── ODE solvers ───────────────────────────────────────────────
15121            | "rk4" | "runge_kutta" | "rk4_ode" | "euler_ode" | "euler_method"
15122            // ── graph algorithms (extended) ────────────────────────────────
15123            | "dijkstra" | "shortest_path" | "bellman_ford" | "bellmanford"
15124            | "floyd_warshall" | "floydwarshall" | "apsp"
15125            | "prim_mst" | "mst" | "prim"
15126            // ── trig extensions ───────────────────────────────────────────
15127            | "cot" | "sec" | "csc" | "acot" | "asec" | "acsc" | "sinc" | "versin" | "versine"
15128            // ── ML activation functions ───────────────────────────────────
15129            | "leaky_relu" | "lrelu" | "elu" | "selu" | "gelu"
15130            | "silu" | "swish" | "mish" | "softplus"
15131            | "hard_sigmoid" | "hardsigmoid" | "hard_swish" | "hardswish"
15132            // ── special functions ─────────────────────────────────────────
15133            | "bessel_j0" | "j0" | "bessel_j1" | "j1"
15134            | "lambert_w" | "lambertw" | "productlog"
15135            // ── Wolfram-Math parity: Bessel/Airy/Hankel/Struve/Kelvin ─────
15136            | "bessel_j" | "bessel_y" | "bessel_i" | "bessel_k"
15137            | "hankel_h1" | "hankel_h2" | "bessel_j_zero"
15138            | "airy_ai" | "airy_bi" | "airy_ai_prime" | "airy_bi_prime"
15139            | "spherical_bessel_j" | "spherical_bessel_y"
15140            | "struve_h" | "struve_l" | "kelvin_ber" | "kelvin_bei"
15141            // ── orthogonal polynomials ────────────────────────────────────
15142            | "legendre_p" | "legendre_q" | "assoc_legendre_p"
15143            | "hermite_h" | "hermite_he" | "laguerre_l" | "assoc_laguerre_l"
15144            | "jacobi_p" | "gegenbauer_c" | "chebyshev_t" | "chebyshev_u"
15145            | "spherical_harmonic_y" | "zernike_r"
15146            // ── elliptic integrals + Jacobi/Weierstrass/theta ─────────────
15147            | "elliptic_k" | "elliptic_e" | "elliptic_pi" | "elliptic_f"
15148            | "elliptic_e_inc" | "elliptic_pi_inc"
15149            | "carlson_rf" | "carlson_rd" | "carlson_rj"
15150            | "jacobi_sn" | "jacobi_cn" | "jacobi_dn" | "jacobi_am"
15151            | "elliptic_theta"
15152            | "weierstrass_p" | "weierstrass_zeta" | "weierstrass_sigma"
15153            // ── zeta / polylog / Lerch ────────────────────────────────────
15154            | "zeta" | "riemann_zeta" | "hurwitz_zeta"
15155            | "polylog" | "dilog" | "lerch_phi"
15156            | "riemann_siegel_z" | "riemann_siegel_theta"
15157            | "dirichlet_eta" | "dirichlet_beta"
15158            // ── hypergeometric ────────────────────────────────────────────
15159            | "hypergeometric_2f1" | "hyper_2f1"
15160            | "hypergeometric_1f1" | "hyper_1f1" | "kummer_m"
15161            | "hypergeometric_0f1" | "hyper_0f1"
15162            | "hypergeometric_pfq" | "hyper_pfq"
15163            | "hypergeometric_u" | "tricomi_u"
15164            // ── modular forms ─────────────────────────────────────────────
15165            | "dedekind_eta" | "klein_j" | "klein_invariant_j"
15166            | "modular_lambda" | "ramanujan_tau"
15167            // ── integrals: Si / Ci / Ei / Li / Fresnel ────────────────────
15168            | "sin_integral" | "si_int" | "cos_integral" | "ci_int"
15169            | "sinh_integral" | "shi_int" | "cosh_integral" | "chi_int"
15170            | "exp_integral_e" | "ei_n" | "exp_integral_ei" | "ei_int"
15171            | "log_integral" | "li_int" | "fresnel_s" | "fresnel_c"
15172            // ── number-theory gaps ────────────────────────────────────────
15173            | "jacobi_symbol" | "kronecker_symbol"
15174            | "primitive_root" | "multiplicative_order"
15175            | "mangoldt_lambda" | "von_mangoldt" | "carmichael_lambda"
15176            | "squares_r" | "thue_morse" | "rudin_shapiro"
15177            | "farey_sequence" | "farey"
15178            | "frobenius_number" | "frobenius_solve" | "stern_brocot"
15179            // ── combinatorial gaps ────────────────────────────────────────
15180            | "stirling_s1" | "stirling_first" | "bell_polynomial_b" | "bell_y"
15181            | "clebsch_gordan" | "three_j_symbol" | "wigner_3j"
15182            | "six_j_symbol" | "wigner_6j" | "nine_j_symbol" | "wigner_9j"
15183            | "debruijn_sequence" | "debruijn" | "wigner_d"
15184            // ── q-series, Mittag-Leffler, Coulomb wave ────────────────────
15185            | "q_pochhammer" | "q_factorial" | "q_binomial"
15186            | "q_hypergeometric_pfq"
15187            | "mittag_leffler_e" | "mittag_leffler"
15188            | "coulomb_wave_f" | "coulomb_wave_g"
15189            // ── inverse special functions ─────────────────────────────────
15190            | "inverse_erf" | "erfinv" | "inverse_erfc" | "erfcinv"
15191            | "inverse_gamma_regularized" | "gamma_lr_inv"
15192            | "inverse_beta_regularized" | "beta_reg_inv"
15193            | "inverse_jacobi_sn"
15194            // ── piecewise / symbolic primitives ───────────────────────────
15195            | "dirac_delta" | "heaviside_theta" | "heaviside"
15196            | "unit_box" | "unit_triangle"
15197            | "square_wave" | "triangle_wave" | "sawtooth_wave" | "dirac_comb"
15198            // ── Tier A: number theory extensions ──────────────────────────
15199            | "liouville_lambda" | "jordan_totient" | "ramanujan_sum"
15200            | "cyclotomic_polynomial" | "cyclotomic" | "legendre_symbol"
15201            | "pythagorean_triple_q" | "gen_pythagorean_triple"
15202            | "sophie_germain_q" | "mersenne_q"
15203            | "lucas_lehmer_test" | "lucas_lehmer"
15204            | "continued_fraction" | "from_continued_fraction" | "convergents"
15205            | "best_rational_approximation" | "best_rational"
15206            // ── Tier B: combinatorial sequences ───────────────────────────
15207            | "motzkin_number" | "motzkin"
15208            | "narayana_number" | "narayana"
15209            | "delannoy_number" | "delannoy"
15210            | "schroder_number" | "schroder" | "large_schroder"
15211            | "small_schroder_number" | "small_schroder"
15212            | "eulerian_number"
15213            | "bernoulli_polynomial" | "euler_polynomial"
15214            | "pell_number" | "pell" | "pell_lucas_number" | "pell_lucas"
15215            | "perrin_number" | "perrin" | "padovan_number" | "padovan"
15216            // ── Tier C: linear algebra extras ─────────────────────────────
15217            | "kronecker_product" | "tensor_product" | "tensor_contract"
15218            | "matrix_rank" | "mrank"
15219            | "companion_matrix" | "companion"
15220            | "characteristic_polynomial" | "charpoly"
15221            | "singular_values" | "svals"
15222            | "nullspace" | "null_space" | "kernel"
15223            // ── Tier D: polynomial algebra ────────────────────────────────
15224            | "polynomial_gcd" | "polygcd"
15225            | "polynomial_quotient" | "polyquot"
15226            | "polynomial_remainder" | "polyrem"
15227            | "polynomial_resultant" | "resultant"
15228            | "polynomial_discriminant" | "discriminant"
15229            | "polynomial_roots" | "polyroots"
15230            // ── Tier E: more distributions ────────────────────────────────
15231            | "gumbel_pdf" | "gumbel_cdf" | "gumbel_quantile"
15232            | "frechet_pdf" | "frechet_cdf" | "frechet_quantile"
15233            | "logistic_pdf" | "logistic_cdf" | "logistic_quantile"
15234            | "rayleigh_pdf" | "rayleigh_cdf" | "rayleigh_quantile"
15235            | "inverse_gamma_pdf" | "inverse_gamma_cdf" | "inverse_gamma_quantile"
15236            | "kumaraswamy_pdf" | "kumaraswamy_cdf" | "kumaraswamy_quantile"
15237            // ── Tier F: Mathieu ───────────────────────────────────────────
15238            | "mathieu_a" | "mathieu_characteristic_a"
15239            | "mathieu_ce" | "mathieu_se"
15240            // ── Tier G: Heun general ──────────────────────────────────────
15241            | "heun_g"
15242            // ── Tier H: wavelets ──────────────────────────────────────────
15243            | "haar_transform" | "haar" | "haar_inverse" | "ihaar"
15244            | "daubechies_db4" | "db4" | "daubechies_db4_inverse" | "idb4"
15245            // ── Tier I: graph algorithms ──────────────────────────────────
15246            | "topo_sort_adj"
15247            | "scc_tarjan" | "tarjan_scc" | "strongly_connected"
15248            | "bipartite_q" | "is_bipartite"
15249            | "max_flow_edmonds_karp" | "max_flow" | "edmonds_karp"
15250            | "min_cut" | "eccentricity"
15251            | "graph_diameter" | "graph_radius"
15252            // ── Tier J: misc fillers ──────────────────────────────────────
15253            | "stieltjes_constant" | "stieltjes"
15254            | "gauss_sum" | "kloosterman_sum"
15255            | "eta_quotient" | "root_approximant"
15256            // ── vector calculus ──────────────────────────────────
15257            | "numerical_gradient" | "ngrad"
15258            | "numerical_jacobian" | "njac"
15259            | "numerical_hessian" | "nhess"
15260            | "numerical_divergence" | "ndiv"
15261            | "numerical_curl" | "ncurl"
15262            | "numerical_laplacian" | "nlap"
15263            // ── optimization ─────────────────────────────────────
15264            | "nelder_mead" | "simplex_min"
15265            | "gradient_descent" | "gd_min"
15266            | "bfgs_minimize" | "bfgs"
15267            | "levenberg_marquardt" | "lev_marq" | "lm_min"
15268            | "conjugate_gradient" | "cg_solve"
15269            | "least_squares" | "lstsq"
15270            // ── integration extras ───────────────────────────────
15271            | "romberg" | "romberg_int"
15272            | "gauss_legendre_quad" | "glquad" | "gl_quad"
15273            | "monte_carlo_integrate" | "mc_int"
15274            | "adaptive_simpson" | "asimp"
15275            // ── LA extras ────────────────────────────────────────
15276            | "lu_decompose" | "ludec"
15277            | "qr_decompose" | "qrdec"
15278            | "householder_reflector" | "householder"
15279            | "givens_rotation" | "givens"
15280            | "forward_substitute" | "fwdsub"
15281            | "back_substitute" | "backsub"
15282            | "hessenberg_reduce" | "hessen"
15283            // ── polynomial helpers ───────────────────────────────
15284            | "poly_derivative" | "polyder"
15285            | "poly_integrate" | "polyint"
15286            | "poly_compose" | "poly_eval_horner" | "horner"
15287            | "pade_approximant" | "pade"
15288            // ── quaternions ──────────────────────────────────────
15289            | "quat_mul" | "quat_conj" | "quat_norm" | "quat_inv"
15290            | "quat_from_axis_angle" | "axis_angle_to_quat"
15291            | "quat_to_axis_angle"
15292            | "quat_to_matrix" | "quat_from_matrix" | "matrix_to_quat"
15293            | "quat_slerp" | "slerp"
15294            | "euler_zyx_to_matrix" | "matrix_to_euler_zyx"
15295            | "rotate_3d_vec"
15296            // ── information theory ───────────────────────────────
15297            | "kl_divergence" | "kl_div"
15298            | "js_divergence" | "js_div"
15299            | "mutual_information" | "mi"
15300            | "cross_entropy_arr" | "cross_entropy_dist"
15301            | "renyi_entropy" | "tsallis_entropy"
15302            // ── quantum ──────────────────────────────────────────
15303            | "pauli_x" | "pauli_y" | "pauli_z"
15304            | "pauli_id" | "pauli_i" | "pauli_identity"
15305            | "ket_bra" | "density_matrix" | "expectation_value" | "expval"
15306            | "commutator" | "anticommutator"
15307            | "partial_trace" | "ptrace"
15308            | "von_neumann_entropy" | "vn_entropy"
15309            // ── stat mech ────────────────────────────────────────
15310            | "bose_einstein" | "fermi_dirac"
15311            | "maxwell_boltzmann_speed" | "mb_speed"
15312            | "partition_function" | "z_partition"
15313            | "helmholtz_free_energy" | "free_energy_f"
15314            | "boltzmann_factor"
15315            | "einstein_specific_heat" | "einstein_cv"
15316            // ── optics ───────────────────────────────────────────
15317            | "fresnel_reflection_te" | "fresnel_reflection_tm"
15318            | "fresnel_transmission_te" | "fresnel_transmission_tm"
15319            | "abcd_thin_lens" | "abcd_free_space"
15320            | "gaussian_beam_q"
15321            // ── astrodynamics ────────────────────────────────────
15322            | "kepler_solve"
15323            | "true_to_eccentric" | "eccentric_to_mean"
15324            | "julian_date" | "j_date"
15325            | "jd_to_gregorian" | "jd_to_date"
15326            | "sidereal_time_gmst" | "gmst"
15327            | "vis_viva" | "orbital_period_kepler"
15328            | "orbital_elements_to_state" | "elem_to_state"
15329            // ── time series ──────────────────────────────────────
15330            | "kalman_step" | "kalman_filter"
15331            | "exponential_smoothing" | "exp_smooth"
15332            | "holt_winters" | "arma_yw_fit" | "ar_yw"
15333            // ── graph centrality ─────────────────────────────────
15334            | "pagerank" | "betweenness_centrality" | "closeness_centrality"
15335            | "eigenvector_centrality" | "degree_centrality" | "triangle_count"
15336            // ── random samplers ──────────────────────────────────
15337            | "rgumbel" | "rfrechet" | "rrayleigh"
15338            | "rlogistic" | "rkumaraswamy" | "rinverse_gamma" | "rinvgamma"
15339            // ── 2D geometry ──────────────────────────────────────
15340            | "graham_scan" | "convex_hull_2d"
15341            | "line_line_intersect_2d" | "ll_intersect_2d"
15342            | "point_segment_distance" | "p_seg_dist"
15343            // ── auto-diff ────────────────────────────────────────
15344            | "forward_diff" | "fdiff"
15345            | "forward_diff_grad" | "fdiff_grad"
15346            // ── stat tests ───────────────────────────────────────
15347            | "bartlett_test" | "levene_test"
15348            | "fishers_exact_test_2x2" | "fishers_exact"
15349            | "mcnemar_test"
15350            | "runs_test" | "wald_wolfowitz"
15351            | "friedman_test" | "kruskal_wallis_test" | "kruskal"
15352            | "sign_test"
15353            | "anderson_darling_normality" | "ad_normality"
15354            | "jarque_bera_test" | "jb_test"
15355            | "ljung_box_test" | "ljung_box"
15356            | "durbin_watson_stat" | "durbin_watson"
15357            // ── distance metrics ─────────────────────────────────
15358            | "mahalanobis_distance" | "mahalanobis_dist"
15359            | "cosine_distance" | "canberra_distance"
15360            | "bray_curtis_distance" | "bray_curtis"
15361            | "l1_distance"
15362            | "chi_squared_distance"
15363            // ── more distributions ───────────────────────────────
15364            | "multivariate_normal_pdf" | "mvn_pdf"
15365            | "multivariate_normal_sample" | "rmvn"
15366            | "dirichlet_pdf" | "dirichlet_sample" | "rdirichlet"
15367            | "skellam_pmf"
15368            | "inverse_gaussian_pdf" | "wald_pdf"
15369            | "inverse_gaussian_cdf" | "wald_cdf"
15370            | "inverse_gaussian_sample" | "rwald"
15371            | "non_central_chi2_pdf" | "ncchi2_pdf"
15372            // ── matrix functions ─────────────────────────────────
15373            | "matrix_exp" | "expm" | "matrix_log" | "logm"
15374            | "matrix_sqrt" | "sqrtm" | "matrix_sin" | "sinm"
15375            | "matrix_cos" | "cosm"
15376            // ── adaptive ODE ─────────────────────────────────────
15377            | "rk45_dormand_prince" | "rk45" | "dopri5"
15378            | "midpoint_step" | "ode_midpoint"
15379            | "heun_step" | "ode_heun"
15380            | "verlet_step" | "ode_verlet"
15381            // ── GLM ──────────────────────────────────────────────
15382            | "logistic_regression" | "logit_fit"
15383            | "poisson_regression"
15384            | "ridge_regression" | "ridge"
15385            | "lasso_coord" | "lasso"
15386            // ── bootstrap/resampling ─────────────────────────────
15387            | "bootstrap_mean_ci" | "boot_mean_ci"
15388            | "jackknife_estimate" | "jackknife"
15389            | "permutation_test_diff" | "perm_test_diff"
15390            // ── time series extras ───────────────────────────────
15391            | "acf_at_lag" | "diff_op" | "lag_op"
15392            | "decompose_classical" | "decompose_ts"
15393            // ── combinatorial generators ─────────────────────────
15394            | "combinations_list" | "permutations_list"
15395            | "cyclic_permutations" | "subsets_of_size"
15396            // ── DP utilities ─────────────────────────────────────
15397            | "longest_increasing_subseq" | "lis"
15398            | "knapsack_01" | "knapsack"
15399            | "subset_sum_target" | "subset_sum"
15400            | "coin_change_min" | "coin_change_minimum"
15401            | "edit_distance_levenshtein" | "edit_distance"
15402            // ── ML metrics ───────────────────────────────────────
15403            | "one_hot_encode" | "onehot" | "label_encode"
15404            | "categorical_cross_entropy" | "cce"
15405            | "classification_metrics" | "binary_metrics"
15406            | "roc_auc" | "auroc"
15407            // ── DSP / image filters ──────────────────────────────
15408            | "gaussian_blur_kernel" | "sobel_x" | "sobel_y"
15409            | "prewitt_x" | "prewitt_y"
15410            | "laplacian_of_gaussian" | "log_kernel"
15411            // ── stochastic processes ─────────────────────────────
15412            | "brownian_path" | "wiener_path"
15413            | "geometric_brownian_path" | "gbm_path"
15414            | "poisson_process" | "random_walk_1d"
15415            // ── compression / info ───────────────────────────────
15416            | "lempel_ziv_complexity" | "lz_complexity"
15417            | "huffman_code_lengths" | "huffman"
15418            | "shannon_entropy_rate" | "block_entropy_rate"
15419            // ── physics / quantum ────────────────────────────────
15420            | "planck_blackbody" | "blackbody"
15421            | "rayleigh_jeans" | "compton_shift"
15422            | "rydberg_energy"
15423            | "hydrogen_radial_wavefunction" | "h_rad_psi"
15424            // ── number theory / algebra ──────────────────────────
15425            | "integer_log" | "ilog"
15426            | "aks_primality" | "aks"
15427            | "elliptic_curve_add" | "ec_add"
15428            | "berlekamp_massey" | "bm_lfsr"
15429            | "bezout_coefficients" | "bezout" | "extended_euclid"
15430            // ── CAS-lite ─────────────────────────────────────────
15431            | "factor_quadratic" | "complete_square"
15432            | "partial_fraction_simple" | "partial_fraction"
15433            // ── more quadrature ──────────────────────────────────
15434            | "gauss_chebyshev_quad" | "gc_quad"
15435            | "gauss_hermite_quad" | "gh_quad"
15436            | "gauss_laguerre_quad" | "glag_quad"
15437            | "clenshaw_curtis_quad" | "cc_quad"
15438            | "tanh_sinh_quad" | "ts_quad"
15439            | "gauss_legendre_2d" | "gl_2d"
15440            | "monte_carlo_2d" | "mc_2d"
15441            // ── more optimization ────────────────────────────────
15442            | "simulated_annealing" | "sa_min"
15443            | "simplex_lp" | "lp_simplex"
15444            | "particle_swarm" | "pso_min"
15445            // ── distributions ────────────────────────────────────
15446            | "gev_pdf" | "gev_cdf" | "gev_sample" | "rgev"
15447            | "gen_pareto_pdf" | "gen_pareto_cdf"
15448            | "gen_pareto_sample" | "rgenpareto"
15449            | "skew_normal_pdf" | "skew_normal_cdf"
15450            | "mixture_normal_pdf"
15451            | "categorical_sample" | "rcat"
15452            | "multinomial_pmf" | "multinomial_sample" | "rmultinom"
15453            | "truncated_normal_pdf"
15454            | "truncated_normal_sample" | "rtnorm"
15455            // ── clustering ───────────────────────────────────────
15456            | "dbscan" | "gmm_em_1d" | "gmm_1d"
15457            | "silhouette_score"
15458            | "davies_bouldin_index" | "db_index"
15459            | "calinski_harabasz_index" | "ch_index"
15460            | "mds_2d" | "pcoa_2d" | "mean_shift"
15461            // ── NN primitives ────────────────────────────────────
15462            | "batch_norm" | "layer_norm"
15463            | "dropout_mask"
15464            | "max_pool_1d" | "avg_pool_1d"
15465            | "attention_softmax" | "positional_encoding"
15466            | "glorot_init" | "xavier_init"
15467            | "he_init" | "kaiming_init"
15468            | "adam_step" | "rmsprop_step"
15469            // ── time series ──────────────────────────────────────
15470            | "ewma" | "ccf" | "periodogram"
15471            | "welch_psd" | "welch"
15472            | "lag_features"
15473            // ── image processing ─────────────────────────────────
15474            | "median_filter_2d"
15475            | "threshold_otsu" | "otsu"
15476            | "histogram_equalize" | "hist_eq"
15477            | "erode_2d" | "dilate_2d"
15478            // ── losses ───────────────────────────────────────────
15479            | "mse_loss" | "mae_loss" | "huber_loss"
15480            // ── spatial ──────────────────────────────────────────
15481            | "vincenty_distance" | "vincenty"
15482            | "mercator_project"
15483            | "destination_from_bearing" | "dest_bearing"
15484            // ── integer sequences ────────────────────────────────
15485            | "recaman" | "recaman_seq"
15486            | "sylvester" | "sylvester_seq"
15487            | "happy_q" | "is_happy"
15488            | "amicable_pair_q"
15489            | "aliquot_sequence"
15490            | "magic_constant"
15491            // ── graph metrics ────────────────────────────────────
15492            | "clustering_coefficient_local" | "cc_local"
15493            | "clustering_coefficient_global" | "cc_global"
15494            | "assortativity" | "common_neighbors" | "jaccard_neighbors"
15495            | "adamic_adar"
15496            | "preferential_attachment_score" | "pa_score"
15497            // ── 3D geometry ──────────────────────────────────────
15498            | "triangle_3d_normal" | "triangle_3d_area"
15499            | "tetrahedron_volume"
15500            | "plane_from_3_points" | "plane_from_pts"
15501            | "point_to_plane_distance" | "pt_plane_dist"
15502            | "ray_triangle_intersect" | "moller_trumbore"
15503            | "ray_sphere_intersect" | "aabb_overlap"
15504            // ── iterative solvers ────────────────────────────────
15505            | "gauss_seidel"
15506            | "jacobi_iteration" | "jacobi_solve"
15507            | "sor_solve" | "sor"
15508            | "thomas_tridiag_solve" | "thomas"
15509            | "richardson_extrapolation" | "richardson"
15510            | "finite_difference_5pt" | "fd5pt"
15511            // ── crypto / algebra ─────────────────────────────────
15512            | "tonelli_shanks_sqrt" | "tonelli_shanks"
15513            | "baby_step_giant_step" | "bsgs"
15514            | "pollard_rho_factor" | "pollard_rho"
15515            | "modular_lcm" | "mlcm"
15516            | "crt_general" | "crt_arbitrary"
15517            // ── physics / chemistry ──────────────────────────────
15518            | "van_der_waals_p" | "vdw_pressure"
15519            | "nernst_equation" | "nernst"
15520            | "arrhenius_rate" | "arrhenius"
15521            | "reduced_mass"
15522            | "ph_to_concentration" | "ph_to_h"
15523            // ── MCMC / SDE / HMM ─────────────────────────────────
15524            | "metropolis_hastings" | "mh_sampler"
15525            | "gibbs_sampler_step" | "gibbs_step"
15526            | "euler_maruyama" | "em_sde"
15527            | "milstein" | "milstein_sde"
15528            | "ornstein_uhlenbeck_path" | "ou_path"
15529            | "hmm_forward" | "hmm_viterbi" | "hmm_backward"
15530            // ── survival / alignment ─────────────────────────────
15531            | "kaplan_meier" | "km_estimator" | "log_rank_test"
15532            | "needleman_wunsch" | "nw_align"
15533            | "smith_waterman" | "sw_align"
15534            // ── chemistry ────────────────────────────────────────
15535            | "gibbs_free_energy" | "delta_g"
15536            | "henderson_hasselbalch" | "hh_eq"
15537            | "radioactive_decay"
15538            | "half_life_to_constant" | "hl_to_lambda"
15539            // ── control theory ───────────────────────────────────
15540            | "pid_step"
15541            | "transfer_function_eval" | "tf_eval"
15542            | "bode_magnitude_db" | "bode_mag_db"
15543            | "bode_phase_deg"
15544            | "lqr_2x2"
15545            // ── game theory ──────────────────────────────────────
15546            | "nash_eq_2x2" | "nash_2x2"
15547            | "shapley_value" | "expected_utility"
15548            // ── operations research ──────────────────────────────
15549            | "hungarian_assignment" | "hungarian"
15550            | "tsp_nearest_neighbor" | "tsp_nn"
15551            | "vertex_cover_2approx" | "vc_2approx"
15552            // ── PDE ──────────────────────────────────────────────
15553            | "heat_eq_1d" | "wave_eq_1d"
15554            | "laplace_2d_jacobi" | "laplace_jacobi"
15555            // ── Bayesian conjugate ───────────────────────────────
15556            | "beta_binomial_update"
15557            | "normal_normal_update"
15558            | "gamma_poisson_update"
15559            | "dirichlet_multinomial_update"
15560            // ── quantum gates ────────────────────────────────────
15561            | "hadamard_gate" | "h_gate"
15562            | "cnot_gate" | "cx_gate"
15563            | "swap_gate" | "cz_gate"
15564            | "qft_matrix" | "phase_gate"
15565            | "s_gate" | "t_gate"
15566            // ── splines ──────────────────────────────────────────
15567            | "bezier_eval"
15568            | "catmull_rom_eval" | "cmr_eval"
15569            | "cubic_hermite_eval" | "ch_eval"
15570            | "bspline_basis" | "nik_basis"
15571            // ── music ────────────────────────────────────────────
15572            | "freq_to_midi" | "midi_to_freq"
15573            | "equal_temperament_freq"
15574            | "cents_difference" | "cents_diff"
15575            // ── astronomy ────────────────────────────────────────
15576            | "redshift_z" | "hubble_distance" | "luminosity_distance"
15577            // ── fluid dynamics ───────────────────────────────────
15578            | "reynolds_number" | "mach_number"
15579            | "prandtl_number" | "bernoulli_velocity"
15580            // ── distributions ────────────────────────────────────
15581            | "negative_binomial_pmf" | "nb_pmf"
15582            | "hypergeometric_pmf"
15583            | "beta_binomial_pmf" | "bb_pmf"
15584            | "von_mises_pdf" | "vmf_pdf"
15585            // ── random graphs ────────────────────────────────────
15586            | "erdos_renyi_random" | "erdos_renyi"
15587            | "barabasi_albert_random" | "barabasi_albert"
15588            | "watts_strogatz_random" | "watts_strogatz"
15589            // ── color science ────────────────────────────────────
15590            | "rgb_to_lab" | "lab_to_rgb"
15591            | "kelvin_to_rgb" | "color_temp_rgb"
15592            // ── integer sequences ────────────────────────────────
15593            | "bell_triangle" | "surjection_count"
15594            | "distinct_partition_count" | "q_partition"
15595            | "fibonacci_q" | "is_fib_number"
15596            // ── stats / divergences / distribs / physics / astro / chem ──
15597            | "bonferroni_correction" | "bonferroni"
15598            | "benjamini_hochberg" | "bh_fdr"
15599            | "tukey_hsd"
15600            | "hellinger_distance"
15601            | "wasserstein_1d" | "earth_movers_1d"
15602            | "chi_squared_divergence"
15603            | "beta_geometric_pmf"
15604            | "generalized_gamma_pdf" | "gengamma_pdf"
15605            | "zip_pmf" | "zero_inflated_poisson_pmf"
15606            | "stefan_boltzmann_luminosity" | "stellar_luminosity"
15607            | "photon_momentum" | "photon_energy_ev"
15608            | "dipole_radiation_power" | "larmor_power"
15609            | "parallax_to_distance" | "hawking_temperature"
15610            | "roche_limit" | "apparent_magnitude" | "distance_modulus"
15611            | "beer_lambert" | "absorbance"
15612            | "rate_law_n"
15613            | "freezing_point_depression" | "fpd"
15614            | "mixed_nash_2x2" | "minimax_2x2"
15615            // ── graphics / DSP / image / clustering / combinatorics / NT ─
15616            | "barycentric_coords_2d" | "barycentric_2d"
15617            | "bresenham_line" | "bilinear_interp_2d"
15618            | "point_in_polygon_2d"
15619            | "hilbert_transform" | "cepstrum"
15620            | "butterworth_lowpass_coeffs" | "butter_lp"
15621            | "savitzky_golay_coeffs" | "sg_coeffs"
15622            | "savitzky_golay_filter" | "sg_filter"
15623            | "canny_edge_intensity" | "canny_intensity"
15624            | "bilateral_filter_basic" | "bilateral_filter"
15625            | "kmeans_pp_init" | "kpp_init"
15626            | "elbow_score" | "wcss"
15627            | "young_tableaux_count" | "syt_count"
15628            | "euler_alt_permutation" | "euler_zigzag"
15629            | "genocchi_number" | "lattice_paths_count"
15630            | "tetration"
15631            | "ackermann_limited" | "ackermann"
15632            | "perfect_power_q" | "b_smooth_q"
15633            // ── networks / crypto / quantum / geom / TS ──────────
15634            | "k_core"
15635            | "rich_club_coefficient" | "rich_club"
15636            | "rsa_basic_encrypt" | "rsa_enc_int"
15637            | "rsa_basic_decrypt" | "rsa_dec_int"
15638            | "dh_shared_secret"
15639            | "bell_state_phi_plus" | "bell_phi_plus"
15640            | "bell_state_psi_minus" | "bell_psi_minus"
15641            | "density_matrix_purity" | "rho_purity"
15642            | "concurrence_2qubit"
15643            | "point_in_circle"
15644            | "circle_circle_intersect_2d"
15645            | "polygon_centroid"
15646            | "sutherland_hodgman_clip" | "sh_clip"
15647            | "kalman_rts_smoother" | "rts_smoother"
15648            // ── bioinformatics ───────────────────────────────────
15649            | "gc_content" | "codon_to_aa"
15650            | "reverse_complement_dna" | "rev_comp_dna"
15651            | "hamming_dna"
15652            | "blosum62_pair_score" | "blosum62"
15653            | "kmer_count"
15654            // ── geographic ───────────────────────────────────────
15655            | "great_circle_bearing" | "gc_bearing"
15656            | "midpoint_lat_lon" | "mid_geo"
15657            | "utm_zone_for"
15658            | "area_polygon_lat_lon" | "geo_polygon_area"
15659            // ── finance ──────────────────────────────────────────
15660            | "crr_binomial_option" | "crr_option"
15661            | "bond_price_clean"
15662            | "bond_yield_to_maturity" | "bond_ytm"
15663            | "modified_duration_bond"
15664            | "convexity_bond" | "bond_convexity"
15665            // ── image quality ────────────────────────────────────
15666            | "ssim" | "psnr" | "mssim"
15667            // ── acoustics ────────────────────────────────────────
15668            | "db_spl_from_pa" | "db_spl"
15669            | "a_weighting_factor" | "a_weight"
15670            | "octave_band_center" | "octave_center"
15671            | "semitone_ratio"
15672            // ── genetics ─────────────────────────────────────────
15673            | "hardy_weinberg"
15674            | "expected_heterozygosity" | "het_e"
15675            | "fst_simple"
15676            | "allele_frequencies"
15677            // ── epidemiology ─────────────────────────────────────
15678            | "sir_step" | "sir_r0" | "doubling_time"
15679            // ── economics ────────────────────────────────────────
15680            | "theil_index"
15681            | "herfindahl_hirschman" | "hhi"
15682            | "atkinson_index"
15683            | "lorenz_curve_points"
15684            // ── APL/J primitives ─────────────────────────────────
15685            | "iota_range" | "iota"
15686            | "reshape_array" | "reshape"
15687            | "grade_up" | "grade_asc"
15688            | "grade_down" | "grade_desc"
15689            // ── plasma physics ───────────────────────────────────
15690            | "plasma_frequency" | "omega_p"
15691            | "debye_length" | "lambda_d"
15692            | "cyclotron_frequency" | "omega_c"
15693            | "larmor_radius" | "gyroradius"
15694            // ── string similarity ────────────────────────────────
15695            | "jaro_winkler_similarity" | "jaro_winkler"
15696            | "metaphone_simple"
15697            // ── rating systems ───────────────────────────────────
15698            | "elo_rating_update" | "elo"
15699            | "glicko_rating_update" | "glicko"
15700            | "dice_sum_pmf"
15701            // ── effect sizes ─────────────────────────────────────
15702            | "cohens_d" | "effect_size_d"
15703            | "cliff_delta"
15704            | "vargha_delaney_a12" | "a12"
15705            // ── control transient ────────────────────────────────
15706            | "step_response_2nd_order" | "step_2nd"
15707            | "overshoot_2nd_order" | "overshoot_pct"
15708            // ── matrix norms ─────────────────────────────────────
15709            | "frobenius_norm"
15710            | "spectral_norm" | "operator_norm_2"
15711            | "trace_matrix" | "tr_mat"
15712            // ── networks ─────────────────────────────────────────
15713            | "homophily_index" | "homophily"
15714            | "dyad_census" | "triad_census"
15715            // ── misc ─────────────────────────────────────────────
15716            | "sigmoid_inverse" | "logit"
15717            // ── list / string / date / color / music / astro / perm / linguistics / regression / combinatorics / PRNG ──
15718            | "partition_at" | "drop_at" | "insert_at_idx"
15719            | "replace_at_index" | "set_at"
15720            | "swap_indices" | "nth_largest" | "nth_smallest"
15721            | "position_of_all_matching" | "positions_of_all"
15722            | "string_take_first" | "string_take_last"
15723            | "string_drop_first" | "string_drop_last"
15724            | "pluralize_simple"
15725            | "singularize_simple" | "singularize"
15726            | "capitalize_words" | "title_words"
15727            | "format_table_simple" | "ascii_table"
15728            | "days_between" | "weeks_between"
15729            | "months_between" | "years_between"
15730            | "first_of_month" | "last_of_month"
15731            | "day_of_week_iso" | "iso_dow"
15732            | "easter_sunday" | "chinese_zodiac"
15733            | "iso_week_number" | "iso_week"
15734            | "relative_luminance" | "wcag_luminance"
15735            | "contrast_ratio_wcag" | "wcag_contrast"
15736            | "delta_e_76" | "delta_e"
15737            | "color_blend_t" | "lerp_color"
15738            | "chord_to_freqs" | "scale_to_intervals"
15739            | "interval_semitones"
15740            | "transpose_freq_semitones" | "transpose_semi"
15741            | "bpm_to_period" | "midi_to_pitch_class"
15742            | "key_signature_for" | "circle_of_fifths_step"
15743            | "moon_phase" | "equation_of_time"
15744            | "solar_declination" | "sidereal_day_period" | "ecliptic_obliquity"
15745            | "permutation_order"
15746            | "permutation_parity" | "perm_sign"
15747            | "identity_permutation"
15748            | "permutation_compose" | "perm_mul"
15749            | "flesch_reading_ease" | "flesch_kincaid_grade"
15750            | "gunning_fog"
15751            | "automated_readability_index" | "ari"
15752            | "lix"
15753            | "adjusted_r_squared" | "adj_r2"
15754            | "aic" | "bic"
15755            | "residuals_compute" | "compute_residuals"
15756            | "composition_count" | "weak_composition_count"
15757            | "necklace_count" | "bracelet_count"
15758            | "multiset_permutations_count" | "multinomial_count"
15759            | "pearson_hash_byte" | "pearson_hash"
15760            | "xorshift32_step" | "lcg_next_u32"
15761            | "fisher_yates_shuffle"
15762            // ── ──────────────────────────────────────────────────
15763            | "tetrahedral_number" | "square_pyramidal_number"
15764            | "octahedral_number" | "pentagonal_pyramidal_number"
15765            | "cake_number" | "cuban_number" | "centered_hexagonal_number"
15766            | "carmichael_q" | "is_carmichael"
15767            | "sphenic_q" | "is_sphenic"
15768            | "seven_smooth_q" | "is_7_smooth"
15769            | "cartesian_product_n" | "cart_n"
15770            | "multiset_union" | "multiset_intersection" | "multiset_difference"
15771            | "polynomial_roots_dk" | "durand_kerner"
15772            | "lin_bairstow_step" | "bairstow"
15773            | "heap_sift_down"
15774            | "fenwick_build" | "bit_build"
15775            | "fenwick_query" | "bit_query"
15776            | "segment_tree_sum" | "seg_sum"
15777            | "kmp_failure" | "kmp"
15778            | "z_array" | "z_func"
15779            | "suffix_array_naive"
15780            | "manacher_radii" | "manacher"
15781            | "rabin_karp_hash" | "lcp_array"
15782            | "regex_escape_simple"
15783            | "horspool_search" | "bm_horspool"
15784            | "lpt_schedule" | "lpt"
15785            | "johnsons_rule" | "johnson_2m"
15786            | "bit_reverse_32" | "bit_reverse"
15787            | "bin_to_gray" | "gray_to_bin"
15788            | "swap_bits_pos" | "swap_bits"
15789            | "hamming_weight" | "popcnt"
15790            | "hamming_distance_int" | "hamdist_int"
15791            | "internal_rate_of_return"
15792            | "modified_irr" | "mirr"
15793            | "payback_period_simple" | "payback_simple"
15794            | "rfc3339_format" | "rfc3339"
15795            | "rfc3339_parse"
15796            | "iso_ordinal_date" | "ordinal_date"
15797            // ── ──────────────────────────────────────────────────
15798            | "lazy_caterer" | "central_polygonal"
15799            | "centered_square" | "centered_triangular" | "centered_pentagonal"
15800            | "star_number" | "dodecahedral_number" | "icosahedral_number"
15801            | "pronic_number" | "squared_triangular"
15802            | "woodall_number" | "cullen_number"
15803            | "repunit" | "repdigit" | "kaprekar_routine_step"
15804            | "smith_q"
15805            | "keith_q" | "is_keith"
15806            | "armstrong_q" | "is_armstrong"
15807            | "fnv1a_hash" | "djb2_hash"
15808            | "jenkins_one_at_a_time" | "jenkins_oat"
15809            | "murmurhash3_x32"
15810            | "adler32_hash" | "crc16_ccitt"
15811            | "vec_dot"
15812            | "l1_norm" | "l2_norm" | "vec_l2"
15813            | "linf_norm" | "max_norm" | "lp_norm"
15814            | "unit_vector"
15815            | "vector_project" | "proj" | "vector_reject"
15816            | "orthogonalize_vectors" | "gram_schmidt"
15817            | "outer_product" | "vec_outer"
15818            | "matrix_diagonal" | "mdiagvec"
15819            | "matrix_anti_diagonal"
15820            | "matrix_symmetric_q" | "matrix_orthogonal_q"
15821            | "geometric_mean_arr" | "harmonic_mean_arr"
15822            | "quadratic_mean_arr" | "lehmer_mean"
15823            | "running_mean" | "running_variance"
15824            | "outlier_iqr_q" | "z_score_robust"
15825            | "geometric_sequence" | "arithmetic_sequence"
15826            | "log_sum_exp" | "lse"
15827            | "log_sigmoid" | "log1p_exp"
15828            | "string_chars"
15829            | "string_words_count" | "word_count_simple"
15830            | "string_lines_count" | "line_count_simple"
15831            | "string_intersperse" | "string_replicate"
15832            | "string_uniq_chars" | "string_letter_frequency"
15833            | "anagram_q" | "is_anagram_q"
15834            | "string_take_while" | "string_drop_while"
15835            | "string_split_at_first" | "string_partition_at_word"
15836            // ── ──────────────────────────────────────────────────
15837 | "relativistic_kinetic"
15838            | "lorentz_factor_v" | "doppler_relativistic"
15839            | "drag_force_quadratic" | "terminal_velocity"
15840            | "carnot_efficiency" | "otto_efficiency"
15841            | "brayton_efficiency" | "diesel_efficiency"
15842            | "specific_heat_const_v" | "speed_of_sound_ideal"
15843            | "kepler_period_au" | "synodic_period"
15844            | "hill_radius" | "jeans_length"
15845            | "chandrasekhar_mass" | "eddington_luminosity"
15846            | "schwarzschild_radius_m" | "gravity_at_radius"
15847            | "gravitational_pe"
15848            | "freefall_time" | "pendulum_freq" | "spring_period"
15849            | "centripetal_accel" | "lens_focal_length"
15850            | "avogadros_number" | "boltzmann_const"
15851            | "planck_const_h" | "gas_constant_r"
15852            | "concentration_dilute" | "partial_pressure"
15853            | "mole_fraction" | "molarity" | "molality"
15854            | "normality_chem" | "ionic_strength"
15855 | "titration_volume"
15856            | "atomic_radius_pm" | "de_broglie_wavelength_kg"
15857 | "lotka_volterra_step"
15858            | "michaelis_menten" | "hill_equation"
15859            | "lineweaver_burk" | "eadie_hofstee_y"
15860            | "arrhenius_temp_q10"
15861            | "body_surface_area_dubois" | "bsa_dubois"
15862            | "bmr_harris_benedict_male" | "bmr_harris_benedict_female"
15863            | "max_heart_rate" | "target_heart_rate"
15864            | "vo2_max_estimate" | "pulse_pressure"
15865            | "mean_arterial_pressure" | "map_bp"
15866            | "dew_point_magnus" | "heat_index_celsius"
15867            | "wind_chill_celsius" | "pressure_altitude_m"
15868            | "density_altitude_m" | "saturation_vapor_pressure"
15869            | "humidex" | "utci_simple"
15870            | "resistance_parallel" | "r_parallel"
15871            | "resistance_series" | "r_series"
15872            | "capacitance_parallel" | "c_parallel"
15873            | "capacitance_series" | "c_series"
15874            | "inductance_parallel" | "l_parallel"
15875            | "inductance_series" | "l_series"
15876            | "voltage_divider" | "current_divider"
15877            | "lc_resonant" | "q_factor_rlc"
15878            | "skin_depth" | "wire_resistance"
15879            | "motor_torque" | "efficiency_ratio"
15880            | "dB_voltage" | "db_voltage"
15881            | "dB_power" | "db_power"
15882            // ── ──────────────────────────────────────────────────
15883            | "bfs_distances" | "dfs_preorder" | "connected_components"
15884            | "graph_is_tree" | "graph_density"
15885            | "graph_average_degree" | "graph_max_degree" | "graph_min_degree"
15886            | "graph_complement"
15887            | "in_degree_directed" | "out_degree_directed"
15888            | "graph_eccentricity_all" | "is_connected"
15889            | "articulation_points" | "bridges_edges"
15890            | "eulerian_path_q" | "hamiltonian_brute"
15891            | "string_to_charcodes" | "charcodes_to_string"
15892            | "string_xor"
15893            | "string_camel_to_snake" | "string_snake_to_camel"
15894            | "string_kebab_to_snake" | "string_snake_to_kebab"
15895            | "palindromic_q" | "substring_count"
15896            | "string_truncate_ellipsis" | "string_expand_tabs"
15897            | "string_normalize_spaces"
15898 | "days_in_year" | "quarter_of_year"
15899            | "zeller_day_of_week" | "age_from_birthdate"
15900            | "business_days_between" | "unix_epoch_to_iso"
15901            | "loan_payment_pmt" | "loan_balance"
15902            | "amortization_total_interest"
15903            | "apr_to_apy" | "apy_to_apr"
15904            | "compound_interest_periods" | "simple_interest_compute"
15905
15906            | "perpetuity_value" | "growing_perpetuity"
15907            | "annuity_present_value" | "annuity_future_value"
15908            | "capm_expected_return"
15909            | "treynor_ratio"
15910            | "jensens_alpha" | "information_ratio"
15911            | "friction_factor_laminar" | "swamee_jain_factor"
15912            | "pipe_pressure_drop" | "orifice_velocity"
15913            | "chezy_velocity" | "manning_velocity"
15914            | "froude_number" | "weber_number" | "grashof_number"
15915            | "nusselt_dittus_boelter"
15916            // ── more extensions ────────────────────────────────────────────
15917            | "mollweide_project" | "robinson_project" | "sinusoidal_project"
15918            | "equirectangular_project" | "lambert_azimuthal_project" | "albers_conic_project"
15919            | "geohash_encode" | "geohash_decode" | "geohash_neighbor" | "geohash_bbox"
15920            | "gabor_kernel" | "unsharp_mask_kernel" | "emboss_kernel"
15921            | "box_blur_kernel" | "motion_blur_kernel" | "sharpen_kernel"
15922            | "edge_detect_kernel" | "sobel_diagonal_kernel" | "haar_2d_step"
15923            | "db4_coeffs" | "db6_coeffs" | "sym4_coeffs" | "coif1_coeffs"
15924            | "aes_sbox_byte" | "aes_inv_sbox_byte"
15925            | "chacha20_qround" | "xtea_round" | "speck_round" | "simon_round"
15926            | "kepler_hyperbolic" | "hohmann_dv1" | "hohmann_dv2" | "hohmann_total"
15927            | "bielliptic_total" | "lambert_simple"
15928            | "horizon_distance" | "solar_zenith_angle" | "air_mass_kasten"
15929            | "solar_constant" | "julian_centuries_j2000"
15930            | "mean_solar_longitude" | "mean_solar_anomaly" | "lst_to_solar"
15931            | "ra_dec_to_az_alt" | "ecliptic_to_equatorial" | "equatorial_to_galactic"
15932            | "orbital_eccentricity" | "semi_major_axis"
15933            | "specific_orbital_energy" | "specific_angular_momentum"
15934            | "toffoli_gate" | "ccx_gate" | "fredkin_gate" | "cswap_gate"
15935            | "iswap_gate" | "sqrt_swap_gate"
15936            | "rx_gate" | "ry_gate" | "rz_gate"
15937            | "ghz_state_n" | "w_state_n"
15938            | "depolarizing_channel" | "dephasing_channel" | "amplitude_damping_channel"
15939            | "quantum_fidelity_pure" | "trace_distance"
15940            | "bell_inequality_chsh" | "pauli_decomposition_2x2"
15941            | "quantum_relative_entropy" | "qft_4_real"
15942            | "bwt_encode" | "bwt_decode" | "mtf_encode" | "mtf_decode"
15943
15944            | "lyndon_factorize" | "christoffel_word" | "sturmian_word"
15945            | "z_function_alt" | "period_of_string" | "borders_of_string"
15946            | "thue_morse_string" | "fibonacci_word"
15947            | "mann_kendall_tau" | "theil_sen_slope" | "hodges_lehmann"
15948            | "huber_m_estimator" | "winsorized_variance_arr"
15949            | "bowley_skewness" | "pearson_skewness_2"
15950            | "concordance_correlation" | "quantile_p"
15951            | "label_propagation_step" | "modularity_q"
15952            | "clique_count_3" | "local_efficiency" | "global_efficiency"
15953            | "diameter_unweighted"
15954            | "aitken_delta_squared" | "wynn_epsilon"
15955            | "shanks_transform" | "levin_t_transform"
15956            | "harmonic_seq_sum" | "alternating_seq_sum"
15957            // ── more extensions (2) ────────────────────────────────────────
15958            | "sparse_csr_build" | "sparse_csr_mul_vec" | "sparse_density"
15959            | "lower_triangular_q" | "upper_triangular_q"
15960            | "diagonal_dominance_q" | "matrix_zero_q" | "matrix_identity_q"
15961            | "matrix_random_uniform" | "matrix_random_normal"
15962            | "andrew_monotone_chain" | "polygon_area_signed"
15963            | "polygon_convex_q" | "iou_2d_axis_aligned" | "hausdorff_distance_2d"
15964            | "minkowski_sum_simple" | "circle_3_points"
15965            | "polygon_winding_number" | "segment_length"
15966            | "segments_parallel_q" | "segments_perpendicular_q"
15967            | "burr_xii_pdf" | "burr_xii_cdf" | "dagum_pdf" | "lomax_pdf"
15968            | "birnbaum_saunders_pdf" | "tukey_lambda_quantile"
15969            | "half_cauchy_pdf" | "half_logistic_pdf" | "reciprocal_pdf"
15970            | "levy_pdf" | "voigt_profile_simple"
15971            | "gompertz_pdf" | "inverse_weibull_pdf"
15972            | "log_gamma_simple" | "inverse_chi2_pdf"
15973            | "poly1305_block_step" | "x25519_field_mul" | "curve25519_mul_simple"
15974            | "secp256k1_y_recover" | "hmac_step_xor"
15975            | "pkcs7_pad" | "pkcs7_unpad" | "xor_byte_string"
15976 | "atbash_cipher"
15977            | "vigenere_encrypt" | "vigenere_decrypt" | "xor_brute_keylen"
15978            | "arima_diff" | "seasonal_diff"
15979            | "garch_step" | "egarch_step"
15980            | "realized_volatility" | "max_drawdown_arr"
15981            | "calmar_ratio" | "omega_ratio" | "kelly_criterion"
15982            | "var_historical" | "cvar_historical"
15983            | "graph_degree_distribution" | "graph_count_edges"
15984            | "graph_bipartite_match_simple" | "graph_count_triangles"
15985            | "graph_avg_clustering" | "graph_transitivity"
15986            | "graph_max_clique_brute" | "graph_independent_set_brute"
15987            | "graph_count_paths_length_k" | "graph_pagerank_simple"
15988            // ── integration / ODE / root finding / optimization ─
15989            | "boole_rule" | "boole_int"
15990            | "gauss_legendre_5" | "gl5"
15991            | "gauss_kronrod_15" | "gk15"
15992
15993            | "midpoint_rule"
15994            | "adams_bashforth_4" | "ab4"
15995            | "heun_method" | "rk45_cash_karp" | "rkck"
15996            | "milne_pc" | "milne"
15997            | "modified_midpoint_ode" | "modmidpoint"
15998            | "backward_euler" | "implicit_euler"
15999            | "crank_nicolson_ode" | "cn_ode"
16000            | "brent_root" | "brent" | "ridders_root" | "ridders"
16001            | "steffensen_root" | "steffensen" | "halley_root" | "halley"
16002            | "householder_root" | "muller_root" | "muller"
16003            | "regula_falsi" | "false_position"
16004            | "secant_root" | "secant"
16005            | "anderson_step" | "aberth_step" | "inverse_quad_interp"
16006            | "lm_step" | "gradient_descent_step"
16007 | "nesterov_step" | "adagrad_step"
16008            | "cg_beta_pr" | "cg_beta_fr" | "bfgs_h_update_1d"
16009            | "wolfe_strong_q" | "dogleg_step"
16010            | "nelder_mead_reflect" | "nelder_mead_expand" | "nelder_mead_contract"
16011            | "sa_accept_prob" | "sa_boltzmann_temp" | "sa_cauchy_temp"
16012            | "sa_geometric_temp" | "acceptance_target"
16013            // ── financial pricing models ────────────────────────
16014            | "bs_call" | "blackscholes_call" | "bs_put" | "blackscholes_put"
16015 | "bs_theta_call" | "bs_rho_call"
16016 | "bachelier_call" | "black76_call"
16017            | "crr_american_call" | "crr_american_put" | "jr_european_call"
16018            | "trinomial_call" | "heston_price_simple" | "sabr_implied_vol"
16019            | "merton_jump_call" | "asian_call_mc" | "barrier_up_out_call"
16020            | "digital_call" | "lookback_call"
16021            | "macaulay_duration" | "forward_rate"
16022            | "discount_continuous" | "ytm_newton"
16023            | "vasicek_bond" | "cir_bond" | "hull_white_drift"
16024            | "cds_upfront" | "black_karasinski_drift" | "quanto_adjustment"
16025            | "fx_forward" | "garman_kohlhagen_call" | "margrabe" | "stulz_min_call"
16026            | "sharpe_annualized"
16027            | "jensen_alpha" | "modified_sharpe"
16028            // ── chemistry ───────────────────────────────────────
16029            | "ph_from_h" | "poh_from_oh" | "pka_from_ka"
16030 | "henderson_base"
16031            | "arrhenius_k" | "eyring_k"
16032            | "first_order_concentration" | "first_order_half_life"
16033            | "second_order_concentration" | "second_order_half_life"
16034            | "zero_order_concentration"
16035
16036            | "ideal_gas_n" | "redlich_kwong_p"
16037            | "compressibility_z"
16038            | "kc_from_rates" | "kp_from_kc" | "reaction_quotient" | "rxn_q"
16039            | "le_chatelier_dir"
16040            | "dg_from_k" | "k_from_dg" | "vant_hoff" | "clausius_clapeyron" | "antoine_p"
16041 | "emf_from_half_cells" | "faraday_mass_deposited"
16042 | "transmittance" | "ksp_from_concs"
16043 | "debye_huckel"
16044            | "cp_monatomic_ideal" | "cv_monatomic_ideal"
16045            | "heat_capacity_q" | "calorimeter_dt" | "enthalpy_reaction"
16046            | "avogadro_count" | "moles_from_mass"
16047            | "dilution_v2" | "raoult_law" | "bp_elevation" | "fp_depression"
16048            | "osmotic_pressure" | "rydberg_lambda" | "bohr_radius_n"
16049            | "bohr_energy_ev" | "photon_energy_freq" | "photon_energy_lambda"
16050            | "de_broglie"
16051            // ── biology / ecology ───────────────────────────────
16052 | "logistic_growth_step" | "logistic_growth_analytic"
16053            | "gompertz_growth_step" | "allee_growth_step"
16054 | "growth_rate_from_ratio"
16055 | "seir_step" | "seird_step" | "sis_step"
16056            | "r0_basic" | "rt_effective" | "herd_immunity_threshold" | "generation_time"
16057 | "inverse_simpson"
16058            | "pielou_evenness" | "margalef_richness" | "menhinick_richness"
16059            | "berger_parker" | "sorensen_dice"
16060            | "rao_quadratic_entropy"
16061 | "selection_step" | "nei_genetic_distance"
16062            | "effective_pop_size" | "carrying_capacity_from_data"
16063            | "petersen_estimator" | "chapman_estimator"
16064            | "lv_competition_step"
16065            | "holling_type1" | "holling_type2" | "holling_type3"
16066            | "leslie_step" | "net_reproductive_rate" | "generation_time_demo"
16067            | "finite_rate_lambda" | "kleibers_law" | "bergmann_adjust"
16068            | "q10" | "species_area" | "intrinsic_growth_rate"
16069            | "macarthur_wilson_immigration" | "macarthur_wilson_extinction"
16070            | "island_equilibrium"
16071            // ── EM / optics / relativity ────────────────────────
16072 | "efield_point" | "epotential_point"
16073 | "capacitor_charge"
16074            | "ohm_voltage" | "power_vi" | "power_i2r"
16075
16076 | "capacitance_parallel_sum"
16077            | "bfield_wire" | "bfield_solenoid" | "lorentz_force_mag"
16078 | "faraday_emf"
16079 | "lc_frequency" | "lc_omega"
16080            | "rc_tau" | "rl_tau"
16081            | "poynting_magnitude" | "em_intensity" | "radiation_pressure"
16082            | "em_wavelength" | "em_frequency"
16083            | "snell_theta2"
16084            | "index_from_speed" | "fresnel_reflection_normal"
16085            | "fresnel_rs" | "fresnel_rp"
16086            | "lensmaker" | "thin_lens_v" | "mirror_equation_v"
16087            | "lens_magnification" | "diffraction_grating_angle"
16088            | "single_slit_min" | "rayleigh_resolution"
16089            | "lorentz_gamma"
16090            | "rel_momentum" | "rel_ke" | "rel_total_energy" | "rel_energy_pm"
16091            | "relativistic_doppler" | "rel_velocity_add"
16092
16093            | "wave_string_speed" | "sound_solid" | "sound_gas"
16094            | "doppler_classical" | "standing_wave_fundamental"
16095            | "open_pipe_harmonic" | "closed_pipe_harmonic"
16096            | "sound_db"
16097            | "alfven_speed"
16098            | "grav_time_dilation" | "grav_redshift"
16099            // ── graph algorithms ────────────────────────────────
16100            | "kosaraju_scc" | "bridges"
16101            | "max_flow_ek" | "min_cut_value" | "hopcroft_karp"
16102
16103 | "katz_centrality" | "hits_simple"
16104            | "pagerank_damped" | "cc_count" | "cc_labels"
16105            | "topological_sort_kahn" | "has_cycle_directed" | "has_cycle_undirected"
16106 | "diameter_bfs" | "radius_bfs"
16107            | "num_edges" | "k_coreness"
16108            | "greedy_coloring" | "chromatic_number_greedy"
16109            | "sum_degrees" | "avg_degree" | "max_degree"
16110            | "is_tree" | "girth"
16111            // ── signal processing ───────────────────────────────
16112            | "hamming_window" | "hann_window" | "blackman_window"
16113            | "blackman_harris_window" | "bartlett_window" | "welch_window"
16114            | "kaiser_window" | "tukey_window" | "gaussian_window"
16115            | "hilbert_envelope"
16116            | "biquad_step" | "biquad_lowpass_coeffs" | "biquad_highpass_coeffs"
16117            | "biquad_bandpass_coeffs" | "biquad_notch_coeffs" | "biquad_allpass_coeffs"
16118            | "biquad_peak_coeffs" | "biquad_lowshelf_coeffs" | "biquad_highshelf_coeffs"
16119            | "butterworth_prewarp" | "butterworth_order"
16120            | "fir_moving_average" | "fir_lowpass_design"
16121 | "spectrogram_simple"
16122            | "zero_pad" | "resample_nearest" | "resample_linear" | "quantize"
16123            | "mu_law_encode" | "mu_law_decode" | "a_law_encode" | "a_law_decode"
16124            | "chirp_linear"
16125            // ── cryptography deep ───────────────────────────────
16126            | "fnv1a_32" | "fnv1a_64" | "sdbm_hash"
16127            | "siphash24"
16128            | "pbkdf2_hmac_step" | "scrypt_round" | "bcrypt_cost_iters"
16129            | "argon2_block_mix" | "hkdf_expand_step"
16130            | "lfsr_galois_step" | "mt19937_temper" | "xorshift64" | "xorshift32"
16131            | "pcg32_step" | "lcg_numrec_step" | "splitmix64_step" | "wyhash_mix"
16132
16133            | "xor_cipher_byte"
16134            | "railfence_encrypt" | "beaufort" | "affine_encrypt" | "substitution_encrypt"
16135            | "letter_frequency" | "english_chi2" | "index_of_coincidence" | "kasiski_repeats"
16136            | "deterministic_prime" | "dh_shared" | "rsa_encrypt_simple"
16137            | "monobit_test" | "approximate_entropy"
16138            // ── ML extensions ───────────────────────────────────
16139            | "gini_impurity" | "entropy_bits" | "information_gain" | "gain_ratio"
16140            | "nb_gaussian_likelihood" | "nb_bernoulli_likelihood" | "nb_multinomial_log_likelihood"
16141            | "adaboost_alpha" | "hinge_loss" | "squared_hinge"
16142            | "logistic_loss"
16143 | "sigmoid_grad" | "tanh_grad"
16144 | "relu_grad"
16145 | "softsign" | "prelu" | "threshold_act"
16146            | "confusion_counts" | "mcc" | "f_beta" | "specificity"
16147            | "balanced_accuracy" | "cohen_kappa" | "brier_score" | "log_loss"
16148            | "tversky" | "mahalanobis_1d"
16149 | "one_hot" | "topk_indices"
16150            | "minmax_scale" | "zscore_norm" | "robust_scale"
16151            // ── geometry / topology ─────────────────────────────
16152            | "triangle_area_heron" | "triangle_area_pts"
16153            | "triangle_inradius" | "triangle_circumradius"
16154            | "regular_ngon_area" | "regular_ngon_inradius" | "regular_ngon_circumradius"
16155 | "n_ball_volume"
16156 | "cylinder_surface" | "cone_surface"
16157
16158            | "ellipsoid_volume" | "ellipsoid_surface_approx"
16159            | "dist_point_line_2d" | "dist_point_plane_3d" | "closest_pt_segment_2d"
16160            | "bbox_from_points"
16161 | "euclidean_distance_nd"
16162 | "hamming_distance_str"
16163 | "great_circle_law_of_cos"
16164            | "initial_bearing" | "midpoint_great_circle"
16165            | "shoelace_area" | "polygon_is_convex" | "convex_hull_jarvis"
16166            | "euler_characteristic" | "genus_from_euler"
16167            | "spherical_triangle_area" | "polygon_with_holes_area" | "picks_theorem"
16168            | "centroid_nd" | "covariance_matrix_pts" | "simplex_volume_3d"
16169            // ── special functions extra ─────────────────────────
16170            | "hyper2f1" | "hyper1f1" | "hyper0f1" | "pochhammer"
16171            | "mathieu_ce0" | "mathieu_se1" | "parabolic_d0" | "parabolic_d1"
16172            | "whittaker_m" | "struve_h0" | "struve_h1"
16173            | "lambert_w0" | "wright_omega"
16174            | "sinhc" | "cosh_minus1_over_x2"
16175            | "sine_integral_si" | "cosine_integral_ci" | "exp_integral_e1"
16176 | "dawson_function" | "owen_t"
16177            | "spherical_bessel_j0" | "spherical_bessel_j1"
16178            | "spherical_bessel_y0" | "spherical_bessel_y1"
16179            | "mod_sph_bessel_i0" | "mod_sph_bessel_i1" | "mod_sph_bessel_k0"
16180            | "coulomb_f0"
16181            | "polylog_li2" | "polylog_n"
16182
16183 | "ti2" | "clausen_cl2"
16184            | "bose_einstein_g" | "fermi_dirac_int"
16185            | "theta3" | "theta2"
16186            | "jacobi_sn_small_q" | "jacobi_cn_small_q" | "jacobi_dn_small_q"
16187            | "riemann_xi" | "bessel_jn_general" | "bessel_in_general"
16188            // ── astronomy / music / color / units ───────────────
16189 | "absolute_magnitude"
16190            | "pc_to_ly" | "ly_to_pc" | "pc_to_au" | "au_to_m"
16191            | "solar_mass_to_kg" | "solar_luminosity_to_w"
16192            | "hubble_distance_mpc" | "comoving_distance_approx" | "critical_density"
16193            | "et_freq_ratio" | "midi_to_hz" | "hz_to_midi" | "cents_between"
16194            | "just_intonation_ratio" | "pythagorean_ratio"
16195            | "beat_frequency" | "bpm_to_spb" | "note_name_to_midi"
16196 | "rgb_to_yiq" | "rgb_to_yuv601"
16197            | "srgb_to_xyz" | "xyz_to_lab" | "delta_e_94"
16198
16199
16200            | "feet_to_meters" | "meters_to_feet"
16201            | "lb_to_kg" | "kg_to_lb"
16202            | "mph_to_kmh" | "kmh_to_mph" | "mps_to_kmh" | "kmh_to_mps" | "knots_to_kmh"
16203 | "atm_to_pa" | "pa_to_atm" | "mmhg_to_pa"
16204            | "ev_to_joules" | "joules_to_ev" | "btu_to_joules" | "kwh_to_joules"
16205            | "bpm_to_midi_tick_us" | "iso226_phon_adjustment"
16206            | "db_to_amp" | "amp_to_db"
16207            | "roman_encode" | "roman_decode" | "number_to_english"
16208            // ── cosmology / GR / FLRW ───────────────────────────
16209            | "hubble_lcdm" | "hubble_time" | "hubble_distance_si" | "critical_density_si"
16210            | "comoving_distance" | "angular_diameter_distance"
16211            | "lookback_time" | "age_at_z" | "scale_factor" | "redshift_from_a"
16212            | "omega_m_at_z" | "lcdm_eos" | "cpl_w" | "deceleration_q"
16213            | "schwarzschild_radius_kg" | "kerr_ergosphere_eq" | "kerr_horizon"
16214 | "bh_entropy" | "bh_evaporation_time"
16215            | "schwarzschild_isco" | "photon_sphere_radius"
16216            | "tidal_force" | "grav_dilation_factor" | "lense_thirring_omega"
16217            | "gw_strain_amplitude" | "chirp_mass" | "grav_binding_energy"
16218            | "roche_limit_rigid" | "roche_limit_fluid"
16219            | "lagrange_l1" | "sphere_of_influence"
16220            | "freefall_velocity_schwarzschild" | "einstein_ring_radius"
16221            | "microlensing_magnification" | "cosmic_distance_modulus_si"
16222            | "cmb_temperature" | "cmb_temperature_at_z"
16223 | "stefan_boltzmann_si" | "planck_spectral_radiance"
16224            | "schwarzschild_g_tt" | "schwarzschild_g_rr" | "kretschmann_schwarzschild"
16225            | "hill_velocity" | "vacuum_energy_density"
16226            | "sound_horizon_recomb" | "bao_scale_today" | "sigma8_default"
16227            | "lensing_convergence" | "sigma_crit"
16228            | "perihelion_precession" | "shapiro_delay" | "light_deflection_angle"
16229 | "tov_mass_limit"
16230            | "main_sequence_lifetime" | "schwarzschild_freefall_time"
16231            | "friedmann_density_total" | "cosmological_constant"
16232
16233 | "planck_energy"
16234            // ── quantum mechanics deep ──────────────────────────
16235            | "pure_state_density" | "purity"
16236            | "linear_entropy" | "quantum_mutual_info"
16237 | "eof_from_concurrence"
16238            | "bell_state_index" | "chsh_expectation" | "tsirelson_bound"
16239            | "pauli_real_part" | "pauli_y_imag"
16240            | "bloch_to_density_real" | "bloch_purity_check"
16241            | "fidelity_pure_real" | "l1_coherence" | "relative_entropy_coherence"
16242            | "kraus_apply" | "bit_flip_prob" | "phase_flip_prob"
16243            | "depolarizing_density_2x2" | "amplitude_damping_excited"
16244            | "quantum_fisher_info" | "cramer_rao_bound" | "squeezing_db" | "heisenberg_min"
16245            | "coherent_mean_photons" | "thermal_mean_photons" | "poisson_photon_pmf"
16246            | "bose_einstein_pmf" | "mandel_q" | "g2_zero"
16247            | "free_particle_energy" | "infinite_well_energy" | "harmonic_oscillator_energy"
16248            | "hydrogen_energy_n" | "stark_shift_linear"
16249            | "zeeman_energy" | "larmor_frequency" | "rabi_frequency"
16250            | "schrodinger_step_real" | "probability_density" | "state_norm" | "state_normalize"
16251 | "quantum_variance" | "spin_casimir"
16252            | "cg_simple" | "wigner_3j_bound" | "qho_ground_state"
16253            | "tunneling_prob" | "gamow_factor" | "compton_wavelength" | "uncertainty_position"
16254            | "berry_phase_spin_half" | "zeno_survival" | "decoherence_time"
16255            | "ramsey_visibility" | "fermi_golden_rule"
16256            // ── bioinformatics deep ─────────────────────────────
16257            | "needleman_wunsch_score" | "smith_waterman_score" | "pam250_score"
16258            | "tanimoto_bits" | "translate_dna" | "transcribe_dna_rna" | "reverse_transcribe"
16259            | "at_content" | "tm_wallace" | "tm_marmur" | "codon_adaptation_index"
16260            | "kmer_jaccard" | "sequence_shannon_info" | "pwm_score"
16261            | "msa_column_entropy" | "seq_logo_information"
16262 | "damerau_levenshtein" | "lcs_length"
16263 | "hirschberg_lcs_length" | "common_kmers"
16264            | "jukes_cantor_distance" | "kimura_2p_distance" | "felsenstein_step"
16265            | "branch_length_substitutions" | "num_unrooted_trees" | "bayes_posterior"
16266            | "hw_expected_counts" | "allele_frequency" | "ld_d" | "ld_r_squared"
16267 | "heterozygosity" | "ne_from_variance"
16268            | "expected_coverage" | "lander_waterman_gaps"
16269            | "bh_adjusted_p" | "zscore_count"
16270 | "go_enrichment_p" | "blosum45_score"
16271            | "henikoff_weight" | "hamming_protein" | "codon_usage_variance"
16272            | "dnds_ratio" | "mutation_rate" | "tajimas_d" | "wattersons_theta"
16273            | "coalescent_expected_time" | "coalescent_tree_length" | "nm_from_fst"
16274            // ── ODE advanced ────────────────────────────────────
16275            | "bdf1_step" | "bdf2_step" | "bdf3_step" | "bdf4_step" | "bdf5_step" | "bdf6_step"
16276            | "ab1_step" | "ab2_step" | "ab3_step"
16277            | "am2_step" | "am3_step" | "am4_step"
16278            | "ros2_step" | "imex_euler_step" | "symplectic_euler_step"
16279            | "leapfrog_step" | "stormer_verlet_step"
16280            | "rk4_single" | "dopri5_combine" | "rkf45_error"
16281            | "lobatto_iiia_2" | "lobatto_iiic_3" | "gauss_irk_2_stage" | "magnus_1st"
16282            | "euler_lte" | "trapezoidal_lte" | "pi_step_size"
16283            | "stiffness_ratio" | "spectral_radius"
16284            | "heun_euler_step" | "bogacki_shampine_step" | "verner_8_combine"
16285            | "rk_combine" | "ab_coeff_sum"
16286            | "newmark_beta_step" | "wilson_theta_step"
16287            | "strang_split" | "lie_split"
16288            | "exp_euler_step" | "etd_rk2" | "dde_euler_step"
16289            | "em_step" | "milstein_step" | "heun_sde_step" | "stratonovich_correction"
16290            | "predictor_corrector" | "numerical_jacobian_col"
16291            | "cn_coefficient" | "imex_theta_split" | "bulirsch_stoer_step"
16292            | "cfl_number" | "diffusion_stability"
16293            | "lax_friedrichs_flux" | "lax_wendroff_flux"
16294            | "van_leer_limiter" | "minmod_limiter" | "superbee_limiter" | "mc_limiter"
16295            // ── cryptanalysis & number theory deep ──────────────
16296            | "pollard_p_minus_1" | "fermat_factor"
16297            | "trial_smallest_factor" | "bsgs_discrete_log"
16298            | "mertens" | "liouville"
16299            | "is_b_smooth" | "primorial_n"
16300            | "pseudoprime_base2" | "strong_pseudoprime"
16301            | "aks_witness_count" | "qs_relation"
16302            | "index_calculus_naive" | "lll_2x2_step" | "coppersmith_bound"
16303            | "shor_period_prob" | "rsa_d_from_e" | "dh_secret"
16304            | "elgamal_encrypt" | "ecc_point_double" | "continued_fraction_sqrt"
16305            | "pell_fundamental" | "sum_two_squares" | "class_number_bound"
16306            | "smith_normal_2x2_step" | "regulator_naive"
16307            | "power_residue_check" | "wieferich_check" | "wilson_test"
16308            | "goldbach_pair" | "english_likeness" | "xor_break_singlebyte"
16309            | "bit_reverse_64"
16310            | "gf256_multiply" | "hash_combine"
16311            // ── econometrics ────────────────────────────────────
16312            | "arch_lm_test" | "breusch_pagan_test" | "white_robust_se"
16313            | "newey_west_se" | "hansen_j_test" | "gmm_moment_condition"
16314            | "hausman_test" | "breusch_godfrey_test" | "box_pierce_test"
16315            | "adf_test_stat" | "pp_test_stat" | "kpss_test_stat"
16316            | "dickey_fuller_critical" | "engle_granger_step"
16317            | "johansen_trace_step" | "vecm_alpha_beta"
16318            | "panel_within_estimator" | "panel_between_estimator"
16319            | "panel_random_effects" | "arellano_bond_step"
16320            | "ols_estimator" | "ols_residual_variance" | "ols_r_squared"
16321            | "ols_adjusted_r2" | "akaike_info_crit" | "bayesian_info_crit"
16322            | "hannan_quinn_ic" | "f_statistic_pooled" | "breusch_pagan_lm"
16323            | "ramsey_reset_test" | "chow_test_stat" | "white_test_stat"
16324            | "goldfeld_quandt" | "wald_test_stat" | "score_test_stat"
16325            | "likelihood_ratio_test" | "two_sls_iv" | "iv_estimator"
16326            | "mle_normal_log_lik" | "mle_exponential_log_lik"
16327            | "mle_poisson_log_lik" | "gmm_moment_function"
16328            | "pooling_test_stat" | "heteroskedasticity_test"
16329            | "robust_se_huber_white" | "bootstrap_se_estimate"
16330            | "heckman_correction" | "tobit_log_likelihood"
16331            | "probit_log_likelihood" | "logit_log_likelihood"
16332            | "multinomial_logit_prob" | "ordered_probit_threshold"
16333            | "panel_var_step" | "impulse_response_step"
16334            | "variance_decomposition" | "granger_causality_chi2"
16335            | "cointegration_residual" | "error_correction_step"
16336            | "random_walk_innovation" | "random_walk_drift_step"
16337            | "ar_model_likelihood" | "ma_model_likelihood"
16338            | "arma_model_innovation"
16339            // ── algebraic topology, knot theory, lie algebras ───
16340            | "euler_char_complex" | "betti_zero" | "betti_one" | "betti_two"
16341            | "genus_surface" | "chern_first_2d" | "genus_curve_arith"
16342            | "genus_curve_geo" | "hodge_diamond_value" | "poincare_duality"
16343            | "fundamental_group_zn" | "homology_rank" | "cohomology_rank"
16344            | "homotopy_group_sphere_pi" | "mapping_class_torus"
16345            | "linking_number_two" | "writhe_polygon" | "torsion_coefficient"
16346            | "simplex_volume_n" | "simplicial_volume" | "nerve_complex_count"
16347            | "cech_zero_cohomology" | "de_rham_zero"
16348            | "poincare_polynomial_eval" | "chromatic_homology_rank"
16349            | "khovanov_q_grading" | "hochschild_zero" | "cyclic_homology_step"
16350            | "group_cohomology_dim" | "group_homology_dim"
16351            | "abelianization_quotient" | "free_group_rank_lower"
16352            | "nilpotency_class_lower" | "solvable_length_upper"
16353            | "schreier_index" | "todd_genus_eval" | "hirzebruch_signature"
16354            | "chern_simons_action" | "gauss_bonnet_total"
16355            | "seifert_genus_lower" | "alexander_polynomial_at_one"
16356            | "jones_polynomial_at_minus_one" | "jones_polynomial_at_i"
16357            | "homfly_evaluation" | "kauffman_bracket_eval"
16358            | "cabling_pair_signature" | "seifert_form_2x2"
16359            | "turaev_alexander_step" | "v_polynomial_eval"
16360            | "polynomial_jones_skein" | "delta_complex_count"
16361            | "poset_zeta_two" | "mobius_poset_two" | "mobius_function_pair"
16362            | "mobius_inversion_step" | "incidence_algebra_dim"
16363            | "quiver_path_count" | "representation_dim_step"
16364            | "weyl_group_order" | "root_system_count"
16365            | "cartan_determinant_a2" | "cartan_matrix_b2"
16366            | "killing_form_su2" | "casimir_eigenvalue_su2"
16367            | "universal_enveloping_dim" | "verma_character_step"
16368            | "plethystic_substitution_value" | "schur_polynomial_eval"
16369            | "hall_inner_product_two" | "plactic_class_size"
16370            | "robinson_schensted_pair" | "yamanouchi_word_count"
16371            | "rsk_size" | "character_su2" | "character_sun"
16372            | "quantum_dimension_su2" | "quantum_dimension_q"
16373            | "fusion_rule_su2_step" | "modular_data_s_value"
16374            | "modular_data_t_value" | "verlinde_count_step"
16375            | "quantum_invariant_eval" | "operad_count_two"
16376            | "moduli_dimension_curves" | "hodge_polynomial_eval"
16377            | "mirror_symmetry_check" | "gromov_witten_invariant"
16378            | "donaldson_invariant" | "seiberg_witten_value"
16379            | "floer_homology_rank" | "khovanov_rasmussen_s"
16380            | "ozsvath_szabo_tau" | "heegaard_genus_lower"
16381            | "fintushel_stern_step" | "bauer_furuta_step"
16382            | "geometric_intersection_number"
16383            | "algebraic_intersection_number"
16384            // ── electrochemistry, batteries, fuel cells ─────────
16385            | "nernst_potential_full" | "electrode_potential_step"
16386            | "exchange_current_density" | "butler_volmer_current"
16387            | "tafel_anodic_current" | "tafel_cathodic_current"
16388            | "mass_transport_overpotential" | "limiting_current_density"
16389            | "diffusion_layer_thickness" | "faradaic_efficiency"
16390            | "coulombic_efficiency_cell" | "energy_efficiency_cell"
16391            | "voltaic_efficiency" | "charge_capacity_battery"
16392            | "energy_density_battery" | "power_density_battery"
16393            | "specific_capacity_active" | "columbic_capacity_lihalfcell"
16394            | "ragone_point" | "peukert_capacity" | "peukert_exponent_fit"
16395            | "shepherd_voltage_step" | "nernst_planck_flux"
16396            | "debye_length_electrolyte" | "debye_huckel_activity"
16397            | "gouy_chapman_potential" | "stern_layer_capacitance"
16398            | "double_layer_capacitance" | "helmholtz_capacitance"
16399            | "zeta_potential_estimate" | "electroosmotic_velocity"
16400            | "hagen_poiseuille_eo" | "diffuse_layer_thickness"
16401            | "poisson_boltzmann_step" | "linearized_pb_step"
16402            | "electrochem_impedance_z" | "randles_circuit_z"
16403            | "warburg_impedance" | "cole_cole_eis" | "nyquist_phase"
16404            | "charge_transfer_resistance" | "solution_resistance_estimate"
16405            | "ionic_conductivity_arrhenius" | "nernst_einstein_diffusivity"
16406            | "walden_product" | "kohlrausch_law"
16407            | "onsager_relation_two_species" | "trasatti_voltammetry_charge"
16408            | "randles_sevcik_peak" | "levich_current_rde"
16409            | "koutecky_levich_intercept" | "mott_schottky_capacitance"
16410            | "flat_band_potential" | "schottky_barrier_height"
16411            | "photocurrent_density" | "quantum_efficiency_photo"
16412            | "overall_efficiency_pec" | "fuel_cell_polarization"
16413            | "electrolyzer_voltage" | "faraday_efficiency_h2"
16414            | "overpotential_oer" | "overpotential_her"
16415            | "electrocrystallization_step" | "nucleation_rate_constant"
16416            | "metal_corrosion_rate" | "pourbaix_line_value"
16417            | "mixed_potential_step" | "electrochemiluminescence_yield"
16418            | "solid_electrolyte_capacity" | "ionic_liquid_viscosity_step"
16419            | "lithium_ion_diffusivity" | "soc_estimate_coulomb"
16420            | "soh_capacity_fade" | "ocv_lithium_ion_step"
16421            | "state_of_charge_kalman" | "thermal_runaway_threshold"
16422            | "joule_heating_battery" | "calorimetric_heat_battery"
16423            | "abuse_test_voltage" | "swelling_strain_step"
16424            | "sei_resistance_growth" | "binder_content_optimal"
16425            | "porosity_active_layer" | "tortuosity_estimate_bruggeman"
16426            | "electrolyte_decomposition_temp" | "gibbs_thomson_undercooling"
16427            | "nernst_diffusion_layer" | "diff_coeff_aqueous_estimate"
16428            | "salt_activity_coefficient" | "mean_activity_coeff_pitzer"
16429            | "osmotic_coefficient_pitzer" | "debye_huckel_screening_factor"
16430            | "ph_at_isoelectric" | "buffer_capacity_acid_base"
16431            | "henderson_hasselbalch_solve" | "titration_endpoint_index"
16432            // ── tensor calculus, GR, differential geometry ──────
16433            | "tensor_contract_two" | "tensor_outer_two" | "tensor_trace_index"
16434            | "tensor_symmetrize_two" | "tensor_antisymmetrize_two"
16435            | "levi_civita_three" | "levi_civita_four"
16436            | "kronecker_three" | "kronecker_four"
16437            | "metric_minkowski_eta_step" | "metric_schwarzschild_step"
16438            | "metric_kerr_step_simple" | "metric_frw_lapse"
16439            | "christoffel_first_kind_step" | "christoffel_second_kind_step"
16440            | "riemann_tensor_step_zero" | "riemann_curvature_normal_form"
16441            | "ricci_tensor_step_zero" | "scalar_curvature_step"
16442            | "einstein_tensor_step" | "weyl_tensor_step_zero"
16443            | "schouten_tensor_step" | "geodesic_equation_step_zero"
16444            | "parallel_transport_step" | "covariant_derivative_step"
16445            | "christoffel_symbol_normalize" | "ricci_identity_step"
16446            | "bianchi_first_identity_check" | "bianchi_second_identity_check"
16447            | "killing_vector_lie_step" | "lie_derivative_scalar_step"
16448            | "lie_derivative_vector_step" | "exterior_derivative_one_form"
16449            | "hodge_star_one_form" | "codifferential_step"
16450            | "laplace_de_rham_step" | "volume_form_riemannian"
16451            | "hodge_inner_product_one" | "sectional_curvature_two_plane"
16452            | "gauss_codazzi_step" | "mainardi_codazzi_step"
16453            | "weingarten_map_step" | "shape_operator_eig"
16454            | "mean_curvature_step" | "gaussian_curvature_step"
16455            | "extrinsic_principal_curv" | "intrinsic_principal_curv"
16456            | "geodesic_curvature_step" | "darboux_frame_step"
16457            | "fermi_normal_step" | "synge_world_function"
16458            | "raychaudhuri_step" | "expansion_scalar_step"
16459            | "shear_tensor_step" | "twist_tensor_step"
16460            | "optical_scalars_step" | "peeling_step_psi4"
16461            | "ads_metric_step" | "de_sitter_metric_step"
16462            | "warped_product_step_zero" | "kaluza_klein_step"
16463            | "brans_dicke_step" | "horndeski_step"
16464            | "einstein_dilaton_step" | "gauss_bonnet_term_2d"
16465            | "chern_pontryagin_4d_step" | "adm_mass_step"
16466            | "komar_mass_step" | "bondi_mass_step"
16467            | "brown_york_quasilocal" | "isolated_horizon_charge"
16468            | "trapped_surface_check" | "apparent_horizon_step"
16469            | "event_horizon_check" | "cosmological_constant_term"
16470            | "de_sitter_radius_step" | "anti_de_sitter_radius_step"
16471            | "penrose_diagram_factor" | "conformal_compactification_step"
16472            | "schwarzschild_kruskal_step" | "gullstrand_painleve_step"
16473            | "kerr_newman_charge_term" | "boyer_lindquist_step"
16474            | "hartle_thorne_metric" | "oppenheimer_volkoff_step"
16475            | "post_newtonian_step" | "shapiro_delay_step"
16476            | "mercury_perihelion_advance"
16477            | "gravitational_wave_quadrupole"
16478            | "plus_polarization_amp" | "cross_polarization_amp"
16479            | "chirp_mass_inspiral_step" | "isco_radius_kerr_step"
16480            | "spin_orbit_coupling_term" | "spin_spin_coupling_term"
16481            | "hawking_area_increase" | "unruh_temperature_full"
16482            | "bekenstein_entropy_step" | "holographic_entanglement_step"
16483            | "ryu_takayanagi_step" | "swampland_distance_check"
16484            // ── information theory, coding, signal processing ──
16485            | "conditional_entropy_step" | "joint_entropy_step"
16486            | "relative_entropy_kl" | "mutual_information_step"
16487            | "chain_rule_entropy" | "fano_inequality_bound"
16488            | "data_processing_inequality" | "arithmetic_coding_interval"
16489            | "range_coding_step" | "golomb_rice_code"
16490            | "elias_gamma_code" | "elias_delta_code" | "exp_golomb_code"
16491            | "fibonacci_code" | "shannon_fano_elias_code"
16492            | "huffman_balanced_step" | "arithmetic_decode_interval"
16493            | "range_decode_step" | "universal_code_length"
16494            | "ziv_lempel_estimate" | "lz77_match_length"
16495            | "lz78_dictionary_growth" | "lzw_step_dict"
16496            | "ppm_predict_prob" | "deflate_huffman_lit"
16497            | "brotli_distance_code_count" | "zstd_window_size_log"
16498            | "mpeg_quant_value" | "jpeg_zig_zag_index"
16499            | "jpeg_dct_8x8_quant" | "hadamard_walsh_transform_step"
16500            | "karhunen_loeve_step" | "discrete_haar_step"
16501            | "db4_wavelet_step" | "biorthogonal_step"
16502            | "beylkin_wavelet_step" | "coiflet_wavelet_step"
16503            | "mallat_pyramid_step" | "threshold_soft_value"
16504            | "threshold_hard_value" | "median_filter_window"
16505            | "mean_filter_window" | "gaussian_filter_window"
16506            | "unsharp_mask_step" | "sobel_kernel_value"
16507            | "prewitt_kernel_value" | "roberts_kernel_value"
16508            | "laplacian_kernel_value" | "canny_threshold_step"
16509            | "hough_accumulator_step" | "ransac_iteration_count"
16510            | "optical_flow_lk_step" | "horn_schunck_step"
16511            | "kalman_predict_state" | "kalman_update_state"
16512            | "particle_filter_resample" | "unscented_sigma_point"
16513            | "ekf_jacobian_step" | "markov_decision_value"
16514            | "bellman_equation_step" | "q_learning_update"
16515            | "policy_iteration_step" | "value_iteration_step"
16516            | "sarsa_update" | "double_q_learning_step"
16517            | "ucb1_action_value" | "thompson_sample_beta"
16518            | "boltzmann_softmax_action" | "explore_exploit_epsilon"
16519            | "montecarlo_returns_step" | "td_zero_update"
16520            | "td_lambda_update" | "gradient_temporal_diff"
16521            | "deep_q_target" | "ddpg_critic_loss_step"
16522            | "ppo_clip_term" | "trpo_kl_constraint"
16523            | "a3c_advantage_step" | "ppo_advantage_step"
16524            | "gae_advantage_step" | "generalized_advantage"
16525            | "information_bottleneck_step" | "free_energy_principle"
16526            | "fisher_info_metric" | "kullback_jensen_div"
16527            | "hellinger_distance_step" | "total_variation_distance"
16528            | "bhattacharyya_coefficient" | "wasserstein_dist_emp"
16529            | "chisquare_metric" | "hellinger_kernel"
16530            | "jensen_shannon_div" | "renyi_divergence_step"
16531            | "amari_alpha_div" | "csiszar_phi_div"
16532            | "sinkhorn_iteration_step" | "sliced_wasserstein"
16533            | "gromov_wasserstein_step" | "spectral_signature_match"
16534            | "mfcc_coeff_step" | "chroma_feature_step"
16535            // ── combinatorial optimization, scheduling ──────────
16536            | "tsp_lower_bound_mst" | "tsp_held_karp_step"
16537            | "christofides_ratio_bound" | "two_opt_swap_delta"
16538            | "or_opt_delta" | "three_opt_delta" | "lin_kernighan_step"
16539            | "nearest_neighbor_tour_step" | "greedy_edge_tour"
16540            | "nearest_insertion_step" | "farthest_insertion_step"
16541            | "cheapest_insertion_step" | "max_flow_ford_fulkerson_step"
16542            | "edmonds_karp_step" | "dinic_blocking_flow"
16543            | "push_relabel_step" | "boykov_kolmogorov_step"
16544            | "mincut_stoer_wagner" | "gomory_hu_step"
16545            | "karger_contract_edge" | "karger_min_cut_count"
16546            | "maximum_bipartite_matching" | "hopcroft_karp_phase"
16547            | "blossom_match_step" | "weighted_match_kuhn_step"
16548            | "hungarian_method_step" | "ap_jonker_volgenant_step"
16549            | "assignment_lower_bound" | "job_shop_makespan_lower"
16550            | "flow_shop_johnson_step" | "parallel_machine_lpt"
16551            | "parallel_machine_spt" | "list_scheduling_step"
16552            | "graham_2approx_bound" | "chc_bound_makespan"
16553            | "bin_packing_first_fit" | "bin_packing_best_fit"
16554            | "bin_packing_next_fit" | "bin_packing_lower_bound_l1"
16555            | "multidim_packing_step" | "knapsack_01_dp_value"
16556            | "knapsack_unbounded_dp" | "knapsack_fractional_step"
16557            | "knapsack_branch_bound" | "knapsack_lp_relaxation"
16558            | "multi_knapsack_step" | "quadratic_assignment_step"
16559            | "qap_lower_bound" | "graph_coloring_dsatur_step"
16560            | "graph_coloring_welsh_powell"
16561            | "graph_coloring_brooks_bound" | "graph_coloring_lp_bound"
16562            | "fractional_chromatic_lower" | "list_coloring_step"
16563            | "edge_coloring_vizing_step" | "clique_number_lower"
16564            | "independence_number_upper" | "vertex_cover_lp_round"
16565            | "dominating_set_greedy_step" | "dominating_set_lp_bound"
16566            | "set_cover_greedy_step" | "set_cover_lp_round"
16567            | "hitting_set_greedy" | "weighted_set_cover_step"
16568            | "matroid_greedy_step" | "matroid_intersection_step"
16569            | "submodular_greedy_step" | "submodular_curvature_bound"
16570            | "nemhauser_wolsey_bound" | "lp_relax_round"
16571            | "branch_and_bound_step" | "cutting_plane_step"
16572            | "gomory_cut_step" | "chvatal_gomory_cut"
16573            | "mixed_integer_round_up" | "mixed_integer_round_down"
16574            | "sos_constraint_check" | "column_generation_step"
16575            | "benders_decomposition_step" | "dantzig_wolfe_step"
16576            | "lagrangian_relax_step" | "lagrangian_dual_step"
16577            | "subgradient_step_size" | "nonlinear_dual_step"
16578            | "augmented_lagrangian_step" | "admm_primal_step"
16579            | "admm_dual_step" | "proximal_gradient_step"
16580            | "nesterov_accelerate_step" | "fista_step" | "ista_step"
16581            | "mirror_descent_step" | "frank_wolfe_step"
16582            | "conditional_gradient_step" | "greedy_set_cover_round"
16583            | "local_search_swap_step" | "tabu_search_move_score"
16584            | "simulated_annealing_step" | "genetic_crossover_one_point"
16585            | "mutation_bit_flip_prob" | "roulette_wheel_select_index"
16586            // ── climate, fluids, atmospheric ────────────────────
16587            | "stefan_boltzmann_radiation" | "emissivity_grey_body"
16588            | "albedo_blackbody_balance" | "solar_constant_at_distance"
16589            | "total_solar_irradiance_step" | "absorbed_short_wave"
16590            | "emitted_long_wave" | "clausius_clapeyron_full"
16591            | "relative_humidity_step" | "dewpoint_temperature_full"
16592            | "wet_bulb_potential" | "virtual_temperature_full"
16593            | "density_altitude_full" | "geopotential_height_full"
16594            | "geometric_height_full" | "adiabatic_lapse_rate_dry"
16595            | "adiabatic_lapse_rate_moist" | "brunt_vaisala_full"
16596            | "richardson_number_step" | "gradient_richardson_full"
16597            | "flux_richardson_full" | "turbulent_kinetic_energy_step"
16598            | "mixing_length_prandtl" | "monin_obukhov_length"
16599            | "similarity_function_phi" | "log_law_wind_profile"
16600            | "power_law_wind_profile" | "ekman_layer_depth"
16601            | "ekman_pumping_step" | "geostrophic_wind_step"
16602            | "gradient_wind_step" | "thermal_wind_step"
16603            | "quasi_geostrophic_omega" | "omega_equation_step"
16604            | "potential_temperature_step" | "equivalent_potential_temp"
16605            | "saturation_equivalent_pt" | "ipv_potential_vorticity"
16606            | "ertel_pv_step" | "absolute_vorticity_step"
16607            | "relative_vorticity_step" | "divergence_omega_step"
16608            | "streamfunction_step" | "velocity_potential_step"
16609            | "helmholtz_decomp_step" | "courant_friedrichs_lewy"
16610            | "peclet_number_step" | "prandtl_number_step"
16611            | "reynolds_full_number" | "schmidt_number_step"
16612            | "sherwood_number_step" | "nusselt_full_number"
16613            | "grashof_number_step" | "rayleigh_number_step"
16614            | "weber_number_step" | "froude_number_step"
16615            | "strouhal_full" | "mach_full_step"
16616            | "biot_number_step" | "fourier_number_step"
16617            | "turbulence_intensity_step" | "hurst_exponent_estimate"
16618            | "detrended_fluct_alpha" | "power_spectrum_slope"
16619            | "spectral_kappa_minus53" | "batchelor_scale_step"
16620            | "kolmogorov_microscale" | "taylor_microscale_step"
16621            | "integral_length_scale" | "turbulent_dissipation_eps"
16622            | "isotropic_relation_check" | "sst_anomaly_step"
16623            | "enso_index_step" | "amo_index_step" | "nao_index_step"
16624            | "soi_oscillation_index" | "pdo_index_step" | "mjo_phase_step"
16625            | "walker_circulation_step" | "hadley_cell_max_lat"
16626            | "ferrel_cell_step" | "itcz_position_lat" | "trade_wind_speed"
16627            | "westerlies_jet_speed" | "polar_vortex_radius"
16628            | "arctic_oscillation_step" | "indian_monsoon_index"
16629            | "african_monsoon_index" | "qbo_oscillation_step"
16630            | "solar_cycle_phase" | "sunspot_relative_number"
16631            | "geomagnetic_kp_index" | "ozone_dobson_total"
16632            | "chlorine_radical_decay" | "montreal_protocol_track"
16633            | "co2_growth_rate_step" | "methane_growth_rate"
16634            | "aerosol_optical_depth" | "ice_age_milankovitch"
16635            | "greenhouse_forcing_step"
16636            // ── game theory, mechanism design, social choice ────
16637            | "game_two_player_value" | "nash_equilibrium_pair"
16638            | "mixed_strategy_value" | "zero_sum_minmax"
16639            | "saddle_point_check" | "correlated_equilibrium_value"
16640            | "shapley_value_two_step" | "banzhaf_index_two"
16641            | "nucleolus_lp_step" | "core_membership_check"
16642            | "imputation_efficient_check" | "imputation_individual_rational"
16643            | "prisoners_dilemma_payoff" | "matching_pennies_payoff"
16644            | "chicken_game_payoff" | "stag_hunt_payoff"
16645            | "battle_sexes_payoff" | "public_goods_game_payoff"
16646            | "tragedy_commons_metric" | "ultimatum_acceptance_prob"
16647            | "dictator_game_share" | "trust_game_repayment"
16648            | "cooperative_game_value" | "characteristic_function"
16649            | "bargaining_set_check" | "kalai_smorodinsky_step"
16650            | "nash_bargaining_solution" | "egalitarian_solution"
16651            | "utilitarian_solution" | "social_welfare_sum"
16652            | "arrow_impossibility_check" | "gibbard_satterthwaite_check"
16653            | "borda_count_step" | "condorcet_winner_check"
16654            | "plurality_winner_step" | "kemeny_score_step"
16655            | "dodgson_swap_count" | "coombs_runoff_step"
16656            | "single_transferable_vote" | "range_voting_score"
16657            | "approval_voting_max" | "schulze_method_step"
16658            | "copeland_score_step" | "black_method_winner"
16659            | "median_voter_step" | "hotelling_location_step"
16660            | "arrow_pareto_check" | "fair_division_envy_free"
16661            | "proportional_share" | "maximin_share"
16662            | "egalitarian_split" | "nash_social_welfare"
16663            | "divisible_goods_proportional" | "indivisible_envy_free_check"
16664            | "adjusted_winner_pct" | "sealed_bid_first_price"
16665            | "sealed_bid_second_price" | "english_auction_step"
16666            | "dutch_auction_step" | "all_pay_auction_step"
16667            | "vcg_payment_step" | "revenue_equivalence_check"
16668            | "truthful_mechanism_check" | "incentive_compatibility_check"
16669            | "mechanism_design_obj" | "double_auction_step"
16670            | "combinatorial_auction_step" | "posted_price_offer_accept"
16671            | "matching_market_step" | "deferred_acceptance_step"
16672            | "boston_mechanism_step" | "top_trading_cycles_step"
16673            | "school_choice_match" | "roommate_match_step"
16674            | "network_formation_step" | "coordination_game_payoff"
16675            | "evolutionary_stable_strategy" | "replicator_dynamics_step"
16676            | "hawk_dove_payoff" | "fictitious_play_step"
16677            | "best_response_dynamic" | "quantal_response_logit"
16678            | "level_k_step" | "cognitive_hierarchy_step"
16679            | "sequential_eq_check" | "subgame_perfect_eq"
16680            | "stackelberg_step" | "cournot_quantity_step"
16681            | "bertrand_price_step" | "hotelling_price_step"
16682            | "collusion_payoff_step" | "folk_theorem_value"
16683            | "repeated_game_avg_payoff" | "discount_factor_step"
16684            | "trigger_strategy_payoff" | "grim_trigger_step"
16685            | "tit_for_tat_step" | "prisoners_repeated_eq"
16686            | "mertens_zamir_step" | "ex_post_value_check"
16687            | "ex_ante_value_check" | "common_knowledge_iterations"
16688            // ── symbolic CAS, decompositions, projections ───────
16689            | "cas_simplify_term" | "cas_expand_two_terms"
16690            | "cas_factor_quadratic" | "cas_partial_fraction_simple"
16691            | "cas_polynomial_gcd_step" | "cas_polynomial_div_step"
16692            | "cas_lagrange_interpolate" | "cas_chebyshev_eval"
16693            | "cas_legendre_eval" | "cas_hermite_eval"
16694            | "cas_laguerre_eval" | "cas_jacobi_eval"
16695            | "cas_gegenbauer_eval" | "cas_taylor_coefficient"
16696            | "cas_padé_diagonal" | "cas_continued_fraction_step"
16697            | "cas_resultant_two" | "cas_subresultant_two"
16698            | "cas_groebner_lt_step" | "cas_buchberger_step"
16699            | "cas_macaulay_matrix_step" | "cas_modular_inverse"
16700            | "cas_extended_euclid_step" | "cas_smith_normal_step"
16701            | "cas_hermite_normal_step" | "cas_radical_simplify"
16702            | "cas_minimal_polynomial" | "cas_gcd_polynomial_step"
16703            | "cas_resultant_x_y" | "cas_solve_linear"
16704            | "cas_solve_quadratic" | "cas_solve_cubic"
16705            | "cas_solve_quartic" | "cas_solve_polynomial_n"
16706            | "cas_root_isolate_step" | "cas_sturm_sequence_step"
16707            | "cas_descartes_rule_count" | "cas_companion_matrix_root"
16708            | "cas_polynomial_roots_kahan"
16709            | "cas_eigenvalue_inverse_iteration" | "cas_qr_iteration_step"
16710            | "cas_jacobi_eigen_step" | "cas_lanczos_iteration_step"
16711            | "cas_arnoldi_iteration_step" | "cas_givens_rotation_apply"
16712            | "cas_householder_reflection" | "cas_modified_gram_schmidt"
16713            | "cas_classical_gram_schmidt" | "cas_rank_revealing_qr"
16714            | "cas_pivoted_lu_step" | "cas_block_lu_step"
16715            | "cas_cholesky_step" | "cas_modified_cholesky"
16716            | "cas_ldlt_step" | "cas_bunch_kaufman_step"
16717            | "cas_woodbury_identity" | "cas_matrix_pencil_step"
16718            | "cas_generalized_eigen" | "cas_singular_value_step"
16719            | "cas_truncated_svd_value" | "cas_pseudoinverse_step"
16720            | "cas_polar_decomposition" | "cas_schur_decomposition_step"
16721            | "cas_quasi_triangular" | "cas_riccati_continuous_step"
16722            | "cas_riccati_discrete_step" | "cas_lyapunov_continuous_step"
16723            | "cas_lyapunov_discrete_step" | "cas_sylvester_equation_step"
16724            | "cas_kronecker_product_step" | "cas_vec_operator_step"
16725            | "cas_matrix_function_step" | "cas_matrix_log_step"
16726            | "cas_matrix_exp_pade" | "cas_matrix_sqrt_step"
16727            | "cas_drazin_inverse_step" | "cas_moore_penrose_step"
16728            | "cas_least_squares_solve" | "cas_total_least_squares"
16729            | "cas_constrained_ls_step" | "cas_truncated_lsq"
16730            | "cas_regularized_lsq_tikhonov" | "cas_basis_pursuit_step"
16731            | "cas_lasso_soft_threshold" | "cas_elastic_net_step"
16732            | "cas_omp_step" | "cas_iht_iteration"
16733            | "cas_cosamp_step" | "cas_admm_lasso_step"
16734            | "cas_proximal_l1_step" | "cas_proximal_l2_step"
16735            | "cas_proximal_l_inf_step" | "cas_indicator_simplex_proj"
16736            | "cas_proj_l1_ball" | "cas_proj_l2_ball"
16737            | "cas_proj_box" | "cas_proj_psd_cone"
16738            | "cas_proj_soc_step" | "cas_proj_exp_cone"
16739            | "cas_dykstra_step" | "cas_alternating_projection"
16740            | "cas_polya_enumeration_step" | "cas_burnside_count_step"
16741            // ── ML primitives — activations, losses, optimizers ─
16742            | "ml_relu_step" | "ml_leaky_relu_step" | "ml_elu_step"
16743            | "ml_selu_step" | "ml_gelu_step" | "ml_swish_step"
16744            | "ml_mish_step" | "ml_softplus_step" | "ml_softsign_step"
16745            | "ml_hard_sigmoid" | "ml_hard_tanh" | "ml_prelu_step"
16746            | "ml_celu_step" | "ml_silu_step" | "ml_logsumexp_step"
16747            | "ml_log_softmax_step" | "ml_log_sigmoid"
16748            | "ml_glu_step" | "ml_geglu_step" | "ml_swiglu_step"
16749            | "ml_attention_score_step" | "ml_scaled_dot_product"
16750            | "ml_multihead_avg" | "ml_softmax_temperature"
16751            | "ml_dropout_mask_prob" | "ml_layer_norm_step"
16752            | "ml_batch_norm_step" | "ml_group_norm_step"
16753            | "ml_rms_norm_step" | "ml_instance_norm_step"
16754            | "ml_weight_norm_step" | "ml_spectral_norm_step"
16755            | "ml_l2_normalize_step" | "ml_huber_loss_step"
16756            | "ml_smooth_l1_loss" | "ml_focal_loss_step"
16757            | "ml_dice_loss_step" | "ml_iou_loss_step"
16758            | "ml_giou_loss_step" | "ml_diou_loss_step"
16759            | "ml_ciou_loss_step" | "ml_contrastive_loss"
16760            | "ml_triplet_loss_step" | "ml_arcface_loss_step"
16761            | "ml_center_loss_step" | "ml_kl_divergence_loss"
16762            | "ml_cross_entropy_loss" | "ml_binary_cross_entropy"
16763            | "ml_label_smoothing" | "ml_mixup_lambda"
16764            | "ml_cutmix_box_iou" | "ml_random_erasing_step"
16765            | "ml_cosine_lr_schedule" | "ml_warmup_lr_step"
16766            | "ml_step_lr_schedule" | "ml_exponential_lr"
16767            | "ml_polynomial_lr" | "ml_one_cycle_lr"
16768            | "ml_inverse_sqrt_lr" | "ml_cyclic_lr_step"
16769            | "ml_sgd_step" | "ml_momentum_step"
16770            | "ml_nesterov_momentum" | "ml_adagrad_step"
16771            | "ml_rmsprop_step" | "ml_adam_step"
16772            | "ml_adamw_step" | "ml_adamax_step"
16773            | "ml_nadam_step" | "ml_radam_step"
16774            | "ml_lookahead_step" | "ml_lamb_step"
16775            | "ml_lars_step" | "ml_yogi_step"
16776            | "ml_amsgrad_step" | "ml_adabelief_step"
16777            | "ml_shampoo_step" | "ml_lion_step"
16778            | "ml_sophia_step" | "ml_gradient_clip_norm"
16779            | "ml_gradient_clip_value" | "ml_gradient_accumulate"
16780            | "ml_gradient_centralize" | "ml_weight_decay_step"
16781            | "ml_he_init_value" | "ml_xavier_init_value"
16782            | "ml_glorot_init_value" | "ml_orthogonal_init"
16783            | "ml_truncnormal_init" | "ml_kaiming_init"
16784            | "ml_lecun_init_value" | "ml_zero_init"
16785            | "ml_constant_init" | "ml_uniform_init"
16786            | "ml_one_hot_index" | "ml_label_to_id"
16787            | "ml_id_to_label_step" | "ml_token_logit_top_k"
16788            | "ml_topk_argmax" | "ml_nucleus_sample_p"
16789            | "ml_temperature_decay" | "ml_repetition_penalty"
16790            | "ml_eos_logit_boost"
16791            // ── NLP — ranking, similarity, language models ──────
16792            | "nlp_bm25_score" | "nlp_tf_idf_step" | "nlp_okapi_score"
16793            | "nlp_word_freq_value" | "nlp_doc_freq_step"
16794            | "nlp_inverse_doc_freq" | "nlp_cosine_similarity_two"
16795            | "nlp_jaccard_similarity_two" | "nlp_overlap_coefficient"
16796            | "nlp_dice_coefficient_two" | "nlp_simpson_coefficient"
16797            | "nlp_levenshtein_dist" | "nlp_damerau_levenshtein"
16798            | "nlp_jaro_distance" | "nlp_jaro_winkler"
16799            | "nlp_hamming_distance" | "nlp_lcs_length" | "nlp_lcs_ratio"
16800            | "nlp_meteor_score" | "nlp_bleu_score_n"
16801            | "nlp_rouge_score_n" | "nlp_chrf_score" | "nlp_ter_score"
16802            | "nlp_wer_score" | "nlp_cer_score" | "nlp_perplexity_value"
16803            | "nlp_bits_per_character" | "nlp_char_ngram_count"
16804            | "nlp_word_ngram_count" | "nlp_skip_gram_count"
16805            | "nlp_byte_pair_merge_step" | "nlp_wordpiece_score"
16806            | "nlp_unigram_lm_score" | "nlp_kneser_ney_step"
16807            | "nlp_witten_bell_step" | "nlp_good_turing_count"
16808            | "nlp_laplace_smoothing" | "nlp_lidstone_smoothing"
16809            | "nlp_jelinek_mercer" | "nlp_dirichlet_smoothing"
16810            | "nlp_query_likelihood_step" | "nlp_kl_lm_div"
16811            | "nlp_pmi_score" | "nlp_npmi_score"
16812            | "nlp_chi2_collocation" | "nlp_loglikelihood_collocation"
16813            | "nlp_t_score_collocation" | "nlp_dunning_log_likelihood"
16814            | "nlp_lda_alpha_step" | "nlp_lda_beta_step"
16815            | "nlp_lda_topic_dist" | "nlp_plsa_step"
16816            | "nlp_word2vec_skipgram_loss" | "nlp_word2vec_cbow_loss"
16817            | "nlp_glove_loss_step" | "nlp_fasttext_subword_count"
16818            | "nlp_byte_level_bpe_step" | "nlp_sentencepiece_score"
16819            | "nlp_unigram_subword_loss" | "nlp_subword_regularization"
16820            | "nlp_pointwise_attn_score" | "nlp_relative_position_bias"
16821            | "nlp_alibi_position_bias" | "nlp_rope_rotary_angle"
16822            | "nlp_rope_apply_step" | "nlp_position_encoding_sin"
16823            | "nlp_position_encoding_cos" | "nlp_pe_freq_band"
16824            | "nlp_max_seq_len_check" | "nlp_token_drop_rate"
16825            | "nlp_byte_frequency" | "nlp_char_frequency"
16826            | "nlp_punct_ratio" | "nlp_uppercase_ratio"
16827            | "nlp_digit_ratio" | "nlp_emoji_ratio"
16828            | "nlp_url_count" | "nlp_email_count" | "nlp_phone_count"
16829            | "nlp_hashtag_count" | "nlp_mention_count"
16830            | "nlp_token_overlap_two" | "nlp_word_mover_dist"
16831            | "nlp_sif_weight_step" | "nlp_doc_embedding_avg"
16832            | "nlp_attention_pool_step" | "nlp_max_pool_step"
16833            | "nlp_avg_pool_step" | "nlp_sum_pool_step"
16834            | "nlp_self_attn_compute_step" | "nlp_cross_attn_compute_step"
16835            | "nlp_window_attn_step" | "nlp_strided_attn_step"
16836            | "nlp_block_attn_step" | "nlp_sliding_window_step"
16837            | "nlp_local_attn_step" | "nlp_dilated_attn_step"
16838            | "nlp_global_attn_step" | "nlp_sparse_attn_score"
16839            | "nlp_linformer_step" | "nlp_performer_step"
16840            | "nlp_reformer_step" | "nlp_longformer_step"
16841            | "nlp_bigbird_step" | "nlp_routing_attn_step"
16842            // ── graphics, geometry, ray tracing, BRDF, color ────
16843            | "gfx_perspective_proj_x" | "gfx_perspective_proj_y"
16844            | "gfx_orthographic_proj" | "gfx_view_matrix_step"
16845            | "gfx_lookat_forward" | "gfx_lookat_right" | "gfx_lookat_up"
16846            | "gfx_quat_to_axis_angle" | "gfx_axis_angle_to_quat"
16847            | "gfx_quat_slerp_step" | "gfx_quat_nlerp_step"
16848            | "gfx_quat_dot_two" | "gfx_quat_inverse_step"
16849            | "gfx_quat_to_euler_pitch" | "gfx_quat_to_euler_yaw"
16850            | "gfx_quat_to_euler_roll" | "gfx_euler_to_quat_x"
16851            | "gfx_euler_to_quat_y" | "gfx_euler_to_quat_z"
16852            | "gfx_euler_to_quat_w" | "gfx_rotation_matrix_xx"
16853            | "gfx_rotation_matrix_yy" | "gfx_rotation_matrix_zz"
16854            | "gfx_translation_matrix_step" | "gfx_scale_matrix_step"
16855            | "gfx_shear_matrix_xy" | "gfx_homogeneous_divide"
16856            | "gfx_screen_space_x" | "gfx_screen_space_y"
16857            | "gfx_ndc_to_screen_x" | "gfx_ndc_to_screen_y"
16858            | "gfx_screen_to_ndc_x" | "gfx_screen_to_ndc_y"
16859            | "gfx_clip_polygon_step" | "gfx_sutherland_hodgman"
16860            | "gfx_cohen_sutherland_code" | "gfx_liang_barsky_t"
16861            | "gfx_bresenham_step_x" | "gfx_bresenham_step_y"
16862            | "gfx_xiaolin_wu_intensity" | "gfx_aabb_intersect_check"
16863            | "gfx_obb_overlap_step" | "gfx_sphere_intersect_t"
16864            | "gfx_ray_triangle_t" | "gfx_ray_plane_t" | "gfx_ray_box_t"
16865            | "gfx_ray_sphere_t" | "gfx_ray_disk_t"
16866            | "gfx_ray_cylinder_t" | "gfx_ray_cone_t"
16867            | "gfx_ray_ellipsoid_t" | "gfx_ray_torus_t_approx"
16868            | "gfx_barycentric_alpha" | "gfx_barycentric_beta"
16869            | "gfx_barycentric_gamma" | "gfx_phong_diffuse_step"
16870            | "gfx_phong_specular_step" | "gfx_phong_ambient_step"
16871            | "gfx_blinn_specular_step" | "gfx_lambert_term"
16872            | "gfx_oren_nayar_term" | "gfx_cook_torrance_d_ggx"
16873            | "gfx_cook_torrance_g_smith" | "gfx_cook_torrance_f_schlick"
16874            | "gfx_disney_principled_d" | "gfx_microfacet_brdf_step"
16875            | "gfx_subsurface_scattering_term" | "gfx_translucent_falloff"
16876            | "gfx_normal_distribution_ggx"
16877            | "gfx_geometric_attenuation_smith"
16878            | "gfx_fresnel_dielectric_step" | "gfx_fresnel_conductor_step"
16879            | "gfx_index_of_refraction" | "gfx_snells_law_angle"
16880            | "gfx_total_internal_reflection" | "gfx_refract_direction_x"
16881            | "gfx_reflect_direction_x" | "gfx_environment_map_uv_u"
16882            | "gfx_environment_map_uv_v" | "gfx_cube_map_face_index"
16883            | "gfx_octahedral_encode_x" | "gfx_octahedral_encode_y"
16884            | "gfx_spherical_harmonic_y00" | "gfx_spherical_harmonic_y10"
16885            | "gfx_spherical_harmonic_y11" | "gfx_spherical_harmonic_y20"
16886            | "gfx_zonal_harmonic_step" | "gfx_irradiance_sh_eval"
16887            | "gfx_radiance_sh_eval" | "gfx_skybox_uv_u" | "gfx_skybox_uv_v"
16888            | "gfx_tonemap_reinhard" | "gfx_tonemap_aces"
16889            | "gfx_tonemap_uncharted2" | "gfx_tonemap_filmic"
16890            | "gfx_gamma_correct_step" | "gfx_srgb_to_linear"
16891            | "gfx_linear_to_srgb" | "gfx_dither_bayer_4x4"
16892            | "gfx_dither_floyd_steinberg" | "gfx_oklab_l_step"
16893            | "gfx_oklab_a_step" | "gfx_oklab_b_step"
16894            | "gfx_oklch_chroma" | "gfx_oklch_hue"
16895            | "gfx_pcg_hash_step" | "gfx_xorshift_step"
16896            | "gfx_halton_step" | "gfx_sobol_step"
16897            | "gfx_van_der_corput" | "gfx_low_discrepancy_step"
16898            | "gfx_blue_noise_value" | "gfx_perlin_noise_step"
16899            | "gfx_simplex_noise_step" | "gfx_fbm_noise_step"
16900            | "gfx_worley_noise_step" | "gfx_voronoi_distance"
16901            | "gfx_curl_noise_step" | "gfx_gradient_noise_step"
16902            | "gfx_value_noise_step" | "gfx_signed_distance_box"
16903            | "gfx_signed_distance_sphere" | "gfx_signed_distance_capsule"
16904            // ── database internals, distributed systems ─────────
16905            | "db_b_tree_split" | "db_b_tree_merge"
16906            | "db_lsm_compaction_step" | "db_skiplist_height_pick"
16907            | "db_bloom_filter_bit_index" | "db_cuckoo_filter_fingerprint"
16908            | "db_quotient_filter_canonical" | "db_count_min_sketch_bin"
16909            | "db_hyperloglog_register_max" | "db_min_hash_value"
16910            | "db_simhash_bit" | "db_consistent_hash_index"
16911            | "db_rendezvous_hash_score" | "db_jump_hash_bucket"
16912            | "db_maglev_hash_step" | "db_lru_cache_eviction_age"
16913            | "db_lfu_cache_decay" | "db_arc_cache_score"
16914            | "db_clock_cache_hand" | "db_tinylfu_admit_score"
16915            | "db_w_tinylfu_freq" | "db_buffer_pool_score"
16916            | "db_query_plan_cost_step" | "db_join_selectivity_step"
16917            | "db_index_seek_cost" | "db_seq_scan_cost"
16918            | "db_index_scan_cost" | "db_sort_cost_estimate"
16919            | "db_hash_join_cost" | "db_merge_join_cost"
16920            | "db_nested_loop_cost" | "db_query_cardinality"
16921            | "db_histogram_bucket_index" | "db_quantile_estimate_p99"
16922            | "db_t_digest_centroid" | "db_kll_quantile_step"
16923            | "db_dd_sketch_bin" | "db_reservoir_sample_index"
16924            | "db_chao_estimator_step" | "db_jaccard_minhash_estimate"
16925            | "db_distinct_estimate_lpc" | "db_distinct_estimate_hll"
16926            | "db_throttle_token_step" | "db_leaky_bucket_step"
16927            | "db_token_bucket_step" | "db_circuit_breaker_step"
16928            | "db_two_phase_commit_step" | "db_three_phase_commit_step"
16929            | "db_paxos_propose_id" | "db_raft_term_advance"
16930            | "db_raft_log_match_check" | "db_zab_epoch_step"
16931            | "db_chubby_lease_step" | "db_logical_clock_step"
16932            | "db_lamport_timestamp" | "db_vector_clock_merge"
16933            | "db_hybrid_logical_clock" | "db_crdt_g_counter_merge"
16934            | "db_crdt_pn_counter_merge" | "db_crdt_lww_register_merge"
16935            | "db_crdt_set_or_merge" | "db_consensus_quorum_size"
16936            | "db_replication_lag_step" | "db_partitions_for_n"
16937            | "db_consistent_lookup_id" | "db_chord_finger_index"
16938            | "db_kademlia_xor_distance" | "db_pastry_routing_step"
16939            | "db_dht_replicate_factor" | "db_partition_failure_check"
16940            | "db_byzantine_quorum_size" | "db_pbft_view_change"
16941            | "db_honey_badger_step" | "db_avalanche_query_step"
16942            | "db_quorum_intersection_check" | "db_anti_entropy_step"
16943            | "db_merkle_node_hash" | "db_merkle_path_verify"
16944            | "db_gossip_fanout_step" | "db_anti_entropy_pull_step"
16945            | "db_split_brain_check" | "db_clock_skew_estimate"
16946            | "db_freshness_score" | "db_read_repair_step"
16947            | "db_hinted_handoff_step" | "db_compaction_score"
16948            | "db_levelled_compaction_step" | "db_size_tiered_compaction"
16949            | "db_universal_compaction_step" | "db_write_amplification"
16950            | "db_read_amplification" | "db_space_amplification"
16951            | "db_block_cache_hit_rate" | "db_page_cache_eviction_age"
16952            | "db_wal_fsync_cost" | "db_group_commit_count"
16953            | "db_replica_lag_threshold" | "db_synchronous_commit_check"
16954            | "db_async_commit_check" | "db_eventual_consistency_check"
16955            | "db_strong_consistency_check" | "db_linearizability_check"
16956            | "db_causal_consistency_check"
16957            // ── networking — TCP, AQM, MIMO, queueing ───────────
16958            | "net_tcp_cwnd_step" | "net_tcp_ssthresh_update"
16959            | "net_tcp_reno_step" | "net_tcp_cubic_step"
16960            | "net_tcp_bbr_step" | "net_tcp_vegas_step"
16961            | "net_tcp_westwood_step" | "net_tcp_compound_step"
16962            | "net_tcp_dctcp_step" | "net_tcp_yeah_step"
16963            | "net_tcp_htcp_step" | "net_tcp_hybla_step"
16964            | "net_tcp_illinois_step" | "net_tcp_lp_step"
16965            | "net_tcp_scalable_step" | "net_tcp_veno_step"
16966            | "net_aiad_step" | "net_aimd_step"
16967            | "net_miad_step" | "net_mimd_step"
16968            | "net_aqm_red_drop_prob" | "net_aqm_codel_target"
16969            | "net_aqm_pie_drop_rate" | "net_aqm_fq_codel_step"
16970            | "net_aqm_blue_step" | "net_aqm_choke_step"
16971            | "net_aqm_sfq_step" | "net_aqm_drr_step"
16972            | "net_aqm_wrr_step" | "net_token_rate_limit"
16973            | "net_traffic_shaper_step" | "net_priority_queue_index"
16974            | "net_packet_loss_estimate" | "net_jitter_estimate"
16975            | "net_latency_avg" | "net_rtt_smoothed"
16976            | "net_rtt_variation" | "net_rto_compute"
16977            | "net_bandwidth_delay_product" | "net_path_capacity_kleinrock"
16978            | "net_loss_rate_to_throughput" | "net_throughput_padhye"
16979            | "net_throughput_mathis" | "net_throughput_response"
16980            | "net_router_buffer_size" | "net_drop_tail_check"
16981            | "net_burst_size_compute" | "net_packet_pacing_step"
16982            | "net_link_capacity_share" | "net_proportional_fair_share"
16983            | "net_max_min_fair_step" | "net_alpha_fair_step"
16984            | "net_kelly_pricing_step" | "net_network_utility_max"
16985            | "net_lyapunov_drift_plus_penalty" | "net_backpressure_step"
16986            | "net_max_weight_match" | "net_qcsma_propose"
16987            | "net_csma_back_off" | "net_alohanet_throughput"
16988            | "net_slotted_aloha_throughput" | "net_csma_efficiency"
16989            | "net_token_ring_efficiency" | "net_polling_efficiency"
16990            | "net_radio_path_loss" | "net_friis_received_power"
16991            | "net_two_ray_ground_loss" | "net_okumura_hata_loss"
16992            | "net_log_distance_path" | "net_shadowing_normal"
16993            | "net_rician_k_factor" | "net_rayleigh_envelope"
16994            | "net_doppler_shift" | "net_capacity_shannon"
16995            | "net_mimo_capacity_step" | "net_zero_forcing_beam"
16996            | "net_mmse_beam_step" | "net_water_filling_power"
16997            | "net_amc_threshold_index" | "net_harq_combining_gain"
16998            | "net_turbo_decode_iter" | "net_ldpc_iteration_step"
16999            | "net_polar_decode_step" | "net_viterbi_step"
17000            | "net_bcjr_step" | "net_outage_probability"
17001            | "net_diversity_gain" | "net_array_gain"
17002            | "net_multiplexing_gain" | "net_coding_gain"
17003            | "net_pruning_gain" | "net_macro_diversity_step"
17004            | "net_micro_diversity_step" | "net_handoff_threshold"
17005            | "net_call_admission_check" | "net_blocking_probability"
17006            | "net_erlang_b_formula" | "net_erlang_c_formula"
17007            | "net_engset_formula" | "net_little_law_l"
17008            | "net_throughput_law" | "net_response_time_law"
17009            | "net_utilization_law" | "net_forced_flow_law"
17010            // ── OS internals — schedulers, I/O, memory ──────────
17011            | "os_priority_aging_step" | "os_mlfq_demote_step"
17012            | "os_mlfq_promote_step" | "os_round_robin_quantum"
17013            | "os_completely_fair_vruntime" | "os_lottery_ticket_count"
17014            | "os_stride_pass_step" | "os_eevdf_eligible"
17015            | "os_cfs_load_balance_step" | "os_eas_energy_estimate"
17016            | "os_smt_threading_share" | "os_numa_node_distance"
17017            | "os_cpu_affinity_score" | "os_thread_migration_cost"
17018            | "os_load_average_decay" | "os_runqueue_depth"
17019            | "os_io_scheduler_deadline" | "os_io_scheduler_cfq_step"
17020            | "os_io_scheduler_noop_step" | "os_io_scheduler_bfq_step"
17021            | "os_io_scheduler_kyber_step" | "os_io_scheduler_mq_deadline"
17022            | "os_anticipation_window" | "os_elevator_step"
17023            | "os_disk_seek_time" | "os_disk_rotational_lat"
17024            | "os_disk_transfer_time" | "os_pre_fetch_window"
17025            | "os_buffer_cache_pages" | "os_dirty_page_threshold"
17026            | "os_writeback_step" | "os_swappiness_factor"
17027            | "os_kswapd_wake_threshold" | "os_oom_score_step"
17028            | "os_page_replacement_lru" | "os_page_replacement_clock"
17029            | "os_page_replacement_2q" | "os_working_set_size"
17030            | "os_thrashing_threshold" | "os_demand_paging_step"
17031            | "os_copy_on_write_check" | "os_zero_page_optimization"
17032            | "os_huge_page_threshold" | "os_transparent_hugepage"
17033            | "os_kasan_shadow_offset" | "os_kfence_check"
17034            | "os_kfence_alloc_index" | "os_slub_object_size_round"
17035            | "os_slab_color_offset" | "os_per_cpu_cache_size"
17036            | "os_buddy_order_pick" | "os_compact_memory_step"
17037            | "os_kvm_vmcs_field_offset" | "os_apic_irq_priority"
17038            | "os_msi_x_vector_count" | "os_iommu_domain_step"
17039            | "os_pci_bus_address" | "os_acpi_state_transition"
17040            | "os_cpufreq_governor_step" | "os_intel_pstate_target"
17041            | "os_amd_pstate_target" | "os_thermal_zone_trip"
17042            | "os_throttle_temperature" | "os_battery_capacity_pct"
17043            | "os_powertop_score" | "os_idle_state_select"
17044            | "os_c_state_residency" | "os_p_state_voltage"
17045            | "os_dvfs_step" | "os_voltage_scaling_step"
17046            | "os_frequency_scaling_step" | "os_inotify_event_count"
17047            | "os_epoll_ctl_count" | "os_io_uring_sqe_count"
17048            | "os_io_uring_cqe_count" | "os_kqueue_event_count"
17049            | "os_systemd_journal_size" | "os_dmesg_severity_level"
17050            | "os_audit_event_priority" | "os_apparmor_profile_active"
17051            | "os_selinux_context_match" | "os_smack_label_compare"
17052            | "os_capability_check" | "os_seccomp_filter_step"
17053            | "os_namespace_isolation" | "os_cgroup_v1_count"
17054            | "os_cgroup_v2_count" | "os_pid_max_value"
17055            | "os_thread_max_value" | "os_file_max_value"
17056            | "os_open_files_count" | "os_socket_max_value"
17057            | "os_inotify_max_watches" | "os_oom_kill_score"
17058            | "os_zswap_compress_ratio" | "os_zram_compress_ratio"
17059            | "os_swap_pressure_score" | "os_pressure_stall_step"
17060            | "os_psi_avg10_step" | "os_psi_avg60_step"
17061            | "os_psi_avg300_step" | "os_load_proc_avg"
17062            | "os_load_user_avg" | "os_load_iowait_avg"
17063            // ── security — KDFs, MFA, PKI, web sec, TLS ─────────
17064            | "sec_argon2_memcost" | "sec_argon2_timecost"
17065            | "sec_argon2_parallelism" | "sec_argon2_block_step"
17066            | "sec_pbkdf2_iter" | "sec_scrypt_n_param"
17067            | "sec_scrypt_r_param" | "sec_scrypt_p_param"
17068            | "sec_balloon_hash_step" | "sec_yescrypt_step"
17069            | "sec_bcrypt_cost_factor" | "sec_bcrypt_round_step"
17070            | "sec_password_strength_zxcvbn" | "sec_haveibeenpwned_check"
17071            | "sec_diceware_word_index" | "sec_xkcd_passphrase_score"
17072            | "sec_passphrase_entropy" | "sec_chosen_charset_strength"
17073            | "sec_keystroke_timing_var" | "sec_2fa_totp_window"
17074            | "sec_totp_drift_check" | "sec_hotp_counter_step"
17075            | "sec_yubikey_otp_check" | "sec_webauthn_attestation_check"
17076            | "sec_fido2_assertion_check" | "sec_certificate_chain_depth"
17077            | "sec_revocation_ocsp_check" | "sec_crl_age_seconds"
17078            | "sec_pki_path_validate" | "sec_x509_subject_match"
17079            | "sec_san_match_count" | "sec_basic_constraints_ca"
17080            | "sec_pinning_compare" | "sec_certificate_transparency"
17081            | "sec_dane_tlsa_match" | "sec_hpkp_pin_match"
17082            | "sec_csp_directive_match" | "sec_csrf_token_match"
17083            | "sec_cors_origin_match" | "sec_xss_filter_score"
17084            | "sec_html_escape_check" | "sec_url_safe_encode_check"
17085            | "sec_path_traversal_detect" | "sec_sqli_pattern_score"
17086            | "sec_xxe_pattern_score" | "sec_xxe_dtd_check"
17087            | "sec_command_injection_score" | "sec_idor_check"
17088            | "sec_jwt_alg_safe" | "sec_jwt_kid_match"
17089            | "sec_jwt_signature_verify" | "sec_oauth2_state_validate"
17090            | "sec_oauth2_pkce_step" | "sec_oauth_nonce_check"
17091            | "sec_session_lifetime" | "sec_idle_timeout_step"
17092            | "sec_login_throttle_step" | "sec_account_lockout_step"
17093            | "sec_password_history_check" | "sec_complexity_policy_score"
17094            | "sec_dictionary_attack_check" | "sec_brute_force_attempts"
17095            | "sec_credential_stuffing_score" | "sec_kerberos_ticket_age"
17096            | "sec_kerberos_pac_check" | "sec_kerberos_pre_auth"
17097            | "sec_ldap_bind_step" | "sec_radius_auth_step"
17098            | "sec_diameter_avp_step" | "sec_saml_assertion_age"
17099            | "sec_oidc_id_token_age" | "sec_acme_dns_challenge"
17100            | "sec_dnssec_signature_check" | "sec_spf_pass_check"
17101            | "sec_dkim_signature_check" | "sec_dmarc_policy_check"
17102            | "sec_arc_chain_step" | "sec_smtp_ssl_check"
17103            | "sec_imap_starttls_check" | "sec_pop3_security_step"
17104            | "sec_tls_alert_severity" | "sec_tls13_handshake_step"
17105            | "sec_tls12_handshake_step" | "sec_tls11_deprecation_check"
17106            | "sec_ssl3_disabled_check" | "sec_cipher_suite_strength"
17107            | "sec_cbc_mac_block_count" | "sec_gcm_iv_unique_check"
17108            | "sec_chachapoly_nonce_check" | "sec_x25519_clamping_step"
17109            | "sec_ed25519_signature_step" | "sec_ed448_signature_step"
17110            | "sec_p384_curve_step" | "sec_secp256k1_step"
17111            | "sec_blake3_chunk_step" | "sec_keccak_round_step"
17112            | "sec_sha3_padding_step" | "sec_argon2_state_advance"
17113            | "sec_chacha20_quarterround" | "sec_aes_round_step"
17114            | "sec_aes_keyschedule_step" | "sec_des_round_step"
17115            | "sec_blowfish_round_step" | "sec_serpent_round_step"
17116            | "sec_twofish_round_step"
17117            // ── calendrical algorithms ──────────────────────────
17118            | "fixed_from_gregorian" | "gregorian_from_fixed"
17119            | "fixed_from_julian" | "julian_from_fixed"
17120            | "iso_week_date" | "hebrew_leap_year"
17121            | "hebrew_year_length" | "fixed_from_hebrew"
17122            | "islamic_leap_year" | "fixed_from_islamic"
17123            | "persian_arithmetic_leap" | "fixed_from_persian"
17124            | "coptic_from_fixed" | "ethiopic_from_fixed"
17125            | "french_revolutionary_leap" | "fixed_from_french"
17126            | "chinese_year_zodiac" | "chinese_lunation_winter"
17127            | "hindu_solar_year" | "hindu_lunisolar_month"
17128            | "maya_long_count_from_fixed" | "mayan_haab_from_fixed"
17129            | "mayan_tzolkin_from_fixed" | "badi_year_from_fixed"
17130            | "bahai_from_fixed" | "easter_gregorian_year"
17131            | "easter_orthodox_year" | "easter_julian_year"
17132            | "day_of_week_zeller" | "iso_day_number"
17133            | "weekday_name_short" | "leap_year_gregorian"
17134
17135            // ── R / SciPy distributions and tests ───────────────
17136            | "dnorm" | "dt" | "df_dist" | "dchisq"
17137            | "glm" | "aov" | "shapiro_wilk" | "anderson_darling"
17138            | "kolmogorov_smirnov" | "spearmanr" | "kendalltau" | "pearsonr"
17139            | "mannwhitneyu" | "wilcoxon" | "kruskal_h"
17140
17141            // ── APL/J/K array primitives ────────────────────────
17142            | "iota_n" | "reduce_axis" | "scan_axis" | "fold_axis"
17143            | "rotate_axis" | "transpose_axis" | "reshape_dim"
17144            | "encode_base" | "decode_base" | "nub_list" | "nub_count"
17145            | "membership_idx" | "deal_n_k" | "roll_n"
17146            | "permute_idx" | "invert_perm"
17147
17148            // ── astronomy / astrometry ──────────────────────────
17149            | "julian_day" | "jd_to_calendar" | "tt_to_tdb"
17150            | "ra_dec_to_alt_az" | "alt_az_to_ra_dec"
17151            | "precession_iau2006" | "nutation_iau2000a"
17152            | "aberration_annual" | "proper_motion_apply"
17153            | "parallax_correction" | "sun_position_low" | "sun_distance_au"
17154            | "moon_position_low" | "moon_phase_age" | "lunation_index"
17155            | "eclipse_magnitude" | "saros_cycle" | "metonic_cycle"
17156            | "orbit_kepler3" | "orbital_period_au" | "orbit_eccentric_anomaly"
17157            | "escape_velocity_body" | "hill_sphere_radius" | "tisserand_param"
17158            | "tle_mean_motion" | "sgp4_propagate_step" | "airy_disk_radius"
17159            | "rayleigh_criterion" | "strehl_ratio" | "au_to_km"
17160
17161            // ── sports analytics — ratings & sabermetric ────────
17162            | "elo_expected" | "elo_update" | "glicko_rating"
17163            | "trueskill_update" | "trueskill_match_quality"
17164            | "pythagorean_expectation" | "war_above_replacement"
17165            | "woba_weight" | "wrc_plus" | "ops_plus" | "era_plus"
17166            | "fip" | "xfip" | "siera" | "babip" | "wpa"
17167            | "win_probability" | "leverage_index" | "clutch_score"
17168            | "shooting_pct" | "save_pct" | "corsi_for" | "fenwick_for"
17169            | "goals_above_avg" | "tackle_efficiency" | "yards_per_attempt"
17170            | "qbr_metric" | "epa_per_play"
17171
17172            // ── Excel/Sheets + bond/loan financial ──────────────
17173            | "vlookup" | "hlookup" | "xlookup" | "index_match"
17174            | "indirect" | "choose" | "offset"
17175            | "sumif" | "countif" | "averageif"
17176            | "sumifs" | "countifs" | "averageifs"
17177            | "sumproduct" | "rank_eq" | "rank_avg" | "percentrank"
17178            | "quartile_inc" | "quartile_exc"
17179            | "xnpv" | "ppmt" | "ipmt" | "rate"
17180            | "macauley_duration" | "convexity" | "yield_to_maturity"
17181            | "accrued_interest" | "clean_price" | "dirty_price"
17182            | "coupon_count" | "skill_score" | "reliability_diagram"
17183            | "taylor_diagram_score"
17184
17185            // ── GIS — geohash, H3, S2, UTM, projections ─────────
17186            | "geohash_neighbors" | "h3_index" | "h3_geo_to_h3"
17187            | "h3_h3_to_geo" | "h3_k_ring" | "h3_neighbor" | "h3_resolution"
17188            | "s2_cell_id" | "s2_cell_at_lat_lng" | "s2_cell_neighbors"
17189            | "utm_from_lat_lng" | "utm_to_lat_lng"
17190            | "mgrs_encode" | "mgrs_decode"
17191            | "lat_lng_to_xy_mercator" | "lat_lng_to_xy_lambert"
17192            | "haversine_dist" | "vincenty_dist" | "andoyer_dist"
17193            | "rhumb_line_bearing"
17194            | "destination_point" | "tile_xyz_to_lat_lng" | "lat_lng_to_tile_xyz"
17195            | "polygon_winding_order" | "point_in_polygon_ray"
17196            | "point_in_polygon_winding" | "segment_intersection"
17197            | "segment_distance_point" | "convex_hull_chan"
17198
17199            // ── robotics & control ──────────────────────────────
17200            | "pid_anti_windup" | "pid_ziegler_nichols"
17201            | "smith_predictor_step" | "lqr_gain_continuous"
17202            | "lqr_gain_discrete" | "lqg_step" | "h_infinity_norm"
17203            | "bode_gain_margin" | "bode_phase_margin"
17204            | "nyquist_encirclement" | "nichols_chart_step"
17205            | "servo_position_velocity" | "servo_torque_step"
17206            | "imu_madgwick_step" | "imu_mahony_step" | "quaternion_from_imu"
17207            | "denavit_hartenberg_h" | "forward_kinematics_dh"
17208            | "inverse_kinematics_2link" | "jacobian_2dof"
17209            | "manipulability_yoshikawa" | "singularity_check_2link"
17210            | "path_dubins_lsl" | "path_dubins_rsr" | "path_reeds_shepp"
17211            | "rrt_extend" | "rrt_star_rewire" | "prm_node_connect"
17212
17213            // ── actuarial science ───────────────────────────────
17214            | "life_expectancy_e0" | "force_of_mortality" | "select_ultimate"
17215            | "annuity_due_an" | "annuity_immediate_an"
17216            | "term_life_a_n_t" | "whole_life_a"
17217            | "endowment_pure_e" | "endowment_combined_a"
17218            | "premium_net" | "level_premium"
17219            | "reserve_prospective" | "reserve_retrospective"
17220            | "gross_premium_load" | "experience_factor"
17221            | "mortality_table_q" | "select_period_step"
17222            | "multi_decrement_q" | "multi_state_pij"
17223            | "credibility_buhlmann" | "loss_severity_lognormal"
17224            | "loss_frequency_poisson" | "ruin_probability_lundberg"
17225            | "cramer_lundberg_step" | "bornhuetter_ferguson"
17226            | "chain_ladder_step" | "ibnr_estimate" | "run_off_triangle_step"
17227
17228            // ── epidemiology / public health ────────────────────
17229            | "r_naught_basic" | "r_effective_t" | "doubling_time_growth"
17230            | "sirs_step" | "seirs_step" | "susceptible_to_infected"
17231            | "attack_rate" | "vaccination_coverage_required"
17232            | "cfr_case_fatality" | "ifr_infection_fatality"
17233            | "dalys_disability_weight" | "qaly_lifetime" | "ylll_pml"
17234            | "rt_serial_interval" | "generation_time_step"
17235            | "gini_inequality_health" | "standardized_mortality_smr"
17236            | "indirect_age_adjusted" | "direct_age_adjusted"
17237            | "odds_ratio_2x2" | "risk_ratio_2x2" | "number_needed_to_treat"
17238            | "attributable_fraction_pop" | "preventive_fraction"
17239            | "contact_tracing_eff" | "cluster_attack_rate"
17240            | "transmission_pair_index"
17241
17242            // ── archive/encoding format primitives ──────────────
17243            | "tar_header_checksum" | "tar_pad_512" | "tar_member_record"
17244            | "zip_local_header" | "zip_central_dir" | "zip_eocd"
17245            | "gzip_member_step" | "gzip_crc32_init" | "gzip_isize"
17246            | "deflate_dynamic_huffman" | "deflate_static_block"
17247            | "lz4_block_step" | "lz4_match_offset"
17248            | "zstd_frame_header" | "brotli_huffman_table"
17249            | "brotli_meta_block" | "lzma_range_step"
17250            | "quoted_printable_encode" | "uuencode_step"
17251            | "modhex_encode" | "percent_encode_full"
17252            | "punycode_encode" | "idn_to_ascii" | "idn_to_unicode"
17253            | "msgpack_pack_int" | "msgpack_pack_str"
17254            | "cbor_encode_uint" | "cbor_encode_str"
17255
17256            // ── chemistry & biochemistry ────────────────────────
17257            | "molecular_weight_compound" | "molarity_dilution"
17258            | "gas_constant_value" | "eyring_rate" | "van_t_hoff_kp"
17259            | "henderson_buffer" | "titration_ph_endpoint"
17260            | "isoelectric_point_protein" | "ka_to_pka" | "pkb_to_kb"
17261            | "amphoteric_check" | "oxidation_number"
17262            | "half_reaction_balance" | "redox_potential_cell"
17263            | "electrolysis_mass" | "spectrophotometer_beer_lambert"
17264            | "epsilon_extinction" | "transmittance_to_a"
17265            | "crystal_field_ligand" | "jahn_teller_check"
17266            | "vsepr_geometry" | "lewis_dot_count"
17267            | "formal_charge" | "resonance_count"
17268            | "ramachandran_phi_psi" | "rg_radius_of_gyration"
17269            | "spectroscopic_factor" | "avogadro_constant"
17270
17271            // ── music theory ────────────────────────────────────
17272            | "cents_between_freqs" | "note_name_from_midi"
17273            | "interval_quality_size" | "scale_pitches_major"
17274            | "scale_pitches_minor" | "mode_pitches_dorian"
17275            | "mode_pitches_phrygian" | "mode_pitches_lydian"
17276            | "chord_root_inversion" | "chord_quality_classify"
17277            | "chord_voicing_close" | "key_signature_sharps"
17278            | "key_signature_flats" | "tempo_to_ms" | "beat_to_seconds"
17279            | "time_sig_subdivision" | "equal_tempered_freq"
17280            | "just_intonation_freq" | "pythagorean_freq"
17281            | "mean_tone_freq" | "werckmeister_iii" | "kirnberger_iii"
17282            | "dynamics_db_level" | "harmonics_partial"
17283
17284            // ── geology, seismology, mineralogy ─────────────────
17285            | "moment_magnitude_mw" | "richter_local_ml"
17286            | "surface_wave_ms" | "body_wave_mb"
17287            | "gutenberg_richter_b" | "omori_aftershock"
17288            | "pga_attenuation" | "arias_intensity" | "shake_map_pga"
17289            | "liquefaction_potential_index" | "spt_n_correction"
17290            | "mineral_mohs_hardness" | "streak_color_index"
17291            | "specific_gravity_water" | "feldspar_classify"
17292            | "silicate_classify" | "igneous_qapf"
17293            | "metamorphic_grade" | "crustal_density_depth"
17294            | "pwave_velocity_depth" | "swave_velocity_depth"
17295            | "gradient_geothermal" | "heat_flow_radiogenic"
17296
17297            // ── BLAS / LAPACK ───────────────────────────────────
17298            | "dgemm" | "sgemm" | "zgemm" | "cgemm"
17299            | "dgemv" | "sgemv" | "dtrsm" | "strsm"
17300            | "dgesv" | "dgetrf" | "dgeqrf" | "dgesvd"
17301            | "dsyevd" | "dpotrf" | "daxpy" | "ddot"
17302            | "dnrm2" | "dscal" | "dasum" | "idamax"
17303            | "dsyrk" | "dgerqf" | "dorgqr" | "dorglq"
17304            | "drot" | "drotg" | "dpbsv" | "dgbsv"
17305            | "dtbsv" | "dtrsv" | "ddrot" | "dgemm3m"
17306            | "dgels" | "dgelsd"
17307
17308            // ── logic, proof, SAT/SMT, type theory ──────────────
17309            | "cnf_unit_propagate" | "cnf_pure_literal_elim"
17310            | "cnf_dpll_branch" | "dpll_clause_learning"
17311            | "two_watched_literals" | "walksat_step"
17312            | "resolution_step" | "subsumption_check"
17313            | "tableau_branch_close" | "sequent_left_intro"
17314            | "sequent_right_intro" | "nbe_normalize"
17315            | "church_numeral_n" | "encode_pair" | "encode_succ"
17316            | "simply_typed_check" | "hindley_milner_step"
17317            | "unification_robinson" | "bdd_apply" | "bdd_restrict"
17318            | "bdd_quantify" | "aig_simplify_step"
17319            | "smt_qf_lia_solve_step" | "smt_qf_uf_combine"
17320            | "model_checking_ctl" | "model_checking_ltl"
17321            | "bisimulation_step" | "coq_tactic_apply"
17322            | "coq_unify_term" | "refl_check" | "sym_check" | "trans_check"
17323
17324            // ── compilers / parsing ─────────────────────────────
17325            | "nfa_to_dfa" | "subset_construction"
17326            | "dfa_minimize_hopcroft" | "regex_to_nfa_thompson"
17327            | "glushkov_construction" | "brzozowski_derivative"
17328            | "ll1_first_set" | "ll1_follow_set" | "ll1_predict_table"
17329            | "lr0_items_step" | "lalr_lookahead_compute"
17330            | "lr1_canonical_collection"
17331            | "earley_scan" | "earley_predict" | "earley_complete"
17332            | "packrat_parse_step" | "ascent_parser_step"
17333            | "pratt_parse_step" | "shunting_yard_step"
17334            | "regex_compile_thompson" | "regex_match_dfa"
17335            | "lex_keyword_classify"
17336            | "peg_seq" | "peg_choice" | "peg_repeat" | "peg_lookahead"
17337            | "dfa_simulate_step" | "bytecode_disasm_step"
17338            | "ssa_phi_insert" | "dom_tree_idom" | "dominance_frontier"
17339
17340            // ── computational linguistics ───────────────────────
17341            | "porter_stem_step" | "snowball_stem_english"
17342            | "snowball_stem_french" | "lemmatize_wordnet"
17343            | "lemmatize_lemmy" | "stem_lancaster"
17344            | "soundex_phonetic" | "metaphone_phonetic"
17345            | "caverphone_2" | "nysiis_phonetic"
17346            | "match_rating_codex" | "daitch_mokotoff"
17347            | "viterbi_pos_tag" | "forward_backward_pos"
17348            | "crf_log_likelihood" | "bigram_perplexity"
17349            | "trigram_perplexity" | "ner_bilou_decode"
17350            | "constituency_cyk" | "dependency_parse_eisner"
17351            | "transition_arc_eager" | "transition_arc_standard"
17352            | "word_alignment_ibm1" | "word_alignment_ibm2"
17353            | "lexicalized_parse" | "coreference_singleton"
17354            | "anaphora_distance" | "head_finding_collins"
17355            | "tree_kernel_collins"
17356
17357            // ── Postgres SQL strings, JSON, aggregates ─────────
17358            | "btrim" | "translate" | "ascii"
17359            | "regexp_split" | "regexp_matches" | "regexp_replace"
17360            | "json_build_object" | "jsonb_set"
17361            | "json_array_length" | "json_extract_path"
17362            | "json_strip_nulls" | "jsonb_pretty"
17363            | "jsonb_path_query" | "json_each"
17364            | "jsonb_array_length" | "jsonb_object_keys"
17365            | "jsonb_typeof" | "array_to_jsonb"
17366            | "ts_match" | "ts_rank" | "ts_headline"
17367            | "substring_similarity" | "levenshtein_dist"
17368            | "word_similarity" | "strict_word_similarity"
17369            | "hstore_to_array" | "array_to_hstore"
17370            | "string_agg" | "array_agg"
17371            | "corr_agg" | "covar_pop" | "covar_samp"
17372            | "regr_slope" | "regr_intercept" | "regr_r2"
17373            | "percentile_cont" | "percentile_disc" | "mode_agg"
17374            | "array_to_string" | "array_position" | "array_positions"
17375            | "array_remove" | "array_replace"
17376            | "xmlforest" | "xmlagg"
17377
17378            // ── Redis-flavour primitives ────────────────────────
17379            | "zadd" | "zrem" | "zrangebyscore"
17380            | "zrank" | "zrevrank" | "zincrby"
17381            | "zcard" | "zcount" | "zlexcount"
17382            | "lpush" | "rpush" | "lrange" | "lrem"
17383            | "hset" | "hget" | "hgetall" | "hlen"
17384            | "hkeys" | "hvals" | "hmset" | "hincrby"
17385            | "sadd" | "srem" | "smembers"
17386            | "sinter" | "sunion" | "sdiff"
17387            | "scard" | "sismember" | "spop"
17388            | "setex" | "setnx" | "expire"
17389            | "ttl" | "pttl" | "persist"
17390            | "incr" | "decr" | "incrby" | "decrby"
17391            | "getset" | "mset" | "mget" | "renamenx"
17392            | "dbsize" | "type_redis" | "exists_key"
17393            | "strlen" | "getrange" | "setrange" | "append_redis"
17394            | "bitcount" | "bitop" | "bitpos"
17395            | "pfadd" | "pfcount"
17396            | "geoadd" | "geodist" | "geohash"
17397            | "xadd" | "xlen" | "xrange"
17398            | "object_encoding" | "debug_object" | "cluster_slots"
17399
17400            // ── NumPy + scipy.special ──────────────────────────
17401            | "argpartition" | "bincount" | "nonzero_count"
17402            | "flatnonzero" | "searchsorted" | "digitize"
17403            | "histogram_bin_edges" | "unique_count"
17404            | "polyfit_rmse"
17405            | "ellipk" | "ellipe"
17406            | "hyp1f1" | "hyp2f1" | "mathieu_b"
17407            | "spherical_jn" | "spherical_yn"
17408            | "jv" | "yn" | "iv" | "kv"
17409            | "airyai" | "airybi"
17410            | "polygamma" | "trigamma" | "loggamma"
17411            | "factorial2" | "factorialk"
17412            | "owens_t" | "marcum_q" | "voigt_profile"
17413            | "chebyt" | "chebyu" | "sph_harm"
17414            | "wofz" | "erfcx" | "erfi" | "dawsn"
17415            | "interp1d"
17416            | "convolve_full" | "convolve_valid" | "correlate_full"
17417            | "kron_product"
17418            | "simpson_rule" | "romberg_quad" | "fixed_quad"
17419            | "ode45_step" | "ode_lsoda" | "solve_ivp_step"
17420            | "root_brentq" | "root_newton" | "root_secant"
17421            | "fmin_powell" | "fmin_cobyla"
17422
17423            // ── economics + game theory ─────────────────────────
17424            | "cobb_douglas" | "ces_production"
17425            | "leontief_input" | "leontief_output"
17426            | "slutsky_decompose"
17427            | "marshallian_demand" | "hicksian_demand"
17428            | "expenditure_function" | "indirect_utility"
17429            | "gale_shapley_step" | "deferred_acceptance"
17430            | "top_trading_cycle" | "vcg_payment" | "myerson_optimal"
17431            | "gini_market" | "hhi_concentration"
17432            | "cournot_eq" | "stackelberg_eq" | "bertrand_eq"
17433            | "monopoly_lerner"
17434            | "consumer_surplus" | "producer_surplus"
17435            | "deadweight_loss" | "tax_incidence"
17436            | "pareto_efficiency" | "edgeworth_box_alloc"
17437            | "social_welfare_utilitarian"
17438            | "social_welfare_rawls" | "social_welfare_nash"
17439            | "arrow_independence"
17440            | "vickrey_auction" | "first_price_seal"
17441            | "english_auction" | "dutch_auction"
17442            | "core_coalition" | "stable_matching_count"
17443            | "gale_optimal" | "pareto_dominance"
17444            | "lerner_index"
17445            | "price_elasticity" | "supply_elasticity"
17446            | "income_elasticity" | "engel_curve" | "cross_elasticity"
17447            | "diff_in_diff" | "did_estimator" | "rdd_estimate"
17448            // ── SciPy.signal — DSP filters, windows, transforms ──
17449            | "hann_w" | "hamming_w" | "blackman_w" | "barthann_w"
17450            | "nuttall_w" | "flattop_w" | "parzen_window" | "tukey_w"
17451            | "taylor_window" | "dpss_window" | "kaiserord_step"
17452            | "butter_lp_re" | "butter_hp_mag"
17453            | "cheby1_lp" | "cheby2_lp" | "ellip_lp" | "bessel_lp"
17454            | "notch_filter"
17455            | "sosfilt_step" | "lfilter_zi_init" | "filtfilt_pad"
17456            | "freqz_eval" | "freqs_eval" | "group_delay_eval"
17457            | "impulse_response_n"
17458            | "tf2zpk_step" | "zpk2tf_step" | "tf2sos_step"
17459            | "zpk2sos_step" | "sos2tf_step"
17460            | "bilinear_xform" | "bilinear_zpk_xform"
17461            | "firwin_lowpass" | "firwin_highpass"
17462            | "firwin_bandpass" | "firwin_bandstop"
17463            | "firwin2_freq" | "remez_design"
17464            | "stft_step" | "istft_step"
17465            | "cwt_morlet" | "ricker_wavelet" | "mexican_hat_wavelet"
17466            | "coherence_xy" | "csd_xy" | "welch_psd_avg"
17467            | "periodogram_basic" | "lombscargle_freq"
17468            | "hilbert_signal" | "envelope_amplitude"
17469            | "deconvolve_step" | "fftconvolve_step" | "oaconvolve_step"
17470            | "upfirdn_step" | "resample_poly_step" | "decimate_step"
17471            | "savgol_coef" | "detrend_linear"
17472            | "wiener_filter" | "medfilt_1d" | "peak_widths_at"
17473            // ── NetworkX graph algorithms ───────────────────────
17474            | "dijkstra_relax" | "bellman_ford_relax"
17475            | "floyd_warshall_step" | "johnson_reweight"
17476            | "astar_search" | "bidirectional_dijkstra"
17477            | "yen_k_shortest" | "ida_star"
17478            | "bfs_count" | "dfs_postorder_done" | "topo_kahn_step"
17479            | "tarjan_scc_step" | "kosaraju_step"
17480            | "kruskal_step" | "prim_step" | "boruvka_step"
17481            | "reverse_delete_step"
17482            | "ford_fulkerson_step" | "edmonds_karp_bfs"
17483            | "dinic_step" | "push_relabel_relabel"
17484            | "stoer_wagner_step" | "karger_step"
17485            | "pagerank_iter" | "hits_authority" | "hits_hub"
17486            | "personalized_pagerank"
17487            | "centrality_degree" | "centrality_closeness"
17488            | "centrality_betweenness" | "centrality_eigenvector"
17489            | "centrality_katz" | "harmonic_centrality" | "load_centrality"
17490            | "clustering_coefficient" | "triangles_count" | "transitivity"
17491            | "modularity_score" | "louvain_gain"
17492            | "label_propagation" | "girvan_newman"
17493            | "articulation_point" | "bridge_edge"
17494            | "edge_connectivity" | "vertex_connectivity"
17495            | "biconnected_components"
17496            | "gx_diameter" | "gx_radius" | "gx_eccentricity"
17497            | "warshall_step"
17498            | "tsp_held_karp" | "tsp_nn_step" | "tsp_christofides"
17499            | "graph_coloring_greedy" | "welsh_powell"
17500            | "vf2_consistent" | "subgraph_isomorphism"
17501            | "hungarian_step" | "hopcroft_karp_step"
17502            | "bron_kerbosch"
17503            | "min_vertex_cover" | "max_independent_set"
17504            | "dominating_set_greedy" | "hamiltonian_path"
17505            | "min_steiner_tree" | "k_shortest_spanning"
17506            | "random_walk_hitting" | "simrank"
17507            // ── Pandas DataFrame ops ────────────────────────────
17508            | "df_groupby" | "df_aggregate" | "df_apply"
17509            | "df_transform" | "df_pivot" | "df_pivot_table"
17510            | "df_melt" | "df_stack" | "df_unstack"
17511            | "df_explode" | "df_get_dummies" | "df_crosstab"
17512            | "df_merge" | "df_join" | "df_concat"
17513            | "df_resample" | "df_rolling" | "df_expanding"
17514            | "df_ewm" | "df_shift" | "df_diff"
17515            | "df_pct_change" | "df_corr" | "df_cov"
17516            | "df_corrwith" | "df_describe" | "df_kurtosis"
17517            | "df_skew" | "df_sem" | "df_mad"
17518            | "df_dropna" | "df_fillna" | "df_interpolate"
17519            | "df_replace" | "df_isnull" | "df_notnull"
17520            | "df_sort_values" | "df_rank" | "df_quantile"
17521            | "df_value_counts" | "df_sample" | "df_nlargest"
17522            | "df_nsmallest" | "df_idxmax" | "df_idxmin"
17523            | "df_clip" | "df_round" | "df_to_datetime"
17524            | "df_to_timedelta" | "df_to_numeric" | "df_eval"
17525            | "df_query" | "df_filter" | "df_drop_duplicates"
17526            | "df_duplicated" | "df_set_index" | "df_reset_index"
17527            // ── PIL/OpenCV image processing ─────────────────────
17528            | "image_resize" | "image_grayscale" | "image_threshold"
17529            | "image_blur_gaussian" | "image_blur_box" | "image_sharpen"
17530            | "image_edge_canny" | "image_edge_sobel" | "image_edge_laplacian"
17531            | "image_dilate" | "image_erode" | "image_morphology_open"
17532            | "image_morphology_close" | "image_histogram" | "image_equalize"
17533            | "image_clahe" | "image_contrast" | "image_brightness"
17534            | "image_gamma" | "image_invert" | "image_sepia"
17535            | "image_posterize" | "image_solarize" | "convolve_2d"
17536            | "filter_median" | "filter_bilateral" | "filter_nlmeans"
17537            | "gabor_filter" | "hog_features" | "harris_corners"
17538            | "shi_tomasi_corners" | "sift_keypoints" | "orb_keypoints"
17539            | "surf_keypoints" | "template_match" | "face_detect_haar"
17540            | "watershed_segment" | "slic_superpixels" | "felzenszwalb_segment"
17541            | "graph_cut_segment" | "hough_lines" | "hough_circles"
17542            | "ransac_homography" | "optical_flow_lk" | "optical_flow_farneback"
17543            | "corner_subpix" | "image_rotate" | "image_flip_h"
17544            | "image_flip_v" | "image_emboss" | "image_motion_blur"
17545            // ── statsmodels ─
17546            | "arima_fit" | "arima_forecast" | "arma_order_select"
17547            | "sarimax_fit" | "garch_fit" | "ewma_smooth"
17548            | "holt_winters_additive" | "holt_winters_multiplicative" | "kalman_filter_step"
17549            | "kalman_smoother_step" | "var_fit" | "vecm_fit"
17550            | "johansen_test" | "phillips_perron" | "adfuller"
17551            | "kpss_test" | "breusch_godfrey" | "ljung_box_q"
17552            | "durbin_watson_d" | "granger_causality" | "cointegration_eg"
17553            | "seasonal_decompose" | "stl_decompose" | "acf_basis"
17554            | "pacf_basis" | "moving_average_filter" | "exp_smooth_simple"
17555            | "exp_smooth_double" | "markov_switching_ar" | "markov_switching_mr"
17556            | "arch_lm" | "state_space_kalman" | "ucm_unobserved_components"
17557            | "spectral_density_estimate" | "bayesian_step" | "pivoted_cholesky_var"
17558            // ── sklearn ─
17559            | "sk_logistic_predict" | "sk_logistic_fit" | "sk_random_forest_fit"
17560            | "sk_gbt_fit" | "sk_xgb_fit" | "sk_lightgbm_fit"
17561            | "sk_svm_fit" | "sk_kmeans_fit" | "sk_dbscan_fit"
17562            | "sk_agglomerative_fit" | "sk_pca_fit" | "sk_tsne_fit"
17563            | "sk_umap_fit" | "sk_isolation_forest_fit" | "sk_lof_fit"
17564            | "sk_kfold_split" | "sk_stratified_kfold" | "sk_cross_val_score"
17565            | "sk_grid_search" | "sk_random_search" | "sk_bayes_search"
17566            | "sk_pipeline_fit" | "sk_standard_scaler" | "sk_min_max_scaler"
17567            | "sk_robust_scaler" | "sk_quantile_transform" | "sk_power_transform"
17568            | "sk_one_hot" | "sk_ordinal_encode" | "sk_label_encode"
17569            | "sk_tfidf" | "sk_count_vectorize" | "sk_silhouette"
17570            | "sk_calinski_harabasz" | "sk_davies_bouldin" | "sk_adjusted_rand"
17571            | "sk_mutual_info" | "sk_lda_topic" | "sk_nmf_topic"
17572            | "sk_word2vec_train" | "sk_doc2vec_train" | "sk_naive_bayes_predict"
17573            | "sk_knn_predict" | "sk_decision_tree_split"
17574            // ── quantum ─
17575            | "qubit_x" | "qubit_y" | "qubit_z"
17576            | "qubit_h" | "qubit_s" | "qubit_t"
17577            | "qubit_rx" | "qubit_ry" | "qubit_rz"
17578            | "qubit_u3" | "qubit_u2" | "qubit_u1"
17579            | "qubit_phase" | "qubit_cnot" | "qubit_cz"
17580            | "qubit_swap" | "qubit_ccx" | "qubit_measure"
17581            | "qubit_reset" | "bell_state" | "ghz_state"
17582            | "w_state" | "qft" | "inverse_qft"
17583            | "grover_iter" | "shor_period" | "vqe_step"
17584            | "qaoa_step" | "qpe_iteration" | "pauli_string_expect"
17585            | "circuit_depth" | "circuit_width" | "gate_decompose"
17586            | "ancilla_alloc" | "bloch_sphere_x" | "bloch_sphere_z"
17587            | "density_matrix_purity_q" | "entanglement_entropy" | "quantum_teleportation"
17588            | "superdense_coding" | "noise_model_depolarize"
17589            // ── b81-misc-utility ─
17590            | "mirr_excel" | "accrint" | "cumipmt"
17591            | "cumprinc" | "dollarde" | "dollarfr"
17592            | "received" | "yieldmat" | "yielddisc"
17593            | "duration_macaulay" | "mduration" | "odddyield"
17594            | "disc_excel" | "effect" | "nominal"
17595            | "intrate" | "price_disc" | "cityhash64"
17596            | "farmhash_64" | "metro_hash_64" | "spookyhash_128"
17597            | "t1ha" | "highway_hash" | "fnv0_32"
17598            | "lose_lose"
17599            | "oat_hash" | "lz4_encode_block" | "snappy_encode"
17600            | "zstd_encode_step" | "brotli_encode_meta" | "lzma_encode_step"
17601            | "bz2_encode_step" | "lzo_encode_step" | "deflate_encode_huffman"
17602            | "lzw_encode" | "gzip_encode_step" | "uri_template_expand"
17603            | "uri_resolve" | "uri_normalize" | "percent_decode_url"
17604            | "url_encode_form" | "url_decode_form" | "punycode_decode_step"
17605            | "idn_normalize" | "url_origin" | "etag_validate"
17606            | "cache_control_parse" | "vary_match" | "content_negotiate"
17607            | "accept_lang_pick" | "range_header_parse" | "if_match_check"
17608            | "if_none_match_check" | "digest_auth_quote" | "www_auth_parse"
17609            // ── b82-misc-utility ─
17610            | "iso8601_duration_parse" | "iso8601_duration_to_seconds" | "rrule_next_occurrence"
17611            | "cron_next_fire" | "date_round_iso" | "week_number_iso"
17612            | "fiscal_year_us" | "age_at_date" | "easter_western"
17613            | "easter_orthodox_year_2" | "chinese_new_year" | "solstice_winter"
17614            | "equinox_spring" | "rgb_to_oklab" | "oklab_to_rgb"
17615            | "rgb_to_cmyk" | "cmyk_to_rgb" | "rgb_to_xyz"
17616            | "xyz_to_rgb" | "rgb_to_yuv" | "yuv_to_rgb"
17617            | "luminance_relative" | "contrast_ratio" | "wcag_pass"
17618            | "color_temperature_kelvin" | "delta_e76" | "delta_e94"
17619            | "delta_e2000" | "color_blend_alpha" | "isbn10_check"
17620            | "isbn13_check" | "ean13_check" | "upc_check"
17621            | "eth_addr_check" | "btc_addr_check" | "ssn_check"
17622            | "vin_check" | "imei_check" | "iban_check"
17623            | "cusip_check" | "kde_silverman_bw" | "kde_scott_bw"
17624            | "kde_bandwidth_lscv" | "kde_epanechnikov" | "kde_gaussian_2d"
17625            | "kde_uniform" | "kde_triangular" | "kde_biweight"
17626            | "kde_triweight" | "kde_cosine" | "kde_logistic_kernel"
17627            // ── number theory (extended) ──────────────────────────────────
17628            | "mod_exp" | "modexp" | "powmod"
17629            | "mod_inv" | "modinv" | "chinese_remainder" | "crt"
17630            | "miller_rabin" | "millerrabin" | "is_probable_prime"
17631            // ── combinatorics (extended) ──────────────────────────────────
17632            | "derangements" | "stirling2" | "stirling_second"
17633            | "bernoulli_number" | "bernoulli" | "harmonic_number" | "harmonic"
17634            // ── physics (new) ─────────────────────────────────────────────
17635            | "drag_force" | "fdrag" | "ideal_gas" | "pv_nrt"
17636            // ── financial greeks & risk ───────────────────────────────────
17637            | "bs_delta" | "bsdelta" | "option_delta"
17638            | "bs_gamma" | "bsgamma" | "option_gamma"
17639            | "bs_vega" | "bsvega" | "option_vega"
17640            | "bs_theta" | "bstheta" | "option_theta"
17641            | "bs_rho" | "bsrho" | "option_rho"
17642            | "bond_duration" | "mac_duration"
17643            // ── DSP extensions ────────────────────────────────────────────
17644            | "dct" | "idct" | "goertzel" | "chirp" | "chirp_signal"
17645            // ── encoding extensions ───────────────────────────────────────
17646            | "base85_encode" | "b85e" | "ascii85_encode" | "a85e"
17647            | "base85_decode" | "b85d" | "ascii85_decode" | "a85d"
17648            // ── R base: distributions ─────────────────────────────────────
17649            | "pnorm" | "pbinom" | "dbinom" | "ppois"
17650            | "punif" | "pexp" | "pweibull" | "plnorm" | "pcauchy"
17651            // ── R base: matrix ops ────────────────────────────────────────
17652            | "rbind" | "cbind"
17653            | "row_sums" | "rowSums" | "col_sums" | "colSums"
17654            | "row_means" | "rowMeans" | "col_means" | "colMeans"
17655            | "outer" | "crossprod" | "tcrossprod"
17656            | "nrow" | "ncol" | "prop_table" | "proptable"
17657            // ── R base: vector ops ────────────────────────────────────────
17658            | "cummax" | "cummin" | "scale_vec" | "scale"
17659            | "which_fn" | "tabulate"
17660            | "duplicated" | "duped" | "rev_vec"
17661            | "seq_fn" | "rep_fn" | "rep"
17662            | "cut_bins" | "cut" | "find_interval" | "findInterval"
17663            | "ecdf_fn" | "ecdf" | "density_est" | "density"
17664            | "embed_ts" | "embed"
17665            // ── R base: stats tests ───────────────────────────────────────
17666            | "shapiro_test" | "shapiro" | "ks_test" | "ks"
17667            | "wilcox_test" | "wilcox" | "mann_whitney"
17668            | "prop_test" | "proptest" | "binom_test" | "binomtest"
17669            // ── R base: apply / functional ────────────────────────────────
17670            | "sapply" | "tapply" | "do_call" | "docall"
17671            // ── R base: ML / clustering ───────────────────────────────────
17672            | "kmeans" | "prcomp" | "pca"
17673            // ── R base: random generators ─────────────────────────────────
17674
17675
17676
17677            // ── R base: quantile functions ────────────────────────────────
17678
17679            // ── R base: additional CDFs ───────────────────────────────────
17680            | "pgamma" | "pbeta" | "pchisq" | "pt_cdf" | "pt" | "pf_cdf" | "pf"
17681            // ── R base: additional PMFs ───────────────────────────────────
17682            | "dgeom" | "dunif" | "dnbinom" | "dhyper"
17683            // ── R base: smoothing / interpolation ─────────────────────────
17684            | "lowess" | "loess" | "approx_fn" | "approx"
17685            // ── R base: linear models ─────────────────────────────────────
17686            | "lm_fit" | "lm"
17687            // ── R base: remaining quantiles ───────────────────────────────
17688 | "qt_fn" | "qf_fn"
17689
17690            // ── R base: time series ───────────────────────────────────────
17691            | "acf_fn" | "acf" | "pacf_fn" | "pacf"
17692            | "diff_lag" | "diff_ts" | "ts_filter" | "filter_ts"
17693            // ── R base: regression diagnostics ────────────────────────────
17694            | "predict_lm" | "predict" | "confint_lm" | "confint"
17695            // ── R base: multivariate stats ────────────────────────────────
17696            | "cor_matrix" | "cor_mat" | "cov_matrix" | "cov_mat"
17697            | "mahalanobis" | "mahal" | "dist_matrix" | "dist_mat"
17698            | "hclust" | "cutree" | "weighted_var" | "wvar" | "cov2cor"
17699            // ── SVG plotting ──────────────────────────────────────────────
17700            | "scatter_svg" | "scatter_plot" | "line_svg" | "line_plot"
17701            | "plot_svg" | "hist_svg" | "histogram_svg"
17702            | "boxplot_svg" | "box_plot" | "bar_svg" | "barchart_svg"
17703            | "pie_svg" | "pie_chart" | "heatmap_svg" | "heatmap"
17704            | "donut_svg" | "donut" | "area_svg" | "area_chart"
17705            | "hbar_svg" | "hbar" | "radar_svg" | "radar" | "spider"
17706            | "candlestick_svg" | "candlestick" | "ohlc"
17707            | "violin_svg" | "violin" | "cor_heatmap" | "cor_matrix_svg"
17708            | "stacked_bar_svg" | "stacked_bar"
17709            | "wordcloud_svg" | "wordcloud" | "wcloud"
17710            | "treemap_svg" | "treemap"
17711            | "pvw"
17712            // ── Cyberpunk terminal art ────────────────────────────────
17713            | "cyber_city" | "cyber_grid" | "cyber_rain" | "matrix_rain"
17714            | "cyber_glitch" | "glitch_text" | "cyber_banner" | "neon_banner"
17715            | "cyber_circuit" | "cyber_skull" | "cyber_eye"
17716            // ── AI primitives (docs/AI_PRIMITIVES.md) ─────────────────
17717            | "ai" | "ai_agent" | "prompt" | "stream_prompt" | "stream_prompt_cb"
17718            | "tokens_of"
17719            | "ai_estimate" | "ai_cost" | "ai_history" | "ai_history_clear"
17720            | "ai_cache_clear" | "ai_cache_size"
17721            | "ai_mock_install" | "ai_mock_clear"
17722            | "ai_config_get" | "ai_config_set" | "ai_routing_get" | "ai_routing_set"
17723            | "ai_register_tool" | "ai_unregister_tool" | "ai_clear_tools" | "ai_tools_list"
17724            | "ai_filter" | "ai_map" | "ai_classify" | "ai_match" | "ai_sort" | "ai_dedupe"
17725            | "ai_extract" | "ai_summarize" | "ai_translate" | "ai_template"
17726            | "ai_session_new" | "ai_session_send" | "ai_session_history"
17727            | "ai_session_close" | "ai_session_reset"
17728            | "ai_session_export" | "ai_session_import"
17729            | "ai_memory_save" | "ai_memory_recall" | "ai_memory_forget"
17730            | "ai_memory_count" | "ai_memory_clear"
17731            | "ai_vision" | "ai_pdf" | "ai_grounded" | "ai_citations"
17732            | "ai_transcribe" | "ai_speak" | "ai_image" | "ai_image_edit" | "ai_image_variation"
17733            | "ai_models" | "ai_describe" | "ai_pricing" | "ai_dashboard"
17734            | "ai_moderate" | "ai_chunk" | "ai_warm" | "ai_compare"
17735            | "ai_last_thinking" | "ai_budget" | "ai_batch" | "ai_pmap"
17736            | "ai_file_upload" | "ai_file_list" | "ai_file_get" | "ai_file_delete"
17737            | "ai_file_anthropic_upload" | "ai_file_anthropic_list" | "ai_file_anthropic_delete"
17738            | "vec_cosine" | "vec_search" | "vec_topk"
17739            // ── AI tool specs ────────────────────────────────────────
17740            | "web_search_tool" | "fetch_url_tool" | "read_file_tool" | "run_code_tool"
17741            // ── MCP (Model Context Protocol) ─────────────────────────
17742            | "mcp_connect" | "mcp_close" | "mcp_tools" | "mcp_call"
17743            | "mcp_resource" | "mcp_resources" | "mcp_prompt" | "mcp_prompts"
17744            | "mcp_attach_to_ai" | "mcp_detach_from_ai" | "mcp_attached"
17745            | "mcp_server_start" | "mcp_serve_registered_tools"
17746            // ── PTY / expect (docs/expect-feature-idea.md) ────────────
17747            | "pty_spawn" | "pty_send" | "pty_read" | "pty_expect" | "pty_expect_table"
17748            | "pty_buffer" | "pty_alive" | "pty_eof" | "pty_close" | "pty_interact"
17749            | "pty_strip_ansi" | "pty_after_eof" | "pty_pending_events"
17750            // ── Stress / telemetry extensions ─────────────────────────
17751            | "stress_fp" | "stress_int" | "stress_cache" | "stress_branch"
17752            | "stress_sort" | "stress_alloc" | "stress_mmap" | "stress_disk"
17753            | "stress_iops" | "stress_net" | "stress_http" | "stress_dns"
17754            | "stress_fork" | "stress_thread" | "stress_aes" | "stress_compress"
17755            | "stress_regex" | "stress_json" | "stress_burst" | "stress_ramp"
17756            | "stress_oscillate" | "stress_all" | "stress_temp" | "stress_thermal_zones"
17757            | "stress_freq" | "stress_throttled" | "stress_load" | "stress_meminfo"
17758            | "stress_cores" | "stress_arm_kill_switch" | "stress_killed"
17759            | "stress_disarm_kill_switch"
17760            | "stress_metrics_record" | "stress_metrics_clear" | "stress_metrics_count"
17761            | "stress_metrics_export" | "stress_metrics_prometheus"
17762            | "stress_metrics_json" | "stress_metrics_csv" | "stress_metrics_watch"
17763            // ── Compliance / secrets ─────────────────────────────────
17764            | "audit_log" | "audit_log_path"
17765            | "secrets_encrypt" | "secrets_decrypt" | "secrets_random_key" | "secrets_kdf"
17766            // ── Web framework (docs/WEB_FRAMEWORK.md) ─────────────────
17767            | "web_route" | "web_resources" | "web_root" | "web_routes_table"
17768            | "web_application_config" | "web_boot_application"
17769            | "web_render" | "web_render_partial" | "web_redirect"
17770            | "web_json" | "web_text" | "web_csv" | "web_markdown"
17771            | "web_params" | "web_request" | "web_set_header" | "web_status"
17772            | "web_before_action" | "web_after_action"
17773            | "web_session" | "web_session_set" | "web_session_get" | "web_session_clear"
17774            | "web_signed" | "web_unsigned"
17775            | "web_cookies" | "web_set_cookie"
17776            | "web_flash" | "web_flash_set" | "web_flash_get"
17777            | "web_validate" | "web_permit"
17778            | "web_password_hash" | "web_password_verify"
17779            | "web_token_for" | "web_token_consume" | "web_csrf_meta_tag"
17780            | "web_security_headers" | "web_can"
17781            | "web_h" | "web_truncate" | "web_pluralize" | "web_time_ago_in_words"
17782            | "web_image_tag" | "web_link_to" | "web_button_to"
17783            | "web_form_with" | "web_form_close"
17784            | "web_text_field" | "web_text_area" | "web_check_box"
17785            | "web_stylesheet_link_tag" | "web_javascript_link_tag"
17786            | "web_yield_content" | "web_content_for"
17787            | "web_etag" | "web_cache_get" | "web_cache_set"
17788            | "web_cache_delete" | "web_cache_clear"
17789            | "web_db_connect" | "web_db_execute" | "web_db_query"
17790            | "web_db_begin" | "web_db_commit" | "web_db_rollback"
17791            | "web_create_table" | "web_drop_table"
17792            | "web_add_column" | "web_remove_column"
17793            | "web_migrate" | "web_rollback"
17794            | "web_model_all" | "web_model_find" | "web_model_first" | "web_model_last"
17795            | "web_model_where" | "web_model_create" | "web_model_update"
17796            | "web_model_destroy" | "web_model_count" | "web_model_increment"
17797            | "web_model_paginate" | "web_model_search" | "web_model_soft_destroy"
17798            | "web_model_with"
17799            | "web_jobs_init" | "web_job_enqueue" | "web_job_dequeue"
17800            | "web_job_complete" | "web_job_fail"
17801            | "web_jobs_list" | "web_jobs_stats" | "web_job_purge"
17802            | "web_jsonapi_resource" | "web_jsonapi_collection" | "web_jsonapi_error"
17803            | "web_bearer_token" | "web_jwt_encode" | "web_jwt_decode"
17804            | "web_otp_secret" | "web_otp_generate" | "web_otp_verify"
17805            | "web_uuid" | "web_now" | "web_log" | "web_rate_limit"
17806            | "web_t" | "web_load_locale" | "web_openapi"
17807            | "web_faker_int" | "web_faker_email" | "web_faker_name"
17808            | "web_faker_sentence" | "web_faker_paragraph"
17809            // ── test runner ─────────────────────────────────────────────────
17810            // In-process equivalents of `stryke check` / `stryke test`. The
17811            // builtin form lets stryke programs (e.g. `exercism_run_all.stk`)
17812            // call them directly without `system "stryke check $f"`, so
17813            // `check_no_interop` from inside `pmaps` is fork-free and
17814            // race-free (per-thread no-interop TLS override).
17815            | "check" | "check_no_interop" | "check_ni"
17816            | "test" | "test_no_interop" | "test_ni"
17817            // ── linear algebra / graphs / dates / special math ──
17818            // ── bits / music theory / hashes / text / statistical tests ──
17819            // ── phonetic / geo projections / base58/91/z85 / astronomy / crc / color blends / compression ──
17820            // ── bioinformatics / 3d geometry / sequence alignment / file headers / hmm ──
17821            // ── game theory / ml inference / chemistry / ops research / info theory ──
17822            // ── cv kernels / information retrieval / rl / color spaces / windows / trie/fenwick/uf / network ──
17823            // ── combinatorics / audio synthesis / search / physics 2d / noise / rng variants ──
17824            // ── ratings / image morphology / computational geometry 2d / crypto / constants / case conversions / photography / unit conversions ──
17825            | "andrew_monotone_hull" | "aperture_stop_to_fnumber" | "arpad_predict" | "bilateral_filter_2d"
17826            | "black_hat_transform" | "canny_edges_full" | "case_alternating" | "case_constant"
17827            | "case_dot" | "case_pascal" | "case_path" | "case_sentence"
17828            | "case_swap" | "case_title_proper" | "case_train" | "closing_2d"
17829            | "cohen_sutherland_clip" | "constants_au_meters" | "constants_avogadro_n" | "constants_bohr_radius"
17830            | "constants_boltzmann_k" | "constants_earth_mass" | "constants_earth_radius" | "constants_electron_charge"
17831            | "constants_electron_mass" | "constants_faraday" | "constants_gas_r" | "constants_gravitational_g"
17832            | "constants_lightyear_meters" | "constants_neutron_mass" | "constants_parsec_meters" | "constants_planck_h"
17833            | "constants_planck_hbar" | "constants_planck" | "constants_proton_mass" | "constants_rydberg"
17834            | "constants_solar_mass" | "constants_solar_radius" | "constants_speed_of_light" | "constants_stefan_boltzmann"
17835            | "contour_area" | "contour_centroid" | "contour_find" | "contour_perimeter"
17836            | "convex_hull_3d" | "convex_hull_3d_simple" | "crop_factor" | "delaunay_triangulate_2d" | "depth_of_field_far"
17837            | "depth_of_field_near" | "dh_compute_shared" | "dilation_2d" | "ec_point_add"
17838            | "ec_point_double" | "ed25519_keypair_simple" | "ed25519_sign_simple" | "ed25519_verify_simple"
17839            | "erosion_2d" | "exposure_value" | "field_of_view" | "focal_length_35mm_equiv"
17840            | "glicko_rd_update" | "glicko_volatility" | "graham_scan_hull" | "hu_moments"
17841            | "hyperfocal_distance" | "liang_barsky_clip" | "minkowski_sum_2d" | "moment_image"
17842            | "morphological_gradient" | "opening_2d" | "pagerank_tournament" | "polygon_inflate"
17843            | "polygon_offset" | "polygon_self_intersects" | "polygon_shrink" | "polygon_simple_check"
17844            | "polygon_winding" | "prewitt_x_kernel" | "prewitt_y_kernel" | "ranking_average"
17845            | "ranking_kendall_tau" | "ranking_spearman_rho" | "roberts_cross_kernel" | "rsa_keypair_simple"
17846            | "rsa_modular_exp" | "scharr_x_kernel" | "scharr_y_kernel" | "schnorr_sign_simple"
17847            | "schnorr_verify_simple" | "shutter_speed_reciprocal" | "sobel_magnitude" | "sunny_16_rule"
17848            | "swiss_pairing" | "top_hat_transform" | "tournament_score" | "trueskill_simple"
17849            | "unit_energy" | "unit_pressure" | "unit_temperature" | "unit_volume_metric_to_us"
17850            | "unit_volume_us_to_metric" | "voronoi_cell_2d" | "weiler_atherton_clip" | "zernike_radial"
17851
17852            | "a_star_grid" | "all_pass_filter" | "am_synth" | "bidirectional_bfs"
17853            | "buoyancy_force" | "center_of_mass_2d" | "center_of_mass_3d" | "centered_polygonal"
17854            | "chorus_simple" | "collision_response_2d" | "comb_filter" | "compositions_count"
17855            | "critical_damping" | "cube_number" | "damping_factor" | "decagonal_number"
17856            | "derangement_count" | "dodecahedral" | "elastic_collision_1d" | "exponential_search"
17857            | "fbm_noise_2d" | "fibonacci_matrix" | "fibonacci_nth_fast" | "fir_filter"
17858            | "flanger_simple" | "floyd_cycle_detect" | "fm_synth_2op" | "freeverb_lite"
17859            | "gnomonic_number" | "greedy_best_first" | "hash_2d_int" | "heptagonal_number"
17860            | "hexagonal_number" | "hyperfactorial" | "icosahedral" | "ida_star_search"
17861            | "inelastic_collision_1d" | "interpolation_search" | "lattice_paths" | "lift_force"
17862            | "lucas_nth" | "moment_of_inertia_cylinder" | "moment_of_inertia_disc" | "moment_of_inertia_rod"
17863            | "moment_of_inertia_sphere" | "mulberry32_next" | "multinomial_coefficient" | "narayana_cow"
17864            | "nonagonal_number" | "octagonal_number" | "partitions_count" | "pcg32_next"
17865            | "pell_nth" | "perlin_2d" | "perlin_3d" | "phaser_simple"
17866            | "plate_reverb_simple" | "poisson_brackets" | "primorial" | "projectile_position"
17867            | "projectile_velocity" | "ridge_noise_2d" | "ring_modulate" | "schroeder_reverb"
17868            | "simplex_2d" | "splitmix64_next" | "spring_oscillator_pos" | "square_pyramidal"
17869            | "super_factorial" | "ternary_search" | "tetrahedral" | "tetranacci"
17870            | "torque_arm" | "turbulence_noise_2d" | "value_noise_2d" | "wavetable_synth"
17871            | "worley_2d" | "xorshift32_next"
17872
17873            | "adaptive_threshold" | "bayes_factor" | "bayesian_beta_update" | "bayesian_normal_update"
17874            | "bm25_score" | "boltzmann_choose" | "braycurtis_dist" | "canberra_dist"
17875            | "canny_edges_simple" | "chebyshev_norm" | "cidr_to_range" | "ciede2000_color_distance"
17876            | "ciede76_color_distance" | "ciede94_color_distance" | "conv1d_apply" | "conv2d_apply"
17877            | "correlate2d" | "cosine_sim_sparse" | "credible_interval_beta" | "credible_interval_normal"
17878            | "dice_coeff" | "earth_mover_1d" | "epsilon_greedy_choose" | "fenwick_new"
17879            | "fenwick_query_prefix" | "fenwick_query_range" | "fenwick_update" | "gaussian_kernel"
17880            | "gradient_magnitude_2d" | "harris_response" | "integral_image" | "ip_subnet_split"
17881            | "ipv6_global_unicast" | "jaccard_sim" | "laplacian_kernel" | "lch_to_rgb"
17882            | "mahalanobis_sq" | "manhattan_norm" | "maximum_a_posteriori" | "minkowski_norm"
17883            | "non_max_suppression" | "oklch_to_rgb" | "otsu_threshold" | "overlap_coeff"
17884            | "posterior_predictive_beta" | "posterior_predictive_normal" | "prior_jeffreys_uniform" | "qlearning_step"
17885            | "range_to_cidr" | "rgb_to_lch" | "rgb_to_oklch" | "rl_discount_returns"
17886            | "rl_n_step_return" | "rl_td_error" | "sarsa_step" | "sliding_dot_product"
17887            | "sobel_x_kernel" | "sobel_y_kernel" | "softmax_choose" | "tanimoto_coeff"
17888            | "tfidf_compute" | "thompson_beta_choose" | "trie_count" | "trie_insert"
17889            | "trie_keys" | "trie_lookup" | "trie_new" | "trie_prefix_search"
17890            | "trie_remove" | "tversky_index" | "ucb1_choose" | "union_find_components"
17891            | "union_find_find" | "union_find_new" | "union_find_union" | "window_bartlett"
17892            | "window_blackman_harris" | "window_flat_top" | "window_gaussian" | "window_welch"
17893
17894            | "alphabeta_value" | "chem_arrhenius_k" | "chem_avogadro" | "chem_balance_check"
17895            | "chem_boiling_point_elevation" | "chem_buffer_capacity" | "chem_celsius_to_fahrenheit" | "chem_celsius_to_kelvin"
17896            | "chem_concentration_to_molarity" | "chem_dilution" | "chem_fahrenheit_to_celsius" | "chem_fahrenheit_to_kelvin"
17897            | "chem_formula_parse" | "chem_freezing_point_depression" | "chem_h_from_ph" | "chem_henderson_hasselbalch"
17898            | "chem_ideal_gas_volume" | "chem_isoelectric_estimate" | "chem_kelvin_to_celsius" | "chem_kelvin_to_fahrenheit"
17899            | "chem_kelvin_to_rankine" | "chem_molality" | "chem_molar_mass" | "chem_molarity_to_normality"
17900            | "chem_partial_pressure" | "chem_ph_from_h" | "chem_pka_lookup" | "chem_rankine_to_kelvin"
17901            | "conditional_entropy" | "edmonds_karp_max_flow" | "expectiminimax_value" | "ford_fulkerson_max_flow"
17902            | "job_schedule_ljf" | "job_schedule_spt" | "joint_entropy" | "js_divergence_distributions"
17903            | "kl_divergence_distributions" | "knapsack_fractional" | "knapsack_unbounded" | "lp_simplex_max"
17904            | "lp_simplex_min" | "matching_bipartite_greedy" | "matching_bipartite_hungarian" | "minimax_value"
17905            | "mixed_strategy_2x2" | "ml_attention_score" | "ml_batch_norm" | "ml_dot_product_attention"
17906            | "ml_dropout_mask" | "ml_elu_layer" | "ml_gelu_layer" | "ml_hinge_loss"
17907            | "ml_huber_loss" | "ml_kl_div_loss" | "ml_label_smooth" | "ml_layer_norm"
17908            | "ml_leaky_relu_layer" | "ml_mae_loss" | "ml_mish_layer" | "ml_mse_loss"
17909            | "ml_one_hot_encode" | "ml_position_encoding" | "ml_relu_layer" | "ml_self_attention"
17910            | "ml_sigmoid_layer" | "ml_softmax_layer" | "ml_softplus_layer" | "ml_swish_layer"
17911            | "ml_tanh_layer" | "ngram_perplexity" | "ngram_prob" | "ngram_top_k_next"
17912            | "ngram_train" | "payoff_matrix" | "relative_entropy" | "tsp_2opt"
17913            | "zero_sum_value"
17914
17915            | "aabb_contains_point" | "aabb_intersects" | "aabb_new" | "aabb_union"
17916            | "aabb_volume" | "backward_algorithm" | "blast_kmer_index" | "bmp_header_read"
17917            | "bootstrap_resample" | "codon_optimize" | "codon_to_amino_acid" | "codon_usage_table"
17918            | "dna_at_content" | "dna_complement" | "dna_gc_content" | "dna_kmer_count"
17919            | "dna_kmer_index" | "dna_melting_temp" | "dna_reverse_complement" | "dna_transcribe"
17920            | "dna_translate" | "elf_header_read" | "forward_algorithm" | "gif_header_read"
17921            | "ico_header_read"
17922            | "jpeg_markers" | "levenshtein_edit_path" | "mach_o_header_read" | "markov_stationary"
17923            | "markov_transition_matrix" | "mat4_determinant" | "mat4_identity" | "mat4_inverse"
17924            | "mat4_look_at" | "mat4_multiply" | "mat4_orthographic" | "mat4_perspective"
17925            | "mat4_rotate_axis" | "mat4_rotate_x" | "mat4_rotate_y" | "mat4_rotate_z"
17926            | "mat4_scale" | "mat4_translate" | "mat4_transpose" | "nw_score"
17927            | "permutation_test" | "plane_distance_to_point" | "plane_normalize" | "png_header_read"
17928            | "profile_hmm_score" | "protein_charge_at_ph" | "protein_hydrophobicity" | "protein_molecular_weight"
17929            | "protein_pI" | "quat_conjugate" | "quat_dot" | "quat_from_euler"
17930            | "quat_identity" | "quat_inverse" | "quat_multiply" | "quat_normalize"
17931            | "quat_to_euler" | "quat_to_mat4" | "ray_aabb_intersect" | "ray_plane_intersect_2"
17932            | "ray_plane_intersect" | "rna_gc_content" | "rna_hamming" | "rna_reverse_complement"
17933            | "rna_to_dna" | "sequence_identity_pct" | "sequence_similarity_pct" | "shuffle_resample"
17934            | "sphere_aabb_intersect" | "sphere_sphere_intersect" | "sw_score" | "tar_header_read"
17935            | "triangle_area_3d" | "triangle_normal" | "vec3_add" | "vec3_cross"
17936            | "vec3_distance" | "vec3_dot" | "vec3_length" | "vec3_lerp"
17937            | "vec3_normalize" | "vec3_project" | "vec3_reflect" | "vec3_refract"
17938            | "vec3_scale" | "vec3_sub" | "vec4_add" | "vec4_dot"
17939            | "vec4_length" | "vec4_scale" | "vec4_sub" | "viterbi_decode"
17940            | "wav_header_read" | "zip_central_directory" | "zip_local_file_header"
17941
17942            | "adler32_combine" | "ase_palette_extract" | "base58check_decode" | "base58check_encode"
17943            | "base91_decode" | "basE91_decode" | "base91_encode" | "basE91_encode"
17944            | "bwt_invert" | "bwt_transform" | "caverphone" | "caverphone2"
17945            | "crc10_atm" | "crc12_dect" | "crc24" | "crc32_bzip2"
17946            | "crc32_jamcrc" | "crc32_mpeg2" | "crc32_xfer" | "crc6_itu"
17947            | "crc64_ecma" | "crc64_xz" | "delta_decode" | "delta_encode"
17948            | "destination_lat_lon" | "double_metaphone_primary" | "double_metaphone_secondary" | "fletcher16"
17949            | "fletcher32" | "fletcher64" | "full_moon_julian" | "fuzzy_substring_match"
17950            | "gamma_correct" | "gamma_uncorrect" | "geomag_declination" | "huffman_decode"
17951            | "huffman_encode" | "julian_to_unix" | "lambert_project" | "lat_lon_to_utm"
17952            | "match_rating_compare" | "mercator_project_x" | "mercator_project_y" | "mercator_unproject_lat"
17953            | "mercator_unproject_lon" | "modified_julian_date" | "moon_age_days" | "moon_distance_km"
17954            | "new_moon_julian" | "nysiis" | "phonex" | "rgb_blend_color_burn"
17955            | "rgb_blend_color_dodge" | "rgb_blend_darken" | "rgb_blend_lighten" | "rgb_blend_multiply"
17956            | "rgb_blend_normal" | "rgb_blend_overlay" | "rgb_blend_screen" | "rle_compress"
17957            | "rle_decompress" | "season_of_year" | "sidereal_time_greenwich" | "sidereal_time_local"
17958            | "solar_noon_unix" | "soundex_v1" | "soundex_v2" | "unix_to_julian"
17959            | "utm_to_lat_lon" | "utm_zone" | "varint_decode" | "varint_encode"
17960            | "vincenty_bearing" | "z85_decode" | "z85_encode" | "zigzag_decode"
17961            | "zigzag_encode"
17962
17963            | "anova_one_way" | "binomial_test" | "bit_clz"
17964            | "bit_count_ones" | "bit_count_zeros" | "bit_ctz" | "bit_extract"
17965            | "bit_first_clear" | "bit_first_set" | "bit_insert" | "bit_last_clear"
17966            | "bit_last_set" | "bit_log2_int" | "bit_parity" | "bit_reverse_u16"
17967            | "bit_reverse_u32" | "bit_reverse_u64" | "bit_reverse_u8" | "bit_rotate_left"
17968            | "bit_rotate_right" | "bit_swap_bytes" | "chi_square_goodness_fit" | "chi_square_independence"
17969            | "chord_augmented" | "chord_diminished" | "chord_diminished7" | "chord_dominant7"
17970            | "chord_major" | "chord_major7" | "chord_minor" | "chord_minor7"
17971            | "crc16_xmodem" | "crc16" | "crc32_zlib" | "crc32c"
17972            | "crc8" | "detab" | "entab" | "fisher_exact_2x2"
17973            | "gray_code_decode" | "gray_code_encode" | "hmac_md5_hex" | "hmac_sha1_hex"
17974            | "hmac_sha256_hex" | "hmac_sha384_hex" | "hmac_sha512_hex" | "indent_block"
17975            | "interval_name" | "jenkins_hash" | "justify_center" | "justify_left"
17976            | "justify_right" | "kruskal_wallis" | "ks_test_one_sample" | "ks_test_two_sample"
17977            | "loose_hash" | "mann_whitney_u" | "midi_to_note_name" | "popcount_u32"
17978            | "popcount_u64" | "proportion_test" | "rank_data" | "scale_blues"
17979            | "scale_chromatic" | "scale_dorian" | "scale_harmonic_minor" | "scale_locrian"
17980            | "scale_lydian" | "scale_major" | "scale_melodic_minor" | "scale_minor"
17981            | "scale_mixolydian" | "scale_pentatonic" | "scale_phrygian" | "seconds_per_beat"
17982            | "strip_indent" | "t_test_paired" | "tempo_to_ms_per_beat" | "truncate_middle"
17983            | "unicode_codepoints" | "wilcoxon_signed_rank" | "word_wrap"
17984
17985            | "beta_function" | "beta_incomplete" | "date_add_days" | "date_add_months"
17986            | "date_add_years" | "date_business_days_between" | "date_day" | "date_dayofweek"
17987            | "date_dayofyear" | "date_days_in_month" | "date_diff_days" | "date_diff_hours"
17988            | "date_diff_minutes" | "date_diff_seconds" | "date_easter" | "date_first_of_month"
17989            | "date_hour" | "date_is_leap" | "date_is_weekend" | "date_iso_format"
17990            | "date_iso_week" | "date_last_of_month" | "date_minute" | "date_month"
17991            | "date_quarter" | "date_second" | "date_str_to_unix" | "date_unix_to_str"
17992            | "date_weekofyear" | "date_year" | "ei" | "expint"
17993            | "gamma_regularized_p" | "gamma_regularized_q" | "graph_articulation_points" | "graph_bellman_ford"
17994            | "graph_betweenness" | "graph_bfs" | "graph_bridges" | "graph_closeness"
17995            | "graph_clustering_coefficient" | "graph_color_greedy" | "graph_connected_components" | "graph_cycle_detect"
17996            | "graph_degree" | "graph_dfs" | "graph_dijkstra" | "graph_eccentricity"
17997            | "graph_eigenvector_centrality" | "graph_floyd_warshall" | "graph_from_edges" | "graph_has_path"
17998            | "graph_in_degree" | "graph_is_bipartite" | "graph_is_connected" | "graph_kosaraju"
17999            | "graph_kruskal_mst" | "graph_out_degree" | "graph_pagerank" | "graph_prim_mst"
18000            | "graph_shortest_path" | "graph_strongly_connected_components" | "graph_tarjan" | "graph_to_adj_list"
18001            | "graph_to_adj_matrix" | "graph_topological_sort" | "hypergeom_1f1" | "hypergeom_2f1"
18002            | "li" | "matrix_adjugate" | "matrix_cholesky_decompose" | "matrix_cofactor"
18003            | "matrix_cols" | "matrix_concat_h" | "matrix_concat_v" | "matrix_determinant"
18004            | "matrix_from_cols" | "matrix_get" | "matrix_kronecker" | "matrix_lu_decompose"
18005            | "matrix_minor" | "matrix_new" | "matrix_norm_frobenius" | "matrix_norm_l1"
18006            | "matrix_norm_linf" | "matrix_outer_product" | "matrix_qr_decompose" | "matrix_reshape"
18007            | "matrix_rows" | "matrix_set" | "matrix_submatrix" | "matrix_swap_cols"
18008            | "matrix_swap_rows" | "matrix_to_string" | "matrix_vec_mul" | "si"
18009            | "sun_rise_unix" | "sun_set_unix" | "zeta_riemann" | "zodiac_sign"
18010            // ── quant / technical indicators / time-series / finance / optimization ──
18011            | "add_seasonality" | "trapezoidal_integrate" | "simpson_integrate"
18012            | "ode_euler" | "fit_curve_least_squares"
18013            | "adf_test" | "adx" | "atr" | "bollinger_lower" | "bollinger_middle"
18014            | "bollinger_upper" | "break_even_price" | "break_even_qty" | "candlestick_pattern_doji"
18015            | "candlestick_pattern_engulfing" | "candlestick_pattern_evening_star" | "candlestick_pattern_hammer" | "candlestick_pattern_morning_star"
18016            | "candlestick_pattern_three_black_crows" | "candlestick_pattern_three_white_soldiers" | "cci"
18017            | "dema" | "diff_pct" | "diff_series"
18018            | "discount_pct" | "donchian_lower" | "donchian_upper" | "double_exponential_smoothing"
18019            | "duration_modified" | "ema" | "expanding_mean" | "expanding_sum"
18020            | "fibonacci_extension" | "fibonacci_retracement" | "finite_difference_central"
18021            | "finite_difference_forward" | "hma"
18022            | "hurst_exponent" | "interp_lagrange"
18023            | "interp_linear" | "kama"
18024            | "keltner_lower" | "keltner_upper" | "lag_series"
18025            | "loan_interest_total" | "loan_payment" | "loan_remaining" | "log_returns"
18026            | "macd_histogram" | "macd_signal" | "macd" | "markup_pct" | "net_present_value" | "obv" | "parabolic_sar" | "pivot_points" | "profit_margin_pct" | "remove_seasonality" | "resistance_level"
18027            | "roc" | "rolling_kurtosis" | "rolling_max"
18028            | "rolling_mean" | "rolling_median" | "rolling_min" | "rolling_skew"
18029            | "rolling_std" | "rolling_sum" | "rolling_var"
18030            | "rsi" | "shift_series" | "simple_returns" | "sma" | "stoch_rsi" | "support_level"
18031            | "tema" | "trend_line" | "treynor"
18032            | "trix" | "true_range" | "twap" | "ulcer_index"
18033            | "volatility_annualized" | "volatility_realized" | "vwap" | "williams_r"
18034            | "wma"
18035            => Some(name),
18036            _ => None,
18037        }
18038    }
18039
18040    /// Reserved hash names that cannot be shadowed by user declarations.
18041    /// These are stryke's reflection hashes populated from builtins metadata.
18042    fn is_reserved_hash_name(name: &str) -> bool {
18043        matches!(
18044            name,
18045            "b" | "pc"
18046                | "e"
18047                | "a"
18048                | "d"
18049                | "c"
18050                | "p"
18051                | "k"
18052                | "all"
18053                | "stryke::builtins"
18054                | "stryke::perl_compats"
18055                | "stryke::extensions"
18056                | "stryke::aliases"
18057                | "stryke::descriptions"
18058                | "stryke::categories"
18059                | "stryke::primaries"
18060                | "stryke::keywords"
18061                | "stryke::all"
18062        )
18063    }
18064
18065    /// Check if a UDF name shadows a stryke builtin and error if so.
18066    /// Called only in non-compat mode — compat mode allows shadowing for Perl 5 parity.
18067    /// Reserved words that cannot be used as function names because they are
18068    /// lexer-level operators or language keywords that would be mis-tokenized.
18069    const RESERVED_FUNCTION_NAMES: &'static [&'static str] = &[
18070        "y",
18071        "tr",
18072        "s",
18073        "m",
18074        "q",
18075        "qq",
18076        "qw",
18077        "qx",
18078        "qr",
18079        "if",
18080        "unless",
18081        "while",
18082        "until",
18083        "for",
18084        "foreach",
18085        "given",
18086        "when",
18087        "else",
18088        "elsif",
18089        "do",
18090        "eval",
18091        "return",
18092        "last",
18093        "next",
18094        "redo",
18095        "goto",
18096        "my",
18097        "our",
18098        "local",
18099        "state",
18100        "sub",
18101        "fn",
18102        "class",
18103        "struct",
18104        "enum",
18105        "trait",
18106        "use",
18107        "no",
18108        "require",
18109        "package",
18110        "BEGIN",
18111        "END",
18112        "CHECK",
18113        "INIT",
18114        "UNITCHECK",
18115        "and",
18116        "or",
18117        "not",
18118        "x",
18119        "eq",
18120        "ne",
18121        "lt",
18122        "gt",
18123        "le",
18124        "ge",
18125        "cmp",
18126    ];
18127
18128    fn check_udf_shadows_builtin(&self, name: &str, line: usize) -> StrykeResult<()> {
18129        // Already namespaced (e.g. `Foo::y`) — package context makes the
18130        // name unambiguous, so it can never shadow a builtin.
18131        if name.contains("::") {
18132            return Ok(());
18133        }
18134        // Reserved syntactic words (`if`, `while`, `package`, …) break
18135        // parsing as function names regardless of package.
18136        if Self::RESERVED_FUNCTION_NAMES.contains(&name) {
18137            return Err(self.syntax_err(
18138                format!("`{name}` is a reserved word and cannot be used as a function name"),
18139                line,
18140            ));
18141        }
18142        // Bare `fn name(...)` inside a non-main `package Foo` registers
18143        // under `Foo::name`. The user sub is callable only via the
18144        // fully-qualified `Foo::name(...)` spelling — bare calls always
18145        // dispatch to the global builtin. Allow the declaration.
18146        if self.current_package != "main" {
18147            return Ok(());
18148        }
18149        // In `package main` (the default), there's no qualified spelling
18150        // to "escape" a builtin name. Reject `fn sum {}` here so callers
18151        // never wonder why bare `sum(1,2,3)` ignored their definition.
18152        if Self::is_known_bareword(name)
18153            || Self::is_try_builtin_name(name)
18154            || crate::list_builtins::is_list_builtin_name(name)
18155        {
18156            return Err(self.syntax_err(
18157                format!(
18158"`{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)"
18159                ),
18160                line,
18161            ));
18162        }
18163        Ok(())
18164    }
18165
18166    /// Check if a hash name shadows a reserved stryke hash and error if so.
18167    /// Called only in non-compat mode.
18168    fn check_hash_shadows_reserved(&self, name: &str, line: usize) -> StrykeResult<()> {
18169        if Self::is_reserved_hash_name(name) {
18170            return Err(self.syntax_err(
18171                format!(
18172"`%{name}` is a stryke reserved hash and cannot be redefined (this is not Perl 5; pass --compat for Perl 5 mode)"
18173                ),
18174                line,
18175            ));
18176        }
18177        Ok(())
18178    }
18179
18180    /// Validate assignment to %hash in non-compat mode.
18181    /// Rejects: scalar, string, arrayref, hashref, coderef, undef, odd-length list.
18182    fn validate_hash_assignment(&self, value: &Expr, line: usize) -> StrykeResult<()> {
18183        match &value.kind {
18184            ExprKind::Integer(_) | ExprKind::Float(_) => {
18185                return Err(self.syntax_err(
18186                    "cannot assign scalar to hash — use %h = (key => value) or %h = %{$hashref}",
18187                    line,
18188                ));
18189            }
18190            ExprKind::String(_) | ExprKind::InterpolatedString(_) | ExprKind::Bareword(_) => {
18191                return Err(self.syntax_err(
18192                    "cannot assign string to hash — use %h = (key => value) or %h = %{$hashref}",
18193                    line,
18194                ));
18195            }
18196            ExprKind::ArrayRef(_) => {
18197                return Err(self.syntax_err(
18198                    "cannot assign arrayref to hash — use %h = @{$arrayref} for even-length list",
18199                    line,
18200                ));
18201            }
18202            ExprKind::ScalarRef(inner) => {
18203                if matches!(inner.kind, ExprKind::ArrayVar(_)) {
18204                    return Err(self.syntax_err(
18205                        "cannot assign \\@array to hash — use %h = @array for even-length list",
18206                        line,
18207                    ));
18208                }
18209                if matches!(inner.kind, ExprKind::HashVar(_)) {
18210                    return Err(self.syntax_err(
18211                        "cannot assign \\%hash to hash — use %h = %other directly",
18212                        line,
18213                    ));
18214                }
18215            }
18216            ExprKind::HashRef(_) => {
18217                return Err(self.syntax_err(
18218                    "cannot assign hashref to hash — use %h = %{$hashref} to dereference",
18219                    line,
18220                ));
18221            }
18222            ExprKind::CodeRef { .. } => {
18223                return Err(self.syntax_err("cannot assign coderef to hash", line));
18224            }
18225            ExprKind::Undef => {
18226                return Err(
18227                    self.syntax_err("cannot assign undef to hash — use %h = () to empty", line)
18228                );
18229            }
18230            ExprKind::List(items)
18231                if items.len() % 2 != 0
18232                    && !items.iter().any(|e| {
18233                        matches!(
18234                            e.kind,
18235                            ExprKind::ArrayVar(_)
18236                                | ExprKind::HashVar(_)
18237                                | ExprKind::FuncCall { .. }
18238                                | ExprKind::Deref { .. }
18239                                | ExprKind::ScalarVar(_)
18240                        )
18241                    }) =>
18242            {
18243                return Err(self.syntax_err(
18244                        format!(
18245                            "odd-length list ({} elements) in hash assignment — missing value for last key",
18246                            items.len()
18247                        ),
18248                        line,
18249                    ));
18250            }
18251            _ => {}
18252        }
18253        Ok(())
18254    }
18255
18256    /// Validate assignment to @array in non-compat mode.
18257    /// Rejects: undef (likely a mistake — use `@a = ()` to empty).
18258    /// Note: bare scalars like `@a = 2` are allowed since Perl coerces them to single-element lists.
18259    /// Note: `@a = {hashref}` is allowed as a common pattern for single-element arrays.
18260    fn validate_array_assignment(&self, value: &Expr, line: usize) -> StrykeResult<()> {
18261        if let ExprKind::Undef = &value.kind {
18262            return Err(
18263                self.syntax_err("cannot assign undef to array — use @a = () to empty", line)
18264            );
18265        }
18266        Ok(())
18267    }
18268
18269    /// Validate assignment to $scalar in non-compat mode.
18270    /// Rejects: list literals (Perl 5 silently returns last element — footgun).
18271    fn validate_scalar_assignment(&self, value: &Expr, line: usize) -> StrykeResult<()> {
18272        if let ExprKind::List(items) = &value.kind {
18273            if items.len() > 1 {
18274                return Err(self.syntax_err(
18275                    format!(
18276                        "cannot assign {}-element list to scalar — Perl 5 silently takes last element; use ($x) = (list) or $x = $list[-1]",
18277                        items.len()
18278                    ),
18279                    line,
18280                ));
18281            }
18282        }
18283        Ok(())
18284    }
18285
18286    /// Validate an assignment based on target type (in non-compat mode only).
18287    fn validate_assignment(&self, target: &Expr, value: &Expr, line: usize) -> StrykeResult<()> {
18288        if crate::compat_mode() {
18289            return Ok(());
18290        }
18291        match &target.kind {
18292            ExprKind::HashVar(_) => self.validate_hash_assignment(value, line),
18293            ExprKind::ArrayVar(_) => self.validate_array_assignment(value, line),
18294            ExprKind::ScalarVar(_) => self.validate_scalar_assignment(value, line),
18295            _ => Ok(()),
18296        }
18297    }
18298
18299    /// Parse a block OR a blockless comparison expression for sort/psort/heap.
18300    /// Blockless: `$a <=> $b` or `$a cmp $b` or any expression → wrapped as a Block.
18301    /// Also accepts a bare function name: `psort my_cmp, @list`.
18302    fn parse_block_or_bareword_cmp_block(&mut self) -> StrykeResult<Block> {
18303        if matches!(self.peek(), Token::LBrace) {
18304            return self.parse_block();
18305        }
18306        let line = self.peek_line();
18307        // Bare sub name: `psort my_cmp, @list`
18308        if let Token::Ident(ref name) = self.peek().clone() {
18309            if matches!(
18310                self.peek_at(1),
18311                Token::Comma | Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
18312            ) {
18313                let name = name.clone();
18314                self.advance();
18315                let body = Expr {
18316                    kind: ExprKind::FuncCall {
18317                        name,
18318                        args: vec![
18319                            Expr {
18320                                kind: ExprKind::ScalarVar("a".to_string()),
18321                                line,
18322                            },
18323                            Expr {
18324                                kind: ExprKind::ScalarVar("b".to_string()),
18325                                line,
18326                            },
18327                        ],
18328                    },
18329                    line,
18330                };
18331                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
18332            }
18333        }
18334        // Blockless expression: `$a <=> $b`, `$b cmp $a`, etc.
18335        let expr = self.parse_assign_expr_stop_at_pipe()?;
18336        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
18337    }
18338
18339    /// After `fan` / `fan_cap` `{ BLOCK }`, optional `, progress => EXPR` or `progress => EXPR` (no comma).
18340    fn parse_fan_optional_progress(
18341        &mut self,
18342        which: &'static str,
18343    ) -> StrykeResult<Option<Box<Expr>>> {
18344        let line = self.peek_line();
18345        if self.eat(&Token::Comma) {
18346            match self.peek() {
18347                Token::Ident(ref kw)
18348                    if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) =>
18349                {
18350                    self.advance();
18351                    self.expect(&Token::FatArrow)?;
18352                    return Ok(Some(Box::new(self.parse_assign_expr()?)));
18353                }
18354                _ => {
18355                    return Err(self.syntax_err(
18356                        format!("{which}: expected `progress => EXPR` after comma"),
18357                        line,
18358                    ));
18359                }
18360            }
18361        }
18362        if let Token::Ident(ref kw) = self.peek().clone() {
18363            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
18364                self.advance();
18365                self.expect(&Token::FatArrow)?;
18366                return Ok(Some(Box::new(self.parse_assign_expr()?)));
18367            }
18368        }
18369        Ok(None)
18370    }
18371
18372    /// Comma-separated assign expressions with optional trailing `, progress => EXPR`
18373    /// (for `pmap_chunked`, `psort`, etc.).
18374    ///
18375    /// Paren-less — individual parts parse through
18376    /// [`Self::parse_assign_expr_stop_at_pipe`] so a trailing `|>` is left for
18377    /// the enclosing pipe-forward loop (left-associative chaining).
18378    fn parse_assign_expr_list_optional_progress(&mut self) -> StrykeResult<(Expr, Option<Expr>)> {
18379        // On the RHS of `|>`, list-taking builtins may be written bare with no
18380        // operand — `@a |> uniq`, `@a |> flatten`, `foo(bar, @a |> psort)`, etc.
18381        // When the next token is a list-terminator, yield an empty placeholder
18382        // list; [`Self::pipe_forward_apply`] substitutes the piped LHS at
18383        // desugar time, so the placeholder is never evaluated.
18384        if self.in_pipe_rhs()
18385            && matches!(
18386                self.peek(),
18387                Token::Semicolon
18388                    | Token::RBrace
18389                    | Token::RParen
18390                    | Token::Eof
18391                    | Token::PipeForward
18392                    | Token::Comma
18393            )
18394        {
18395            return Ok((self.pipe_placeholder_list(self.peek_line()), None));
18396        }
18397        let mut parts = vec![self.parse_assign_expr_stop_at_pipe()?];
18398        loop {
18399            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
18400                break;
18401            }
18402            if matches!(
18403                self.peek(),
18404                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
18405            ) {
18406                break;
18407            }
18408            if self.peek_is_postfix_stmt_modifier_keyword() {
18409                break;
18410            }
18411            if let Token::Ident(ref kw) = self.peek().clone() {
18412                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
18413                    self.advance();
18414                    self.expect(&Token::FatArrow)?;
18415                    let prog = self.parse_assign_expr_stop_at_pipe()?;
18416                    return Ok((merge_expr_list(parts), Some(prog)));
18417                }
18418            }
18419            parts.push(self.parse_assign_expr_stop_at_pipe()?);
18420        }
18421        Ok((merge_expr_list(parts), None))
18422    }
18423
18424    fn parse_one_arg(&mut self) -> StrykeResult<Expr> {
18425        if matches!(self.peek(), Token::LParen) {
18426            self.advance();
18427            let expr = self.parse_expression()?;
18428            self.expect(&Token::RParen)?;
18429            Ok(expr)
18430        } else {
18431            self.parse_assign_expr_stop_at_pipe()
18432        }
18433    }
18434
18435    /// Bare argument for a Perl-5 named unary operator (`defined`, `length`,
18436    /// `abs`, `scalar`, `ref`, `keys`, `values`, etc.). Named unary precedence
18437    /// sits between shift (`<<`/`>>`) and comparison (`<`/`>`), so we parse
18438    /// only down to shift level. The surrounding `&&` / `||` / `==` / `<` /
18439    /// equality / logical / ternary stay outside the unary's argument.
18440    /// Without this `defined $x && Y` mis-parsed as `defined($x && Y)` and
18441    /// silently returned true whenever `$x` was defined — see the skip-list
18442    /// debugging write-up. Same scope rule for `length` etc.
18443    fn parse_named_unary_arg(&mut self) -> StrykeResult<Expr> {
18444        if matches!(self.peek(), Token::LParen) {
18445            self.advance();
18446            let expr = self.parse_expression()?;
18447            self.expect(&Token::RParen)?;
18448            Ok(expr)
18449        } else {
18450            self.parse_shift()
18451        }
18452    }
18453
18454    fn parse_one_arg_or_default(&mut self) -> StrykeResult<Expr> {
18455        // Treat a line boundary as a hard arg terminator: if the next
18456        // token is on a *later* line than the named-unary keyword we
18457        // just consumed, default the operand to `$_` and stop. Without
18458        // this, `my $x = uc` followed by `my $y = 5` on the next line
18459        // mis-parses by silently swallowing `my $y = 5` as the implicit
18460        // argument to `uc`. Stryke (like Perl/shell) terminates
18461        // statements at newline; continuation requires explicit `\`.
18462        // The check skips when the *next* token is itself a binary /
18463        // postfix operator that legitimately continues the expression
18464        // (handled by the existing operator stop-list below).
18465        let prev = self.prev_line();
18466        if self.peek_line() > prev {
18467            return Ok(Expr {
18468                kind: ExprKind::ScalarVar("_".into()),
18469                line: prev,
18470            });
18471        }
18472        // Default to `$_` when the next token cannot start an argument expression
18473        // because it has lower precedence than a named unary operator. Perl 5
18474        // named unary precedence sits above ternary / comparison / logical / bitwise
18475        // / assignment / list ops; everything below should terminate the implicit
18476        // argument and let the surrounding expression continue.
18477        // See `perldoc perlop` ("Named Unary Operators").
18478        if matches!(
18479            self.peek(),
18480            // Statement / list / call boundaries
18481            Token::Semicolon
18482                | Token::RBrace
18483                | Token::RParen
18484                | Token::RBracket
18485                | Token::Eof
18486                | Token::Comma
18487                | Token::FatArrow
18488                | Token::PipeForward
18489            // Ternary `? :`
18490                | Token::Question
18491                | Token::Colon
18492            // Comparison / equality (numeric + string)
18493                | Token::NumEq | Token::NumNe | Token::NumLt | Token::NumGt
18494                | Token::NumLe | Token::NumGe | Token::Spaceship
18495                | Token::StrEq | Token::StrNe | Token::StrLt | Token::StrGt
18496                | Token::StrLe | Token::StrGe | Token::StrCmp
18497            // Logical (symbolic and word forms) + defined-or
18498                | Token::LogAnd | Token::LogOr | Token::LogNot
18499                | Token::LogAndWord | Token::LogOrWord | Token::LogNotWord
18500                | Token::DefinedOr
18501            // Range (lower precedence than named unary)
18502                | Token::Range | Token::RangeExclusive
18503            // Assignment (any compound form)
18504                | Token::Assign | Token::PlusAssign | Token::MinusAssign
18505                | Token::MulAssign | Token::DivAssign | Token::ModAssign
18506                | Token::PowAssign | Token::DotAssign | Token::AndAssign
18507                | Token::OrAssign | Token::XorAssign | Token::DefinedOrAssign
18508                | Token::ShiftLeftAssign | Token::ShiftRightAssign
18509                | Token::BitAndAssign | Token::BitOrAssign
18510        ) {
18511            return Ok(Expr {
18512                kind: ExprKind::ScalarVar("_".into()),
18513                line: self.peek_line(),
18514            });
18515        }
18516        // `f()` — empty parens default to `$_`, matching Perl 5 semantics.
18517        // `perldoc -f length`: "If EXPR is omitted, returns the length of $_."
18518        // Perl accepts both `length` and `length()` as `length($_)`.
18519        if matches!(self.peek(), Token::LParen) && matches!(self.peek_at(1), Token::RParen) {
18520            let line = self.peek_line();
18521            self.advance(); // (
18522            self.advance(); // )
18523            return Ok(Expr {
18524                kind: ExprKind::ScalarVar("_".into()),
18525                line,
18526            });
18527        }
18528        // Named-unary precedence: parenless arg only goes down to shift level,
18529        // so surrounding `eq` / `==` / `?:` / `&&` / `||` stay outside. Without
18530        // this, `ref $x eq "FOO"` mis-parses as `ref ($x eq "FOO")`.
18531        // (PARITY-016 — also fixes `length $s == 3 ? "Y" : "N"` etc.)
18532        self.parse_named_unary_arg()
18533    }
18534
18535    /// Array operand for `shift` / `pop`: default `@_`, or `shift(@a)` / `shift()` (empty parens = `@_`).
18536    fn parse_one_arg_or_argv(&mut self) -> StrykeResult<Expr> {
18537        let line = self.prev_line(); // line where shift/pop keyword was
18538        if matches!(self.peek(), Token::LParen) {
18539            self.advance();
18540            if matches!(self.peek(), Token::RParen) {
18541                self.advance();
18542                return Ok(Expr {
18543                    kind: ExprKind::ArrayVar("_".into()),
18544                    line: self.peek_line(),
18545                });
18546            }
18547            let expr = self.parse_expression()?;
18548            self.expect(&Token::RParen)?;
18549            return Ok(expr);
18550        }
18551        // Implicit semicolon: if next token is on a different line, don't consume it
18552        if matches!(
18553            self.peek(),
18554            Token::Semicolon
18555                | Token::RBrace
18556                | Token::RParen
18557                | Token::Eof
18558                | Token::Comma
18559                | Token::PipeForward
18560        ) || self.peek_line() > line
18561        {
18562            Ok(Expr {
18563                kind: ExprKind::ArrayVar("_".into()),
18564                line,
18565            })
18566        } else {
18567            self.parse_assign_expr()
18568        }
18569    }
18570
18571    fn parse_builtin_args(&mut self) -> StrykeResult<Vec<Expr>> {
18572        if matches!(self.peek(), Token::LParen) {
18573            self.advance();
18574            let args = self.parse_arg_list()?;
18575            self.expect(&Token::RParen)?;
18576            Ok(args)
18577        } else if self.suppress_parenless_call > 0 && matches!(self.peek(), Token::Ident(_)) {
18578            // In thread context, don't consume barewords as arguments
18579            // so `t filesf sorted ep` parses `sorted` as a stage, not an arg to filesf
18580            Ok(vec![])
18581        } else {
18582            self.parse_list_until_terminator()
18583        }
18584    }
18585
18586    /// Check if the next token is `=>` (fat arrow). If so, the preceding bareword
18587    /// should be treated as an auto-quoted string (hash key), not a function call.
18588    /// Returns `Some(Expr::String(name))` if fat arrow follows, `None` otherwise.
18589    #[inline]
18590    fn fat_arrow_autoquote(&self, name: &str, line: usize) -> Option<Expr> {
18591        if matches!(self.peek(), Token::FatArrow) {
18592            Some(Expr {
18593                kind: ExprKind::String(name.to_string()),
18594                line,
18595            })
18596        } else {
18597            None
18598        }
18599    }
18600
18601    /// Parse a hash subscript key inside `{…}`.
18602    ///
18603    /// Perl auto-quotes a single bareword before `}`, even for keywords:
18604    /// `$h{print}`, `$r->{f}` etc. all yield the string key. Stryke also
18605    /// auto-quotes the string-comparison and word-logical operator tokens
18606    /// (`eq`, `ne`, `lt`, `gt`, `le`, `ge`, `cmp`, `and`, `or`, `not`, `x`)
18607    /// here — the lexer eagerly converts those identifiers to operator tokens,
18608    /// but inside `{…}` followed by `}` they're plainly hash keys.
18609    /// Stryke exception: topic-slot barewords (`_`, `_<`, `_0`, `_0<`, …)
18610    /// resolve to the topic value, not the literal name — `$h{_<}` ≡ `$h{$_<}`.
18611    fn parse_hash_subscript_key(&mut self) -> StrykeResult<Expr> {
18612        let line = self.peek_line();
18613        if let Token::Ident(ref k) = self.peek().clone() {
18614            if matches!(self.peek_at(1), Token::RBrace) && !Self::is_underscore_topic_slot(k) {
18615                let s = k.clone();
18616                self.advance();
18617                return Ok(Expr {
18618                    kind: ExprKind::String(s),
18619                    line,
18620                });
18621            }
18622        }
18623        if matches!(self.peek_at(1), Token::RBrace) {
18624            if let Some(s) = Self::operator_keyword_to_ident_str(self.peek()) {
18625                self.advance();
18626                return Ok(Expr {
18627                    kind: ExprKind::String(s.to_string()),
18628                    line,
18629                });
18630            }
18631        }
18632        self.parse_expression()
18633    }
18634
18635    /// `progress` introducing the optional `progress => EXPR` suffix for `glob_par` / `par_sed`.
18636    #[inline]
18637    fn peek_is_glob_par_progress_kw(&self) -> bool {
18638        matches!(self.peek(), Token::Ident(ref kw) if kw == "progress")
18639            && matches!(self.peek_at(1), Token::FatArrow)
18640    }
18641
18642    /// Pattern list for `glob_par` / `par_sed` inside `(...)`, stopping before `)` or `progress =>`.
18643    fn parse_pattern_list_until_rparen_or_progress(&mut self) -> StrykeResult<Vec<Expr>> {
18644        let mut args = Vec::new();
18645        loop {
18646            if matches!(self.peek(), Token::RParen | Token::Eof) {
18647                break;
18648            }
18649            if self.peek_is_glob_par_progress_kw() {
18650                break;
18651            }
18652            args.push(self.parse_assign_expr()?);
18653            match self.peek() {
18654                Token::RParen => break,
18655                Token::Comma => {
18656                    self.advance();
18657                    if matches!(self.peek(), Token::RParen) {
18658                        break;
18659                    }
18660                    if self.peek_is_glob_par_progress_kw() {
18661                        break;
18662                    }
18663                }
18664                _ => {
18665                    return Err(self.syntax_err(
18666                        "expected `,`, `)`, or `progress =>` after argument in `glob_par` / `par_sed`",
18667                        self.peek_line(),
18668                    ));
18669                }
18670            }
18671        }
18672        Ok(args)
18673    }
18674
18675    /// Paren-less pattern list for `glob_par` / `par_sed`, stopping before stmt end or `progress =>`.
18676    fn parse_pattern_list_glob_par_bare(&mut self) -> StrykeResult<Vec<Expr>> {
18677        let mut args = Vec::new();
18678        loop {
18679            if matches!(
18680                self.peek(),
18681                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
18682            ) {
18683                break;
18684            }
18685            if self.peek_is_postfix_stmt_modifier_keyword() {
18686                break;
18687            }
18688            if self.peek_is_glob_par_progress_kw() {
18689                break;
18690            }
18691            args.push(self.parse_assign_expr()?);
18692            if !self.eat(&Token::Comma) {
18693                break;
18694            }
18695            if self.peek_is_glob_par_progress_kw() {
18696                break;
18697            }
18698        }
18699        Ok(args)
18700    }
18701
18702    /// `glob_pat EXPR, ...` or `glob_pat(...)` plus optional `, progress => EXPR` / inner `progress =>`.
18703    fn parse_glob_par_or_par_sed_args(&mut self) -> StrykeResult<(Vec<Expr>, Option<Box<Expr>>)> {
18704        if matches!(self.peek(), Token::LParen) {
18705            self.advance();
18706            let args = self.parse_pattern_list_until_rparen_or_progress()?;
18707            let progress = if self.peek_is_glob_par_progress_kw() {
18708                self.advance();
18709                self.expect(&Token::FatArrow)?;
18710                Some(Box::new(self.parse_assign_expr()?))
18711            } else {
18712                None
18713            };
18714            self.expect(&Token::RParen)?;
18715            Ok((args, progress))
18716        } else {
18717            let args = self.parse_pattern_list_glob_par_bare()?;
18718            // Comma after the last pattern was consumed inside `parse_pattern_list_glob_par_bare`.
18719            let progress = if self.peek_is_glob_par_progress_kw() {
18720                self.advance();
18721                self.expect(&Token::FatArrow)?;
18722                Some(Box::new(self.parse_assign_expr()?))
18723            } else {
18724                None
18725            };
18726            Ok((args, progress))
18727        }
18728    }
18729
18730    pub(crate) fn parse_arg_list(&mut self) -> StrykeResult<Vec<Expr>> {
18731        let mut args = Vec::new();
18732        // Inside `(...)`, `|>` is a normal operator again (e.g. `f(2 |> g, 3)`),
18733        // so shadow any outer paren-less-arg suppression from
18734        // `no_pipe_forward_depth`. Saturating so nested mixes are safe.
18735        let saved_no_pf = self.no_pipe_forward_depth;
18736        self.no_pipe_forward_depth = 0;
18737        while !matches!(
18738            self.peek(),
18739            Token::RParen | Token::RBracket | Token::RBrace | Token::Eof
18740        ) {
18741            let arg = match self.parse_assign_expr() {
18742                Ok(e) => e,
18743                Err(err) => {
18744                    self.no_pipe_forward_depth = saved_no_pf;
18745                    return Err(err);
18746                }
18747            };
18748            args.push(arg);
18749            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
18750                break;
18751            }
18752        }
18753        self.no_pipe_forward_depth = saved_no_pf;
18754        Ok(args)
18755    }
18756
18757    /// Parse a comma-separated list of slice subscript args. Each arg may be a regular
18758    /// expression, a closed range (`1:3`, `1..3:2`), or an open-ended Python-style colon
18759    /// range (`:`, `::`, `:N`, `N:`, `::-1`, `:N:M`, `N::M`, `::M`). Open-ended forms
18760    /// produce `ExprKind::SliceRange`; closed `1:3` produces `ExprKind::Range` (legacy).
18761    ///
18762    /// `is_hash` enables fat-comma-style bareword auto-quoting for endpoints — `{a:c:1}`
18763    /// treats `a` and `c` as string keys without quoting (cannot be a function call;
18764    /// use `func():other` if you actually want to invoke).
18765    pub(crate) fn parse_slice_arg_list(&mut self, is_hash: bool) -> StrykeResult<Vec<Expr>> {
18766        let mut args = Vec::new();
18767        let saved_no_pf = self.no_pipe_forward_depth;
18768        self.no_pipe_forward_depth = 0;
18769        while !matches!(
18770            self.peek(),
18771            Token::RParen | Token::RBracket | Token::RBrace | Token::Eof
18772        ) {
18773            let arg = match self.parse_slice_arg(is_hash) {
18774                Ok(e) => e,
18775                Err(err) => {
18776                    self.no_pipe_forward_depth = saved_no_pf;
18777                    return Err(err);
18778                }
18779            };
18780            args.push(arg);
18781            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
18782                break;
18783            }
18784        }
18785        self.no_pipe_forward_depth = saved_no_pf;
18786        Ok(args)
18787    }
18788
18789    /// Parse one slice subscript argument (see [`Self::parse_slice_arg_list`]).
18790    fn parse_slice_arg(&mut self, is_hash: bool) -> StrykeResult<Expr> {
18791        let line = self.peek_line();
18792
18793        // Open-start: `:` or `::` immediately
18794        if matches!(self.peek(), Token::Colon) {
18795            self.advance();
18796            return self.finish_slice_range(None, false, is_hash, line);
18797        }
18798        if matches!(self.peek(), Token::PackageSep) {
18799            self.advance();
18800            return self.finish_slice_range(None, true, is_hash, line);
18801        }
18802
18803        // Parse FROM with `:` suppressed inside `parse_range` so it doesn't get
18804        // consumed as a colon-range there — we want to handle the colon ourselves.
18805        self.suppress_colon_range = self.suppress_colon_range.saturating_add(1);
18806        let result = self.parse_slice_endpoint(is_hash);
18807        self.suppress_colon_range = self.suppress_colon_range.saturating_sub(1);
18808        let from_expr = result?;
18809
18810        // Trailing `:` or `::` after the FROM endpoint?
18811        if matches!(self.peek(), Token::Colon) {
18812            self.advance();
18813            return self.finish_slice_range(Some(Box::new(from_expr)), false, is_hash, line);
18814        }
18815        if matches!(self.peek(), Token::PackageSep) {
18816            self.advance();
18817            return self.finish_slice_range(Some(Box::new(from_expr)), true, is_hash, line);
18818        }
18819
18820        Ok(from_expr)
18821    }
18822
18823    /// After consuming the first colon (or `::` pair), parse the rest of the slice range.
18824    /// `double` is true if we just consumed `::` — TO is implicit `None`, the next
18825    /// expression (if any) is STEP.
18826    ///
18827    /// Returns `ExprKind::Range` for fully-closed forms (legacy compatibility) and
18828    /// `ExprKind::SliceRange` whenever any endpoint is omitted (open-ended).
18829    fn finish_slice_range(
18830        &mut self,
18831        from: Option<Box<Expr>>,
18832        double: bool,
18833        is_hash: bool,
18834        line: usize,
18835    ) -> StrykeResult<Expr> {
18836        let (to, step) = if double {
18837            // `::` so TO is implicit; STEP is whatever (if anything) follows.
18838            let step_v = self.parse_slice_optional_endpoint(is_hash)?;
18839            (None, step_v)
18840        } else {
18841            // single `:` — parse TO, then optional `:STEP`.
18842            let to_v = self.parse_slice_optional_endpoint(is_hash)?;
18843            let step_v = if matches!(self.peek(), Token::Colon) {
18844                self.advance();
18845                self.parse_slice_optional_endpoint(is_hash)?
18846            } else if matches!(self.peek(), Token::PackageSep) {
18847                return Err(
18848                    self.syntax_err("Unexpected `::` after slice TO endpoint".to_string(), line)
18849                );
18850            } else {
18851                None
18852            };
18853            (to_v, step_v)
18854        };
18855
18856        // Closed form (both endpoints present) — produce a regular `Range` so the
18857        // rest of the compiler/VM keeps reusing existing range-expansion paths.
18858        if let (Some(f), Some(t)) = (from.as_ref(), to.as_ref()) {
18859            return Ok(Expr {
18860                kind: ExprKind::Range {
18861                    from: f.clone(),
18862                    to: t.clone(),
18863                    exclusive: false,
18864                    step,
18865                },
18866                line,
18867            });
18868        }
18869
18870        Ok(Expr {
18871            kind: ExprKind::SliceRange { from, to, step },
18872            line,
18873        })
18874    }
18875
18876    /// Parse an optional slice endpoint: returns `None` if the next token closes the slice
18877    /// arg (`,`, `]`, `}`, or another `:`). Otherwise parses an endpoint expression.
18878    fn parse_slice_optional_endpoint(&mut self, is_hash: bool) -> StrykeResult<Option<Box<Expr>>> {
18879        if matches!(
18880            self.peek(),
18881            Token::Colon
18882                | Token::PackageSep
18883                | Token::Comma
18884                | Token::RBracket
18885                | Token::RBrace
18886                | Token::Eof
18887        ) {
18888            return Ok(None);
18889        }
18890        self.suppress_colon_range = self.suppress_colon_range.saturating_add(1);
18891        let r = self.parse_slice_endpoint(is_hash);
18892        self.suppress_colon_range = self.suppress_colon_range.saturating_sub(1);
18893        Ok(Some(Box::new(r?)))
18894    }
18895
18896    /// Parse a single slice endpoint expression. For hash slices, a bareword `Ident`
18897    /// followed by `:`, `::`, `,`, `]`, or `}` auto-quotes (fat-comma style); otherwise
18898    /// fall through to standard expression parsing. For array slices, no auto-quote.
18899    fn parse_slice_endpoint(&mut self, is_hash: bool) -> StrykeResult<Expr> {
18900        if is_hash {
18901            if let Token::Ident(name) = self.peek().clone() {
18902                if matches!(
18903                    self.peek_at(1),
18904                    Token::Colon
18905                        | Token::PackageSep
18906                        | Token::Comma
18907                        | Token::RBracket
18908                        | Token::RBrace
18909                ) {
18910                    let line = self.peek_line();
18911                    self.advance();
18912                    return Ok(Expr {
18913                        kind: ExprKind::String(name),
18914                        line,
18915                    });
18916                }
18917            }
18918        }
18919        self.parse_assign_expr()
18920    }
18921
18922    /// Arguments for `->name` / `->SUPER::name` **without** `(...)`. Unlike `die foo + 1`
18923    /// (unary `+` on `1` passed to `foo`), Perl treats `$o->meth + 5` as infix `+` after a
18924    /// no-arg method call; we must not consume that `+` as the start of a first argument.
18925    fn parse_method_arg_list_no_paren(&mut self) -> StrykeResult<Vec<Expr>> {
18926        let mut args = Vec::new();
18927        let call_line = self.prev_line();
18928        loop {
18929            // `$g->next { ... }` — `{` starts the enclosing statement's block, not an anonymous
18930            // hash argument to `next` (paren-less method call has no args here).
18931            if args.is_empty() && matches!(self.peek(), Token::LBrace) {
18932                break;
18933            }
18934            if matches!(
18935                self.peek(),
18936                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
18937            ) {
18938                break;
18939            }
18940            if let Token::Ident(ref kw) = self.peek().clone() {
18941                if matches!(
18942                    kw.as_str(),
18943                    "if" | "unless" | "while" | "until" | "for" | "foreach"
18944                ) {
18945                    break;
18946                }
18947            }
18948            // `foo($obj->meth, $x)` — comma separates *outer* args; it is not the start of a
18949            // paren-less method argument (those use spaces: `$obj->meth $a, $b`).
18950            if args.is_empty()
18951                && (self.peek_method_arg_infix_terminator() || matches!(self.peek(), Token::Comma))
18952            {
18953                break;
18954            }
18955            // Implicit semicolon: if no args collected yet and next token is on a different
18956            // line, treat newline as statement boundary. Allows `$p->method\nnext_stmt`.
18957            if args.is_empty() && self.peek_line() > call_line {
18958                break;
18959            }
18960            args.push(self.parse_assign_expr()?);
18961            if !self.eat(&Token::Comma) {
18962                break;
18963            }
18964        }
18965        Ok(args)
18966    }
18967
18968    /// Tokens that end a paren-less method arg list when no comma-separated args yet (infix on
18969    /// the whole `->meth` expression).
18970    fn peek_method_arg_infix_terminator(&self) -> bool {
18971        matches!(
18972            self.peek(),
18973            Token::Plus
18974                | Token::Minus
18975                | Token::Star
18976                | Token::Slash
18977                | Token::Percent
18978                | Token::Power
18979                | Token::Dot
18980                | Token::X
18981                | Token::NumEq
18982                | Token::NumNe
18983                | Token::NumLt
18984                | Token::NumGt
18985                | Token::NumLe
18986                | Token::NumGe
18987                | Token::Spaceship
18988                | Token::StrEq
18989                | Token::StrNe
18990                | Token::StrLt
18991                | Token::StrGt
18992                | Token::StrLe
18993                | Token::StrGe
18994                | Token::StrCmp
18995                | Token::LogAnd
18996                | Token::LogOr
18997                | Token::LogAndWord
18998                | Token::LogOrWord
18999                | Token::DefinedOr
19000                | Token::BitAnd
19001                | Token::BitOr
19002                | Token::BitXor
19003                | Token::ShiftLeft
19004                | Token::ShiftRight
19005                | Token::Range
19006                | Token::RangeExclusive
19007                | Token::BindMatch
19008                | Token::BindNotMatch
19009                | Token::Arrow
19010                // `($a->b) ? $a->c : $a->d` — `->c` must not slurp the ternary `:` / `?`.
19011                | Token::Question
19012                | Token::Colon
19013                // Assignment operators: `$obj->field = val` is setter sugar, not method arg.
19014                | Token::Assign
19015                | Token::PlusAssign
19016                | Token::MinusAssign
19017                | Token::MulAssign
19018                | Token::DivAssign
19019                | Token::ModAssign
19020                | Token::PowAssign
19021                | Token::DotAssign
19022                | Token::AndAssign
19023                | Token::OrAssign
19024                | Token::XorAssign
19025                | Token::DefinedOrAssign
19026                | Token::ShiftLeftAssign
19027                | Token::ShiftRightAssign
19028                | Token::BitAndAssign
19029                | Token::BitOrAssign
19030        )
19031    }
19032
19033    fn parse_list_until_terminator(&mut self) -> StrykeResult<Vec<Expr>> {
19034        self.parse_list_until_terminator_inner(false)
19035    }
19036
19037    /// Variant of `parse_list_until_terminator` that allows `|>` within arguments.
19038    /// Used by print-like statements (`p`, `say`, `print`, `printf`) so that
19039    /// `p @a |> sum` parses as `p(sum(@a))` rather than `sum(p(@a))`, matching
19040    /// the behavior of `~>` thread-first macro.
19041    fn parse_list_until_terminator_allow_pipe(&mut self) -> StrykeResult<Vec<Expr>> {
19042        self.parse_list_until_terminator_inner(true)
19043    }
19044
19045    fn parse_list_until_terminator_inner(&mut self, allow_pipe: bool) -> StrykeResult<Vec<Expr>> {
19046        let mut args = Vec::new();
19047        // Line of the last consumed token (the keyword / function name that
19048        // triggered this arg parse).  Used for implicit-semicolon: if no args
19049        // have been parsed yet and the next token is on a *different* line,
19050        // treat the newline as a statement boundary and stop.
19051        let call_line = self.prev_line();
19052        loop {
19053            // When `allow_pipe` is false, `|>` terminates the list (preserving
19054            // left-associativity for chains like `@a |> head 2 |> join "-"`).
19055            // When true (print-like statements), `|>` is allowed within args.
19056            let is_terminator = if allow_pipe {
19057                matches!(
19058                    self.peek(),
19059                    Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
19060                )
19061            } else {
19062                matches!(
19063                    self.peek(),
19064                    Token::Semicolon
19065                        | Token::RBrace
19066                        | Token::RParen
19067                        | Token::Eof
19068                        | Token::PipeForward
19069                )
19070            };
19071            if is_terminator {
19072                break;
19073            }
19074            // Check for postfix modifiers — stop before `expr for LIST` / `expr if COND` etc.
19075            if let Token::Ident(ref kw) = self.peek().clone() {
19076                if matches!(
19077                    kw.as_str(),
19078                    "if" | "unless" | "while" | "until" | "for" | "foreach"
19079                ) {
19080                    break;
19081                }
19082            }
19083            // Implicit semicolons: if no args have been collected yet and the
19084            // next token is on a different line from the call keyword, treat
19085            // the newline as a statement boundary.  This prevents paren-less
19086            // calls (`say`, `print`, user subs) from greedily swallowing the
19087            // *next* statement when the author omitted a semicolon.
19088            // After a comma continuation, multi-line arg lists still work.
19089            if args.is_empty() && self.peek_line() > call_line {
19090                break;
19091            }
19092            // When `allow_pipe` is true, pipe chains are consumed within each
19093            // argument. When false, `|>` terminates the whole call list, so
19094            // individual args must not absorb a following `|>`.
19095            if allow_pipe {
19096                args.push(self.parse_assign_expr()?);
19097            } else {
19098                args.push(self.parse_assign_expr_stop_at_pipe()?);
19099            }
19100            if !self.eat(&Token::Comma) {
19101                break;
19102            }
19103        }
19104        Ok(args)
19105    }
19106
19107    /// Body of `+{ ... }` — Perl's force-hashref idiom. The opening `+` and `{`
19108    /// have already been consumed. Tries the normal `KEY => VAL, …` shape first
19109    /// (so `+{ a => 1, b => 2 }` is identical to `{ a => 1, b => 2 }`); on
19110    /// failure falls back to "single list-yielding expression treated as a
19111    /// flat key/value spread" so `+{ map { (k, v) } LIST }` works without
19112    /// the user needing a temp `my %h = ...; \%h` shuffle.
19113    fn parse_forced_hashref_body(&mut self, line: usize) -> StrykeResult<Expr> {
19114        let saved = self.pos;
19115        if let Ok(pairs) = self.try_parse_hash_ref() {
19116            return Ok(Expr {
19117                kind: ExprKind::HashRef(pairs),
19118                line,
19119            });
19120        }
19121        // Empty `+{}` is the empty hashref.
19122        self.pos = saved;
19123        if matches!(self.peek(), Token::RBrace) {
19124            self.advance();
19125            return Ok(Expr {
19126                kind: ExprKind::HashRef(vec![]),
19127                line,
19128            });
19129        }
19130        // Single expression — eval as list, flatten into key/value pairs via the
19131        // existing __HASH_SPREAD__ sentinel that `ExprKind::HashRef` already
19132        // handles in [`Interpreter::eval_expr`].
19133        let inner = self.parse_expression()?;
19134        self.expect(&Token::RBrace)?;
19135        let sentinel_key = Expr {
19136            kind: ExprKind::String("__HASH_SPREAD__".into()),
19137            line,
19138        };
19139        Ok(Expr {
19140            kind: ExprKind::HashRef(vec![(sentinel_key, inner)]),
19141            line,
19142        })
19143    }
19144
19145    fn try_parse_hash_ref(&mut self) -> StrykeResult<Vec<(Expr, Expr)>> {
19146        let mut pairs = Vec::new();
19147        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
19148            // Perl autoquotes a bareword immediately before `=>` (hash key), even for keywords like
19149            // `pos`, `bless`, `return` — see Text::Balanced `_failmsg` (`pos => $pos`).
19150            // Stryke exception: topic-slot barewords (`_`, `_<`, `_0`, `_0<`, `_!N!`, …)
19151            // resolve to the topic value, not the literal name — `{ _ => 1 }` ≡ `{ $_ => 1 }`.
19152            let line = self.peek_line();
19153            let key = if let Token::Ident(ref name) = self.peek().clone() {
19154                if matches!(self.peek_at(1), Token::FatArrow)
19155                    && !Self::is_underscore_topic_slot(name)
19156                {
19157                    self.advance();
19158                    Expr {
19159                        kind: ExprKind::String(name.clone()),
19160                        line,
19161                    }
19162                } else {
19163                    self.parse_assign_expr()?
19164                }
19165            } else {
19166                self.parse_assign_expr()?
19167            };
19168            // If the key expression is a hash/array variable and is followed by `}` or `,`
19169            // with no `=>`, treat the whole thing as a hash-from-expression construction.
19170            // This handles `{ %a }`, `{ %a, key => val }`, etc.
19171            if matches!(self.peek(), Token::RBrace | Token::Comma)
19172                && matches!(
19173                    key.kind,
19174                    ExprKind::HashVar(_)
19175                        | ExprKind::Deref {
19176                            kind: Sigil::Hash,
19177                            ..
19178                        }
19179                )
19180            {
19181                // Synthesize a pair whose key/value is spread from the hash expression.
19182                // Use a sentinel "spread" pair: key=the hash expr, value=undef.
19183                // The evaluator will flatten this.
19184                let sentinel_key = Expr {
19185                    kind: ExprKind::String("__HASH_SPREAD__".into()),
19186                    line,
19187                };
19188                pairs.push((sentinel_key, key));
19189                self.eat(&Token::Comma);
19190                continue;
19191            }
19192            // Expect => or , after key
19193            if self.eat(&Token::FatArrow) || self.eat(&Token::Comma) {
19194                let val = self.parse_assign_expr()?;
19195                pairs.push((key, val));
19196                self.eat(&Token::Comma);
19197            } else {
19198                return Err(self.syntax_err("Expected => or , in hash ref", key.line));
19199            }
19200        }
19201        self.expect(&Token::RBrace)?;
19202        Ok(pairs)
19203    }
19204
19205    /// Parse `key => val, key => val, ...` up to (but not consuming) `term`.
19206    /// Used by the `%[…]` and `%{k=>v,…}` sugar to build an inline hashref
19207    /// AST node, sidestepping the block/hashref ambiguity that `try_parse_hash_ref`
19208    /// navigates. Caller expects and consumes `term` itself.
19209    fn parse_hashref_pairs_until(&mut self, term: &Token) -> StrykeResult<Vec<(Expr, Expr)>> {
19210        let mut pairs = Vec::new();
19211        while !matches!(&self.peek(), t if std::mem::discriminant(*t) == std::mem::discriminant(term))
19212            && !matches!(self.peek(), Token::Eof)
19213        {
19214            let line = self.peek_line();
19215            let key = if let Token::Ident(ref name) = self.peek().clone() {
19216                if matches!(self.peek_at(1), Token::FatArrow)
19217                    && !Self::is_underscore_topic_slot(name)
19218                {
19219                    self.advance();
19220                    Expr {
19221                        kind: ExprKind::String(name.clone()),
19222                        line,
19223                    }
19224                } else {
19225                    self.parse_assign_expr()?
19226                }
19227            } else {
19228                self.parse_assign_expr()?
19229            };
19230            if self.eat(&Token::FatArrow) || self.eat(&Token::Comma) {
19231                let val = self.parse_assign_expr()?;
19232                pairs.push((key, val));
19233                self.eat(&Token::Comma);
19234            } else {
19235                return Err(self.syntax_err("Expected => or , in hash ref", key.line));
19236            }
19237        }
19238        Ok(pairs)
19239    }
19240
19241    /// Inside an interpolated string, after a `$name`/`${EXPR}`/`$name[i]`/`$name{k}` base
19242    /// expression, consume any chain of `->[…]`, `->{…}`, **adjacent** `[…]`, or `{…}`
19243    /// subscripts. Perl auto-implies `->` between consecutive subscripts, so
19244    /// `$matrix[1][1]` is `$matrix[1]->[1]` and `$h{a}{b}` is `$h{a}->{b}`.
19245    /// Each step wraps the current expression in an `ArrowDeref`.
19246    fn interp_chain_subscripts(
19247        &self,
19248        chars: &[char],
19249        i: &mut usize,
19250        mut base: Expr,
19251        line: usize,
19252    ) -> Expr {
19253        loop {
19254            // Optional `->` connector
19255            let (after, requires_subscript) =
19256                if *i + 1 < chars.len() && chars[*i] == '-' && chars[*i + 1] == '>' {
19257                    (*i + 2, true)
19258                } else {
19259                    (*i, false)
19260                };
19261            if after >= chars.len() {
19262                break;
19263            }
19264            match chars[after] {
19265                '[' => {
19266                    *i = after + 1;
19267                    let mut idx_str = String::new();
19268                    while *i < chars.len() && chars[*i] != ']' {
19269                        idx_str.push(chars[*i]);
19270                        *i += 1;
19271                    }
19272                    if *i < chars.len() {
19273                        *i += 1;
19274                    }
19275                    let idx_expr = if let Some(rest) = idx_str.strip_prefix('$') {
19276                        Expr {
19277                            kind: ExprKind::ScalarVar(rest.to_string()),
19278                            line,
19279                        }
19280                    } else if let Ok(n) = idx_str.parse::<i64>() {
19281                        Expr {
19282                            kind: ExprKind::Integer(n),
19283                            line,
19284                        }
19285                    } else {
19286                        Expr {
19287                            kind: ExprKind::String(idx_str),
19288                            line,
19289                        }
19290                    };
19291                    base = Expr {
19292                        kind: ExprKind::ArrowDeref {
19293                            expr: Box::new(base),
19294                            index: Box::new(idx_expr),
19295                            kind: DerefKind::Array,
19296                        },
19297                        line,
19298                    };
19299                }
19300                '{' => {
19301                    *i = after + 1;
19302                    let mut key = String::new();
19303                    let mut depth = 1usize;
19304                    while *i < chars.len() && depth > 0 {
19305                        if chars[*i] == '{' {
19306                            depth += 1;
19307                        } else if chars[*i] == '}' {
19308                            depth -= 1;
19309                            if depth == 0 {
19310                                break;
19311                            }
19312                        }
19313                        key.push(chars[*i]);
19314                        *i += 1;
19315                    }
19316                    if *i < chars.len() {
19317                        *i += 1;
19318                    }
19319                    let key_expr = if let Some(rest) = key.strip_prefix('$') {
19320                        Expr {
19321                            kind: ExprKind::ScalarVar(rest.to_string()),
19322                            line,
19323                        }
19324                    } else {
19325                        Expr {
19326                            kind: ExprKind::String(key),
19327                            line,
19328                        }
19329                    };
19330                    base = Expr {
19331                        kind: ExprKind::ArrowDeref {
19332                            expr: Box::new(base),
19333                            index: Box::new(key_expr),
19334                            kind: DerefKind::Hash,
19335                        },
19336                        line,
19337                    };
19338                }
19339                _ => {
19340                    if requires_subscript {
19341                        // `->method()` etc — not interpolated, leave for literal output.
19342                    }
19343                    break;
19344                }
19345            }
19346        }
19347        base
19348    }
19349
19350    /// Reject `$a` / `$b` references in `--no-interop` mode (lexer catches them
19351    /// outside double-quoted strings; this catches the in-string interpolation
19352    /// path which has its own parser bypassing `Token::ScalarVar`).
19353    fn no_interop_check_scalar_var_name(&self, name: &str, line: usize) -> StrykeResult<()> {
19354        if crate::no_interop_mode() && (name == "a" || name == "b") {
19355            return Err(self.syntax_err(
19356                format!(
19357                    "stryke uses `_` / `_1` (bareword in code) or `$_` / `$_1` \
19358                     (sigil inside string interpolation / when whitespace would \
19359                     change parsing) instead of `${}` (--no-interop is active)",
19360                    name
19361                ),
19362                line,
19363            ));
19364        }
19365        Ok(())
19366    }
19367
19368    fn parse_interpolated_string(&self, s: &str, line: usize) -> StrykeResult<Expr> {
19369        // Parse $var and @var inside double-quoted strings
19370        let mut parts = Vec::new();
19371        let mut literal = String::new();
19372        let chars: Vec<char> = s.chars().collect();
19373        let mut i = 0;
19374
19375        'istr: while i < chars.len() {
19376            if chars[i] == LITERAL_DOLLAR_IN_DQUOTE {
19377                literal.push('$');
19378                i += 1;
19379                continue;
19380            }
19381            if chars[i] == LITERAL_AT_IN_DQUOTE {
19382                literal.push('@');
19383                i += 1;
19384                continue;
19385            }
19386            // "\\$x" in source: one backslash in the string, then interpolate $x (Perl double-quoted string).
19387            if chars[i] == '\\' && i + 1 < chars.len() && chars[i + 1] == '$' {
19388                literal.push('\\');
19389                i += 1;
19390                // i now points at '$' — fall through to $ handling below
19391            }
19392            if chars[i] == '$' && i + 1 < chars.len() {
19393                if !literal.is_empty() {
19394                    parts.push(StringPart::Literal(std::mem::take(&mut literal)));
19395                }
19396                i += 1; // past `$`
19397                        // Perl allows whitespace between `$` and the variable name (`$ foo` → `$foo`).
19398                while i < chars.len() && chars[i].is_whitespace() {
19399                    i += 1;
19400                }
19401                if i >= chars.len() {
19402                    return Err(self.syntax_err("Final $ should be \\$ or $name", line));
19403                }
19404                // `$#name` — last index of `@name` (Perl `$#array`).
19405                if chars[i] == '#' {
19406                    i += 1;
19407                    let mut sname = String::from("#");
19408                    while i < chars.len()
19409                        && (chars[i].is_alphanumeric() || chars[i] == '_' || chars[i] == ':')
19410                    {
19411                        sname.push(chars[i]);
19412                        i += 1;
19413                    }
19414                    while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
19415                        sname.push_str("::");
19416                        i += 2;
19417                        while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
19418                            sname.push(chars[i]);
19419                            i += 1;
19420                        }
19421                    }
19422                    self.no_interop_check_scalar_var_name(&sname, line)?;
19423                    parts.push(StringPart::ScalarVar(sname));
19424                    continue;
19425                }
19426                // `$$` — process id (Perl `$$`), only when the two `$` are adjacent (no whitespace
19427                // between) and the second `$` is not followed by a word character or digit (`$$x`
19428                // / `$$_` / `$$0` are `$` + `$x` / `$_` / `$0`).
19429                if chars[i] == '$' {
19430                    let next_c = chars.get(i + 1).copied();
19431                    let is_pid = match next_c {
19432                        None => true,
19433                        Some(c)
19434                            if !c.is_ascii_digit() && !matches!(c, 'A'..='Z' | 'a'..='z' | '_') =>
19435                        {
19436                            true
19437                        }
19438                        _ => false,
19439                    };
19440                    if is_pid {
19441                        parts.push(StringPart::ScalarVar("$$".to_string()));
19442                        i += 1; // consume second `$`
19443                        continue;
19444                    }
19445                    i += 1; // skip second `$` — same as a single `$` before the identifier
19446                }
19447                if chars[i] == '{' {
19448                    // `${…}` — braced variable OR expression interpolation.
19449                    //   `${name}`              → ScalarVar(name)        (Perl standard)
19450                    //   `${$ref}` / `${\EXPR}` → deref the expression   (Perl standard)
19451                    //   `${name}[idx]` / `${name}{k}` / `${$r}[i]` …    chain after `}`
19452                    // stryke's prior `#{expr}` form remains supported elsewhere.
19453                    i += 1;
19454                    let mut inner = String::new();
19455                    let mut depth = 1usize;
19456                    while i < chars.len() && depth > 0 {
19457                        match chars[i] {
19458                            '{' => depth += 1,
19459                            '}' => {
19460                                depth -= 1;
19461                                if depth == 0 {
19462                                    break;
19463                                }
19464                            }
19465                            _ => {}
19466                        }
19467                        inner.push(chars[i]);
19468                        i += 1;
19469                    }
19470                    if i < chars.len() {
19471                        i += 1; // skip closing }
19472                    }
19473
19474                    // Distinguish "name" from "expression". If trimmed inner starts with
19475                    // `$`, `\`, or contains operator/punctuation chars, treat as Perl
19476                    // expression and emit a scalar deref. Otherwise, plain variable name.
19477                    let trimmed = inner.trim();
19478                    let is_expr = trimmed.starts_with('$')
19479                        || trimmed.starts_with('\\')
19480                        || trimmed.starts_with('@')   // `${@arr}` rare but valid
19481                        || trimmed.starts_with('%')   // `${%h}`   rare but valid
19482                        || trimmed.contains(['(', '+', '-', '*', '/', '.', '?', '&', '|']);
19483                    let mut base: Expr = if is_expr {
19484                        // Re-parse the inner content as a Perl expression. Wrap in
19485                        // `Deref { kind: Sigil::Scalar }` to dereference the resulting
19486                        // scalar reference (Perl: `${$r}` ≡ `$$r`).
19487                        match parse_expression_from_str(trimmed, "<interp>") {
19488                            Ok(e) => Expr {
19489                                kind: ExprKind::Deref {
19490                                    expr: Box::new(e),
19491                                    kind: Sigil::Scalar,
19492                                },
19493                                line,
19494                            },
19495                            Err(_) => Expr {
19496                                kind: ExprKind::ScalarVar(inner.clone()),
19497                                line,
19498                            },
19499                        }
19500                    } else {
19501                        // Treat as a plain (possibly qualified) variable name.
19502                        self.no_interop_check_scalar_var_name(&inner, line)?;
19503                        Expr {
19504                            kind: ExprKind::ScalarVar(inner),
19505                            line,
19506                        }
19507                    };
19508
19509                    // After `${…}` we may see `[idx]` / `{key}` for indexing into the
19510                    // dereferenced array/hash (`${$ar}[1]`, `${$hr}{k}`), and arrow
19511                    // chains thereafter.
19512                    base = self.interp_chain_subscripts(&chars, &mut i, base, line);
19513                    parts.push(StringPart::Expr(base));
19514                } else if chars[i] == '^' {
19515                    // `$^V`, `$^O`, … — name stored as `^V`, `^O`, … (see [`Interpreter::get_special_var`]).
19516                    let mut name = String::from("^");
19517                    i += 1;
19518                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
19519                        name.push(chars[i]);
19520                        i += 1;
19521                    }
19522                    if i < chars.len() && chars[i] == '{' {
19523                        i += 1; // skip {
19524                        let mut key = String::new();
19525                        let mut depth = 1;
19526                        while i < chars.len() && depth > 0 {
19527                            if chars[i] == '{' {
19528                                depth += 1;
19529                            } else if chars[i] == '}' {
19530                                depth -= 1;
19531                                if depth == 0 {
19532                                    break;
19533                                }
19534                            }
19535                            key.push(chars[i]);
19536                            i += 1;
19537                        }
19538                        if i < chars.len() {
19539                            i += 1;
19540                        }
19541                        let key_expr = if let Some(rest) = key.strip_prefix('$') {
19542                            Expr {
19543                                kind: ExprKind::ScalarVar(rest.to_string()),
19544                                line,
19545                            }
19546                        } else {
19547                            Expr {
19548                                kind: ExprKind::String(key),
19549                                line,
19550                            }
19551                        };
19552                        parts.push(StringPart::Expr(Expr {
19553                            kind: ExprKind::HashElement {
19554                                hash: name,
19555                                key: Box::new(key_expr),
19556                            },
19557                            line,
19558                        }));
19559                    } else if i < chars.len() && chars[i] == '[' {
19560                        i += 1;
19561                        let mut idx_str = String::new();
19562                        while i < chars.len() && chars[i] != ']' {
19563                            idx_str.push(chars[i]);
19564                            i += 1;
19565                        }
19566                        if i < chars.len() {
19567                            i += 1;
19568                        }
19569                        let idx_expr = if let Some(rest) = idx_str.strip_prefix('$') {
19570                            Expr {
19571                                kind: ExprKind::ScalarVar(rest.to_string()),
19572                                line,
19573                            }
19574                        } else if let Ok(n) = idx_str.parse::<i64>() {
19575                            Expr {
19576                                kind: ExprKind::Integer(n),
19577                                line,
19578                            }
19579                        } else {
19580                            Expr {
19581                                kind: ExprKind::String(idx_str),
19582                                line,
19583                            }
19584                        };
19585                        parts.push(StringPart::Expr(Expr {
19586                            kind: ExprKind::ArrayElement {
19587                                array: name,
19588                                index: Box::new(idx_expr),
19589                            },
19590                            line,
19591                        }));
19592                    } else {
19593                        self.no_interop_check_scalar_var_name(&name, line)?;
19594                        parts.push(StringPart::ScalarVar(name));
19595                    }
19596                } else if chars[i].is_alphabetic() || chars[i] == '_' {
19597                    let mut name = String::new();
19598                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
19599                        name.push(chars[i]);
19600                        i += 1;
19601                    }
19602                    // Package-qualified names: `$Foo::x`, `$Foo::Bar::baz`. Mirror
19603                    // the `$#Foo::a` continuation logic. Without this, `"$Foo::x"`
19604                    // captures only `Foo` and leaves `::x` as literal text — the
19605                    // interpolation reads bare `$Foo`, which is undef.
19606                    while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
19607                        name.push_str("::");
19608                        i += 2;
19609                        while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
19610                            name.push(chars[i]);
19611                            i += 1;
19612                        }
19613                    }
19614                    // `$_<`, `$_<<`, … — outer topic (stryke extension). Also
19615                    // `$_N<`, `$_N<<` for positional aliases. And the indexed
19616                    // shortcut `$_<N` ≡ `$_<<<...<` (N chevrons), so `"$_<3"`
19617                    // and `"$_<<<"` interpolate identically.
19618                    let is_topic_slot = name == "_"
19619                        || (name.len() > 1
19620                            && name.starts_with('_')
19621                            && name[1..].bytes().all(|b| b.is_ascii_digit()));
19622                    if is_topic_slot {
19623                        // Try indexed-ascent first: `<` immediately followed by digits.
19624                        let try_indexed = chars.get(i) == Some(&'<')
19625                            && chars.get(i + 1).is_some_and(|c| c.is_ascii_digit());
19626                        let mut handled_indexed = false;
19627                        if try_indexed {
19628                            let mut j = i + 1;
19629                            while j < chars.len() && chars[j].is_ascii_digit() {
19630                                j += 1;
19631                            }
19632                            let digits: String = chars[i + 1..j].iter().collect();
19633                            if let Ok(n) = digits.parse::<usize>() {
19634                                if n >= 1 {
19635                                    for _ in 0..n {
19636                                        name.push('<');
19637                                    }
19638                                    i = j;
19639                                    handled_indexed = true;
19640                                }
19641                            }
19642                        }
19643                        if !handled_indexed {
19644                            while i < chars.len() && chars[i] == '<' {
19645                                name.push('<');
19646                                i += 1;
19647                            }
19648                        }
19649                    }
19650                    // `--no-interop`: `$a` / `$b` are Perl-isms; reject inside
19651                    // string interpolation too. Catches both `"$a"` and `"$a[0]"`
19652                    // / `"$a{k}"` / `"$a->[0]"` because every branch below uses
19653                    // `name` to build the expression.
19654                    self.no_interop_check_scalar_var_name(&name, line)?;
19655                    // Build the base expression, then thread arrow-deref chains
19656                    // (`->[…]` / `->{…}`) onto it so things like `$ar->[2]`,
19657                    // `$href->{k}`, and chained `$x->{a}[1]->{b}` interpolate
19658                    // correctly inside double-quoted strings (Perl convention).
19659                    let mut base = if i < chars.len() && chars[i] == '{' {
19660                        // $hash{key}
19661                        i += 1; // skip {
19662                        let mut key = String::new();
19663                        let mut depth = 1;
19664                        while i < chars.len() && depth > 0 {
19665                            if chars[i] == '{' {
19666                                depth += 1;
19667                            } else if chars[i] == '}' {
19668                                depth -= 1;
19669                                if depth == 0 {
19670                                    break;
19671                                }
19672                            }
19673                            key.push(chars[i]);
19674                            i += 1;
19675                        }
19676                        if i < chars.len() {
19677                            i += 1;
19678                        } // skip }
19679                        let key_expr = if let Some(rest) = key.strip_prefix('$') {
19680                            Expr {
19681                                kind: ExprKind::ScalarVar(rest.to_string()),
19682                                line,
19683                            }
19684                        } else {
19685                            Expr {
19686                                kind: ExprKind::String(key),
19687                                line,
19688                            }
19689                        };
19690                        Expr {
19691                            kind: ExprKind::HashElement {
19692                                hash: name,
19693                                key: Box::new(key_expr),
19694                            },
19695                            line,
19696                        }
19697                    } else if i < chars.len() && chars[i] == '[' {
19698                        // $array[idx]
19699                        i += 1;
19700                        let mut idx_str = String::new();
19701                        while i < chars.len() && chars[i] != ']' {
19702                            idx_str.push(chars[i]);
19703                            i += 1;
19704                        }
19705                        if i < chars.len() {
19706                            i += 1;
19707                        }
19708                        let idx_expr = if let Some(rest) = idx_str.strip_prefix('$') {
19709                            Expr {
19710                                kind: ExprKind::ScalarVar(rest.to_string()),
19711                                line,
19712                            }
19713                        } else if let Ok(n) = idx_str.parse::<i64>() {
19714                            Expr {
19715                                kind: ExprKind::Integer(n),
19716                                line,
19717                            }
19718                        } else {
19719                            Expr {
19720                                kind: ExprKind::String(idx_str),
19721                                line,
19722                            }
19723                        };
19724                        Expr {
19725                            kind: ExprKind::ArrayElement {
19726                                array: name,
19727                                index: Box::new(idx_expr),
19728                            },
19729                            line,
19730                        }
19731                    } else {
19732                        // Bare $name — defer to the chain-extension loop below.
19733                        Expr {
19734                            kind: ExprKind::ScalarVar(name),
19735                            line,
19736                        }
19737                    };
19738
19739                    // Chain `->[…]` / `->{…}` AND adjacent `[…]` / `{…}` — Perl
19740                    // implies `->` between consecutive subscripts (`$m[1][2]`
19741                    // ≡ `$m[1]->[2]`).  See `interp_chain_subscripts`.
19742                    base = self.interp_chain_subscripts(&chars, &mut i, base, line);
19743                    parts.push(StringPart::Expr(base));
19744                } else if chars[i].is_ascii_digit() {
19745                    // $0 (program name), $1…$n (regexp captures). Perl disallows $01, $02, …
19746                    if chars[i] == '0' {
19747                        i += 1;
19748                        if i < chars.len() && chars[i].is_ascii_digit() {
19749                            return Err(self.syntax_err(
19750                                "Numeric variables with more than one digit may not start with '0'",
19751                                line,
19752                            ));
19753                        }
19754                        parts.push(StringPart::ScalarVar("0".into()));
19755                    } else {
19756                        let start = i;
19757                        while i < chars.len() && chars[i].is_ascii_digit() {
19758                            i += 1;
19759                        }
19760                        parts.push(StringPart::ScalarVar(chars[start..i].iter().collect()));
19761                    }
19762                } else {
19763                    let c = chars[i];
19764                    let probe = c.to_string();
19765                    // `&` is the regex-match special var — semantically symmetric with
19766                    // backtick (`$``) prematch and apostrophe (`$'`) postmatch which
19767                    // are already handled here. `is_special_scalar_name_for_get` doesn't
19768                    // currently list `&`/`'`/`` ` `` (those have separate runtime paths
19769                    // for set/clear under regex updates), so we add them inline.
19770                    if VMHelper::is_special_scalar_name_for_get(&probe)
19771                        || matches!(c, '\'' | '`' | '&')
19772                    {
19773                        i += 1;
19774                        // Check for hash element access: `$+{key}`, `$-{key}`, etc.
19775                        if i < chars.len() && chars[i] == '{' {
19776                            i += 1; // skip {
19777                            let mut key = String::new();
19778                            let mut depth = 1;
19779                            while i < chars.len() && depth > 0 {
19780                                if chars[i] == '{' {
19781                                    depth += 1;
19782                                } else if chars[i] == '}' {
19783                                    depth -= 1;
19784                                    if depth == 0 {
19785                                        break;
19786                                    }
19787                                }
19788                                key.push(chars[i]);
19789                                i += 1;
19790                            }
19791                            if i < chars.len() {
19792                                i += 1;
19793                            } // skip }
19794                            let key_expr = if let Some(rest) = key.strip_prefix('$') {
19795                                Expr {
19796                                    kind: ExprKind::ScalarVar(rest.to_string()),
19797                                    line,
19798                                }
19799                            } else {
19800                                Expr {
19801                                    kind: ExprKind::String(key),
19802                                    line,
19803                                }
19804                            };
19805                            let mut base = Expr {
19806                                kind: ExprKind::HashElement {
19807                                    hash: probe,
19808                                    key: Box::new(key_expr),
19809                                },
19810                                line,
19811                            };
19812                            base = self.interp_chain_subscripts(&chars, &mut i, base, line);
19813                            parts.push(StringPart::Expr(base));
19814                        } else {
19815                            // Check for arrow deref chain: `$@->{key}`, etc.
19816                            let mut base = Expr {
19817                                kind: ExprKind::ScalarVar(probe),
19818                                line,
19819                            };
19820                            base = self.interp_chain_subscripts(&chars, &mut i, base, line);
19821                            if matches!(base.kind, ExprKind::ScalarVar(_)) {
19822                                // No chain extension — use the simpler ScalarVar part
19823                                if let ExprKind::ScalarVar(name) = base.kind {
19824                                    self.no_interop_check_scalar_var_name(&name, line)?;
19825                                    parts.push(StringPart::ScalarVar(name));
19826                                }
19827                            } else {
19828                                parts.push(StringPart::Expr(base));
19829                            }
19830                        }
19831                    } else {
19832                        literal.push('$');
19833                        literal.push(c);
19834                        i += 1;
19835                    }
19836                }
19837            } else if chars[i] == '@' && i + 1 < chars.len() {
19838                let next = chars[i + 1];
19839                // `@$aref` / `@${expr}` — array dereference in interpolation (Perl `"@$r"` → elements of @$r).
19840                if next == '$' {
19841                    if !literal.is_empty() {
19842                        parts.push(StringPart::Literal(std::mem::take(&mut literal)));
19843                    }
19844                    i += 1; // past `@`
19845                    debug_assert_eq!(chars[i], '$');
19846                    i += 1; // past `$`
19847                    while i < chars.len() && chars[i].is_whitespace() {
19848                        i += 1;
19849                    }
19850                    if i >= chars.len() {
19851                        return Err(self.syntax_err(
19852                            "Expected variable or block after `@$` in double-quoted string",
19853                            line,
19854                        ));
19855                    }
19856                    let inner_expr = if chars[i] == '{' {
19857                        i += 1;
19858                        let start = i;
19859                        let mut depth = 1usize;
19860                        while i < chars.len() && depth > 0 {
19861                            match chars[i] {
19862                                '{' => depth += 1,
19863                                '}' => {
19864                                    depth -= 1;
19865                                    if depth == 0 {
19866                                        break;
19867                                    }
19868                                }
19869                                _ => {}
19870                            }
19871                            i += 1;
19872                        }
19873                        if depth != 0 {
19874                            return Err(self.syntax_err(
19875                                "Unterminated `${ ... }` after `@` in double-quoted string",
19876                                line,
19877                            ));
19878                        }
19879                        let inner: String = chars[start..i].iter().collect();
19880                        i += 1; // closing `}`
19881                        parse_expression_from_str(inner.trim(), "-e")?
19882                    } else {
19883                        let mut name = String::new();
19884                        if chars[i] == '^' {
19885                            name.push('^');
19886                            i += 1;
19887                            while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_')
19888                            {
19889                                name.push(chars[i]);
19890                                i += 1;
19891                            }
19892                        } else {
19893                            while i < chars.len()
19894                                && (chars[i].is_alphanumeric()
19895                                    || chars[i] == '_'
19896                                    || chars[i] == ':')
19897                            {
19898                                name.push(chars[i]);
19899                                i += 1;
19900                            }
19901                            while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
19902                                name.push_str("::");
19903                                i += 2;
19904                                while i < chars.len()
19905                                    && (chars[i].is_alphanumeric() || chars[i] == '_')
19906                                {
19907                                    name.push(chars[i]);
19908                                    i += 1;
19909                                }
19910                            }
19911                        }
19912                        if name.is_empty() {
19913                            return Err(self.syntax_err(
19914                                "Expected identifier after `@$` in double-quoted string",
19915                                line,
19916                            ));
19917                        }
19918                        Expr {
19919                            kind: ExprKind::ScalarVar(name),
19920                            line,
19921                        }
19922                    };
19923                    parts.push(StringPart::Expr(Expr {
19924                        kind: ExprKind::Deref {
19925                            expr: Box::new(inner_expr),
19926                            kind: Sigil::Array,
19927                        },
19928                        line,
19929                    }));
19930                    continue 'istr;
19931                }
19932                if next == '{' {
19933                    if !literal.is_empty() {
19934                        parts.push(StringPart::Literal(std::mem::take(&mut literal)));
19935                    }
19936                    i += 2; // `@{`
19937                    let start = i;
19938                    let mut depth = 1usize;
19939                    while i < chars.len() && depth > 0 {
19940                        match chars[i] {
19941                            '{' => depth += 1,
19942                            '}' => {
19943                                depth -= 1;
19944                                if depth == 0 {
19945                                    break;
19946                                }
19947                            }
19948                            _ => {}
19949                        }
19950                        i += 1;
19951                    }
19952                    if depth != 0 {
19953                        return Err(
19954                            self.syntax_err("Unterminated @{ ... } in double-quoted string", line)
19955                        );
19956                    }
19957                    let inner: String = chars[start..i].iter().collect();
19958                    i += 1; // closing `}`
19959                    let inner_expr = parse_expression_from_str(inner.trim(), "-e")?;
19960                    parts.push(StringPart::Expr(Expr {
19961                        kind: ExprKind::Deref {
19962                            expr: Box::new(inner_expr),
19963                            kind: Sigil::Array,
19964                        },
19965                        line,
19966                    }));
19967                    continue 'istr;
19968                }
19969                if !(next.is_alphabetic() || next == '_' || next == '+' || next == '-') {
19970                    literal.push(chars[i]);
19971                    i += 1;
19972                } else {
19973                    if !literal.is_empty() {
19974                        parts.push(StringPart::Literal(std::mem::take(&mut literal)));
19975                    }
19976                    i += 1;
19977                    let mut name = String::new();
19978                    if i < chars.len() && (chars[i] == '+' || chars[i] == '-') {
19979                        name.push(chars[i]);
19980                        i += 1;
19981                    } else {
19982                        while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
19983                            name.push(chars[i]);
19984                            i += 1;
19985                        }
19986                        while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
19987                            name.push_str("::");
19988                            i += 2;
19989                            while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_')
19990                            {
19991                                name.push(chars[i]);
19992                                i += 1;
19993                            }
19994                        }
19995                    }
19996                    if i < chars.len() && chars[i] == '[' {
19997                        i += 1;
19998                        let start_inner = i;
19999                        let mut depth = 1usize;
20000                        while i < chars.len() && depth > 0 {
20001                            match chars[i] {
20002                                '[' => depth += 1,
20003                                ']' => depth -= 1,
20004                                _ => {}
20005                            }
20006                            if depth == 0 {
20007                                let inner: String = chars[start_inner..i].iter().collect();
20008                                i += 1; // closing ]
20009                                let indices = parse_slice_indices_from_str(inner.trim(), "-e")?;
20010                                parts.push(StringPart::Expr(Expr {
20011                                    kind: ExprKind::ArraySlice {
20012                                        array: name.clone(),
20013                                        indices,
20014                                    },
20015                                    line,
20016                                }));
20017                                continue 'istr;
20018                            }
20019                            i += 1;
20020                        }
20021                        return Err(self.syntax_err(
20022                            "Unterminated [ in array slice inside quoted string",
20023                            line,
20024                        ));
20025                    }
20026                    parts.push(StringPart::ArrayVar(name));
20027                }
20028            } else if chars[i] == '#'
20029                && i + 1 < chars.len()
20030                && chars[i + 1] == '{'
20031                && !crate::compat_mode()
20032            {
20033                // #{expr} — Ruby-style expression interpolation (stryke extension).
20034                if !literal.is_empty() {
20035                    parts.push(StringPart::Literal(std::mem::take(&mut literal)));
20036                }
20037                i += 2; // skip `#{`
20038                let mut inner = String::new();
20039                let mut depth = 1usize;
20040                while i < chars.len() && depth > 0 {
20041                    match chars[i] {
20042                        '{' => depth += 1,
20043                        '}' => {
20044                            depth -= 1;
20045                            if depth == 0 {
20046                                break;
20047                            }
20048                        }
20049                        _ => {}
20050                    }
20051                    inner.push(chars[i]);
20052                    i += 1;
20053                }
20054                if i < chars.len() {
20055                    i += 1; // skip closing `}`
20056                }
20057                let expr = parse_block_from_str(inner.trim(), "-e", line)?;
20058                parts.push(StringPart::Expr(expr));
20059            } else {
20060                literal.push(chars[i]);
20061                i += 1;
20062            }
20063        }
20064        if !literal.is_empty() {
20065            parts.push(StringPart::Literal(literal));
20066        }
20067
20068        if parts.len() == 1 {
20069            if let StringPart::Literal(s) = &parts[0] {
20070                return Ok(Expr {
20071                    kind: ExprKind::String(s.clone()),
20072                    line,
20073                });
20074            }
20075        }
20076        if parts.is_empty() {
20077            return Ok(Expr {
20078                kind: ExprKind::String(String::new()),
20079                line,
20080            });
20081        }
20082
20083        Ok(Expr {
20084            kind: ExprKind::InterpolatedString(parts),
20085            line,
20086        })
20087    }
20088
20089    fn expr_to_overload_key(&self, e: &Expr) -> StrykeResult<String> {
20090        match &e.kind {
20091            ExprKind::String(s) => Ok(s.clone()),
20092            _ => Err(self.syntax_err(
20093                "overload key must be a string literal (e.g. '\"\"' or '+')",
20094                e.line,
20095            )),
20096        }
20097    }
20098
20099    fn expr_to_overload_sub(&mut self, e: &Expr) -> StrykeResult<String> {
20100        match &e.kind {
20101            ExprKind::String(s) => Ok(s.clone()),
20102            ExprKind::Integer(n) => Ok(n.to_string()),
20103            ExprKind::SubroutineRef(s) | ExprKind::SubroutineCodeRef(s) => Ok(s.clone()),
20104            // Anonymous sub: `use overload "+" => sub { ... };` — promote the
20105            // anon body into a synthetic top-level SubDecl so the overload
20106            // table can hold the name like the named-sub case. (PARITY-012)
20107            ExprKind::CodeRef { params, body } => {
20108                let id = self.next_overload_anon_id;
20109                self.next_overload_anon_id = self.next_overload_anon_id.saturating_add(1);
20110                let name = format!("__overload_anon_{}", id);
20111                self.pending_synthetic_subs.push(Statement {
20112                    label: None,
20113                    kind: StmtKind::SubDecl {
20114                        name: name.clone(),
20115                        params: params.clone(),
20116                        body: body.clone(),
20117                        prototype: None,
20118                    },
20119                    line: e.line,
20120                });
20121                Ok(name)
20122            }
20123            _ => Err(self.syntax_err(
20124                "overload handler must be a string literal, number (e.g. fallback => 1), or \\&subname (method in current package)",
20125                e.line,
20126            )),
20127        }
20128    }
20129}
20130
20131fn merge_expr_list(parts: Vec<Expr>) -> Expr {
20132    if parts.len() == 1 {
20133        parts.into_iter().next().unwrap()
20134    } else {
20135        let line = parts.first().map(|e| e.line).unwrap_or(0);
20136        Expr {
20137            kind: ExprKind::List(parts),
20138            line,
20139        }
20140    }
20141}
20142
20143/// Parse a single expression from `s` (e.g. contents of `@{ ... }` inside a double-quoted string).
20144pub fn parse_expression_from_str(s: &str, file: &str) -> StrykeResult<Expr> {
20145    let mut lexer = Lexer::new_with_file(s, file);
20146    let tokens = lexer.tokenize()?;
20147    let mut parser = Parser::new_with_file(tokens, file);
20148    let e = parser.parse_expression()?;
20149    if !parser.at_eof() {
20150        return Err(parser.syntax_err(
20151            "Extra tokens in embedded string expression",
20152            parser.peek_line(),
20153        ));
20154    }
20155    Ok(e)
20156}
20157
20158/// Parse a statement list from `s` and wrap as `do { ... }` (for `#{...}` interpolation).
20159pub fn parse_block_from_str(s: &str, file: &str, line: usize) -> StrykeResult<Expr> {
20160    let mut lexer = Lexer::new_with_file(s, file);
20161    let tokens = lexer.tokenize()?;
20162    let mut parser = Parser::new_with_file(tokens, file);
20163    let stmts = parser.parse_statements()?;
20164    let inner_line = stmts.first().map(|st| st.line).unwrap_or(line);
20165    let inner = Expr {
20166        kind: ExprKind::CodeRef {
20167            params: vec![],
20168            body: stmts,
20169        },
20170        line: inner_line,
20171    };
20172    Ok(Expr {
20173        kind: ExprKind::Do(Box::new(inner)),
20174        line,
20175    })
20176}
20177
20178/// Comma-separated expressions on a `format` value line (below a picture line).
20179/// Parse `[ ... ]` contents for `@a[...]` (same rules as `parse_arg_list` / comma-separated indices).
20180pub fn parse_slice_indices_from_str(s: &str, file: &str) -> StrykeResult<Vec<Expr>> {
20181    let mut lexer = Lexer::new_with_file(s, file);
20182    let tokens = lexer.tokenize()?;
20183    let mut parser = Parser::new_with_file(tokens, file);
20184    parser.parse_arg_list()
20185}
20186
20187pub fn parse_format_value_line(line: &str) -> StrykeResult<Vec<Expr>> {
20188    let trimmed = line.trim();
20189    if trimmed.is_empty() {
20190        return Ok(vec![]);
20191    }
20192    let mut lexer = Lexer::new(trimmed);
20193    let tokens = lexer.tokenize()?;
20194    let mut parser = Parser::new(tokens);
20195    let mut exprs = Vec::new();
20196    loop {
20197        if parser.at_eof() {
20198            break;
20199        }
20200        // Assignment-level expressions so `a, b` yields two fields (not one comma list).
20201        exprs.push(parser.parse_assign_expr()?);
20202        if parser.eat(&Token::Comma) {
20203            continue;
20204        }
20205        if !parser.at_eof() {
20206            return Err(parser.syntax_err("Extra tokens in format value line", parser.peek_line()));
20207        }
20208        break;
20209    }
20210    Ok(exprs)
20211}
20212
20213#[cfg(test)]
20214mod tests {
20215    use super::*;
20216
20217    fn parse_ok(code: &str) -> Program {
20218        let mut lexer = Lexer::new(code);
20219        let tokens = lexer.tokenize().expect("tokenize");
20220        let mut parser = Parser::new(tokens);
20221        parser.parse_program().expect("parse")
20222    }
20223
20224    fn parse_err(code: &str) -> String {
20225        let mut lexer = Lexer::new(code);
20226        let tokens = match lexer.tokenize() {
20227            Ok(t) => t,
20228            Err(e) => return e.message,
20229        };
20230        let mut parser = Parser::new(tokens);
20231        parser.parse_program().unwrap_err().message
20232    }
20233
20234    #[test]
20235    fn parse_empty_program() {
20236        let p = parse_ok("");
20237        assert!(p.statements.is_empty());
20238    }
20239
20240    #[test]
20241    fn parse_semicolons_only() {
20242        let p = parse_ok(";;");
20243        assert!(p.statements.len() <= 3);
20244    }
20245
20246    #[test]
20247    fn parse_simple_scalar_assignment() {
20248        let p = parse_ok("$x = 1");
20249        assert_eq!(p.statements.len(), 1);
20250    }
20251
20252    #[test]
20253    fn parse_simple_array_assignment() {
20254        let p = parse_ok("@arr = (1, 2, 3)");
20255        assert_eq!(p.statements.len(), 1);
20256    }
20257
20258    #[test]
20259    fn parse_simple_hash_assignment() {
20260        let p = parse_ok("%h = (a => 1, b => 2)");
20261        assert_eq!(p.statements.len(), 1);
20262    }
20263
20264    #[test]
20265    fn parse_subroutine_decl() {
20266        let p = parse_ok("fn foo { 1 }");
20267        assert_eq!(p.statements.len(), 1);
20268        match &p.statements[0].kind {
20269            StmtKind::SubDecl { name, .. } => assert_eq!(name, "foo"),
20270            _ => panic!("expected SubDecl"),
20271        }
20272    }
20273
20274    #[test]
20275    fn parse_class_method_expr_body_shorthand() {
20276        let p = parse_ok("class X { fn adg = \"\" }");
20277        match &p.statements[0].kind {
20278            StmtKind::ClassDecl { def } => {
20279                let m = def.method("adg").expect("adg method");
20280                let body = m.body.as_ref().expect("body");
20281                assert_eq!(body.len(), 1);
20282                match &body[0].kind {
20283                    StmtKind::Expression(e) => match &e.kind {
20284                        ExprKind::String(s) => assert!(s.is_empty()),
20285                        _ => panic!("expected string expr, got {:?}", e.kind),
20286                    },
20287                    _ => panic!("expected expression stmt"),
20288                }
20289            }
20290            _ => panic!("expected ClassDecl"),
20291        }
20292    }
20293
20294    #[test]
20295    fn parse_named_fn_eq_shorthand_with_sig() {
20296        let p = parse_ok("fn add_one($x) = $x + 1");
20297        match &p.statements[0].kind {
20298            StmtKind::SubDecl {
20299                name, params, body, ..
20300            } => {
20301                assert_eq!(name, "add_one");
20302                assert_eq!(params.len(), 1);
20303                assert_eq!(body.len(), 1);
20304            }
20305            _ => panic!("expected SubDecl"),
20306        }
20307    }
20308
20309    #[test]
20310    fn parse_anon_fn_eq_shorthand_with_sig() {
20311        let p = parse_ok("my $f = fn($x) = 23");
20312        match &p.statements[0].kind {
20313            StmtKind::My(decls) => {
20314                let init = decls[0].initializer.as_ref().expect("initializer");
20315                match &init.kind {
20316                    ExprKind::CodeRef { params, body } => {
20317                        assert_eq!(params.len(), 1);
20318                        assert_eq!(body.len(), 1);
20319                    }
20320                    _ => panic!("expected CodeRef"),
20321                }
20322            }
20323            _ => panic!("expected My"),
20324        }
20325    }
20326
20327    #[test]
20328    fn parse_struct_method_eq_shorthand() {
20329        let p = parse_ok("struct S { fn double($a) = $a * 2 }");
20330        match &p.statements[0].kind {
20331            StmtKind::StructDecl { def } => {
20332                assert_eq!(def.methods.len(), 1);
20333                assert_eq!(def.methods[0].name, "double");
20334                assert_eq!(def.methods[0].body.len(), 1);
20335            }
20336            _ => panic!("expected StructDecl"),
20337        }
20338    }
20339
20340    #[test]
20341    fn parse_trait_method_eq_shorthand() {
20342        let p = parse_ok("trait T { fn k = 0 }");
20343        match &p.statements[0].kind {
20344            StmtKind::TraitDecl { def } => {
20345                let m = def.method("k").expect("k");
20346                let body = m.body.as_ref().expect("default body");
20347                assert_eq!(body.len(), 1);
20348            }
20349            _ => panic!("expected TraitDecl"),
20350        }
20351    }
20352
20353    #[test]
20354    fn parse_fn_eq_shorthand_rejects_top_level_comma() {
20355        let msg = parse_err("fn z = 1, 2");
20356        assert!(
20357            msg.contains("single expression") || msg.contains("comma"),
20358            "{}",
20359            msg
20360        );
20361    }
20362
20363    #[test]
20364    fn parse_subroutine_with_prototype() {
20365        let p = parse_ok("fn foo ($$) { 1 }");
20366        assert_eq!(p.statements.len(), 1);
20367        match &p.statements[0].kind {
20368            StmtKind::SubDecl { prototype, .. } => {
20369                assert!(prototype.is_some());
20370            }
20371            _ => panic!("expected SubDecl"),
20372        }
20373    }
20374
20375    #[test]
20376    fn parse_anonymous_fn() {
20377        let p = parse_ok("my $f = fn { 1 }");
20378        assert_eq!(p.statements.len(), 1);
20379    }
20380
20381    #[test]
20382    fn parse_if_statement() {
20383        let p = parse_ok("if (1) { 2 }");
20384        assert_eq!(p.statements.len(), 1);
20385        matches!(&p.statements[0].kind, StmtKind::If { .. });
20386    }
20387
20388    #[test]
20389    fn parse_if_elsif_else() {
20390        let p = parse_ok("if (0) { 1 } elsif (1) { 2 } else { 3 }");
20391        assert_eq!(p.statements.len(), 1);
20392    }
20393
20394    #[test]
20395    fn parse_unless_statement() {
20396        let p = parse_ok("unless (0) { 1 }");
20397        assert_eq!(p.statements.len(), 1);
20398    }
20399
20400    #[test]
20401    fn parse_while_loop() {
20402        let p = parse_ok("while ($x) { $x-- }");
20403        assert_eq!(p.statements.len(), 1);
20404    }
20405
20406    #[test]
20407    fn parse_until_loop() {
20408        let p = parse_ok("until ($x) { $x++ }");
20409        assert_eq!(p.statements.len(), 1);
20410    }
20411
20412    #[test]
20413    fn parse_for_c_style() {
20414        let p = parse_ok("for (my $i=0; $i<10; $i++) { 1 }");
20415        assert_eq!(p.statements.len(), 1);
20416    }
20417
20418    #[test]
20419    fn parse_foreach_loop() {
20420        let p = parse_ok("foreach my $x (@arr) { 1 }");
20421        assert_eq!(p.statements.len(), 1);
20422    }
20423
20424    #[test]
20425    fn parse_loop_with_label() {
20426        let p = parse_ok("OUTER: for my $i (1..10) { last OUTER }");
20427        assert_eq!(p.statements.len(), 1);
20428        assert_eq!(p.statements[0].label.as_deref(), Some("OUTER"));
20429    }
20430
20431    #[test]
20432    fn parse_begin_block() {
20433        let p = parse_ok("BEGIN { 1 }");
20434        assert_eq!(p.statements.len(), 1);
20435        matches!(&p.statements[0].kind, StmtKind::Begin(_));
20436    }
20437
20438    #[test]
20439    fn parse_end_block() {
20440        let p = parse_ok("END { 1 }");
20441        assert_eq!(p.statements.len(), 1);
20442        matches!(&p.statements[0].kind, StmtKind::End(_));
20443    }
20444
20445    #[test]
20446    fn parse_package_statement() {
20447        let p = parse_ok("package Foo::Bar");
20448        assert_eq!(p.statements.len(), 1);
20449        match &p.statements[0].kind {
20450            StmtKind::Package { name } => assert_eq!(name, "Foo::Bar"),
20451            _ => panic!("expected Package"),
20452        }
20453    }
20454
20455    #[test]
20456    fn parse_use_statement() {
20457        let p = parse_ok("use strict");
20458        assert_eq!(p.statements.len(), 1);
20459    }
20460
20461    #[test]
20462    fn parse_no_statement() {
20463        let p = parse_ok("no warnings");
20464        assert_eq!(p.statements.len(), 1);
20465    }
20466
20467    #[test]
20468    fn parse_require_bareword() {
20469        let p = parse_ok("require Foo::Bar");
20470        assert_eq!(p.statements.len(), 1);
20471    }
20472
20473    #[test]
20474    fn parse_require_string() {
20475        let p = parse_ok(r#"require "foo.pl""#);
20476        assert_eq!(p.statements.len(), 1);
20477    }
20478
20479    #[test]
20480    fn parse_eval_block() {
20481        let p = parse_ok("eval { 1 }");
20482        assert_eq!(p.statements.len(), 1);
20483    }
20484
20485    #[test]
20486    fn parse_eval_string() {
20487        let p = parse_ok(r#"eval "1 + 2""#);
20488        assert_eq!(p.statements.len(), 1);
20489    }
20490
20491    #[test]
20492    fn parse_qw_word_list() {
20493        let p = parse_ok("my @a = qw(foo bar baz)");
20494        assert_eq!(p.statements.len(), 1);
20495    }
20496
20497    #[test]
20498    fn parse_q_string() {
20499        let p = parse_ok("my $s = q{hello}");
20500        assert_eq!(p.statements.len(), 1);
20501    }
20502
20503    #[test]
20504    fn parse_qq_string() {
20505        let p = parse_ok(r#"my $s = qq(hello $x)"#);
20506        assert_eq!(p.statements.len(), 1);
20507    }
20508
20509    #[test]
20510    fn parse_regex_match() {
20511        let p = parse_ok(r#"$x =~ /foo/"#);
20512        assert_eq!(p.statements.len(), 1);
20513    }
20514
20515    #[test]
20516    fn parse_regex_substitution() {
20517        let p = parse_ok(r#"$x =~ s/foo/bar/g"#);
20518        assert_eq!(p.statements.len(), 1);
20519    }
20520
20521    #[test]
20522    fn parse_transliterate() {
20523        let p = parse_ok(r#"$x =~ tr/a-z/A-Z/"#);
20524        assert_eq!(p.statements.len(), 1);
20525    }
20526
20527    #[test]
20528    fn parse_ternary_operator() {
20529        let p = parse_ok("my $x = $a ? 1 : 2");
20530        assert_eq!(p.statements.len(), 1);
20531    }
20532
20533    #[test]
20534    fn parse_arrow_method_call() {
20535        let p = parse_ok("$obj->method()");
20536        assert_eq!(p.statements.len(), 1);
20537    }
20538
20539    #[test]
20540    fn parse_arrow_deref_hash() {
20541        let p = parse_ok("$r->{key}");
20542        assert_eq!(p.statements.len(), 1);
20543    }
20544
20545    #[test]
20546    fn parse_arrow_deref_array() {
20547        let p = parse_ok("$r->[0]");
20548        assert_eq!(p.statements.len(), 1);
20549    }
20550
20551    #[test]
20552    fn parse_chained_arrow_deref() {
20553        let p = parse_ok("$r->{a}[0]{b}");
20554        assert_eq!(p.statements.len(), 1);
20555    }
20556
20557    #[test]
20558    fn parse_my_multiple_vars() {
20559        let p = parse_ok("my ($a, $b, $c) = (1, 2, 3)");
20560        assert_eq!(p.statements.len(), 1);
20561    }
20562
20563    #[test]
20564    fn parse_our_scalar() {
20565        let p = parse_ok("our $VERSION = '1.0'");
20566        assert_eq!(p.statements.len(), 1);
20567    }
20568
20569    #[test]
20570    fn parse_local_scalar() {
20571        let p = parse_ok("local $/ = undef");
20572        assert_eq!(p.statements.len(), 1);
20573    }
20574
20575    #[test]
20576    fn parse_state_variable() {
20577        let p = parse_ok("fn Test::counter { state $n = 0; $n++ }");
20578        assert_eq!(p.statements.len(), 1);
20579    }
20580
20581    #[test]
20582    fn parse_postfix_if() {
20583        let p = parse_ok("print 1 if $x");
20584        assert_eq!(p.statements.len(), 1);
20585    }
20586
20587    #[test]
20588    fn parse_postfix_unless() {
20589        let p = parse_ok("die 'error' unless $ok");
20590        assert_eq!(p.statements.len(), 1);
20591    }
20592
20593    #[test]
20594    fn parse_postfix_while() {
20595        let p = parse_ok("$x++ while $x < 10");
20596        assert_eq!(p.statements.len(), 1);
20597    }
20598
20599    #[test]
20600    fn parse_postfix_for() {
20601        let p = parse_ok("print for @arr");
20602        assert_eq!(p.statements.len(), 1);
20603    }
20604
20605    #[test]
20606    fn parse_last_next_redo() {
20607        let p = parse_ok("for (@a) { next if $_ < 0; last if $_ > 10 }");
20608        assert_eq!(p.statements.len(), 1);
20609    }
20610
20611    #[test]
20612    fn parse_return_statement() {
20613        let p = parse_ok("fn foo { return 42 }");
20614        assert_eq!(p.statements.len(), 1);
20615    }
20616
20617    #[test]
20618    fn parse_wantarray() {
20619        let p = parse_ok("fn foo { wantarray ? @a : $a }");
20620        assert_eq!(p.statements.len(), 1);
20621    }
20622
20623    #[test]
20624    fn parse_caller_builtin() {
20625        let p = parse_ok("my @c = caller");
20626        assert_eq!(p.statements.len(), 1);
20627    }
20628
20629    #[test]
20630    fn parse_ref_to_array() {
20631        let p = parse_ok("my $r = \\@arr");
20632        assert_eq!(p.statements.len(), 1);
20633    }
20634
20635    #[test]
20636    fn parse_ref_to_hash() {
20637        let p = parse_ok("my $r = \\%hash");
20638        assert_eq!(p.statements.len(), 1);
20639    }
20640
20641    #[test]
20642    fn parse_ref_to_scalar() {
20643        let p = parse_ok("my $r = \\$x");
20644        assert_eq!(p.statements.len(), 1);
20645    }
20646
20647    #[test]
20648    fn parse_deref_scalar() {
20649        let p = parse_ok("my $v = $$r");
20650        assert_eq!(p.statements.len(), 1);
20651    }
20652
20653    #[test]
20654    fn parse_deref_array() {
20655        let p = parse_ok("my @a = @$r");
20656        assert_eq!(p.statements.len(), 1);
20657    }
20658
20659    #[test]
20660    fn parse_deref_hash() {
20661        let p = parse_ok("my %h = %$r");
20662        assert_eq!(p.statements.len(), 1);
20663    }
20664
20665    #[test]
20666    fn parse_blessed_ref() {
20667        let p = parse_ok("bless $r, 'Foo'");
20668        assert_eq!(p.statements.len(), 1);
20669    }
20670
20671    #[test]
20672    fn parse_heredoc_basic() {
20673        let p = parse_ok("my $s = <<END;\nfoo\nEND");
20674        assert_eq!(p.statements.len(), 1);
20675    }
20676
20677    #[test]
20678    fn parse_heredoc_quoted() {
20679        let p = parse_ok("my $s = <<'END';\nfoo\nEND");
20680        assert_eq!(p.statements.len(), 1);
20681    }
20682
20683    #[test]
20684    fn parse_do_block() {
20685        let p = parse_ok("my $x = do { 1 + 2 }");
20686        assert_eq!(p.statements.len(), 1);
20687    }
20688
20689    #[test]
20690    fn parse_do_file() {
20691        let p = parse_ok(r#"do "foo.pl""#);
20692        assert_eq!(p.statements.len(), 1);
20693    }
20694
20695    #[test]
20696    fn parse_map_expression() {
20697        let p = parse_ok("my @b = map { $_ * 2 } @a");
20698        assert_eq!(p.statements.len(), 1);
20699    }
20700
20701    /// `on $cluster () …` must keep `()` as SOURCE (empty list), not postfix
20702    /// indirect `($cluster)()` which leaves `map` as SOURCE and breaks parsing.
20703    #[test]
20704    fn parse_dist_thread_on_scalar_empty_list_source() {
20705        let p = parse_ok("~d> on $c () map { _ * 2 }");
20706        assert_eq!(p.statements.len(), 1);
20707        let StmtKind::Expression(root) = &p.statements[0].kind else {
20708            panic!("expected Expression statement");
20709        };
20710        let ExprKind::DistReduceExpr { cluster, list, .. } = &root.kind else {
20711            panic!("expected DistReduceExpr, got {:?}", root.kind);
20712        };
20713        assert!(
20714            matches!(cluster.kind, ExprKind::ScalarVar(ref s) if s == "c"),
20715            "expected cluster $c, got {:?}",
20716            cluster.kind
20717        );
20718        assert!(
20719            matches!(list.kind, ExprKind::List(ref v) if v.is_empty()),
20720            "expected empty list source, got {:?}",
20721            list.kind
20722        );
20723    }
20724
20725    #[test]
20726    fn parse_grep_expression() {
20727        let p = parse_ok("my @b = grep { $_ > 0 } @a");
20728        assert_eq!(p.statements.len(), 1);
20729    }
20730
20731    #[test]
20732    fn parse_sort_expression() {
20733        let p = parse_ok("my @b = sort { $a <=> $b } @a");
20734        assert_eq!(p.statements.len(), 1);
20735    }
20736
20737    #[test]
20738    fn pipe_sort_does_not_swallow_next_my_decl() {
20739        // Regression: bare `|> sort` followed by `\n my $x = ...` used
20740        // to eat the next stmt as sort's argument list. After the fix,
20741        // both statements must appear in the AST.
20742        let p = parse_ok("my @s = @data |> sort\nmy $j = join(\",\", @s)");
20743        assert_eq!(
20744            p.statements.len(),
20745            2,
20746            "expected 2 stmts (sort + join decl), got {}: {:?}",
20747            p.statements.len(),
20748            p.statements
20749                .iter()
20750                .map(|s| format!("{:?}", s.kind).chars().take(60).collect::<String>())
20751                .collect::<Vec<_>>(),
20752        );
20753    }
20754
20755    #[test]
20756    fn pipe_sort_multiline_pipeline_preserves_next_decl() {
20757        // Same shape but with maps/grep stages between the source and
20758        // `sort` — mirrors the original `test_oop_inventory_threaded_pin`
20759        // bug fixture.
20760        let p = parse_ok(
20761            "my @bk = @{$inv->by_cat(\"bakery\")} |> maps { _->label() } |> sort\nmy $j = join(\"|\", @bk)",
20762        );
20763        assert_eq!(p.statements.len(), 2);
20764    }
20765
20766    #[test]
20767    fn parse_pipe_forward() {
20768        let p = parse_ok("@a |> map { $_ * 2 }");
20769        assert_eq!(p.statements.len(), 1);
20770    }
20771
20772    #[test]
20773    fn parse_expression_from_str_simple() {
20774        let e = parse_expression_from_str("$x + 1", "-e").unwrap();
20775        assert!(matches!(e.kind, ExprKind::BinOp { .. }));
20776    }
20777
20778    #[test]
20779    fn parse_expression_from_str_extra_tokens_error() {
20780        let err = parse_expression_from_str("$x; $y", "-e").unwrap_err();
20781        assert!(err.message.contains("Extra tokens"));
20782    }
20783
20784    #[test]
20785    fn parse_slice_indices_from_str_basic() {
20786        let indices = parse_slice_indices_from_str("0, 1, 2", "-e").unwrap();
20787        assert_eq!(indices.len(), 3);
20788    }
20789
20790    #[test]
20791    fn parse_format_value_line_empty() {
20792        let exprs = parse_format_value_line("").unwrap();
20793        assert!(exprs.is_empty());
20794    }
20795
20796    #[test]
20797    fn parse_format_value_line_single() {
20798        let exprs = parse_format_value_line("$x").unwrap();
20799        assert_eq!(exprs.len(), 1);
20800    }
20801
20802    #[test]
20803    fn parse_format_value_line_multiple() {
20804        let exprs = parse_format_value_line("$a, $b, $c").unwrap();
20805        assert_eq!(exprs.len(), 3);
20806    }
20807
20808    #[test]
20809    fn parse_unclosed_brace_error() {
20810        let err = parse_err("fn foo {");
20811        assert!(!err.is_empty());
20812    }
20813
20814    #[test]
20815    fn parse_unclosed_paren_error() {
20816        let err = parse_err("print (1, 2");
20817        assert!(!err.is_empty());
20818    }
20819
20820    #[test]
20821    fn parse_invalid_statement_error() {
20822        let err = parse_err("???");
20823        assert!(!err.is_empty());
20824    }
20825
20826    #[test]
20827    fn merge_expr_list_single() {
20828        let e = Expr {
20829            kind: ExprKind::Integer(1),
20830            line: 1,
20831        };
20832        let merged = merge_expr_list(vec![e.clone()]);
20833        matches!(merged.kind, ExprKind::Integer(1));
20834    }
20835
20836    #[test]
20837    fn merge_expr_list_multiple() {
20838        let e1 = Expr {
20839            kind: ExprKind::Integer(1),
20840            line: 1,
20841        };
20842        let e2 = Expr {
20843            kind: ExprKind::Integer(2),
20844            line: 1,
20845        };
20846        let merged = merge_expr_list(vec![e1, e2]);
20847        matches!(merged.kind, ExprKind::List(_));
20848    }
20849
20850    // ── --no-interop strict-mode rejections ─────────────────────────────
20851    //
20852    // `--no-interop` is the bot firewall: it rejects Perl 5 idioms the
20853    // parser would otherwise accept, forcing stryke-only spellings. Each
20854    // of these pins one rejection rule so a later refactor can't silently
20855    // accept the un-idiomatic form. We RAII the TLS flag so sibling tests
20856    // running in parallel don't see the override.
20857
20858    struct NoInteropGuard {
20859        saved: Option<bool>,
20860    }
20861    impl NoInteropGuard {
20862        fn on() -> Self {
20863            let saved = crate::no_interop_mode_tls();
20864            crate::set_no_interop_mode_tls(Some(true));
20865            Self { saved }
20866        }
20867    }
20868    impl Drop for NoInteropGuard {
20869        fn drop(&mut self) {
20870            crate::set_no_interop_mode_tls(self.saved);
20871        }
20872    }
20873
20874    #[test]
20875    fn no_interop_rejects_sub_keyword() {
20876        let _g = NoInteropGuard::on();
20877        let err = parse_err("sub foo { 1 }");
20878        assert!(
20879            err.contains("--no-interop") && err.contains("fn"),
20880            "sub rejected with fn hint: got {err:?}"
20881        );
20882    }
20883
20884    #[test]
20885    fn no_interop_rejects_say() {
20886        let _g = NoInteropGuard::on();
20887        let err = parse_err("say 1");
20888        assert!(
20889            err.contains("--no-interop") && err.contains("`p`"),
20890            "say rejected with p hint: got {err:?}"
20891        );
20892    }
20893
20894    #[test]
20895    fn no_interop_rejects_scalar_keyword() {
20896        let _g = NoInteropGuard::on();
20897        let err = parse_err("my $n = scalar @x");
20898        assert!(
20899            err.contains("--no-interop") && (err.contains("len") || err.contains("cnt")),
20900            "scalar rejected with len/cnt hint: got {err:?}"
20901        );
20902    }
20903
20904    #[test]
20905    fn no_interop_rejects_reverse() {
20906        let _g = NoInteropGuard::on();
20907        let err = parse_err("my @y = reverse @x");
20908        assert!(
20909            err.contains("--no-interop") && err.contains("rev"),
20910            "reverse rejected with rev hint: got {err:?}"
20911        );
20912    }
20913
20914    /// And the inverse — the stryke spellings (`fn`, `p`, `len`, `rev`)
20915    /// must parse cleanly under the same flag. A regression that
20916    /// accidentally rejects the canonical form is just as bad as one
20917    /// that accepts the Perl 5 form.
20918    #[test]
20919    fn no_interop_accepts_stryke_idioms() {
20920        let _g = NoInteropGuard::on();
20921        // Each of these used to be the Perl 5 form; stryke's equivalent
20922        // must parse without error.
20923        parse_ok("fn foo { 1 }");
20924        parse_ok("p 1");
20925        parse_ok("my @x = (1, 2, 3); my $n = len(@x)");
20926        parse_ok("my @x = (1, 2, 3); my @y = rev(@x)");
20927    }
20928
20929    /// `--no-interop` must NOT affect default-mode parsing. Tests run in
20930    /// parallel; the guard's Drop restores the flag, but verify the
20931    /// happy-path Perl 5 forms still parse with the flag *off* so we know
20932    /// the guard mechanics actually restore.
20933    #[test]
20934    fn default_mode_still_accepts_perl5_forms() {
20935        // No guard installed — process default (off in tests).
20936        parse_ok("sub foo { 1 }");
20937        parse_ok("say 1");
20938    }
20939
20940    /// `$a` / `$b` outside a sort or reduce block is rejected under
20941    /// `--no-interop` — stryke routes the user to `$_0` / `$_1`
20942    /// implicit-positional names which work everywhere (including in
20943    /// sort blocks). Pins the lexer arm at `parser.rs::18964` shape.
20944    #[test]
20945    fn no_interop_rejects_bare_dollar_a_dollar_b() {
20946        let _g = NoInteropGuard::on();
20947        // Bare reference outside a block context.
20948        let err = parse_err("my $x = $a + $b");
20949        assert!(
20950            err.contains("--no-interop") || err.contains("$_0") || err.contains("$_1"),
20951            "$a/$b rejected with positional hint: got {err:?}"
20952        );
20953    }
20954
20955    /// And inside a sort block too: stryke's strict mode wants
20956    /// `$_0` / `$_1` even there (per the temperature_converter and
20957    /// quicksort_no_interop examples).
20958    #[test]
20959    fn no_interop_rejects_dollar_a_inside_sort_block() {
20960        let _g = NoInteropGuard::on();
20961        let err = parse_err("my @s = sort { $a <=> $b } (3, 1, 2)");
20962        assert!(
20963            err.contains("--no-interop") || err.contains("$_0") || err.contains("$_1"),
20964            "$a in sort block rejected: got {err:?}"
20965        );
20966    }
20967
20968    /// And the inverse — `$_0` / `$_1` inside a sort block parses
20969    /// clean under `--no-interop`.
20970    #[test]
20971    fn no_interop_accepts_positional_underscore_in_sort_block() {
20972        let _g = NoInteropGuard::on();
20973        parse_ok("my @s = sort { $_0 <=> $_1 } (3, 1, 2)");
20974    }
20975
20976    // ── stryke-specific grammar pins (parse with the flag on or off) ────
20977
20978    /// Colon ranges `start:end` are stryke-canonical (not `..`).
20979    /// `1:10`, `0:N`, `-5:5` all parse.
20980    #[test]
20981    fn colon_range_parses_in_for_loop() {
20982        let _g = NoInteropGuard::on();
20983        parse_ok("for my $i (1:10) { p $i }");
20984        parse_ok("my @r = 0:99");
20985        parse_ok("my @r = -5:5");
20986    }
20987
20988    /// Postfix `if` / `unless` / `for` modifiers parse on a statement.
20989    #[test]
20990    fn postfix_statement_modifiers_parse() {
20991        let _g = NoInteropGuard::on();
20992        parse_ok("p _ for (1, 2, 3)");
20993        parse_ok("p 1 if 1");
20994        parse_ok("p 0 unless 0");
20995    }
20996
20997    /// Pipe-forward `|>` desugars at parse time — verify it accepts both
20998    /// the bare-function (`x |> f`) and the block (`x |> { _ * 2 }`) forms.
20999    #[test]
21000    fn pipe_forward_accepts_both_function_and_block_rhs() {
21001        let _g = NoInteropGuard::on();
21002        parse_ok("my $r = 1:10 |> sum");
21003        parse_ok("my @r = 1:10 |> maps { _ * 2 }");
21004        parse_ok("my @r = 1:10 |> grep { _ % 2 == 0 } |> maps { _ + 1 }");
21005    }
21006
21007    /// `_0`, `_1`, ... bareword positional params (no sigil).
21008    #[test]
21009    fn bareword_positional_underscore_n_parses_in_blocks() {
21010        let _g = NoInteropGuard::on();
21011        // `_0` / `_1` inside a sort block: the canonical strict spelling.
21012        parse_ok("my @s = sort { _0 <=> _1 } (3, 1, 2)");
21013        // And inside a maps block as the per-item topic.
21014        parse_ok("my @r = maps { _0 * 2 } (1, 2, 3)");
21015    }
21016
21017    /// Declarative types — `struct`, `enum`, `class`, `trait` — must
21018    /// parse under `--no-interop` (they're stryke extensions, not Perl 5
21019    /// shapes). Pin each via a minimal declaration.
21020    #[test]
21021    fn no_interop_accepts_struct_decl() {
21022        let _g = NoInteropGuard::on();
21023        parse_ok("struct Point { x => Int, y => Int }");
21024    }
21025
21026    #[test]
21027    fn no_interop_accepts_enum_decl() {
21028        let _g = NoInteropGuard::on();
21029        parse_ok("enum Color { Red, Green, Blue }");
21030        // Data-carrying variants use the `Variant => Type` shape, not
21031        // `Variant(Type)` — the latter is reserved for pattern
21032        // destructuring in match arms.
21033        parse_ok("enum Maybe { Just => Int, Nothing }");
21034    }
21035
21036    #[test]
21037    fn no_interop_accepts_class_decl_with_methods() {
21038        let _g = NoInteropGuard::on();
21039        parse_ok(
21040            "class Rect {\n    width: Float\n    height: Float\n\n    fn area { $self->width * $self->height }\n}",
21041        );
21042    }
21043
21044    #[test]
21045    fn no_interop_accepts_trait_decl() {
21046        let _g = NoInteropGuard::on();
21047        parse_ok("trait Greeter { fn greet; fn loudly { p \"GREET\" } }");
21048    }
21049
21050    /// Compound-assign operators — `||=` defined-or-assign, `//=` exists-
21051    /// or-assign — are stryke- and Perl-compat and should round-trip.
21052    /// These are the lazy-init idiom for hash-of-array buckets used in
21053    /// `csv_summary_no_interop.stk`.
21054    #[test]
21055    fn defined_or_assign_compound_operators_parse() {
21056        let _g = NoInteropGuard::on();
21057        parse_ok("my $h = {}; $h->{x} ||= []");
21058        parse_ok("my $v; $v //= 0");
21059    }
21060
21061    /// `+{ ... }` is the unambiguous hashref literal (vs `{ ... }` block).
21062    /// Used in the CSV demo to push rows.
21063    #[test]
21064    fn explicit_hashref_literal_parses() {
21065        let _g = NoInteropGuard::on();
21066        parse_ok("my $row = +{ region => \"north\", qty => 10 }");
21067        parse_ok("my @rows = (+{ a => 1 }, +{ a => 2 })");
21068    }
21069
21070    /// `eval { … }` block + `$@` error-variable inspection is the
21071    /// canonical exception form used in `rpn_calc_no_interop.stk`.
21072    #[test]
21073    fn eval_block_and_dollar_at_parse() {
21074        let _g = NoInteropGuard::on();
21075        parse_ok("my $r = eval { 1 + 2 }; p $@ if $@");
21076        parse_ok("eval { die \"boom\" }; p $@");
21077    }
21078
21079    /// `try { … } catch ($e) { … }` is the stryke-extension exception
21080    /// shape (Perl-5-on-steroids); must parse under `--no-interop`.
21081    #[test]
21082    fn try_catch_parses() {
21083        let _g = NoInteropGuard::on();
21084        parse_ok("try { die \"boom\" } catch ($e) { p $e }");
21085    }
21086
21087    /// File-test operators (`-d`, `-f`, `-r`, `-e`, …) are unary prefix
21088    /// ops on a filename or filehandle. Stryke inherits the Perl shape.
21089    #[test]
21090    fn file_test_operators_parse() {
21091        let _g = NoInteropGuard::on();
21092        parse_ok("p 1 if -d \"/tmp\"");
21093        parse_ok("p 2 if -f \"/etc/hosts\"");
21094        parse_ok("p 3 if -e $0");
21095        parse_ok("my $sz = -s \"/etc/hosts\"");
21096    }
21097
21098    /// `~>` (thread-first) and `~>>` (thread-last) macros — stryke's
21099    /// signature pipeline operators alongside `|>`. Pin the basic
21100    /// parsing shape.
21101    #[test]
21102    fn thread_macros_parse() {
21103        let _g = NoInteropGuard::on();
21104        parse_ok("my $r = ~> 5 +1 *2");
21105        parse_ok("my @r = ~> (1,2,3) maps { _ * 2 }");
21106    }
21107
21108    /// Hash-destructure parameter — `fn f({ a => $a, b => $b })` —
21109    /// is a stryke-extension signature shape used to unpack a hashref
21110    /// at call time.
21111    #[test]
21112    fn hash_destructure_sub_signature_parses() {
21113        let _g = NoInteropGuard::on();
21114        parse_ok("fn handle({ name => $name, qty => $qty }) { p \"$name x $qty\" }");
21115    }
21116
21117    /// Anonymous fn (`fn { ... }`) — the implicit-positional closure
21118    /// shape. Used as a first-class value: assigned, returned, passed.
21119    #[test]
21120    fn anonymous_fn_parses() {
21121        let _g = NoInteropGuard::on();
21122        parse_ok("my $f = fn { _0 * 2 }");
21123        parse_ok("my @doubled = maps { _0 * 2 } (1, 2, 3)");
21124    }
21125
21126    /// Ternary `cond ? a : b` chains for inline branching.
21127    #[test]
21128    fn ternary_and_chained_ternary_parse() {
21129        let _g = NoInteropGuard::on();
21130        parse_ok("my $r = $x > 0 ? \"pos\" : \"neg\"");
21131        parse_ok("my $r = $x > 0 ? \"pos\" : $x < 0 ? \"neg\" : \"zero\"");
21132    }
21133
21134    /// Negative indices on array slice — `@arr[-3:-1]` for the last
21135    /// three elements. Used in the parallel_primes demo.
21136    #[test]
21137    fn array_slice_with_negative_indices_parses() {
21138        let _g = NoInteropGuard::on();
21139        parse_ok("my @arr = (1,2,3,4,5); my @tail = @arr[-3:-1]");
21140        parse_ok("my @arr = (1,2,3); my @last_two = @arr[-2:]");
21141    }
21142
21143    /// `state $x` declarations (function-local persistent storage)
21144    /// used by the memoised-fib demo for the cache hash.
21145    #[test]
21146    fn state_variable_declaration_parses() {
21147        let _g = NoInteropGuard::on();
21148        // Use clearly-non-builtin fn names (`counter` / `memo` clash
21149        // with stryke builtins).
21150        parse_ok("fn my_counter { state $n = 0; $n++; $n }");
21151        parse_ok("fn my_memo($k) { state %cache; $cache{$k} //= compute($k) }");
21152    }
21153
21154    /// `our` declarations are package-globals — still legal in strict
21155    /// mode (just sub/say/scalar/reverse are rejected, not `our`).
21156    #[test]
21157    fn our_declaration_parses() {
21158        let _g = NoInteropGuard::on();
21159        parse_ok("our $VERSION = 1.0");
21160        parse_ok("our @EXPORT = (1, 2, 3)");
21161    }
21162
21163    /// Regex binding operators — `=~` (match) and `!~` (negated match).
21164    /// Used in roman_numerals_no_interop for input validation.
21165    #[test]
21166    fn regex_binding_operators_parse() {
21167        let _g = NoInteropGuard::on();
21168        parse_ok("p 1 if $s =~ /^\\d+$/");
21169        parse_ok("p 0 unless $s !~ /[A-Z]/");
21170        parse_ok("my @m = $s =~ /(\\w+)/g");
21171    }
21172
21173    /// Nested data-structure literals — hash of array of hash, the
21174    /// shape used by csv_summary_no_interop (`%by_region` is a
21175    /// hash of arrays of hashrefs).
21176    #[test]
21177    fn nested_data_structure_literals_parse() {
21178        let _g = NoInteropGuard::on();
21179        parse_ok("my %h = (a => [1, 2, 3], b => [4, 5])");
21180        parse_ok("my @rows = (+{ x => 1 }, +{ x => 2 })");
21181        parse_ok("my %grid = (cells => [+{ row => 1 }, +{ row => 2 }])");
21182    }
21183
21184    /// Anonymous fn with explicit params — `fn ($x, $y) { ... }`.
21185    /// Complements the `fn { _0 + _1 }` implicit-positional form.
21186    #[test]
21187    fn anonymous_fn_with_explicit_params_parses() {
21188        let _g = NoInteropGuard::on();
21189        parse_ok("my $add = fn ($x, $y) { $x + $y }");
21190        parse_ok("my $h = fn ($v, %opts) { p $v; p %opts }");
21191    }
21192
21193    /// `package Foo;` and `package Foo::Bar;` declarations — qualified
21194    /// namespace setup. Still legal in strict mode.
21195    #[test]
21196    fn package_declaration_parses() {
21197        let _g = NoInteropGuard::on();
21198        parse_ok("package Foo; my $x = 1");
21199        parse_ok("package Foo::Bar::Baz; our $VERSION = 0.01");
21200    }
21201
21202    /// `next` / `last` / `redo` loop-control statements (used in
21203    /// balanced_brackets / brainfuck / sieve demos to break out of
21204    /// inner loops).
21205    #[test]
21206    fn loop_control_keywords_parse() {
21207        let _g = NoInteropGuard::on();
21208        parse_ok("for my $i (1:10) { next if $i % 2; p $i }");
21209        parse_ok("while (1) { last if $done }");
21210        parse_ok("my $rerun = 0; for (1:5) { if ($rerun) { redo } }");
21211    }
21212
21213    /// Labelled loops + labelled `next` / `last`. Used when you need
21214    /// to break out of nested loops.
21215    #[test]
21216    fn labelled_loops_parse() {
21217        let _g = NoInteropGuard::on();
21218        parse_ok("OUTER: for my $i (1:10) { last OUTER if $i > 5 }");
21219        parse_ok("OUTER: for my $i (1:3) { INNER: for my $j (1:3) { next OUTER if $j > $i } }");
21220    }
21221
21222    /// String-repeat operator `x` — `\"-\" x 40` for a separator line,
21223    /// `(0) x N` for an initialized array. Both shapes appear in the
21224    /// histogram / sieve / brainfuck demos.
21225    #[test]
21226    fn string_repeat_x_operator_parses() {
21227        let _g = NoInteropGuard::on();
21228        parse_ok("my $sep = \"-\" x 40");
21229        parse_ok("my @zeros = (0) x 100");
21230        parse_ok("my $bar = \"#\" x $count");
21231    }
21232
21233    /// `chomp` / `chop` builtins — mutate-in-place string ops on the
21234    /// topic or an explicit lvalue. Used in stdin-reading scripts.
21235    #[test]
21236    fn chomp_chop_parse() {
21237        let _g = NoInteropGuard::on();
21238        parse_ok("chomp(my $line = <STDIN>)");
21239        parse_ok("my $s = \"hi\\n\"; chomp $s");
21240        parse_ok("my $t = \"hi\"; chop $t");
21241    }
21242
21243    /// Substitution operator `s/pat/repl/flags` — used by
21244    /// palindrome_no_interop to strip non-alphanumerics.
21245    #[test]
21246    fn substitution_operator_parses() {
21247        let _g = NoInteropGuard::on();
21248        parse_ok("my $s = \"abc\"; $s =~ s/b/X/");
21249        parse_ok("my $s = \"AaBb\"; $s =~ s/[a-z]//g");
21250        parse_ok("my $s = \"Hello\"; $s =~ s/(.)/\\1\\1/g");
21251    }
21252
21253    /// `sprintf` for column-aligned strings — used by every demo's
21254    /// table output.
21255    #[test]
21256    fn sprintf_parses_with_format_specs() {
21257        let _g = NoInteropGuard::on();
21258        parse_ok("my $row = sprintf(\"%-10s %5d\", \"foo\", 42)");
21259        parse_ok("p sprintf(\"%.3f ms\", 1.234)");
21260        parse_ok("p sprintf(\"%04x\", 255)");
21261    }
21262
21263    /// `<STDIN>` diamond reads — list and scalar context. Used by
21264    /// wordcount / csv_summary / anagram demos.
21265    #[test]
21266    fn diamond_stdin_reads_parse() {
21267        let _g = NoInteropGuard::on();
21268        parse_ok("my $line = <STDIN>");
21269        parse_ok("my @lines = <STDIN>");
21270        parse_ok("while (my $line = <STDIN>) { p $line }");
21271    }
21272
21273    /// Chained method calls — `$obj->foo->bar(arg)`. Used in
21274    /// build_destroy and class demos.
21275    #[test]
21276    fn chained_method_calls_parse() {
21277        let _g = NoInteropGuard::on();
21278        parse_ok("$obj->foo->bar");
21279        parse_ok("my $r = $row->{region}");
21280        parse_ok("$obj->set(1)->get");
21281    }
21282
21283    /// Hash dereference syntaxes — `%$href`, `keys %$href`,
21284    /// `$href->{key}`, `%{ expr }`. Used by set_ops_no_interop.
21285    #[test]
21286    fn hash_deref_forms_parse() {
21287        let _g = NoInteropGuard::on();
21288        parse_ok("my $h = +{a=>1}; my %copy = %$h");
21289        parse_ok("my $h = +{a=>1}; my @k = keys %$h");
21290        parse_ok("my $h = +{a=>1}; p $h->{a}");
21291        parse_ok("my $h = +{a=>1, b=>2}; p len(keys %{$h})");
21292    }
21293
21294    /// `unless` postfix on a `die` — the canonical assertion shape
21295    /// every demo uses for self-tests.
21296    #[test]
21297    fn die_unless_assertion_parses() {
21298        let _g = NoInteropGuard::on();
21299        parse_ok("die \"x must be 1\" unless 1 == 1");
21300        parse_ok("die \"empty list\" if len(@x) == 0");
21301        parse_ok("my $x = 1; die \"hi\" unless $x");
21302    }
21303
21304    /// `qw(...)` quote-words literal — the bareword-list shape used in
21305    /// the older example scripts.
21306    #[test]
21307    fn qw_literal_parses() {
21308        let _g = NoInteropGuard::on();
21309        parse_ok("my @w = qw(red green blue)");
21310        parse_ok("for my $name (qw(Alice Bob Carol)) { p $name }");
21311    }
21312
21313    /// Array slice with explicit indices — `@arr[0, 2, 4]`. Different
21314    /// from range slice `@arr[1:3]`.
21315    #[test]
21316    fn array_slice_with_explicit_indices_parses() {
21317        let _g = NoInteropGuard::on();
21318        parse_ok("my @a = (10, 20, 30, 40); my @s = @a[0, 2]");
21319        parse_ok("my @a = (10, 20, 30); my @s = @a[2, 0, 1]");
21320    }
21321
21322    /// Hash slice — `@h{'a', 'b'}` returns the list of values at keys
21323    /// 'a' and 'b'. Different sigil context than scalar hash access.
21324    #[test]
21325    fn hash_slice_with_keys_parses() {
21326        let _g = NoInteropGuard::on();
21327        parse_ok("my %h = (a=>1, b=>2, c=>3); my @v = @h{'a','c'}");
21328    }
21329
21330    /// Bitwise operators — `&`, `|`, `^`, `~`, `<<`, `>>`. Used by
21331    /// bitops_no_interop. Each binds tighter than the comparison
21332    /// operators, looser than the arithmetic ones (Perl precedence).
21333    #[test]
21334    fn bitwise_operators_parse() {
21335        let _g = NoInteropGuard::on();
21336        parse_ok("my $x = 0xAA; my $y = $x & 0x0F");
21337        parse_ok("my $x = 1; my $y = $x | 2 | 4");
21338        parse_ok("my $x = 0xFF; my $y = $x ^ 0x80");
21339        parse_ok("my $x = 0xAA; my $y = ~$x & 0xff");
21340        parse_ok("my $x = 1; my $y = $x << 4");
21341        parse_ok("my $x = 0xF0; my $y = $x >> 2");
21342    }
21343
21344    /// `0x`, `0b`, `0o` numeric literal prefixes — hex / binary /
21345    /// octal. Used freely in bitops + numeric-conversion demos.
21346    #[test]
21347    fn numeric_literal_prefixes_parse() {
21348        let _g = NoInteropGuard::on();
21349        parse_ok("my $hex = 0xCAFEBABE");
21350        parse_ok("my $bin = 0b1101");
21351        parse_ok("my $hex8 = 0xff & 0x0f");
21352    }
21353
21354    /// Negative array index — `$arr[-1]`, `$arr[-2]`. Used by
21355    /// shunting_yard (`$ops[-1]` to peek stack top).
21356    #[test]
21357    fn negative_array_index_parses() {
21358        let _g = NoInteropGuard::on();
21359        // `@a` / `@b` array names share the `$a` / `$b` reservation in
21360        // strict mode — use `@arr` instead.
21361        parse_ok("my @arr = (1, 2, 3); p $arr[-1]");
21362        parse_ok("my @stack = (10, 20, 30); p $stack[-1] if len(@stack) > 0");
21363    }
21364
21365    /// `last` / `next` / `return` as standalone statements (already
21366    /// pinned alongside `last LABEL` but not as the lone-line form).
21367    #[test]
21368    fn loop_control_standalone_parses() {
21369        let _g = NoInteropGuard::on();
21370        parse_ok("while (1) { last }");
21371        parse_ok("for my $i (1:10) { next if $i == 3 }");
21372        parse_ok("fn ret { return 42 }");
21373    }
21374
21375    /// `shift @args` / `pop @args` — common destructuring shape inside
21376    /// functions for "first arg" / "last arg" pickoff (used by morse
21377    /// to peel the mode off `@ARGV`).
21378    #[test]
21379    fn shift_pop_on_array_parses() {
21380        let _g = NoInteropGuard::on();
21381        parse_ok("my @a = (1, 2, 3); my $first = shift @a");
21382        parse_ok("my @a = (1, 2, 3); my $last  = pop @a");
21383        parse_ok("fn drop_first(@xs) { shift @xs; @xs }");
21384    }
21385
21386    /// Special internal names — `__FILE__`, `__LINE__`, `__PACKAGE__`
21387    /// — accessed by tooling and error reporting.
21388    #[test]
21389    fn special_internal_names_parse() {
21390        let _g = NoInteropGuard::on();
21391        parse_ok("p __FILE__");
21392        parse_ok("p __LINE__");
21393        parse_ok("p __PACKAGE__");
21394        parse_ok("p __FILE__ . \":\" . __LINE__");
21395    }
21396
21397    /// Nested ternary RHS with parens / chained alternatives. Used by
21398    /// the conway demo's pattern dispatch.
21399    #[test]
21400    fn deeply_nested_ternary_parses() {
21401        let _g = NoInteropGuard::on();
21402        parse_ok("my $g = ($p eq \"a\") ? 1 : ($p eq \"b\") ? 2 : ($p eq \"c\") ? 3 : 4");
21403    }
21404
21405    /// Array-of-hashref + index-into-hash subscript chain:
21406    /// `$rows[0]->{name}`, `$rows[-1]->{score}`. Used in csv_summary
21407    /// and ranking demos.
21408    #[test]
21409    fn array_of_hashref_chained_subscript_parses() {
21410        let _g = NoInteropGuard::on();
21411        parse_ok("my @rows = (+{name=>'a',sc=>1}); p $rows[0]->{name}");
21412        parse_ok("my @rows = (+{n=>10},+{n=>20}); p $rows[-1]->{n}");
21413    }
21414
21415    /// Nested `for` loops with two index variables — the 2D grid walk
21416    /// shape used in conway.
21417    #[test]
21418    fn nested_for_loops_parse() {
21419        let _g = NoInteropGuard::on();
21420        parse_ok("for my $r (0:5) { for my $c (0:5) { p \"$r,$c\" } }");
21421    }
21422
21423    /// Compound assignment `$x .= ...` (string append). Used by every
21424    /// demo that builds output strings incrementally (brainfuck,
21425    /// RLE encode, Caesar).
21426    #[test]
21427    fn dot_assign_string_append_parses() {
21428        let _g = NoInteropGuard::on();
21429        parse_ok("my $s = \"a\"; $s .= \"b\"");
21430        parse_ok("my $out = \"\"; $out .= \"x\" for (1:3)");
21431    }
21432
21433    /// `unshift @arr, $v` — push to the FRONT of an array; used by
21434    /// graph_bfs for back-tracking the path.
21435    #[test]
21436    fn unshift_parses() {
21437        let _g = NoInteropGuard::on();
21438        parse_ok("my @path = (3, 4); unshift @path, 2; unshift @path, 1");
21439        parse_ok("my @q; unshift @q, $_ for (1:5)");
21440    }
21441
21442    /// `defined` and `// 0` defined-or fallback. Used by the
21443    /// graph_bfs neighbour lookup (`$REVERSE{$ch} // 0`).
21444    #[test]
21445    fn defined_or_fallback_parses() {
21446        let _g = NoInteropGuard::on();
21447        parse_ok("my $x; my $y = $x // 0");
21448        parse_ok("my %h = (a=>1); my $v = $h{missing} // -1");
21449        parse_ok("p 1 if defined $foo");
21450    }
21451
21452    /// `for my $x (rev 0:N)` — reverse a range with the stryke `rev`
21453    /// keyword. Used by knapsack back-tracking.
21454    #[test]
21455    fn rev_over_range_parses() {
21456        let _g = NoInteropGuard::on();
21457        parse_ok("for my $i (rev 0:9) { p $i }");
21458        parse_ok("my @r = rev (1, 2, 3, 4)");
21459    }
21460
21461    /// Array deref `@$ref`, `@{$expr}`. Used freely in graph_bfs +
21462    /// knapsack to walk arrays-of-arrayrefs.
21463    #[test]
21464    fn array_deref_forms_parse() {
21465        let _g = NoInteropGuard::on();
21466        parse_ok("my $r = [1, 2, 3]; my @copy = @$r");
21467        parse_ok("my $r = [1, 2, 3]; my @copy = @{$r}");
21468        parse_ok("my @rs = ([1], [2, 3]); my @flat; push @flat, @$_ for @rs");
21469    }
21470
21471    /// Hashref via `[k]` doesn't exist — but `$ref->{k}` and `${$ref}{k}`
21472    /// are the two equivalent shapes. Pin both.
21473    #[test]
21474    fn hashref_subscript_alt_forms_parse() {
21475        let _g = NoInteropGuard::on();
21476        parse_ok("my $h = +{a=>1}; p $h->{a}");
21477        parse_ok("my $h = +{a=>1}; p ${$h}{a}");
21478    }
21479
21480    /// `abs($x)`, `int($x)`, `sqrt($x)` — common numeric builtins
21481    /// invoked as functions.
21482    #[test]
21483    fn numeric_builtins_parse() {
21484        let _g = NoInteropGuard::on();
21485        parse_ok("my $x = abs(-5)");
21486        parse_ok("my $f = int(3.7)");
21487        parse_ok("my $s = sqrt(2)");
21488        parse_ok("my $n = int($x * 100 + 0.5) / 100");
21489    }
21490
21491    /// `local $var` declaration — dynamic-scoped binding restored
21492    /// on block exit. Different from `my` (lexical) and `our`
21493    /// (package-global).
21494    #[test]
21495    fn local_declaration_parses() {
21496        let _g = NoInteropGuard::on();
21497        // Strict mode rejects `sub` — use anonymous `fn` for the scope.
21498        parse_ok("our $g = 1; (fn { local $g = 99; p $g })->()");
21499    }
21500
21501    /// `srand($seed)` and `rand($n)` — deterministic random with
21502    /// optional bound. Used by dice + histogram demos for repeatable
21503    /// CI output.
21504    #[test]
21505    fn srand_rand_parse() {
21506        let _g = NoInteropGuard::on();
21507        parse_ok("srand(42); my $r = rand(6)");
21508        parse_ok("srand(); my $r = int(rand(100))");
21509    }
21510
21511    /// `substr($s, $i, $n)` for slicing strings (2-arg + 3-arg forms).
21512    /// Used by base64 + soundex + RPN demos.
21513    #[test]
21514    fn substr_parses() {
21515        let _g = NoInteropGuard::on();
21516        parse_ok("my $s = \"hello\"; p substr($s, 0, 1)");
21517        parse_ok("my $s = \"hello\"; p substr($s, 1)");
21518        parse_ok("my $s = \"hello\"; p substr($s, -2)");
21519    }
21520
21521    /// Underscore separator in numeric literals — `1_000_000` for
21522    /// readability. Used in `pmap { ... } 1:1_000` style code.
21523    #[test]
21524    fn underscore_separators_in_numbers_parse() {
21525        let _g = NoInteropGuard::on();
21526        parse_ok("my $n = 1_000_000");
21527        parse_ok("my $r = 1_000_000 / 365");
21528        parse_ok("my $hex = 0xff_ff");
21529    }
21530
21531    /// 2D array-of-arrayref construction `[[a,b], [c,d]]` and access
21532    /// `$grid->[0]->[1]`. Used by interval_merge and dijkstra demos.
21533    #[test]
21534    fn arrayref_of_arrayref_access_parses() {
21535        let _g = NoInteropGuard::on();
21536        parse_ok("my $grid = [[1, 2], [3, 4]]");
21537        parse_ok("my $grid = [[1, 2], [3, 4]]; p $grid->[0]->[1]");
21538        parse_ok("my $grid = [[1, 2], [3, 4]]; p $grid->[1][0]");
21539    }
21540
21541    /// Mutating array element via arrow-arrow chain — `$ref->[0]->[1] = $v`.
21542    /// Used by interval_merge to extend an interval's end in place.
21543    #[test]
21544    fn arrow_chain_assignment_parses() {
21545        let _g = NoInteropGuard::on();
21546        parse_ok("my $g = [[0, 0]]; $g->[0]->[1] = 99");
21547        parse_ok("my $g = [[0, 0]]; $g->[0][1] = 99");
21548    }
21549
21550    /// Tuple destructure from arrayref element — `my ($a, $b) = @$e`.
21551    /// Pin the no-interop renames (`$a` reserved).
21552    #[test]
21553    fn tuple_destructure_from_arrayref_parses() {
21554        let _g = NoInteropGuard::on();
21555        parse_ok("my $e = [10, 20]; my ($lhs, $rhs) = @$e");
21556        parse_ok("my $e = [\"x\", 1]; my ($name, $weight) = ($e->[0], $e->[1])");
21557    }
21558
21559    /// `next unless` / `last unless` postfix on a loop body. Common
21560    /// guard shape inside the rolling-stats sliding loops.
21561    #[test]
21562    fn next_last_unless_postfix_parses() {
21563        let _g = NoInteropGuard::on();
21564        parse_ok("for my $i (1:10) { next unless $i % 2 == 0; p $i }");
21565        parse_ok("while (1) { last unless $live }");
21566    }
21567
21568    /// `keys %{ ... }` with parenthesised hash dereference — the
21569    /// shape used by deepish hash-of-hash access.
21570    #[test]
21571    fn keys_on_braced_hash_deref_parses() {
21572        let _g = NoInteropGuard::on();
21573        parse_ok("my $h = +{a=>1}; my @k = keys %{$h}");
21574        parse_ok("my $h = +{a=>+{b=>1}}; my @k = keys %{$h->{a}}");
21575    }
21576
21577    /// Namespaced fn definition — `fn Module::method($x) { ... }`.
21578    /// All UDFs in the examples/ demos use this form so future stryke
21579    /// stdlib additions don't shadow user code (or vice versa).
21580    #[test]
21581    fn namespaced_fn_decl_parses() {
21582        let _g = NoInteropGuard::on();
21583        parse_ok("fn Module::method($x) { $x * 2 }");
21584        parse_ok("fn Foo::Bar::helper { 42 }");
21585        parse_ok("fn Demo::run { p \"running\" }");
21586    }
21587
21588    /// Calling a namespaced fn — `Module::method($arg)`. Also used as
21589    /// a method on an explicit invocant in some contexts.
21590    #[test]
21591    fn namespaced_fn_call_parses() {
21592        let _g = NoInteropGuard::on();
21593        parse_ok("fn Module::add($x, $y) { $x + $y } p Module::add(2, 3)");
21594        // `caller` itself is a stryke builtin — use a namespaced caller
21595        // to avoid the clash.
21596        parse_ok("fn Foo::Bar::baz { 1 } fn Demo::main { Foo::Bar::baz() + Foo::Bar::baz() }");
21597    }
21598
21599    /// `index($haystack, $needle)` builtin — the canonical string
21600    /// substring-position lookup that KMP cross-checks against.
21601    #[test]
21602    fn index_builtin_parses() {
21603        let _g = NoInteropGuard::on();
21604        parse_ok("my $i = index(\"hello world\", \"world\")");
21605        parse_ok("my $i = index($s, $pat, 0)");
21606    }
21607
21608    /// `for my $i (rev 1:N)` — reverse iteration over a colon range.
21609    /// Already pinned in `rev_over_range_parses` but here for the
21610    /// `rev (list)` form on an array literal.
21611    #[test]
21612    fn rev_on_array_literal_parses() {
21613        let _g = NoInteropGuard::on();
21614        parse_ok("my @r = rev (1, 2, 3, 4)");
21615        parse_ok("for my $x (rev (\"a\", \"b\", \"c\")) { p $x }");
21616    }
21617
21618    /// Numeric comparison + string comparison side by side — the
21619    /// pattern used by sort blocks with secondary tie-breaking
21620    /// (numeric primary, string secondary). `$a` / `$b` are reserved
21621    /// under --no-interop; use `$_0` / `$_1` for sort comparator args.
21622    #[test]
21623    fn numeric_and_string_comparison_in_one_expr_parses() {
21624        let _g = NoInteropGuard::on();
21625        parse_ok("my $r = $_0 <=> $_1 || $name cmp $other");
21626        parse_ok("p 1 if $x == $y && $name eq \"foo\"");
21627    }
21628
21629    /// Bareword positional names — `_0`, `_1`, `_N` without sigil —
21630    /// are stryke's idiomatic spelling in code contexts. The sigil
21631    /// form (`$_0`, `$_1`) is reserved for string interpolation where
21632    /// a bareword would just be a literal substring. Pin the bareword
21633    /// shape in every common closure position.
21634    #[test]
21635    fn bareword_positional_in_sort_reduce_blocks_parses() {
21636        let _g = NoInteropGuard::on();
21637        // Sort comparator with bareword positional names.
21638        parse_ok("my @s = sort { _0 <=> _1 } (3, 1, 2)");
21639        // Reduce accumulator + element.
21640        parse_ok("my $r = (1, 2, 3) |> reduce { _0 + _1 }");
21641        // Reduce on string concat — _0 acc, _1 next.
21642        parse_ok("my $s = (\"a\", \"b\") |> reduce { _0 . _1 }");
21643    }
21644
21645    /// Bareword topic `_` inside `maps` / `grep` / `pmap` / `pgrep`
21646    /// closure bodies. Tightest form; no sigil needed.
21647    #[test]
21648    fn bareword_topic_in_maps_grep_parses() {
21649        let _g = NoInteropGuard::on();
21650        parse_ok("my @r = (1, 2, 3) |> maps { _ * 2 }");
21651        parse_ok("my @r = (1, 2, 3, 4) |> grep { _ % 2 == 0 }");
21652        parse_ok("my @r = 1:100 |> pmap { _ ** 3 }");
21653        parse_ok("my @r = 1:100 |> pgrep { _ % 7 == 0 }");
21654    }
21655
21656    /// `_` and `_1` in arithmetic expression context (no sigil).
21657    /// Inside string interpolation, only the sigil form `${_}` /
21658    /// `$_0` / `$_1` works — pin the contrast.
21659    #[test]
21660    fn bareword_vs_sigil_in_string_interp_parses() {
21661        let _g = NoInteropGuard::on();
21662        // Bareword in code: tight, idiomatic.
21663        parse_ok("my @r = (5, 10) |> maps { _ + 1 }");
21664        // Sigil in string interp: the bareword would just be the
21665        // literal characters underscore-zero, so the sigil form is
21666        // required here.
21667        parse_ok("p \"first=$_0 second=$_1\"");
21668        parse_ok("p \"got: $_\"");
21669    }
21670
21671    /// Bareword `_` topic inside a `for $_` -style postfix-for body.
21672    /// Used in Conway's life-counter — `$n += _ for @$g`.
21673    #[test]
21674    fn bareword_topic_in_postfix_for_parses() {
21675        let _g = NoInteropGuard::on();
21676        parse_ok("my $n = 0; $n += _ for (1, 2, 3, 4)");
21677        parse_ok("my %h; $h{_}++ for (\"a\", \"b\", \"a\")");
21678    }
21679
21680    /// Bareword `_` with hash-subscript on the outer side —
21681    /// `$h{_}++` is tighter than `$h{$_}++` (no sigil on key).
21682    #[test]
21683    fn bareword_topic_as_hash_key_parses() {
21684        let _g = NoInteropGuard::on();
21685        parse_ok("my %h; $h{_}++ for (\"a\", \"b\", \"a\")");
21686        parse_ok("my @arr = (1, 2, 3); my %seen; $seen{$arr[_]}++ for (0, 1, 2)");
21687    }
21688
21689    /// Hashref subscript inside a `grep` block using the bareword
21690    /// topic — the FirstU::pipe_find shape: `grep { $seen{$c[_]} == 1 }`.
21691    #[test]
21692    fn bareword_topic_inside_subscript_chain_parses() {
21693        let _g = NoInteropGuard::on();
21694        parse_ok(
21695            "my @c = (\"a\", \"b\", \"c\"); my %seen = (a => 1, b => 2, c => 1); \
21696             my @hits = grep { $seen{$c[_]} == 1 } (0, 1, 2)",
21697        );
21698    }
21699
21700    /// `ref($x)` builtin — used by flatten to discriminate arrayref
21701    /// from scalar leaves.
21702    #[test]
21703    fn ref_builtin_parses() {
21704        let _g = NoInteropGuard::on();
21705        parse_ok("my $x = [1, 2]; p ref($x)");
21706        parse_ok("my $r = +{a => 1}; p 1 if ref($r) eq \"HASH\"");
21707    }
21708
21709    /// Expression-bodied recursive `fn` — style guide rule 6. The body
21710    /// is a single ternary that re-invokes the same fn; Kadane / gcd /
21711    /// lcm demos all use this shape.
21712    #[test]
21713    fn expression_bodied_recursive_fn_parses() {
21714        let _g = NoInteropGuard::on();
21715        parse_ok("fn N::gcd = _1 == 0 ? _0 : N::gcd(_1, _0 % _1)");
21716        parse_ok("fn N::lcm = _0 * _1 / N::gcd(_0, _1)");
21717    }
21718
21719    /// Expression-bodied `fn` whose body is a `|> reduce` over the
21720    /// implicit topic — used by gcd_list / lcm_list.
21721    #[test]
21722    fn expression_bodied_pipe_reduce_parses() {
21723        let _g = NoInteropGuard::on();
21724        parse_ok(
21725            "fn N::gcd = _1 == 0 ? _0 : N::gcd(_1, _0 % _1); \
21726                  fn N::gcd_list = @{_} |> reduce { N::gcd(_0, _1) }",
21727        );
21728    }
21729
21730    /// Reduce-fold with a hashref accumulator seeded by prepending the
21731    /// init to the input — Boyer-Moore / Kadane idiom.
21732    #[test]
21733    fn reduce_fold_with_hashref_accumulator_parses() {
21734        let _g = NoInteropGuard::on();
21735        parse_ok(
21736            "my @xs = (3, 2, 3); \
21737             my $st = (+{cur => 0, best => -100}, @xs) |> reduce { \
21738                +{ cur => _1, best => _0->{best} } \
21739             }",
21740        );
21741    }
21742
21743    /// Native array-deref slicing `@$arr[$lo:$hi]` — used by
21744    /// rolling_stats to slice the input window.
21745    #[test]
21746    fn array_deref_slice_with_variable_bounds_parses() {
21747        let _g = NoInteropGuard::on();
21748        parse_ok(
21749            "my @a = (10, 20, 30, 40, 50); my $r = \\@a; \
21750             my $lo = 1; my $hi = 3; \
21751             my @s = @$r[$lo:$hi]",
21752        );
21753    }
21754
21755    /// `min(@$v[$lo:$hi])` / `max(@$v[$lo:$hi])` — rolling_stats hot
21756    /// path. Paren-less `min @$v[..]` parses wrong; pinned with parens.
21757    #[test]
21758    fn min_max_over_array_deref_slice_parses() {
21759        let _g = NoInteropGuard::on();
21760        parse_ok(
21761            "my @a = (1, 3, 2, 5); my $r = \\@a; \
21762             my $lo = 0; my $hi = 2; \
21763             my $m1 = min(@$r[$lo:$hi]); \
21764             my $m2 = max(@$r[$lo:$hi])",
21765        );
21766    }
21767
21768    /// `flat_maps` with recursive call inside the block — flatten
21769    /// demo's central idiom; verifies the recursive call inside a
21770    /// pipeline stage parses cleanly.
21771    #[test]
21772    fn flat_maps_with_recursive_call_parses() {
21773        let _g = NoInteropGuard::on();
21774        parse_ok(
21775            "fn Flat::flatten($r) = @$r |> flat_maps { \
21776                ref(_) eq \"ARRAY\" ? Flat::flatten(_) : (_) \
21777             }",
21778        );
21779    }
21780
21781    /// Inner `map { _->[$i] }` capturing outer lexical `$i` — zip's
21782    /// N-list helper. Bareword topic inside, `$i` is the lexical.
21783    #[test]
21784    fn nested_map_with_outer_lexical_capture_parses() {
21785        let _g = NoInteropGuard::on();
21786        parse_ok(
21787            "my @lists = ([1,2,3], [10,20,30]); \
21788             my @r = 0:2 |> maps { my $i = _; [map { _->[$i] } @lists] }",
21789        );
21790    }
21791
21792    /// `@{_}` deref of the topic — required when sigil-form `@$_` is
21793    /// being avoided per style guide. zip's `len(@{_})` shape.
21794    #[test]
21795    fn array_deref_of_bareword_topic_parses() {
21796        let _g = NoInteropGuard::on();
21797        parse_ok("my @lists = ([1,2,3], [10,20,30]); p min(map { len(@{_}) } @lists)");
21798    }
21799
21800    /// `~>` thread-macro stages for `glob`, `rand`, `srand` — these
21801    /// builtins have their own `ExprKind` (Glob, Rand, Srand), not the
21802    /// generic FuncCall path, so they previously fell through to the
21803    /// default arm and produced "Undefined subroutine" at runtime.
21804    #[test]
21805    fn thread_macro_accepts_glob_rand_srand_stages() {
21806        parse_ok("my @r = ~> \"/tmp/*\" glob sort");
21807        parse_ok("my $i = ~> 100 rand int");
21808        parse_ok("~> 42 srand");
21809    }
21810
21811    /// Recursive expression-bodied `fn` with path compression — the
21812    /// union-find demo idiom: `fn UF::find($uf, $x) { ... }` body
21813    /// re-invokes itself and writes the result into a hashref slot.
21814    #[test]
21815    fn recursive_fn_with_arrayref_assignment_parses() {
21816        let _g = NoInteropGuard::on();
21817        parse_ok(
21818            "fn UF::find($uf, $x) { \
21819                return $x if $uf->{parent}[$x] == $x; \
21820                $uf->{parent}[$x] = UF::find($uf, $uf->{parent}[$x]); \
21821                $uf->{parent}[$x] \
21822             }",
21823        );
21824    }
21825
21826    /// Hash-initialised with computed list inside arrayref literal —
21827    /// `[0:$n - 1]` and `[(0) x $n]` as field values. Used by UF::new.
21828    #[test]
21829    fn hashref_init_with_range_and_repeat_parses() {
21830        let _g = NoInteropGuard::on();
21831        parse_ok("fn UF::new($n) = +{ parent => [0:$n - 1], rank => [(0) x $n], count => $n }");
21832    }
21833
21834    /// Postfix-for over an arrayref-deref: `Trie::insert($t, $_) for @$words`.
21835    /// Used by Trie::from_words.
21836    #[test]
21837    fn postfix_for_arrayref_deref_parses() {
21838        let _g = NoInteropGuard::on();
21839        parse_ok("my @words = (\"a\", \"b\"); my $r = \\@words; my @out; push @out, $_ for @$r");
21840    }
21841
21842    /// Tuple-swap destructure inside a block — UF::union flips ra/rb
21843    /// for union-by-rank.
21844    #[test]
21845    fn tuple_swap_destructure_parses() {
21846        let _g = NoInteropGuard::on();
21847        parse_ok("my $ra = 1; my $rb = 2; ($ra, $rb) = ($rb, $ra)");
21848    }
21849
21850    /// 2-D array initialised with explicit row arrayrefs — the
21851    /// `--no-interop` mode rejects 2-D autoviv (`$d[$i][$j] = X`
21852    /// on un-initialized `@d`), so each row must be an arrayref
21853    /// literal first. Damerau-Levenshtein demo's matrix setup.
21854    #[test]
21855    fn explicit_2d_array_row_init_parses() {
21856        let _g = NoInteropGuard::on();
21857        parse_ok(
21858            "my @d; my $m = 3; my $n = 4; \
21859             for my $i (0:$m) { $d[$i] = [(0) x ($n + 1)] }",
21860        );
21861    }
21862
21863    /// `min()` with 3 arguments — Damerau-Levenshtein uses this for the
21864    /// deletion/insertion/substitution step.
21865    #[test]
21866    fn min_with_three_args_parses() {
21867        let _g = NoInteropGuard::on();
21868        parse_ok("my $x = min(1, 2, 3)");
21869        parse_ok("my @d; $d[0][0] = 5; my $r = min($d[0][0] + 1, $d[0][0] + 1, $d[0][0] + 0)");
21870    }
21871
21872    /// String slice with `$s[N:M]` where M is `len(...)`-based.
21873    /// Trie::count_with_prefix shape.
21874    #[test]
21875    fn string_slice_with_len_bound_parses() {
21876        let _g = NoInteropGuard::on();
21877        parse_ok(
21878            "my $w = \"apple\"; my $pre = \"app\"; \
21879             my $ok = len($w) >= len($pre) && $w[0:len($pre) - 1] eq $pre",
21880        );
21881    }
21882
21883    /// Sort comparator with `_0->[N] <=> _1->[N]` — sorting an
21884    /// array-of-arrayrefs by a positional field. Kruskal MST pattern.
21885    #[test]
21886    fn sort_block_with_arrow_deref_topic_parses() {
21887        let _g = NoInteropGuard::on();
21888        parse_ok(
21889            "my @edges = ([0,1,4], [2,3,1], [1,2,2]); \
21890             my @sorted = sort { _0->[2] <=> _1->[2] } @edges",
21891        );
21892    }
21893
21894    /// C-style `for` header with declarations and post-decrement —
21895    /// Knuth shuffle's inner loop walks high-to-low.
21896    #[test]
21897    fn cstyle_for_with_postdecrement_parses() {
21898        let _g = NoInteropGuard::on();
21899        parse_ok(
21900            "my @arr = (1, 2, 3, 4, 5); \
21901             my $n = len(@arr); \
21902             for (my $i = $n - 1; $i > 0; $i--) { p $arr[$i] }",
21903        );
21904    }
21905
21906    /// Tuple-swap on array-deref index pairs — Knuth-shuffle inner
21907    /// step swaps `$r->[$i]` and `$r->[$j]` via destructure.
21908    #[test]
21909    fn tuple_swap_arrayref_index_parses() {
21910        let _g = NoInteropGuard::on();
21911        parse_ok(
21912            "my @arr = (1, 2, 3); my $r = \\@arr; my $i = 0; my $j = 2; \
21913             ($r->[$i], $r->[$j]) = ($r->[$j], $r->[$i])",
21914        );
21915    }
21916
21917    /// Recursive backtracking — N-queens pushes a column, recurses,
21918    /// then pops. Verifies array mutation inside recursive fn calls.
21919    #[test]
21920    fn recursive_backtracking_arrayref_mutation_parses() {
21921        let _g = NoInteropGuard::on();
21922        parse_ok(
21923            "fn Q::go($n, $cols, $count_ref) { \
21924                my $r = len(@$cols); \
21925                if ($r == $n) { $$count_ref++; return } \
21926                for my $c (0:$n - 1) { \
21927                    push @$cols, $c; \
21928                    Q::go($n, $cols, $count_ref); \
21929                    pop @$cols \
21930                } \
21931             }",
21932        );
21933    }
21934
21935    /// Doubly-linked list as a hash-of-{prev,next} — LRU cache's
21936    /// node-table pattern. `$nodes->{$k}{prev}` chain.
21937    #[test]
21938    fn nested_hashref_chain_parses() {
21939        let _g = NoInteropGuard::on();
21940        parse_ok(
21941            "my $c = +{ nodes => +{ a => +{ val => 1, prev => undef, next => \"b\" } } }; \
21942             my $p = $c->{nodes}{a}{prev}; \
21943             my $n = $c->{nodes}{a}{next}",
21944        );
21945    }
21946
21947    /// `$$count_ref++` — dereference a scalar-ref and post-increment.
21948    /// Used in Queens::recur to update a shared counter.
21949    #[test]
21950    fn scalar_ref_postincrement_parses() {
21951        let _g = NoInteropGuard::on();
21952        parse_ok("my $n = 0; my $ref = \\$n; $$ref++; p $n");
21953    }
21954
21955    /// `$h{$k}` autoviv chain — Markov bigram table builds a
21956    /// hash-of-hash where the inner is created on first access.
21957    #[test]
21958    fn hash_of_hash_autoviv_increment_parses() {
21959        let _g = NoInteropGuard::on();
21960        parse_ok(
21961            "my %table; \
21962             my $prev = \"the\"; my $next = \"quick\"; \
21963             $table{$prev} //= +{}; \
21964             $table{$prev}{$next}++",
21965        );
21966    }
21967
21968    /// `for (... ; ...) { ... }` C-style with literal counter — Knuth
21969    /// shuffle's high-to-low walk and similar.
21970    #[test]
21971    fn cstyle_for_with_literal_bounds_parses() {
21972        let _g = NoInteropGuard::on();
21973        parse_ok("my $sum = 0; for (my $i = 0; $i < 10; $i++) { $sum += $i }");
21974    }
21975
21976    /// Recursive expression-bodied `fn` with ternary base case —
21977    /// Josephus closed-form. Single-letter tail segments in namespaced
21978    /// names (`J::s`, `Foo::m`, `Foo::q`, `Foo::qx`, `Foo::qr`) are
21979    /// identifiers — the `::` prefix disambiguates from the
21980    /// `s/.../.../`, `m//`, `q//`, etc. quote-like operators.
21981    #[test]
21982    fn recursive_expression_body_with_ternary_parses() {
21983        let _g = NoInteropGuard::on();
21984        parse_ok("fn J::s($n, $k) = $n == 1 ? 0 : (J::s($n - 1, $k) + $k) % $n");
21985    }
21986
21987    /// All quote-like single/two-letter operators (`s`, `m`, `q`, `qq`,
21988    /// `qx`, `qr`, `tr`, `y`) are valid namespaced identifier tails
21989    /// after `::` — they're not lexed as their regex/quote forms.
21990    #[test]
21991    fn namespaced_quote_like_tail_segments_parse() {
21992        let _g = NoInteropGuard::on();
21993        parse_ok("fn Foo::s($x) = $x + 1");
21994        parse_ok("fn Foo::m($x) = $x * 2");
21995        parse_ok("fn Foo::q($x) = $x");
21996        parse_ok("fn Foo::qq($x) = $x");
21997        parse_ok("fn Foo::qx($x) = $x");
21998        parse_ok("fn Foo::qr($x) = $x");
21999        parse_ok("fn Foo::tr($x) = $x");
22000        parse_ok("fn Foo::y($x) = $x");
22001    }
22002
22003    /// `splice @arr, $i, 1` — remove a single element from middle of
22004    /// an array. Josephus simulate uses this.
22005    #[test]
22006    fn splice_single_remove_parses() {
22007        let _g = NoInteropGuard::on();
22008        parse_ok("my @circle = 0:5; splice @circle, 2, 1");
22009    }
22010
22011    /// `atan2(0, -1)` for π — Monte Carlo's true-reference value.
22012    #[test]
22013    fn atan2_call_parses() {
22014        let _g = NoInteropGuard::on();
22015        parse_ok("fn MC::true_pi = atan2(0, -1)");
22016        parse_ok("my $pi = atan2(0, -1)");
22017    }
22018
22019    /// Flat 1-D array indexed as 2-D via `r * COLS + c` — Sudoku
22020    /// board layout. Arithmetic inside subscripts.
22021    #[test]
22022    fn flat_2d_array_indexing_parses() {
22023        let _g = NoInteropGuard::on();
22024        parse_ok(
22025            "my @board = (0) x 81; \
22026             my $r = 3; my $c = 5; \
22027             $board[$r * 9 + $c] = 7; \
22028             my $v = $board[$r * 9 + $c]",
22029        );
22030    }
22031
22032    /// `par` is callable as a top-level expression, not just an
22033    /// `~>` thread-macro stage. Prefix form: `par { BLOCK } LIST`.
22034    /// (Previously parser only accepted `par` inside thread macros,
22035    /// emitting "Undefined subroutine &par" at runtime for any other
22036    /// call site.)
22037    #[test]
22038    fn par_top_level_prefix_form_parses() {
22039        let _g = NoInteropGuard::on();
22040        parse_ok("my @r = par { _ * 2 } (1, 2, 3, 4)");
22041        parse_ok("par { p _ } @big");
22042    }
22043
22044    /// 2-D DP table with `max()` step — LCS / Levenshtein / Damerau /
22045    /// general edit-distance pattern. Validates that 3-way max +
22046    /// nested arrayref subscript chain parses cleanly.
22047    #[test]
22048    fn dp_max_step_chained_subscript_parses() {
22049        let _g = NoInteropGuard::on();
22050        parse_ok(
22051            "my @d; for my $i (0:3) { $d[$i] = [(0) x 4] } \
22052             $d[1][1] = max($d[0][1], $d[1][0]); \
22053             my $r = $d[1][1]",
22054        );
22055    }
22056
22057    /// Rolling polynomial hash arithmetic — Rabin-Karp's window update.
22058    #[test]
22059    fn rolling_hash_arithmetic_parses() {
22060        let _g = NoInteropGuard::on();
22061        parse_ok(
22062            "my $h = 0; my $base = 257; my $mod = 1000000007; my $high = 256; \
22063             my $drop = 65; my $add = 90; \
22064             $h = (($h - $drop * $high) * $base + $add) % $mod; \
22065             $h = ($h + $mod) % $mod",
22066        );
22067    }
22068
22069    /// Triple-nested loop with index expressions on a 2-D arrayref —
22070    /// Floyd-Warshall's k/i/j signature.
22071    #[test]
22072    fn triple_nested_2d_via_k_parses() {
22073        let _g = NoInteropGuard::on();
22074        parse_ok(
22075            "my @d; for my $i (0:3) { $d[$i] = [(0) x 4] } \
22076             for my $k (0:3) { for my $i (0:3) { for my $j (0:3) { \
22077                $d[$i][$j] = $d[$i][$k] + $d[$k][$j] \
22078                    if $d[$i][$k] + $d[$k][$j] < $d[$i][$j] \
22079             } } }",
22080        );
22081    }
22082
22083    /// DP fill with `@dp = ($INF) x ($amount + 1)` repeat-init.
22084    /// Coin-change shape.
22085    #[test]
22086    fn dp_array_repeat_init_parses() {
22087        let _g = NoInteropGuard::on();
22088        parse_ok("my $amount = 11; my $INF = 1e18; my @dp = ($INF) x ($amount + 1); $dp[0] = 0");
22089    }
22090
22091    /// `join("", rev split //, $s)` — palindrome check pipeline.
22092    #[test]
22093    fn rev_split_join_chain_parses() {
22094        let _g = NoInteropGuard::on();
22095        parse_ok("my $s = \"abc\"; my $r = join(\"\", rev split //, $s)");
22096    }
22097
22098    /// C-style `for` with explicit init / cond / decrement step —
22099    /// heap-sort's sift-down walks the heap children from end down.
22100    #[test]
22101    fn cstyle_for_decrement_with_arrayref_swap_parses() {
22102        let _g = NoInteropGuard::on();
22103        parse_ok(
22104            "my @arr = (1, 2, 3); my $r = \\@arr; \
22105             for (my $end = len(@arr) - 1; $end > 0; $end--) { \
22106                ($r->[0], $r->[$end]) = ($r->[$end], $r->[0]) \
22107             }",
22108        );
22109    }
22110
22111    /// `shift @q` inside a while-loop driving a BFS-style queue —
22112    /// Kahn's topological sort.
22113    #[test]
22114    fn shift_in_while_loop_parses() {
22115        let _g = NoInteropGuard::on();
22116        parse_ok(
22117            "my @q = (0, 1, 2); my @out; \
22118             while (len(@q) > 0) { my $u = shift @q; push @out, $u }",
22119        );
22120    }
22121
22122    /// Modular exponentiation by squaring — Miller-Rabin's core. While
22123    /// loop with `int($e / 2)`, modular multiply, and conditional update.
22124    #[test]
22125    fn mod_pow_squaring_loop_parses() {
22126        let _g = NoInteropGuard::on();
22127        parse_ok(
22128            "fn MR::mod_pow($base, $exp, $m) { \
22129                my $result = 1; my $bs = $base % $m; my $e = $exp; \
22130                while ($e > 0) { \
22131                    $result = ($result * $bs) % $m if $e % 2 == 1; \
22132                    $e = int($e / 2); \
22133                    $bs = ($bs * $bs) % $m \
22134                } \
22135                $result \
22136             }",
22137        );
22138    }
22139
22140    /// 2-D DP traceback: walk back from dp[n][target], conditionally
22141    /// taking or skipping each item. Subset-sum reconstruct shape.
22142    #[test]
22143    fn dp_traceback_walk_parses() {
22144        let _g = NoInteropGuard::on();
22145        parse_ok(
22146            "my @xs = (1, 2, 3); my $n = 3; my $target = 4; \
22147             my @dp; for my $i (0:$n) { $dp[$i] = [(0) x ($target + 1)] } \
22148             my @out; my $j = $target; \
22149             for (my $i = $n; $i > 0; $i--) { \
22150                my $v = $xs[$i - 1]; \
22151                if ($j >= $v && $dp[$i - 1][$j - $v] == 1) { \
22152                    unshift @out, $v; $j -= $v \
22153                } \
22154             }",
22155        );
22156    }
22157
22158    /// Binary search inside a `for` loop — LIS patience-sort variant
22159    /// using `tails` array. while-loop with `int(($lo+$hi)/2)`.
22160    #[test]
22161    fn binary_search_in_for_loop_parses() {
22162        let _g = NoInteropGuard::on();
22163        parse_ok(
22164            "my @xs = (3, 1, 4, 1, 5); my @tails; \
22165             for my $x (@xs) { \
22166                my $lo = 0; my $hi = len @tails; \
22167                while ($lo < $hi) { \
22168                    my $mid = int(($lo + $hi) / 2); \
22169                    if ($tails[$mid] < $x) { $lo = $mid + 1 } else { $hi = $mid } \
22170                } \
22171                if ($lo == len @tails) { push @tails, $x } else { $tails[$lo] = $x } \
22172             }",
22173        );
22174    }
22175
22176    /// Edge-relaxation loop with destructure + early-skip — Bellman-Ford
22177    /// shape. Tests `my ($u, $w, $cost) = @$e` inside a for-loop.
22178    #[test]
22179    fn edge_relaxation_destructure_parses() {
22180        let _g = NoInteropGuard::on();
22181        parse_ok(
22182            "my @edges = ([0, 1, 5], [1, 2, -3]); \
22183             my @dist = (0, 1e18, 1e18); my $INF = 1e18; \
22184             for my $e (@edges) { \
22185                my ($u, $w, $cost) = @$e; \
22186                next if $dist[$u] >= $INF; \
22187                $dist[$w] = $dist[$u] + $cost if $dist[$u] + $cost < $dist[$w] \
22188             }",
22189        );
22190    }
22191
22192    /// Bitwise `&` with negation: `$x & -$x` — Fenwick tree's
22193    /// lowest-set-bit isolation.
22194    #[test]
22195    fn bitwise_and_with_negation_parses() {
22196        let _g = NoInteropGuard::on();
22197        parse_ok("fn Fenwick::lsb($x) = $x & -$x");
22198        parse_ok("my $k = 12; my $lo_bit = $k & -$k");
22199    }
22200
22201    /// `@count[v] = old_v; running += c` — counting-sort cumulative
22202    /// transform. Tests serial assign-and-update inside a for loop.
22203    #[test]
22204    fn counting_sort_cumulative_loop_parses() {
22205        let _g = NoInteropGuard::on();
22206        parse_ok(
22207            "my @count = (3, 1, 2, 4); my $running = 0; \
22208             for my $v (0:3) { \
22209                my $c = $count[$v]; \
22210                $count[$v] = $running; \
22211                $running += $c \
22212             }",
22213        );
22214    }
22215
22216    /// Recursive tree walk with hashref nodes — Huffman tree traversal
22217    /// pattern. Validates that `defined $node` + `exists $node->{sym}`
22218    /// + child-recursion all parse cleanly.
22219    #[test]
22220    fn recursive_tree_walk_with_hashref_parses() {
22221        let _g = NoInteropGuard::on();
22222        parse_ok(
22223            "fn Huff::walk($node, $prefix, $codes) { \
22224                return unless defined $node; \
22225                if (exists $node->{sym}) { \
22226                    $codes->{$node->{sym}} = $prefix eq \"\" ? \"0\" : $prefix; \
22227                    return \
22228                } \
22229                Huff::walk($node->{left},  $prefix . \"0\", $codes); \
22230                Huff::walk($node->{right}, $prefix . \"1\", $codes) \
22231             }",
22232        );
22233    }
22234
22235    /// Z-array maintained-window arithmetic — three-way min via
22236    /// ternary, increment-while-match. Z-algorithm core.
22237    #[test]
22238    fn z_array_window_arithmetic_parses() {
22239        let _g = NoInteropGuard::on();
22240        parse_ok(
22241            "my @c = (\"a\", \"b\", \"a\"); my $n = 3; \
22242             my @z = (0) x $n; $z[0] = $n; \
22243             my $l = 0; my $r = 0; my $i = 1; \
22244             if ($i < $r) { \
22245                my $inside = $r - $i < $z[$i - $l] ? $r - $i : $z[$i - $l]; \
22246                $z[$i] = $inside \
22247             } \
22248             while ($i + $z[$i] < $n && $c[$z[$i]] eq $c[$i + $z[$i]]) { $z[$i]++ }",
22249        );
22250    }
22251
22252    /// BFS expansion with parent-linked hashref nodes — A* /
22253    /// general pathfinding shape. Inner `for` over neighbor offsets.
22254    #[test]
22255    fn bfs_with_parent_link_node_parses() {
22256        let _g = NoInteropGuard::on();
22257        parse_ok(
22258            "my @open = (+{ r => 0, c => 0, g => 0, parent => undef }); \
22259             while (len(@open) > 0) { \
22260                my $cur = shift @open; \
22261                for my $d ([-1, 0], [1, 0], [0, -1], [0, 1]) { \
22262                    my ($dr, $dc) = @$d; \
22263                    push @open, +{ \
22264                        r => $cur->{r} + $dr, c => $cur->{c} + $dc, \
22265                        g => $cur->{g} + 1, parent => $cur \
22266                    } \
22267                } \
22268                last \
22269             }",
22270        );
22271    }
22272
22273    /// Cross-product sort comparator with collinear tiebreak —
22274    /// Graham scan's polar sort.
22275    #[test]
22276    fn cross_product_sort_comparator_parses() {
22277        let _g = NoInteropGuard::on();
22278        parse_ok(
22279            "fn Hull::cross($p1, $p2, $p3) = \
22280                ($p2->[0] - $p1->[0]) * ($p3->[1] - $p1->[1]) - \
22281                ($p2->[1] - $p1->[1]) * ($p3->[0] - $p1->[0]); \
22282             my $pivot = [0, 0]; my @pts = ([1, 1], [2, 0]); \
22283             my @sorted = sort { \
22284                my $c = Hull::cross($pivot, _0, _1); \
22285                $c == 0 ? 0 : ($c < 0 ? 1 : -1) \
22286             } @pts",
22287        );
22288    }
22289
22290    /// Lomuto partition + recursive bisection — quickselect's loop.
22291    /// Inner loop with `$i++` + swap on each match.
22292    #[test]
22293    fn lomuto_partition_loop_parses() {
22294        let _g = NoInteropGuard::on();
22295        parse_ok(
22296            "fn QS::partition($arr, $lo, $hi) { \
22297                my $pivot = $arr->[$hi]; \
22298                my $i = $lo - 1; \
22299                for my $j ($lo:$hi - 1) { \
22300                    if ($arr->[$j] <= $pivot) { \
22301                        $i++; \
22302                        ($arr->[$i], $arr->[$j]) = ($arr->[$j], $arr->[$i]) \
22303                    } \
22304                } \
22305                ($arr->[$i + 1], $arr->[$hi]) = ($arr->[$hi], $arr->[$i + 1]); \
22306                $i + 1 \
22307             }",
22308        );
22309    }
22310
22311    /// `do { x } while (cond)` shape with diff-then-gcd — Pollard rho's
22312    /// tortoise-and-hare loop. Tests absolute-difference via ternary.
22313    #[test]
22314    fn tortoise_hare_diff_loop_parses() {
22315        let _g = NoInteropGuard::on();
22316        parse_ok(
22317            "my $x = 2; my $y = 2; my $n = 35; my $d = 1; \
22318             while ($d == 1) { \
22319                $x = ($x * $x + 1) % $n; \
22320                $y = ($y * $y + 1) % $n; \
22321                $y = ($y * $y + 1) % $n; \
22322                my $diff = $x > $y ? $x - $y : $y - $x; \
22323                $d = $diff \
22324             }",
22325        );
22326    }
22327
22328    /// Recursive ext_gcd returning a 3-tuple via arrayref destructure.
22329    /// Modular-inverse pattern.
22330    #[test]
22331    fn ext_gcd_recursive_destructure_parses() {
22332        let _g = NoInteropGuard::on();
22333        parse_ok(
22334            "fn Mod::ext_gcd($va, $vb) { \
22335                return [$va, 1, 0] if $vb == 0; \
22336                my $r = Mod::ext_gcd($vb, $va % $vb); \
22337                my ($g, $x1, $y1) = @$r; \
22338                [$g, $y1, $x1 - int($va / $vb) * $y1] \
22339             }",
22340        );
22341    }
22342
22343    /// Convolution-recurrence DP — Catalan number computation.
22344    /// Inner accumulator with mult on each iteration.
22345    #[test]
22346    fn convolution_recurrence_dp_parses() {
22347        let _g = NoInteropGuard::on();
22348        parse_ok(
22349            "my @c = (1); my $n = 5; \
22350             for my $i (1:$n) { \
22351                my $sum = 0; \
22352                for my $j (0:$i - 1) { $sum += $c[$j] * $c[$i - 1 - $j] } \
22353                push @c, $sum \
22354             }",
22355        );
22356    }
22357
22358    /// Tarjan SCC state: shared mutable hashref carries the entire
22359    /// algorithm's bookkeeping (`index`, `idx_of`, `low_of`,
22360    /// `on_stack`, `stack`, `sccs`) — passed by reference to the
22361    /// recursive worker.
22362    #[test]
22363    fn tarjan_scc_shared_state_parses() {
22364        let _g = NoInteropGuard::on();
22365        parse_ok(
22366            "fn SCC::strong_connect($s, $v) { \
22367                $s->{idx_of}{$v} = $s->{index}; \
22368                $s->{low_of}{$v} = $s->{index}; \
22369                $s->{index}++; \
22370                push @{$s->{stack}}, $v; \
22371                $s->{on_stack}{$v} = 1; \
22372                for my $w (@{$s->{adj}{$v}}) { \
22373                    if (!exists $s->{idx_of}{$w}) { \
22374                        SCC::strong_connect($s, $w); \
22375                        $s->{low_of}{$v} = $s->{low_of}{$w} if $s->{low_of}{$w} < $s->{low_of}{$v} \
22376                    } \
22377                } \
22378             }",
22379        );
22380    }
22381
22382    /// Heap's algorithm permutation generator — recursive in-place
22383    /// swap with parity-conditional pivot choice.
22384    #[test]
22385    fn heaps_algorithm_recursive_parses() {
22386        let _g = NoInteropGuard::on();
22387        parse_ok(
22388            "fn Perm::heaps_inner($arr, $n, $out) { \
22389                if ($n == 1) { my @snap = @$arr; push @$out, \\@snap; return } \
22390                for my $i (0:$n - 1) { \
22391                    Perm::heaps_inner($arr, $n - 1, $out); \
22392                    if ($n % 2 == 0) { \
22393                        ($arr->[$i], $arr->[$n - 1]) = ($arr->[$n - 1], $arr->[$i]) \
22394                    } else { \
22395                        ($arr->[0], $arr->[$n - 1]) = ($arr->[$n - 1], $arr->[0]) \
22396                    } \
22397                } \
22398             }",
22399        );
22400    }
22401
22402    /// `format` is a Perl FORMAT-declaration keyword. The lexer must
22403    /// NOT eat `format` when it appears as a hash key
22404    /// (`$h{format}`, `{format => ...}`), a method name
22405    /// (`$obj->format`), a namespaced tail (`Foo::format`), or a
22406    /// list/expr item with terminator follow-up. Previously
22407    /// `$opts{format} = "csv"` triggered "Expected '=' after format
22408    /// name" because the lexer greedily entered format-decl mode.
22409    #[test]
22410    fn format_as_hash_key_parses() {
22411        parse_ok("my %opts; $opts{format} = \"csv\"");
22412        parse_ok("my %opts = (format => \"csv\", level => 9)");
22413        parse_ok("my $h = +{ format => \"csv\" }");
22414        parse_ok("my @keys = ($h->{format}, $h->{level})");
22415    }
22416
22417    /// `format` after `->` is a method name, not the format keyword.
22418    #[test]
22419    fn format_as_method_call_parses() {
22420        parse_ok("class Foo { val: Str; fn format($self) { \"x\" } } my $f = Foo(val => \"y\"); my $s = $f->format()");
22421    }
22422
22423    /// `format` after `::` is a namespaced fn name tail.
22424    #[test]
22425    fn format_as_namespaced_tail_parses() {
22426        parse_ok("fn Foo::format($x) = $x . \"!\"");
22427        parse_ok("fn Foo::format($x) = $x . \"!\"; my $r = Foo::format(\"hi\")");
22428    }
22429
22430    /// Compound-assign on hash arrow-deref leaves the new value on the
22431    /// stack (uses `SetArrowHashKeep`, not `SetArrowHash`). Previously
22432    /// the no-keep variant left nothing for the statement-level Pop,
22433    /// which then ate a slot from the CALLER's stack frame — corrupting
22434    /// `dec($h) + dec($h) + dec($h)`-style multi-call expressions.
22435    /// See tests/suite/hashref_assignment_pin.rs for runtime pins.
22436    #[test]
22437    fn arrow_hash_compound_assign_parses_all_ops() {
22438        let _g = NoInteropGuard::on();
22439        parse_ok("my $h = +{n=>10}; $h->{n} -= 1");
22440        parse_ok("my $h = +{n=>10}; $h->{n} += 1");
22441        parse_ok("my $h = +{n=>10}; $h->{n} *= 2");
22442        parse_ok("my $h = +{n=>10}; $h->{n} /= 2");
22443        parse_ok("my $h = +{n=>10}; $h->{n} %= 3");
22444        parse_ok("my $h = +{n=>\"x\"}; $h->{n} .= \"y\"");
22445    }
22446
22447    /// The compound-assign yields the NEW value as its expression
22448    /// result, same as plain `$h->{k} = v` does. Validated at parse
22449    /// time by accepting the use-as-rvalue shape `my $v = $h->{n} -= 1`.
22450    #[test]
22451    fn arrow_hash_compound_assign_value_chains_parses() {
22452        let _g = NoInteropGuard::on();
22453        parse_ok("my $h = +{n=>10}; my $v = $h->{n} -= 1");
22454        parse_ok("my $h = +{n=>10}; my @list = ($h->{n} += 5, $h->{n} += 5)");
22455        parse_ok("my $h = +{n=>10}; my $double = ($h->{n} -= 1) * 2");
22456    }
22457}