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::{
5    is_frozen_decl, is_lexical_decl, is_lexical_or_our_decl, is_post_style_decl, Token,
6};
7use crate::vm_helper::VMHelper;
8
9/// True when `[` after `expr` is chained array access (`$r->{k}[0]`, `$a[1][2]`, `$$r[0]`).
10/// False for `(sort ...)[0]` / `@{ ... }[i]` — those slice a list value, not an array ref container.
11fn postfix_lbracket_is_arrow_container(expr: &Expr) -> bool {
12    matches!(
13        expr.kind,
14        ExprKind::ArrayElement { .. }
15            | ExprKind::HashElement { .. }
16            | ExprKind::ArrowDeref { .. }
17            | ExprKind::Deref {
18                kind: Sigil::Scalar,
19                ..
20            }
21    )
22}
23
24fn destructure_stmt_from_var_decls(keyword: &str, decls: Vec<VarDecl>, line: usize) -> Statement {
25    let kind = match keyword {
26        "my" => StmtKind::My(decls),
27        "mysync" => StmtKind::MySync(decls),
28        "our" => StmtKind::Our(decls),
29        "oursync" => StmtKind::OurSync(decls),
30        "local" => StmtKind::Local(decls),
31        "state" => StmtKind::State(decls),
32        _ => unreachable!("parse_my_our_local keyword"),
33    };
34    Statement {
35        label: None,
36        kind,
37        line,
38    }
39}
40
41fn destructure_stmt_die_string(line: usize, msg: &str) -> Statement {
42    Statement {
43        label: None,
44        kind: StmtKind::Expression(Expr {
45            kind: ExprKind::Die(vec![Expr {
46                kind: ExprKind::String(msg.to_string()),
47                line,
48            }]),
49            line,
50        }),
51        line,
52    }
53}
54
55fn destructure_stmt_unless_die(line: usize, cond: Expr, msg: &str) -> Statement {
56    Statement {
57        label: None,
58        kind: StmtKind::Unless {
59            condition: cond,
60            body: vec![destructure_stmt_die_string(line, msg)],
61            else_block: None,
62        },
63        line,
64    }
65}
66
67fn destructure_expr_scalar_tmp(name: &str, line: usize) -> Expr {
68    Expr {
69        kind: ExprKind::ScalarVar(name.to_string()),
70        line,
71    }
72}
73
74fn destructure_expr_array_len(tmp: &str, line: usize) -> Expr {
75    Expr {
76        kind: ExprKind::Deref {
77            expr: Box::new(destructure_expr_scalar_tmp(tmp, line)),
78            kind: Sigil::Array,
79        },
80        line,
81    }
82}
83/// `Parser` — see fields for layout.
84pub struct Parser {
85    /// `tokens` field.
86    tokens: Vec<(Token, usize)>,
87    /// `pos` field.
88    pos: usize,
89    /// Monotonic slot id for `rate_limit(...)` sliding-window state in the interpreter.
90    next_rate_limit_slot: u32,
91    /// When > 0, `expr` `(` is not parsed as [`ExprKind::IndirectCall`] — e.g. `sort $k (1)` must
92    /// treat `(1)` as the sort list, not `$k(1)`.
93    suppress_indirect_paren_call: u32,
94    /// When > 0, the current expression is being parsed as the RHS of `|>`
95    /// (pipe-forward). Builtins that normally require a list/string/second arg
96    /// (`map`, `grep`, `sort`, `join`, `reverse` / `reversed`, `split`, …) may accept a
97    /// placeholder when this flag is set, because [`Self::pipe_forward_apply`]
98    /// will substitute the piped value in afterwards.
99    pipe_rhs_depth: u32,
100    /// When > 0 we are parsing inside a `{ … }` block (function body, `map`/`grep`,
101    /// `for`, `if`, anonymous coderef, etc.). Inside any block, bare `_` is a topic
102    /// reference (`$_[0]`/`$_`), so `my $i = _` means "capture the topic" and must
103    /// NOT be auto-wrapped as an implicit zero-arg coderef. Only at the true top
104    /// level (depth 0 — module scope) is `_` unbound, allowing `my $f = _ * 2` to
105    /// parse as `my $f = fn { _ * 2 }`. Bumped in [`Self::parse_block`].
106    block_depth: u32,
107    /// When > 0, [`Self::parse_pipe_forward`] will **not** consume a trailing `|>`
108    /// and leaves it for an outer parser instead. Bumped while parsing paren-less
109    /// arg lists (`parse_list_until_terminator`, paren-less method args, `map`/`grep`
110    /// LIST, …) so `@a |> head 2 |> join "-"` chains left-associatively as
111    /// `(@a |> head 2) |> join "-"` instead of `head` swallowing the outer `|>`
112    /// as part of its first arg. Reset to 0 on entry to any parenthesized
113    /// arg list (`parse_arg_list`) so `head(2 |> foo, 3)` still works.
114    no_pipe_forward_depth: u32,
115    /// When > 0, `{` after a scalar / scalar deref is not `%hash{key}` / `->{}`, so
116    /// `if let` / `while let` scrutinees can be followed by `{ ... }`.
117    suppress_scalar_hash_brace: u32,
118    /// Counter for `while let` / similar desugar temps (`$__while_let_0`, …).
119    next_desugar_tmp: u32,
120    /// Source path for [`StrykeError`] (matches lexer / `parse_with_file`).
121    error_file: String,
122    /// User-declared sub names (for allowing UDF to shadow stryke extensions in compat mode).
123    declared_subs: std::collections::HashSet<String>,
124    /// When > 0, `parse_named_expr` will not consume following barewords as paren-less
125    /// function arguments. Used by thread macro to prevent `t Color::Red p` from
126    /// interpreting `p` as an argument to the enum constructor instead of a stage.
127    suppress_parenless_call: u32,
128    /// Pre-built input expression for the next `parse_thread_macro_inner`
129    /// call. Used by `~p>` continuation parsing (`||>` / `|then|`) to
130    /// thread the par_reduce result into a normal `~>` continuation
131    /// without re-parsing a source expression.
132    pending_thread_input: Option<Expr>,
133    /// When > 0, `parse_multiplication` will not consume `Token::Slash` as division.
134    /// Used by thread macro so `/pattern/` is left for the stage parser to handle.
135    suppress_slash_as_div: u32,
136    /// When > 0, the lexer should not interpret `m/`, `s/`, etc. as regex-starters.
137    /// Used by thread macro to prevent `/m/` from being misparsed.
138    pub suppress_m_regex: u32,
139    /// When > 0, `parse_range` will not consume `:` as the short-form range operator.
140    /// Bumped while parsing the then-branch of a ternary `? :` so `a ? b : c` doesn't
141    /// misparse `b : c` as a range.
142    suppress_colon_range: u32,
143    /// Counter (depth-tracked like [`Self::suppress_colon_range`]) that
144    /// disables `~` as a range separator. Used inside paired `~...~` char-
145    /// index/slice subscripts so the closing `~` doesn't get eaten as a
146    /// range op. `:` range is still allowed inside (e.g. `$_~1:3~` is a
147    /// slice with a `:` range as the index).
148    suppress_tilde_range: u32,
149    /// When true, `pipe_forward_apply` uses thread-last semantics (append to args)
150    /// instead of thread-first (prepend). Set by `->>` thread macro.
151    thread_last_mode: bool,
152    /// When true, we're parsing a module (via `use`/`require`), not user code.
153    /// Modules are allowed to shadow builtins; user code is not (unless `--compat`).
154    pub parsing_module: bool,
155    /// `self.pos` immediately after consuming a paren-list close (`(EXPR)`,
156    /// `(EXPR, …)`, `()`) or `qw(…)` in `parse_primary`. The `x` operator
157    /// reads this at parse time to distinguish `(LIST) x N` (list repetition)
158    /// from `EXPR x N` (scalar string repetition). The compare is exact: any
159    /// postfix consumption (`->method()`, `[idx]`, …) advances `self.pos`
160    /// past this checkpoint, so list-repeat fires only when `x` is the very
161    /// next token after the closing paren.
162    list_construct_close_pos: Option<usize>,
163    /// Synthetic SubDecl statements queued by anonymous-sub overload handlers
164    /// (`use overload "+" => sub { ... }`) — drained at the end of
165    /// [`Self::parse_program`] and prepended to the top-level statements so
166    /// the package-qualified synthetic name resolves at runtime. (PARITY-012)
167    pending_synthetic_subs: Vec<Statement>,
168    /// Counter for unique anonymous-overload-handler names.
169    next_overload_anon_id: u32,
170    /// Token-vector indices where the lexer emitted a *bare* positional alias
171    /// (`_`, `_0`, `_1`, …) — i.e. without a leading `$` sigil. Populated by
172    /// [`crate::lexer::Lexer::tokenize`]. Consulted by [`Self::parse_my_our_local`]
173    /// to auto-wrap an RHS expression that contains free positional aliases
174    /// into an implicit zero-arg coderef, so `my $f = _ * 2` ≡
175    /// `my $f = fn { _ * 2 }`.
176    pub bare_positional_indices: std::collections::HashSet<usize>,
177    /// Current package context — updated by `parse_package`. Defaults to
178    /// `"main"`. Used by [`Self::check_udf_shadows_builtin`] to allow
179    /// `fn name(...)` inside `package Foo` to shadow stryke builtins
180    /// (the bare `name` becomes `Foo::name`, so the builtin remains
181    /// reachable via the unqualified call from outside the package).
182    current_package: String,
183}
184
185impl Parser {
186    /// `new` — see implementation.
187    pub fn new(tokens: Vec<(Token, usize)>) -> Self {
188        Self::new_with_file(tokens, "-e")
189    }
190    /// `new_with_file` — see implementation.
191    pub fn new_with_file(tokens: Vec<(Token, usize)>, file: impl Into<String>) -> Self {
192        Self {
193            tokens,
194            pos: 0,
195            next_rate_limit_slot: 0,
196            suppress_indirect_paren_call: 0,
197            pipe_rhs_depth: 0,
198            no_pipe_forward_depth: 0,
199            suppress_scalar_hash_brace: 0,
200            next_desugar_tmp: 0,
201            error_file: file.into(),
202            declared_subs: std::collections::HashSet::new(),
203            suppress_parenless_call: 0,
204            pending_thread_input: None,
205            suppress_slash_as_div: 0,
206            suppress_m_regex: 0,
207            suppress_colon_range: 0,
208            suppress_tilde_range: 0,
209            thread_last_mode: false,
210            pending_synthetic_subs: Vec::new(),
211            next_overload_anon_id: 0,
212            parsing_module: false,
213            list_construct_close_pos: None,
214            bare_positional_indices: std::collections::HashSet::new(),
215            block_depth: 0,
216            current_package: "main".to_string(),
217        }
218    }
219
220    fn alloc_desugar_tmp(&mut self) -> u32 {
221        let n = self.next_desugar_tmp;
222        self.next_desugar_tmp = self.next_desugar_tmp.saturating_add(1);
223        n
224    }
225
226    /// True when we are currently parsing the RHS of a `|>` pipe-forward.
227    /// Used by builtins (`map`, `grep`, `sort`, `join`, …) to supply a
228    /// placeholder list instead of erroring on a missing operand.
229    #[inline]
230    fn in_pipe_rhs(&self) -> bool {
231        self.pipe_rhs_depth > 0
232    }
233
234    /// List-slurping builtin: the operand is entirely the LHS of `|>` (no following list tokens).
235    /// A newline after the builtin name also terminates the pipe stage (implicit semicolon).
236    fn pipe_supplies_slurped_list_operand(&self) -> bool {
237        self.in_pipe_rhs()
238            && (matches!(
239                self.peek(),
240                Token::Semicolon
241                    | Token::RBrace
242                    | Token::RParen
243                    | Token::Eof
244                    | Token::Comma
245                    | Token::PipeForward
246            ) || self.peek_line() > self.prev_line())
247    }
248
249    /// Empty placeholder list used as a stand-in for the list operand of
250    /// list-taking builtins when they appear on the RHS of `|>`.
251    /// [`Self::pipe_forward_apply`] rewrites this slot with the actual piped
252    /// value at desugar time, so the placeholder is never evaluated.
253    #[inline]
254    fn pipe_placeholder_list(&self, line: usize) -> Expr {
255        Expr {
256            kind: ExprKind::List(vec![]),
257            line,
258        }
259    }
260
261    /// List builtins that take `{ BLOCK }, LIST` and accept the threaded list at
262    /// `args[1]` via [`Self::pipe_forward_apply`]. Used by both the pipe-forward
263    /// dispatcher and `parse_thread_stage_with_block` so `~> @a NAME { ... }` and
264    /// `@a |> NAME { ... }` route through the same substitution.
265    fn is_block_then_list_pipe_builtin(name: &str) -> bool {
266        matches!(
267            name,
268            "pfirst"
269                | "pany"
270                | "any"
271                | "all"
272                | "none"
273                | "first"
274                | "find_index"
275                | "firstidx"
276                | "first_index"
277                | "take_while"
278                | "drop_while"
279                | "skip_while"
280                | "reject"
281                | "grepv"
282                | "tap"
283                | "peek"
284                | "group_by"
285                | "chunk_by"
286                | "partition"
287                | "min_by"
288                | "max_by"
289                | "zip_with"
290                | "count_by"
291        )
292    }
293
294    /// Lift a `Bareword("f")` to `FuncCall { f, [$_] }`.
295    ///
296    /// stryke extension contexts (map/grep/fore expression forms, pipe-forward)
297    /// call this so that `map sha512, @list` invokes `sha512($_)` for each
298    /// element instead of stringifying the bareword.  Non-bareword expressions
299    /// pass through unchanged.
300    ///
301    /// Also injects `$_` into known builtins that were parsed with zero
302    /// arguments (e.g. `fore unlink`, `map stat`) so they operate on the
303    /// topic variable instead of being no-ops.
304    fn lift_bareword_to_topic_call(expr: Expr) -> Expr {
305        let line = expr.line;
306        let topic = || Expr {
307            kind: ExprKind::ScalarVar("_".into()),
308            line,
309        };
310        match expr.kind {
311            ExprKind::Bareword(ref name) => Expr {
312                kind: ExprKind::FuncCall {
313                    name: name.clone(),
314                    args: vec![topic()],
315                },
316                line,
317            },
318            // Builtins that take Vec<Expr> args — inject $_ when empty.
319            ExprKind::Unlink(ref args) if args.is_empty() => Expr {
320                kind: ExprKind::Unlink(vec![topic()]),
321                line,
322            },
323            ExprKind::Chmod(ref args) if args.is_empty() => Expr {
324                kind: ExprKind::Chmod(vec![topic()]),
325                line,
326            },
327            // Builtins that take Box<Expr> — inject $_ when arg is implicit.
328            ExprKind::Stat(_) => expr,
329            ExprKind::Lstat(_) => expr,
330            ExprKind::Readlink(_) => expr,
331            // rev with empty list should use $_
332            ExprKind::Rev(ref inner) => {
333                if matches!(inner.kind, ExprKind::List(ref v) if v.is_empty()) {
334                    Expr {
335                        kind: ExprKind::Rev(Box::new(topic())),
336                        line,
337                    }
338                } else {
339                    expr
340                }
341            }
342            _ => expr,
343        }
344    }
345
346    /// `parse_assign_expr` with `no_pipe_forward_depth` bumped for the
347    /// duration, so any trailing `|>` is left to the enclosing parser instead
348    /// of being absorbed into this sub-expression. Used by paren-less arg
349    /// parsers (`parse_list_until_terminator`, `chunked`/`windowed` paren-less,
350    /// paren-less method args, …) so `@a |> head 2 |> join "-"` chains
351    /// left-associatively instead of letting `head`'s first arg swallow the
352    /// outer `|>`. The counter is restored on both success and error paths.
353    fn parse_assign_expr_stop_at_pipe(&mut self) -> StrykeResult<Expr> {
354        self.no_pipe_forward_depth = self.no_pipe_forward_depth.saturating_add(1);
355        let r = self.parse_assign_expr();
356        self.no_pipe_forward_depth = self.no_pipe_forward_depth.saturating_sub(1);
357        r
358    }
359
360    fn syntax_err(&self, message: impl Into<String>, line: usize) -> StrykeError {
361        StrykeError::new(ErrorKind::Syntax, message, line, self.error_file.clone())
362    }
363
364    /// Coderef-in-block-position helper for tier-2 list builtins (`any`,
365    /// `all`, `none`, `first`, `take_while`, …). Returns `Some([f, list])`
366    /// when the next tokens look like `$f [,] LIST` (or `$f` alone in
367    /// pipe-RHS); `None` when the caller should fall through to the block
368    /// form. The first arg is any coderef-shaped expression — runtime
369    /// checks `as_code_ref()` and dispatches.
370    fn try_parse_coderef_listop_args(&mut self, line: usize) -> StrykeResult<Option<Vec<Expr>>> {
371        if !matches!(self.peek(), Token::ScalarVar(_) | Token::Backslash) {
372            return Ok(None);
373        }
374        let f = self.parse_assign_expr_stop_at_pipe()?;
375        let _ = self.eat(&Token::Comma);
376        let list = if self.in_pipe_rhs()
377            && matches!(
378                self.peek(),
379                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
380            ) {
381            self.pipe_placeholder_list(line)
382        } else {
383            self.parse_expression()?
384        };
385        Ok(Some(vec![f, list]))
386    }
387
388    fn alloc_rate_limit_slot(&mut self) -> u32 {
389        let s = self.next_rate_limit_slot;
390        self.next_rate_limit_slot = self.next_rate_limit_slot.saturating_add(1);
391        s
392    }
393
394    fn peek(&self) -> &Token {
395        self.tokens
396            .get(self.pos)
397            .map(|(t, _)| t)
398            .unwrap_or(&Token::Eof)
399    }
400
401    fn peek_line(&self) -> usize {
402        self.tokens.get(self.pos).map(|(_, l)| *l).unwrap_or(0)
403    }
404
405    fn peek_at(&self, offset: usize) -> &Token {
406        self.tokens
407            .get(self.pos + offset)
408            .map(|(t, _)| t)
409            .unwrap_or(&Token::Eof)
410    }
411
412    fn advance(&mut self) -> (Token, usize) {
413        let tok = self
414            .tokens
415            .get(self.pos)
416            .cloned()
417            .unwrap_or((Token::Eof, 0));
418        self.pos += 1;
419        tok
420    }
421
422    /// Line number of the most recently consumed token (the token at `pos - 1`).
423    fn prev_line(&self) -> usize {
424        if self.pos > 0 {
425            self.tokens.get(self.pos - 1).map(|(_, l)| *l).unwrap_or(0)
426        } else {
427            0
428        }
429    }
430
431    /// Check if `{ ... }` starting at current position looks like a hashref rather than a block.
432    /// Heuristics (assuming current token is `{`):
433    /// - `{ bareword =>` → hashref
434    /// - `{ "string" =>` → hashref
435    /// - `{ $var =>` → hashref
436    /// - `{ 0 =>` → hashref (numeric key)
437    /// - `{ %hash }` or `{ %hash, ...}` → hashref (spread)
438    /// - `{ }` (empty) → hashref
439    fn looks_like_hashref(&self) -> bool {
440        debug_assert!(matches!(self.peek(), Token::LBrace));
441        let tok1 = self.peek_at(1);
442        let tok2 = self.peek_at(2);
443        match tok1 {
444            Token::RBrace => true,
445            Token::Ident(_)
446            | Token::SingleString(_)
447            | Token::DoubleString(_)
448            | Token::ScalarVar(_)
449            | Token::Integer(_) => matches!(tok2, Token::FatArrow),
450            Token::HashVar(_) => matches!(tok2, Token::RBrace | Token::Comma),
451            _ => false,
452        }
453    }
454
455    fn expect(&mut self, expected: &Token) -> StrykeResult<usize> {
456        let (tok, line) = self.advance();
457        if std::mem::discriminant(&tok) == std::mem::discriminant(expected) {
458            Ok(line)
459        } else {
460            Err(self.syntax_err(format!("Expected {:?}, got {:?}", expected, tok), line))
461        }
462    }
463
464    fn eat(&mut self, expected: &Token) -> bool {
465        if std::mem::discriminant(self.peek()) == std::mem::discriminant(expected) {
466            self.advance();
467            true
468        } else {
469            false
470        }
471    }
472
473    fn at_eof(&self) -> bool {
474        matches!(self.peek(), Token::Eof)
475    }
476
477    /// True when a file test (`-d`, `-f`, …) may omit its operand and use `$_` (Perl filetest default).
478    fn filetest_allows_implicit_topic(tok: &Token) -> bool {
479        matches!(
480            tok,
481            Token::RParen
482                | Token::Semicolon
483                | Token::Comma
484                | Token::RBrace
485                | Token::Eof
486                | Token::LogAnd
487                | Token::LogOr
488                | Token::LogAndWord
489                | Token::LogOrWord
490                | Token::PipeForward
491        )
492    }
493
494    /// True when the next token is a statement-starting keyword on a *different*
495    /// line from `stmt_line`.  Used by `parse_use` / `parse_no` to stop parsing
496    /// import lists when semicolons are omitted (stryke extension).
497    fn next_is_new_stmt_keyword(&self, stmt_line: usize) -> bool {
498        // Semicolons-optional is a stryke extension; in compat mode, require them.
499        if crate::compat_mode() {
500            return false;
501        }
502        if self.peek_line() == stmt_line {
503            return false;
504        }
505        matches!(
506            self.peek(),
507            Token::Ident(ref kw) if matches!(kw.as_str(),
508                "use" | "import" | "no" | "my" | "our" | "local" | "sub" | "struct" | "enum"
509                | "if" | "unless" | "while" | "until" | "for" | "foreach" | "loop" | "ploop"
510                | "pwhile" | "return" | "last" | "next" | "redo" | "package" | "require"
511                | "BEGIN" | "END" | "UNITCHECK" | "frozen" | "const" | "typed"
512                // stryke-specific declaration keywords that start a new
513                // statement on a fresh line. Without these, a bare `use
514                // strict` / `use warnings` followed by `fn foo { ... }`
515                // on the next line swallows `foo` as an import argument.
516                | "fn" | "class" | "abstract" | "final" | "trait"
517                | "state" | "mysync" | "varsync" | "oursync" | "var" | "val"
518            )
519        )
520    }
521
522    /// True when the next token is on a different line from `stmt_line` and could
523    /// start a new statement. More permissive than `next_is_new_stmt_keyword` —
524    /// includes sigil-prefixed variables like `$var`, `@arr`, `%hash`.
525    fn next_is_new_statement_start(&self, stmt_line: usize) -> bool {
526        if crate::compat_mode() {
527            return false;
528        }
529        if self.peek_line() == stmt_line {
530            return false;
531        }
532        matches!(
533            self.peek(),
534            Token::ScalarVar(_)
535                | Token::DerefScalarVar(_)
536                | Token::ArrayVar(_)
537                | Token::HashVar(_)
538                | Token::LBrace
539        ) || self.next_is_new_stmt_keyword(stmt_line)
540    }
541
542    // ── Top level ──
543    /// `parse_program` — see implementation.
544    pub fn parse_program(&mut self) -> StrykeResult<Program> {
545        let mut statements = self.parse_statements()?;
546        // Prepend any synthetic SubDecl stubs queued by anonymous overload
547        // handlers so the package-qualified synthetic names resolve when the
548        // overload table is consulted at runtime. (PARITY-012)
549        if !self.pending_synthetic_subs.is_empty() {
550            let synthetics = std::mem::take(&mut self.pending_synthetic_subs);
551            let mut combined = Vec::with_capacity(synthetics.len() + statements.len());
552            combined.extend(synthetics);
553            combined.append(&mut statements);
554            statements = combined;
555        }
556        Ok(Program { statements })
557    }
558
559    /// Parse statements until EOF. Used by parse_program and parse_block_from_str.
560    pub fn parse_statements(&mut self) -> StrykeResult<Vec<Statement>> {
561        let mut statements = Vec::new();
562        while !self.at_eof() {
563            if matches!(self.peek(), Token::Semicolon) {
564                let line = self.peek_line();
565                self.advance();
566                statements.push(Statement {
567                    label: None,
568                    kind: StmtKind::Empty,
569                    line,
570                });
571                continue;
572            }
573            statements.push(self.parse_statement()?);
574        }
575        Ok(statements)
576    }
577
578    // ── Statements ──
579
580    fn parse_statement(&mut self) -> StrykeResult<Statement> {
581        let line = self.peek_line();
582
583        // Statement label `FOO:` / `boot:` / `BAR_BAZ:` (not `Foo::` — that is `Ident` + `::`).
584        // Uppercase-only was too strict: XSLoader.pm uses `boot:` before `my $xs = ...`.
585        let label = match self.peek().clone() {
586            Token::Ident(_) => {
587                if matches!(self.peek_at(1), Token::Colon)
588                    && !matches!(self.peek_at(2), Token::Colon)
589                {
590                    let (tok, _) = self.advance();
591                    let l = match tok {
592                        Token::Ident(l) => l,
593                        _ => unreachable!(),
594                    };
595                    self.advance(); // ':'
596                    Some(l)
597                } else {
598                    None
599                }
600            }
601            _ => None,
602        };
603
604        let mut stmt = match self.peek().clone() {
605            Token::FormatDecl { .. } => {
606                let tok_line = self.peek_line();
607                let (tok, _) = self.advance();
608                match tok {
609                    Token::FormatDecl { name, lines } => Statement {
610                        label: label.clone(),
611                        kind: StmtKind::FormatDecl { name, lines },
612                        line: tok_line,
613                    },
614                    _ => unreachable!(),
615                }
616            }
617            Token::Ident(ref kw) => match kw.as_str() {
618                "if" => self.parse_if()?,
619                "unless" => self.parse_unless()?,
620                "while" => {
621                    let mut s = self.parse_while()?;
622                    if let StmtKind::While {
623                        label: ref mut lbl, ..
624                    } = s.kind
625                    {
626                        *lbl = label.clone();
627                    }
628                    s
629                }
630                // `loop { ... }` — Rust-style infinite loop, exactly
631                // equivalent to `while (1) { ... }`. Desugars to a
632                // `While` stmt with a synthesized `1` condition so
633                // `last` / `next` / labels / continue blocks all
634                // share the existing while-loop runtime.
635                "loop" => {
636                    let mut s = self.parse_loop()?;
637                    if let StmtKind::While {
638                        label: ref mut lbl, ..
639                    } = s.kind
640                    {
641                        *lbl = label.clone();
642                    }
643                    s
644                }
645                // `ploop [N] { ... }` / `pwhile [N] (COND) { ... }` —
646                // parallel `loop` / `while` (stryke extensions; in compat
647                // mode they stay ordinary sub names).
648                "ploop" if !crate::compat_mode() => self.parse_ploop()?,
649                "pwhile" if !crate::compat_mode() => self.parse_pwhile()?,
650                "until" => {
651                    let mut s = self.parse_until()?;
652                    if let StmtKind::Until {
653                        label: ref mut lbl, ..
654                    } = s.kind
655                    {
656                        *lbl = label.clone();
657                    }
658                    s
659                }
660                "for" => {
661                    let mut s = self.parse_for_or_foreach()?;
662                    match s.kind {
663                        StmtKind::For {
664                            label: ref mut lbl, ..
665                        }
666                        | StmtKind::Foreach {
667                            label: ref mut lbl, ..
668                        } => *lbl = label.clone(),
669                        _ => {}
670                    }
671                    s
672                }
673                "foreach" => {
674                    let mut s = self.parse_foreach()?;
675                    if let StmtKind::Foreach {
676                        label: ref mut lbl, ..
677                    } = s.kind
678                    {
679                        *lbl = label.clone();
680                    }
681                    s
682                }
683                "sub" => {
684                    if crate::no_interop_mode() {
685                        return Err(self.syntax_err(
686                            "stryke uses `fn` instead of `sub` (--no-interop is active)",
687                            self.peek_line(),
688                        ));
689                    }
690                    self.parse_sub_decl(true)?
691                }
692                "fn" => self.parse_sub_decl(false)?,
693                "struct" => {
694                    if crate::compat_mode() {
695                        return Err(self.syntax_err(
696                            "`struct` is a stryke extension (disabled by --compat)",
697                            self.peek_line(),
698                        ));
699                    }
700                    self.parse_struct_decl()?
701                }
702                "enum" => {
703                    if crate::compat_mode() {
704                        return Err(self.syntax_err(
705                            "`enum` is a stryke extension (disabled by --compat)",
706                            self.peek_line(),
707                        ));
708                    }
709                    self.parse_enum_decl()?
710                }
711                "class" => {
712                    if crate::compat_mode() {
713                        // TODO: parse Perl 5.38 class syntax with :isa()
714                        return Err(self.syntax_err(
715                            "Perl 5.38 `class` syntax not yet implemented in --compat mode",
716                            self.peek_line(),
717                        ));
718                    }
719                    self.parse_class_decl(false, false)?
720                }
721                "abstract" => {
722                    self.advance(); // abstract
723                    if !matches!(self.peek(), Token::Ident(ref s) if s == "class") {
724                        return Err(self.syntax_err(
725                            "`abstract` must be followed by `class`",
726                            self.peek_line(),
727                        ));
728                    }
729                    self.parse_class_decl(true, false)?
730                }
731                "final" => {
732                    self.advance(); // final
733                    if !matches!(self.peek(), Token::Ident(ref s) if s == "class") {
734                        return Err(self
735                            .syntax_err("`final` must be followed by `class`", self.peek_line()));
736                    }
737                    self.parse_class_decl(false, true)?
738                }
739                "trait" => {
740                    if crate::compat_mode() {
741                        return Err(self.syntax_err(
742                            "`trait` is a stryke extension (disabled by --compat)",
743                            self.peek_line(),
744                        ));
745                    }
746                    self.parse_trait_decl()?
747                }
748                "my" => self.parse_my_our_local("my", false)?,
749                // `var $x = …` / `val $x = …` — Kotlin/Scala-style aliases for
750                // `my` / `const my`. Both are common user identifiers (struct
751                // field names, hash keys, around-advice targets, user-defined
752                // sub names like `fn val { 1 }`). Match-guard on the lookahead
753                // so they ONLY dispatch as declarators when the next token is
754                // a sigil binding (`$x` / `@x` / `%x` / `*x`), an LParen for
755                // list destructuring (`var ($a, $b) = …`), or a `typed`
756                // modifier. Every other shape (`val()` / `val,` / `val =>` /
757                // `val { … }`) falls through to the default arm so the bare
758                // identifier is parsed as an expression statement.
759                "var" if matches!(
760                    self.peek_at(1),
761                    Token::ScalarVar(_)
762                        | Token::ArrayVar(_)
763                        | Token::HashVar(_)
764                        | Token::Star
765                        | Token::LParen
766                ) || matches!(
767                    self.peek_at(1),
768                    Token::Ident(ref k) if k == "typed"
769                ) =>
770                {
771                    if crate::compat_mode() {
772                        return Err(self.syntax_err(
773                            "`var` is a stryke extension (disabled by --compat)",
774                            self.peek_line(),
775                        ));
776                    }
777                    self.parse_my_our_local("my", false)?
778                }
779                "val" if matches!(
780                    self.peek_at(1),
781                    Token::ScalarVar(_)
782                        | Token::ArrayVar(_)
783                        | Token::HashVar(_)
784                        | Token::Star
785                        | Token::LParen
786                ) || matches!(
787                    self.peek_at(1),
788                    Token::Ident(ref k) if k == "typed"
789                ) =>
790                {
791                    if crate::compat_mode() {
792                        return Err(self.syntax_err(
793                            "`val` is a stryke extension (disabled by --compat)",
794                            self.peek_line(),
795                        ));
796                    }
797                    let mut stmt = self.parse_my_our_local("my", true)?;
798                    if let StmtKind::My(ref mut decls) = stmt.kind {
799                        for decl in decls.iter_mut() {
800                            decl.frozen = true;
801                        }
802                    }
803                    stmt
804                }
805                "state" => self.parse_my_our_local("state", false)?,
806                "mysync" => {
807                    if crate::compat_mode() {
808                        return Err(self.syntax_err(
809                            "`mysync` is a stryke extension (disabled by --compat)",
810                            self.peek_line(),
811                        ));
812                    }
813                    self.parse_my_our_local("mysync", false)?
814                }
815                // `varsync $x = …` — Kotlin-style alias for `mysync`,
816                // same way `var` aliases `my`. Parses identically to
817                // `mysync` (StmtKind::MySync), so all downstream
818                // bytecode + reflection treats it as the same binding.
819                "varsync" => {
820                    if crate::compat_mode() {
821                        return Err(self.syntax_err(
822                            "`varsync` is a stryke extension (disabled by --compat)",
823                            self.peek_line(),
824                        ));
825                    }
826                    self.parse_my_our_local("mysync", false)?
827                }
828                "oursync" => {
829                    if crate::compat_mode() {
830                        return Err(self.syntax_err(
831                            "`oursync` is a stryke extension (disabled by --compat)",
832                            self.peek_line(),
833                        ));
834                    }
835                    self.parse_my_our_local("oursync", false)?
836                }
837                "frozen" | "const" => {
838                    let leading = kw.as_str().to_string();
839                    if crate::compat_mode() {
840                        return Err(self.syntax_err(
841                            format!("`{leading}` is a stryke extension (disabled by --compat)"),
842                            self.peek_line(),
843                        ));
844                    }
845                    // `frozen my $x = val;` / `const my $x = val;` — the two
846                    // spellings are interchangeable (`const` is the
847                    // more-familiar name for new users). Also accept
848                    // `frozen var` / `const var` / `frozen val` / `const
849                    // val` so the Kotlin/Scala-style aliases compose with
850                    // the modifier the same way `my` does. `val` already
851                    // means frozen on its own — pairing it with
852                    // `const`/`frozen` is redundant but idempotent (the
853                    // post-parse loop below sets `decl.frozen = true`,
854                    // which `val`'s own arm would have set too).
855                    self.advance(); // consume "frozen"/"const"
856                    if let Token::Ident(ref kw) = self.peek().clone() {
857                        if is_lexical_decl(kw) {
858                            // Accept type annotations the same way `typed
859                            // my $x : Int` does — `const`/`frozen` is
860                            // orthogonal to typing, and `: Type` after a
861                            // name is unambiguous in either form.
862                            let mut stmt = self.parse_my_our_local("my", true)?;
863                            if let StmtKind::My(ref mut decls) = stmt.kind {
864                                for decl in decls.iter_mut() {
865                                    decl.frozen = true;
866                                }
867                            }
868                            stmt
869                        } else {
870                            return Err(self.syntax_err(
871                                format!("Expected 'my' / 'var' / 'val' after '{leading}'"),
872                                self.peek_line(),
873                            ));
874                        }
875                    } else {
876                        return Err(self.syntax_err(
877                            format!("Expected 'my' / 'var' / 'val' after '{leading}'"),
878                            self.peek_line(),
879                        ));
880                    }
881                }
882                "typed" => {
883                    if crate::compat_mode() {
884                        return Err(self.syntax_err(
885                            "`typed` is a stryke extension (disabled by --compat)",
886                            self.peek_line(),
887                        ));
888                    }
889                    self.advance();
890                    if let Token::Ident(ref kw) = self.peek().clone() {
891                        // `typed my $x : Int = …` is the canonical form;
892                        // `typed var` / `typed val` accept the same
893                        // alias-keyword set as the `frozen`/`const` arm
894                        // above. For `typed val`, the original `val` arm
895                        // would have set `decl.frozen = true`; preserve
896                        // that here so the modifier composition matches
897                        // (`typed val $x : Int = …` ≡ `const typed my $x : Int = …`).
898                        let raw_kw = kw.clone();
899                        if is_lexical_decl(&raw_kw) {
900                            let mut stmt = self.parse_my_our_local("my", true)?;
901                            if is_frozen_decl(&raw_kw) {
902                                if let StmtKind::My(ref mut decls) = stmt.kind {
903                                    for decl in decls.iter_mut() {
904                                        decl.frozen = true;
905                                    }
906                                }
907                            }
908                            stmt
909                        } else {
910                            return Err(self.syntax_err(
911                                "Expected 'my' / 'var' / 'val' after 'typed'",
912                                self.peek_line(),
913                            ));
914                        }
915                    } else {
916                        return Err(self.syntax_err(
917                            "Expected 'my' / 'var' / 'val' after 'typed'",
918                            self.peek_line(),
919                        ));
920                    }
921                }
922                "our" => self.parse_my_our_local("our", false)?,
923                "local" => self.parse_my_our_local("local", false)?,
924                "package" => self.parse_package()?,
925                // `import` is a syntactic alias for `use` — same parse,
926                // same AST node. Lets `import Foo qw(bar);` work for
927                // users coming from Python/JS spellings.
928                "use" | "import" => self.parse_use()?,
929                "no" => self.parse_no()?,
930                "return" => self.parse_return()?,
931                "last" => {
932                    self.advance();
933                    let lbl = self.try_take_loop_label();
934                    let stmt = Statement {
935                        label: None,
936                        kind: StmtKind::Last(lbl.or(label.clone())),
937                        line,
938                    };
939                    self.parse_stmt_postfix_modifier(stmt)?
940                }
941                "next" => {
942                    self.advance();
943                    let lbl = self.try_take_loop_label();
944                    let stmt = Statement {
945                        label: None,
946                        kind: StmtKind::Next(lbl.or(label.clone())),
947                        line,
948                    };
949                    self.parse_stmt_postfix_modifier(stmt)?
950                }
951                "redo" => {
952                    self.advance();
953                    let lbl = self.try_take_loop_label();
954                    let stmt = Statement {
955                        label: None,
956                        kind: StmtKind::Redo(lbl.or(label.clone())),
957                        line,
958                    };
959                    self.parse_stmt_postfix_modifier(stmt)?
960                }
961                "BEGIN" => {
962                    self.advance();
963                    let block = self.parse_block()?;
964                    Statement {
965                        label: None,
966                        kind: StmtKind::Begin(block),
967                        line,
968                    }
969                }
970                "END" => {
971                    self.advance();
972                    let block = self.parse_block()?;
973                    Statement {
974                        label: None,
975                        kind: StmtKind::End(block),
976                        line,
977                    }
978                }
979                "UNITCHECK" => {
980                    self.advance();
981                    let block = self.parse_block()?;
982                    Statement {
983                        label: None,
984                        kind: StmtKind::UnitCheck(block),
985                        line,
986                    }
987                }
988                "CHECK" => {
989                    self.advance();
990                    let block = self.parse_block()?;
991                    Statement {
992                        label: None,
993                        kind: StmtKind::Check(block),
994                        line,
995                    }
996                }
997                "INIT" => {
998                    self.advance();
999                    let block = self.parse_block()?;
1000                    Statement {
1001                        label: None,
1002                        kind: StmtKind::Init(block),
1003                        line,
1004                    }
1005                }
1006                "goto" => {
1007                    self.advance();
1008                    let target = self.parse_expression()?;
1009                    let stmt = Statement {
1010                        label: None,
1011                        kind: StmtKind::Goto {
1012                            target: Box::new(target),
1013                        },
1014                        line,
1015                    };
1016                    // `goto $l if COND;` / `goto &$cr if defined &$cr;` (XSLoader.pm)
1017                    self.parse_stmt_postfix_modifier(stmt)?
1018                }
1019                "continue" => {
1020                    self.advance();
1021                    let block = self.parse_block()?;
1022                    Statement {
1023                        label: None,
1024                        kind: StmtKind::Continue(block),
1025                        line,
1026                    }
1027                }
1028                "before"
1029                    if matches!(
1030                        self.peek_at(1),
1031                        Token::SingleString(_) | Token::DoubleString(_)
1032                    ) =>
1033                {
1034                    self.parse_advice_decl(crate::ast::AdviceKind::Before)?
1035                }
1036                "after"
1037                    if matches!(
1038                        self.peek_at(1),
1039                        Token::SingleString(_) | Token::DoubleString(_)
1040                    ) =>
1041                {
1042                    self.parse_advice_decl(crate::ast::AdviceKind::After)?
1043                }
1044                "around"
1045                    if matches!(
1046                        self.peek_at(1),
1047                        Token::SingleString(_) | Token::DoubleString(_)
1048                    ) =>
1049                {
1050                    self.parse_advice_decl(crate::ast::AdviceKind::Around)?
1051                }
1052                "try" => self.parse_try_catch()?,
1053                "defer" => self.parse_defer_stmt()?,
1054                "tie" => self.parse_tie_stmt()?,
1055                "given" => self.parse_given()?,
1056                "when" => self.parse_when_stmt()?,
1057                "default" => self.parse_default_stmt()?,
1058                "eval_timeout" => self.parse_eval_timeout()?,
1059                "do" => {
1060                    if matches!(self.peek_at(1), Token::LBrace) {
1061                        self.advance();
1062                        let body = self.parse_block()?;
1063                        if let Token::Ident(ref w) = self.peek().clone() {
1064                            if w == "while" {
1065                                self.advance();
1066                                self.expect(&Token::LParen)?;
1067                                let mut condition = self.parse_expression()?;
1068                                Self::mark_match_scalar_g_for_boolean_condition(&mut condition);
1069                                self.expect(&Token::RParen)?;
1070                                self.eat(&Token::Semicolon);
1071                                Statement {
1072                                    label: label.clone(),
1073                                    kind: StmtKind::DoWhile { body, condition },
1074                                    line,
1075                                }
1076                            } else {
1077                                let inner_line = body.first().map(|s| s.line).unwrap_or(line);
1078                                let inner = Expr {
1079                                    kind: ExprKind::CodeRef {
1080                                        params: vec![],
1081                                        body,
1082                                    },
1083                                    line: inner_line,
1084                                };
1085                                let expr = Expr {
1086                                    kind: ExprKind::Do(Box::new(inner)),
1087                                    line,
1088                                };
1089                                let stmt = Statement {
1090                                    label: label.clone(),
1091                                    kind: StmtKind::Expression(expr),
1092                                    line,
1093                                };
1094                                // `do { } if EXPR` / `do { } unless EXPR` — postfix modifier, not a new `if (` statement.
1095                                self.parse_stmt_postfix_modifier(stmt)?
1096                            }
1097                        } else {
1098                            let inner_line = body.first().map(|s| s.line).unwrap_or(line);
1099                            let inner = Expr {
1100                                kind: ExprKind::CodeRef {
1101                                    params: vec![],
1102                                    body,
1103                                },
1104                                line: inner_line,
1105                            };
1106                            let expr = Expr {
1107                                kind: ExprKind::Do(Box::new(inner)),
1108                                line,
1109                            };
1110                            let stmt = Statement {
1111                                label: label.clone(),
1112                                kind: StmtKind::Expression(expr),
1113                                line,
1114                            };
1115                            self.parse_stmt_postfix_modifier(stmt)?
1116                        }
1117                    } else {
1118                        if let Some(expr) = self.try_parse_bareword_stmt_call() {
1119                            let stmt = self.maybe_postfix_modifier(expr)?;
1120                            self.parse_stmt_postfix_modifier(stmt)?
1121                        } else {
1122                            let expr = self.parse_expression()?;
1123                            let stmt = self.maybe_postfix_modifier(expr)?;
1124                            self.parse_stmt_postfix_modifier(stmt)?
1125                        }
1126                    }
1127                }
1128                _ => {
1129                    // `foo;` or `{ foo }` — bareword statement is a zero-arg call (topic `$_` at runtime).
1130                    if let Some(expr) = self.try_parse_bareword_stmt_call() {
1131                        let stmt = self.maybe_postfix_modifier(expr)?;
1132                        self.parse_stmt_postfix_modifier(stmt)?
1133                    } else {
1134                        let expr = self.parse_expression()?;
1135                        let stmt = self.maybe_postfix_modifier(expr)?;
1136                        self.parse_stmt_postfix_modifier(stmt)?
1137                    }
1138                }
1139            },
1140            Token::LBrace => {
1141                // Disambiguate hashref `{ k => v }` from block `{ stmt; stmt }`.
1142                // If it looks like a hashref, parse as expression; otherwise parse as block.
1143                if self.looks_like_hashref() {
1144                    let expr = self.parse_expression()?;
1145                    let stmt = self.maybe_postfix_modifier(expr)?;
1146                    self.parse_stmt_postfix_modifier(stmt)?
1147                } else {
1148                    let block = self.parse_block()?;
1149                    let stmt = Statement {
1150                        label: None,
1151                        kind: StmtKind::Block(block),
1152                        line,
1153                    };
1154                    // `{ … } if EXPR` / `{ … } unless EXPR` — same postfix rule as `do { } if …` (not `if (`).
1155                    self.parse_stmt_postfix_modifier(stmt)?
1156                }
1157            }
1158            _ => {
1159                let expr = self.parse_expression()?;
1160                let stmt = self.maybe_postfix_modifier(expr)?;
1161                self.parse_stmt_postfix_modifier(stmt)?
1162            }
1163        };
1164
1165        stmt.label = label;
1166        Ok(stmt)
1167    }
1168
1169    /// Consume an immediately-following loop label after `next`/`last`/`redo`.
1170    /// Matches identifiers that look like Perl loop labels — uppercase letters,
1171    /// digits, or `_`, and must start with a non-digit. Anything else
1172    /// (lowercase function names, `if`, `unless`, `(EXPR`, …) is left for the
1173    /// `EXPR`-form / postfix-modifier paths.
1174    fn try_take_loop_label(&mut self) -> Option<String> {
1175        let Token::Ident(s) = self.peek() else {
1176            return None;
1177        };
1178        let mut chars = s.chars();
1179        let first = chars.next()?;
1180        if !(first.is_ascii_uppercase() || first == '_') {
1181            return None;
1182        }
1183        let ok = s
1184            .chars()
1185            .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_');
1186        if !ok {
1187            return None;
1188        }
1189        let (Token::Ident(l), _) = self.advance() else {
1190            unreachable!()
1191        };
1192        Some(l)
1193    }
1194
1195    /// Handle postfix if/unless on statement-level keywords like last/next.
1196    fn parse_stmt_postfix_modifier(&mut self, stmt: Statement) -> StrykeResult<Statement> {
1197        let line = stmt.line;
1198        // Implicit semicolon: a modifier keyword on a new line is a new
1199        // statement, not a postfix modifier.  This prevents semicolon-less
1200        // code like `my $x = "val"\nif ($x) { ... }` from being mis-parsed
1201        // as `my $x = "val" if ($x) { ... }`.
1202        //
1203        // Disabled in `--compat`: Perl 5 source is whitespace-insensitive
1204        // between statements (terminated by `;`), so multi-line postfix
1205        // modifiers like Test.pm's `print ... \n  if defined &X;` must
1206        // attach to the prior statement.
1207        if !crate::compat_mode() && self.peek_line() > self.prev_line() {
1208            self.eat(&Token::Semicolon);
1209            return Ok(stmt);
1210        }
1211        if let Token::Ident(ref kw) = self.peek().clone() {
1212            match kw.as_str() {
1213                "if" => {
1214                    self.advance();
1215                    let mut cond = self.parse_expression()?;
1216                    Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
1217                    self.eat(&Token::Semicolon);
1218                    return Ok(Statement {
1219                        label: None,
1220                        kind: StmtKind::If {
1221                            condition: cond,
1222                            body: vec![stmt],
1223                            elsifs: vec![],
1224                            else_block: None,
1225                        },
1226                        line,
1227                    });
1228                }
1229                "unless" => {
1230                    self.advance();
1231                    let mut cond = self.parse_expression()?;
1232                    Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
1233                    self.eat(&Token::Semicolon);
1234                    return Ok(Statement {
1235                        label: None,
1236                        kind: StmtKind::Unless {
1237                            condition: cond,
1238                            body: vec![stmt],
1239                            else_block: None,
1240                        },
1241                        line,
1242                    });
1243                }
1244                "while" | "until" | "for" | "foreach" => {
1245                    // `do { } for @a` / `{ } while COND` — same postfix forms as [`maybe_postfix_modifier`],
1246                    // not a new `for (` / `while (` statement (which would require `(` after `for`).
1247                    if let Some(expr) = Self::stmt_into_postfix_body_expr(stmt) {
1248                        let out = self.maybe_postfix_modifier(expr)?;
1249                        self.eat(&Token::Semicolon);
1250                        return Ok(out);
1251                    }
1252                    return Err(self.syntax_err(
1253                        format!("postfix `{}` is not supported on this statement form", kw),
1254                        self.peek_line(),
1255                    ));
1256                }
1257                // `{ } pmap @a` / `{ } pflat_map @a` / `{ } pfor @a` / `do { } …` — same shapes as prefix forms.
1258                "pmap" | "pflat_map" | "pgrep" | "pfor" | "pforeach" | "preduce" | "pcache" | "par" => {
1259                    let line = stmt.line;
1260                    let block = self.stmt_into_parallel_block(stmt)?;
1261                    let which = kw.as_str();
1262                    self.advance();
1263                    self.eat(&Token::Comma);
1264                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
1265                    self.eat(&Token::Semicolon);
1266                    let list = Box::new(list);
1267                    let progress = progress.map(Box::new);
1268                    let kind = match which {
1269                        "pmap" => ExprKind::PMapExpr {
1270                            block,
1271                            list,
1272                            progress,
1273                            flat_outputs: false,
1274                            on_cluster: None,
1275                            stream: false,
1276                        },
1277                        "pflat_map" => ExprKind::PMapExpr {
1278                            block,
1279                            list,
1280                            progress,
1281                            flat_outputs: true,
1282                            on_cluster: None,
1283                            stream: false,
1284                        },
1285                        "pgrep" => ExprKind::PGrepExpr {
1286                            block,
1287                            list,
1288                            progress,
1289                            stream: false,
1290                        },
1291                        "pfor" | "pforeach" => ExprKind::PForExpr {
1292                            block,
1293                            list,
1294                            progress,
1295                        },
1296                        "preduce" => ExprKind::PReduceExpr {
1297                            block,
1298                            list,
1299                            progress,
1300                        },
1301                        "pcache" => ExprKind::PcacheExpr {
1302                            block,
1303                            list,
1304                            progress,
1305                        },
1306                        "par" => ExprKind::ParExpr { block, list },
1307                        _ => unreachable!(),
1308                    };
1309                    return Ok(Statement {
1310                        label: None,
1311                        kind: StmtKind::Expression(Expr { kind, line }),
1312                        line,
1313                    });
1314                }
1315                _ => {}
1316            }
1317        }
1318        self.eat(&Token::Semicolon);
1319        Ok(stmt)
1320    }
1321
1322    /// Block body for postfix `pmap` / `pfor` / … — bare `{ }`, `do { }`, or any expression
1323    /// statement (wrapped as a one-line block, e.g. `` `cmd` pfor @a ``).
1324    fn stmt_into_parallel_block(&self, stmt: Statement) -> StrykeResult<Block> {
1325        let line = stmt.line;
1326        match stmt.kind {
1327            StmtKind::Block(block) => Ok(block),
1328            StmtKind::Expression(expr) => {
1329                if let ExprKind::Do(ref inner) = expr.kind {
1330                    if let ExprKind::CodeRef { ref body, .. } = inner.kind {
1331                        return Ok(body.clone());
1332                    }
1333                }
1334                Ok(vec![Statement {
1335                    label: None,
1336                    kind: StmtKind::Expression(expr),
1337                    line,
1338                }])
1339            }
1340            _ => Err(self.syntax_err(
1341                "postfix parallel op expects `do { }`, a bare `{ }` block, or an expression statement",
1342                line,
1343            )),
1344        }
1345    }
1346
1347    /// `StmtKind::Expression` or a bare block (`StmtKind::Block`) as an [`Expr`] for postfix
1348    /// `while` / `until` / `for` / `foreach` (mirrors `do { }` → [`ExprKind::Do`]\([`ExprKind::CodeRef`]\)).
1349    fn stmt_into_postfix_body_expr(stmt: Statement) -> Option<Expr> {
1350        match stmt.kind {
1351            StmtKind::Expression(expr) => Some(expr),
1352            StmtKind::Block(block) => {
1353                let line = stmt.line;
1354                let inner = Expr {
1355                    kind: ExprKind::CodeRef {
1356                        params: vec![],
1357                        body: block,
1358                    },
1359                    line,
1360                };
1361                Some(Expr {
1362                    kind: ExprKind::Do(Box::new(inner)),
1363                    line,
1364                })
1365            }
1366            _ => None,
1367        }
1368    }
1369
1370    /// Statement-modifier keywords that must not be consumed as part of a comma-separated list
1371    /// (same set as [`parse_list_until_terminator`]).
1372    fn peek_is_postfix_stmt_modifier_keyword(&self) -> bool {
1373        matches!(
1374            self.peek(),
1375            Token::Ident(ref kw)
1376                if matches!(
1377                    kw.as_str(),
1378                    "if" | "unless" | "while" | "until" | "for" | "foreach"
1379                )
1380        )
1381    }
1382
1383    /// Token classes whose precedence sits below a Perl-style named unary
1384    /// operator. When one of these is the next token after a unary keyword
1385    /// (`length`, `len`, `cnt`, …), the keyword takes no explicit argument
1386    /// and the surrounding expression continues. Mirrors the `parse_one_arg_or_default`
1387    /// boundary set; kept as a separate predicate so other parse paths can
1388    /// reuse it without committing to default-to-`$_` semantics.
1389    fn peek_is_named_unary_terminator(&self) -> bool {
1390        matches!(
1391            self.peek(),
1392            Token::Semicolon
1393                | Token::RBrace
1394                | Token::RParen
1395                | Token::RBracket
1396                | Token::Eof
1397                | Token::Comma
1398                | Token::FatArrow
1399                | Token::PipeForward
1400                | Token::Question
1401                | Token::Colon
1402                | Token::NumEq
1403                | Token::NumNe
1404                | Token::NumLt
1405                | Token::NumGt
1406                | Token::NumLe
1407                | Token::NumGe
1408                | Token::Spaceship
1409                | Token::StrEq
1410                | Token::StrNe
1411                | Token::StrLt
1412                | Token::StrGt
1413                | Token::StrLe
1414                | Token::StrGe
1415                | Token::StrCmp
1416                | Token::LogAnd
1417                | Token::LogOr
1418                | Token::LogAndWord
1419                | Token::LogOrWord
1420                | Token::DefinedOr
1421                | Token::Range
1422                | Token::RangeExclusive
1423                | Token::Assign
1424                | Token::PlusAssign
1425                | Token::MinusAssign
1426                | Token::MulAssign
1427                | Token::DivAssign
1428                | Token::ModAssign
1429                | Token::PowAssign
1430                | Token::DotAssign
1431                | Token::AndAssign
1432                | Token::OrAssign
1433                | Token::XorAssign
1434                | Token::DefinedOrAssign
1435                | Token::ShiftLeftAssign
1436                | Token::ShiftRightAssign
1437                | Token::BitAndAssign
1438                | Token::BitOrAssign
1439        )
1440    }
1441
1442    fn maybe_postfix_modifier(&mut self, expr: Expr) -> StrykeResult<Statement> {
1443        let line = expr.line;
1444        // Implicit semicolon: modifier keyword on a new line starts a new
1445        // statement. Disabled in `--compat` — see `parse_stmt_postfix_modifier`.
1446        if !crate::compat_mode() && self.peek_line() > self.prev_line() {
1447            return Ok(Statement {
1448                label: None,
1449                kind: StmtKind::Expression(expr),
1450                line,
1451            });
1452        }
1453        match self.peek() {
1454            Token::Ident(ref kw) => match kw.as_str() {
1455                "if" => {
1456                    self.advance();
1457                    let cond = self.parse_expression()?;
1458                    Ok(Statement {
1459                        label: None,
1460                        kind: StmtKind::Expression(Expr {
1461                            kind: ExprKind::PostfixIf {
1462                                expr: Box::new(expr),
1463                                condition: Box::new(cond),
1464                            },
1465                            line,
1466                        }),
1467                        line,
1468                    })
1469                }
1470                "unless" => {
1471                    self.advance();
1472                    let cond = self.parse_expression()?;
1473                    Ok(Statement {
1474                        label: None,
1475                        kind: StmtKind::Expression(Expr {
1476                            kind: ExprKind::PostfixUnless {
1477                                expr: Box::new(expr),
1478                                condition: Box::new(cond),
1479                            },
1480                            line,
1481                        }),
1482                        line,
1483                    })
1484                }
1485                "while" => {
1486                    self.advance();
1487                    let cond = self.parse_expression()?;
1488                    Ok(Statement {
1489                        label: None,
1490                        kind: StmtKind::Expression(Expr {
1491                            kind: ExprKind::PostfixWhile {
1492                                expr: Box::new(expr),
1493                                condition: Box::new(cond),
1494                            },
1495                            line,
1496                        }),
1497                        line,
1498                    })
1499                }
1500                "until" => {
1501                    self.advance();
1502                    let cond = self.parse_expression()?;
1503                    Ok(Statement {
1504                        label: None,
1505                        kind: StmtKind::Expression(Expr {
1506                            kind: ExprKind::PostfixUntil {
1507                                expr: Box::new(expr),
1508                                condition: Box::new(cond),
1509                            },
1510                            line,
1511                        }),
1512                        line,
1513                    })
1514                }
1515                "for" | "foreach" => {
1516                    self.advance();
1517                    let list = self.parse_expression()?;
1518                    Ok(Statement {
1519                        label: None,
1520                        kind: StmtKind::Expression(Expr {
1521                            kind: ExprKind::PostfixForeach {
1522                                expr: Box::new(expr),
1523                                list: Box::new(list),
1524                            },
1525                            line,
1526                        }),
1527                        line,
1528                    })
1529                }
1530                _ => Ok(Statement {
1531                    label: None,
1532                    kind: StmtKind::Expression(expr),
1533                    line,
1534                }),
1535            },
1536            _ => Ok(Statement {
1537                label: None,
1538                kind: StmtKind::Expression(expr),
1539                line,
1540            }),
1541        }
1542    }
1543
1544    /// `name;` or `name}` — a bare identifier statement is a sub call with no explicit args (`$_` implied).
1545    fn try_parse_bareword_stmt_call(&mut self) -> Option<Expr> {
1546        let saved = self.pos;
1547        let line = self.peek_line();
1548        let mut name = match self.peek() {
1549            Token::Ident(n) => n.clone(),
1550            _ => return None,
1551        };
1552        // Names that begin `parse_named_expr` (builtins / `undef` / …) must use that path, not a sub call.
1553        if name.starts_with('\x00') || !Self::bareword_stmt_may_be_sub(&name) {
1554            return None;
1555        }
1556        self.advance();
1557        while self.eat(&Token::PackageSep) {
1558            match self.advance() {
1559                (Token::Ident(part), _) => {
1560                    name = format!("{}::{}", name, part);
1561                }
1562                _ => {
1563                    self.pos = saved;
1564                    return None;
1565                }
1566            }
1567        }
1568        match self.peek() {
1569            Token::Semicolon | Token::RBrace => Some(Expr {
1570                kind: ExprKind::FuncCall { name, args: vec![] },
1571                line,
1572            }),
1573            _ => {
1574                self.pos = saved;
1575                None
1576            }
1577        }
1578    }
1579
1580    /// Map an operator-keyword token (the lexer converts `eq`, `ne`, …, `and`,
1581    /// `or`, `not`, `x` to dedicated tokens) back to its identifier spelling.
1582    /// Used in hash-key contexts where the bareword form is the user's intent.
1583    pub(crate) fn operator_keyword_to_ident_str(tok: &Token) -> Option<&'static str> {
1584        Some(match tok {
1585            Token::StrEq => "eq",
1586            Token::StrNe => "ne",
1587            Token::StrLt => "lt",
1588            Token::StrGt => "gt",
1589            Token::StrLe => "le",
1590            Token::StrGe => "ge",
1591            Token::StrCmp => "cmp",
1592            Token::LogAndWord => "and",
1593            Token::LogOrWord => "or",
1594            Token::LogNotWord => "not",
1595            Token::X => "x",
1596            _ => return None,
1597        })
1598    }
1599
1600    /// Bare names that resolve to the topic-slot scalar matrix:
1601    /// `_`, `_0`, `_1`, …, `_N`, plus `_<+`, `_N<+` for the 4-deep outer chain.
1602    /// These must NOT be treated as zero-arg sub calls — they're scalar var refs.
1603    pub(crate) fn is_underscore_topic_slot(name: &str) -> bool {
1604        if name == "_" {
1605            return true;
1606        }
1607        if !name.starts_with('_') || name.len() < 2 {
1608            return false;
1609        }
1610        let bytes = name.as_bytes();
1611        let mut i = 1;
1612        // Optional digit run (positional slot index).
1613        while i < bytes.len() && bytes[i].is_ascii_digit() {
1614            i += 1;
1615        }
1616        // Then any number of `<` chevrons (runtime cap at 5; lexer accepts more).
1617        let chevrons_start = i;
1618        while i < bytes.len() && bytes[i] == b'<' {
1619            i += 1;
1620        }
1621        // Must be one of: `_`, `_N`, `_<+`, `_N<+`. No other trailing chars.
1622        i == bytes.len() && (i > 1 || chevrons_start > 1)
1623    }
1624
1625    /// Bareword names that map to Perl special variables / filehandles /
1626    /// compile-time tokens. A user-defined sub with any of these names
1627    /// would shadow the special variable's expression-position usage and
1628    /// produce silently-broken code. Reject at parse time with a
1629    /// foot-gun error message.
1630    ///
1631    /// Sigil-form spellings (`$@`, `$!`, `@ARGV`, `%ENV`, etc.) are caught
1632    /// separately via the `parse_sub_decl` catch-all branch — those don't
1633    /// even lex as `Token::Ident` so they hit a different code path.
1634    pub(crate) fn is_reserved_special_var_name(name: &str) -> bool {
1635        matches!(
1636            name,
1637            // Standard filehandles (Perl: STDIN, STDOUT, STDERR, ARGV, …)
1638            "STDIN" | "STDOUT" | "STDERR" | "ARGV" | "ARGVOUT" | "DATA"
1639            // Package globals, normally accessed via sigils (@ARGV, %ENV,
1640            // @INC, %SIG, @ISA, %ENV, etc.) — bareword shadow is a foot-gun.
1641            // NOTE: `AUTOLOAD` is intentionally NOT in this list — `fn
1642            // AUTOLOAD { ... }` is the legitimate Perl idiom for handling
1643            // missing-method dispatch. The runtime sets `$AUTOLOAD` to the
1644            // missing sub's qualified name before invoking the user's
1645            // AUTOLOAD sub. Adding it here would break that mechanism.
1646            | "ENV" | "INC" | "SIG" | "ISA"
1647            | "EXPORT" | "EXPORT_OK" | "EXPORT_TAGS"
1648            | "VERSION"
1649            // Compile-time tokens (resolve to constants at parse time).
1650            | "__FILE__" | "__LINE__" | "__PACKAGE__" | "__SUB__"
1651            | "__DATA__" | "__END__"
1652        )
1653    }
1654
1655    /// Identifiers that start a [`parse_named_expr`] arm (builtins / special forms), not a bare sub call.
1656    fn bareword_stmt_may_be_sub(name: &str) -> bool {
1657        // Topic-slot scalar names (`_`, `_N`, `_<+`, `_N<+`) are scalar
1658        // variables, not zero-arg sub calls. Without this guard, the
1659        // statement-position parser would emit `Op::Call("_0", 0)` and fail
1660        // at runtime with "Undefined subroutine &_0".
1661        if Self::is_underscore_topic_slot(name) {
1662            return false;
1663        }
1664        !matches!(
1665            name,
1666            "__FILE__"
1667                | "__LINE__"
1668                | "__PACKAGE__"
1669                | "__SUB__"
1670                | "abs"
1671                | "async"
1672                | "spawn"
1673                | "atan2"
1674                | "await"
1675                | "barrier"
1676                | "bless"
1677                | "burp"
1678                | "caller"
1679                | "capture"
1680                | "cat"
1681                | "chdir"
1682                | "chmod"
1683                | "chomp"
1684                | "chop"
1685                | "chr"
1686                | "chown"
1687                | "closedir"
1688                | "close"
1689                | "collect"
1690                | "cos"
1691                | "crypt"
1692                | "defined"
1693                | "dec"
1694                | "delete"
1695                | "die"
1696                | "deque"
1697                | "do"
1698                | "each"
1699                | "eof"
1700                | "fore"
1701                | "eval"
1702                | "exec"
1703                | "exists"
1704                | "exit"
1705                | "exp"
1706                | "fan"
1707                | "fan_cap"
1708                | "fc"
1709                | "fetch_url"
1710                | "d"
1711                | "dirs"
1712                | "dr"
1713                | "f"
1714                | "fi"
1715                | "files"
1716                | "filesf"
1717                | "filter"
1718                | "fr"
1719                | "getcwd"
1720                | "glob_par"
1721                | "par_sed"
1722                | "glob"
1723                | "god"
1724                | "grep"
1725                | "greps"
1726                | "heap"
1727                | "hex"
1728                | "inc"
1729                | "index"
1730                | "int"
1731                | "join"
1732                | "keys"
1733                | "lcfirst"
1734                | "lc"
1735                | "length"
1736                | "link"
1737                | "log"
1738                | "lstat"
1739                | "map"
1740                | "flat_map"
1741                | "maps"
1742                | "flat_maps"
1743                | "flatten"
1744                | "frequencies"
1745                | "freq"
1746                | "pfrequencies"
1747                | "pfreq"
1748                | "interleave"
1749                | "ddump"
1750                | "stringify"
1751                | "str"
1752                | "s"
1753                | "input"
1754                | "lines"
1755                | "words"
1756                | "chars"
1757                | "digits"
1758                | "letters"
1759                | "letters_uc"
1760                | "letters_lc"
1761                | "punctuation"
1762                | "sentences"
1763                | "paragraphs"
1764                | "sections"
1765                | "numbers"
1766                | "graphemes"
1767                | "columns"
1768                | "trim"
1769                | "avg"
1770                | "top"
1771                | "pager"
1772                | "pg"
1773                | "less"
1774                | "count_by"
1775                | "to_file"
1776                | "to_json"
1777                | "to_csv"
1778                | "grep_v"
1779                | "select_keys"
1780                | "pluck"
1781                | "clamp"
1782                | "normalize"
1783                | "stddev"
1784                | "squared"
1785                | "square"
1786                | "cubed"
1787                | "cube"
1788                | "expt"
1789                | "pow"
1790                | "pw"
1791                | "snake_case"
1792                | "camel_case"
1793                | "kebab_case"
1794                | "to_toml"
1795                | "to_yaml"
1796                | "to_xml"
1797                | "to_html"
1798                | "to_markdown"
1799                | "xopen"
1800                | "clip"
1801                | "paste"
1802                | "to_table"
1803                | "sparkline"
1804                | "bar_chart"
1805                | "flame"
1806                | "set"
1807                | "list_count"
1808                | "list_size"
1809                | "count"
1810                | "size"
1811                | "cnt"
1812                | "len"
1813                | "all"
1814                | "any"
1815                | "none"
1816                | "take_while"
1817                | "drop_while"
1818                | "skip_while"
1819                | "skip"
1820                | "first_or"
1821                | "tap"
1822                | "peek"
1823                | "partition"
1824                | "min_by"
1825                | "max_by"
1826                | "zip_with"
1827                | "group_by"
1828                | "chunk_by"
1829                | "with_index"
1830                | "puniq"
1831                | "pfirst"
1832                | "pany"
1833                | "uniq"
1834                | "distinct"
1835                | "shuffle"
1836                | "shuffled"
1837                | "chunked"
1838                | "windowed"
1839                | "match"
1840                | "mkdir"
1841                | "every"
1842                | "gen"
1843                | "oct"
1844                | "open"
1845                | "p"
1846                | "opendir"
1847                | "ord"
1848                | "par"
1849                | "par_lines"
1850                | "par_walk"
1851                | "pipe"
1852                | "pipes"
1853                | "block_devices"
1854                | "char_devices"
1855                | "exe"
1856                | "executables"
1857                | "rate_limit"
1858                | "retry"
1859                | "pcache"
1860                | "pchannel"
1861                | "pfor"
1862                | "pforeach"
1863                | "pgrep"
1864                | "pgreps"
1865                | "pipeline"
1866                | "pmap_chunked"
1867                | "pmap_reduce"
1868                | "par_reduce"
1869                | "pmap_on"
1870                | "pflat_map_on"
1871                | "pmap"
1872                | "pmaps"
1873                | "pflat_map"
1874                | "pflat_maps"
1875                | "pop"
1876                | "pos"
1877                | "ppool"
1878                | "preduce_init"
1879                | "preduce"
1880                | "pselect"
1881                | "printf"
1882                | "print"
1883                | "pr"
1884                | "psort"
1885                | "push"
1886                | "pwatch"
1887                | "rand"
1888                | "readdir"
1889                | "readlink"
1890                | "reduce"
1891                | "fold"
1892                | "inject"
1893                | "first"
1894                | "detect"
1895                | "find"
1896                | "find_all"
1897                | "find_index"
1898                | "firstidx"
1899                | "first_index"
1900                | "ref"
1901                | "rename"
1902                | "require"
1903                | "rev"
1904                | "reverse"
1905                | "reversed"
1906                | "rewinddir"
1907                | "rindex"
1908                | "rmdir"
1909                | "rm"
1910                | "say"
1911                | "scalar"
1912                | "seekdir"
1913                | "shift"
1914                | "sin"
1915                | "slurp"
1916                | "swallow"
1917                | "ingest"
1918                | "sockets"
1919                | "sort"
1920                | "splice"
1921                | "splice_last"
1922                | "splice1"
1923                | "spl_last"
1924                | "split"
1925                | "sprintf"
1926                | "sqrt"
1927                | "srand"
1928                | "stat"
1929                | "study"
1930                | "substr"
1931                | "symlink"
1932                | "sym_links"
1933                | "system"
1934                | "telldir"
1935                | "timer"
1936                | "trace"
1937                | "ucfirst"
1938                | "uc"
1939                | "undef"
1940                | "umask"
1941                | "unlink"
1942                | "unshift"
1943                | "utime"
1944                | "values"
1945                | "wantarray"
1946                | "warn"
1947                | "watch"
1948                | "yield"
1949                | "sub"
1950        )
1951    }
1952
1953    fn parse_block(&mut self) -> StrykeResult<Block> {
1954        self.expect(&Token::LBrace)?;
1955        // Statements inside a block are NOT pipe RHS - reset depth so nested `~>`
1956        // parses its own input instead of using `$_[0]` placeholder.
1957        let saved_pipe_rhs_depth = self.pipe_rhs_depth;
1958        self.pipe_rhs_depth = 0;
1959        self.block_depth += 1;
1960        let mut stmts = Vec::new();
1961        // `{ |$a, $b| body }` — Ruby-style block params.
1962        // Desugars to `my $a = $_` (1 param), `my $a = $a; my $b = $b` (2 — sort/reduce),
1963        // or `my $p = $_N` for positional N≥3.
1964        if let Some(param_stmts) = self.try_parse_block_params()? {
1965            stmts.extend(param_stmts);
1966        }
1967        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
1968            if self.eat(&Token::Semicolon) {
1969                continue;
1970            }
1971            stmts.push(self.parse_statement()?);
1972        }
1973        self.expect(&Token::RBrace)?;
1974        self.pipe_rhs_depth = saved_pipe_rhs_depth;
1975        self.block_depth -= 1;
1976        Self::default_topic_for_sole_bareword(&mut stmts);
1977        Ok(stmts)
1978    }
1979
1980    /// Try to parse `|$var1, $var2, ...|` at the start of a block.
1981    /// Returns `None` if the leading `|` is not block-param syntax.
1982    /// When successful, returns `my $var = <implicit>` assignment statements
1983    /// that alias the block's positional arguments.
1984    fn try_parse_block_params(&mut self) -> StrykeResult<Option<Vec<Statement>>> {
1985        if !matches!(self.peek(), Token::BitOr) {
1986            return Ok(None);
1987        }
1988        // Lookahead: `| $scalar [, $scalar]* |` — verify before consuming.
1989        let mut i = 1; // skip the opening `|`
1990        loop {
1991            match self.peek_at(i) {
1992                Token::ScalarVar(_) => i += 1,
1993                _ => return Ok(None), // not `|$var...|`
1994            }
1995            match self.peek_at(i) {
1996                Token::BitOr => break,  // closing `|`
1997                Token::Comma => i += 1, // more params
1998                _ => return Ok(None),   // not block params
1999            }
2000        }
2001        // Confirmed — consume and build assignments.
2002        let line = self.peek_line();
2003        self.advance(); // eat opening `|`
2004        let mut names = Vec::new();
2005        loop {
2006            if let Token::ScalarVar(ref name) = self.peek().clone() {
2007                names.push(name.clone());
2008                self.advance();
2009            }
2010            if self.eat(&Token::BitOr) {
2011                break;
2012            }
2013            self.expect(&Token::Comma)?;
2014        }
2015        // Generate `my $name = <source>` for each param.
2016        // 1 param  → source is `$_` (map/grep/each/for topic)
2017        // 2 params → sources are `$a`, `$b` (sort/reduce)
2018        // N params → sources are `$_`, `$_1`, `$_2`, … (positional)
2019        let sources: Vec<&str> = match names.len() {
2020            1 => vec!["_"],
2021            2 => vec!["a", "b"],
2022            n => {
2023                // Can't return borrowed from a generated vec, handle below.
2024                let _ = n;
2025                vec![] // sentinel — handled in the else branch
2026            }
2027        };
2028        let mut stmts = Vec::with_capacity(names.len());
2029        if !sources.is_empty() {
2030            for (name, src) in names.iter().zip(sources.iter()) {
2031                stmts.push(Statement {
2032                    label: None,
2033                    kind: StmtKind::My(vec![VarDecl {
2034                        sigil: Sigil::Scalar,
2035                        name: name.clone(),
2036                        initializer: Some(Expr {
2037                            kind: ExprKind::ScalarVar(src.to_string()),
2038                            line,
2039                        }),
2040                        frozen: false,
2041                        type_annotation: None,
2042                        list_context: false,
2043                    }]),
2044                    line,
2045                });
2046            }
2047        } else {
2048            // N≥3: positional `$_`, `$_1`, `$_2`, …
2049            for (idx, name) in names.iter().enumerate() {
2050                let src = if idx == 0 {
2051                    "_".to_string()
2052                } else {
2053                    format!("_{idx}")
2054                };
2055                stmts.push(Statement {
2056                    label: None,
2057                    kind: StmtKind::My(vec![VarDecl {
2058                        sigil: Sigil::Scalar,
2059                        name: name.clone(),
2060                        initializer: Some(Expr {
2061                            kind: ExprKind::ScalarVar(src),
2062                            line,
2063                        }),
2064                        frozen: false,
2065                        type_annotation: None,
2066                        list_context: false,
2067                    }]),
2068                    line,
2069                });
2070            }
2071        }
2072        Ok(Some(stmts))
2073    }
2074
2075    /// Block shorthand: when the body is literally one bare builtin call
2076    /// (`{ uc }`, `{ basename }`, `{ to_json }`), inject `$_` as its first
2077    /// argument so `map { basename }` == `map { basename($_) }` uniformly.
2078    ///
2079    /// Without this, the ExprKind-modeled core names (`uc`/`lc`/`length`/…)
2080    /// default to `$_` via their own parse arms, but generic `FuncCall`-
2081    /// dispatched builtins (`basename`/`to_json`/`tj`/`bn`) are called with
2082    /// empty args and return the wrong value. This rewrite levels the
2083    /// playing field at parse time — no per-builtin handling needed.
2084    ///
2085    /// Narrow by design: fires only when the block has *exactly one*
2086    /// expression statement whose sole content is a known-bareword call
2087    /// with zero args. Multi-statement blocks and blocks with any other
2088    /// content are untouched.
2089    fn default_topic_for_sole_bareword(stmts: &mut [Statement]) {
2090        let [only] = stmts else { return };
2091        let StmtKind::Expression(ref mut expr) = only.kind else {
2092            return;
2093        };
2094        let topic_line = expr.line;
2095        let topic_arg = || Expr {
2096            kind: ExprKind::ScalarVar("_".to_string()),
2097            line: topic_line,
2098        };
2099        match expr.kind {
2100            // Zero-arg FuncCall whose name is a known builtin → inject `$_`.
2101            ExprKind::FuncCall {
2102                ref name,
2103                ref mut args,
2104            } if args.is_empty()
2105                && (Self::is_known_bareword(name) || Self::is_try_builtin_name(name)) =>
2106            {
2107                args.push(topic_arg());
2108            }
2109            // Lone bareword (the parser sometimes keeps a bareword as a
2110            // `Bareword` node instead of a zero-arg `FuncCall` —
2111            // e.g. `{ to_json }`, `{ ddump }`). Promote to a call.
2112            ExprKind::Bareword(ref name)
2113                if (Self::is_known_bareword(name) || Self::is_try_builtin_name(name)) =>
2114            {
2115                let n = name.clone();
2116                expr.kind = ExprKind::FuncCall {
2117                    name: n,
2118                    args: vec![topic_arg()],
2119                };
2120            }
2121            _ => {}
2122        }
2123    }
2124
2125    /// `defer { BLOCK }` — register a block to run when the current scope exits.
2126    /// Desugars to a `defer__internal(fn { BLOCK })` function call that the compiler
2127    /// handles specially by emitting Op::DeferBlock.
2128    fn parse_defer_stmt(&mut self) -> StrykeResult<Statement> {
2129        let line = self.peek_line();
2130        self.advance(); // defer
2131        let body = self.parse_block()?;
2132        self.eat(&Token::Semicolon);
2133        // Desugar: defer { BLOCK } → defer__internal(fn { BLOCK })
2134        let coderef = Expr {
2135            kind: ExprKind::CodeRef {
2136                params: vec![],
2137                body,
2138            },
2139            line,
2140        };
2141        Ok(Statement {
2142            label: None,
2143            kind: StmtKind::Expression(Expr {
2144                kind: ExprKind::FuncCall {
2145                    name: "defer__internal".to_string(),
2146                    args: vec![coderef],
2147                },
2148                line,
2149            }),
2150            line,
2151        })
2152    }
2153
2154    /// `try { } catch ($err) { }` with optional `finally { }`
2155    fn parse_try_catch(&mut self) -> StrykeResult<Statement> {
2156        let line = self.peek_line();
2157        self.advance(); // try
2158        let try_block = self.parse_block()?;
2159        match self.peek() {
2160            Token::Ident(ref k) if k == "catch" => {
2161                self.advance();
2162            }
2163            _ => {
2164                return Err(self.syntax_err("expected 'catch' after try block", self.peek_line()));
2165            }
2166        }
2167        self.expect(&Token::LParen)?;
2168        let catch_var = self.parse_scalar_var_name()?;
2169        self.expect(&Token::RParen)?;
2170        let catch_block = self.parse_block()?;
2171        let finally_block = match self.peek() {
2172            Token::Ident(ref k) if k == "finally" => {
2173                self.advance();
2174                Some(self.parse_block()?)
2175            }
2176            _ => None,
2177        };
2178        self.eat(&Token::Semicolon);
2179        Ok(Statement {
2180            label: None,
2181            kind: StmtKind::TryCatch {
2182                try_block,
2183                catch_var,
2184                catch_block,
2185                finally_block,
2186            },
2187            line,
2188        })
2189    }
2190
2191    /// `thread EXPR stage1 stage2 ...` — Clojure-style threading macro.
2192    /// Desugars to `EXPR |> stage1 |> stage2 |> ...`
2193    ///
2194    /// When `thread_last` is true (`->>` syntax), injects as last arg instead of first.
2195    ///
2196    /// When invoked as the RHS of `|>` (e.g. `LHS |> t s1 s2 ...`), the init
2197    /// is not parsed from tokens — using `parse_unary()` there lets the first
2198    /// bareword greedily consume the next token as its arg, which misparses
2199    /// `t inc pow($_, 2) p` as init=`inc(pow(…))` + stage=`p` instead of three
2200    /// separate stages. Instead, seed init with `$_[0]`, run every remaining
2201    /// token through the stage loop, and wrap the resulting chain in a
2202    /// `CodeRef`. The outer `pipe_forward_apply` then calls it with `lhs` as
2203    /// `$_[0]`, giving `LHS |> t s1 s2 s3` == `LHS |> s1 |> s2 |> s3`.
2204    fn parse_thread_macro(&mut self, _line: usize, thread_last: bool) -> StrykeResult<Expr> {
2205        self.parse_thread_macro_inner(_line, thread_last, None)
2206    }
2207
2208    /// Shared core for `~>` / `~>>` / `~s>` / `~s>>`. When
2209    /// `parallel_collector` is `Some` (streaming-mode entry from `~s>` /
2210    /// `~s>>`), after each stage is parsed we push the (just-built) stage
2211    /// expression into the collector and reset `result` to `$_` so the
2212    /// next stage parses against a fresh topic. The collector ends up
2213    /// with one Expr per stage where each stage's input is `$_`, ready
2214    /// to be wrapped as a `fn { ... }` closure for the per-item
2215    /// streaming runtime (`_thread_par_run`).
2216    fn parse_thread_macro_inner(
2217        &mut self,
2218        _line: usize,
2219        thread_last: bool,
2220        mut parallel_collector: Option<&mut Vec<Expr>>,
2221    ) -> StrykeResult<Expr> {
2222        // Set thread-last mode for pipe_forward_apply calls within this macro
2223        let saved_thread_last = self.thread_last_mode;
2224        self.thread_last_mode = thread_last;
2225
2226        let pipe_rhs_wrap = self.in_pipe_rhs();
2227        // `pending_thread_input` (set by `~p>` continuation parsing after
2228        // `||>` / `|then|`) supplies a pre-built input expression so we
2229        // skip parsing a source.
2230        let mut result = if let Some(pre) = self.pending_thread_input.take() {
2231            pre
2232        } else if pipe_rhs_wrap {
2233            Expr {
2234                kind: ExprKind::ArrayElement {
2235                    array: "_".to_string(),
2236                    index: Box::new(Expr {
2237                        kind: ExprKind::Integer(0),
2238                        line: _line,
2239                    }),
2240                },
2241                line: _line,
2242            }
2243        } else {
2244            // Suppress paren-less function calls so `t Color::Red p` parses
2245            // the enum variant without consuming `p` as an argument.
2246            self.suppress_parenless_call = self.suppress_parenless_call.saturating_add(1);
2247            let expr = self.parse_thread_input();
2248            self.suppress_parenless_call = self.suppress_parenless_call.saturating_sub(1);
2249            expr?
2250        };
2251        // Capture the source expression for parallel mode BEFORE any stage
2252        // is parsed, then reset `result` to `$_` so the first stage's parse
2253        // reads the topic instead of the source.
2254        let source_for_par = if parallel_collector.is_some() {
2255            let src = std::mem::replace(
2256                &mut result,
2257                Expr {
2258                    kind: ExprKind::ScalarVar("_".into()),
2259                    line: _line,
2260                },
2261            );
2262            Some(src)
2263        } else {
2264            None
2265        };
2266
2267        // Track line where the last stage ended (initially the input expression's line).
2268        let mut last_stage_end_line = self.prev_line();
2269
2270        // Parse stages until we hit a statement terminator
2271        loop {
2272            // Newline termination: if the next token is on a different line than where
2273            // the previous stage ended, the thread macro terminates. This allows
2274            // `~> @arr map { $_ * 2 }` on one line followed by `my @b = ...` on the next
2275            // without requiring a semicolon.
2276            if self.peek_line() > last_stage_end_line {
2277                break;
2278            }
2279
2280            // Check for terminators - |> ends thread and allows piping the result.
2281            // Variables ($x, @x, %x) and declaration keywords (my, our, local, state)
2282            // cannot be stages, so they implicitly terminate the thread macro.
2283            match self.peek() {
2284                Token::Semicolon
2285                | Token::RBrace
2286                | Token::RParen
2287                | Token::RBracket
2288                | Token::PipeForward
2289                | Token::Eof
2290                | Token::ScalarVar(_)
2291                | Token::ArrayVar(_)
2292                | Token::HashVar(_)
2293                | Token::Comma => break,
2294                // `||>` (LogOr + NumGt): chunk-parallel → sequential boundary
2295                // for `~p>` macros. Other thread macros never see this in
2296                // practice; if it appears, terminate the macro and let the
2297                // outer parser handle it.
2298                Token::LogOr if matches!(self.peek_at(1), Token::NumGt) => break,
2299                // `|then|` (BitOr + Ident("then") + BitOr): same boundary.
2300                Token::BitOr
2301                    if matches!(self.peek_at(1), Token::Ident(ref n) if n == "then")
2302                        && matches!(self.peek_at(2), Token::BitOr) =>
2303                {
2304                    break
2305                }
2306                Token::Ident(ref kw)
2307                    if matches!(
2308                        kw.as_str(),
2309                        "my" | "var" | "val" | "our"
2310                            | "local"
2311                            | "state"
2312                            | "if"
2313                            | "unless"
2314                            | "while"
2315                            | "until"
2316                            | "for"
2317                            | "foreach"
2318                            | "return"
2319                            | "last"
2320                            | "next"
2321                            | "redo"
2322                    ) =>
2323                {
2324                    break
2325                }
2326                _ => {}
2327            }
2328
2329            let stage_line = self.peek_line();
2330
2331            // Parse a stage and apply it to result via pipe
2332            match self.peek().clone() {
2333                // `>{ block }` — standalone anonymous block (sugar for fn { })
2334                Token::ArrowBrace => {
2335                    self.advance(); // consume `>{`
2336                    let mut stmts = Vec::new();
2337                    while !matches!(self.peek(), Token::RBrace | Token::Eof) {
2338                        if self.eat(&Token::Semicolon) {
2339                            continue;
2340                        }
2341                        stmts.push(self.parse_statement()?);
2342                    }
2343                    self.expect(&Token::RBrace)?;
2344                    let code_ref = Expr {
2345                        kind: ExprKind::CodeRef {
2346                            params: vec![],
2347                            body: stmts,
2348                        },
2349                        line: stage_line,
2350                    };
2351                    result = self.pipe_forward_apply(result, code_ref, stage_line)?;
2352                }
2353                // `sub { block }` — blocked in no-interop mode
2354                Token::Ident(ref name) if name == "sub" => {
2355                    if crate::no_interop_mode() {
2356                        return Err(self.syntax_err(
2357                            "stryke uses `fn {}` instead of `sub {}` (--no-interop)",
2358                            stage_line,
2359                        ));
2360                    }
2361                    self.advance(); // consume `sub`
2362                    let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
2363                    let body = self.parse_block()?;
2364                    let code_ref = Expr {
2365                        kind: ExprKind::CodeRef { params, body },
2366                        line: stage_line,
2367                    };
2368                    result = self.pipe_forward_apply(result, code_ref, stage_line)?;
2369                }
2370                // `fn { block }` — stryke anonymous function
2371                Token::Ident(ref name) if name == "fn" => {
2372                    self.advance(); // consume `fn`
2373                    let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
2374                    self.parse_sub_attributes()?;
2375                    let body = self.parse_fn_eq_body_or_block(false)?;
2376                    let code_ref = Expr {
2377                        kind: ExprKind::CodeRef { params, body },
2378                        line: stage_line,
2379                    };
2380                    result = self.pipe_forward_apply(result, code_ref, stage_line)?;
2381                }
2382                // `ident` possibly followed by block (or namespaced like `Foo::Bar::func`)
2383                Token::Ident(ref name) => {
2384                    let mut func_name = name.clone();
2385                    self.advance();
2386
2387                    // Collect namespaced function name (e.g., Rosetta::Stack::push)
2388                    while matches!(self.peek(), Token::PackageSep) {
2389                        self.advance(); // consume `::`
2390                        if let Token::Ident(ref part) = self.peek().clone() {
2391                            func_name.push_str("::");
2392                            func_name.push_str(part);
2393                            self.advance();
2394                        } else {
2395                            return Err(self.syntax_err(
2396                                format!(
2397                                    "Expected identifier after `::` in thread stage, got {:?}",
2398                                    self.peek()
2399                                ),
2400                                stage_line,
2401                            ));
2402                        }
2403                    }
2404
2405                    // Handle s/// and tr/// encoded tokens
2406                    if func_name.starts_with('\x00') {
2407                        let parts: Vec<&str> = func_name.split('\x00').collect();
2408                        if parts.len() >= 4 && parts[1] == "s" {
2409                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
2410                            let stage = Expr {
2411                                kind: ExprKind::Substitution {
2412                                    expr: Box::new(result.clone()),
2413                                    pattern: parts[2].to_string(),
2414                                    replacement: parts[3].to_string(),
2415                                    flags: format!("{}r", parts.get(4).unwrap_or(&"")),
2416                                    delim,
2417                                },
2418                                line: stage_line,
2419                            };
2420                            result = stage;
2421                            last_stage_end_line = self.prev_line();
2422                            continue;
2423                        }
2424                        if parts.len() >= 4 && parts[1] == "tr" {
2425                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
2426                            let stage = Expr {
2427                                kind: ExprKind::Transliterate {
2428                                    expr: Box::new(result.clone()),
2429                                    from: parts[2].to_string(),
2430                                    to: parts[3].to_string(),
2431                                    flags: format!("{}r", parts.get(4).unwrap_or(&"")),
2432                                    delim,
2433                                },
2434                                line: stage_line,
2435                            };
2436                            result = stage;
2437                            last_stage_end_line = self.prev_line();
2438                            continue;
2439                        }
2440                        return Err(
2441                            self.syntax_err("Unexpected encoded token in thread", stage_line)
2442                        );
2443                    }
2444
2445                    // `map +{ ... }` — hashref expression form (not a code block).
2446                    // The `+` disambiguates: `+{` is always a hashref constructor.
2447                    // Desugars to `MapExprComma` so pipe_forward_apply threads the
2448                    // list correctly: `t LIST map +{k => $_}` → `map +{k => $_}, LIST`.
2449                    if matches!(self.peek(), Token::Plus)
2450                        && matches!(self.peek_at(1), Token::LBrace)
2451                    {
2452                        self.advance(); // consume `+`
2453                        self.expect(&Token::LBrace)?;
2454                        // try_parse_hash_ref consumes the closing `}`
2455                        let pairs = self.try_parse_hash_ref()?;
2456                        let hashref_expr = Expr {
2457                            kind: ExprKind::HashRef(pairs),
2458                            line: stage_line,
2459                        };
2460                        let flatten_array_refs =
2461                            matches!(func_name.as_str(), "flat_map" | "flat_maps");
2462                        let stream = matches!(func_name.as_str(), "maps" | "flat_maps");
2463                        // Placeholder list — pipe_forward_apply replaces it with `result`.
2464                        let placeholder = Expr {
2465                            kind: ExprKind::Undef,
2466                            line: stage_line,
2467                        };
2468                        let map_node = Expr {
2469                            kind: ExprKind::MapExprComma {
2470                                expr: Box::new(hashref_expr),
2471                                list: Box::new(placeholder),
2472                                flatten_array_refs,
2473                                stream,
2474                            },
2475                            line: stage_line,
2476                        };
2477                        result = self.pipe_forward_apply(result, map_node, stage_line)?;
2478                    // `pmap_chunked CHUNK_SIZE { BLOCK }` — parallel chunked map
2479                    } else if func_name == "pmap_chunked" {
2480                        let chunk_size = self.parse_assign_expr()?;
2481                        let block = self.parse_block_or_bareword_block()?;
2482                        let placeholder = self.pipe_placeholder_list(stage_line);
2483                        let stage = Expr {
2484                            kind: ExprKind::PMapChunkedExpr {
2485                                chunk_size: Box::new(chunk_size),
2486                                block,
2487                                list: Box::new(placeholder),
2488                                progress: None,
2489                            },
2490                            line: stage_line,
2491                        };
2492                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2493                    // `preduce_init INIT { BLOCK }` — parallel reduce with init value
2494                    } else if func_name == "preduce_init" {
2495                        let init = self.parse_assign_expr()?;
2496                        let block = self.parse_block_or_bareword_block()?;
2497                        let placeholder = self.pipe_placeholder_list(stage_line);
2498                        let stage = Expr {
2499                            kind: ExprKind::PReduceInitExpr {
2500                                init: Box::new(init),
2501                                block,
2502                                list: Box::new(placeholder),
2503                                progress: None,
2504                            },
2505                            line: stage_line,
2506                        };
2507                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2508                    // `pmap_reduce { MAP } { REDUCE }` — parallel map-reduce
2509                    } else if func_name == "pmap_reduce" {
2510                        let map_block = self.parse_block_or_bareword_block()?;
2511                        let reduce_block = if matches!(self.peek(), Token::LBrace) {
2512                            self.parse_block()?
2513                        } else {
2514                            self.expect(&Token::Comma)?;
2515                            self.parse_block_or_bareword_cmp_block()?
2516                        };
2517                        let placeholder = self.pipe_placeholder_list(stage_line);
2518                        let stage = Expr {
2519                            kind: ExprKind::PMapReduceExpr {
2520                                map_block,
2521                                reduce_block,
2522                                list: Box::new(placeholder),
2523                                progress: None,
2524                            },
2525                            line: stage_line,
2526                        };
2527                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2528                    // `par_reduce { extract } [ { merge } ]` — chunk-extract-merge.
2529                    // First block runs per chunk in parallel; optional second
2530                    // block reduces pairwise across chunks (omit for auto-merge
2531                    // by result type).
2532                    } else if func_name == "par_reduce" {
2533                        let extract_block = self.parse_block_or_bareword_block()?;
2534                        let reduce_block = if matches!(self.peek(), Token::LBrace) {
2535                            Some(self.parse_block()?)
2536                        } else {
2537                            None
2538                        };
2539                        let placeholder = self.pipe_placeholder_list(stage_line);
2540                        let stage = Expr {
2541                            kind: ExprKind::ParReduceExpr {
2542                                extract_block,
2543                                reduce_block,
2544                                list: Box::new(placeholder),
2545                            },
2546                            line: stage_line,
2547                        };
2548                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2549                    // `pmap_on $cluster { BLOCK }` — parallel map dispatched to a remote
2550                    // cluster. Mirrors the `pmap_chunked` thread-stage shape; the cluster
2551                    // expression is parsed before the block, the threaded list slots in
2552                    // as the placeholder.
2553                    } else if func_name == "pmap_on" || func_name == "pflat_map_on" {
2554                        // Suppress `$cluster { ... }` auto-arrow (`$h->{...}`) so the
2555                        // brace opens the block, not a hash subscript.
2556                        self.suppress_scalar_hash_brace =
2557                            self.suppress_scalar_hash_brace.saturating_add(1);
2558                        let cluster = self.parse_assign_expr();
2559                        self.suppress_scalar_hash_brace =
2560                            self.suppress_scalar_hash_brace.saturating_sub(1);
2561                        let cluster = cluster?;
2562                        // Optional comma between cluster and block (matches the
2563                        // canonical `pmap_on $c, { BLOCK } @list` form in the LSP docs).
2564                        self.eat(&Token::Comma);
2565                        let block = self.parse_block_or_bareword_block()?;
2566                        let placeholder = self.pipe_placeholder_list(stage_line);
2567                        let stage = Expr {
2568                            kind: ExprKind::PMapExpr {
2569                                block,
2570                                list: Box::new(placeholder),
2571                                progress: None,
2572                                flat_outputs: func_name == "pflat_map_on",
2573                                on_cluster: Some(Box::new(cluster)),
2574                                stream: false,
2575                            },
2576                            line: stage_line,
2577                        };
2578                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2579                    // Check if followed by a block (like `filter { }`, `sort { }`, `map { }`)
2580                    } else if matches!(self.peek(), Token::LBrace) {
2581                        // Parse as a block-taking builtin
2582                        self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_add(1);
2583                        let stage = self.parse_thread_stage_with_block(&func_name, stage_line)?;
2584                        self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_sub(1);
2585                        result = self.pipe_forward_apply(result, stage, stage_line)?;
2586                    } else if matches!(self.peek(), Token::LParen) {
2587                        // Special handling for join(sep) and split(pattern) in thread context.
2588                        // These take the threaded list/string as their data argument, not as $_.
2589                        if func_name == "join" {
2590                            self.advance(); // consume `(`
2591                            let separator = self.parse_assign_expr()?;
2592                            self.expect(&Token::RParen)?;
2593                            let placeholder = self.pipe_placeholder_list(stage_line);
2594                            let stage = Expr {
2595                                kind: ExprKind::JoinExpr {
2596                                    separator: Box::new(separator),
2597                                    list: Box::new(placeholder),
2598                                },
2599                                line: stage_line,
2600                            };
2601                            result = self.pipe_forward_apply(result, stage, stage_line)?;
2602                        } else if func_name == "split" {
2603                            self.advance(); // consume `(`
2604                            let pattern = self.parse_assign_expr()?;
2605                            let limit = if self.eat(&Token::Comma) {
2606                                Some(Box::new(self.parse_assign_expr()?))
2607                            } else {
2608                                None
2609                            };
2610                            self.expect(&Token::RParen)?;
2611                            let placeholder = Expr {
2612                                kind: ExprKind::ScalarVar("_".to_string()),
2613                                line: stage_line,
2614                            };
2615                            let stage = Expr {
2616                                kind: ExprKind::SplitExpr {
2617                                    pattern: Box::new(pattern),
2618                                    string: Box::new(placeholder),
2619                                    limit,
2620                                },
2621                                line: stage_line,
2622                            };
2623                            result = self.pipe_forward_apply(result, stage, stage_line)?;
2624                        } else {
2625                            // `name($_-bearing-args)` — parse explicit args, require at
2626                            // least one `$_` placeholder, then wrap as a `>{...}` block
2627                            // so the threaded value binds to `$_` at any position.
2628                            // Examples:
2629                            //   t 10 add2($_, 5) p      → add2(10, 5)
2630                            //   t 10 sub2(20, $_) p     → sub2(20, 10)
2631                            //   t 10 add3($_, 5, 10) p  → add3(10, 5, 10)
2632                            // To pass the threaded value as a sole arg, use bare form:
2633                            //   t 10 add2 p   (not `add2()`)
2634                            self.advance(); // consume `(`
2635                            let mut call_args = Vec::new();
2636                            while !matches!(self.peek(), Token::RParen | Token::Eof) {
2637                                call_args.push(self.parse_assign_expr()?);
2638                                if !self.eat(&Token::Comma) {
2639                                    break;
2640                                }
2641                            }
2642                            self.expect(&Token::RParen)?;
2643                            // Thread-LAST: substitute `result` (the threaded value) DIRECTLY as
2644                            // the last arg of the call — `~>> @arr fold_sum(0)` becomes the
2645                            // straight call `fold_sum(0, @arr)`. Pre-fix this wrapped the call
2646                            // in a CodeRef whose body referenced `$_`, then invoked the coderef
2647                            // with `lhs` as args. With ArrowCall flattening (`@arr` source) `$_`
2648                            // bound to the first array element only — `fold_sum(0, 1)` = 1
2649                            // instead of the sum 91. Inlining avoids the indirection entirely
2650                            // and lets the callee's slurpy `@xs` receive the source as a list.
2651                            //
2652                            // Thread-FIRST keeps the CodeRef-with-$_ path because the convention
2653                            // there is per-item streaming, and the existing macros (`~>` / `~s>`)
2654                            // build on that.
2655                            if self.thread_last_mode
2656                                && !call_args.iter().any(Self::expr_contains_topic_var)
2657                            {
2658                                call_args.push(result);
2659                                result = Expr {
2660                                    kind: ExprKind::FuncCall {
2661                                        name: func_name.clone(),
2662                                        args: call_args,
2663                                    },
2664                                    line: stage_line,
2665                                };
2666                            } else {
2667                                if !call_args.iter().any(Self::expr_contains_topic_var) {
2668                                    let topic = Expr {
2669                                        kind: ExprKind::ScalarVar("_".to_string()),
2670                                        line: stage_line,
2671                                    };
2672                                    call_args.insert(0, topic);
2673                                }
2674                                let call_expr = Expr {
2675                                    kind: ExprKind::FuncCall {
2676                                        name: func_name.clone(),
2677                                        args: call_args,
2678                                    },
2679                                    line: stage_line,
2680                                };
2681                                let code_ref = Expr {
2682                                    kind: ExprKind::CodeRef {
2683                                        params: vec![],
2684                                        body: vec![Statement {
2685                                            label: None,
2686                                            kind: StmtKind::Expression(call_expr),
2687                                            line: stage_line,
2688                                        }],
2689                                    },
2690                                    line: stage_line,
2691                                };
2692                                result = self.pipe_forward_apply(result, code_ref, stage_line)?;
2693                            }
2694                        }
2695                    } else {
2696                        // Bare function name — handle unary builtins specially
2697                        result = self.thread_apply_bare_func(&func_name, result, stage_line)?;
2698                    }
2699                }
2700                // `/pattern/flags` — grep filter (desugar to `grep { /pattern/flags }`)
2701                Token::Regex(ref pattern, ref flags, delim) => {
2702                    let pattern = pattern.clone();
2703                    let flags = flags.clone();
2704                    self.advance();
2705                    result =
2706                        self.thread_regex_grep_stage(result, pattern, flags, delim, stage_line);
2707                }
2708                // Handle `/` that was lexed as Slash (division) because it followed a term.
2709                // In thread stage context, `/pattern/` should be a regex filter.
2710                Token::Slash => {
2711                    self.advance(); // consume opening /
2712
2713                    // Special case: if next token is Ident("m") or similar followed by Regex,
2714                    // the lexer interpreted `/m/` as `/ m/pattern/` where `m/` started a new regex.
2715                    // We need to handle this: the pattern is just "m" (or whatever the ident is).
2716                    if let Token::Ident(ref ident_s) = self.peek().clone() {
2717                        if matches!(ident_s.as_str(), "m" | "s" | "tr" | "y" | "qr")
2718                            && matches!(self.peek_at(1), Token::Regex(..))
2719                        {
2720                            // The `m` (or s/tr/y/qr) is our pattern, the Regex token was misparsed
2721                            self.advance(); // consume the ident
2722                                            // The Token::Regex after it was a misparsed `m/...` - we need to
2723                                            // extract what would have been the closing `/` situation.
2724                                            // Actually, the lexer consumed everything. Let's just use the ident
2725                                            // as the pattern and expect a closing slash.
2726                            if let Token::Regex(ref misparsed_pattern, ref misparsed_flags, _) =
2727                                self.peek().clone()
2728                            {
2729                                // The misparsed regex ate our closing `/`.
2730                                // For `/m/`, lexer saw `m/` and parsed until next `/`, finding nothing or wrong content.
2731                                // Actually for `/m/ less`, after Slash, lexer sees `m`, then `/`,
2732                                // interprets as m// regex start, reads until next `/` (none) -> error.
2733                                // So we shouldn't reach here if there was an error.
2734                                // But if lexer succeeded parsing `m/ less/` as regex, we'd have wrong pattern.
2735                                // This is getting complicated. Let me try a different approach.
2736                                // Just consume the Regex token and issue a warning? No, let's reconstruct.
2737                                // Skip for now and fall through to manual parsing.
2738                                let _ = (misparsed_pattern, misparsed_flags);
2739                            }
2740                        }
2741                    }
2742
2743                    // Manually parse the regex pattern from tokens until we hit another Slash
2744                    let mut pattern = String::new();
2745                    loop {
2746                        match self.peek().clone() {
2747                            Token::Slash => {
2748                                self.advance(); // consume closing /
2749                                break;
2750                            }
2751                            Token::Eof | Token::Semicolon | Token::Newline => {
2752                                return Err(self
2753                                    .syntax_err("Unterminated regex in thread stage", stage_line));
2754                            }
2755                            // Handle case where lexer misparsed m/pattern/ as Ident("m") + Regex
2756                            Token::Regex(ref inner_pattern, ref inner_flags, delim) => {
2757                                // This means `/m/` was lexed as Slash, then `m/` started a regex.
2758                                // The Regex token contains whatever was between the inner `m/` and closing `/`.
2759                                // For `/m/ less`, lexer would fail earlier. For `/m/i`, it might work weirdly.
2760                                // The safest: if we see a Regex token here and pattern is empty or just "m"/"s"/etc,
2761                                // treat the previous ident as the whole pattern and this Regex as misparsed.
2762                                // Actually, let's just prepend the ident we may have seen and use empty pattern.
2763                                // This is a lexer bug workaround.
2764                                if pattern.is_empty()
2765                                    || matches!(pattern.as_str(), "m" | "s" | "tr" | "y" | "qr")
2766                                {
2767                                    // The whole thing was probably `/X/` where X is m/s/tr/y/qr
2768                                    // and lexer misparsed. The Regex token is garbage.
2769                                    // Just use the ident as pattern and ignore this Regex.
2770                                    // But we already advanced past the ident...
2771                                    // This is messy. Let me try a cleaner approach.
2772                                    let _ = (inner_pattern, inner_flags, delim);
2773                                }
2774                                // For now, error out - this case is too complex
2775                                return Err(self.syntax_err(
2776                                    "Complex regex in thread stage - use m/pattern/ syntax instead",
2777                                    stage_line,
2778                                ));
2779                            }
2780                            Token::Ident(ref s) => {
2781                                pattern.push_str(s);
2782                                self.advance();
2783                            }
2784                            Token::Integer(n) => {
2785                                pattern.push_str(&n.to_string());
2786                                self.advance();
2787                            }
2788                            Token::ScalarVar(ref v) => {
2789                                pattern.push('$');
2790                                pattern.push_str(v);
2791                                self.advance();
2792                            }
2793                            Token::Dot => {
2794                                pattern.push('.');
2795                                self.advance();
2796                            }
2797                            Token::Star => {
2798                                pattern.push('*');
2799                                self.advance();
2800                            }
2801                            Token::Plus => {
2802                                pattern.push('+');
2803                                self.advance();
2804                            }
2805                            Token::Question => {
2806                                pattern.push('?');
2807                                self.advance();
2808                            }
2809                            Token::LParen => {
2810                                pattern.push('(');
2811                                self.advance();
2812                            }
2813                            Token::RParen => {
2814                                pattern.push(')');
2815                                self.advance();
2816                            }
2817                            Token::LBracket => {
2818                                pattern.push('[');
2819                                self.advance();
2820                            }
2821                            Token::RBracket => {
2822                                pattern.push(']');
2823                                self.advance();
2824                            }
2825                            Token::Backslash => {
2826                                pattern.push('\\');
2827                                self.advance();
2828                            }
2829                            Token::BitOr => {
2830                                pattern.push('|');
2831                                self.advance();
2832                            }
2833                            Token::Power => {
2834                                pattern.push_str("**");
2835                                self.advance();
2836                            }
2837                            Token::BitXor => {
2838                                pattern.push('^');
2839                                self.advance();
2840                            }
2841                            Token::Minus => {
2842                                pattern.push('-');
2843                                self.advance();
2844                            }
2845                            _ => {
2846                                return Err(self.syntax_err(
2847                                    format!("Unexpected token in regex pattern: {:?}", self.peek()),
2848                                    stage_line,
2849                                ));
2850                            }
2851                        }
2852                    }
2853                    // Parse optional flags (sequence of letters after closing /)
2854                    // Be careful: single letters like 'e' could be regex flags OR thread
2855                    // stages like `fore`/`e`. If followed by `{`, it's a stage, not a flag.
2856                    let mut flags = String::new();
2857                    if let Token::Ident(ref s) = self.peek().clone() {
2858                        let is_flag_only =
2859                            s.chars().all(|c| "gimsxecor".contains(c)) && s.len() <= 6;
2860                        let followed_by_brace = matches!(self.peek_at(1), Token::LBrace);
2861                        if is_flag_only && !followed_by_brace {
2862                            flags.push_str(s);
2863                            self.advance();
2864                        }
2865                    }
2866                    result = self.thread_regex_grep_stage(result, pattern, flags, '/', stage_line);
2867                }
2868                tok => {
2869                    return Err(self.syntax_err(
2870                        format!(
2871                            "thread: expected stage (ident, fn {{}}, s///, tr///, or /re/), got {:?}",
2872                            tok
2873                        ),
2874                        stage_line,
2875                    ));
2876                }
2877            };
2878            last_stage_end_line = self.prev_line();
2879            // Parallel mode: each iteration of the loop has produced a
2880            // stage expression where `$_` is the input. Push it into the
2881            // collector and reset `result` to `$_` so the next stage
2882            // parses against a fresh topic.
2883            if let Some(stages) = parallel_collector.as_mut() {
2884                let stage_body = std::mem::replace(
2885                    &mut result,
2886                    Expr {
2887                        kind: ExprKind::ScalarVar("_".into()),
2888                        line: stage_line,
2889                    },
2890                );
2891                stages.push(stage_body);
2892            }
2893        }
2894
2895        // Restore thread-last mode
2896        self.thread_last_mode = saved_thread_last;
2897
2898        // Parallel mode: lower to `_thread_par_run(source_expr, [stage_closures], thread_last)`.
2899        // The runtime treats the source value as a list-of-items and feeds
2900        // each item into stage 1 via a bounded channel. Each stage runs in
2901        // its own worker; stages are wrapped as `fn { body }` closures so
2902        // the runtime sets `$_` to the current item before invoking.
2903        if let Some(stages) = parallel_collector {
2904            let source_expr = source_for_par.unwrap_or(result);
2905            if stages.is_empty() {
2906                return Err(self.syntax_err(
2907                    "~p> / ~p>> require at least one stage after the source",
2908                    _line,
2909                ));
2910            }
2911            // Wrap each stage body in `[ ... ]` (an ArrayRef) so list-returning
2912            // ops like `map`/`grep` propagate their full output instead of
2913            // collapsing to a scalar count. The runtime worker peels one
2914            // level of array-ref via `map_flatten_outputs(true)` so each
2915            // element flows downstream as its own item.
2916            let stage_closures: Vec<Expr> = stages
2917                .drain(..)
2918                .map(|body| {
2919                    let body_line = body.line;
2920                    let wrapped = Expr {
2921                        kind: ExprKind::ArrayRef(vec![body]),
2922                        line: body_line,
2923                    };
2924                    Expr {
2925                        kind: ExprKind::CodeRef {
2926                            params: vec![],
2927                            body: vec![Statement {
2928                                label: None,
2929                                kind: StmtKind::Expression(wrapped),
2930                                line: body_line,
2931                            }],
2932                        },
2933                        line: body_line,
2934                    }
2935                })
2936                .collect();
2937            let stages_arr = Expr {
2938                kind: ExprKind::ArrayRef(stage_closures),
2939                line: _line,
2940            };
2941            let thread_last_flag = Expr {
2942                kind: ExprKind::Integer(if thread_last { 1 } else { 0 }),
2943                line: _line,
2944            };
2945            // Argument order: stages, thread_last, source... — source
2946            // is LAST so its list expansion (`(1,2,3)`, `@a`, ranges)
2947            // lands in the variadic tail. Pre-fix the source was first
2948            // and any list source flattened across the slot, breaking
2949            // the `args.len() == 3` invariant in `_thread_par_run` and
2950            // hitting "expected 3 args" for `~s> (1,2,3) sum` etc.
2951            return Ok(Expr {
2952                kind: ExprKind::FuncCall {
2953                    name: "_thread_par_run".into(),
2954                    args: vec![stages_arr, thread_last_flag, source_expr],
2955                },
2956                line: _line,
2957            });
2958        }
2959
2960        if pipe_rhs_wrap {
2961            // Wrap as `fn { …stages threaded from $_[0]… }` so the outer
2962            // `pipe_forward_apply` can invoke it with `lhs` as the arg.
2963            let body_line = result.line;
2964            return Ok(Expr {
2965                kind: ExprKind::CodeRef {
2966                    params: vec![],
2967                    body: vec![Statement {
2968                        label: None,
2969                        kind: StmtKind::Expression(result),
2970                        line: body_line,
2971                    }],
2972                },
2973                line: _line,
2974            });
2975        }
2976        Ok(result)
2977    }
2978
2979    /// Build a grep filter stage from a regex pattern for the thread macro.
2980    fn thread_regex_grep_stage(
2981        &self,
2982        list: Expr,
2983        pattern: String,
2984        flags: String,
2985        delim: char,
2986        line: usize,
2987    ) -> Expr {
2988        let topic = Expr {
2989            kind: ExprKind::ScalarVar("_".to_string()),
2990            line,
2991        };
2992        let match_expr = Expr {
2993            kind: ExprKind::Match {
2994                expr: Box::new(topic),
2995                pattern,
2996                flags,
2997                scalar_g: false,
2998                delim,
2999            },
3000            line,
3001        };
3002        let block = vec![Statement {
3003            label: None,
3004            kind: StmtKind::Expression(match_expr),
3005            line,
3006        }];
3007        Expr {
3008            kind: ExprKind::GrepExpr {
3009                block,
3010                list: Box::new(list),
3011                keyword: crate::ast::GrepBuiltinKeyword::Grep,
3012            },
3013            line,
3014        }
3015    }
3016
3017    /// Check whether an expression contains a `$_` reference anywhere in its sub-tree.
3018    /// Used by the thread macro to validate `name(args)` call-stages: the threaded
3019    /// value is bound to `$_` via a wrapping CodeRef, so at least one `$_` placeholder
3020    /// must appear in the args, otherwise the threaded value is silently dropped.
3021    ///
3022    /// Implementation uses Rust's `Debug` to serialize the entire sub-tree once and
3023    /// scan for the canonical `ScalarVar("_")` representation. This avoids a
3024    /// per-variant walker that would need to be updated whenever new `ExprKind`
3025    /// variants are added (and would silently miss any it forgot to handle).
3026    /// Parse-time perf is non-critical and the AST is small at this scope.
3027    fn expr_contains_topic_var(e: &Expr) -> bool {
3028        format!("{:?}", e).contains("ScalarVar(\"_\")")
3029    }
3030
3031    /// Walk tokens in `[rhs_start, rhs_end)` looking for a *free* bare
3032    /// topic-slot index (one in `self.bare_positional_indices` at
3033    /// brace-depth 0 within the RHS). Any `_` inside `{ ... }` is
3034    /// considered bound by whatever defines that block (closure,
3035    /// hash literal, map/grep/sort/match arm) and doesn't count.
3036    ///
3037    /// Special case: when the RHS *starts* with a thread-macro intro
3038    /// token (`~>`, `->>`, `~>>`, `~p>`, `~s>`, `~d>` and `-Last`
3039    /// variants), the macro itself binds `_` for all stage expressions
3040    /// — only the immediate input expression after the arrow can
3041    /// trigger the wrap. `->> 10 div(_, 2)` is eager (input = `10`,
3042    /// `_` is the threaded placeholder), but `~> _ uc` wraps (input
3043    /// is the free bare `_`).
3044    ///
3045    /// Drives the implicit-coderef sugar in `parse_my_our_local`.
3046    fn rhs_has_free_bare_topic_slot(&self, rhs_start: usize, rhs_end: usize) -> bool {
3047        let end = rhs_end.min(self.tokens.len());
3048        if rhs_start < end && Self::is_thread_arrow(&self.tokens[rhs_start].0) {
3049            // Only the input expression (first token after the arrow)
3050            // can trigger the wrap; everything else is a stage and
3051            // its bare `_` is the threaded placeholder.
3052            let input = rhs_start + 1;
3053            return input < end && self.bare_positional_indices.contains(&input);
3054        }
3055        let mut brace_depth = 0i32;
3056        for i in rhs_start..end {
3057            if brace_depth == 0 && self.bare_positional_indices.contains(&i) {
3058                return true;
3059            }
3060            match &self.tokens[i].0 {
3061                Token::LBrace | Token::ArrowBrace => brace_depth += 1,
3062                Token::RBrace => brace_depth -= 1,
3063                _ => {}
3064            }
3065        }
3066        false
3067    }
3068
3069    fn is_thread_arrow(tok: &Token) -> bool {
3070        matches!(
3071            tok,
3072            Token::ThreadArrow
3073                | Token::ThreadArrowLast
3074                | Token::ThreadArrowStream
3075                | Token::ThreadArrowStreamLast
3076                | Token::ThreadArrowPar
3077                | Token::ThreadArrowParLast
3078                | Token::ThreadArrowDist
3079                | Token::ThreadArrowDistLast
3080        )
3081    }
3082
3083    /// Apply a bare function name in thread context, handling unary builtins specially.
3084    fn thread_apply_bare_func(&self, name: &str, arg: Expr, line: usize) -> StrykeResult<Expr> {
3085        let kind = match name {
3086            // String functions
3087            "uc" => ExprKind::Uc(Box::new(arg)),
3088            "lc" => ExprKind::Lc(Box::new(arg)),
3089            "ucfirst" | "ufc" => ExprKind::Ucfirst(Box::new(arg)),
3090            "lcfirst" | "lfc" => ExprKind::Lcfirst(Box::new(arg)),
3091            "fc" => ExprKind::Fc(Box::new(arg)),
3092            "chomp" => ExprKind::Chomp(Box::new(arg)),
3093            "chop" => ExprKind::Chop(Box::new(arg)),
3094            "length" => ExprKind::Length(Box::new(arg)),
3095            "len" | "cnt" => ExprKind::FuncCall {
3096                name: "count".to_string(),
3097                args: vec![arg],
3098            },
3099            "quotemeta" | "qm" => ExprKind::Quotemeta(Box::new(arg)),
3100            // Numeric functions
3101            "abs" => ExprKind::Abs(Box::new(arg)),
3102            "int" => ExprKind::Int(Box::new(arg)),
3103            "sqrt" | "sq" => ExprKind::Sqrt(Box::new(arg)),
3104            "sin" => ExprKind::Sin(Box::new(arg)),
3105            "cos" => ExprKind::Cos(Box::new(arg)),
3106            "exp" => ExprKind::Exp(Box::new(arg)),
3107            "log" => ExprKind::Log(Box::new(arg)),
3108            "hex" => ExprKind::Hex(Box::new(arg)),
3109            "oct" => ExprKind::Oct(Box::new(arg)),
3110            "chr" => ExprKind::Chr(Box::new(arg)),
3111            "ord" => ExprKind::Ord(Box::new(arg)),
3112            "rand" => ExprKind::Rand(Some(Box::new(arg))),
3113            "srand" => ExprKind::Srand(Some(Box::new(arg))),
3114            // Type/ref functions
3115            "defined" | "def" => ExprKind::Defined(Box::new(arg)),
3116            "ref" => ExprKind::Ref(Box::new(arg)),
3117            "scalar" => {
3118                if crate::no_interop_mode() {
3119                    return Err(self.syntax_err(
3120                        "stryke uses `len` (also `cnt` / `count`) instead of `scalar` (--no-interop)",
3121                        line,
3122                    ));
3123                }
3124                ExprKind::ScalarContext(Box::new(arg))
3125            }
3126            // Array/hash functions
3127            "keys" => ExprKind::Keys(Box::new(arg)),
3128            "values" => ExprKind::Values(Box::new(arg)),
3129            "each" => ExprKind::Each(Box::new(arg)),
3130            "pop" => ExprKind::Pop(Box::new(arg)),
3131            "shift" => ExprKind::Shift(Box::new(arg)),
3132            "reverse" => {
3133                if crate::no_interop_mode() {
3134                    return Err(self.syntax_err(
3135                        "stryke uses `rev` instead of `reverse` (--no-interop)",
3136                        line,
3137                    ));
3138                }
3139                ExprKind::ReverseExpr(Box::new(arg))
3140            }
3141            "reversed" | "rv" | "rev" => ExprKind::Rev(Box::new(arg)),
3142            "sort" | "so" => ExprKind::SortExpr {
3143                cmp: None,
3144                list: Box::new(arg),
3145            },
3146            "psort" => ExprKind::PSortExpr {
3147                cmp: None,
3148                list: Box::new(arg),
3149                progress: None,
3150            },
3151            "uniq" | "distinct" | "uq" => ExprKind::FuncCall {
3152                name: "uniq".to_string(),
3153                args: vec![arg],
3154            },
3155            "trim" | "tm" => ExprKind::FuncCall {
3156                name: "trim".to_string(),
3157                args: vec![arg],
3158            },
3159            "flatten" | "fl" => ExprKind::FuncCall {
3160                name: "flatten".to_string(),
3161                args: vec![arg],
3162            },
3163            "compact" | "cpt" => ExprKind::FuncCall {
3164                name: "compact".to_string(),
3165                args: vec![arg],
3166            },
3167            "shuffle" | "shuf" => ExprKind::FuncCall {
3168                name: "shuffle".to_string(),
3169                args: vec![arg],
3170            },
3171            "frequencies" | "freq" | "frq" => ExprKind::FuncCall {
3172                name: "frequencies".to_string(),
3173                args: vec![arg],
3174            },
3175            "pfrequencies" | "pfreq" | "pfrq" => ExprKind::FuncCall {
3176                name: "pfrequencies".to_string(),
3177                args: vec![arg],
3178            },
3179            "dedup" | "dup" => ExprKind::FuncCall {
3180                name: "dedup".to_string(),
3181                args: vec![arg],
3182            },
3183            "enumerate" | "en" => ExprKind::FuncCall {
3184                name: "enumerate".to_string(),
3185                args: vec![arg],
3186            },
3187            "lines" | "ln" => ExprKind::FuncCall {
3188                name: "lines".to_string(),
3189                args: vec![arg],
3190            },
3191            "words" | "wd" => ExprKind::FuncCall {
3192                name: "words".to_string(),
3193                args: vec![arg],
3194            },
3195            "chars" | "ch" => ExprKind::FuncCall {
3196                name: "chars".to_string(),
3197                args: vec![arg],
3198            },
3199            "digits" | "dg" => ExprKind::FuncCall {
3200                name: "digits".to_string(),
3201                args: vec![arg],
3202            },
3203            "letters" | "lts" => ExprKind::FuncCall {
3204                name: "letters".to_string(),
3205                args: vec![arg],
3206            },
3207            "letters_uc" => ExprKind::FuncCall {
3208                name: "letters_uc".to_string(),
3209                args: vec![arg],
3210            },
3211            "letters_lc" => ExprKind::FuncCall {
3212                name: "letters_lc".to_string(),
3213                args: vec![arg],
3214            },
3215            "punctuation" | "punct" => ExprKind::FuncCall {
3216                name: "punctuation".to_string(),
3217                args: vec![arg],
3218            },
3219            "sentences" | "sents" => ExprKind::FuncCall {
3220                name: "sentences".to_string(),
3221                args: vec![arg],
3222            },
3223            "paragraphs" | "paras" => ExprKind::FuncCall {
3224                name: "paragraphs".to_string(),
3225                args: vec![arg],
3226            },
3227            "sections" | "sects" => ExprKind::FuncCall {
3228                name: "sections".to_string(),
3229                args: vec![arg],
3230            },
3231            "numbers" | "nums" => ExprKind::FuncCall {
3232                name: "numbers".to_string(),
3233                args: vec![arg],
3234            },
3235            "graphemes" | "grs" => ExprKind::FuncCall {
3236                name: "graphemes".to_string(),
3237                args: vec![arg],
3238            },
3239            "columns" | "cols" => ExprKind::FuncCall {
3240                name: "columns".to_string(),
3241                args: vec![arg],
3242            },
3243            // File functions
3244            "slurp" | "sl" => ExprKind::Slurp(Box::new(arg)),
3245            "swallow" | "swa" => ExprKind::Swallow(Box::new(arg)),
3246            "ingest" | "ing" => ExprKind::Ingest(Box::new(arg)),
3247            "burp" => ExprKind::Burp(Box::new(arg)),
3248            "god" => ExprKind::God(Box::new(arg)),
3249            "glob" => ExprKind::Glob(vec![arg]),
3250            "chdir" => ExprKind::Chdir(Box::new(arg)),
3251            "stat" => ExprKind::Stat(Box::new(arg)),
3252            "lstat" => ExprKind::Lstat(Box::new(arg)),
3253            "readlink" => ExprKind::Readlink(Box::new(arg)),
3254            "readdir" => ExprKind::Readdir(Box::new(arg)),
3255            "close" => ExprKind::Close(Box::new(arg)),
3256            "basename" | "bn" => ExprKind::FuncCall {
3257                name: "basename".to_string(),
3258                args: vec![arg],
3259            },
3260            "dirname" | "dn" => ExprKind::FuncCall {
3261                name: "dirname".to_string(),
3262                args: vec![arg],
3263            },
3264            "realpath" | "rp" => ExprKind::FuncCall {
3265                name: "realpath".to_string(),
3266                args: vec![arg],
3267            },
3268            "which" | "wh" => ExprKind::FuncCall {
3269                name: "which".to_string(),
3270                args: vec![arg],
3271            },
3272            // Other
3273            "eval" => ExprKind::Eval(Box::new(arg)),
3274            "require" => ExprKind::Require(Box::new(arg)),
3275            "study" => ExprKind::Study(Box::new(arg)),
3276            // Case conversion
3277            "snake_case" | "sc" => ExprKind::FuncCall {
3278                name: "snake_case".to_string(),
3279                args: vec![arg],
3280            },
3281            "camel_case" | "cc" => ExprKind::FuncCall {
3282                name: "camel_case".to_string(),
3283                args: vec![arg],
3284            },
3285            "kebab_case" | "kc" => ExprKind::FuncCall {
3286                name: "kebab_case".to_string(),
3287                args: vec![arg],
3288            },
3289            // Serialization
3290            "to_json" | "tj" => ExprKind::FuncCall {
3291                name: "to_json".to_string(),
3292                args: vec![arg],
3293            },
3294            "to_yaml" | "ty" => ExprKind::FuncCall {
3295                name: "to_yaml".to_string(),
3296                args: vec![arg],
3297            },
3298            "to_toml" | "tt" => ExprKind::FuncCall {
3299                name: "to_toml".to_string(),
3300                args: vec![arg],
3301            },
3302            "to_csv" | "tc" => ExprKind::FuncCall {
3303                name: "to_csv".to_string(),
3304                args: vec![arg],
3305            },
3306            "to_xml" | "tx" => ExprKind::FuncCall {
3307                name: "to_xml".to_string(),
3308                args: vec![arg],
3309            },
3310            "to_html" | "th" => ExprKind::FuncCall {
3311                name: "to_html".to_string(),
3312                args: vec![arg],
3313            },
3314            "to_markdown" | "to_md" | "tmd" => ExprKind::FuncCall {
3315                name: "to_markdown".to_string(),
3316                args: vec![arg],
3317            },
3318            "xopen" | "xo" => ExprKind::FuncCall {
3319                name: "xopen".to_string(),
3320                args: vec![arg],
3321            },
3322            "clip" | "clipboard" | "pbcopy" => ExprKind::FuncCall {
3323                name: "clip".to_string(),
3324                args: vec![arg],
3325            },
3326            "to_table" | "table" | "tbl" => ExprKind::FuncCall {
3327                name: "to_table".to_string(),
3328                args: vec![arg],
3329            },
3330            "sparkline" | "spark" => ExprKind::FuncCall {
3331                name: "sparkline".to_string(),
3332                args: vec![arg],
3333            },
3334            "bar_chart" | "bars" => ExprKind::FuncCall {
3335                name: "bar_chart".to_string(),
3336                args: vec![arg],
3337            },
3338            "flame" | "flamechart" => ExprKind::FuncCall {
3339                name: "flame".to_string(),
3340                args: vec![arg],
3341            },
3342            "ddump" | "dd" => ExprKind::FuncCall {
3343                name: "ddump".to_string(),
3344                args: vec![arg],
3345            },
3346            "say" => {
3347                if crate::no_interop_mode() {
3348                    return Err(
3349                        self.syntax_err("stryke uses `p` instead of `say` (--no-interop)", line)
3350                    );
3351                }
3352                ExprKind::Say {
3353                    handle: None,
3354                    args: vec![arg],
3355                }
3356            }
3357            "p" => ExprKind::Say {
3358                handle: None,
3359                args: vec![arg],
3360            },
3361            "print" => ExprKind::Print {
3362                handle: None,
3363                args: vec![arg],
3364            },
3365            "warn" => ExprKind::Warn(vec![arg]),
3366            "die" => ExprKind::Die(vec![arg]),
3367            "stringify" | "str" => ExprKind::FuncCall {
3368                name: "stringify".to_string(),
3369                args: vec![arg],
3370            },
3371            "json_decode" | "jd" => ExprKind::FuncCall {
3372                name: "json_decode".to_string(),
3373                args: vec![arg],
3374            },
3375            "yaml_decode" | "yd" => ExprKind::FuncCall {
3376                name: "yaml_decode".to_string(),
3377                args: vec![arg],
3378            },
3379            "toml_decode" | "td" => ExprKind::FuncCall {
3380                name: "toml_decode".to_string(),
3381                args: vec![arg],
3382            },
3383            "xml_decode" | "xd" => ExprKind::FuncCall {
3384                name: "xml_decode".to_string(),
3385                args: vec![arg],
3386            },
3387            "json_encode" | "je" => ExprKind::FuncCall {
3388                name: "json_encode".to_string(),
3389                args: vec![arg],
3390            },
3391            "yaml_encode" | "ye" => ExprKind::FuncCall {
3392                name: "yaml_encode".to_string(),
3393                args: vec![arg],
3394            },
3395            "toml_encode" | "te" => ExprKind::FuncCall {
3396                name: "toml_encode".to_string(),
3397                args: vec![arg],
3398            },
3399            "xml_encode" | "xe" => ExprKind::FuncCall {
3400                name: "xml_encode".to_string(),
3401                args: vec![arg],
3402            },
3403            // Encoding
3404            "base64_encode" | "b64e" => ExprKind::FuncCall {
3405                name: "base64_encode".to_string(),
3406                args: vec![arg],
3407            },
3408            "base64_decode" | "b64d" => ExprKind::FuncCall {
3409                name: "base64_decode".to_string(),
3410                args: vec![arg],
3411            },
3412            "hex_encode" | "hxe" => ExprKind::FuncCall {
3413                name: "hex_encode".to_string(),
3414                args: vec![arg],
3415            },
3416            "hex_decode" | "hxd" => ExprKind::FuncCall {
3417                name: "hex_decode".to_string(),
3418                args: vec![arg],
3419            },
3420            "url_encode" | "uri_escape" | "ue" => ExprKind::FuncCall {
3421                name: "url_encode".to_string(),
3422                args: vec![arg],
3423            },
3424            "url_decode" | "uri_unescape" | "ud" => ExprKind::FuncCall {
3425                name: "url_decode".to_string(),
3426                args: vec![arg],
3427            },
3428            "gzip" | "gz" => ExprKind::FuncCall {
3429                name: "gzip".to_string(),
3430                args: vec![arg],
3431            },
3432            "gunzip" | "ugz" => ExprKind::FuncCall {
3433                name: "gunzip".to_string(),
3434                args: vec![arg],
3435            },
3436            "zstd" | "zst" => ExprKind::FuncCall {
3437                name: "zstd".to_string(),
3438                args: vec![arg],
3439            },
3440            "zstd_decode" | "uzst" => ExprKind::FuncCall {
3441                name: "zstd_decode".to_string(),
3442                args: vec![arg],
3443            },
3444            // Crypto
3445            "sha256" | "s256" => ExprKind::FuncCall {
3446                name: "sha256".to_string(),
3447                args: vec![arg],
3448            },
3449            "sha1" | "s1" => ExprKind::FuncCall {
3450                name: "sha1".to_string(),
3451                args: vec![arg],
3452            },
3453            "md5" | "m5" => ExprKind::FuncCall {
3454                name: "md5".to_string(),
3455                args: vec![arg],
3456            },
3457            "uuid" | "uid" => ExprKind::FuncCall {
3458                name: "uuid".to_string(),
3459                args: vec![arg],
3460            },
3461            // Datetime
3462            "datetime_utc" | "utc" => ExprKind::FuncCall {
3463                name: "datetime_utc".to_string(),
3464                args: vec![arg],
3465            },
3466            // Bare `e` / `fore` / `ep` in thread context: foreach element, say it.
3467            // `t @list e` == `@list |> e p` == `@list |> ep` == foreach (@list) { say }
3468            "e" | "fore" | "ep" => ExprKind::ForEachExpr {
3469                block: vec![Statement {
3470                    label: None,
3471                    kind: StmtKind::Expression(Expr {
3472                        kind: ExprKind::Say {
3473                            handle: None,
3474                            args: vec![Expr {
3475                                kind: ExprKind::ScalarVar("_".into()),
3476                                line,
3477                            }],
3478                        },
3479                        line,
3480                    }),
3481                    line,
3482                }],
3483                list: Box::new(arg),
3484            },
3485            // Default: generic function call
3486            _ => ExprKind::FuncCall {
3487                name: name.to_string(),
3488                args: vec![arg],
3489            },
3490        };
3491        Ok(Expr { kind, line })
3492    }
3493
3494    /// Parse a thread stage that has a block: `map { }`, `filter { }`, `sort { }`, etc.
3495    /// In thread context, we only parse the block - the list comes from the piped result.
3496    fn parse_thread_stage_with_block(&mut self, name: &str, line: usize) -> StrykeResult<Expr> {
3497        let block = self.parse_block()?;
3498        // Use a placeholder for the list - pipe_forward_apply will replace it
3499        let placeholder = self.pipe_placeholder_list(line);
3500
3501        match name {
3502            "map" | "flat_map" | "maps" | "flat_maps" => {
3503                let flatten_array_refs = matches!(name, "flat_map" | "flat_maps");
3504                let stream = matches!(name, "maps" | "flat_maps");
3505                Ok(Expr {
3506                    kind: ExprKind::MapExpr {
3507                        block,
3508                        list: Box::new(placeholder),
3509                        flatten_array_refs,
3510                        stream,
3511                    },
3512                    line,
3513                })
3514            }
3515            "grep" | "greps" | "filter" | "fi" | "find_all" | "gr" => {
3516                let keyword = match name {
3517                    "grep" | "gr" => crate::ast::GrepBuiltinKeyword::Grep,
3518                    "greps" => crate::ast::GrepBuiltinKeyword::Greps,
3519                    "filter" | "fi" => crate::ast::GrepBuiltinKeyword::Filter,
3520                    "find_all" => crate::ast::GrepBuiltinKeyword::FindAll,
3521                    _ => unreachable!(),
3522                };
3523                Ok(Expr {
3524                    kind: ExprKind::GrepExpr {
3525                        block,
3526                        list: Box::new(placeholder),
3527                        keyword,
3528                    },
3529                    line,
3530                })
3531            }
3532            "sort" | "so" => Ok(Expr {
3533                kind: ExprKind::SortExpr {
3534                    cmp: Some(SortComparator::Block(block)),
3535                    list: Box::new(placeholder),
3536                },
3537                line,
3538            }),
3539            "reduce" | "rd" => Ok(Expr {
3540                kind: ExprKind::ReduceExpr {
3541                    block,
3542                    list: Box::new(placeholder),
3543                },
3544                line,
3545            }),
3546            "fore" | "e" | "ep" => Ok(Expr {
3547                kind: ExprKind::ForEachExpr {
3548                    block,
3549                    list: Box::new(placeholder),
3550                },
3551                line,
3552            }),
3553            "par" => Ok(Expr {
3554                kind: ExprKind::ParExpr {
3555                    block,
3556                    list: Box::new(placeholder),
3557                },
3558                line,
3559            }),
3560            "pmap" | "pflat_map" | "pmaps" | "pflat_maps" => Ok(Expr {
3561                kind: ExprKind::PMapExpr {
3562                    block,
3563                    list: Box::new(placeholder),
3564                    progress: None,
3565                    flat_outputs: name == "pflat_map" || name == "pflat_maps",
3566                    on_cluster: None,
3567                    stream: name == "pmaps" || name == "pflat_maps",
3568                },
3569                line,
3570            }),
3571            "pgrep" | "pgreps" => Ok(Expr {
3572                kind: ExprKind::PGrepExpr {
3573                    block,
3574                    list: Box::new(placeholder),
3575                    progress: None,
3576                    stream: name == "pgreps",
3577                },
3578                line,
3579            }),
3580            "pfor" | "pforeach" => Ok(Expr {
3581                kind: ExprKind::PForExpr {
3582                    block,
3583                    list: Box::new(placeholder),
3584                    progress: None,
3585                },
3586                line,
3587            }),
3588            "preduce" => Ok(Expr {
3589                kind: ExprKind::PReduceExpr {
3590                    block,
3591                    list: Box::new(placeholder),
3592                    progress: None,
3593                },
3594                line,
3595            }),
3596            "pcache" => Ok(Expr {
3597                kind: ExprKind::PcacheExpr {
3598                    block,
3599                    list: Box::new(placeholder),
3600                    progress: None,
3601                },
3602                line,
3603            }),
3604            "psort" => Ok(Expr {
3605                kind: ExprKind::PSortExpr {
3606                    cmp: Some(block),
3607                    list: Box::new(placeholder),
3608                    progress: None,
3609                },
3610                line,
3611            }),
3612            _ => {
3613                // Generic: parse block and treat as FuncCall with code ref arg.
3614                // Block-then-list pipe builtins (`pfirst`, `any`, `take_while`, etc.)
3615                // need the threaded list slot pre-allocated at args[1] so
3616                // `pipe_forward_apply` can substitute the lhs there (parser.rs:5823).
3617                // For everything else, the generic pipe-forward arm prepends or
3618                // appends the lhs based on `thread_last_mode`.
3619                let code_ref = Expr {
3620                    kind: ExprKind::CodeRef {
3621                        params: vec![],
3622                        body: block,
3623                    },
3624                    line,
3625                };
3626                let args = if Self::is_block_then_list_pipe_builtin(name) {
3627                    vec![code_ref, placeholder]
3628                } else {
3629                    vec![code_ref]
3630                };
3631                Ok(Expr {
3632                    kind: ExprKind::FuncCall {
3633                        name: name.to_string(),
3634                        args,
3635                    },
3636                    line,
3637                })
3638            }
3639        }
3640    }
3641
3642    /// `tie %hash | tie @arr | tie $x , 'Class', ...args`
3643    fn parse_tie_stmt(&mut self) -> StrykeResult<Statement> {
3644        let line = self.peek_line();
3645        self.advance(); // tie
3646                        // `tie my $x, Class` and `tie our $x, Class` — common Perl idiom.
3647                        // Desugar by emitting an implicit `my $x` (or `our $x`) declaration
3648                        // before the tie. The tie target then references the just-declared
3649                        // variable. Without this, `tie my $x, Class, ARGS` errors with
3650                        // "tie expects $scalar, @array, or %hash, got Ident(\"my\")".
3651        let mut implicit_decl: Option<Statement> = None;
3652        if let Token::Ident(kw) = self.peek().clone() {
3653            if is_lexical_or_our_decl(&kw) {
3654                let kw_line = self.peek_line();
3655                self.advance(); // my / var / val / our
3656                                // Read the variable being declared (must be Scalar/Array/Hash).
3657                let (decl_sigil, decl_name) = match self.peek().clone() {
3658                    Token::ScalarVar(s) => (Sigil::Scalar, s),
3659                    Token::ArrayVar(a) => (Sigil::Array, a),
3660                    Token::HashVar(h) => (Sigil::Hash, h),
3661                    tok => {
3662                        return Err(self.syntax_err(
3663                            format!("expected variable after `tie {}`, got {:?}", kw, tok),
3664                            self.peek_line(),
3665                        ));
3666                    }
3667                };
3668                let decls = vec![VarDecl {
3669                    sigil: decl_sigil,
3670                    name: decl_name.clone(),
3671                    initializer: None,
3672                    // `val` makes the implicit decl frozen (matches the
3673                    // var/val statement-form arms in parse_statement).
3674                    frozen: is_frozen_decl(&kw),
3675                    type_annotation: None,
3676                    list_context: false,
3677                }];
3678                implicit_decl = Some(Statement {
3679                    label: None,
3680                    kind: if is_lexical_decl(&kw) {
3681                        StmtKind::My(decls)
3682                    } else {
3683                        StmtKind::Our(decls)
3684                    },
3685                    line: kw_line,
3686                });
3687                // Don't advance past the variable token here — fall through
3688                // to the existing match below so `target` is built from the
3689                // same token (the ScalarVar/ArrayVar/HashVar path will
3690                // advance and capture the name).
3691            }
3692        }
3693        let target = match self.peek().clone() {
3694            Token::HashVar(h) => {
3695                self.advance();
3696                TieTarget::Hash(h)
3697            }
3698            Token::ArrayVar(a) => {
3699                self.advance();
3700                TieTarget::Array(a)
3701            }
3702            Token::ScalarVar(s) => {
3703                self.advance();
3704                TieTarget::Scalar(s)
3705            }
3706            tok => {
3707                return Err(self.syntax_err(
3708                    format!("tie expects $scalar, @array, or %hash, got {:?}", tok),
3709                    self.peek_line(),
3710                ));
3711            }
3712        };
3713        self.expect(&Token::Comma)?;
3714        let class = self.parse_assign_expr()?;
3715        let mut args = Vec::new();
3716        while self.eat(&Token::Comma) {
3717            if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof) {
3718                break;
3719            }
3720            args.push(self.parse_assign_expr()?);
3721        }
3722        self.eat(&Token::Semicolon);
3723        let tie_stmt = Statement {
3724            label: None,
3725            kind: StmtKind::Tie {
3726                target,
3727                class,
3728                args,
3729            },
3730            line,
3731        };
3732        if let Some(decl) = implicit_decl {
3733            // Wrap the implicit `my $x` + tie in a `StmtGroup` so they live
3734            // in the same lexical block (the parser desugar is invisible to
3735            // callers; `StmtGroup` runs statements in order without a frame
3736            // push).
3737            Ok(Statement {
3738                label: None,
3739                kind: StmtKind::StmtGroup(vec![decl, tie_stmt]),
3740                line,
3741            })
3742        } else {
3743            Ok(tie_stmt)
3744        }
3745    }
3746
3747    /// `given (EXPR) { ... }`
3748    fn parse_given(&mut self) -> StrykeResult<Statement> {
3749        let line = self.peek_line();
3750        self.advance();
3751        self.expect(&Token::LParen)?;
3752        let topic = self.parse_expression()?;
3753        self.expect(&Token::RParen)?;
3754        let body = self.parse_block()?;
3755        self.eat(&Token::Semicolon);
3756        Ok(Statement {
3757            label: None,
3758            kind: StmtKind::Given { topic, body },
3759            line,
3760        })
3761    }
3762
3763    /// `when (COND) { ... }` — only meaningful inside `given`
3764    fn parse_when_stmt(&mut self) -> StrykeResult<Statement> {
3765        let line = self.peek_line();
3766        self.advance();
3767        self.expect(&Token::LParen)?;
3768        let cond = self.parse_expression()?;
3769        self.expect(&Token::RParen)?;
3770        let body = self.parse_block()?;
3771        self.eat(&Token::Semicolon);
3772        Ok(Statement {
3773            label: None,
3774            kind: StmtKind::When { cond, body },
3775            line,
3776        })
3777    }
3778
3779    /// `default { ... }` — only meaningful inside `given`
3780    fn parse_default_stmt(&mut self) -> StrykeResult<Statement> {
3781        let line = self.peek_line();
3782        self.advance();
3783        let body = self.parse_block()?;
3784        self.eat(&Token::Semicolon);
3785        Ok(Statement {
3786            label: None,
3787            kind: StmtKind::DefaultCase { body },
3788            line,
3789        })
3790    }
3791
3792    /// `cond { EXPR => RESULT, ..., default => RESULT }`
3793    ///
3794    /// Desugars to an if/elsif/else chain at parse time.
3795    /// Each arm is `condition => { body }` or `condition => expr`.
3796    /// `default => ...` becomes the else branch.
3797    fn parse_cond_expr(&mut self, line: usize) -> StrykeResult<Expr> {
3798        self.expect(&Token::LBrace)?;
3799
3800        let mut arms: Vec<(Expr, Block)> = Vec::new();
3801        let mut else_block: Option<Block> = None;
3802
3803        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
3804            let arm_line = self.peek_line();
3805
3806            // Check for `default =>`
3807            let is_default = matches!(self.peek(), Token::Ident(ref s) if s == "default")
3808                && matches!(self.peek_at(1), Token::FatArrow);
3809
3810            if is_default {
3811                self.advance(); // consume `default`
3812                self.advance(); // consume `=>`
3813                let body = if matches!(self.peek(), Token::LBrace) {
3814                    self.parse_block()?
3815                } else {
3816                    let expr = self.parse_assign_expr()?;
3817                    vec![Statement {
3818                        label: None,
3819                        kind: StmtKind::Expression(expr),
3820                        line: arm_line,
3821                    }]
3822                };
3823                else_block = Some(body);
3824                self.eat(&Token::Comma);
3825                break; // default must be last
3826            }
3827
3828            // Parse condition expression (stop before `=>`)
3829            let condition = self.parse_assign_expr()?;
3830            self.expect(&Token::FatArrow)?;
3831
3832            let body = if matches!(self.peek(), Token::LBrace) {
3833                self.parse_block()?
3834            } else {
3835                let expr = self.parse_assign_expr()?;
3836                vec![Statement {
3837                    label: None,
3838                    kind: StmtKind::Expression(expr),
3839                    line: arm_line,
3840                }]
3841            };
3842
3843            arms.push((condition, body));
3844            self.eat(&Token::Comma);
3845        }
3846
3847        self.expect(&Token::RBrace)?;
3848
3849        if arms.is_empty() {
3850            return Err(self.syntax_err("cond requires at least one condition arm", line));
3851        }
3852
3853        // Build if/elsif/else chain from the arms.
3854        let (first_cond, first_body) = arms.remove(0);
3855        let elsifs: Vec<(Expr, Block)> = arms;
3856
3857        // Wrap in a do-block so `cond { ... }` is an expression.
3858        let if_stmt = Statement {
3859            label: None,
3860            kind: StmtKind::If {
3861                condition: first_cond,
3862                body: first_body,
3863                elsifs,
3864                else_block,
3865            },
3866            line,
3867        };
3868        let inner = Expr {
3869            kind: ExprKind::CodeRef {
3870                params: vec![],
3871                body: vec![if_stmt],
3872            },
3873            line,
3874        };
3875        Ok(Expr {
3876            kind: ExprKind::Do(Box::new(inner)),
3877            line,
3878        })
3879    }
3880
3881    /// `match (EXPR) { PATTERN => EXPR, ... }`
3882    fn parse_algebraic_match_expr(&mut self, line: usize) -> StrykeResult<Expr> {
3883        self.expect(&Token::LParen)?;
3884        let subject = self.parse_expression()?;
3885        self.expect(&Token::RParen)?;
3886        self.expect(&Token::LBrace)?;
3887        let mut arms = Vec::new();
3888        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
3889            if self.eat(&Token::Semicolon) {
3890                continue;
3891            }
3892            let pattern = self.parse_match_pattern()?;
3893            let guard = if matches!(self.peek(), Token::Ident(ref s) if s == "if") {
3894                self.advance();
3895                // Use assign-level parsing so `=>` after the guard is not consumed as a comma/fat-comma
3896                // separator (see [`Self::parse_comma_expr`]).
3897                Some(Box::new(self.parse_assign_expr()?))
3898            } else {
3899                None
3900            };
3901            self.expect(&Token::FatArrow)?;
3902            // Use assign-level parsing so commas separate arms, not `List` elements.
3903            let body = self.parse_assign_expr()?;
3904            arms.push(MatchArm {
3905                pattern,
3906                guard,
3907                body,
3908            });
3909            self.eat(&Token::Comma);
3910        }
3911        self.expect(&Token::RBrace)?;
3912        Ok(Expr {
3913            kind: ExprKind::AlgebraicMatch {
3914                subject: Box::new(subject),
3915                arms,
3916            },
3917            line,
3918        })
3919    }
3920
3921    fn parse_match_pattern(&mut self) -> StrykeResult<MatchPattern> {
3922        match self.peek().clone() {
3923            Token::Regex(pattern, flags, _delim) => {
3924                self.advance();
3925                Ok(MatchPattern::Regex { pattern, flags })
3926            }
3927            Token::Ident(ref s) if s == "_" => {
3928                self.advance();
3929                Ok(MatchPattern::Any)
3930            }
3931            Token::Ident(ref s) if s == "Some" => {
3932                self.advance();
3933                self.expect(&Token::LParen)?;
3934                let name = self.parse_scalar_var_name()?;
3935                self.expect(&Token::RParen)?;
3936                Ok(MatchPattern::OptionSome(name))
3937            }
3938            Token::LBracket => self.parse_match_array_pattern(),
3939            Token::LBrace => self.parse_match_hash_pattern(),
3940            Token::LParen => {
3941                self.advance();
3942                let e = self.parse_expression()?;
3943                self.expect(&Token::RParen)?;
3944                Ok(MatchPattern::Value(Box::new(e)))
3945            }
3946            _ => {
3947                let e = self.parse_assign_expr()?;
3948                Ok(MatchPattern::Value(Box::new(e)))
3949            }
3950        }
3951    }
3952
3953    /// Contents of `[ ... ]` for algebraic array patterns and `sub ($a, [ ... ])` signatures.
3954    fn parse_match_array_elems_until_rbracket(&mut self) -> StrykeResult<Vec<MatchArrayElem>> {
3955        let mut elems = Vec::new();
3956        if self.eat(&Token::RBracket) {
3957            return Ok(vec![]);
3958        }
3959        loop {
3960            if matches!(self.peek(), Token::Star) {
3961                self.advance();
3962                elems.push(MatchArrayElem::Rest);
3963                self.eat(&Token::Comma);
3964                if !matches!(self.peek(), Token::RBracket) {
3965                    return Err(self.syntax_err(
3966                        "`*` must be the last element in an array match pattern",
3967                        self.peek_line(),
3968                    ));
3969                }
3970                self.expect(&Token::RBracket)?;
3971                return Ok(elems);
3972            }
3973            if let Token::ArrayVar(name) = self.peek().clone() {
3974                self.advance();
3975                elems.push(MatchArrayElem::RestBind(name));
3976                self.eat(&Token::Comma);
3977                if !matches!(self.peek(), Token::RBracket) {
3978                    return Err(self.syntax_err(
3979                        "`@name` rest bind must be the last element in an array match pattern",
3980                        self.peek_line(),
3981                    ));
3982                }
3983                self.expect(&Token::RBracket)?;
3984                return Ok(elems);
3985            }
3986            if let Token::ScalarVar(name) = self.peek().clone() {
3987                self.advance();
3988                elems.push(MatchArrayElem::CaptureScalar(name));
3989                if self.eat(&Token::Comma) {
3990                    if matches!(self.peek(), Token::RBracket) {
3991                        break;
3992                    }
3993                    continue;
3994                }
3995                break;
3996            }
3997            let e = self.parse_assign_expr()?;
3998            elems.push(MatchArrayElem::Expr(e));
3999            if self.eat(&Token::Comma) {
4000                if matches!(self.peek(), Token::RBracket) {
4001                    break;
4002                }
4003                continue;
4004            }
4005            break;
4006        }
4007        self.expect(&Token::RBracket)?;
4008        Ok(elems)
4009    }
4010
4011    fn parse_match_array_pattern(&mut self) -> StrykeResult<MatchPattern> {
4012        self.expect(&Token::LBracket)?;
4013        let elems = self.parse_match_array_elems_until_rbracket()?;
4014        Ok(MatchPattern::Array(elems))
4015    }
4016
4017    fn parse_match_hash_pattern(&mut self) -> StrykeResult<MatchPattern> {
4018        self.expect(&Token::LBrace)?;
4019        let mut pairs = Vec::new();
4020        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
4021            if self.eat(&Token::Semicolon) {
4022                continue;
4023            }
4024            let key = self.parse_assign_expr()?;
4025            self.expect(&Token::FatArrow)?;
4026            match self.advance().0 {
4027                Token::Ident(ref s) if s == "_" => {
4028                    pairs.push(MatchHashPair::KeyOnly { key });
4029                }
4030                Token::ScalarVar(name) => {
4031                    pairs.push(MatchHashPair::Capture { key, name });
4032                }
4033                tok => {
4034                    return Err(self.syntax_err(
4035                        format!(
4036                            "hash match pattern must bind with `=> $name` or `=> _`, got {:?}",
4037                            tok
4038                        ),
4039                        self.peek_line(),
4040                    ));
4041                }
4042            }
4043            self.eat(&Token::Comma);
4044        }
4045        self.expect(&Token::RBrace)?;
4046        Ok(MatchPattern::Hash(pairs))
4047    }
4048
4049    /// `eval_timeout SECS { ... }`
4050    fn parse_eval_timeout(&mut self) -> StrykeResult<Statement> {
4051        let line = self.peek_line();
4052        self.advance();
4053        let timeout = self.parse_postfix()?;
4054        let body = self.parse_block_or_bareword_block_no_args()?;
4055        self.eat(&Token::Semicolon);
4056        Ok(Statement {
4057            label: None,
4058            kind: StmtKind::EvalTimeout { timeout, body },
4059            line,
4060        })
4061    }
4062
4063    fn mark_match_scalar_g_for_boolean_condition(cond: &mut Expr) {
4064        match &mut cond.kind {
4065            ExprKind::Match {
4066                flags, scalar_g, ..
4067            } if flags.contains('g') => {
4068                *scalar_g = true;
4069            }
4070            ExprKind::UnaryOp {
4071                op: UnaryOp::LogNot,
4072                expr,
4073            } => {
4074                if let ExprKind::Match {
4075                    flags, scalar_g, ..
4076                } = &mut expr.kind
4077                {
4078                    if flags.contains('g') {
4079                        *scalar_g = true;
4080                    }
4081                }
4082            }
4083            _ => {}
4084        }
4085    }
4086
4087    fn parse_if(&mut self) -> StrykeResult<Statement> {
4088        let line = self.peek_line();
4089        self.advance(); // 'if'
4090        if matches!(self.peek(), Token::Ident(ref s) if s == "let") {
4091            if crate::compat_mode() {
4092                return Err(self.syntax_err(
4093                    "`if let` is a stryke extension (disabled by --compat)",
4094                    line,
4095                ));
4096            }
4097            return self.parse_if_let(line);
4098        }
4099        self.expect(&Token::LParen)?;
4100        let mut cond = self.parse_expression()?;
4101        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
4102        self.expect(&Token::RParen)?;
4103        let body = self.parse_block()?;
4104
4105        let mut elsifs = Vec::new();
4106        let mut else_block = None;
4107
4108        loop {
4109            if let Token::Ident(ref kw) = self.peek().clone() {
4110                if kw == "elsif" {
4111                    self.advance();
4112                    self.expect(&Token::LParen)?;
4113                    let mut c = self.parse_expression()?;
4114                    Self::mark_match_scalar_g_for_boolean_condition(&mut c);
4115                    self.expect(&Token::RParen)?;
4116                    let b = self.parse_block()?;
4117                    elsifs.push((c, b));
4118                    continue;
4119                }
4120                if kw == "else" {
4121                    self.advance();
4122                    else_block = Some(self.parse_block()?);
4123                }
4124            }
4125            break;
4126        }
4127
4128        Ok(Statement {
4129            label: None,
4130            kind: StmtKind::If {
4131                condition: cond,
4132                body,
4133                elsifs,
4134                else_block,
4135            },
4136            line,
4137        })
4138    }
4139
4140    /// `if let PAT = EXPR { ... } [ else { ... } ]` — desugars to [`ExprKind::AlgebraicMatch`].
4141    fn parse_if_let(&mut self, line: usize) -> StrykeResult<Statement> {
4142        self.advance(); // `let`
4143        let pattern = self.parse_match_pattern()?;
4144        self.expect(&Token::Assign)?;
4145        // Use assign-level parsing so a following `{ ... }` is the `if let` body, not an anon hash.
4146        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_add(1);
4147        let rhs = self.parse_assign_expr();
4148        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_sub(1);
4149        let rhs = rhs?;
4150        let then_block = self.parse_block()?;
4151        let else_block_opt = match self.peek().clone() {
4152            Token::Ident(ref kw) if kw == "else" => {
4153                self.advance();
4154                Some(self.parse_block()?)
4155            }
4156            Token::Ident(ref kw) if kw == "elsif" => {
4157                return Err(self.syntax_err(
4158                    "`if let` does not support `elsif`; use `else { }` or a full `match`",
4159                    self.peek_line(),
4160                ));
4161            }
4162            _ => None,
4163        };
4164        let then_expr = Self::expr_do_anon_block(then_block, line);
4165        let else_expr = if let Some(eb) = else_block_opt {
4166            Self::expr_do_anon_block(eb, line)
4167        } else {
4168            Expr {
4169                kind: ExprKind::Undef,
4170                line,
4171            }
4172        };
4173        let arms = vec![
4174            MatchArm {
4175                pattern,
4176                guard: None,
4177                body: then_expr,
4178            },
4179            MatchArm {
4180                pattern: MatchPattern::Any,
4181                guard: None,
4182                body: else_expr,
4183            },
4184        ];
4185        Ok(Statement {
4186            label: None,
4187            kind: StmtKind::Expression(Expr {
4188                kind: ExprKind::AlgebraicMatch {
4189                    subject: Box::new(rhs),
4190                    arms,
4191                },
4192                line,
4193            }),
4194            line,
4195        })
4196    }
4197
4198    fn expr_do_anon_block(block: Block, outer_line: usize) -> Expr {
4199        let inner_line = block.first().map(|s| s.line).unwrap_or(outer_line);
4200        Expr {
4201            kind: ExprKind::Do(Box::new(Expr {
4202                kind: ExprKind::CodeRef {
4203                    params: vec![],
4204                    body: block,
4205                },
4206                line: inner_line,
4207            })),
4208            line: outer_line,
4209        }
4210    }
4211
4212    fn parse_unless(&mut self) -> StrykeResult<Statement> {
4213        let line = self.peek_line();
4214        self.advance(); // 'unless'
4215        self.expect(&Token::LParen)?;
4216        let mut cond = self.parse_expression()?;
4217        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
4218        self.expect(&Token::RParen)?;
4219        let body = self.parse_block()?;
4220        let else_block = if let Token::Ident(ref kw) = self.peek().clone() {
4221            if kw == "else" {
4222                self.advance();
4223                Some(self.parse_block()?)
4224            } else {
4225                None
4226            }
4227        } else {
4228            None
4229        };
4230        Ok(Statement {
4231            label: None,
4232            kind: StmtKind::Unless {
4233                condition: cond,
4234                body,
4235                else_block,
4236            },
4237            line,
4238        })
4239    }
4240
4241    /// `loop { BLOCK }` — Rust-style infinite loop, desugars to
4242    /// `while (1) { BLOCK }`. No parens, no condition. `last` / `next`
4243    /// (with optional labels), `redo`, and `continue` blocks all work
4244    /// because the resulting StmtKind::While is what the runtime
4245    /// already handles.
4246    fn parse_loop(&mut self) -> StrykeResult<Statement> {
4247        let line = self.peek_line();
4248        self.advance(); // 'loop'
4249        let body = self.parse_block()?;
4250        let continue_block = self.parse_optional_continue_block()?;
4251        Ok(Statement {
4252            label: None,
4253            kind: StmtKind::While {
4254                condition: Expr {
4255                    kind: ExprKind::Integer(1),
4256                    line,
4257                },
4258                body,
4259                label: None,
4260                continue_block,
4261            },
4262            line,
4263        })
4264    }
4265
4266    /// `ploop { BODY }` / `ploop N { BODY }` — parallel variant of `loop`:
4267    /// each of N rayon workers (default `thread_count()`) runs BODY as an
4268    /// infinite loop. Desugars to `pfor { while (1) { BODY } } 1..N`, so
4269    /// `$_` is the 1-based worker id and `last` exits that worker's loop;
4270    /// the construct finishes when every worker has exited. Shares the
4271    /// existing `pfor` runtime — no new ExprKind or opcode.
4272    fn parse_ploop(&mut self) -> StrykeResult<Statement> {
4273        let line = self.peek_line();
4274        self.advance(); // 'ploop'
4275        let count = if matches!(self.peek(), Token::LBrace) {
4276            None
4277        } else {
4278            // Primary only — `parse_assign_expr` would read the body's
4279            // `{` as a hash subscript on a `$var` count (`$k {` → `$k{…}`).
4280            Some(self.parse_primary()?)
4281        };
4282        let body = self.parse_block()?;
4283        let continue_block = self.parse_optional_continue_block()?;
4284        let condition = Expr {
4285            kind: ExprKind::Integer(1),
4286            line,
4287        };
4288        self.parallel_worker_loop_stmt(line, count, condition, body, continue_block)
4289    }
4290
4291    /// `pwhile (COND) { BODY }` / `pwhile N (COND) { BODY }` — parallel
4292    /// variant of `while`: each of N rayon workers (default
4293    /// `thread_count()`) runs `while (COND) { BODY }`. COND re-evaluates
4294    /// per worker per iteration, so a shared `mysync` flag or a draining
4295    /// queue stops all workers. Same desugar family as `ploop`:
4296    /// `pfor { while (COND) { BODY } } 1..N`.
4297    fn parse_pwhile(&mut self) -> StrykeResult<Statement> {
4298        let line = self.peek_line();
4299        self.advance(); // 'pwhile'
4300        let count = if matches!(self.peek(), Token::LParen) {
4301            None
4302        } else {
4303            // Primary only — `parse_assign_expr` would swallow the
4304            // following `(COND)` as a call on the count.
4305            Some(self.parse_primary()?)
4306        };
4307        self.expect(&Token::LParen)?;
4308        let mut condition = self.parse_expression()?;
4309        Self::mark_match_scalar_g_for_boolean_condition(&mut condition);
4310        self.expect(&Token::RParen)?;
4311        let body = self.parse_block()?;
4312        let continue_block = self.parse_optional_continue_block()?;
4313        self.parallel_worker_loop_stmt(line, count, condition, body, continue_block)
4314    }
4315
4316    /// Shared desugar for `ploop` / `pwhile`: wrap BODY in a
4317    /// `while (condition)` worker loop and fan it out over
4318    /// `pfor { … } 1..N` (N defaults to `thread_count()`), binding the
4319    /// 1-based worker id to `$_`.
4320    fn parallel_worker_loop_stmt(
4321        &mut self,
4322        line: usize,
4323        count: Option<Expr>,
4324        condition: Expr,
4325        body: Block,
4326        continue_block: Option<Block>,
4327    ) -> StrykeResult<Statement> {
4328        let worker_loop = Statement {
4329            label: None,
4330            kind: StmtKind::While {
4331                condition,
4332                body,
4333                label: None,
4334                continue_block,
4335            },
4336            line,
4337        };
4338        let list = match count {
4339            Some(n) => Expr {
4340                kind: ExprKind::Range {
4341                    from: Box::new(Expr {
4342                        kind: ExprKind::Integer(1),
4343                        line,
4344                    }),
4345                    to: Box::new(n),
4346                    exclusive: false,
4347                    step: None,
4348                },
4349                line,
4350            },
4351            None => parse_expression_from_str("1..thread_count()", "<ploop>")?,
4352        };
4353        Ok(Statement {
4354            label: None,
4355            kind: StmtKind::Expression(Expr {
4356                kind: ExprKind::PForExpr {
4357                    block: vec![worker_loop],
4358                    list: Box::new(list),
4359                    progress: None,
4360                },
4361                line,
4362            }),
4363            line,
4364        })
4365    }
4366
4367    fn parse_while(&mut self) -> StrykeResult<Statement> {
4368        let line = self.peek_line();
4369        self.advance(); // 'while'
4370        if matches!(self.peek(), Token::Ident(ref s) if s == "let") {
4371            if crate::compat_mode() {
4372                return Err(self.syntax_err(
4373                    "`while let` is a stryke extension (disabled by --compat)",
4374                    line,
4375                ));
4376            }
4377            return self.parse_while_let(line);
4378        }
4379        self.expect(&Token::LParen)?;
4380        let mut cond = self.parse_expression()?;
4381        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
4382        self.expect(&Token::RParen)?;
4383        let body = self.parse_block()?;
4384        let continue_block = self.parse_optional_continue_block()?;
4385        Ok(Statement {
4386            label: None,
4387            kind: StmtKind::While {
4388                condition: cond,
4389                body,
4390                label: None,
4391                continue_block,
4392            },
4393            line,
4394        })
4395    }
4396
4397    /// `while let PAT = EXPR { ... }` — desugars to a `match` that returns 0/1 plus `unless ($tmp) { last }`
4398    /// so bytecode does not run `last` inside a tree-assisted [`Op::AlgebraicMatch`] arm.
4399    fn parse_while_let(&mut self, line: usize) -> StrykeResult<Statement> {
4400        self.advance(); // `let`
4401        let pattern = self.parse_match_pattern()?;
4402        self.expect(&Token::Assign)?;
4403        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_add(1);
4404        let rhs = self.parse_assign_expr();
4405        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_sub(1);
4406        let rhs = rhs?;
4407        let mut user_body = self.parse_block()?;
4408        let continue_block = self.parse_optional_continue_block()?;
4409        user_body.push(Statement::new(
4410            StmtKind::Expression(Expr {
4411                kind: ExprKind::Integer(1),
4412                line,
4413            }),
4414            line,
4415        ));
4416        let tmp = format!("__while_let_{}", self.alloc_desugar_tmp());
4417        let match_expr = Expr {
4418            kind: ExprKind::AlgebraicMatch {
4419                subject: Box::new(rhs),
4420                arms: vec![
4421                    MatchArm {
4422                        pattern,
4423                        guard: None,
4424                        body: Self::expr_do_anon_block(user_body, line),
4425                    },
4426                    MatchArm {
4427                        pattern: MatchPattern::Any,
4428                        guard: None,
4429                        body: Expr {
4430                            kind: ExprKind::Integer(0),
4431                            line,
4432                        },
4433                    },
4434                ],
4435            },
4436            line,
4437        };
4438        let my_stmt = Statement::new(
4439            StmtKind::My(vec![VarDecl {
4440                sigil: Sigil::Scalar,
4441                name: tmp.clone(),
4442                initializer: Some(match_expr),
4443                frozen: false,
4444                type_annotation: None,
4445                list_context: false,
4446            }]),
4447            line,
4448        );
4449        let unless_last = Statement::new(
4450            StmtKind::Unless {
4451                condition: Expr {
4452                    kind: ExprKind::ScalarVar(tmp),
4453                    line,
4454                },
4455                body: vec![Statement::new(StmtKind::Last(None), line)],
4456                else_block: None,
4457            },
4458            line,
4459        );
4460        Ok(Statement::new(
4461            StmtKind::While {
4462                condition: Expr {
4463                    kind: ExprKind::Integer(1),
4464                    line,
4465                },
4466                body: vec![my_stmt, unless_last],
4467                label: None,
4468                continue_block,
4469            },
4470            line,
4471        ))
4472    }
4473
4474    fn parse_until(&mut self) -> StrykeResult<Statement> {
4475        let line = self.peek_line();
4476        self.advance(); // 'until'
4477        self.expect(&Token::LParen)?;
4478        let mut cond = self.parse_expression()?;
4479        Self::mark_match_scalar_g_for_boolean_condition(&mut cond);
4480        self.expect(&Token::RParen)?;
4481        let body = self.parse_block()?;
4482        let continue_block = self.parse_optional_continue_block()?;
4483        Ok(Statement {
4484            label: None,
4485            kind: StmtKind::Until {
4486                condition: cond,
4487                body,
4488                label: None,
4489                continue_block,
4490            },
4491            line,
4492        })
4493    }
4494
4495    /// `continue { ... }` after a loop body (optional).
4496    fn parse_optional_continue_block(&mut self) -> StrykeResult<Option<Block>> {
4497        if let Token::Ident(ref kw) = self.peek().clone() {
4498            if kw == "continue" {
4499                self.advance();
4500                return Ok(Some(self.parse_block()?));
4501            }
4502        }
4503        Ok(None)
4504    }
4505
4506    fn parse_for_or_foreach(&mut self) -> StrykeResult<Statement> {
4507        let line = self.peek_line();
4508        self.advance(); // 'for'
4509
4510        // Peek to determine if C-style for or foreach
4511        // C-style: for (init; cond; step)
4512        // foreach-style: for $var (list) or for (list)
4513        match self.peek() {
4514            Token::LParen => {
4515                // Check if next after ( is a semicolon or an assignment — C-style
4516                // Or if it's a list — foreach-style
4517                // Heuristic: if the token after ( is 'my' or '$' followed by
4518                // content that contains ';', it's C-style.
4519                let saved = self.pos;
4520                self.advance(); // consume (
4521                                // Look for semicolon at paren depth 0
4522                let mut depth = 1;
4523                let mut has_semi = false;
4524                let mut scan = self.pos;
4525                while scan < self.tokens.len() {
4526                    match &self.tokens[scan].0 {
4527                        Token::LParen => depth += 1,
4528                        Token::RParen => {
4529                            depth -= 1;
4530                            if depth == 0 {
4531                                break;
4532                            }
4533                        }
4534                        Token::Semicolon if depth == 1 => {
4535                            has_semi = true;
4536                            break;
4537                        }
4538                        _ => {}
4539                    }
4540                    scan += 1;
4541                }
4542                self.pos = saved;
4543
4544                if has_semi {
4545                    self.parse_c_style_for(line)
4546                } else {
4547                    // foreach without explicit var — uses $_
4548                    self.expect(&Token::LParen)?;
4549                    let list = self.parse_expression()?;
4550                    self.expect(&Token::RParen)?;
4551                    let body = self.parse_block()?;
4552                    let continue_block = self.parse_optional_continue_block()?;
4553                    Ok(Statement {
4554                        label: None,
4555                        kind: StmtKind::Foreach {
4556                            var: "_".to_string(),
4557                            list,
4558                            body,
4559                            label: None,
4560                            continue_block,
4561                        },
4562                        line,
4563                    })
4564                }
4565            }
4566            // `for my $x (LIST)` / `for var $x (LIST)` / `for val $x (LIST)`.
4567            // `var` / `val` are Kotlin/Scala-style aliases for `my` / `const my`
4568            // (see `parse_primary`); gate on the lookahead being a sigil so
4569            // `for var(@list)` (`var` as a bareword fn name) still routes to
4570            // the no-decl `Token::LParen` arm above instead of dispatching as
4571            // a decl. `val` in this position currently parses identically to
4572            // `my` (the loop variable's frozenness isn't surfaced in
4573            // `StmtKind::Foreach`'s `var` field today), keeping the surface
4574            // syntax consistent with the other var/val sites.
4575            Token::Ident(ref kw)
4576                if is_lexical_decl(kw) && matches!(self.peek_at(1), Token::ScalarVar(_)) =>
4577            {
4578                self.advance(); // 'my' / 'var' / 'val'
4579                let var = self.parse_scalar_var_name()?;
4580                self.expect(&Token::LParen)?;
4581                let list = self.parse_expression()?;
4582                self.expect(&Token::RParen)?;
4583                let body = self.parse_block()?;
4584                let continue_block = self.parse_optional_continue_block()?;
4585                Ok(Statement {
4586                    label: None,
4587                    kind: StmtKind::Foreach {
4588                        var,
4589                        list,
4590                        body,
4591                        label: None,
4592                        continue_block,
4593                    },
4594                    line,
4595                })
4596            }
4597            Token::ScalarVar(_) => {
4598                let var = self.parse_scalar_var_name()?;
4599                self.expect(&Token::LParen)?;
4600                let list = self.parse_expression()?;
4601                self.expect(&Token::RParen)?;
4602                let body = self.parse_block()?;
4603                let continue_block = self.parse_optional_continue_block()?;
4604                Ok(Statement {
4605                    label: None,
4606                    kind: StmtKind::Foreach {
4607                        var,
4608                        list,
4609                        body,
4610                        label: None,
4611                        continue_block,
4612                    },
4613                    line,
4614                })
4615            }
4616            _ => self.parse_c_style_for(line),
4617        }
4618    }
4619
4620    fn parse_c_style_for(&mut self, line: usize) -> StrykeResult<Statement> {
4621        self.expect(&Token::LParen)?;
4622        let init = if self.eat(&Token::Semicolon) {
4623            None
4624        } else {
4625            let s = self.parse_statement()?;
4626            self.eat(&Token::Semicolon);
4627            Some(Box::new(s))
4628        };
4629        let mut condition = if matches!(self.peek(), Token::Semicolon) {
4630            None
4631        } else {
4632            Some(self.parse_expression()?)
4633        };
4634        if let Some(ref mut c) = condition {
4635            Self::mark_match_scalar_g_for_boolean_condition(c);
4636        }
4637        self.expect(&Token::Semicolon)?;
4638        let step = if matches!(self.peek(), Token::RParen) {
4639            None
4640        } else {
4641            Some(self.parse_expression()?)
4642        };
4643        self.expect(&Token::RParen)?;
4644        let body = self.parse_block()?;
4645        let continue_block = self.parse_optional_continue_block()?;
4646        Ok(Statement {
4647            label: None,
4648            kind: StmtKind::For {
4649                init,
4650                condition,
4651                step,
4652                body,
4653                label: None,
4654                continue_block,
4655            },
4656            line,
4657        })
4658    }
4659
4660    fn parse_foreach(&mut self) -> StrykeResult<Statement> {
4661        let line = self.peek_line();
4662        self.advance(); // 'foreach'
4663        let var = match self.peek() {
4664            // Same gating as `parse_for_or_foreach` above — `var`/`val` only
4665            // dispatch as the loop-binding declarator when the next token is
4666            // a sigil. Bare `foreach var (@xs) {...}` (no sigil) routes to
4667            // the no-decl arm and parses `var` as an expression.
4668            Token::Ident(ref kw)
4669                if is_lexical_decl(kw) && matches!(self.peek_at(1), Token::ScalarVar(_)) =>
4670            {
4671                self.advance();
4672                self.parse_scalar_var_name()?
4673            }
4674            Token::ScalarVar(_) => self.parse_scalar_var_name()?,
4675            _ => "_".to_string(),
4676        };
4677        self.expect(&Token::LParen)?;
4678        let list = self.parse_expression()?;
4679        self.expect(&Token::RParen)?;
4680        let body = self.parse_block()?;
4681        let continue_block = self.parse_optional_continue_block()?;
4682        Ok(Statement {
4683            label: None,
4684            kind: StmtKind::Foreach {
4685                var,
4686                list,
4687                body,
4688                label: None,
4689                continue_block,
4690            },
4691            line,
4692        })
4693    }
4694
4695    fn parse_scalar_var_name(&mut self) -> StrykeResult<String> {
4696        match self.advance() {
4697            (Token::ScalarVar(name), _) => Ok(name),
4698            (tok, line) => {
4699                Err(self.syntax_err(format!("Expected scalar variable, got {:?}", tok), line))
4700            }
4701        }
4702    }
4703
4704    /// After `(` was consumed: Perl5 prototype characters until `)` (or `$)` + `{`).
4705    fn parse_legacy_sub_prototype_tail(&mut self) -> StrykeResult<String> {
4706        let mut s = String::new();
4707        loop {
4708            match self.peek().clone() {
4709                Token::RParen => {
4710                    self.advance();
4711                    break;
4712                }
4713                Token::Eof => {
4714                    return Err(self.syntax_err(
4715                        "Unterminated sub prototype (expected ')' before end of input)",
4716                        self.peek_line(),
4717                    ));
4718                }
4719                Token::ScalarVar(v) if v == ")" => {
4720                    // Lexer merges `$` + `)` into one token (`$)`). In `sub name ($) {`, the
4721                    // closing `)` of the prototype is not a separate `RParen` — next is `{`.
4722                    self.advance();
4723                    s.push('$');
4724                    if matches!(self.peek(), Token::LBrace) {
4725                        break;
4726                    }
4727                }
4728                Token::Ident(i) => {
4729                    let i = i.clone();
4730                    self.advance();
4731                    s.push_str(&i);
4732                }
4733                Token::Semicolon => {
4734                    self.advance();
4735                    s.push(';');
4736                }
4737                Token::LParen => {
4738                    self.advance();
4739                    s.push('(');
4740                }
4741                Token::LBracket => {
4742                    self.advance();
4743                    s.push('[');
4744                }
4745                Token::RBracket => {
4746                    self.advance();
4747                    s.push(']');
4748                }
4749                Token::Backslash => {
4750                    self.advance();
4751                    s.push('\\');
4752                }
4753                Token::Comma => {
4754                    self.advance();
4755                    s.push(',');
4756                }
4757                Token::ScalarVar(v) => {
4758                    let v = v.clone();
4759                    self.advance();
4760                    s.push('$');
4761                    s.push_str(&v);
4762                }
4763                Token::ArrayVar(v) => {
4764                    let v = v.clone();
4765                    self.advance();
4766                    s.push('@');
4767                    s.push_str(&v);
4768                }
4769                // Bare `@` / `%` in prototypes (e.g. Try::Tiny's `sub try (&;@)`).
4770                Token::ArrayAt => {
4771                    self.advance();
4772                    s.push('@');
4773                }
4774                Token::HashVar(v) => {
4775                    let v = v.clone();
4776                    self.advance();
4777                    s.push('%');
4778                    s.push_str(&v);
4779                }
4780                Token::HashPercent => {
4781                    self.advance();
4782                    s.push('%');
4783                }
4784                // Bare `%` outside hash-sigil context (e.g. `sub f ($;%)`,
4785                // `sub f (\[$@%])`). The lexer falls back to `Token::Percent`
4786                // (the modulo operator) when no identifier follows.
4787                Token::Percent => {
4788                    self.advance();
4789                    s.push('%');
4790                }
4791                // Bare `*` — typeglob slurp in prototypes: `sub f (*)`,
4792                // `sub f (\*)`.
4793                Token::Star => {
4794                    self.advance();
4795                    s.push('*');
4796                }
4797                Token::Plus => {
4798                    self.advance();
4799                    s.push('+');
4800                }
4801                Token::Minus => {
4802                    self.advance();
4803                    s.push('-');
4804                }
4805                Token::BitAnd => {
4806                    self.advance();
4807                    s.push('&');
4808                }
4809                tok => {
4810                    return Err(self.syntax_err(
4811                        format!("Unexpected token in sub prototype: {:?}", tok),
4812                        self.peek_line(),
4813                    ));
4814                }
4815            }
4816        }
4817        Ok(s)
4818    }
4819
4820    fn sub_signature_list_starts_here(&self) -> bool {
4821        // Signature parameters use identifier names ($foo, @args, %opts).
4822        // Any sigil followed by a non-identifier char is a Perl special
4823        // variable ($;, $,, $., $!, $@, $_, $$, $), …) and indicates
4824        // legacy-prototype context, not signature context. The lexer
4825        // greedily consumes `$;` as `ScalarVar(";")`, `$,` as
4826        // `ScalarVar(",")`, etc., so this check has to run on the
4827        // captured name, not on the raw bytes.
4828        fn is_ident_name(name: &str) -> bool {
4829            let mut chars = name.chars();
4830            match chars.next() {
4831                Some(c) if c.is_ascii_alphabetic() || c == '_' => {
4832                    chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
4833                }
4834                _ => false,
4835            }
4836        }
4837        match self.peek() {
4838            Token::LBrace | Token::LBracket => true,
4839            Token::ScalarVar(name) => is_ident_name(name),
4840            Token::ArrayVar(name) | Token::HashVar(name) => is_ident_name(name),
4841            _ => false,
4842        }
4843    }
4844
4845    fn parse_sub_signature_hash_key(&mut self) -> StrykeResult<String> {
4846        let (tok, line) = self.advance();
4847        match tok {
4848            Token::Ident(i) => Ok(i),
4849            Token::SingleString(s) | Token::DoubleString(s) => Ok(s),
4850            tok => Err(self.syntax_err(
4851                format!(
4852                    "sub signature: expected hash key (identifier or string), got {:?}",
4853                    tok
4854                ),
4855                line,
4856            )),
4857        }
4858    }
4859
4860    fn parse_sub_signature_param_list(&mut self) -> StrykeResult<Vec<SubSigParam>> {
4861        let mut params = Vec::new();
4862        loop {
4863            if matches!(self.peek(), Token::RParen) {
4864                break;
4865            }
4866            match self.peek().clone() {
4867                Token::ScalarVar(name) => {
4868                    if name == "$$" || name == ")" {
4869                        return Err(self.syntax_err(
4870                            format!(
4871                                "`{name}` cannot start a stryke sub signature (use legacy prototype `($$)` etc.)"
4872                            ),
4873                            self.peek_line(),
4874                        ));
4875                    }
4876                    self.advance();
4877                    let ty = if self.eat(&Token::Colon) {
4878                        match self.peek() {
4879                            Token::Ident(ref tname) => {
4880                                let tname = tname.clone();
4881                                self.advance();
4882                                Some(match tname.as_str() {
4883                                    "Int" => PerlTypeName::Int,
4884                                    "Str" => PerlTypeName::Str,
4885                                    "Float" => PerlTypeName::Float,
4886                                    "Bool" => PerlTypeName::Bool,
4887                                    "Array" => PerlTypeName::Array,
4888                                    "Hash" => PerlTypeName::Hash,
4889                                    "Ref" => PerlTypeName::Ref,
4890                                    "Any" => PerlTypeName::Any,
4891                                    _ => PerlTypeName::Struct(tname),
4892                                })
4893                            }
4894                            _ => {
4895                                return Err(self.syntax_err(
4896                                    "expected type name after `:` in sub signature",
4897                                    self.peek_line(),
4898                                ));
4899                            }
4900                        }
4901                    } else {
4902                        None
4903                    };
4904                    // Check for default value: `$x = expr`
4905                    let default = if self.eat(&Token::Assign) {
4906                        Some(Box::new(self.parse_ternary()?))
4907                    } else {
4908                        None
4909                    };
4910                    params.push(SubSigParam::Scalar(name, ty, default));
4911                }
4912                Token::ArrayVar(name) => {
4913                    self.advance();
4914                    let default = if self.eat(&Token::Assign) {
4915                        Some(Box::new(self.parse_ternary()?))
4916                    } else {
4917                        None
4918                    };
4919                    params.push(SubSigParam::Array(name, default));
4920                }
4921                Token::HashVar(name) => {
4922                    self.advance();
4923                    let default = if self.eat(&Token::Assign) {
4924                        Some(Box::new(self.parse_ternary()?))
4925                    } else {
4926                        None
4927                    };
4928                    params.push(SubSigParam::Hash(name, default));
4929                }
4930                Token::LBracket => {
4931                    self.advance();
4932                    let elems = self.parse_match_array_elems_until_rbracket()?;
4933                    params.push(SubSigParam::ArrayDestruct(elems));
4934                }
4935                Token::LBrace => {
4936                    self.advance();
4937                    let mut pairs = Vec::new();
4938                    loop {
4939                        if matches!(self.peek(), Token::RBrace | Token::Eof) {
4940                            break;
4941                        }
4942                        if self.eat(&Token::Comma) {
4943                            continue;
4944                        }
4945                        let key = self.parse_sub_signature_hash_key()?;
4946                        self.expect(&Token::FatArrow)?;
4947                        let bind = self.parse_scalar_var_name()?;
4948                        pairs.push((key, bind));
4949                        self.eat(&Token::Comma);
4950                    }
4951                    self.expect(&Token::RBrace)?;
4952                    params.push(SubSigParam::HashDestruct(pairs));
4953                }
4954                tok => {
4955                    return Err(self.syntax_err(
4956                        format!(
4957                            "expected `$name`, `[ ... ]`, or `{{ ... }}` in sub signature, got {:?}",
4958                            tok
4959                        ),
4960                        self.peek_line(),
4961                    ));
4962                }
4963            }
4964            match self.peek() {
4965                Token::Comma => {
4966                    self.advance();
4967                    if matches!(self.peek(), Token::RParen) {
4968                        return Err(self.syntax_err(
4969                            "trailing `,` before `)` in sub signature",
4970                            self.peek_line(),
4971                        ));
4972                    }
4973                }
4974                Token::RParen => break,
4975                _ => {
4976                    return Err(self.syntax_err(
4977                        format!(
4978                            "expected `,` or `)` after sub signature parameter, got {:?}",
4979                            self.peek()
4980                        ),
4981                        self.peek_line(),
4982                    ));
4983                }
4984            }
4985        }
4986        Ok(params)
4987    }
4988
4989    /// Optional `sub` parens: either a Perl 5 prototype string or a stryke **`$name` / `{ k => $v }`** signature.
4990    fn parse_sub_sig_or_prototype_opt(
4991        &mut self,
4992    ) -> StrykeResult<(Vec<SubSigParam>, Option<String>)> {
4993        if !matches!(self.peek(), Token::LParen) {
4994            return Ok((vec![], None));
4995        }
4996        self.advance();
4997        if matches!(self.peek(), Token::RParen) {
4998            self.advance();
4999            return Ok((vec![], Some(String::new())));
5000        }
5001        if self.sub_signature_list_starts_here() {
5002            let params = self.parse_sub_signature_param_list()?;
5003            self.expect(&Token::RParen)?;
5004            return Ok((params, None));
5005        }
5006        let proto = self.parse_legacy_sub_prototype_tail()?;
5007        Ok((vec![], Some(proto)))
5008    }
5009
5010    /// Optional subroutine attributes after name/prototype: `sub foo : lvalue { }`, `sub : ATTR(ARGS) { }`.
5011    fn parse_sub_attributes(&mut self) -> StrykeResult<()> {
5012        while self.eat(&Token::Colon) {
5013            match self.advance() {
5014                (Token::Ident(_), _) => {}
5015                (tok, line) => {
5016                    return Err(self.syntax_err(
5017                        format!("Expected attribute name after `:`, got {:?}", tok),
5018                        line,
5019                    ));
5020                }
5021            }
5022            if self.eat(&Token::LParen) {
5023                let mut depth = 1usize;
5024                while depth > 0 {
5025                    match self.advance().0 {
5026                        Token::LParen => depth += 1,
5027                        Token::RParen => {
5028                            depth -= 1;
5029                        }
5030                        Token::Eof => {
5031                            return Err(self.syntax_err(
5032                                "Unterminated sub attribute argument list",
5033                                self.peek_line(),
5034                            ));
5035                        }
5036                        _ => {}
5037                    }
5038                }
5039            }
5040        }
5041        Ok(())
5042    }
5043
5044    /// After `fn` + optional `(SIG)` + attrs: stryke-only `= EXPR` (one assign-level expression;
5045    /// no top-level `,` after the expression). Returns `None` if the next token is not `=`.
5046    fn try_parse_fn_assign_shorthand_body(&mut self) -> StrykeResult<Option<Block>> {
5047        if !self.eat(&Token::Assign) {
5048            return Ok(None);
5049        }
5050        let expr = self.parse_assign_expr()?;
5051        if matches!(self.peek(), Token::Comma) {
5052            return Err(self.syntax_err(
5053                "`fn ... =` allows only a single expression; use `fn ... { ... }` for multiple statements",
5054                self.peek_line(),
5055            ));
5056        }
5057        let eline = expr.line;
5058        self.eat(&Token::Semicolon);
5059        let mut body = vec![Statement {
5060            label: None,
5061            kind: StmtKind::Expression(expr),
5062            line: eline,
5063        }];
5064        Self::default_topic_for_sole_bareword(&mut body);
5065        Ok(Some(body))
5066    }
5067
5068    /// After `fn` + optional `(SIG)` + attrs: `{ ... }` or stryke-only `= EXPR` (see
5069    /// [`Self::try_parse_fn_assign_shorthand_body`]). `sub` always requires `{ ... }`.
5070    fn parse_fn_eq_body_or_block(&mut self, is_sub_keyword: bool) -> StrykeResult<Block> {
5071        if !is_sub_keyword {
5072            if let Some(block) = self.try_parse_fn_assign_shorthand_body()? {
5073                return Ok(block);
5074            }
5075        }
5076        self.parse_block()
5077    }
5078
5079    fn parse_sub_decl(&mut self, is_sub_keyword: bool) -> StrykeResult<Statement> {
5080        let line = self.peek_line();
5081        self.advance(); // 'sub' or 'fn'
5082        match self.peek().clone() {
5083            Token::Ident(_) => {
5084                let name = self.parse_package_qualified_identifier()?;
5085                // Topic-slot barewords (`_`, `_<`, `_<<`, `_<<<`, `_<<<<`,
5086                // `_0`, `_1`, …, `_N`, plus `_N<+` chain forms) are scalar
5087                // refs to the current/positional/outer topic. A user-defined
5088                // sub with any of these names — bare or package-qualified —
5089                // would shadow the topic in expression position and silently
5090                // break every `_`-aware builtin (`map { _ }`, `say _`,
5091                // `lc _`, …). Reject ALL forms at parse time, including
5092                // `Foo::_`, `Pkg::_0`, `My::Module::_<<<<`.
5093                let bare = name.rsplit("::").next().unwrap_or(&name);
5094                if Self::is_underscore_topic_slot(bare) {
5095                    return Err(self.syntax_err(
5096                        format!(
5097                            "`fn {}` would shadow the topic-slot scalar; pick a different name",
5098                            name
5099                        ),
5100                        line,
5101                    ));
5102                }
5103                if Self::is_reserved_special_var_name(bare) {
5104                    return Err(self.syntax_err(
5105                        format!(
5106                            "`fn {}` would shadow a Perl special variable / filehandle / compile-time token; pick a different name",
5107                            name
5108                        ),
5109                        line,
5110                    ));
5111                }
5112                // Allow shadowing builtins:
5113                // - In compat mode (full Perl 5)
5114                // - When parsing a module (imports should work)
5115                // Block shadowing:
5116                // - In user code (default mode, not parsing module)
5117                // - Always in no-interop mode
5118                let allow_shadow =
5119                    crate::compat_mode() || (self.parsing_module && !crate::no_interop_mode());
5120                if !allow_shadow {
5121                    self.check_udf_shadows_builtin(&name, line)?;
5122                }
5123                self.declared_subs.insert(name.clone());
5124                let (params, prototype) = self.parse_sub_sig_or_prototype_opt()?;
5125                self.parse_sub_attributes()?;
5126                let body = self.parse_fn_eq_body_or_block(is_sub_keyword)?;
5127                Ok(Statement {
5128                    label: None,
5129                    kind: StmtKind::SubDecl {
5130                        name,
5131                        params,
5132                        body,
5133                        prototype,
5134                    },
5135                    line,
5136                })
5137            }
5138            Token::LParen | Token::LBrace | Token::Colon => {
5139                // In no-interop mode, `sub {}` anonymous is not allowed — must use `fn {}`
5140                if is_sub_keyword && crate::no_interop_mode() {
5141                    return Err(self.syntax_err(
5142                        "stryke uses `fn {}` instead of `sub {}` (--no-interop)",
5143                        line,
5144                    ));
5145                }
5146                // Statement-level anonymous sub: `fn { }`, `sub () { }`, `sub :lvalue { }`
5147                let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
5148                self.parse_sub_attributes()?;
5149                let body = self.parse_fn_eq_body_or_block(is_sub_keyword)?;
5150                Ok(Statement {
5151                    label: None,
5152                    kind: StmtKind::Expression(Expr {
5153                        kind: ExprKind::CodeRef { params, body },
5154                        line,
5155                    }),
5156                    line,
5157                })
5158            }
5159            tok => {
5160                // Sigil-form topic-slot names (`fn $_`, `fn $_<`, `fn $_0`,
5161                // `fn @_`, `fn %_`, …) are also rejected with the same
5162                // foot-gun message as the bareword form. Without this branch
5163                // the user gets a confusing generic "Expected sub name" error.
5164                let topic_name = match &tok {
5165                    Token::ScalarVar(n) | Token::ArrayVar(n) | Token::HashVar(n)
5166                        if Self::is_underscore_topic_slot(n) =>
5167                    {
5168                        Some((
5169                            match &tok {
5170                                Token::ScalarVar(_) => '$',
5171                                Token::ArrayVar(_) => '@',
5172                                Token::HashVar(_) => '%',
5173                                _ => unreachable!(),
5174                            },
5175                            n.clone(),
5176                        ))
5177                    }
5178                    _ => None,
5179                };
5180                if let Some((sigil, n)) = topic_name {
5181                    return Err(self.syntax_err(
5182                        format!(
5183                            "`fn {}{}` would shadow the topic-slot scalar; pick a different name",
5184                            sigil, n
5185                        ),
5186                        self.peek_line(),
5187                    ));
5188                }
5189                // Sigil-form Perl special variables / globals — same foot-gun.
5190                // Catches `fn $@`, `fn $!`, `fn $/`, `fn $\\`, `fn $,`, `fn $;`,
5191                // `fn $"`, `fn $.`, `fn $0`, `fn $$`, `fn $?`, `fn $1`-`$9`,
5192                // `fn $^I`, `fn @ARGV`, `fn @INC`, `fn %ENV`, `fn %SIG`, etc.
5193                let special_var = match &tok {
5194                    Token::ScalarVar(n) | Token::ArrayVar(n) | Token::HashVar(n) => Some((
5195                        match &tok {
5196                            Token::ScalarVar(_) => '$',
5197                            Token::ArrayVar(_) => '@',
5198                            Token::HashVar(_) => '%',
5199                            _ => unreachable!(),
5200                        },
5201                        n.clone(),
5202                    )),
5203                    _ => None,
5204                };
5205                if let Some((sigil, n)) = special_var {
5206                    return Err(self.syntax_err(
5207                        format!(
5208                            "`fn {}{}` would shadow a Perl special variable / global; pick a different name",
5209                            sigil, n
5210                        ),
5211                        self.peek_line(),
5212                    ));
5213                }
5214                // After `fn`, `%` lexes as `Token::Percent` (modulo) rather
5215                // than a hash sigil — but `fn %ENV { }`, `fn %SIG { }`,
5216                // `fn %_ { }`, etc. all reach here. Emit the same foot-gun
5217                // message as the sigil-form catch above.
5218                if matches!(tok, Token::Percent) {
5219                    return Err(self.syntax_err(
5220                        "`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 { ... }`",
5221                        self.peek_line(),
5222                    ));
5223                }
5224                Err(self.syntax_err(
5225                    format!("Expected sub name, `(`, `{{`, or `:`, got {:?}", tok),
5226                    self.peek_line(),
5227                ))
5228            }
5229        }
5230    }
5231
5232    /// `before|after|around "<glob>" { ... }` — register AOP advice.
5233    /// The pattern is a glob (`*`, `?`) matched against the called sub's bare name.
5234    fn parse_advice_decl(&mut self, kind: crate::ast::AdviceKind) -> StrykeResult<Statement> {
5235        let line = self.peek_line();
5236        self.advance(); // before/after/around
5237        let pattern = match self.advance() {
5238            (Token::SingleString(s), _) | (Token::DoubleString(s), _) => s,
5239            (tok, err_line) => {
5240                return Err(self.syntax_err(
5241                    format!(
5242                        "Expected string-literal pattern after `{}`, got {:?}",
5243                        match kind {
5244                            crate::ast::AdviceKind::Before => "before",
5245                            crate::ast::AdviceKind::After => "after",
5246                            crate::ast::AdviceKind::Around => "around",
5247                        },
5248                        tok
5249                    ),
5250                    err_line,
5251                ));
5252            }
5253        };
5254        let body = self.parse_block()?;
5255        Ok(Statement {
5256            label: None,
5257            kind: StmtKind::AdviceDecl {
5258                kind,
5259                pattern,
5260                body,
5261            },
5262            line,
5263        })
5264    }
5265
5266    /// `struct Name { field => Type, ... ; fn method { } }`
5267    fn parse_struct_decl(&mut self) -> StrykeResult<Statement> {
5268        let line = self.peek_line();
5269        self.advance(); // struct
5270        let raw_name = self.parse_package_qualified_identifier().map_err(|_| {
5271            self.syntax_err(
5272                format!("Expected struct name, got {:?}", self.peek()),
5273                self.peek_line(),
5274            )
5275        })?;
5276        let name = if raw_name.contains("::") || self.current_package == "main" {
5277            raw_name
5278        } else {
5279            format!("{}::{}", self.current_package, raw_name)
5280        };
5281        self.expect(&Token::LBrace)?;
5282        let mut fields = Vec::new();
5283        let mut methods = Vec::new();
5284        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
5285            // Check for method definition: `fn name { }` or `fn name { }`
5286            let is_method = match self.peek() {
5287                Token::Ident(s) => s == "fn" || s == "sub",
5288                _ => false,
5289            };
5290            if is_method {
5291                let is_sub_keyword = matches!(self.peek(), Token::Ident(ref s) if s == "sub");
5292                self.advance(); // fn/sub
5293                let method_name = match self.advance() {
5294                    (Token::Ident(n), _) => n,
5295                    (tok, err_line) => {
5296                        return Err(self
5297                            .syntax_err(format!("Expected method name, got {:?}", tok), err_line))
5298                    }
5299                };
5300                // Parse optional signature: `($self, $arg: Type, ...)`
5301                let params = if self.eat(&Token::LParen) {
5302                    let p = self.parse_sub_signature_param_list()?;
5303                    self.expect(&Token::RParen)?;
5304                    p
5305                } else {
5306                    Vec::new()
5307                };
5308                let body = if is_sub_keyword {
5309                    self.parse_block()?
5310                } else {
5311                    self.parse_fn_eq_body_or_block(false)?
5312                };
5313                methods.push(crate::ast::StructMethod {
5314                    name: method_name,
5315                    params,
5316                    body,
5317                });
5318                // Optional trailing comma/semicolon after method
5319                self.eat(&Token::Comma);
5320                self.eat(&Token::Semicolon);
5321                continue;
5322            }
5323
5324            let field_name = match self.advance() {
5325                (Token::Ident(n), _) => n,
5326                (tok, err_line) => {
5327                    return Err(
5328                        self.syntax_err(format!("Expected field name, got {:?}", tok), err_line)
5329                    )
5330                }
5331            };
5332            // Support three forms:
5333            //   - `field => Type`   (Perl-style fat-comma)
5334            //   - `field: Type`     (Rust/class-style colon)
5335            //   - bare `field`      (implies Any type)
5336            let ty = if self.eat(&Token::FatArrow) || self.eat(&Token::Colon) {
5337                self.parse_type_name()?
5338            } else {
5339                crate::ast::PerlTypeName::Any
5340            };
5341            let default = if self.eat(&Token::Assign) {
5342                // Use parse_ternary to avoid consuming commas (next field separator)
5343                Some(self.parse_ternary()?)
5344            } else {
5345                None
5346            };
5347            fields.push(StructField {
5348                name: field_name,
5349                ty,
5350                default,
5351            });
5352            if !self.eat(&Token::Comma) {
5353                // Also allow semicolons as field separators
5354                self.eat(&Token::Semicolon);
5355            }
5356        }
5357        self.expect(&Token::RBrace)?;
5358        self.eat(&Token::Semicolon);
5359        Ok(Statement {
5360            label: None,
5361            kind: StmtKind::StructDecl {
5362                def: StructDef {
5363                    name,
5364                    fields,
5365                    methods,
5366                },
5367            },
5368            line,
5369        })
5370    }
5371
5372    /// `enum Name { Variant1, Variant2 => Type, ... }`
5373    fn parse_enum_decl(&mut self) -> StrykeResult<Statement> {
5374        let line = self.peek_line();
5375        self.advance(); // enum
5376        let raw_name = self.parse_package_qualified_identifier().map_err(|_| {
5377            self.syntax_err(
5378                format!("Expected enum name, got {:?}", self.peek()),
5379                self.peek_line(),
5380            )
5381        })?;
5382        let name = if raw_name.contains("::") || self.current_package == "main" {
5383            raw_name
5384        } else {
5385            format!("{}::{}", self.current_package, raw_name)
5386        };
5387        self.expect(&Token::LBrace)?;
5388        let mut variants = Vec::new();
5389        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
5390            let variant_name = match self.advance() {
5391                (Token::Ident(n), _) => n,
5392                (tok, err_line) => {
5393                    return Err(
5394                        self.syntax_err(format!("Expected variant name, got {:?}", tok), err_line)
5395                    )
5396                }
5397            };
5398            let ty = if self.eat(&Token::FatArrow) {
5399                Some(self.parse_type_name()?)
5400            } else {
5401                None
5402            };
5403            variants.push(EnumVariant {
5404                name: variant_name,
5405                ty,
5406            });
5407            if !self.eat(&Token::Comma) {
5408                self.eat(&Token::Semicolon);
5409            }
5410        }
5411        self.expect(&Token::RBrace)?;
5412        self.eat(&Token::Semicolon);
5413        Ok(Statement {
5414            label: None,
5415            kind: StmtKind::EnumDecl {
5416                def: EnumDef { name, variants },
5417            },
5418            line,
5419        })
5420    }
5421
5422    /// `[abstract|final] class Name extends Parent impl Trait { fields; methods }`
5423    fn parse_class_decl(&mut self, is_abstract: bool, is_final: bool) -> StrykeResult<Statement> {
5424        use crate::ast::{ClassDef, ClassField, ClassMethod, ClassStaticField, Visibility};
5425        let line = self.peek_line();
5426        self.advance(); // class
5427        let raw_name = self.parse_package_qualified_identifier().map_err(|_| {
5428            self.syntax_err(
5429                format!("Expected class name, got {:?}", self.peek()),
5430                self.peek_line(),
5431            )
5432        })?;
5433        // Bare `class Point` inside `package Geo` registers as `Geo::Point`,
5434        // matching the unqualified-fn rule. Already-qualified names pass
5435        // through unchanged, and `main` keeps the bare spelling so
5436        // existing test code that calls `Point->new(...)` still resolves.
5437        let name = if raw_name.contains("::") || self.current_package == "main" {
5438            raw_name
5439        } else {
5440            format!("{}::{}", self.current_package, raw_name)
5441        };
5442
5443        // Parse `extends Parent1, Parent2` (each may be namespaced: `Foo::Base`)
5444        let mut extends = Vec::new();
5445        if matches!(self.peek(), Token::Ident(ref s) if s == "extends") {
5446            self.advance(); // extends
5447            loop {
5448                let parent = self.parse_package_qualified_identifier().map_err(|_| {
5449                    self.syntax_err(
5450                        format!(
5451                            "Expected parent class name after `extends`, got {:?}",
5452                            self.peek()
5453                        ),
5454                        self.peek_line(),
5455                    )
5456                })?;
5457                extends.push(parent);
5458                if !self.eat(&Token::Comma) {
5459                    break;
5460                }
5461            }
5462        }
5463
5464        // Parse `impl Trait1, Trait2` (each may be namespaced: `Foo::Trait`)
5465        let mut implements = Vec::new();
5466        if matches!(self.peek(), Token::Ident(ref s) if s == "impl") {
5467            self.advance(); // impl
5468            loop {
5469                let trait_name = self.parse_package_qualified_identifier().map_err(|_| {
5470                    self.syntax_err(
5471                        format!("Expected trait name after `impl`, got {:?}", self.peek()),
5472                        self.peek_line(),
5473                    )
5474                })?;
5475                implements.push(trait_name);
5476                if !self.eat(&Token::Comma) {
5477                    break;
5478                }
5479            }
5480        }
5481
5482        self.expect(&Token::LBrace)?;
5483        let mut fields = Vec::new();
5484        let mut methods = Vec::new();
5485        let mut static_fields = Vec::new();
5486
5487        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
5488            // Check for visibility modifier
5489            let visibility = match self.peek() {
5490                Token::Ident(ref s) if s == "pub" => {
5491                    self.advance();
5492                    Visibility::Public
5493                }
5494                Token::Ident(ref s) if s == "priv" => {
5495                    self.advance();
5496                    Visibility::Private
5497                }
5498                Token::Ident(ref s) if s == "prot" => {
5499                    self.advance();
5500                    Visibility::Protected
5501                }
5502                _ => Visibility::Public, // default public
5503            };
5504
5505            // Check for static field: `static name: Type = default`
5506            if matches!(self.peek(), Token::Ident(ref s) if s == "static") {
5507                self.advance(); // static
5508
5509                // Could be a static method (`static fn`) or static field
5510                if matches!(self.peek(), Token::Ident(ref s) if s == "fn" || s == "sub") {
5511                    // static fn is same as fn Self.name — handled below but not here
5512                    return Err(self.syntax_err(
5513                        "use `fn Self.name` for static methods, not `static fn`",
5514                        self.peek_line(),
5515                    ));
5516                }
5517
5518                let field_name = match self.advance() {
5519                    (Token::Ident(n), _) => n,
5520                    (tok, err_line) => {
5521                        return Err(self.syntax_err(
5522                            format!("Expected static field name, got {:?}", tok),
5523                            err_line,
5524                        ))
5525                    }
5526                };
5527
5528                let ty = if self.eat(&Token::Colon) {
5529                    self.parse_type_name()?
5530                } else {
5531                    crate::ast::PerlTypeName::Any
5532                };
5533
5534                let default = if self.eat(&Token::Assign) {
5535                    Some(self.parse_ternary()?)
5536                } else {
5537                    None
5538                };
5539
5540                static_fields.push(ClassStaticField {
5541                    name: field_name,
5542                    ty,
5543                    visibility,
5544                    default,
5545                });
5546
5547                if !self.eat(&Token::Comma) {
5548                    self.eat(&Token::Semicolon);
5549                }
5550                continue;
5551            }
5552
5553            // Check for `final` modifier before fn
5554            let method_is_final = matches!(self.peek(), Token::Ident(ref s) if s == "final");
5555            if method_is_final {
5556                self.advance(); // final
5557            }
5558
5559            // Check for method: `fn name` or `fn Self.name` (static)
5560            let is_method = matches!(self.peek(), Token::Ident(ref s) if s == "fn" || s == "sub");
5561            if is_method {
5562                self.advance(); // fn/sub
5563
5564                // Check for static method: `fn Self.name`
5565                let is_static = matches!(self.peek(), Token::Ident(ref s) if s == "Self");
5566                if is_static {
5567                    self.advance(); // Self
5568                    self.expect(&Token::Dot)?;
5569                }
5570
5571                let method_name = match self.advance() {
5572                    (Token::Ident(n), _) => n,
5573                    (tok, err_line) => {
5574                        return Err(self
5575                            .syntax_err(format!("Expected method name, got {:?}", tok), err_line))
5576                    }
5577                };
5578
5579                // Parse optional signature
5580                let params = if self.eat(&Token::LParen) {
5581                    let p = self.parse_sub_signature_param_list()?;
5582                    self.expect(&Token::RParen)?;
5583                    p
5584                } else {
5585                    Vec::new()
5586                };
5587
5588                // Body: `{ ... }`, or `= expr` (same rules as top-level `fn`), or omitted (abstract)
5589                let body = if let Some(b) = self.try_parse_fn_assign_shorthand_body()? {
5590                    Some(b)
5591                } else if matches!(self.peek(), Token::LBrace) {
5592                    Some(self.parse_block()?)
5593                } else {
5594                    None
5595                };
5596
5597                methods.push(ClassMethod {
5598                    name: method_name,
5599                    params,
5600                    body,
5601                    visibility,
5602                    is_static,
5603                    is_final: method_is_final,
5604                });
5605                self.eat(&Token::Comma);
5606                self.eat(&Token::Semicolon);
5607                continue;
5608            } else if method_is_final {
5609                return Err(self.syntax_err("`final` must be followed by `fn`", self.peek_line()));
5610            }
5611
5612            // Parse field: `name: Type = default`
5613            let field_name = match self.advance() {
5614                (Token::Ident(n), _) => n,
5615                (tok, err_line) => {
5616                    return Err(
5617                        self.syntax_err(format!("Expected field name, got {:?}", tok), err_line)
5618                    )
5619                }
5620            };
5621
5622            // Type via colon (`name: Type`) OR fat-comma (`name => Type`).
5623            // The Perl-flavored struct-style fat-comma is accepted on
5624            // classes for symmetry with struct fields.
5625            let ty = if self.eat(&Token::Colon) || self.eat(&Token::FatArrow) {
5626                self.parse_type_name()?
5627            } else {
5628                crate::ast::PerlTypeName::Any
5629            };
5630
5631            // Default value after `=`
5632            let default = if self.eat(&Token::Assign) {
5633                Some(self.parse_ternary()?)
5634            } else {
5635                None
5636            };
5637
5638            fields.push(ClassField {
5639                name: field_name,
5640                ty,
5641                visibility,
5642                default,
5643            });
5644
5645            if !self.eat(&Token::Comma) {
5646                self.eat(&Token::Semicolon);
5647            }
5648        }
5649
5650        self.expect(&Token::RBrace)?;
5651        self.eat(&Token::Semicolon);
5652
5653        Ok(Statement {
5654            label: None,
5655            kind: StmtKind::ClassDecl {
5656                def: ClassDef {
5657                    name,
5658                    is_abstract,
5659                    is_final,
5660                    extends,
5661                    implements,
5662                    fields,
5663                    methods,
5664                    static_fields,
5665                },
5666            },
5667            line,
5668        })
5669    }
5670
5671    /// `trait Name { fn required; fn with_default { } }`
5672    fn parse_trait_decl(&mut self) -> StrykeResult<Statement> {
5673        use crate::ast::{ClassMethod, TraitDef, Visibility};
5674        let line = self.peek_line();
5675        self.advance(); // trait
5676        let raw_name = self.parse_package_qualified_identifier().map_err(|_| {
5677            self.syntax_err(
5678                format!("Expected trait name, got {:?}", self.peek()),
5679                self.peek_line(),
5680            )
5681        })?;
5682        let name = if raw_name.contains("::") || self.current_package == "main" {
5683            raw_name
5684        } else {
5685            format!("{}::{}", self.current_package, raw_name)
5686        };
5687
5688        self.expect(&Token::LBrace)?;
5689        let mut methods = Vec::new();
5690
5691        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
5692            // Optional visibility
5693            let visibility = match self.peek() {
5694                Token::Ident(ref s) if s == "pub" => {
5695                    self.advance();
5696                    Visibility::Public
5697                }
5698                Token::Ident(ref s) if s == "priv" => {
5699                    self.advance();
5700                    Visibility::Private
5701                }
5702                Token::Ident(ref s) if s == "prot" => {
5703                    self.advance();
5704                    Visibility::Protected
5705                }
5706                _ => Visibility::Public,
5707            };
5708
5709            // Expect `fn` or `sub`
5710            if !matches!(self.peek(), Token::Ident(ref s) if s == "fn" || s == "sub") {
5711                return Err(self.syntax_err("Expected `fn` in trait definition", self.peek_line()));
5712            }
5713            self.advance(); // fn/sub
5714
5715            let method_name = match self.advance() {
5716                (Token::Ident(n), _) => n,
5717                (tok, err_line) => {
5718                    return Err(
5719                        self.syntax_err(format!("Expected method name, got {:?}", tok), err_line)
5720                    )
5721                }
5722            };
5723
5724            // Optional signature
5725            let params = if self.eat(&Token::LParen) {
5726                let p = self.parse_sub_signature_param_list()?;
5727                self.expect(&Token::RParen)?;
5728                p
5729            } else {
5730                Vec::new()
5731            };
5732
5733            // Body: `{ ... }`, `= expr`, or omitted (required method)
5734            let body = if let Some(b) = self.try_parse_fn_assign_shorthand_body()? {
5735                Some(b)
5736            } else if matches!(self.peek(), Token::LBrace) {
5737                Some(self.parse_block()?)
5738            } else {
5739                None
5740            };
5741
5742            methods.push(ClassMethod {
5743                name: method_name,
5744                params,
5745                body,
5746                visibility,
5747                is_static: false,
5748                is_final: false,
5749            });
5750
5751            self.eat(&Token::Comma);
5752            self.eat(&Token::Semicolon);
5753        }
5754
5755        self.expect(&Token::RBrace)?;
5756        self.eat(&Token::Semicolon);
5757
5758        Ok(Statement {
5759            label: None,
5760            kind: StmtKind::TraitDecl {
5761                def: TraitDef { name, methods },
5762            },
5763            line,
5764        })
5765    }
5766
5767    fn local_simple_target_to_var_decl(target: &Expr) -> Option<VarDecl> {
5768        match &target.kind {
5769            ExprKind::ScalarVar(name) => Some(VarDecl {
5770                sigil: Sigil::Scalar,
5771                name: name.clone(),
5772                initializer: None,
5773                frozen: false,
5774                type_annotation: None,
5775                list_context: false,
5776            }),
5777            ExprKind::ArrayVar(name) => Some(VarDecl {
5778                sigil: Sigil::Array,
5779                name: name.clone(),
5780                initializer: None,
5781                frozen: false,
5782                type_annotation: None,
5783                list_context: false,
5784            }),
5785            ExprKind::HashVar(name) => Some(VarDecl {
5786                sigil: Sigil::Hash,
5787                name: name.clone(),
5788                initializer: None,
5789                frozen: false,
5790                type_annotation: None,
5791                list_context: false,
5792            }),
5793            ExprKind::Typeglob(name) => Some(VarDecl {
5794                sigil: Sigil::Typeglob,
5795                name: name.clone(),
5796                initializer: None,
5797                frozen: false,
5798                type_annotation: None,
5799                list_context: false,
5800            }),
5801            _ => None,
5802        }
5803    }
5804
5805    fn parse_decl_array_destructure(
5806        &mut self,
5807        keyword: &str,
5808        line: usize,
5809    ) -> StrykeResult<Statement> {
5810        self.expect(&Token::LBracket)?;
5811        let elems = self.parse_match_array_elems_until_rbracket()?;
5812        self.expect(&Token::Assign)?;
5813        self.suppress_scalar_hash_brace += 1;
5814        let rhs = self.parse_expression()?;
5815        self.suppress_scalar_hash_brace -= 1;
5816        let stmt = self.desugar_array_destructure(keyword, line, elems, rhs)?;
5817        self.parse_stmt_postfix_modifier(stmt)
5818    }
5819
5820    fn parse_decl_hash_destructure(
5821        &mut self,
5822        keyword: &str,
5823        line: usize,
5824    ) -> StrykeResult<Statement> {
5825        let MatchPattern::Hash(pairs) = self.parse_match_hash_pattern()? else {
5826            unreachable!("parse_match_hash_pattern returns Hash");
5827        };
5828        self.expect(&Token::Assign)?;
5829        self.suppress_scalar_hash_brace += 1;
5830        let rhs = self.parse_expression()?;
5831        self.suppress_scalar_hash_brace -= 1;
5832        let stmt = self.desugar_hash_destructure(keyword, line, pairs, rhs)?;
5833        self.parse_stmt_postfix_modifier(stmt)
5834    }
5835
5836    fn desugar_array_destructure(
5837        &mut self,
5838        keyword: &str,
5839        line: usize,
5840        elems: Vec<MatchArrayElem>,
5841        rhs: Expr,
5842    ) -> StrykeResult<Statement> {
5843        let tmp = format!("__stryke_ds_{}", self.alloc_desugar_tmp());
5844        let mut stmts: Vec<Statement> = Vec::new();
5845        stmts.push(destructure_stmt_from_var_decls(
5846            keyword,
5847            vec![VarDecl {
5848                sigil: Sigil::Scalar,
5849                name: tmp.clone(),
5850                initializer: Some(rhs),
5851                frozen: false,
5852                type_annotation: None,
5853                list_context: false,
5854            }],
5855            line,
5856        ));
5857
5858        let has_rest = elems
5859            .iter()
5860            .any(|e| matches!(e, MatchArrayElem::Rest | MatchArrayElem::RestBind(_)));
5861        let fixed_slots = elems
5862            .iter()
5863            .filter(|e| {
5864                matches!(
5865                    e,
5866                    MatchArrayElem::CaptureScalar(_) | MatchArrayElem::Expr(_)
5867                )
5868            })
5869            .count();
5870        if !has_rest {
5871            let cond = Expr {
5872                kind: ExprKind::BinOp {
5873                    left: Box::new(destructure_expr_array_len(&tmp, line)),
5874                    op: BinOp::NumEq,
5875                    right: Box::new(Expr {
5876                        kind: ExprKind::Integer(fixed_slots as i64),
5877                        line,
5878                    }),
5879                },
5880                line,
5881            };
5882            stmts.push(destructure_stmt_unless_die(
5883                line,
5884                cond,
5885                "array destructure: length mismatch",
5886            ));
5887        }
5888
5889        let mut idx: i64 = 0;
5890        for elem in elems {
5891            match elem {
5892                MatchArrayElem::Rest => break,
5893                MatchArrayElem::RestBind(name) => {
5894                    let list_source = Expr {
5895                        kind: ExprKind::Deref {
5896                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
5897                            kind: Sigil::Array,
5898                        },
5899                        line,
5900                    };
5901                    let last_ix = Expr {
5902                        kind: ExprKind::BinOp {
5903                            left: Box::new(destructure_expr_array_len(&tmp, line)),
5904                            op: BinOp::Sub,
5905                            right: Box::new(Expr {
5906                                kind: ExprKind::Integer(1),
5907                                line,
5908                            }),
5909                        },
5910                        line,
5911                    };
5912                    let range = Expr {
5913                        kind: ExprKind::Range {
5914                            from: Box::new(Expr {
5915                                kind: ExprKind::Integer(idx),
5916                                line,
5917                            }),
5918                            to: Box::new(last_ix),
5919                            exclusive: false,
5920                            step: None,
5921                        },
5922                        line,
5923                    };
5924                    let slice = Expr {
5925                        kind: ExprKind::AnonymousListSlice {
5926                            source: Box::new(list_source),
5927                            indices: vec![range],
5928                        },
5929                        line,
5930                    };
5931                    stmts.push(destructure_stmt_from_var_decls(
5932                        keyword,
5933                        vec![VarDecl {
5934                            sigil: Sigil::Array,
5935                            name,
5936                            initializer: Some(slice),
5937                            frozen: false,
5938                            type_annotation: None,
5939                            list_context: false,
5940                        }],
5941                        line,
5942                    ));
5943                    break;
5944                }
5945                MatchArrayElem::CaptureScalar(name) => {
5946                    let arrow = Expr {
5947                        kind: ExprKind::ArrowDeref {
5948                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
5949                            index: Box::new(Expr {
5950                                kind: ExprKind::Integer(idx),
5951                                line,
5952                            }),
5953                            kind: DerefKind::Array,
5954                        },
5955                        line,
5956                    };
5957                    stmts.push(destructure_stmt_from_var_decls(
5958                        keyword,
5959                        vec![VarDecl {
5960                            sigil: Sigil::Scalar,
5961                            name,
5962                            initializer: Some(arrow),
5963                            frozen: false,
5964                            list_context: false,
5965                            type_annotation: None,
5966                        }],
5967                        line,
5968                    ));
5969                    idx += 1;
5970                }
5971                MatchArrayElem::Expr(e) => {
5972                    let elem_subj = Expr {
5973                        kind: ExprKind::ArrowDeref {
5974                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
5975                            index: Box::new(Expr {
5976                                kind: ExprKind::Integer(idx),
5977                                line,
5978                            }),
5979                            kind: DerefKind::Array,
5980                        },
5981                        line,
5982                    };
5983                    let match_expr = Expr {
5984                        kind: ExprKind::AlgebraicMatch {
5985                            subject: Box::new(elem_subj),
5986                            arms: vec![
5987                                MatchArm {
5988                                    pattern: MatchPattern::Value(Box::new(e.clone())),
5989                                    guard: None,
5990                                    body: Expr {
5991                                        kind: ExprKind::Integer(0),
5992                                        line,
5993                                    },
5994                                },
5995                                MatchArm {
5996                                    pattern: MatchPattern::Any,
5997                                    guard: None,
5998                                    body: Expr {
5999                                        kind: ExprKind::Die(vec![Expr {
6000                                            kind: ExprKind::String(
6001                                                "array destructure: element pattern mismatch"
6002                                                    .to_string(),
6003                                            ),
6004                                            line,
6005                                        }]),
6006                                        line,
6007                                    },
6008                                },
6009                            ],
6010                        },
6011                        line,
6012                    };
6013                    stmts.push(Statement {
6014                        label: None,
6015                        kind: StmtKind::Expression(match_expr),
6016                        line,
6017                    });
6018                    idx += 1;
6019                }
6020            }
6021        }
6022
6023        Ok(Statement {
6024            label: None,
6025            kind: StmtKind::StmtGroup(stmts),
6026            line,
6027        })
6028    }
6029
6030    fn desugar_hash_destructure(
6031        &mut self,
6032        keyword: &str,
6033        line: usize,
6034        pairs: Vec<MatchHashPair>,
6035        rhs: Expr,
6036    ) -> StrykeResult<Statement> {
6037        let tmp = format!("__stryke_ds_{}", self.alloc_desugar_tmp());
6038        let mut stmts: Vec<Statement> = Vec::new();
6039        stmts.push(destructure_stmt_from_var_decls(
6040            keyword,
6041            vec![VarDecl {
6042                sigil: Sigil::Scalar,
6043                name: tmp.clone(),
6044                initializer: Some(rhs),
6045                frozen: false,
6046                type_annotation: None,
6047                list_context: false,
6048            }],
6049            line,
6050        ));
6051
6052        for pair in pairs {
6053            match pair {
6054                MatchHashPair::KeyOnly { key } => {
6055                    let exists_op = Expr {
6056                        kind: ExprKind::Exists(Box::new(Expr {
6057                            kind: ExprKind::ArrowDeref {
6058                                expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
6059                                index: Box::new(key),
6060                                kind: DerefKind::Hash,
6061                            },
6062                            line,
6063                        })),
6064                        line,
6065                    };
6066                    stmts.push(destructure_stmt_unless_die(
6067                        line,
6068                        exists_op,
6069                        "hash destructure: missing required key",
6070                    ));
6071                }
6072                MatchHashPair::Capture { key, name } => {
6073                    let init = Expr {
6074                        kind: ExprKind::ArrowDeref {
6075                            expr: Box::new(destructure_expr_scalar_tmp(&tmp, line)),
6076                            index: Box::new(key),
6077                            kind: DerefKind::Hash,
6078                        },
6079                        line,
6080                    };
6081                    stmts.push(destructure_stmt_from_var_decls(
6082                        keyword,
6083                        vec![VarDecl {
6084                            sigil: Sigil::Scalar,
6085                            name,
6086                            initializer: Some(init),
6087                            frozen: false,
6088                            type_annotation: None,
6089                            list_context: false,
6090                        }],
6091                        line,
6092                    ));
6093                }
6094            }
6095        }
6096
6097        Ok(Statement {
6098            label: None,
6099            kind: StmtKind::StmtGroup(stmts),
6100            line,
6101        })
6102    }
6103
6104    fn parse_my_our_local(
6105        &mut self,
6106        keyword: &str,
6107        allow_type_annotation: bool,
6108    ) -> StrykeResult<Statement> {
6109        let line = self.peek_line();
6110        self.advance(); // 'my'/'our'/'local'
6111
6112        if keyword == "local"
6113            && !matches!(self.peek(), Token::LParen | Token::LBracket | Token::LBrace)
6114        {
6115            let target = self.parse_postfix()?;
6116            let mut initializer: Option<Expr> = None;
6117            if self.eat(&Token::Assign) {
6118                initializer = Some(self.parse_expression()?);
6119            } else if matches!(
6120                self.peek(),
6121                Token::OrAssign | Token::DefinedOrAssign | Token::AndAssign
6122            ) {
6123                if matches!(&target.kind, ExprKind::Typeglob(_)) {
6124                    return Err(self.syntax_err(
6125                        "compound assignment on typeglob declaration is not supported",
6126                        self.peek_line(),
6127                    ));
6128                }
6129                let op = match self.peek().clone() {
6130                    Token::OrAssign => BinOp::LogOr,
6131                    Token::DefinedOrAssign => BinOp::DefinedOr,
6132                    Token::AndAssign => BinOp::LogAnd,
6133                    _ => unreachable!(),
6134                };
6135                self.advance();
6136                let rhs = self.parse_assign_expr()?;
6137                let tgt_line = target.line;
6138                initializer = Some(Expr {
6139                    kind: ExprKind::CompoundAssign {
6140                        target: Box::new(target.clone()),
6141                        op,
6142                        value: Box::new(rhs),
6143                    },
6144                    line: tgt_line,
6145                });
6146            }
6147
6148            let kind = if let Some(mut decl) = Self::local_simple_target_to_var_decl(&target) {
6149                decl.initializer = initializer;
6150                StmtKind::Local(vec![decl])
6151            } else {
6152                StmtKind::LocalExpr {
6153                    target,
6154                    initializer,
6155                }
6156            };
6157            let stmt = Statement {
6158                label: None,
6159                kind,
6160                line,
6161            };
6162            return self.parse_stmt_postfix_modifier(stmt);
6163        }
6164
6165        if matches!(self.peek(), Token::LBracket) {
6166            return self.parse_decl_array_destructure(keyword, line);
6167        }
6168        if matches!(self.peek(), Token::LBrace) {
6169            return self.parse_decl_hash_destructure(keyword, line);
6170        }
6171
6172        let mut decls = Vec::new();
6173        let used_parens = self.eat(&Token::LParen);
6174
6175        if used_parens {
6176            // my ($a, @b, %c)
6177            while !matches!(self.peek(), Token::RParen | Token::Eof) {
6178                let decl = self.parse_var_decl(allow_type_annotation)?;
6179                decls.push(decl);
6180                if !self.eat(&Token::Comma) {
6181                    break;
6182                }
6183            }
6184            self.expect(&Token::RParen)?;
6185        } else {
6186            decls.push(self.parse_var_decl(allow_type_annotation)?);
6187        }
6188        // my ($x) = @a  → list context on the scalar (gets first element, not count)
6189        if used_parens {
6190            for decl in &mut decls {
6191                decl.list_context = true;
6192            }
6193        }
6194
6195        // Optional initializer: my $x = expr — plus `our @EXPORT = our @EXPORT_OK = qw(...)` (Try::Tiny).
6196        if self.eat(&Token::Assign) {
6197            if keyword == "our" && decls.len() == 1 {
6198                while matches!(self.peek(), Token::Ident(ref i) if i == "our") {
6199                    self.advance();
6200                    decls.push(self.parse_var_decl(allow_type_annotation)?);
6201                    if !self.eat(&Token::Assign) {
6202                        return Err(self.syntax_err(
6203                            "expected `=` after `our` in chained our-declaration",
6204                            self.peek_line(),
6205                        ));
6206                    }
6207                }
6208            }
6209            let rhs_start_pos = self.pos;
6210            let mut val = self.parse_expression()?;
6211            let rhs_end_pos = self.pos;
6212            // Stryke implicit-coderef sugar: when the RHS contains a
6213            // *free* bare topic-slot reference (`_`, `_0`, `_1`, `_<`,
6214            // `_<<`, etc. — no `$` sigil), auto-wrap the RHS in
6215            // `fn { ... }`. Forces consistent coderef semantics across
6216            // every topic-using form:
6217            //   `my $sq  = _ * _`                     → CODE ref
6218            //   `my $up  = uc _`                      → CODE ref
6219            //   `my $rev = ~> _ >{...} rev join("")`  → CODE ref
6220            // To compute eagerly using the current topic, use the
6221            // explicit `$_` / `$_N` / `$_<` sigil-prefixed forms —
6222            // those keep Perl semantics and never auto-wrap.
6223            //
6224            // "Free" = at brace-depth 0 within the RHS token stream.
6225            // Any `_` inside `{ ... }` (closure body, hash literal,
6226            // map/grep/sort/match block) is bound to whatever defines
6227            // that block and doesn't trigger the wrap, so
6228            //   `my $r = call(fn { _ < 4 })`   — `_` is inner-fn's
6229            //   `my $h = { k => _ }`           — `_` is hash value at depth 1
6230            //   `my $kind = match { _ => x }`  — `_` is wildcard pattern
6231            // all stay eager.
6232            if !crate::compat_mode()
6233                && self.block_depth == 0
6234                && decls.len() == 1
6235                && matches!(decls[0].sigil, Sigil::Scalar)
6236                && !matches!(
6237                    val.kind,
6238                    ExprKind::CodeRef { .. }
6239                        | ExprKind::SubroutineRef(_)
6240                        | ExprKind::SubroutineCodeRef(_)
6241                        | ExprKind::DynamicSubCodeRef(_)
6242                )
6243                && self.rhs_has_free_bare_topic_slot(rhs_start_pos, rhs_end_pos)
6244            {
6245                let val_line = val.line;
6246                val = Expr {
6247                    kind: ExprKind::CodeRef {
6248                        params: Vec::new(),
6249                        body: vec![Statement {
6250                            label: None,
6251                            kind: StmtKind::Expression(val),
6252                            line: val_line,
6253                        }],
6254                    },
6255                    line: val_line,
6256                };
6257            }
6258            // Validate assignment for single variable declarations (not destructuring)
6259            // `my ($a, $b) = (1, 2)` is destructuring, not scalar-from-list
6260            if !crate::compat_mode() && decls.len() == 1 {
6261                let decl = &decls[0];
6262                let target_kind = match decl.sigil {
6263                    Sigil::Scalar => ExprKind::ScalarVar(decl.name.clone()),
6264                    Sigil::Array => ExprKind::ArrayVar(decl.name.clone()),
6265                    Sigil::Hash => ExprKind::HashVar(decl.name.clone()),
6266                    Sigil::Typeglob => {
6267                        // Skip validation for typeglob
6268                        if decls.len() == 1 {
6269                            decls[0].initializer = Some(val);
6270                        } else {
6271                            for d in &mut decls {
6272                                d.initializer = Some(val.clone());
6273                            }
6274                        }
6275                        return Ok(Statement {
6276                            label: None,
6277                            kind: match keyword {
6278                                "my" => StmtKind::My(decls),
6279                                "mysync" => StmtKind::MySync(decls),
6280                                "our" => StmtKind::Our(decls),
6281                                "oursync" => StmtKind::OurSync(decls),
6282                                "local" => StmtKind::Local(decls),
6283                                "state" => StmtKind::State(decls),
6284                                _ => unreachable!(),
6285                            },
6286                            line,
6287                        });
6288                    }
6289                };
6290                let target = Expr {
6291                    kind: target_kind,
6292                    line,
6293                };
6294                self.validate_assignment(&target, &val, line)?;
6295            }
6296            if decls.len() == 1 {
6297                decls[0].initializer = Some(val);
6298            } else {
6299                for decl in &mut decls {
6300                    decl.initializer = Some(val.clone());
6301                }
6302            }
6303        } else if decls.len() == 1 {
6304            // `our $Verbose ||= 0` (Exporter.pm) — compound assign on a single decl
6305            let op = match self.peek().clone() {
6306                Token::OrAssign => Some(BinOp::LogOr),
6307                Token::DefinedOrAssign => Some(BinOp::DefinedOr),
6308                Token::AndAssign => Some(BinOp::LogAnd),
6309                _ => None,
6310            };
6311            if let Some(op) = op {
6312                let d = &decls[0];
6313                if matches!(d.sigil, Sigil::Typeglob) {
6314                    return Err(self.syntax_err(
6315                        "compound assignment on typeglob declaration is not supported",
6316                        self.peek_line(),
6317                    ));
6318                }
6319                self.advance();
6320                let rhs = self.parse_assign_expr()?;
6321                let target = Expr {
6322                    kind: match d.sigil {
6323                        Sigil::Scalar => ExprKind::ScalarVar(d.name.clone()),
6324                        Sigil::Array => ExprKind::ArrayVar(d.name.clone()),
6325                        Sigil::Hash => ExprKind::HashVar(d.name.clone()),
6326                        Sigil::Typeglob => unreachable!(),
6327                    },
6328                    line,
6329                };
6330                decls[0].initializer = Some(Expr {
6331                    kind: ExprKind::CompoundAssign {
6332                        target: Box::new(target),
6333                        op,
6334                        value: Box::new(rhs),
6335                    },
6336                    line,
6337                });
6338            }
6339        }
6340
6341        let kind = match keyword {
6342            "my" => StmtKind::My(decls),
6343            "mysync" => StmtKind::MySync(decls),
6344            "our" => StmtKind::Our(decls),
6345            "oursync" => StmtKind::OurSync(decls),
6346            "local" => StmtKind::Local(decls),
6347            "state" => StmtKind::State(decls),
6348            _ => unreachable!(),
6349        };
6350        let stmt = Statement {
6351            label: None,
6352            kind,
6353            line,
6354        };
6355        // `my $x = 1 if $y;` — statement modifier applies to the whole declaration (Perl).
6356        self.parse_stmt_postfix_modifier(stmt)
6357    }
6358
6359    fn parse_var_decl(&mut self, allow_type_annotation: bool) -> StrykeResult<VarDecl> {
6360        let mut decl = match self.advance() {
6361            (Token::ScalarVar(name), _) => VarDecl {
6362                sigil: Sigil::Scalar,
6363                name,
6364                initializer: None,
6365                frozen: false,
6366                type_annotation: None,
6367                list_context: false,
6368            },
6369            (Token::ArrayVar(name), _) => VarDecl {
6370                sigil: Sigil::Array,
6371                name,
6372                initializer: None,
6373                frozen: false,
6374                type_annotation: None,
6375                list_context: false,
6376            },
6377            (Token::HashVar(name), line) => {
6378                if !crate::compat_mode() {
6379                    self.check_hash_shadows_reserved(&name, line)?;
6380                }
6381                VarDecl {
6382                    sigil: Sigil::Hash,
6383                    name,
6384                    initializer: None,
6385                    frozen: false,
6386                    type_annotation: None,
6387                    list_context: false,
6388                }
6389            }
6390            (Token::Star, _line) => {
6391                let name = match self.advance() {
6392                    (Token::Ident(n), _) => n,
6393                    (tok, l) => {
6394                        return Err(self
6395                            .syntax_err(format!("Expected identifier after *, got {:?}", tok), l));
6396                    }
6397                };
6398                VarDecl {
6399                    sigil: Sigil::Typeglob,
6400                    name,
6401                    initializer: None,
6402                    frozen: false,
6403                    type_annotation: None,
6404                    list_context: false,
6405                }
6406            }
6407            // `my ($a, undef, $c) = (1, 2, 3)` — Perl idiom for discarding a
6408            // slot in a list assignment. The interpreter treats `undef`-named
6409            // scalar decls as throwaway: declared into a unique sink so the
6410            // distribute-to-decls loop advances past the slot.
6411            (Token::Ident(ref kw), _)
6412                if kw == "undef" || (kw == "null" && !crate::compat_mode()) =>
6413            {
6414                VarDecl {
6415                    sigil: Sigil::Scalar,
6416                    // Synthesize a name that user code cannot reference. Each
6417                    // sink slot in a list-assign gets its own unique name so the
6418                    // declarations don't collide.
6419                    name: format!("__undef_sink_{}", self.pos),
6420                    initializer: None,
6421                    frozen: false,
6422                    type_annotation: None,
6423                    list_context: false,
6424                }
6425            }
6426            (tok, line) => {
6427                return Err(self.syntax_err(
6428                    format!("Expected variable in declaration, got {:?}", tok),
6429                    line,
6430                ));
6431            }
6432        };
6433        if allow_type_annotation && self.eat(&Token::Colon) {
6434            let ty = self.parse_type_name()?;
6435            if decl.sigil != Sigil::Scalar {
6436                return Err(self.syntax_err(
6437                    "`: Type` is only valid for scalar declarations (typed my $name : Int)",
6438                    self.peek_line(),
6439                ));
6440            }
6441            decl.type_annotation = Some(ty);
6442        }
6443        Ok(decl)
6444    }
6445
6446    fn parse_type_name(&mut self) -> StrykeResult<PerlTypeName> {
6447        match self.advance() {
6448            (Token::Ident(name), _) => match name.as_str() {
6449                "Int" => Ok(PerlTypeName::Int),
6450                "Str" => Ok(PerlTypeName::Str),
6451                "Float" => Ok(PerlTypeName::Float),
6452                "Bool" => Ok(PerlTypeName::Bool),
6453                "Array" => Ok(PerlTypeName::Array),
6454                "Hash" => Ok(PerlTypeName::Hash),
6455                "Ref" => Ok(PerlTypeName::Ref),
6456                "Any" => Ok(PerlTypeName::Any),
6457                _ => Ok(PerlTypeName::Struct(name)),
6458            },
6459            (tok, err_line) => Err(self.syntax_err(
6460                format!("Expected type name after `:`, got {:?}", tok),
6461                err_line,
6462            )),
6463        }
6464    }
6465
6466    fn parse_package(&mut self) -> StrykeResult<Statement> {
6467        let line = self.peek_line();
6468        self.advance(); // 'package'
6469        let name = match self.advance() {
6470            (Token::Ident(n), _) => n,
6471            (tok, line) => {
6472                return Err(self.syntax_err(format!("Expected package name, got {:?}", tok), line))
6473            }
6474        };
6475        // Handle Foo::Bar
6476        let mut full_name = name;
6477        while self.eat(&Token::PackageSep) {
6478            if let (Token::Ident(part), _) = self.advance() {
6479                full_name = format!("{}::{}", full_name, part);
6480            }
6481        }
6482        self.eat(&Token::Semicolon);
6483        // Track the active package so subsequent `fn name(...)` decls can be
6484        // recognised as `Pkg::name` for shadow-of-builtin checks.
6485        self.current_package = full_name.clone();
6486        Ok(Statement {
6487            label: None,
6488            kind: StmtKind::Package { name: full_name },
6489            line,
6490        })
6491    }
6492
6493    fn parse_use(&mut self) -> StrykeResult<Statement> {
6494        let line = self.peek_line();
6495        // Capture the keyword (`use` or its alias `import`) so error
6496        // messages point at the spelling the user actually typed.
6497        let kw_name = match self.peek() {
6498            Token::Ident(s) if s == "import" => "import",
6499            _ => "use",
6500        };
6501        self.advance(); // 'use' / 'import'
6502        let (tok, tok_line) = self.advance();
6503        match tok {
6504            Token::Float(v) => {
6505                self.eat(&Token::Semicolon);
6506                Ok(Statement {
6507                    label: None,
6508                    kind: StmtKind::UsePerlVersion { version: v },
6509                    line,
6510                })
6511            }
6512            Token::Integer(n) => {
6513                if matches!(self.peek(), Token::Semicolon | Token::Eof) {
6514                    self.eat(&Token::Semicolon);
6515                    Ok(Statement {
6516                        label: None,
6517                        kind: StmtKind::UsePerlVersion { version: n as f64 },
6518                        line,
6519                    })
6520                } else {
6521                    Err(self.syntax_err(
6522                        format!("Expected ';' after {} VERSION (got {:?})", kw_name, self.peek()),
6523                        line,
6524                    ))
6525                }
6526            }
6527            Token::Ident(n) => {
6528                let mut full_name = n;
6529                while self.eat(&Token::PackageSep) {
6530                    if let (Token::Ident(part), _) = self.advance() {
6531                        full_name = format!("{}::{}", full_name, part);
6532                    }
6533                }
6534                if full_name == "overload" {
6535                    let mut pairs = Vec::new();
6536                    let mut parse_overload_pairs = |this: &mut Self| -> StrykeResult<()> {
6537                        loop {
6538                            if matches!(this.peek(), Token::RParen | Token::Semicolon | Token::Eof)
6539                            {
6540                                break;
6541                            }
6542                            let key_e = this.parse_assign_expr()?;
6543                            this.expect(&Token::FatArrow)?;
6544                            let val_e = this.parse_assign_expr()?;
6545                            let key = this.expr_to_overload_key(&key_e)?;
6546                            let val = this.expr_to_overload_sub(&val_e)?;
6547                            pairs.push((key, val));
6548                            if !this.eat(&Token::Comma) {
6549                                break;
6550                            }
6551                        }
6552                        Ok(())
6553                    };
6554                    if self.eat(&Token::LParen) {
6555                        // `use overload ();` — common in JSON::PP and other modules.
6556                        parse_overload_pairs(self)?;
6557                        self.expect(&Token::RParen)?;
6558                    } else if !matches!(self.peek(), Token::Semicolon | Token::Eof) {
6559                        parse_overload_pairs(self)?;
6560                    }
6561                    self.eat(&Token::Semicolon);
6562                    return Ok(Statement {
6563                        label: None,
6564                        kind: StmtKind::UseOverload { pairs },
6565                        line,
6566                    });
6567                }
6568                let mut imports = Vec::new();
6569                // Imports must start on the SAME LINE as `use Module`.
6570                // Without this, a bare `use K8s` followed by `p "…"`
6571                // on the next line silently swallowed the `p` call as
6572                // an import expression — failing later with the
6573                // confusing "pragma import must be a compile-time
6574                // string" error pointing at the next-line statement.
6575                // The legitimate multi-line form uses `,` to continue.
6576                let on_same_line = self.peek_line() == tok_line;
6577                // Perl-style version pin: `use Some::Module 2.13` — a
6578                // numeric literal in the FIRST import slot is the
6579                // package-version requirement, not an import argument.
6580                // Matches Perl 5's `use Module VERSION LIST` grammar:
6581                // the resolver pins to `<store>/<name>@<VERSION>/`
6582                // directly (bypassing lockfile / installed.toml).
6583                // Distinct from the bare `use VERSION` form a few
6584                // arms up, which is handled by `UsePerlVersion`. A
6585                // trailing `, LIST` is still consumed as imports.
6586                let mut version: Option<String> = None;
6587                if on_same_line {
6588                    match self.peek().clone() {
6589                        Token::Integer(n) => {
6590                            self.advance();
6591                            version = Some(n.to_string());
6592                            self.eat(&Token::Comma);
6593                        }
6594                        Token::Float(v) => {
6595                            self.advance();
6596                            version = Some(format_version_float(v));
6597                            self.eat(&Token::Comma);
6598                        }
6599                        _ => {}
6600                    }
6601                }
6602                if on_same_line
6603                    && !matches!(self.peek(), Token::Semicolon | Token::Eof)
6604                    && !self.next_is_new_statement_start(tok_line)
6605                {
6606                    loop {
6607                        if matches!(self.peek(), Token::Semicolon | Token::Eof) {
6608                            break;
6609                        }
6610                        imports.push(self.parse_expression()?);
6611                        if !self.eat(&Token::Comma) {
6612                            break;
6613                        }
6614                    }
6615                }
6616                self.eat(&Token::Semicolon);
6617                Ok(Statement {
6618                    label: None,
6619                    kind: StmtKind::Use {
6620                        module: full_name,
6621                        imports,
6622                        version,
6623                    },
6624                    line,
6625                })
6626            }
6627            other => Err(self.syntax_err(
6628                format!("Expected module name or version after {}, got {:?}", kw_name, other),
6629                tok_line,
6630            )),
6631        }
6632    }
6633
6634    fn parse_no(&mut self) -> StrykeResult<Statement> {
6635        let line = self.peek_line();
6636        self.advance(); // 'no'
6637        let module = match self.advance() {
6638            (Token::Ident(n), tok_line) => (n, tok_line),
6639            (tok, line) => {
6640                return Err(self.syntax_err(
6641                    format!("Expected module name after no, got {:?}", tok),
6642                    line,
6643                ))
6644            }
6645        };
6646        let (module_name, tok_line) = module;
6647        let mut full_name = module_name;
6648        while self.eat(&Token::PackageSep) {
6649            if let (Token::Ident(part), _) = self.advance() {
6650                full_name = format!("{}::{}", full_name, part);
6651            }
6652        }
6653        let mut imports = Vec::new();
6654        if !matches!(self.peek(), Token::Semicolon | Token::Eof)
6655            && !self.next_is_new_statement_start(tok_line)
6656        {
6657            loop {
6658                if matches!(self.peek(), Token::Semicolon | Token::Eof) {
6659                    break;
6660                }
6661                imports.push(self.parse_expression()?);
6662                if !self.eat(&Token::Comma) {
6663                    break;
6664                }
6665            }
6666        }
6667        self.eat(&Token::Semicolon);
6668        Ok(Statement {
6669            label: None,
6670            kind: StmtKind::No {
6671                module: full_name,
6672                imports,
6673            },
6674            line,
6675        })
6676    }
6677
6678    fn parse_return(&mut self) -> StrykeResult<Statement> {
6679        let line = self.peek_line();
6680        self.advance(); // 'return'
6681                        // No-value return: terminator tokens AND any postfix statement-modifier
6682                        // keyword (`if`/`unless`/`while`/`until`/`for`/`foreach`). Without this
6683                        // the postfix-modifier check below never fires for valueless returns —
6684                        // `parse_assign_expr` would see `if` and look it up as a sub call,
6685                        // producing the misleading "Undefined subroutine &if" error.
6686        let val = if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof)
6687            || self.peek_is_postfix_stmt_modifier_keyword()
6688        {
6689            None
6690        } else {
6691            // Parse the operand as a comma-list — Perl's `return` is a
6692            // list-operator, so `return 1, 2, 3` returns the list (1, 2, 3).
6693            // (BUG-010) Stay below pipe-forward and stop at postfix
6694            // statement-modifier keywords like `if` / `unless`.
6695            let first = self.parse_assign_expr()?;
6696            if matches!(self.peek(), Token::Comma | Token::FatArrow) {
6697                let mut items = vec![first];
6698                while self.eat(&Token::Comma) || self.eat(&Token::FatArrow) {
6699                    if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof)
6700                        || self.peek_is_postfix_stmt_modifier_keyword()
6701                    {
6702                        break;
6703                    }
6704                    items.push(self.parse_assign_expr()?);
6705                }
6706                let line = items.first().map(|e| e.line).unwrap_or(line);
6707                Some(Expr {
6708                    kind: ExprKind::List(items),
6709                    line,
6710                })
6711            } else {
6712                Some(first)
6713            }
6714        };
6715        // Check for postfix modifiers on return
6716        let stmt = Statement {
6717            label: None,
6718            kind: StmtKind::Return(val),
6719            line,
6720        };
6721        if let Token::Ident(ref kw) = self.peek().clone() {
6722            match kw.as_str() {
6723                "if" => {
6724                    self.advance();
6725                    let cond = self.parse_expression()?;
6726                    self.eat(&Token::Semicolon);
6727                    return Ok(Statement {
6728                        label: None,
6729                        kind: StmtKind::If {
6730                            condition: cond,
6731                            body: vec![stmt],
6732                            elsifs: vec![],
6733                            else_block: None,
6734                        },
6735                        line,
6736                    });
6737                }
6738                "unless" => {
6739                    self.advance();
6740                    let cond = self.parse_expression()?;
6741                    self.eat(&Token::Semicolon);
6742                    return Ok(Statement {
6743                        label: None,
6744                        kind: StmtKind::Unless {
6745                            condition: cond,
6746                            body: vec![stmt],
6747                            else_block: None,
6748                        },
6749                        line,
6750                    });
6751                }
6752                _ => {}
6753            }
6754        }
6755        self.eat(&Token::Semicolon);
6756        Ok(stmt)
6757    }
6758
6759    // ── Expressions (Pratt / precedence climbing) ──
6760
6761    fn parse_expression(&mut self) -> StrykeResult<Expr> {
6762        self.parse_comma_expr()
6763    }
6764
6765    fn parse_comma_expr(&mut self) -> StrykeResult<Expr> {
6766        // Word-op precedence (or/and/not) sits ABOVE assignment in Perl —
6767        // `EXPR or $err = $@` parses as `EXPR or ($err = $@)`, NOT
6768        // `(EXPR or $err) = $@`. Entering through `parse_or_word` here
6769        // (instead of `parse_assign_expr` directly) gives `or`/`and`/`not`
6770        // looser binding than `=`, matching `perlop`. The deeper chain
6771        // (`parse_not_word → parse_assign_expr → parse_ternary → … →
6772        // parse_log_or → …`) handles tighter operators normally.
6773        let expr = self.parse_or_word()?;
6774        let mut exprs = vec![expr];
6775        while self.eat(&Token::Comma) || self.eat(&Token::FatArrow) {
6776            if matches!(
6777                self.peek(),
6778                Token::RParen | Token::RBracket | Token::RBrace | Token::Semicolon | Token::Eof
6779            ) {
6780                break; // trailing comma
6781            }
6782            exprs.push(self.parse_or_word()?);
6783        }
6784        if exprs.len() == 1 {
6785            return Ok(exprs.pop().unwrap());
6786        }
6787        let line = exprs[0].line;
6788        Ok(Expr {
6789            kind: ExprKind::List(exprs),
6790            line,
6791        })
6792    }
6793
6794    fn parse_assign_expr(&mut self) -> StrykeResult<Expr> {
6795        let expr = self.parse_ternary()?;
6796        let line = expr.line;
6797
6798        match self.peek().clone() {
6799            Token::Assign => {
6800                self.advance();
6801                let right = self.parse_assign_expr()?;
6802                // Desugar `$obj->field = value` into `$obj->field(value)` (setter call)
6803                if let ExprKind::MethodCall { ref args, .. } = expr.kind {
6804                    if args.is_empty() {
6805                        // Destructure again to take ownership
6806                        let ExprKind::MethodCall {
6807                            object,
6808                            method,
6809                            super_call,
6810                            ..
6811                        } = expr.kind
6812                        else {
6813                            unreachable!()
6814                        };
6815                        return Ok(Expr {
6816                            kind: ExprKind::MethodCall {
6817                                object,
6818                                method,
6819                                args: vec![right],
6820                                super_call,
6821                            },
6822                            line,
6823                        });
6824                    }
6825                }
6826                self.validate_assignment(&expr, &right, line)?;
6827                Ok(Expr {
6828                    kind: ExprKind::Assign {
6829                        target: Box::new(expr),
6830                        value: Box::new(right),
6831                    },
6832                    line,
6833                })
6834            }
6835            Token::PlusAssign => {
6836                self.advance();
6837                let r = self.parse_assign_expr()?;
6838                Ok(Expr {
6839                    kind: ExprKind::CompoundAssign {
6840                        target: Box::new(expr),
6841                        op: BinOp::Add,
6842                        value: Box::new(r),
6843                    },
6844                    line,
6845                })
6846            }
6847            Token::MinusAssign => {
6848                self.advance();
6849                let r = self.parse_assign_expr()?;
6850                Ok(Expr {
6851                    kind: ExprKind::CompoundAssign {
6852                        target: Box::new(expr),
6853                        op: BinOp::Sub,
6854                        value: Box::new(r),
6855                    },
6856                    line,
6857                })
6858            }
6859            Token::MulAssign => {
6860                self.advance();
6861                let r = self.parse_assign_expr()?;
6862                Ok(Expr {
6863                    kind: ExprKind::CompoundAssign {
6864                        target: Box::new(expr),
6865                        op: BinOp::Mul,
6866                        value: Box::new(r),
6867                    },
6868                    line,
6869                })
6870            }
6871            Token::DivAssign => {
6872                self.advance();
6873                let r = self.parse_assign_expr()?;
6874                Ok(Expr {
6875                    kind: ExprKind::CompoundAssign {
6876                        target: Box::new(expr),
6877                        op: BinOp::Div,
6878                        value: Box::new(r),
6879                    },
6880                    line,
6881                })
6882            }
6883            Token::ModAssign => {
6884                self.advance();
6885                let r = self.parse_assign_expr()?;
6886                Ok(Expr {
6887                    kind: ExprKind::CompoundAssign {
6888                        target: Box::new(expr),
6889                        op: BinOp::Mod,
6890                        value: Box::new(r),
6891                    },
6892                    line,
6893                })
6894            }
6895            Token::PowAssign => {
6896                self.advance();
6897                let r = self.parse_assign_expr()?;
6898                Ok(Expr {
6899                    kind: ExprKind::CompoundAssign {
6900                        target: Box::new(expr),
6901                        op: BinOp::Pow,
6902                        value: Box::new(r),
6903                    },
6904                    line,
6905                })
6906            }
6907            Token::XAssign => {
6908                // `$s x= N` has no matching `BinOp::Repeat`; desugar to
6909                // `$s = $s x N` so we can reuse the existing `ExprKind::Repeat`
6910                // evaluator (scalar-repeat path; list-repeat fires only when
6911                // the LHS is a syntactic list literal).
6912                self.advance();
6913                let r = self.parse_assign_expr()?;
6914                let lhs_for_repeat = expr.clone();
6915                Ok(Expr {
6916                    kind: ExprKind::Assign {
6917                        target: Box::new(expr),
6918                        value: Box::new(Expr {
6919                            kind: ExprKind::Repeat {
6920                                expr: Box::new(lhs_for_repeat),
6921                                count: Box::new(r),
6922                                list_repeat: false,
6923                            },
6924                            line,
6925                        }),
6926                    },
6927                    line,
6928                })
6929            }
6930            Token::DotAssign => {
6931                self.advance();
6932                let r = self.parse_assign_expr()?;
6933                Ok(Expr {
6934                    kind: ExprKind::CompoundAssign {
6935                        target: Box::new(expr),
6936                        op: BinOp::Concat,
6937                        value: Box::new(r),
6938                    },
6939                    line,
6940                })
6941            }
6942            Token::BitAndAssign => {
6943                self.advance();
6944                let r = self.parse_assign_expr()?;
6945                Ok(Expr {
6946                    kind: ExprKind::CompoundAssign {
6947                        target: Box::new(expr),
6948                        op: BinOp::BitAnd,
6949                        value: Box::new(r),
6950                    },
6951                    line,
6952                })
6953            }
6954            Token::BitOrAssign => {
6955                self.advance();
6956                let r = self.parse_assign_expr()?;
6957                Ok(Expr {
6958                    kind: ExprKind::CompoundAssign {
6959                        target: Box::new(expr),
6960                        op: BinOp::BitOr,
6961                        value: Box::new(r),
6962                    },
6963                    line,
6964                })
6965            }
6966            Token::XorAssign => {
6967                self.advance();
6968                let r = self.parse_assign_expr()?;
6969                Ok(Expr {
6970                    kind: ExprKind::CompoundAssign {
6971                        target: Box::new(expr),
6972                        op: BinOp::BitXor,
6973                        value: Box::new(r),
6974                    },
6975                    line,
6976                })
6977            }
6978            Token::ShiftLeftAssign => {
6979                self.advance();
6980                let r = self.parse_assign_expr()?;
6981                Ok(Expr {
6982                    kind: ExprKind::CompoundAssign {
6983                        target: Box::new(expr),
6984                        op: BinOp::ShiftLeft,
6985                        value: Box::new(r),
6986                    },
6987                    line,
6988                })
6989            }
6990            Token::ShiftRightAssign => {
6991                self.advance();
6992                let r = self.parse_assign_expr()?;
6993                Ok(Expr {
6994                    kind: ExprKind::CompoundAssign {
6995                        target: Box::new(expr),
6996                        op: BinOp::ShiftRight,
6997                        value: Box::new(r),
6998                    },
6999                    line,
7000                })
7001            }
7002            Token::OrAssign => {
7003                self.advance();
7004                let r = self.parse_assign_expr()?;
7005                Ok(Expr {
7006                    kind: ExprKind::CompoundAssign {
7007                        target: Box::new(expr),
7008                        op: BinOp::LogOr,
7009                        value: Box::new(r),
7010                    },
7011                    line,
7012                })
7013            }
7014            Token::DefinedOrAssign => {
7015                self.advance();
7016                let r = self.parse_assign_expr()?;
7017                Ok(Expr {
7018                    kind: ExprKind::CompoundAssign {
7019                        target: Box::new(expr),
7020                        op: BinOp::DefinedOr,
7021                        value: Box::new(r),
7022                    },
7023                    line,
7024                })
7025            }
7026            Token::AndAssign => {
7027                self.advance();
7028                let r = self.parse_assign_expr()?;
7029                Ok(Expr {
7030                    kind: ExprKind::CompoundAssign {
7031                        target: Box::new(expr),
7032                        op: BinOp::LogAnd,
7033                        value: Box::new(r),
7034                    },
7035                    line,
7036                })
7037            }
7038            _ => Ok(expr),
7039        }
7040    }
7041
7042    fn parse_ternary(&mut self) -> StrykeResult<Expr> {
7043        let expr = self.parse_pipe_forward()?;
7044        if self.eat(&Token::Question) {
7045            let line = expr.line;
7046            self.suppress_colon_range = self.suppress_colon_range.saturating_add(1);
7047            let then_expr = self.parse_assign_expr();
7048            self.suppress_colon_range = self.suppress_colon_range.saturating_sub(1);
7049            let then_expr = then_expr?;
7050            self.expect(&Token::Colon)?;
7051            let else_expr = self.parse_assign_expr()?;
7052            return Ok(Expr {
7053                kind: ExprKind::Ternary {
7054                    condition: Box::new(expr),
7055                    then_expr: Box::new(then_expr),
7056                    else_expr: Box::new(else_expr),
7057                },
7058                line,
7059            });
7060        }
7061        Ok(expr)
7062    }
7063
7064    /// `EXPR |> CALL` — pipe-forward (F#/Elixir). Left-associative; the LHS is threaded
7065    /// in as the **first argument** of the RHS call at parse time (pure AST rewrite,
7066    /// no runtime cost). `x |> f(a, b)` → `f(x, a, b)`; `x |> f` → `f(x)`; chain
7067    /// `x |> f |> g(2)` → `g(f(x), 2)`. Precedence sits between `?:` and `||`, so
7068    /// `x + 1 |> f || y` parses as `f(x + 1) || y`.
7069    fn parse_pipe_forward(&mut self) -> StrykeResult<Expr> {
7070        // After moving word-ops (or/and/not) above the assignment level,
7071        // pipe_forward must descend into `parse_range` (which itself
7072        // descends into `parse_log_or`) — calling `parse_or_word` here
7073        // would re-introduce `or` at a wrong place in the precedence chain
7074        // (it now sits above `parse_comma_expr`). We skip past `parse_range`
7075        // rather than `parse_log_or` so `..` stays reachable.
7076        let mut left = self.parse_range()?;
7077        // Inside a paren-less arg list, `|>` is a hard terminator for the
7078        // enclosing call — leave it for the outer `parse_pipe_forward` loop
7079        // so `qw(…) |> head 2 |> join "-"` chains left-to-right as
7080        // `(qw(…) |> head 2) |> join "-"` instead of `head` swallowing the
7081        // outer `|>` via its first-arg `parse_assign_expr`.
7082        if self.no_pipe_forward_depth > 0 {
7083            return Ok(left);
7084        }
7085        while matches!(self.peek(), Token::PipeForward) {
7086            if crate::compat_mode() {
7087                return Err(self.syntax_err(
7088                    "pipe-forward operator `|>` is a stryke extension (disabled by --compat)",
7089                    left.line,
7090                ));
7091            }
7092            let line = left.line;
7093            self.advance();
7094            // Set pipe-RHS context so list-taking builtins (`map`, `grep`,
7095            // `join`, …) accept a placeholder in place of their list operand.
7096            self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_add(1);
7097            // RHS of `|>` parses at the same precedence as the LHS — see
7098            // the comment at the top of `parse_pipe_forward` for why this
7099            // descends into `parse_range` instead of `parse_or_word`.
7100            let right_result = self.parse_range();
7101            self.pipe_rhs_depth = self.pipe_rhs_depth.saturating_sub(1);
7102            let right = right_result?;
7103            left = self.pipe_forward_apply(left, right, line)?;
7104        }
7105        Ok(left)
7106    }
7107
7108    /// Desugar `lhs |> rhs`: thread `lhs` into the call that `rhs` represents as
7109    /// its **first** argument (Elixir / R / proposed-JS convention).
7110    ///
7111    /// The strategy depends on the shape of `rhs`:
7112    /// - Generic calls (`FuncCall`, `MethodCall`, `IndirectCall`) and variadic
7113    ///   builtins (`Print`, `Say`, `Printf`, `Die`, `Warn`, `Sprintf`, `System`,
7114    ///   `Exec`, `Unlink`, `Chmod`, `Chown`, `Glob`, …) — **prepend** `lhs` to
7115    ///   the args list. So `URL |> json_jq ".[]"` → `json_jq(URL, ".[]")`,
7116    ///   matching the `(data, filter)` signature the builtin expects.
7117    /// - Unary-style builtins (`Length`, `Abs`, `Lc`, `Uc`, `Defined`, `Ref`,
7118    ///   `Keys`, `Values`, `Pop`, `Shift`, …) — **replace** the sole operand with
7119    ///   `lhs` (these parse a single default `$_` when called without an arg, so
7120    ///   piping overrides that default; first-arg and last-arg are identical).
7121    /// - List-taking higher-order forms (`map`, `flat_map`, `grep`, `sort`, `join`, `reduce`, `fold`,
7122    ///   `pmap`, `pflat_map`, `pgrep`, `pfor`, …) — **replace** the `list` field with `lhs`, so
7123    ///   `@arr |> map { $_ * 2 }` becomes `map { $_ * 2 } @arr`.
7124    /// - `Bareword("f")` — lift to `FuncCall { f, [lhs] }`.
7125    /// - Scalar / deref / coderef expressions — wrap in `IndirectCall` with `lhs`
7126    ///   as the sole argument.
7127    /// - Ambiguous forms (binary ops, ternaries, literals, lists) — parse error,
7128    ///   since silently calling a non-callable at runtime would be worse.
7129    fn pipe_forward_apply(&self, lhs: Expr, rhs: Expr, line: usize) -> StrykeResult<Expr> {
7130        let Expr { kind, line: rline } = rhs;
7131        let new_kind = match kind {
7132            // ── Generic / user-defined calls ───────────────────────────────────
7133            ExprKind::FuncCall { name, mut args } => {
7134                // Stryke builtins are unprefixed; `CORE::` callers route back to the
7135                // bare-name pipe-forward dispatch below.
7136                let dispatch_name: &str = name.strip_prefix("CORE::").unwrap_or(name.as_str());
7137                match dispatch_name {
7138                    "puniq" | "uniq" | "distinct" | "flatten" | "set" | "list_count"
7139                    | "list_size" | "count" | "size" | "cnt" | "len" | "with_index" | "shuffle"
7140                    | "shuffled" | "frequencies" | "freq" | "pfrequencies" | "pfreq"
7141                    | "interleave" | "ddump" | "stringify" | "str" | "lines" | "words"
7142                    | "chars" | "digits" | "letters" | "letters_uc" | "letters_lc"
7143                    | "punctuation" | "numbers" | "graphemes" | "columns" | "sentences"
7144                    | "paragraphs" | "sections" | "trim" | "avg" | "to_json" | "to_csv"
7145                    | "to_toml" | "to_yaml" | "to_xml" | "to_html" | "from_json" | "from_csv"
7146                    | "from_toml" | "from_yaml" | "from_xml" | "to_markdown" | "to_table"
7147                    | "xopen" | "clip" | "sparkline" | "bar_chart" | "flame" | "stddev"
7148                    | "squared" | "sq" | "square" | "cubed" | "cb" | "cube" | "normalize"
7149                    | "snake_case" | "camel_case" | "kebab_case" => {
7150                        if args.is_empty() {
7151                            args.push(lhs);
7152                        } else {
7153                            args[0] = lhs;
7154                        }
7155                    }
7156                    "chunked" | "windowed" => {
7157                        if args.is_empty() {
7158                            return Err(self.syntax_err(
7159                                "|>: chunked(N) / windowed(N) needs size — e.g. `@a |> windowed(2)`",
7160                                line,
7161                            ));
7162                        }
7163                        args.insert(0, lhs);
7164                    }
7165                    "reduce" | "fold" => {
7166                        args.push(lhs);
7167                    }
7168                    "grep_v" | "pluck" | "tee" | "nth" | "chunk" => {
7169                        // data |> grep_v "pattern" → grep_v("pattern", data...)
7170                        // data |> pluck "key" → pluck("key", data...)
7171                        // data |> tee "file" → tee("file", data...)
7172                        // data |> nth N → nth(N, data...)
7173                        // data |> chunk N → chunk(N, data...)
7174                        args.push(lhs);
7175                    }
7176                    "enumerate" | "dedup" => {
7177                        // data |> enumerate → enumerate(data)
7178                        // data |> dedup → dedup(data)
7179                        args.insert(0, lhs);
7180                    }
7181                    "clamp" => {
7182                        // data |> clamp MIN, MAX → clamp(MIN, MAX, data...)
7183                        args.push(lhs);
7184                    }
7185                    n if Self::is_block_then_list_pipe_builtin(n) => {
7186                        if args.len() < 2 {
7187                            return Err(self.syntax_err(
7188                                format!(
7189                                    "|>: `{name}` needs {{ BLOCK }}, LIST so the list can receive the pipe"
7190                                ),
7191                                line,
7192                            ));
7193                        }
7194                        args[1] = lhs;
7195                    }
7196                    "take" | "head" | "tail" | "drop" => {
7197                        if args.is_empty() {
7198                            return Err(self.syntax_err(
7199                                "|>: `{name}` needs N last — e.g. `@a |> take(3)` for `take(@a, 3)`",
7200                                line,
7201                            ));
7202                        }
7203                        // `LIST |> take N` → `take(LIST, N)` (prepend piped list before trailing count)
7204                        args.insert(0, lhs);
7205                    }
7206                    _ => {
7207                        if self.thread_last_mode {
7208                            args.push(lhs);
7209                        } else {
7210                            args.insert(0, lhs);
7211                        }
7212                    }
7213                }
7214                ExprKind::FuncCall { name, args }
7215            }
7216            ExprKind::MethodCall {
7217                object,
7218                method,
7219                mut args,
7220                super_call,
7221            } => {
7222                if self.thread_last_mode {
7223                    args.push(lhs);
7224                } else {
7225                    args.insert(0, lhs);
7226                }
7227                ExprKind::MethodCall {
7228                    object,
7229                    method,
7230                    args,
7231                    super_call,
7232                }
7233            }
7234            ExprKind::IndirectCall {
7235                target,
7236                mut args,
7237                ampersand,
7238                pass_caller_arglist: _,
7239            } => {
7240                if self.thread_last_mode {
7241                    args.push(lhs);
7242                } else {
7243                    args.insert(0, lhs);
7244                }
7245                ExprKind::IndirectCall {
7246                    target,
7247                    args,
7248                    ampersand,
7249                    // Prepending an explicit first arg means this is no longer
7250                    // "pass the caller's @_" — that form is only bare `&$cr`.
7251                    pass_caller_arglist: false,
7252                }
7253            }
7254
7255            // ── Print-like / diagnostic ops (variadic) ─────────────────────────
7256            ExprKind::Print { handle, mut args } => {
7257                if self.thread_last_mode {
7258                    args.push(lhs);
7259                } else {
7260                    args.insert(0, lhs);
7261                }
7262                ExprKind::Print { handle, args }
7263            }
7264            ExprKind::Say { handle, mut args } => {
7265                if self.thread_last_mode {
7266                    args.push(lhs);
7267                } else {
7268                    args.insert(0, lhs);
7269                }
7270                ExprKind::Say { handle, args }
7271            }
7272            ExprKind::Printf { handle, mut args } => {
7273                if self.thread_last_mode {
7274                    args.push(lhs);
7275                } else {
7276                    args.insert(0, lhs);
7277                }
7278                ExprKind::Printf { handle, args }
7279            }
7280            ExprKind::Die(mut args) => {
7281                if self.thread_last_mode {
7282                    args.push(lhs);
7283                } else {
7284                    args.insert(0, lhs);
7285                }
7286                ExprKind::Die(args)
7287            }
7288            ExprKind::Warn(mut args) => {
7289                if self.thread_last_mode {
7290                    args.push(lhs);
7291                } else {
7292                    args.insert(0, lhs);
7293                }
7294                ExprKind::Warn(args)
7295            }
7296
7297            // ── Sprintf: first-arg pipe threads lhs into the `format` slot ─────
7298            //   `"n=%d" |> sprintf(42)` → `sprintf("n=%d", 42)` is awkward,
7299            //   but piping the format string is the rarer case. Prepending
7300            //   to the values list gives `sprintf(format, lhs, ...args)` for
7301            //   the common `$n |> sprintf "count=%d"` case.
7302            ExprKind::Sprintf { format, mut args } => {
7303                if self.thread_last_mode {
7304                    args.push(lhs);
7305                } else {
7306                    args.insert(0, lhs);
7307                }
7308                ExprKind::Sprintf { format, args }
7309            }
7310
7311            // ── System / exec / globbing / filesystem variadics ────────────────
7312            ExprKind::System(mut args) => {
7313                if self.thread_last_mode {
7314                    args.push(lhs);
7315                } else {
7316                    args.insert(0, lhs);
7317                }
7318                ExprKind::System(args)
7319            }
7320            ExprKind::Exec(mut args) => {
7321                if self.thread_last_mode {
7322                    args.push(lhs);
7323                } else {
7324                    args.insert(0, lhs);
7325                }
7326                ExprKind::Exec(args)
7327            }
7328            ExprKind::Unlink(mut args) => {
7329                if self.thread_last_mode {
7330                    args.push(lhs);
7331                } else {
7332                    args.insert(0, lhs);
7333                }
7334                ExprKind::Unlink(args)
7335            }
7336            ExprKind::Chmod(mut args) => {
7337                if self.thread_last_mode {
7338                    args.push(lhs);
7339                } else {
7340                    args.insert(0, lhs);
7341                }
7342                ExprKind::Chmod(args)
7343            }
7344            ExprKind::Chown(mut args) => {
7345                if self.thread_last_mode {
7346                    args.push(lhs);
7347                } else {
7348                    args.insert(0, lhs);
7349                }
7350                ExprKind::Chown(args)
7351            }
7352            ExprKind::Glob(mut args) => {
7353                if self.thread_last_mode {
7354                    args.push(lhs);
7355                } else {
7356                    args.insert(0, lhs);
7357                }
7358                ExprKind::Glob(args)
7359            }
7360            ExprKind::Files(mut args) => {
7361                if self.thread_last_mode {
7362                    args.push(lhs);
7363                } else {
7364                    args.insert(0, lhs);
7365                }
7366                ExprKind::Files(args)
7367            }
7368            ExprKind::Filesf(mut args) => {
7369                if self.thread_last_mode {
7370                    args.push(lhs);
7371                } else {
7372                    args.insert(0, lhs);
7373                }
7374                ExprKind::Filesf(args)
7375            }
7376            ExprKind::FilesfRecursive(mut args) => {
7377                if self.thread_last_mode {
7378                    args.push(lhs);
7379                } else {
7380                    args.insert(0, lhs);
7381                }
7382                ExprKind::FilesfRecursive(args)
7383            }
7384            ExprKind::Dirs(mut args) => {
7385                if self.thread_last_mode {
7386                    args.push(lhs);
7387                } else {
7388                    args.insert(0, lhs);
7389                }
7390                ExprKind::Dirs(args)
7391            }
7392            ExprKind::DirsRecursive(mut args) => {
7393                if self.thread_last_mode {
7394                    args.push(lhs);
7395                } else {
7396                    args.insert(0, lhs);
7397                }
7398                ExprKind::DirsRecursive(args)
7399            }
7400            ExprKind::SymLinks(mut args) => {
7401                if self.thread_last_mode {
7402                    args.push(lhs);
7403                } else {
7404                    args.insert(0, lhs);
7405                }
7406                ExprKind::SymLinks(args)
7407            }
7408            ExprKind::Sockets(mut args) => {
7409                if self.thread_last_mode {
7410                    args.push(lhs);
7411                } else {
7412                    args.insert(0, lhs);
7413                }
7414                ExprKind::Sockets(args)
7415            }
7416            ExprKind::Pipes(mut args) => {
7417                if self.thread_last_mode {
7418                    args.push(lhs);
7419                } else {
7420                    args.insert(0, lhs);
7421                }
7422                ExprKind::Pipes(args)
7423            }
7424            ExprKind::BlockDevices(mut args) => {
7425                if self.thread_last_mode {
7426                    args.push(lhs);
7427                } else {
7428                    args.insert(0, lhs);
7429                }
7430                ExprKind::BlockDevices(args)
7431            }
7432            ExprKind::CharDevices(mut args) => {
7433                if self.thread_last_mode {
7434                    args.push(lhs);
7435                } else {
7436                    args.insert(0, lhs);
7437                }
7438                ExprKind::CharDevices(args)
7439            }
7440            ExprKind::GlobPar { mut args, progress } => {
7441                if self.thread_last_mode {
7442                    args.push(lhs);
7443                } else {
7444                    args.insert(0, lhs);
7445                }
7446                ExprKind::GlobPar { args, progress }
7447            }
7448            ExprKind::ParSed { mut args, progress } => {
7449                if self.thread_last_mode {
7450                    args.push(lhs);
7451                } else {
7452                    args.insert(0, lhs);
7453                }
7454                ExprKind::ParSed { args, progress }
7455            }
7456
7457            // ── Unary-style builtins: replace the lone operand with `lhs` ──────
7458            ExprKind::Length(_) => ExprKind::Length(Box::new(lhs)),
7459            ExprKind::Abs(_) => ExprKind::Abs(Box::new(lhs)),
7460            ExprKind::Int(_) => ExprKind::Int(Box::new(lhs)),
7461            ExprKind::Sqrt(_) => ExprKind::Sqrt(Box::new(lhs)),
7462            ExprKind::Sin(_) => ExprKind::Sin(Box::new(lhs)),
7463            ExprKind::Cos(_) => ExprKind::Cos(Box::new(lhs)),
7464            ExprKind::Exp(_) => ExprKind::Exp(Box::new(lhs)),
7465            ExprKind::Log(_) => ExprKind::Log(Box::new(lhs)),
7466            ExprKind::Hex(_) => ExprKind::Hex(Box::new(lhs)),
7467            ExprKind::Oct(_) => ExprKind::Oct(Box::new(lhs)),
7468            ExprKind::Lc(_) => ExprKind::Lc(Box::new(lhs)),
7469            ExprKind::Uc(_) => ExprKind::Uc(Box::new(lhs)),
7470            ExprKind::Lcfirst(_) => ExprKind::Lcfirst(Box::new(lhs)),
7471            ExprKind::Ucfirst(_) => ExprKind::Ucfirst(Box::new(lhs)),
7472            ExprKind::Fc(_) => ExprKind::Fc(Box::new(lhs)),
7473            ExprKind::Chr(_) => ExprKind::Chr(Box::new(lhs)),
7474            ExprKind::Ord(_) => ExprKind::Ord(Box::new(lhs)),
7475            ExprKind::Chomp(_) => ExprKind::Chomp(Box::new(lhs)),
7476            ExprKind::Chop(_) => ExprKind::Chop(Box::new(lhs)),
7477            ExprKind::Defined(_) => ExprKind::Defined(Box::new(lhs)),
7478            ExprKind::Ref(_) => ExprKind::Ref(Box::new(lhs)),
7479            ExprKind::ScalarContext(_) => ExprKind::ScalarContext(Box::new(lhs)),
7480            ExprKind::Keys(_) => ExprKind::Keys(Box::new(lhs)),
7481            ExprKind::Values(_) => ExprKind::Values(Box::new(lhs)),
7482            ExprKind::Each(_) => ExprKind::Each(Box::new(lhs)),
7483            ExprKind::Pop(_) => ExprKind::Pop(Box::new(lhs)),
7484            ExprKind::Shift(_) => ExprKind::Shift(Box::new(lhs)),
7485            ExprKind::Delete(_) => ExprKind::Delete(Box::new(lhs)),
7486            ExprKind::Exists(_) => ExprKind::Exists(Box::new(lhs)),
7487            ExprKind::ReverseExpr(_) => ExprKind::ReverseExpr(Box::new(lhs)),
7488            ExprKind::Rev(_) => ExprKind::Rev(Box::new(lhs)),
7489            ExprKind::Slurp(_) => ExprKind::Slurp(Box::new(lhs)),
7490            ExprKind::Swallow(_) => ExprKind::Swallow(Box::new(lhs)),
7491            ExprKind::Ingest(_) => ExprKind::Ingest(Box::new(lhs)),
7492            ExprKind::Burp(_) => ExprKind::Burp(Box::new(lhs)),
7493            ExprKind::God(_) => ExprKind::God(Box::new(lhs)),
7494            ExprKind::Capture(_) => ExprKind::Capture(Box::new(lhs)),
7495            ExprKind::Qx(_) => ExprKind::Qx(Box::new(lhs)),
7496            ExprKind::FetchUrl(_) => ExprKind::FetchUrl(Box::new(lhs)),
7497            ExprKind::Close(_) => ExprKind::Close(Box::new(lhs)),
7498            ExprKind::Chdir(_) => ExprKind::Chdir(Box::new(lhs)),
7499            ExprKind::Readdir(_) => ExprKind::Readdir(Box::new(lhs)),
7500            ExprKind::Closedir(_) => ExprKind::Closedir(Box::new(lhs)),
7501            ExprKind::Rewinddir(_) => ExprKind::Rewinddir(Box::new(lhs)),
7502            ExprKind::Telldir(_) => ExprKind::Telldir(Box::new(lhs)),
7503            ExprKind::Stat(_) => ExprKind::Stat(Box::new(lhs)),
7504            ExprKind::Lstat(_) => ExprKind::Lstat(Box::new(lhs)),
7505            ExprKind::Readlink(_) => ExprKind::Readlink(Box::new(lhs)),
7506            ExprKind::Study(_) => ExprKind::Study(Box::new(lhs)),
7507            ExprKind::Await(_) => ExprKind::Await(Box::new(lhs)),
7508            ExprKind::Eval(_) => ExprKind::Eval(Box::new(lhs)),
7509            ExprKind::Rand(_) => ExprKind::Rand(Some(Box::new(lhs))),
7510            ExprKind::Srand(_) => ExprKind::Srand(Some(Box::new(lhs))),
7511            ExprKind::Pos(_) => ExprKind::Pos(Some(Box::new(lhs))),
7512            ExprKind::Exit(_) => ExprKind::Exit(Some(Box::new(lhs))),
7513
7514            // ── Higher-order / list-taking forms: replace the `list` slot ──────
7515            ExprKind::MapExpr {
7516                block,
7517                list: _,
7518                flatten_array_refs,
7519                stream,
7520            } => ExprKind::MapExpr {
7521                block,
7522                list: Box::new(lhs),
7523                flatten_array_refs,
7524                stream,
7525            },
7526            ExprKind::MapExprComma {
7527                expr,
7528                list: _,
7529                flatten_array_refs,
7530                stream,
7531            } => ExprKind::MapExprComma {
7532                expr,
7533                list: Box::new(lhs),
7534                flatten_array_refs,
7535                stream,
7536            },
7537            ExprKind::GrepExpr {
7538                block,
7539                list: _,
7540                keyword,
7541            } => ExprKind::GrepExpr {
7542                block,
7543                list: Box::new(lhs),
7544                keyword,
7545            },
7546            ExprKind::GrepExprComma {
7547                expr,
7548                list: _,
7549                keyword,
7550            } => ExprKind::GrepExprComma {
7551                expr,
7552                list: Box::new(lhs),
7553                keyword,
7554            },
7555            ExprKind::ForEachExpr { block, list: _ } => ExprKind::ForEachExpr {
7556                block,
7557                list: Box::new(lhs),
7558            },
7559            ExprKind::SortExpr { cmp, list: _ } => ExprKind::SortExpr {
7560                cmp,
7561                list: Box::new(lhs),
7562            },
7563            ExprKind::JoinExpr { separator, list: _ } => ExprKind::JoinExpr {
7564                separator,
7565                list: Box::new(lhs),
7566            },
7567            ExprKind::ReduceExpr { block, list: _ } => ExprKind::ReduceExpr {
7568                block,
7569                list: Box::new(lhs),
7570            },
7571            ExprKind::PMapExpr {
7572                block,
7573                list: _,
7574                progress,
7575                flat_outputs,
7576                on_cluster,
7577                stream,
7578            } => ExprKind::PMapExpr {
7579                block,
7580                list: Box::new(lhs),
7581                progress,
7582                flat_outputs,
7583                on_cluster,
7584                stream,
7585            },
7586            ExprKind::ParExpr { block, list: _ } => ExprKind::ParExpr {
7587                block,
7588                list: Box::new(lhs),
7589            },
7590            ExprKind::ParReduceExpr {
7591                extract_block,
7592                reduce_block,
7593                list: _,
7594            } => ExprKind::ParReduceExpr {
7595                extract_block,
7596                reduce_block,
7597                list: Box::new(lhs),
7598            },
7599            ExprKind::PMapChunkedExpr {
7600                chunk_size,
7601                block,
7602                list: _,
7603                progress,
7604            } => ExprKind::PMapChunkedExpr {
7605                chunk_size,
7606                block,
7607                list: Box::new(lhs),
7608                progress,
7609            },
7610            ExprKind::PGrepExpr {
7611                block,
7612                list: _,
7613                progress,
7614                stream,
7615            } => ExprKind::PGrepExpr {
7616                block,
7617                list: Box::new(lhs),
7618                progress,
7619                stream,
7620            },
7621            ExprKind::PForExpr {
7622                block,
7623                list: _,
7624                progress,
7625            } => ExprKind::PForExpr {
7626                block,
7627                list: Box::new(lhs),
7628                progress,
7629            },
7630            ExprKind::PSortExpr {
7631                cmp,
7632                list: _,
7633                progress,
7634            } => ExprKind::PSortExpr {
7635                cmp,
7636                list: Box::new(lhs),
7637                progress,
7638            },
7639            ExprKind::PReduceExpr {
7640                block,
7641                list: _,
7642                progress,
7643            } => ExprKind::PReduceExpr {
7644                block,
7645                list: Box::new(lhs),
7646                progress,
7647            },
7648            ExprKind::PcacheExpr {
7649                block,
7650                list: _,
7651                progress,
7652            } => ExprKind::PcacheExpr {
7653                block,
7654                list: Box::new(lhs),
7655                progress,
7656            },
7657            ExprKind::PReduceInitExpr {
7658                init,
7659                block,
7660                list: _,
7661                progress,
7662            } => ExprKind::PReduceInitExpr {
7663                init,
7664                block,
7665                list: Box::new(lhs),
7666                progress,
7667            },
7668            ExprKind::PMapReduceExpr {
7669                map_block,
7670                reduce_block,
7671                list: _,
7672                progress,
7673            } => ExprKind::PMapReduceExpr {
7674                map_block,
7675                reduce_block,
7676                list: Box::new(lhs),
7677                progress,
7678            },
7679
7680            // ── Push / unshift: first arg is the array, so pipe the LHS
7681            //     into the **values** list — `"x" |> push(@arr)` → `push @arr, "x"`
7682            //     is unchanged, but `@arr |> push "x"` is unnatural; use push
7683            //     directly for that.
7684            ExprKind::Push { array, mut values } => {
7685                values.insert(0, lhs);
7686                ExprKind::Push { array, values }
7687            }
7688            ExprKind::Unshift { array, mut values } => {
7689                values.insert(0, lhs);
7690                ExprKind::Unshift { array, values }
7691            }
7692
7693            // ── Split: pipe the subject string — `$line |> split /,/` ─────────
7694            ExprKind::SplitExpr {
7695                pattern,
7696                string: _,
7697                limit,
7698            } => ExprKind::SplitExpr {
7699                pattern,
7700                string: Box::new(lhs),
7701                limit,
7702            },
7703
7704            // ── Regex ops: pipe the subject — `$str |> s/\n//g` ────────────────
7705            //    Auto-inject `r` flag so the substitution returns the modified
7706            //    string instead of the match count (non-destructive / Perl /r).
7707            ExprKind::Substitution {
7708                pattern,
7709                replacement,
7710                mut flags,
7711                expr: _,
7712                delim,
7713            } => {
7714                if !flags.contains('r') {
7715                    flags.push('r');
7716                }
7717                ExprKind::Substitution {
7718                    expr: Box::new(lhs),
7719                    pattern,
7720                    replacement,
7721                    flags,
7722                    delim,
7723                }
7724            }
7725            ExprKind::Transliterate {
7726                from,
7727                to,
7728                mut flags,
7729                expr: _,
7730                delim,
7731            } => {
7732                if !flags.contains('r') {
7733                    flags.push('r');
7734                }
7735                ExprKind::Transliterate {
7736                    expr: Box::new(lhs),
7737                    from,
7738                    to,
7739                    flags,
7740                    delim,
7741                }
7742            }
7743            ExprKind::Match {
7744                pattern,
7745                flags,
7746                scalar_g,
7747                expr: _,
7748                delim,
7749            } => ExprKind::Match {
7750                expr: Box::new(lhs),
7751                pattern,
7752                flags,
7753                scalar_g,
7754                delim,
7755            },
7756            // Bare `/regex/` (no explicit `m`): promote to Match on piped LHS
7757            ExprKind::Regex(pattern, flags) => ExprKind::Match {
7758                expr: Box::new(lhs),
7759                pattern,
7760                flags,
7761                scalar_g: false,
7762                delim: '/',
7763            },
7764
7765            // ── Bareword function name → plain unary call ──────────────────────
7766            ExprKind::Bareword(name) => match name.as_str() {
7767                "reverse" => {
7768                    if crate::no_interop_mode() {
7769                        return Err(self.syntax_err(
7770                            "stryke uses `rev` instead of `reverse` (--no-interop)",
7771                            line,
7772                        ));
7773                    }
7774                    ExprKind::ReverseExpr(Box::new(lhs))
7775                }
7776                "rv" | "reversed" | "rev" => ExprKind::Rev(Box::new(lhs)),
7777                "uq" | "uniq" | "distinct" => ExprKind::FuncCall {
7778                    name: "uniq".to_string(),
7779                    args: vec![lhs],
7780                },
7781                "fl" | "flatten" => ExprKind::FuncCall {
7782                    name: "flatten".to_string(),
7783                    args: vec![lhs],
7784                },
7785                _ => ExprKind::FuncCall {
7786                    name,
7787                    args: vec![lhs],
7788                },
7789            },
7790
7791            // ── Callable scalars / coderefs / derefs → IndirectCall ────────────
7792            kind @ (ExprKind::ScalarVar(_)
7793            | ExprKind::ArrayElement { .. }
7794            | ExprKind::HashElement { .. }
7795            | ExprKind::Deref { .. }
7796            | ExprKind::ArrowDeref { .. }
7797            | ExprKind::CodeRef { .. }
7798            | ExprKind::SubroutineRef(_)
7799            | ExprKind::SubroutineCodeRef(_)
7800            | ExprKind::DynamicSubCodeRef(_)) => ExprKind::IndirectCall {
7801                target: Box::new(Expr { kind, line: rline }),
7802                args: vec![lhs],
7803                ampersand: false,
7804                pass_caller_arglist: false,
7805            },
7806
7807            // `LHS |> >{ BLOCK }` — the `>{}` form is parsed everywhere as `Do(CodeRef)` (IIFE).
7808            // On the RHS of `|>` we want pipe-apply semantics instead: unwrap the Do and invoke
7809            // the inner coderef with `lhs` as `$_[0]`, matching `LHS |> fn { ... }`.
7810            ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. }) => {
7811                ExprKind::IndirectCall {
7812                    target: inner,
7813                    args: vec![lhs],
7814                    ampersand: false,
7815                    pass_caller_arglist: false,
7816                }
7817            }
7818
7819            other => {
7820                return Err(self.syntax_err(
7821                    format!(
7822                        "right-hand side of `|>` must be a call, builtin, or coderef \
7823                         expression (got {})",
7824                        Self::expr_kind_name(&other)
7825                    ),
7826                    line,
7827                ));
7828            }
7829        };
7830        Ok(Expr {
7831            kind: new_kind,
7832            line,
7833        })
7834    }
7835
7836    /// Short label for an `ExprKind` (used in `|>` error messages).
7837    fn expr_kind_name(kind: &ExprKind) -> &'static str {
7838        match kind {
7839            ExprKind::Integer(_) | ExprKind::Float(_) => "numeric literal",
7840            ExprKind::String(_) | ExprKind::InterpolatedString(_) => "string literal",
7841            ExprKind::BinOp { .. } => "binary expression",
7842            ExprKind::UnaryOp { .. } => "unary expression",
7843            ExprKind::Ternary { .. } => "ternary expression",
7844            ExprKind::Assign { .. } | ExprKind::CompoundAssign { .. } => "assignment",
7845            ExprKind::List(_) => "list expression",
7846            ExprKind::Range { .. } => "range expression",
7847            _ => "expression",
7848        }
7849    }
7850
7851    // or / not (lowest precedence word operators)
7852    fn parse_or_word(&mut self) -> StrykeResult<Expr> {
7853        let mut left = self.parse_and_word()?;
7854        while matches!(self.peek(), Token::LogOrWord) {
7855            let line = left.line;
7856            self.advance();
7857            let right = self.parse_and_word()?;
7858            left = Expr {
7859                kind: ExprKind::BinOp {
7860                    left: Box::new(left),
7861                    op: BinOp::LogOrWord,
7862                    right: Box::new(right),
7863                },
7864                line,
7865            };
7866        }
7867        Ok(left)
7868    }
7869
7870    fn parse_and_word(&mut self) -> StrykeResult<Expr> {
7871        let mut left = self.parse_not_word()?;
7872        while matches!(self.peek(), Token::LogAndWord) {
7873            let line = left.line;
7874            self.advance();
7875            let right = self.parse_not_word()?;
7876            left = Expr {
7877                kind: ExprKind::BinOp {
7878                    left: Box::new(left),
7879                    op: BinOp::LogAndWord,
7880                    right: Box::new(right),
7881                },
7882                line,
7883            };
7884        }
7885        Ok(left)
7886    }
7887
7888    fn parse_not_word(&mut self) -> StrykeResult<Expr> {
7889        if matches!(self.peek(), Token::LogNotWord) {
7890            let line = self.peek_line();
7891            self.advance();
7892            let expr = self.parse_not_word()?;
7893            return Ok(Expr {
7894                kind: ExprKind::UnaryOp {
7895                    op: UnaryOp::LogNotWord,
7896                    expr: Box::new(expr),
7897                },
7898                line,
7899            });
7900        }
7901        // Descend into assignment level — `not` sits ABOVE `=` in Perl
7902        // precedence, so `not $x = 5` parses as `not ($x = 5)`.
7903        self.parse_assign_expr()
7904    }
7905
7906    fn parse_log_or(&mut self) -> StrykeResult<Expr> {
7907        let mut left = self.parse_log_and()?;
7908        loop {
7909            let op = match self.peek() {
7910                Token::LogOr => BinOp::LogOr,
7911                Token::DefinedOr => BinOp::DefinedOr,
7912                _ => break,
7913            };
7914            let line = left.line;
7915            self.advance();
7916            let right = self.parse_log_and()?;
7917            left = Expr {
7918                kind: ExprKind::BinOp {
7919                    left: Box::new(left),
7920                    op,
7921                    right: Box::new(right),
7922                },
7923                line,
7924            };
7925        }
7926        Ok(left)
7927    }
7928
7929    fn parse_log_and(&mut self) -> StrykeResult<Expr> {
7930        let mut left = self.parse_bit_or()?;
7931        while matches!(self.peek(), Token::LogAnd) {
7932            let line = left.line;
7933            self.advance();
7934            let right = self.parse_bit_or()?;
7935            left = Expr {
7936                kind: ExprKind::BinOp {
7937                    left: Box::new(left),
7938                    op: BinOp::LogAnd,
7939                    right: Box::new(right),
7940                },
7941                line,
7942            };
7943        }
7944        Ok(left)
7945    }
7946
7947    fn parse_bit_or(&mut self) -> StrykeResult<Expr> {
7948        let mut left = self.parse_bit_xor()?;
7949        while matches!(self.peek(), Token::BitOr) {
7950            let line = left.line;
7951            self.advance();
7952            let right = self.parse_bit_xor()?;
7953            left = Expr {
7954                kind: ExprKind::BinOp {
7955                    left: Box::new(left),
7956                    op: BinOp::BitOr,
7957                    right: Box::new(right),
7958                },
7959                line,
7960            };
7961        }
7962        Ok(left)
7963    }
7964
7965    fn parse_bit_xor(&mut self) -> StrykeResult<Expr> {
7966        let mut left = self.parse_bit_and()?;
7967        while matches!(self.peek(), Token::BitXor) {
7968            let line = left.line;
7969            self.advance();
7970            let right = self.parse_bit_and()?;
7971            left = Expr {
7972                kind: ExprKind::BinOp {
7973                    left: Box::new(left),
7974                    op: BinOp::BitXor,
7975                    right: Box::new(right),
7976                },
7977                line,
7978            };
7979        }
7980        Ok(left)
7981    }
7982
7983    fn parse_bit_and(&mut self) -> StrykeResult<Expr> {
7984        let mut left = self.parse_equality()?;
7985        while matches!(self.peek(), Token::BitAnd) {
7986            let line = left.line;
7987            self.advance();
7988            let right = self.parse_equality()?;
7989            left = Expr {
7990                kind: ExprKind::BinOp {
7991                    left: Box::new(left),
7992                    op: BinOp::BitAnd,
7993                    right: Box::new(right),
7994                },
7995                line,
7996            };
7997        }
7998        Ok(left)
7999    }
8000
8001    fn parse_equality(&mut self) -> StrykeResult<Expr> {
8002        let mut left = self.parse_comparison()?;
8003        loop {
8004            let op = match self.peek() {
8005                Token::NumEq => BinOp::NumEq,
8006                Token::NumNe => BinOp::NumNe,
8007                Token::StrEq => BinOp::StrEq,
8008                Token::StrNe => BinOp::StrNe,
8009                Token::Spaceship => BinOp::Spaceship,
8010                Token::StrCmp => BinOp::StrCmp,
8011                _ => break,
8012            };
8013            let line = left.line;
8014            self.advance();
8015            let right = self.parse_comparison()?;
8016            left = Expr {
8017                kind: ExprKind::BinOp {
8018                    left: Box::new(left),
8019                    op,
8020                    right: Box::new(right),
8021                },
8022                line,
8023            };
8024        }
8025        Ok(left)
8026    }
8027
8028    fn parse_comparison(&mut self) -> StrykeResult<Expr> {
8029        let left = self.parse_shift()?;
8030        let first_op = match self.peek() {
8031            Token::NumLt => BinOp::NumLt,
8032            Token::NumGt => BinOp::NumGt,
8033            Token::NumLe => BinOp::NumLe,
8034            Token::NumGe => BinOp::NumGe,
8035            Token::StrLt => BinOp::StrLt,
8036            Token::StrGt => BinOp::StrGt,
8037            Token::StrLe => BinOp::StrLe,
8038            Token::StrGe => BinOp::StrGe,
8039            _ => return Ok(left),
8040        };
8041        let line = left.line;
8042        self.advance();
8043        let middle = self.parse_shift()?;
8044
8045        let second_op = match self.peek() {
8046            Token::NumLt => Some(BinOp::NumLt),
8047            Token::NumGt => Some(BinOp::NumGt),
8048            Token::NumLe => Some(BinOp::NumLe),
8049            Token::NumGe => Some(BinOp::NumGe),
8050            Token::StrLt => Some(BinOp::StrLt),
8051            Token::StrGt => Some(BinOp::StrGt),
8052            Token::StrLe => Some(BinOp::StrLe),
8053            Token::StrGe => Some(BinOp::StrGe),
8054            _ => None,
8055        };
8056
8057        if second_op.is_none() {
8058            return Ok(Expr {
8059                kind: ExprKind::BinOp {
8060                    left: Box::new(left),
8061                    op: first_op,
8062                    right: Box::new(middle),
8063                },
8064                line,
8065            });
8066        }
8067
8068        // Chained comparison: `a < b < c` → `(a < b) && (b < c)`
8069        // Collect all operands and operators for chains like `1 < x < 10 < y`
8070        let mut operands = vec![left, middle];
8071        let mut ops = vec![first_op];
8072
8073        loop {
8074            let op = match self.peek() {
8075                Token::NumLt => BinOp::NumLt,
8076                Token::NumGt => BinOp::NumGt,
8077                Token::NumLe => BinOp::NumLe,
8078                Token::NumGe => BinOp::NumGe,
8079                Token::StrLt => BinOp::StrLt,
8080                Token::StrGt => BinOp::StrGt,
8081                Token::StrLe => BinOp::StrLe,
8082                Token::StrGe => BinOp::StrGe,
8083                _ => break,
8084            };
8085            self.advance();
8086            ops.push(op);
8087            operands.push(self.parse_shift()?);
8088        }
8089
8090        // Build `(a op0 b) && (b op1 c) && (c op2 d) && ...`
8091        let mut result = Expr {
8092            kind: ExprKind::BinOp {
8093                left: Box::new(operands[0].clone()),
8094                op: ops[0],
8095                right: Box::new(operands[1].clone()),
8096            },
8097            line,
8098        };
8099
8100        for i in 1..ops.len() {
8101            let cmp = Expr {
8102                kind: ExprKind::BinOp {
8103                    left: Box::new(operands[i].clone()),
8104                    op: ops[i],
8105                    right: Box::new(operands[i + 1].clone()),
8106                },
8107                line,
8108            };
8109            result = Expr {
8110                kind: ExprKind::BinOp {
8111                    left: Box::new(result),
8112                    op: BinOp::LogAnd,
8113                    right: Box::new(cmp),
8114                },
8115                line,
8116            };
8117        }
8118
8119        Ok(result)
8120    }
8121
8122    fn parse_shift(&mut self) -> StrykeResult<Expr> {
8123        let mut left = self.parse_addition()?;
8124        loop {
8125            let op = match self.peek() {
8126                Token::ShiftLeft => BinOp::ShiftLeft,
8127                Token::ShiftRight => BinOp::ShiftRight,
8128                _ => break,
8129            };
8130            let line = left.line;
8131            self.advance();
8132            let right = self.parse_addition()?;
8133            left = Expr {
8134                kind: ExprKind::BinOp {
8135                    left: Box::new(left),
8136                    op,
8137                    right: Box::new(right),
8138                },
8139                line,
8140            };
8141        }
8142        Ok(left)
8143    }
8144
8145    fn parse_addition(&mut self) -> StrykeResult<Expr> {
8146        let mut left = self.parse_multiplication()?;
8147        loop {
8148            // Implicit semicolon: `-` or `+` on a new line is a unary operator on
8149            // the next statement, not a binary operator continuing this expression.
8150            let op = match self.peek() {
8151                Token::Plus if self.peek_line() == self.prev_line() => BinOp::Add,
8152                Token::Minus if self.peek_line() == self.prev_line() => BinOp::Sub,
8153                Token::Dot => BinOp::Concat,
8154                _ => break,
8155            };
8156            let line = left.line;
8157            self.advance();
8158            let right = self.parse_multiplication()?;
8159            left = Expr {
8160                kind: ExprKind::BinOp {
8161                    left: Box::new(left),
8162                    op,
8163                    right: Box::new(right),
8164                },
8165                line,
8166            };
8167        }
8168        Ok(left)
8169    }
8170
8171    fn parse_multiplication(&mut self) -> StrykeResult<Expr> {
8172        let mut left = self.parse_regex_bind()?;
8173        loop {
8174            let op = match self.peek() {
8175                Token::Star => BinOp::Mul,
8176                Token::Slash if self.suppress_slash_as_div == 0 => BinOp::Div,
8177                // Implicit semicolon: `%` on a new line is a hash dereference or hash
8178                // sigil for the next statement, not modulo operator on this expression.
8179                Token::Percent if self.peek_line() == self.prev_line() => BinOp::Mod,
8180                Token::X => {
8181                    let line = left.line;
8182                    // List-repeat fires when the LHS was just closed by a
8183                    // list-constructor paren (`(EXPR)`, `(LIST)`, `()`) or
8184                    // `qw(...)`. `parse_primary` records the post-close
8185                    // position; an exact match against `self.pos` here means
8186                    // no postfix consumed any tokens between the close and
8187                    // the `x`, so the LHS is intrinsically a list construct.
8188                    let list_repeat = self.list_construct_close_pos == Some(self.pos);
8189                    self.advance();
8190                    let right = self.parse_regex_bind()?;
8191                    left = Expr {
8192                        kind: ExprKind::Repeat {
8193                            expr: Box::new(left),
8194                            count: Box::new(right),
8195                            list_repeat,
8196                        },
8197                        line,
8198                    };
8199                    continue;
8200                }
8201                _ => break,
8202            };
8203            let line = left.line;
8204            self.advance();
8205            let right = self.parse_regex_bind()?;
8206            left = Expr {
8207                kind: ExprKind::BinOp {
8208                    left: Box::new(left),
8209                    op,
8210                    right: Box::new(right),
8211                },
8212                line,
8213            };
8214        }
8215        Ok(left)
8216    }
8217
8218    fn parse_regex_bind(&mut self) -> StrykeResult<Expr> {
8219        let left = self.parse_unary()?;
8220        match self.peek() {
8221            Token::BindMatch => {
8222                let line = left.line;
8223                self.advance();
8224                match self.peek().clone() {
8225                    Token::Regex(pattern, flags, delim) => {
8226                        self.advance();
8227                        Ok(Expr {
8228                            kind: ExprKind::Match {
8229                                expr: Box::new(left),
8230                                pattern,
8231                                flags,
8232                                scalar_g: false,
8233                                delim,
8234                            },
8235                            line,
8236                        })
8237                    }
8238                    Token::Ident(ref s) if s.starts_with('\x00') => {
8239                        let (Token::Ident(encoded), _) = self.advance() else {
8240                            unreachable!()
8241                        };
8242                        let parts: Vec<&str> = encoded.split('\x00').collect();
8243                        if parts.len() >= 4 && parts[1] == "s" {
8244                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
8245                            Ok(Expr {
8246                                kind: ExprKind::Substitution {
8247                                    expr: Box::new(left),
8248                                    pattern: parts[2].to_string(),
8249                                    replacement: parts[3].to_string(),
8250                                    flags: parts.get(4).unwrap_or(&"").to_string(),
8251                                    delim,
8252                                },
8253                                line,
8254                            })
8255                        } else if parts.len() >= 4 && parts[1] == "tr" {
8256                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
8257                            Ok(Expr {
8258                                kind: ExprKind::Transliterate {
8259                                    expr: Box::new(left),
8260                                    from: parts[2].to_string(),
8261                                    to: parts[3].to_string(),
8262                                    flags: parts.get(4).unwrap_or(&"").to_string(),
8263                                    delim,
8264                                },
8265                                line,
8266                            })
8267                        } else {
8268                            Err(self.syntax_err("Invalid regex binding", line))
8269                        }
8270                    }
8271                    _ => {
8272                        let rhs = self.parse_unary()?;
8273                        Ok(Expr {
8274                            kind: ExprKind::BinOp {
8275                                left: Box::new(left),
8276                                op: BinOp::BindMatch,
8277                                right: Box::new(rhs),
8278                            },
8279                            line,
8280                        })
8281                    }
8282                }
8283            }
8284            Token::BindNotMatch => {
8285                let line = left.line;
8286                self.advance();
8287                match self.peek().clone() {
8288                    Token::Regex(pattern, flags, delim) => {
8289                        self.advance();
8290                        Ok(Expr {
8291                            kind: ExprKind::UnaryOp {
8292                                op: UnaryOp::LogNot,
8293                                expr: Box::new(Expr {
8294                                    kind: ExprKind::Match {
8295                                        expr: Box::new(left),
8296                                        pattern,
8297                                        flags,
8298                                        scalar_g: false,
8299                                        delim,
8300                                    },
8301                                    line,
8302                                }),
8303                            },
8304                            line,
8305                        })
8306                    }
8307                    Token::Ident(ref s) if s.starts_with('\x00') => {
8308                        let (Token::Ident(encoded), _) = self.advance() else {
8309                            unreachable!()
8310                        };
8311                        let parts: Vec<&str> = encoded.split('\x00').collect();
8312                        if parts.len() >= 4 && parts[1] == "s" {
8313                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
8314                            Ok(Expr {
8315                                kind: ExprKind::UnaryOp {
8316                                    op: UnaryOp::LogNot,
8317                                    expr: Box::new(Expr {
8318                                        kind: ExprKind::Substitution {
8319                                            expr: Box::new(left),
8320                                            pattern: parts[2].to_string(),
8321                                            replacement: parts[3].to_string(),
8322                                            flags: parts.get(4).unwrap_or(&"").to_string(),
8323                                            delim,
8324                                        },
8325                                        line,
8326                                    }),
8327                                },
8328                                line,
8329                            })
8330                        } else if parts.len() >= 4 && parts[1] == "tr" {
8331                            let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
8332                            Ok(Expr {
8333                                kind: ExprKind::UnaryOp {
8334                                    op: UnaryOp::LogNot,
8335                                    expr: Box::new(Expr {
8336                                        kind: ExprKind::Transliterate {
8337                                            expr: Box::new(left),
8338                                            from: parts[2].to_string(),
8339                                            to: parts[3].to_string(),
8340                                            flags: parts.get(4).unwrap_or(&"").to_string(),
8341                                            delim,
8342                                        },
8343                                        line,
8344                                    }),
8345                                },
8346                                line,
8347                            })
8348                        } else {
8349                            Err(self.syntax_err("Invalid regex binding after !~", line))
8350                        }
8351                    }
8352                    _ => {
8353                        let rhs = self.parse_unary()?;
8354                        Ok(Expr {
8355                            kind: ExprKind::BinOp {
8356                                left: Box::new(left),
8357                                op: BinOp::BindNotMatch,
8358                                right: Box::new(rhs),
8359                            },
8360                            line,
8361                        })
8362                    }
8363                }
8364            }
8365            _ => Ok(left),
8366        }
8367    }
8368
8369    /// Parse thread macro input. Like `parse_range` but suppresses `/` as division
8370    /// so that `/pattern/` is left for the thread stage parser to handle as regex filter.
8371    fn parse_thread_input(&mut self) -> StrykeResult<Expr> {
8372        self.suppress_slash_as_div = self.suppress_slash_as_div.saturating_add(1);
8373        let result = self.parse_range();
8374        self.suppress_slash_as_div = self.suppress_slash_as_div.saturating_sub(1);
8375        result
8376    }
8377
8378    /// Parse `~p>` / `~p>>` parallel-chunk thread-macros. Equivalent to
8379    /// `par_reduce { stage1 |> stage2 |> ... } SOURCE`, with optional
8380    /// `||>` or `|then|` mid-pipeline boundary that switches to a normal
8381    /// `~>` / `~>>` continuation operating on the auto-merged result.
8382    fn parse_thread_macro_chunk_par(
8383        &mut self,
8384        line: usize,
8385        thread_last: bool,
8386    ) -> StrykeResult<Expr> {
8387        // Source: same parsing rules as `~>`.
8388        self.suppress_parenless_call = self.suppress_parenless_call.saturating_add(1);
8389        let source_expr = self.parse_thread_input();
8390        self.suppress_parenless_call = self.suppress_parenless_call.saturating_sub(1);
8391        let source_expr = source_expr?;
8392
8393        // Per-chunk stage chain: stages operate on `@_` (the chunk elements)
8394        // which the par_reduce runtime binds as the argument array. Use
8395        // `pending_thread_input` to seed the stage chain with `@_`.
8396        self.pending_thread_input = Some(Expr {
8397            kind: ExprKind::ArrayVar("_".into()),
8398            line,
8399        });
8400        let chunk_chain = self.parse_thread_macro_inner(line, thread_last, None);
8401        self.pending_thread_input = None;
8402        let chunk_chain = chunk_chain?;
8403
8404        // `parse_thread_macro_inner` (under pipe_rhs_depth > 0) wraps its
8405        // result as `fn { ... stages applied to $_[0] ... }`. Unwrap to
8406        // get the bare Block (`Vec<Statement>`) for the `par_reduce`
8407        // extract slot.
8408        let extract_block: Block = match chunk_chain.kind {
8409            ExprKind::CodeRef { params: _, body } => body,
8410            _ => vec![Statement {
8411                label: None,
8412                kind: StmtKind::Expression(chunk_chain),
8413                line,
8414            }],
8415        };
8416
8417        let par_reduce = Expr {
8418            kind: ExprKind::ParReduceExpr {
8419                extract_block,
8420                reduce_block: None,
8421                list: Box::new(source_expr),
8422            },
8423            line,
8424        };
8425
8426        // Check for `||>` / `|then|` boundary; if present, parse the
8427        // continuation as a normal `~>` / `~>>` thread macro with the
8428        // par_reduce result as its input.
8429        if self.eat_chunk_par_split_boundary() {
8430            return self.parse_thread_macro_continuation(par_reduce, line, thread_last);
8431        }
8432        Ok(par_reduce)
8433    }
8434
8435    /// Parse `~d>` / `~d>>` distributed thread-macros. Same chunk-block
8436    /// semantics as `~p>` (stages operate on `@_`) but chunks ship to a
8437    /// `RemoteCluster` via the existing `cluster::run_cluster` dispatcher.
8438    /// Syntax: `~d> on EXPR SOURCE stage1 stage2 ...`. The `on EXPR` slot
8439    /// is required; without it the operator falls through to a syntax
8440    /// error (no implicit default-cluster in v1).
8441    fn parse_thread_macro_dist(&mut self, line: usize, thread_last: bool) -> StrykeResult<Expr> {
8442        // Required `on EXPR` — the cluster operand.
8443        let on_ok = matches!(self.peek(), Token::Ident(ref s) if s == "on");
8444        if !on_ok {
8445            return Err(
8446                self.syntax_err("~d>: expected `on <cluster-expr>` after the operator", line)
8447            );
8448        }
8449        self.advance(); // consume `on`
8450                        // Parse cluster expr — same parse-rules as a thread-macro input
8451                        // (avoid pulling stages into the cluster expression).
8452        self.suppress_parenless_call = self.suppress_parenless_call.saturating_add(1);
8453        // Without this, `on $cluster () map { … }` parses `()` as a postfix
8454        // indirect call on `$cluster`, stealing the empty list meant as SOURCE.
8455        // Zero-arg cluster from a scalar sub: `on ($factory())` or `on $f->()`.
8456        self.suppress_indirect_paren_call = self.suppress_indirect_paren_call.saturating_add(1);
8457        let cluster_expr = self.parse_thread_input();
8458        self.suppress_indirect_paren_call = self.suppress_indirect_paren_call.saturating_sub(1);
8459        self.suppress_parenless_call = self.suppress_parenless_call.saturating_sub(1);
8460        let cluster_expr = cluster_expr?;
8461
8462        // Source list: same rules as `~p>` source.
8463        self.suppress_parenless_call = self.suppress_parenless_call.saturating_add(1);
8464        let source_expr = self.parse_thread_input();
8465        self.suppress_parenless_call = self.suppress_parenless_call.saturating_sub(1);
8466        let source_expr = source_expr?;
8467
8468        // Stage chain seeded with `@_` — matches `~p>` chunk-block
8469        // semantics. The VM-side eval prepends `@_ = $_;` to the shipped
8470        // block source so the remote agent's `set_topic(chunk_flat_array)`
8471        // is reflected into `@_` before user stages run.
8472        self.pending_thread_input = Some(Expr {
8473            kind: ExprKind::ArrayVar("_".into()),
8474            line,
8475        });
8476        let chunk_chain = self.parse_thread_macro_inner(line, thread_last, None);
8477        self.pending_thread_input = None;
8478        let chunk_chain = chunk_chain?;
8479
8480        let extract_block: Block = match chunk_chain.kind {
8481            ExprKind::CodeRef { params: _, body } => body,
8482            _ => vec![Statement {
8483                label: None,
8484                kind: StmtKind::Expression(chunk_chain),
8485                line,
8486            }],
8487        };
8488
8489        let dist_reduce = Expr {
8490            kind: ExprKind::DistReduceExpr {
8491                cluster: Box::new(cluster_expr),
8492                extract_block,
8493                list: Box::new(source_expr),
8494            },
8495            line,
8496        };
8497
8498        // `||>` / `|then|` boundary continuation, same as `~p>`.
8499        if self.eat_chunk_par_split_boundary() {
8500            return self.parse_thread_macro_continuation(dist_reduce, line, thread_last);
8501        }
8502        Ok(dist_reduce)
8503    }
8504
8505    /// Parse a `~>` / `~>>` continuation after a `||>` / `|then|`
8506    /// chunk-parallel-to-sequential boundary. Reuses
8507    /// `parse_thread_macro_inner` with `result_init: Some(prior)` so the
8508    /// stage loop threads from the par_reduce result instead of parsing
8509    /// a fresh source expression.
8510    fn parse_thread_macro_continuation(
8511        &mut self,
8512        prior: Expr,
8513        line: usize,
8514        thread_last: bool,
8515    ) -> StrykeResult<Expr> {
8516        self.pending_thread_input = Some(prior);
8517        let res = self.parse_thread_macro_inner(line, thread_last, None);
8518        self.pending_thread_input = None;
8519        res
8520    }
8521
8522    /// Try to consume `||>` (LogOr followed by `>`) or `|then|`
8523    /// (`Pipe Ident("then") Pipe`) as the chunk-parallel → sequential
8524    /// switch marker. Returns true if a boundary was consumed.
8525    fn eat_chunk_par_split_boundary(&mut self) -> bool {
8526        // `||>` = `LogOr` token (already merged in lex) followed by `>`.
8527        if matches!(self.peek(), Token::LogOr) && matches!(self.peek_at(1), Token::NumGt) {
8528            self.advance(); // ||
8529            self.advance(); // >
8530            return true;
8531        }
8532        // `|then|` = `BitOr` + `Ident("then")` + `BitOr`.
8533        if matches!(self.peek(), Token::BitOr) {
8534            if let Token::Ident(name) = self.peek_at(1).clone() {
8535                if name == "then" && matches!(self.peek_at(2), Token::BitOr) {
8536                    self.advance(); // |
8537                    self.advance(); // then
8538                    self.advance(); // |
8539                    return true;
8540                }
8541            }
8542        }
8543        false
8544    }
8545
8546    /// Perl `..` / `...` operator — precedence sits between `?:` and `||` (`perlop`), so
8547    /// `$x .. $x + 3` parses as `$x .. ($x + 3)` and `1..$n||5` parses as `1..($n||5)`. Both
8548    /// operands recurse through `parse_log_or`, which in turn walks down through all tighter
8549    /// operators (additive, multiplicative, regex bind, unary). Non-associative: the right
8550    /// operand is a single `parse_log_or` so `1..5..10` is a parse error in Perl, but we accept
8551    /// it greedily (left-associated) because the lexer already forbids `..` after a range RHS.
8552    fn parse_range(&mut self) -> StrykeResult<Expr> {
8553        let left = self.parse_log_or()?;
8554        let line = left.line;
8555        // `1..10` (traditional inclusive) / `1...10` (exclusive) / `1:10`
8556        // (short form) / `1~10` (universal short form). The `~` separator
8557        // works for every range type and is the only viable separator for
8558        // IPv6 since IPv6 already uses `:` internally; `:` would collide.
8559        // It also dodges `!`'s collision with the `_!N!` paired char-index
8560        // syntax. Single-`~` (vs `!!!` triple) keeps the surface simple.
8561        let (exclusive, _colon_style) = if self.eat(&Token::RangeExclusive) {
8562            (true, false)
8563        } else if self.eat(&Token::Range) {
8564            (false, false)
8565        } else if self.suppress_colon_range == 0 && self.eat(&Token::Colon) {
8566            // `1:10` short form — only valid for numeric ranges, not ternary
8567            // Lookahead: must be followed by something that looks like a range endpoint
8568            (false, true)
8569        } else if self.suppress_tilde_range == 0 && self.eat(&Token::BitNot) {
8570            (false, true)
8571        } else {
8572            return Ok(left);
8573        };
8574        let right = self.parse_log_or()?;
8575        // Optional step: `1..100:2` / `1:100:2` / `IPV6~IPV6~STEP`. `~` is
8576        // gated by `suppress_tilde_range` so paired char-index (`$x~5~`)
8577        // doesn't get its closing delimiter eaten as a range op.
8578        let step = if self.eat(&Token::Colon)
8579            || (self.suppress_tilde_range == 0 && self.eat(&Token::BitNot))
8580        {
8581            Some(Box::new(self.parse_unary()?))
8582        } else {
8583            None
8584        };
8585        Ok(Expr {
8586            kind: ExprKind::Range {
8587                from: Box::new(left),
8588                to: Box::new(right),
8589                exclusive,
8590                step,
8591            },
8592            line,
8593        })
8594    }
8595
8596    /// `name` or `Foo::Bar::baz` — used after `sub`, unary `&`, etc.
8597    fn parse_package_qualified_identifier(&mut self) -> StrykeResult<String> {
8598        let mut name = match self.advance() {
8599            (Token::Ident(n), _) => n,
8600            (tok, l) => {
8601                return Err(self.syntax_err(format!("Expected identifier, got {:?}", tok), l));
8602            }
8603        };
8604        while self.eat(&Token::PackageSep) {
8605            match self.advance() {
8606                (Token::Ident(part), _) => {
8607                    name.push_str("::");
8608                    name.push_str(&part);
8609                }
8610                // Topic-slot scalars (`_`, `_<<<<`, `_3`, etc.) lex as
8611                // `Token::ScalarVar` per the lexer's reservation. Accept
8612                // them as the trailing segment of a package-qualified
8613                // name so callers (e.g. `parse_sub_decl`) can reject the
8614                // full name with a friendly "would shadow topic-slot"
8615                // message rather than a generic "Expected identifier
8616                // after `::`" lexer-level error.
8617                (Token::ScalarVar(part), _) if Self::is_underscore_topic_slot(&part) => {
8618                    name.push_str("::");
8619                    name.push_str(&part);
8620                }
8621                (tok, l) => {
8622                    return Err(self
8623                        .syntax_err(format!("Expected identifier after `::`, got {:?}", tok), l));
8624                }
8625            }
8626        }
8627        Ok(name)
8628    }
8629
8630    /// After consuming unary `&`: `name` or `Foo::Bar::baz` (Perl `&foo` / `&Foo::bar`).
8631    fn parse_qualified_subroutine_name(&mut self) -> StrykeResult<String> {
8632        self.parse_package_qualified_identifier()
8633    }
8634
8635    fn parse_unary(&mut self) -> StrykeResult<Expr> {
8636        let line = self.peek_line();
8637        match self.peek().clone() {
8638            Token::Minus => {
8639                self.advance();
8640                let expr = self.parse_power()?;
8641                Ok(Expr {
8642                    kind: ExprKind::UnaryOp {
8643                        op: UnaryOp::Negate,
8644                        expr: Box::new(expr),
8645                    },
8646                    line,
8647                })
8648            }
8649            // Unary `+EXPR` — Perl uses this to disambiguate barewords in hash subscripts (`$h{+Foo}`)
8650            // and for scalar context; treat as a no-op on the parsed operand.
8651            // Special case: `+{ ... }` forces hashref interpretation (Perl idiom),
8652            // even when the body is a list-yielding expression like `+{ map { ... } @arr }`.
8653            // Without this, `{ map { ... } @arr }` falls back to block/CodeRef parsing
8654            // because the body doesn't fit `KEY => VAL` shape.
8655            Token::Plus => {
8656                self.advance();
8657                if matches!(self.peek(), Token::LBrace) {
8658                    let line = self.peek_line();
8659                    self.advance(); // consume {
8660                    return self.parse_forced_hashref_body(line);
8661                }
8662                self.parse_unary()
8663            }
8664            Token::LogNot => {
8665                self.advance();
8666                let expr = self.parse_unary()?;
8667                Ok(Expr {
8668                    kind: ExprKind::UnaryOp {
8669                        op: UnaryOp::LogNot,
8670                        expr: Box::new(expr),
8671                    },
8672                    line,
8673                })
8674            }
8675            Token::BitNot => {
8676                self.advance();
8677                let expr = self.parse_unary()?;
8678                Ok(Expr {
8679                    kind: ExprKind::UnaryOp {
8680                        op: UnaryOp::BitNot,
8681                        expr: Box::new(expr),
8682                    },
8683                    line,
8684                })
8685            }
8686            Token::Increment => {
8687                self.advance();
8688                let expr = self.parse_postfix()?;
8689                Ok(Expr {
8690                    kind: ExprKind::UnaryOp {
8691                        op: UnaryOp::PreIncrement,
8692                        expr: Box::new(expr),
8693                    },
8694                    line,
8695                })
8696            }
8697            Token::Decrement => {
8698                self.advance();
8699                let expr = self.parse_postfix()?;
8700                Ok(Expr {
8701                    kind: ExprKind::UnaryOp {
8702                        op: UnaryOp::PreDecrement,
8703                        expr: Box::new(expr),
8704                    },
8705                    line,
8706                })
8707            }
8708            Token::BitAnd => {
8709                // Unary `&name` / `&Pkg::name` (call / coderef); binary `&` is in `parse_bit_and`.
8710                // `&$coderef(...)` — call sub whose ref is in a scalar (core `B.pm` / `&$recurse($sym)`).
8711                self.advance();
8712                if matches!(self.peek(), Token::LBrace) {
8713                    self.advance();
8714                    let inner = self.parse_expression()?;
8715                    self.expect(&Token::RBrace)?;
8716                    return Ok(Expr {
8717                        kind: ExprKind::DynamicSubCodeRef(Box::new(inner)),
8718                        line,
8719                    });
8720                }
8721                if matches!(self.peek(), Token::Ident(_)) {
8722                    let name = self.parse_qualified_subroutine_name()?;
8723                    return Ok(Expr {
8724                        kind: ExprKind::SubroutineRef(name),
8725                        line,
8726                    });
8727                }
8728                let target = self.parse_primary()?;
8729                if matches!(self.peek(), Token::LParen) {
8730                    self.advance();
8731                    let args = self.parse_arg_list()?;
8732                    self.expect(&Token::RParen)?;
8733                    return Ok(Expr {
8734                        kind: ExprKind::IndirectCall {
8735                            target: Box::new(target),
8736                            args,
8737                            ampersand: true,
8738                            pass_caller_arglist: false,
8739                        },
8740                        line,
8741                    });
8742                }
8743                // `&$coderef` / `&{expr}` with no `(...)` — call with caller's @_ (Perl `&$sub`).
8744                Ok(Expr {
8745                    kind: ExprKind::IndirectCall {
8746                        target: Box::new(target),
8747                        args: vec![],
8748                        ampersand: true,
8749                        pass_caller_arglist: true,
8750                    },
8751                    line,
8752                })
8753            }
8754            Token::Backslash => {
8755                self.advance();
8756                let expr = self.parse_unary()?;
8757                if let ExprKind::SubroutineRef(name) = expr.kind {
8758                    return Ok(Expr {
8759                        kind: ExprKind::SubroutineCodeRef(name),
8760                        line,
8761                    });
8762                }
8763                if matches!(expr.kind, ExprKind::DynamicSubCodeRef(_)) {
8764                    return Ok(expr);
8765                }
8766                // `\` uses `ScalarRef`; array/hash vars and `\@{...}` lower to binding or alias refs.
8767                Ok(Expr {
8768                    kind: ExprKind::ScalarRef(Box::new(expr)),
8769                    line,
8770                })
8771            }
8772            Token::FileTest(op) => {
8773                self.advance();
8774                // Perl: `-d` with no operand uses `$_` (e.g. `if (-d)` inside `for` / `while read`).
8775                let expr = if Self::filetest_allows_implicit_topic(self.peek()) {
8776                    Expr {
8777                        kind: ExprKind::ScalarVar("_".into()),
8778                        line: self.peek_line(),
8779                    }
8780                } else {
8781                    self.parse_unary()?
8782                };
8783                Ok(Expr {
8784                    kind: ExprKind::FileTest {
8785                        op,
8786                        expr: Box::new(expr),
8787                    },
8788                    line,
8789                })
8790            }
8791            _ => self.parse_power(),
8792        }
8793    }
8794
8795    fn parse_power(&mut self) -> StrykeResult<Expr> {
8796        let left = self.parse_postfix()?;
8797        if matches!(self.peek(), Token::Power) {
8798            let line = left.line;
8799            self.advance();
8800            let right = self.parse_unary()?; // right-associative
8801            return Ok(Expr {
8802                kind: ExprKind::BinOp {
8803                    left: Box::new(left),
8804                    op: BinOp::Pow,
8805                    right: Box::new(right),
8806                },
8807                line,
8808            });
8809        }
8810        Ok(left)
8811    }
8812
8813    fn parse_postfix(&mut self) -> StrykeResult<Expr> {
8814        let mut expr = self.parse_primary()?;
8815        loop {
8816            match self.peek().clone() {
8817                Token::Increment => {
8818                    // Implicit semicolon: `++` on a new line is a prefix operator
8819                    // on the next statement, not postfix on the previous expression.
8820                    if self.peek_line() > self.prev_line() {
8821                        break;
8822                    }
8823                    let line = expr.line;
8824                    self.advance();
8825                    expr = Expr {
8826                        kind: ExprKind::PostfixOp {
8827                            expr: Box::new(expr),
8828                            op: PostfixOp::Increment,
8829                        },
8830                        line,
8831                    };
8832                }
8833                Token::Decrement => {
8834                    // Implicit semicolon: `--` on a new line is a prefix operator
8835                    // on the next statement, not postfix on the previous expression.
8836                    if self.peek_line() > self.prev_line() {
8837                        break;
8838                    }
8839                    let line = expr.line;
8840                    self.advance();
8841                    expr = Expr {
8842                        kind: ExprKind::PostfixOp {
8843                            expr: Box::new(expr),
8844                            op: PostfixOp::Decrement,
8845                        },
8846                        line,
8847                    };
8848                }
8849                Token::LParen => {
8850                    if self.suppress_indirect_paren_call > 0 {
8851                        break;
8852                    }
8853                    // Implicit semicolon: `(` on a new line after an expression
8854                    // is a new statement, not a postfix code-ref call.
8855                    // e.g.  `my $x = $ENV{"KEY"}\n($y =~ s/.../.../)`
8856                    if self.peek_line() > self.prev_line() {
8857                        break;
8858                    }
8859                    let line = expr.line;
8860                    self.advance();
8861                    let args = self.parse_arg_list()?;
8862                    self.expect(&Token::RParen)?;
8863                    expr = Expr {
8864                        kind: ExprKind::IndirectCall {
8865                            target: Box::new(expr),
8866                            args,
8867                            ampersand: false,
8868                            pass_caller_arglist: false,
8869                        },
8870                        line,
8871                    };
8872                }
8873                Token::Arrow => {
8874                    let line = expr.line;
8875                    self.advance();
8876                    match self.peek().clone() {
8877                        Token::LBracket => {
8878                            self.advance();
8879                            let index = self.parse_expression()?;
8880                            self.expect(&Token::RBracket)?;
8881                            expr = Expr {
8882                                kind: ExprKind::ArrowDeref {
8883                                    expr: Box::new(expr),
8884                                    index: Box::new(index),
8885                                    kind: DerefKind::Array,
8886                                },
8887                                line,
8888                            };
8889                        }
8890                        Token::LBrace => {
8891                            self.advance();
8892                            let key = self.parse_hash_subscript_key()?;
8893                            self.expect(&Token::RBrace)?;
8894                            expr = Expr {
8895                                kind: ExprKind::ArrowDeref {
8896                                    expr: Box::new(expr),
8897                                    index: Box::new(key),
8898                                    kind: DerefKind::Hash,
8899                                },
8900                                line,
8901                            };
8902                        }
8903                        Token::LParen => {
8904                            self.advance();
8905                            let args = self.parse_arg_list()?;
8906                            self.expect(&Token::RParen)?;
8907                            expr = Expr {
8908                                kind: ExprKind::ArrowDeref {
8909                                    expr: Box::new(expr),
8910                                    index: Box::new(Expr {
8911                                        kind: ExprKind::List(args),
8912                                        line,
8913                                    }),
8914                                    kind: DerefKind::Call,
8915                                },
8916                                line,
8917                            };
8918                        }
8919                        Token::Ident(method) => {
8920                            self.advance();
8921                            if method == "SUPER" {
8922                                self.expect(&Token::PackageSep)?;
8923                                let real_method = match self.advance() {
8924                                    (Token::Ident(n), _) => n,
8925                                    (tok, l) => {
8926                                        return Err(self.syntax_err(
8927                                            format!(
8928                                                "Expected method name after SUPER::, got {:?}",
8929                                                tok
8930                                            ),
8931                                            l,
8932                                        ));
8933                                    }
8934                                };
8935                                let args = if self.eat(&Token::LParen) {
8936                                    let a = self.parse_arg_list()?;
8937                                    self.expect(&Token::RParen)?;
8938                                    a
8939                                } else {
8940                                    self.parse_method_arg_list_no_paren()?
8941                                };
8942                                expr = Expr {
8943                                    kind: ExprKind::MethodCall {
8944                                        object: Box::new(expr),
8945                                        method: real_method,
8946                                        args,
8947                                        super_call: true,
8948                                    },
8949                                    line,
8950                                };
8951                            } else {
8952                                let mut method_name = method;
8953                                while self.eat(&Token::PackageSep) {
8954                                    match self.advance() {
8955                                        (Token::Ident(part), _) => {
8956                                            method_name.push_str("::");
8957                                            method_name.push_str(&part);
8958                                        }
8959                                        (tok, l) => {
8960                                            return Err(self.syntax_err(
8961                                                format!(
8962                                                    "Expected identifier after :: in method name, got {:?}",
8963                                                    tok
8964                                                ),
8965                                                l,
8966                                            ));
8967                                        }
8968                                    }
8969                                }
8970                                let args = if self.eat(&Token::LParen) {
8971                                    let a = self.parse_arg_list()?;
8972                                    self.expect(&Token::RParen)?;
8973                                    a
8974                                } else {
8975                                    self.parse_method_arg_list_no_paren()?
8976                                };
8977                                expr = Expr {
8978                                    kind: ExprKind::MethodCall {
8979                                        object: Box::new(expr),
8980                                        method: method_name,
8981                                        args,
8982                                        super_call: false,
8983                                    },
8984                                    line,
8985                                };
8986                            }
8987                        }
8988                        // Postfix dereference (Perl 5.20+, default 5.24+):
8989                        //   `$ref->@*`         — full array      ≡ `@{$ref}`
8990                        //   `$ref->@[i,j]`     — array slice     ≡ `@{$ref}[i,j]`
8991                        //   `$ref->@{k,l}`     — hash slice (vals) ≡ `@{$ref}{k,l}`
8992                        //   `$ref->%*`         — full hash       ≡ `%{$ref}`
8993                        Token::ArrayAt => {
8994                            self.advance(); // consume `@`
8995                            match self.peek().clone() {
8996                                Token::Star => {
8997                                    self.advance();
8998                                    expr = Expr {
8999                                        kind: ExprKind::Deref {
9000                                            expr: Box::new(expr),
9001                                            kind: Sigil::Array,
9002                                        },
9003                                        line,
9004                                    };
9005                                }
9006                                Token::LBracket => {
9007                                    self.advance();
9008                                    let indices = self.parse_slice_arg_list(false)?;
9009                                    self.expect(&Token::RBracket)?;
9010                                    let source = Expr {
9011                                        kind: ExprKind::Deref {
9012                                            expr: Box::new(expr),
9013                                            kind: Sigil::Array,
9014                                        },
9015                                        line,
9016                                    };
9017                                    expr = Expr {
9018                                        kind: ExprKind::AnonymousListSlice {
9019                                            source: Box::new(source),
9020                                            indices,
9021                                        },
9022                                        line,
9023                                    };
9024                                }
9025                                Token::LBrace => {
9026                                    self.advance();
9027                                    let keys = self.parse_slice_arg_list(true)?;
9028                                    self.expect(&Token::RBrace)?;
9029                                    expr = Expr {
9030                                        kind: ExprKind::HashSliceDeref {
9031                                            container: Box::new(expr),
9032                                            keys,
9033                                        },
9034                                        line,
9035                                    };
9036                                }
9037                                tok => {
9038                                    return Err(self.syntax_err(
9039                                        format!(
9040                                            "Expected `*`, `[…]`, or `{{…}}` after `->@`, got {:?}",
9041                                            tok
9042                                        ),
9043                                        line,
9044                                    ));
9045                                }
9046                            }
9047                        }
9048                        Token::HashPercent => {
9049                            self.advance(); // consume `%`
9050                            match self.peek().clone() {
9051                                Token::Star => {
9052                                    self.advance();
9053                                    expr = Expr {
9054                                        kind: ExprKind::Deref {
9055                                            expr: Box::new(expr),
9056                                            kind: Sigil::Hash,
9057                                        },
9058                                        line,
9059                                    };
9060                                }
9061                                tok => {
9062                                    return Err(self.syntax_err(
9063                                        format!("Expected `*` after `->%`, got {:?}", tok),
9064                                        line,
9065                                    ));
9066                                }
9067                            }
9068                        }
9069                        // `x` is lexed as `Token::X` (repeat op); after `->` it is a method name.
9070                        Token::X => {
9071                            self.advance();
9072                            let args = if self.eat(&Token::LParen) {
9073                                let a = self.parse_arg_list()?;
9074                                self.expect(&Token::RParen)?;
9075                                a
9076                            } else {
9077                                self.parse_method_arg_list_no_paren()?
9078                            };
9079                            expr = Expr {
9080                                kind: ExprKind::MethodCall {
9081                                    object: Box::new(expr),
9082                                    method: "x".to_string(),
9083                                    args,
9084                                    super_call: false,
9085                                },
9086                                line,
9087                            };
9088                        }
9089                        _ => break,
9090                    }
9091                }
9092                Token::LBracket => {
9093                    // Implicit semicolon: `[` on a new line is a new statement (array literal),
9094                    // not an array subscript on the preceding expression.
9095                    if self.peek_line() > self.prev_line() {
9096                        break;
9097                    }
9098                    // `$a[i]` — or chained `$r->{k}[i]` / `$a[1][2]` — or list slice `(sort ...)[0]`.
9099                    let line = expr.line;
9100                    if matches!(expr.kind, ExprKind::ScalarVar(_)) {
9101                        if let ExprKind::ScalarVar(ref name) = expr.kind {
9102                            let name = name.clone();
9103                            self.advance();
9104                            // Parse full expression to handle comma operator correctly:
9105                            // `$a[1, 2]` evaluates comma expr (returns last value = 2)
9106                            let index = self.parse_expression()?;
9107                            self.expect(&Token::RBracket)?;
9108                            expr = Expr {
9109                                kind: ExprKind::ArrayElement {
9110                                    array: name,
9111                                    index: Box::new(index),
9112                                },
9113                                line,
9114                            };
9115                        }
9116                    } else if postfix_lbracket_is_arrow_container(&expr) {
9117                        self.advance();
9118                        let indices = self.parse_arg_list()?;
9119                        self.expect(&Token::RBracket)?;
9120                        expr = Expr {
9121                            kind: ExprKind::ArrowDeref {
9122                                expr: Box::new(expr),
9123                                index: Box::new(Expr {
9124                                    kind: ExprKind::List(indices),
9125                                    line,
9126                                }),
9127                                kind: DerefKind::Array,
9128                            },
9129                            line,
9130                        };
9131                    } else {
9132                        self.advance();
9133                        let indices = self.parse_arg_list()?;
9134                        self.expect(&Token::RBracket)?;
9135                        expr = Expr {
9136                            kind: ExprKind::AnonymousListSlice {
9137                                source: Box::new(expr),
9138                                indices,
9139                            },
9140                            line,
9141                        };
9142                    }
9143                }
9144                Token::LBrace => {
9145                    if self.suppress_scalar_hash_brace > 0 {
9146                        break;
9147                    }
9148                    // Implicit semicolon: `{` on a new line is a new statement (block/hashref),
9149                    // not a hash subscript on the preceding expression.
9150                    if self.peek_line() > self.prev_line() {
9151                        break;
9152                    }
9153                    // `$h{k}`, or chained `$h{k2}{k3}` / `$r->{a}{b}` / `$a[0]{k}` — second+ `{…}` is
9154                    // hash subscript on the scalar value (same as `-> { … }` without extra `->`).
9155                    let line = expr.line;
9156                    let is_scalar_named_hash = matches!(expr.kind, ExprKind::ScalarVar(_));
9157                    let is_chainable_hash_subscript = is_scalar_named_hash
9158                        || matches!(
9159                            expr.kind,
9160                            ExprKind::HashElement { .. }
9161                                | ExprKind::ArrayElement { .. }
9162                                | ExprKind::ArrowDeref { .. }
9163                                | ExprKind::Deref {
9164                                    kind: Sigil::Scalar,
9165                                    ..
9166                                }
9167                        );
9168                    if !is_chainable_hash_subscript {
9169                        break;
9170                    }
9171                    self.advance();
9172                    let key = self.parse_hash_subscript_key()?;
9173                    self.expect(&Token::RBrace)?;
9174                    expr = if is_scalar_named_hash {
9175                        if let ExprKind::ScalarVar(ref name) = expr.kind {
9176                            let name = name.clone();
9177                            // Perl: `$_ { k }` means `$_->{k}` (implicit arrow), not the `%_` stash hash.
9178                            if name == "_" {
9179                                Expr {
9180                                    kind: ExprKind::ArrowDeref {
9181                                        expr: Box::new(Expr {
9182                                            kind: ExprKind::ScalarVar("_".into()),
9183                                            line,
9184                                        }),
9185                                        index: Box::new(key),
9186                                        kind: DerefKind::Hash,
9187                                    },
9188                                    line,
9189                                }
9190                            } else {
9191                                Expr {
9192                                    kind: ExprKind::HashElement {
9193                                        hash: name,
9194                                        key: Box::new(key),
9195                                    },
9196                                    line,
9197                                }
9198                            }
9199                        } else {
9200                            unreachable!("is_scalar_named_hash implies ScalarVar");
9201                        }
9202                    } else {
9203                        Expr {
9204                            kind: ExprKind::ArrowDeref {
9205                                expr: Box::new(expr),
9206                                index: Box::new(key),
9207                                kind: DerefKind::Hash,
9208                            },
9209                            line,
9210                        }
9211                    };
9212                }
9213                Token::LogNot | Token::BitNot => {
9214                    // Stryke universal string-subscript sugar — paired `!…!`
9215                    // OR paired `~…~`: `$VAR!N!`, `$VAR~N~`, `$VAR!1:5:2!`,
9216                    // `_!N!`, `_~from:to:step~`. Returns substring of the
9217                    // scalar (Unicode chars).  Distinct from `[N]` which has
9218                    // Perl's `@VAR[N]` / `$_[N]` semantics. Both forms work on
9219                    // any scalar (named or topic) without colliding: `!` and
9220                    // `~` after a value have no current postfix meaning (`!=`
9221                    // / `!~` are pre-merged binary tokens; `~` is prefix-only
9222                    // bit-not). The opening and closing delimiter must match.
9223                    //
9224                    // Implementation: rewrite to ArrayElement with a
9225                    // synthetic name `__topicstr__$NAME`. The interpreter
9226                    // and VM strip the prefix and dispatch to char-of-string
9227                    // (and slice-of-string for Range indices).
9228                    if !matches!(expr.kind, ExprKind::ScalarVar(_)) {
9229                        break;
9230                    }
9231                    if self.peek_line() > self.prev_line() {
9232                        break;
9233                    }
9234                    let opener = self.peek().clone();
9235                    let line = expr.line;
9236                    let name = if let ExprKind::ScalarVar(ref n) = expr.kind {
9237                        n.clone()
9238                    } else {
9239                        unreachable!()
9240                    };
9241                    self.advance(); // consume opening `!` or `~`
9242                                    // Suppress `~` as a range separator while parsing the
9243                                    // paired index — `$_~5~` would otherwise consume the
9244                                    // closing `~` as a range op. `:` is still allowed so
9245                                    // `$_~1:3~` (slice with `:` range index) keeps working.
9246                    self.suppress_tilde_range = self.suppress_tilde_range.saturating_add(1);
9247                    let index_result = self.parse_expression();
9248                    self.suppress_tilde_range = self.suppress_tilde_range.saturating_sub(1);
9249                    let index = index_result?;
9250                    let close_match = matches!(
9251                        (&opener, self.peek()),
9252                        (Token::LogNot, Token::LogNot) | (Token::BitNot, Token::BitNot)
9253                    );
9254                    if !close_match {
9255                        let want = if matches!(opener, Token::LogNot) {
9256                            "!"
9257                        } else {
9258                            "~"
9259                        };
9260                        return Err(self.syntax_err(
9261                            format!("expected closing `{}` for string subscript", want),
9262                            self.peek_line(),
9263                        ));
9264                    }
9265                    self.advance(); // consume closing delimiter
9266                    expr = Expr {
9267                        kind: ExprKind::ArrayElement {
9268                            array: format!("__topicstr__{}", name),
9269                            index: Box::new(index),
9270                        },
9271                        line,
9272                    };
9273                }
9274                _ => break,
9275            }
9276        }
9277        Ok(expr)
9278    }
9279
9280    fn parse_primary(&mut self) -> StrykeResult<Expr> {
9281        let line = self.peek_line();
9282        // `my $x = …` (or `our` / `state` / `local`) used inside an expression —
9283        // typically `if (my $x = …)` / `while (my $line = <FH>)`.  Returns the
9284        // assigned value(s); has the side effect of declaring the variable in
9285        // the current scope.  See `ExprKind::MyExpr`.
9286        if let Token::Ident(ref kw) = self.peek().clone() {
9287            // `var` / `val` are surface aliases for `my` / `const my` but they
9288            // are also common user identifiers (struct field names, hash keys,
9289            // method names — `struct Node { val => Int }`, `Box(val => undef)`,
9290            // `$rec->val`). Only treat them as declarators when the lookahead
9291            // is unambiguous: a sigil token (`$x` / `@x` / `%x` / `*x`), an
9292            // `(` for list destructuring, or a `typed` modifier. In all other
9293            // positions (`val =>` / `val:` / `val,` / `val)` / `val;`) they
9294            // are barewords and fall through to the normal expression parser.
9295            let is_var_val_decl_start = is_post_style_decl(kw)
9296                && matches!(
9297                    self.peek_at(1),
9298                    Token::ScalarVar(_)
9299                        | Token::ArrayVar(_)
9300                        | Token::HashVar(_)
9301                        | Token::Star
9302                        | Token::LParen
9303                ) || is_post_style_decl(kw)
9304                    && matches!(
9305                        self.peek_at(1),
9306                        Token::Ident(ref kw) if kw == "typed"
9307                    );
9308            let eager_kw = matches!(kw.as_str(), "my" | "our" | "state" | "local");
9309            if eager_kw || is_var_val_decl_start {
9310                let raw_kw = kw.clone();
9311                // `var` / `val` are surface aliases; normalize to `my` for
9312                // the inner parser (same as the statement-form dispatch).
9313                // `val` requires marking the resulting decls frozen so the
9314                // expression form `if (val $x = …) { … }` matches its
9315                // statement-form counterpart `if (const my $x = …) { … }`.
9316                let kw_owned: String = if is_post_style_decl(&raw_kw) {
9317                    "my".to_string()
9318                } else {
9319                    raw_kw.clone()
9320                };
9321                let allow_type = is_frozen_decl(&raw_kw);
9322                let mark_frozen = is_frozen_decl(&raw_kw);
9323                // Parse exactly like the statement form via `parse_my_our_local`,
9324                // then unwrap the resulting `StmtKind::*` back into a list of
9325                // `VarDecl`s for the expression node.  This re-uses the full
9326                // syntax (typed sigs, list destructuring, type annotations).
9327                let saved_pos = self.pos;
9328                let stmt = self.parse_my_our_local(&kw_owned, allow_type)?;
9329                let mut decls = match stmt.kind {
9330                    StmtKind::My(d)
9331                    | StmtKind::Our(d)
9332                    | StmtKind::State(d)
9333                    | StmtKind::Local(d) => d,
9334                    _ => {
9335                        // `local *FOO = …` / non-decl forms — fall back to the
9336                        // statement parser (already advanced); restore position
9337                        // and let the surrounding code handle it as a statement
9338                        // by erroring loudly here.
9339                        self.pos = saved_pos;
9340                        return Err(self.syntax_err(
9341                            "`my`/`our`/`local` in expression must declare variables",
9342                            line,
9343                        ));
9344                    }
9345                };
9346                if mark_frozen {
9347                    for d in decls.iter_mut() {
9348                        d.frozen = true;
9349                    }
9350                }
9351                return Ok(Expr {
9352                    kind: ExprKind::MyExpr {
9353                        keyword: kw_owned,
9354                        decls,
9355                    },
9356                    line,
9357                });
9358            }
9359        }
9360        match self.peek().clone() {
9361            Token::Integer(n) => {
9362                self.advance();
9363                Ok(Expr {
9364                    kind: ExprKind::Integer(n),
9365                    line,
9366                })
9367            }
9368            Token::Float(f) => {
9369                self.advance();
9370                Ok(Expr {
9371                    kind: ExprKind::Float(f),
9372                    line,
9373                })
9374            }
9375            // `>{ BLOCK }` — IIFE block expression (immediately-invoked anonymous sub).
9376            // Valid in any expression position; evaluates the block and yields its last value.
9377            // In thread-macro stage position (`EXPR |>` already consumed by the stage loop in
9378            // `parse_thread_macro`), the explicit branch at ~1417 wins and the block is
9379            // instead pipe-applied as a coderef — that path is never reached from here.
9380            Token::ArrowBrace => {
9381                self.advance();
9382                let mut stmts = Vec::new();
9383                while !matches!(self.peek(), Token::RBrace | Token::Eof) {
9384                    if self.eat(&Token::Semicolon) {
9385                        continue;
9386                    }
9387                    stmts.push(self.parse_statement()?);
9388                }
9389                self.expect(&Token::RBrace)?;
9390                let inner_line = stmts.first().map(|s| s.line).unwrap_or(line);
9391                let inner = Expr {
9392                    kind: ExprKind::CodeRef {
9393                        params: vec![],
9394                        body: stmts,
9395                    },
9396                    line: inner_line,
9397                };
9398                Ok(Expr {
9399                    kind: ExprKind::Do(Box::new(inner)),
9400                    line,
9401                })
9402            }
9403            Token::Star => {
9404                self.advance();
9405                if matches!(self.peek(), Token::LBrace) {
9406                    self.advance();
9407                    let inner = self.parse_expression()?;
9408                    self.expect(&Token::RBrace)?;
9409                    return Ok(Expr {
9410                        kind: ExprKind::Deref {
9411                            expr: Box::new(inner),
9412                            kind: Sigil::Typeglob,
9413                        },
9414                        line,
9415                    });
9416                }
9417                // `*$_{$k}`, `*${expr}`, `*$foo` — typeglob from a sigil expression (Perl 5 `*$globref`).
9418                if matches!(
9419                    self.peek(),
9420                    Token::ScalarVar(_)
9421                        | Token::ArrayVar(_)
9422                        | Token::HashVar(_)
9423                        | Token::DerefScalarVar(_)
9424                        | Token::HashPercent
9425                ) {
9426                    let inner = self.parse_postfix()?;
9427                    return Ok(Expr {
9428                        kind: ExprKind::TypeglobExpr(Box::new(inner)),
9429                        line,
9430                    });
9431                }
9432                // `x` tokenizes as `Token::X` (repeat op) — still a valid package/typeglob name.
9433                let mut full_name = match self.advance() {
9434                    (Token::Ident(n), _) => n,
9435                    (Token::X, _) => "x".to_string(),
9436                    (tok, l) => {
9437                        return Err(self
9438                            .syntax_err(format!("Expected identifier after *, got {:?}", tok), l));
9439                    }
9440                };
9441                while self.eat(&Token::PackageSep) {
9442                    match self.advance() {
9443                        (Token::Ident(part), _) => {
9444                            full_name = format!("{}::{}", full_name, part);
9445                        }
9446                        (Token::X, _) => {
9447                            full_name = format!("{}::x", full_name);
9448                        }
9449                        (tok, l) => {
9450                            return Err(self.syntax_err(
9451                                format!("Expected identifier after :: in typeglob, got {:?}", tok),
9452                                l,
9453                            ));
9454                        }
9455                    }
9456                }
9457                Ok(Expr {
9458                    kind: ExprKind::Typeglob(full_name),
9459                    line,
9460                })
9461            }
9462            Token::SingleString(s) => {
9463                self.advance();
9464                Ok(Expr {
9465                    kind: ExprKind::String(s),
9466                    line,
9467                })
9468            }
9469            Token::DoubleString(s) => {
9470                self.advance();
9471                self.parse_interpolated_string(&s, line)
9472            }
9473            Token::BacktickString(s) => {
9474                self.advance();
9475                let inner = self.parse_interpolated_string(&s, line)?;
9476                Ok(Expr {
9477                    kind: ExprKind::Qx(Box::new(inner)),
9478                    line,
9479                })
9480            }
9481            Token::HereDoc(_, body, interpolate) => {
9482                self.advance();
9483                if interpolate {
9484                    self.parse_interpolated_string(&body, line)
9485                } else {
9486                    Ok(Expr {
9487                        kind: ExprKind::String(body),
9488                        line,
9489                    })
9490                }
9491            }
9492            Token::Regex(pattern, flags, _delim) => {
9493                self.advance();
9494                Ok(Expr {
9495                    kind: ExprKind::Regex(pattern, flags),
9496                    line,
9497                })
9498            }
9499            Token::QW(words) => {
9500                self.advance();
9501                // `qw(a b c) x N` is list-repeat in Perl even without explicit
9502                // outer parens — `qw(...)` is itself a list constructor.
9503                self.list_construct_close_pos = Some(self.pos);
9504                Ok(Expr {
9505                    kind: ExprKind::QW(words),
9506                    line,
9507                })
9508            }
9509            Token::DerefScalarVar(name) => {
9510                self.advance();
9511                Ok(Expr {
9512                    kind: ExprKind::Deref {
9513                        expr: Box::new(Expr {
9514                            kind: ExprKind::ScalarVar(name),
9515                            line,
9516                        }),
9517                        kind: Sigil::Scalar,
9518                    },
9519                    line,
9520                })
9521            }
9522            Token::ScalarVar(name) => {
9523                self.advance();
9524                Ok(Expr {
9525                    kind: ExprKind::ScalarVar(name),
9526                    line,
9527                })
9528            }
9529            Token::ArrayVar(name) => {
9530                self.advance();
9531                // Check for slice: @arr[...] (array slice) or @hash{...} (hash slice)
9532                match self.peek() {
9533                    Token::LBracket => {
9534                        self.advance();
9535                        let indices = self.parse_slice_arg_list(false)?;
9536                        self.expect(&Token::RBracket)?;
9537                        Ok(Expr {
9538                            kind: ExprKind::ArraySlice {
9539                                array: name,
9540                                indices,
9541                            },
9542                            line,
9543                        })
9544                    }
9545                    Token::LBrace if self.suppress_scalar_hash_brace == 0 => {
9546                        self.advance();
9547                        let keys = self.parse_slice_arg_list(true)?;
9548                        self.expect(&Token::RBrace)?;
9549                        Ok(Expr {
9550                            kind: ExprKind::HashSlice { hash: name, keys },
9551                            line,
9552                        })
9553                    }
9554                    _ => Ok(Expr {
9555                        kind: ExprKind::ArrayVar(name),
9556                        line,
9557                    }),
9558                }
9559            }
9560            Token::HashVar(name) => {
9561                self.advance();
9562                // `%h{KEYS}` — Perl 5.20+ key-value slice. Parser-level
9563                // disambiguation: `%h` immediately followed by `{` is a kv-
9564                // slice; `%h` alone (or followed by `=`, list ops, etc.) is
9565                // the bare hash. (BUG-008)
9566                if matches!(self.peek(), Token::LBrace) && self.suppress_scalar_hash_brace == 0 {
9567                    self.advance(); // {
9568                    let keys = self.parse_slice_arg_list(true)?;
9569                    self.expect(&Token::RBrace)?;
9570                    return Ok(Expr {
9571                        kind: ExprKind::HashKvSlice { hash: name, keys },
9572                        line,
9573                    });
9574                }
9575                Ok(Expr {
9576                    kind: ExprKind::HashVar(name),
9577                    line,
9578                })
9579            }
9580            Token::HashPercent => {
9581                // `%$href` — hash ref deref; `%{ $expr }` — symbolic / braced form
9582                self.advance();
9583                if matches!(self.peek(), Token::ScalarVar(_)) {
9584                    let n = match self.advance() {
9585                        (Token::ScalarVar(n), _) => n,
9586                        (tok, l) => {
9587                            return Err(self.syntax_err(
9588                                format!("Expected scalar variable after %%, got {:?}", tok),
9589                                l,
9590                            ));
9591                        }
9592                    };
9593                    return Ok(Expr {
9594                        kind: ExprKind::Deref {
9595                            expr: Box::new(Expr {
9596                                kind: ExprKind::ScalarVar(n),
9597                                line,
9598                            }),
9599                            kind: Sigil::Hash,
9600                        },
9601                        line,
9602                    });
9603                }
9604                // `%[a => 1, b => 2]` — sugar for `%{+{a=>1,b=>2}}`: dereference an
9605                // anonymous hashref inline, using `[...]` as the delimiter to avoid
9606                // the block-vs-hashref ambiguity that `%{a=>1}` has in real Perl.
9607                // Real Perl errors on `%[...]` syntactically, so no compat risk.
9608                if matches!(self.peek(), Token::LBracket) {
9609                    self.advance();
9610                    let pairs = self.parse_hashref_pairs_until(&Token::RBracket)?;
9611                    self.expect(&Token::RBracket)?;
9612                    let href = Expr {
9613                        kind: ExprKind::HashRef(pairs),
9614                        line,
9615                    };
9616                    return Ok(Expr {
9617                        kind: ExprKind::Deref {
9618                            expr: Box::new(href),
9619                            kind: Sigil::Hash,
9620                        },
9621                        line,
9622                    });
9623                }
9624                self.expect(&Token::LBrace)?;
9625                // Peek to disambiguate `%{ $ref }` (deref a hashref expression) from
9626                // `%{ k => v }` (inline hash literal). Real Perl's block-vs-hashref
9627                // heuristic is famously unreliable — when the first non-whitespace
9628                // token is an ident/string followed by `=>`, treat the whole thing
9629                // as a hashref literal to make `%{a=>1,b=>2}` work reliably.
9630                let looks_like_pair = matches!(
9631                    self.peek(),
9632                    Token::Ident(_) | Token::SingleString(_) | Token::DoubleString(_)
9633                ) && matches!(self.peek_at(1), Token::FatArrow);
9634                let inner = if looks_like_pair {
9635                    let pairs = self.parse_hashref_pairs_until(&Token::RBrace)?;
9636                    Expr {
9637                        kind: ExprKind::HashRef(pairs),
9638                        line,
9639                    }
9640                } else {
9641                    self.parse_expression()?
9642                };
9643                self.expect(&Token::RBrace)?;
9644                Ok(Expr {
9645                    kind: ExprKind::Deref {
9646                        expr: Box::new(inner),
9647                        kind: Sigil::Hash,
9648                    },
9649                    line,
9650                })
9651            }
9652            Token::ArrayAt => {
9653                self.advance();
9654                // `@{ $expr }` / `@{ "Pkg::NAME" }` — symbolic array (e.g. `@{"$pkg\::EXPORT"}` in Exporter.pm)
9655                if matches!(self.peek(), Token::LBrace) {
9656                    self.advance();
9657                    let inner = self.parse_expression()?;
9658                    self.expect(&Token::RBrace)?;
9659                    // `@{$href}{k1,k2}` — hash slice through a hashref using
9660                    // the curly-brace deref form. Mirrors the `@$href{KEYS}`
9661                    // path (BUG-091/BUG-217). Likewise `@{$aref}[i,j]` is the
9662                    // array-slice-through-arrayref form.
9663                    if matches!(self.peek(), Token::LBrace) {
9664                        self.advance();
9665                        let keys = self.parse_slice_arg_list(true)?;
9666                        self.expect(&Token::RBrace)?;
9667                        return Ok(Expr {
9668                            kind: ExprKind::HashSliceDeref {
9669                                container: Box::new(inner),
9670                                keys,
9671                            },
9672                            line,
9673                        });
9674                    }
9675                    if matches!(self.peek(), Token::LBracket) {
9676                        self.advance();
9677                        let indices = self.parse_slice_arg_list(false)?;
9678                        self.expect(&Token::RBracket)?;
9679                        let source = Expr {
9680                            kind: ExprKind::Deref {
9681                                expr: Box::new(inner),
9682                                kind: Sigil::Array,
9683                            },
9684                            line,
9685                        };
9686                        return Ok(Expr {
9687                            kind: ExprKind::AnonymousListSlice {
9688                                source: Box::new(source),
9689                                indices,
9690                            },
9691                            line,
9692                        });
9693                    }
9694                    return Ok(Expr {
9695                        kind: ExprKind::Deref {
9696                            expr: Box::new(inner),
9697                            kind: Sigil::Array,
9698                        },
9699                        line,
9700                    });
9701                }
9702                // `@[a, b, c]` — sugar for `@{[a, b, c]}`: dereference an
9703                // anonymous arrayref inline. Real Perl rejects `@[...]` at
9704                // the parser level, so this extension has no compat risk.
9705                if matches!(self.peek(), Token::LBracket) {
9706                    self.advance();
9707                    let mut elems = Vec::new();
9708                    if !matches!(self.peek(), Token::RBracket) {
9709                        elems.push(self.parse_assign_expr()?);
9710                        while self.eat(&Token::Comma) {
9711                            if matches!(self.peek(), Token::RBracket) {
9712                                break;
9713                            }
9714                            elems.push(self.parse_assign_expr()?);
9715                        }
9716                    }
9717                    self.expect(&Token::RBracket)?;
9718                    let aref = Expr {
9719                        kind: ExprKind::ArrayRef(elems),
9720                        line,
9721                    };
9722                    return Ok(Expr {
9723                        kind: ExprKind::Deref {
9724                            expr: Box::new(aref),
9725                            kind: Sigil::Array,
9726                        },
9727                        line,
9728                    });
9729                }
9730                // `@$arr` — array dereference; `@$h{k1,k2}` — hash slice via hashref
9731                let container = match self.peek().clone() {
9732                    Token::ScalarVar(n) => {
9733                        self.advance();
9734                        Expr {
9735                            kind: ExprKind::ScalarVar(n),
9736                            line,
9737                        }
9738                    }
9739                    _ => {
9740                        return Err(self.syntax_err(
9741                            "Expected `$name`, `{`, or `[` after `@` (e.g. `@$aref`, `@{expr}`, `@[1,2,3]`, or `@$href{keys}`)",
9742                            line,
9743                        ));
9744                    }
9745                };
9746                if matches!(self.peek(), Token::LBrace) {
9747                    self.advance();
9748                    let keys = self.parse_slice_arg_list(true)?;
9749                    self.expect(&Token::RBrace)?;
9750                    return Ok(Expr {
9751                        kind: ExprKind::HashSliceDeref {
9752                            container: Box::new(container),
9753                            keys,
9754                        },
9755                        line,
9756                    });
9757                }
9758                Ok(Expr {
9759                    kind: ExprKind::Deref {
9760                        expr: Box::new(container),
9761                        kind: Sigil::Array,
9762                    },
9763                    line,
9764                })
9765            }
9766            Token::LParen => {
9767                self.advance();
9768                if matches!(self.peek(), Token::RParen) {
9769                    self.advance();
9770                    // Empty `() x 3` is a no-op list repeat — record the close
9771                    // position so `Token::X` knows the LHS was a list literal.
9772                    self.list_construct_close_pos = Some(self.pos);
9773                    return Ok(Expr {
9774                        kind: ExprKind::List(vec![]),
9775                        line,
9776                    });
9777                }
9778                // Inside parens, pipe-forward is allowed even if we're in a
9779                // paren-less arg context. Save and restore no_pipe_forward_depth.
9780                let saved_no_pipe = self.no_pipe_forward_depth;
9781                self.no_pipe_forward_depth = 0;
9782                // Thread-macro `on` may set `suppress_indirect_paren_call` so
9783                // `on $c ()` does not steal `()`; inside explicit `(...)` use
9784                // normal postfix-`(` rules (`on ($factory())`).
9785                let saved_indirect = self.suppress_indirect_paren_call;
9786                self.suppress_indirect_paren_call = 0;
9787                let expr = self.parse_expression();
9788                self.no_pipe_forward_depth = saved_no_pipe;
9789                self.suppress_indirect_paren_call = saved_indirect;
9790                let expr = expr?;
9791                self.expect(&Token::RParen)?;
9792                // Mark this paren as a list-constructor for the `x` operator
9793                // (parse_multiplication compares `self.pos` at the X token to
9794                // this checkpoint). Function-call parens (`f(args)`) don't
9795                // reach this branch; they're parsed by the call machinery.
9796                self.list_construct_close_pos = Some(self.pos);
9797                Ok(expr)
9798            }
9799            Token::LBracket => {
9800                self.advance();
9801                let elems = self.parse_arg_list()?;
9802                self.expect(&Token::RBracket)?;
9803                Ok(Expr {
9804                    kind: ExprKind::ArrayRef(elems),
9805                    line,
9806                })
9807            }
9808            Token::LBrace => {
9809                // Could be hash ref or block — disambiguate
9810                self.advance();
9811                // Try to parse as hash ref: { key => val, ... }
9812                let saved = self.pos;
9813                match self.try_parse_hash_ref() {
9814                    Ok(pairs) => Ok(Expr {
9815                        kind: ExprKind::HashRef(pairs),
9816                        line,
9817                    }),
9818                    Err(_) => {
9819                        self.pos = saved;
9820                        // Parse as block, wrap in code ref
9821                        let mut stmts = Vec::new();
9822                        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
9823                            if self.eat(&Token::Semicolon) {
9824                                continue;
9825                            }
9826                            stmts.push(self.parse_statement()?);
9827                        }
9828                        self.expect(&Token::RBrace)?;
9829                        Ok(Expr {
9830                            kind: ExprKind::CodeRef {
9831                                params: vec![],
9832                                body: stmts,
9833                            },
9834                            line,
9835                        })
9836                    }
9837                }
9838            }
9839            Token::Diamond => {
9840                self.advance();
9841                Ok(Expr {
9842                    kind: ExprKind::ReadLine(None),
9843                    line,
9844                })
9845            }
9846            Token::ReadLine(handle) => {
9847                self.advance();
9848                Ok(Expr {
9849                    kind: ExprKind::ReadLine(Some(handle)),
9850                    line,
9851                })
9852            }
9853
9854            // Named functions / builtins
9855            Token::ThreadArrow => {
9856                self.advance();
9857                self.parse_thread_macro(line, false)
9858            }
9859            Token::ThreadArrowLast => {
9860                self.advance();
9861                self.parse_thread_macro(line, true)
9862            }
9863            Token::ThreadArrowStream => {
9864                self.advance();
9865                let mut stages = Vec::new();
9866                self.parse_thread_macro_inner(line, false, Some(&mut stages))
9867            }
9868            Token::ThreadArrowStreamLast => {
9869                self.advance();
9870                let mut stages = Vec::new();
9871                self.parse_thread_macro_inner(line, true, Some(&mut stages))
9872            }
9873            Token::ThreadArrowPar => {
9874                self.advance();
9875                self.parse_thread_macro_chunk_par(line, false)
9876            }
9877            Token::ThreadArrowParLast => {
9878                self.advance();
9879                self.parse_thread_macro_chunk_par(line, true)
9880            }
9881            Token::ThreadArrowDist => {
9882                self.advance();
9883                self.parse_thread_macro_dist(line, false)
9884            }
9885            Token::ThreadArrowDistLast => {
9886                self.advance();
9887                self.parse_thread_macro_dist(line, true)
9888            }
9889            Token::Ident(ref name) => {
9890                let name = name.clone();
9891                // Handle s///
9892                if name.starts_with('\x00') {
9893                    self.advance();
9894                    let parts: Vec<&str> = name.split('\x00').collect();
9895                    if parts.len() >= 4 && parts[1] == "s" {
9896                        let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
9897                        return Ok(Expr {
9898                            kind: ExprKind::Substitution {
9899                                expr: Box::new(Expr {
9900                                    kind: ExprKind::ScalarVar("_".into()),
9901                                    line,
9902                                }),
9903                                pattern: parts[2].to_string(),
9904                                replacement: parts[3].to_string(),
9905                                flags: parts.get(4).unwrap_or(&"").to_string(),
9906                                delim,
9907                            },
9908                            line,
9909                        });
9910                    }
9911                    if parts.len() >= 4 && parts[1] == "tr" {
9912                        let delim = parts.get(5).and_then(|s| s.chars().next()).unwrap_or('/');
9913                        return Ok(Expr {
9914                            kind: ExprKind::Transliterate {
9915                                expr: Box::new(Expr {
9916                                    kind: ExprKind::ScalarVar("_".into()),
9917                                    line,
9918                                }),
9919                                from: parts[2].to_string(),
9920                                to: parts[3].to_string(),
9921                                flags: parts.get(4).unwrap_or(&"").to_string(),
9922                                delim,
9923                            },
9924                            line,
9925                        });
9926                    }
9927                    return Err(self.syntax_err("Unexpected encoded token", line));
9928                }
9929                self.parse_named_expr(name)
9930            }
9931
9932            // `%name` when lexer emitted `Token::Percent` (due to preceding term context)
9933            // instead of `Token::HashVar`. This happens after `t` (thread macro) etc.
9934            Token::Percent => {
9935                self.advance();
9936                match self.peek().clone() {
9937                    Token::Ident(name) => {
9938                        self.advance();
9939                        Ok(Expr {
9940                            kind: ExprKind::HashVar(name),
9941                            line,
9942                        })
9943                    }
9944                    Token::ScalarVar(n) => {
9945                        self.advance();
9946                        Ok(Expr {
9947                            kind: ExprKind::Deref {
9948                                expr: Box::new(Expr {
9949                                    kind: ExprKind::ScalarVar(n),
9950                                    line,
9951                                }),
9952                                kind: Sigil::Hash,
9953                            },
9954                            line,
9955                        })
9956                    }
9957                    Token::LBrace => {
9958                        self.advance();
9959                        let looks_like_pair = matches!(
9960                            self.peek(),
9961                            Token::Ident(_) | Token::SingleString(_) | Token::DoubleString(_)
9962                        ) && matches!(self.peek_at(1), Token::FatArrow);
9963                        let inner = if looks_like_pair {
9964                            let pairs = self.parse_hashref_pairs_until(&Token::RBrace)?;
9965                            Expr {
9966                                kind: ExprKind::HashRef(pairs),
9967                                line,
9968                            }
9969                        } else {
9970                            self.parse_expression()?
9971                        };
9972                        self.expect(&Token::RBrace)?;
9973                        Ok(Expr {
9974                            kind: ExprKind::Deref {
9975                                expr: Box::new(inner),
9976                                kind: Sigil::Hash,
9977                            },
9978                            line,
9979                        })
9980                    }
9981                    Token::LBracket => {
9982                        self.advance();
9983                        let pairs = self.parse_hashref_pairs_until(&Token::RBracket)?;
9984                        self.expect(&Token::RBracket)?;
9985                        let href = Expr {
9986                            kind: ExprKind::HashRef(pairs),
9987                            line,
9988                        };
9989                        Ok(Expr {
9990                            kind: ExprKind::Deref {
9991                                expr: Box::new(href),
9992                                kind: Sigil::Hash,
9993                            },
9994                            line,
9995                        })
9996                    }
9997                    tok => Err(self.syntax_err(
9998                        format!(
9999                            "Expected identifier, `$`, `{{`, or `[` after `%`, got {:?}",
10000                            tok
10001                        ),
10002                        line,
10003                    )),
10004                }
10005            }
10006
10007            tok => Err(self.syntax_err(format!("Unexpected token {:?}", tok), line)),
10008        }
10009    }
10010
10011    fn parse_named_expr(&mut self, mut name: String) -> StrykeResult<Expr> {
10012        let line = self.peek_line();
10013        self.advance(); // consume the ident
10014        while self.eat(&Token::PackageSep) {
10015            match self.advance() {
10016                (Token::Ident(part), _) => {
10017                    name = format!("{}::{}", name, part);
10018                }
10019                (tok, err_line) => {
10020                    return Err(self.syntax_err(
10021                        format!("Expected identifier after `::`, got {:?}", tok),
10022                        err_line,
10023                    ));
10024                }
10025            }
10026        }
10027
10028        // Fat-arrow auto-quoting: ANY bareword (including keywords/builtins)
10029        // before `=>` is treated as a string key, matching Perl 5 semantics.
10030        // e.g. `(print => 1, pr => "x", sort => 3)` are all valid hash pairs.
10031        // Stryke exception: topic-slot barewords (`_`, `_<`, `_0`, `_0<`, …) are
10032        // scalar references to the topic / positional / outer-topic chain — they
10033        // must evaluate as the topic value, not the literal name.
10034        if matches!(self.peek(), Token::FatArrow) && !Self::is_underscore_topic_slot(&name) {
10035            return Ok(Expr {
10036                kind: ExprKind::String(name),
10037                line,
10038            });
10039        }
10040
10041        if crate::compat_mode() {
10042            if let Some(ext) = Self::stryke_extension_name(&name) {
10043                if !self.declared_subs.contains(&name) {
10044                    return Err(self.syntax_err(
10045                        format!("`{ext}` is a stryke extension (disabled by --compat)"),
10046                        line,
10047                    ));
10048                }
10049            }
10050        }
10051
10052        // `CORE::length(...)` etc. — strip the explicit core-dispatch prefix so
10053        // the keyword arms below match the bare name and produce the same
10054        // `ExprKind::Length` / `ExprKind::Print` / etc. as the unprefixed form.
10055        // Matches Perl 5's `CORE::` namespace, which routes back to the
10056        // built-in implementation regardless of any same-named user sub.
10057        // (PARITY-011)
10058        if let Some(rest) = name.strip_prefix("CORE::") {
10059            name = rest.to_string();
10060        }
10061
10062        // `null` — stryke alias for `undef` (JS/SQL spelling). Normalized
10063        // here so every `undef` arm (value, `undef $x`, list sink) sees the
10064        // canonical name. `fn null` is blocked by RESERVED_FUNCTION_NAMES;
10065        // compat mode leaves `null` as an ordinary sub name.
10066        if name == "null" && !crate::compat_mode() {
10067            name = "undef".to_string();
10068        }
10069
10070        match name.as_str() {
10071            "__FILE__" => Ok(Expr {
10072                kind: ExprKind::MagicConst(MagicConstKind::File),
10073                line,
10074            }),
10075            "__LINE__" => Ok(Expr {
10076                kind: ExprKind::MagicConst(MagicConstKind::Line),
10077                line,
10078            }),
10079            "__SUB__" => Ok(Expr {
10080                kind: ExprKind::MagicConst(MagicConstKind::Sub),
10081                line,
10082            }),
10083            // `__PACKAGE__` is a compile-time constant set to the currently
10084            // active package, so a sub body in `package Demo::P1` keeps
10085            // returning `"Demo::P1"` regardless of the caller's package
10086            // (Perl 5 documented behavior).
10087            "__PACKAGE__" => Ok(Expr {
10088                kind: ExprKind::String(self.current_package.clone()),
10089                line,
10090            }),
10091            "stdin" => Ok(Expr {
10092                kind: ExprKind::FuncCall {
10093                    name: "stdin".into(),
10094                    args: vec![],
10095                },
10096                line,
10097            }),
10098            "range" => {
10099                let args = self.parse_builtin_args()?;
10100                Ok(Expr {
10101                    kind: ExprKind::FuncCall {
10102                        name: "range".into(),
10103                        args,
10104                    },
10105                    line,
10106                })
10107            }
10108            "print" | "pr" => self.parse_print_like(|h, a| ExprKind::Print { handle: h, args: a }),
10109            "say" => {
10110                if crate::no_interop_mode() {
10111                    return Err(
10112                        self.syntax_err("stryke uses `p` instead of `say` (--no-interop)", line)
10113                    );
10114                }
10115                self.parse_print_like(|h, a| ExprKind::Say { handle: h, args: a })
10116            }
10117            "p" => self.parse_print_like(|h, a| ExprKind::Say { handle: h, args: a }),
10118            "printf" => self.parse_print_like(|h, a| ExprKind::Printf { handle: h, args: a }),
10119            "die" => {
10120                let args = self.parse_list_until_terminator()?;
10121                Ok(Expr {
10122                    kind: ExprKind::Die(args),
10123                    line,
10124                })
10125            }
10126            "warn" => {
10127                let args = self.parse_list_until_terminator()?;
10128                Ok(Expr {
10129                    kind: ExprKind::Warn(args),
10130                    line,
10131                })
10132            }
10133            // `croak` / `confess` — `Carp` builtins available without `use Carp`
10134            // (matches the doc claim in `lsp.rs:1243`). For now both desugar to
10135            // `die` — TODO: croak should report caller's file/line, confess
10136            // should append a full stack trace.
10137            "croak" | "confess" => {
10138                let args = self.parse_list_until_terminator()?;
10139                Ok(Expr {
10140                    kind: ExprKind::Die(args),
10141                    line,
10142                })
10143            }
10144            // `carp` / `cluck` — `Carp` warning siblings of `croak`/`confess`.
10145            "carp" | "cluck" => {
10146                let args = self.parse_list_until_terminator()?;
10147                Ok(Expr {
10148                    kind: ExprKind::Warn(args),
10149                    line,
10150                })
10151            }
10152            "chomp" => {
10153                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10154                    return Ok(e);
10155                }
10156                let a = self.parse_one_arg_or_default()?;
10157                Ok(Expr {
10158                    kind: ExprKind::Chomp(Box::new(a)),
10159                    line,
10160                })
10161            }
10162            "chop" => {
10163                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10164                    return Ok(e);
10165                }
10166                let a = self.parse_one_arg_or_default()?;
10167                Ok(Expr {
10168                    kind: ExprKind::Chop(Box::new(a)),
10169                    line,
10170                })
10171            }
10172            "length" => {
10173                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10174                    return Ok(e);
10175                }
10176                let a = self.parse_one_arg_or_default()?;
10177                Ok(Expr {
10178                    kind: ExprKind::Length(Box::new(a)),
10179                    line,
10180                })
10181            }
10182            "defined" => {
10183                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10184                    return Ok(e);
10185                }
10186                // Named-unary precedence: `defined X && Y` is `(defined X) && Y`,
10187                // not `defined(X && Y)`. The default `parse_one_arg_or_default`
10188                // path is greedy (calls `parse_assign_expr_stop_at_pipe`), which
10189                // would let `&&` bind into the argument and silently make
10190                // `defined $h{k} && $h{k} > 0`-style guards always-true when the
10191                // hash element existed. `parse_named_unary_arg` stops at shift
10192                // level so logical operators stay outside.
10193                let a = if matches!(
10194                    self.peek(),
10195                    Token::Semicolon
10196                        | Token::RBrace
10197                        | Token::RParen
10198                        | Token::RBracket
10199                        | Token::Eof
10200                        | Token::Comma
10201                        | Token::FatArrow
10202                        | Token::PipeForward
10203                        | Token::Question
10204                        | Token::Colon
10205                        | Token::NumEq
10206                        | Token::NumNe
10207                        | Token::NumLt
10208                        | Token::NumGt
10209                        | Token::NumLe
10210                        | Token::NumGe
10211                        | Token::Spaceship
10212                        | Token::StrEq
10213                        | Token::StrNe
10214                        | Token::StrLt
10215                        | Token::StrGt
10216                        | Token::StrLe
10217                        | Token::StrGe
10218                        | Token::StrCmp
10219                        | Token::LogAnd
10220                        | Token::LogOr
10221                        | Token::LogNot
10222                        | Token::LogAndWord
10223                        | Token::LogOrWord
10224                        | Token::LogNotWord
10225                        | Token::DefinedOr
10226                        | Token::Range
10227                        | Token::RangeExclusive
10228                        | Token::Assign
10229                        | Token::PlusAssign
10230                        | Token::MinusAssign
10231                        | Token::MulAssign
10232                        | Token::DivAssign
10233                        | Token::ModAssign
10234                        | Token::PowAssign
10235                        | Token::DotAssign
10236                        | Token::AndAssign
10237                        | Token::OrAssign
10238                        | Token::XorAssign
10239                        | Token::DefinedOrAssign
10240                        | Token::ShiftLeftAssign
10241                        | Token::ShiftRightAssign
10242                        | Token::BitAndAssign
10243                        | Token::BitOrAssign
10244                ) {
10245                    Expr {
10246                        kind: ExprKind::ScalarVar("_".into()),
10247                        line: self.peek_line(),
10248                    }
10249                } else if matches!(self.peek(), Token::LParen)
10250                    && matches!(self.peek_at(1), Token::RParen)
10251                {
10252                    let pl = self.peek_line();
10253                    self.advance();
10254                    self.advance();
10255                    Expr {
10256                        kind: ExprKind::ScalarVar("_".into()),
10257                        line: pl,
10258                    }
10259                } else {
10260                    self.parse_named_unary_arg()?
10261                };
10262                Ok(Expr {
10263                    kind: ExprKind::Defined(Box::new(a)),
10264                    line,
10265                })
10266            }
10267            "ref" => {
10268                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10269                    return Ok(e);
10270                }
10271                let a = self.parse_one_arg_or_default()?;
10272                Ok(Expr {
10273                    kind: ExprKind::Ref(Box::new(a)),
10274                    line,
10275                })
10276            }
10277            "undef" => {
10278                // `undef $var` sets `$var` to undef — but a variable on a new line
10279                // is a separate statement (implicit semicolon), not an argument.
10280                if self.peek_line() == self.prev_line()
10281                    && matches!(
10282                        self.peek(),
10283                        Token::ScalarVar(_) | Token::ArrayVar(_) | Token::HashVar(_)
10284                    )
10285                {
10286                    let target = self.parse_primary()?;
10287                    return Ok(Expr {
10288                        kind: ExprKind::Assign {
10289                            target: Box::new(target),
10290                            value: Box::new(Expr {
10291                                kind: ExprKind::Undef,
10292                                line,
10293                            }),
10294                        },
10295                        line,
10296                    });
10297                }
10298                Ok(Expr {
10299                    kind: ExprKind::Undef,
10300                    line,
10301                })
10302            }
10303            "scalar" => {
10304                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10305                    return Ok(e);
10306                }
10307                if crate::no_interop_mode() {
10308                    return Err(self.syntax_err(
10309                        "stryke uses `len` (also `cnt` / `count`) instead of `scalar` (--no-interop)",
10310                        line,
10311                    ));
10312                }
10313                let a = self.parse_one_arg_or_default()?;
10314                Ok(Expr {
10315                    kind: ExprKind::ScalarContext(Box::new(a)),
10316                    line,
10317                })
10318            }
10319            "abs" => {
10320                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10321                    return Ok(e);
10322                }
10323                let a = self.parse_one_arg_or_default()?;
10324                Ok(Expr {
10325                    kind: ExprKind::Abs(Box::new(a)),
10326                    line,
10327                })
10328            }
10329            // stryke unary numeric extensions — treat like `abs` so a bare
10330            // identifier in `map { inc }` / `for (…) { p inc }` becomes a
10331            // call with implicit `$_` rather than falling through to the
10332            // generic `Bareword` arm (which stringifies to `"inc"`).
10333            "inc" | "dec" => {
10334                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10335                    return Ok(e);
10336                }
10337                let a = self.parse_one_arg_or_default()?;
10338                Ok(Expr {
10339                    kind: ExprKind::FuncCall {
10340                        name,
10341                        args: vec![a],
10342                    },
10343                    line,
10344                })
10345            }
10346            "int" => {
10347                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10348                    return Ok(e);
10349                }
10350                let a = self.parse_one_arg_or_default()?;
10351                Ok(Expr {
10352                    kind: ExprKind::Int(Box::new(a)),
10353                    line,
10354                })
10355            }
10356            "sqrt" => {
10357                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10358                    return Ok(e);
10359                }
10360                let a = self.parse_one_arg_or_default()?;
10361                Ok(Expr {
10362                    kind: ExprKind::Sqrt(Box::new(a)),
10363                    line,
10364                })
10365            }
10366            "sin" => {
10367                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10368                    return Ok(e);
10369                }
10370                let a = self.parse_one_arg_or_default()?;
10371                Ok(Expr {
10372                    kind: ExprKind::Sin(Box::new(a)),
10373                    line,
10374                })
10375            }
10376            "cos" => {
10377                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10378                    return Ok(e);
10379                }
10380                let a = self.parse_one_arg_or_default()?;
10381                Ok(Expr {
10382                    kind: ExprKind::Cos(Box::new(a)),
10383                    line,
10384                })
10385            }
10386            "atan2" => {
10387                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10388                    return Ok(e);
10389                }
10390                let args = self.parse_builtin_args()?;
10391                if args.len() != 2 {
10392                    return Err(self.syntax_err("atan2 requires two arguments", line));
10393                }
10394                Ok(Expr {
10395                    kind: ExprKind::Atan2 {
10396                        y: Box::new(args[0].clone()),
10397                        x: Box::new(args[1].clone()),
10398                    },
10399                    line,
10400                })
10401            }
10402            "exp" => {
10403                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10404                    return Ok(e);
10405                }
10406                let a = self.parse_one_arg_or_default()?;
10407                Ok(Expr {
10408                    kind: ExprKind::Exp(Box::new(a)),
10409                    line,
10410                })
10411            }
10412            "log" => {
10413                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10414                    return Ok(e);
10415                }
10416                let a = self.parse_one_arg_or_default()?;
10417                Ok(Expr {
10418                    kind: ExprKind::Log(Box::new(a)),
10419                    line,
10420                })
10421            }
10422            "input" => {
10423                let args = if matches!(
10424                    self.peek(),
10425                    Token::Semicolon
10426                        | Token::RBrace
10427                        | Token::RParen
10428                        | Token::Eof
10429                        | Token::Comma
10430                        | Token::PipeForward
10431                ) {
10432                    vec![]
10433                } else if matches!(self.peek(), Token::LParen) {
10434                    self.advance();
10435                    if matches!(self.peek(), Token::RParen) {
10436                        self.advance();
10437                        vec![]
10438                    } else {
10439                        let a = self.parse_expression()?;
10440                        self.expect(&Token::RParen)?;
10441                        vec![a]
10442                    }
10443                } else {
10444                    let a = self.parse_one_arg()?;
10445                    vec![a]
10446                };
10447                Ok(Expr {
10448                    kind: ExprKind::FuncCall {
10449                        name: "input".to_string(),
10450                        args,
10451                    },
10452                    line,
10453                })
10454            }
10455            "rand" => {
10456                if matches!(
10457                    self.peek(),
10458                    Token::Semicolon
10459                        | Token::RBrace
10460                        | Token::RParen
10461                        | Token::Eof
10462                        | Token::Comma
10463                        | Token::PipeForward
10464                ) {
10465                    Ok(Expr {
10466                        kind: ExprKind::Rand(None),
10467                        line,
10468                    })
10469                } else if matches!(self.peek(), Token::LParen) {
10470                    self.advance();
10471                    if matches!(self.peek(), Token::RParen) {
10472                        self.advance();
10473                        Ok(Expr {
10474                            kind: ExprKind::Rand(None),
10475                            line,
10476                        })
10477                    } else {
10478                        let a = self.parse_expression()?;
10479                        self.expect(&Token::RParen)?;
10480                        Ok(Expr {
10481                            kind: ExprKind::Rand(Some(Box::new(a))),
10482                            line,
10483                        })
10484                    }
10485                } else {
10486                    let a = self.parse_one_arg()?;
10487                    Ok(Expr {
10488                        kind: ExprKind::Rand(Some(Box::new(a))),
10489                        line,
10490                    })
10491                }
10492            }
10493            "srand" => {
10494                if matches!(
10495                    self.peek(),
10496                    Token::Semicolon
10497                        | Token::RBrace
10498                        | Token::RParen
10499                        | Token::Eof
10500                        | Token::Comma
10501                        | Token::PipeForward
10502                ) {
10503                    Ok(Expr {
10504                        kind: ExprKind::Srand(None),
10505                        line,
10506                    })
10507                } else if matches!(self.peek(), Token::LParen) {
10508                    self.advance();
10509                    if matches!(self.peek(), Token::RParen) {
10510                        self.advance();
10511                        Ok(Expr {
10512                            kind: ExprKind::Srand(None),
10513                            line,
10514                        })
10515                    } else {
10516                        let a = self.parse_expression()?;
10517                        self.expect(&Token::RParen)?;
10518                        Ok(Expr {
10519                            kind: ExprKind::Srand(Some(Box::new(a))),
10520                            line,
10521                        })
10522                    }
10523                } else {
10524                    let a = self.parse_one_arg()?;
10525                    Ok(Expr {
10526                        kind: ExprKind::Srand(Some(Box::new(a))),
10527                        line,
10528                    })
10529                }
10530            }
10531            "hex" => {
10532                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10533                    return Ok(e);
10534                }
10535                let a = self.parse_one_arg_or_default()?;
10536                Ok(Expr {
10537                    kind: ExprKind::Hex(Box::new(a)),
10538                    line,
10539                })
10540            }
10541            "oct" => {
10542                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10543                    return Ok(e);
10544                }
10545                let a = self.parse_one_arg_or_default()?;
10546                Ok(Expr {
10547                    kind: ExprKind::Oct(Box::new(a)),
10548                    line,
10549                })
10550            }
10551            "chr" => {
10552                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10553                    return Ok(e);
10554                }
10555                let a = self.parse_one_arg_or_default()?;
10556                Ok(Expr {
10557                    kind: ExprKind::Chr(Box::new(a)),
10558                    line,
10559                })
10560            }
10561            "ord" => {
10562                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10563                    return Ok(e);
10564                }
10565                let a = self.parse_one_arg_or_default()?;
10566                Ok(Expr {
10567                    kind: ExprKind::Ord(Box::new(a)),
10568                    line,
10569                })
10570            }
10571            "lc" => {
10572                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10573                    return Ok(e);
10574                }
10575                let a = self.parse_one_arg_or_default()?;
10576                Ok(Expr {
10577                    kind: ExprKind::Lc(Box::new(a)),
10578                    line,
10579                })
10580            }
10581            "uc" => {
10582                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10583                    return Ok(e);
10584                }
10585                let a = self.parse_one_arg_or_default()?;
10586                Ok(Expr {
10587                    kind: ExprKind::Uc(Box::new(a)),
10588                    line,
10589                })
10590            }
10591            "lcfirst" => {
10592                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10593                    return Ok(e);
10594                }
10595                let a = self.parse_one_arg_or_default()?;
10596                Ok(Expr {
10597                    kind: ExprKind::Lcfirst(Box::new(a)),
10598                    line,
10599                })
10600            }
10601            "ucfirst" => {
10602                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10603                    return Ok(e);
10604                }
10605                let a = self.parse_one_arg_or_default()?;
10606                Ok(Expr {
10607                    kind: ExprKind::Ucfirst(Box::new(a)),
10608                    line,
10609                })
10610            }
10611            "fc" => {
10612                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10613                    return Ok(e);
10614                }
10615                let a = self.parse_one_arg_or_default()?;
10616                Ok(Expr {
10617                    kind: ExprKind::Fc(Box::new(a)),
10618                    line,
10619                })
10620            }
10621            "crypt" => {
10622                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10623                    return Ok(e);
10624                }
10625                let args = self.parse_builtin_args()?;
10626                if args.len() != 2 {
10627                    return Err(self.syntax_err("crypt requires two arguments", line));
10628                }
10629                Ok(Expr {
10630                    kind: ExprKind::Crypt {
10631                        plaintext: Box::new(args[0].clone()),
10632                        salt: Box::new(args[1].clone()),
10633                    },
10634                    line,
10635                })
10636            }
10637            "pos" => {
10638                if matches!(
10639                    self.peek(),
10640                    Token::Semicolon
10641                        | Token::RBrace
10642                        | Token::RParen
10643                        | Token::Eof
10644                        | Token::Comma
10645                        | Token::PipeForward
10646                ) {
10647                    Ok(Expr {
10648                        kind: ExprKind::Pos(None),
10649                        line,
10650                    })
10651                } else if matches!(self.peek(), Token::Assign) {
10652                    // Perl: `pos = EXPR` is `pos($_) = EXPR` (Text::Balanced `_eb_delims`).
10653                    self.advance();
10654                    let rhs = self.parse_assign_expr()?;
10655                    Ok(Expr {
10656                        kind: ExprKind::Assign {
10657                            target: Box::new(Expr {
10658                                kind: ExprKind::Pos(Some(Box::new(Expr {
10659                                    kind: ExprKind::ScalarVar("_".into()),
10660                                    line,
10661                                }))),
10662                                line,
10663                            }),
10664                            value: Box::new(rhs),
10665                        },
10666                        line,
10667                    })
10668                } else if matches!(self.peek(), Token::LParen) {
10669                    self.advance();
10670                    if matches!(self.peek(), Token::RParen) {
10671                        self.advance();
10672                        Ok(Expr {
10673                            kind: ExprKind::Pos(None),
10674                            line,
10675                        })
10676                    } else {
10677                        let a = self.parse_expression()?;
10678                        self.expect(&Token::RParen)?;
10679                        Ok(Expr {
10680                            kind: ExprKind::Pos(Some(Box::new(a))),
10681                            line,
10682                        })
10683                    }
10684                } else {
10685                    let saved = self.pos;
10686                    let subj = self.parse_unary()?;
10687                    if matches!(self.peek(), Token::Assign) {
10688                        self.advance();
10689                        let rhs = self.parse_assign_expr()?;
10690                        Ok(Expr {
10691                            kind: ExprKind::Assign {
10692                                target: Box::new(Expr {
10693                                    kind: ExprKind::Pos(Some(Box::new(subj))),
10694                                    line,
10695                                }),
10696                                value: Box::new(rhs),
10697                            },
10698                            line,
10699                        })
10700                    } else {
10701                        self.pos = saved;
10702                        let a = self.parse_one_arg()?;
10703                        Ok(Expr {
10704                            kind: ExprKind::Pos(Some(Box::new(a))),
10705                            line,
10706                        })
10707                    }
10708                }
10709            }
10710            "study" => {
10711                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10712                    return Ok(e);
10713                }
10714                let a = self.parse_one_arg_or_default()?;
10715                Ok(Expr {
10716                    kind: ExprKind::Study(Box::new(a)),
10717                    line,
10718                })
10719            }
10720            "push" => {
10721                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10722                    return Ok(e);
10723                }
10724                let args = self.parse_builtin_args()?;
10725                let (first, rest) = args
10726                    .split_first()
10727                    .ok_or_else(|| self.syntax_err("push requires arguments", line))?;
10728                // Perl 5.24+ rejects `push SCALAR, ...` at parse time. Reject any
10729                // first arg that is unambiguously a scalar (literal scalar var or
10730                // numeric/string literal). Array refs (`@$x`), bindings, slices,
10731                // and `our @a` style remain permitted.
10732                if matches!(
10733                    first.kind,
10734                    ExprKind::ScalarVar(_)
10735                        | ExprKind::Integer(_)
10736                        | ExprKind::Float(_)
10737                        | ExprKind::String(_)
10738                ) {
10739                    return Err(self
10740                        .syntax_err("Experimental push on scalar is now forbidden", line)
10741                        .with_near("at EOF"));
10742                }
10743                Ok(Expr {
10744                    kind: ExprKind::Push {
10745                        array: Box::new(first.clone()),
10746                        values: rest.to_vec(),
10747                    },
10748                    line,
10749                })
10750            }
10751            "pop" => {
10752                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10753                    return Ok(e);
10754                }
10755                let a = self.parse_one_arg_or_argv()?;
10756                Ok(Expr {
10757                    kind: ExprKind::Pop(Box::new(a)),
10758                    line,
10759                })
10760            }
10761            "shift" => {
10762                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10763                    return Ok(e);
10764                }
10765                let a = self.parse_one_arg_or_argv()?;
10766                Ok(Expr {
10767                    kind: ExprKind::Shift(Box::new(a)),
10768                    line,
10769                })
10770            }
10771            "unshift" => {
10772                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10773                    return Ok(e);
10774                }
10775                let args = self.parse_builtin_args()?;
10776                let (first, rest) = args
10777                    .split_first()
10778                    .ok_or_else(|| self.syntax_err("unshift requires arguments", line))?;
10779                Ok(Expr {
10780                    kind: ExprKind::Unshift {
10781                        array: Box::new(first.clone()),
10782                        values: rest.to_vec(),
10783                    },
10784                    line,
10785                })
10786            }
10787            "splice" => {
10788                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10789                    return Ok(e);
10790                }
10791                let args = self.parse_builtin_args()?;
10792                let mut iter = args.into_iter();
10793                let array = Box::new(
10794                    iter.next()
10795                        .ok_or_else(|| self.syntax_err("splice requires arguments", line))?,
10796                );
10797                let offset = iter.next().map(Box::new);
10798                let length = iter.next().map(Box::new);
10799                let replacement: Vec<Expr> = iter.collect();
10800                Ok(Expr {
10801                    kind: ExprKind::Splice {
10802                        array,
10803                        offset,
10804                        length,
10805                        replacement,
10806                    },
10807                    line,
10808                })
10809            }
10810            // `splice_last(@a, off[, n])` is the stryke spelling of Perl's
10811            // `scalar splice(@a, off, n)` — returns the LAST removed element
10812            // (or undef if nothing was removed). Desugars to `tail(splice(...))`
10813            // so the array is still mutated in place.
10814            "splice_last" | "splice1" | "spl_last" => {
10815                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10816                    return Ok(e);
10817                }
10818                let args = self.parse_builtin_args()?;
10819                let mut iter = args.into_iter();
10820                let array = Box::new(
10821                    iter.next()
10822                        .ok_or_else(|| self.syntax_err("splice_last requires arguments", line))?,
10823                );
10824                let offset = iter.next().map(Box::new);
10825                let length = iter.next().map(Box::new);
10826                let replacement: Vec<Expr> = iter.collect();
10827                let splice_expr = Expr {
10828                    kind: ExprKind::Splice {
10829                        array,
10830                        offset,
10831                        length,
10832                        replacement,
10833                    },
10834                    line,
10835                };
10836                Ok(Expr {
10837                    kind: ExprKind::FuncCall {
10838                        name: "tail".to_string(),
10839                        args: vec![splice_expr],
10840                    },
10841                    line,
10842                })
10843            }
10844            "delete" => {
10845                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10846                    return Ok(e);
10847                }
10848                let a = self.parse_postfix()?;
10849                Ok(Expr {
10850                    kind: ExprKind::Delete(Box::new(a)),
10851                    line,
10852                })
10853            }
10854            "exists" => {
10855                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10856                    return Ok(e);
10857                }
10858                // `parse_postfix` starts at `parse_primary` which doesn't
10859                // accept the leading `&` of `&subname` — call `parse_unary`
10860                // instead so `exists &main::myf` parses the same as
10861                // `defined &main::myf` already does.
10862                let a = self.parse_unary()?;
10863                Ok(Expr {
10864                    kind: ExprKind::Exists(Box::new(a)),
10865                    line,
10866                })
10867            }
10868            "keys" => {
10869                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10870                    return Ok(e);
10871                }
10872                let a = self.parse_one_arg_or_default()?;
10873                Ok(Expr {
10874                    kind: ExprKind::Keys(Box::new(a)),
10875                    line,
10876                })
10877            }
10878            "values" => {
10879                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10880                    return Ok(e);
10881                }
10882                let a = self.parse_one_arg_or_default()?;
10883                Ok(Expr {
10884                    kind: ExprKind::Values(Box::new(a)),
10885                    line,
10886                })
10887            }
10888            "each" => {
10889                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
10890                    return Ok(e);
10891                }
10892                let a = self.parse_one_arg_or_default()?;
10893                Ok(Expr {
10894                    kind: ExprKind::Each(Box::new(a)),
10895                    line,
10896                })
10897            }
10898            "fore" | "e" | "ep" => {
10899                // `fore { BLOCK } LIST` / `ep` — forEach expression (pipe-forward friendly)
10900                if matches!(self.peek(), Token::LBrace) {
10901                    let (block, list) = self.parse_block_list()?;
10902                    Ok(Expr {
10903                        kind: ExprKind::ForEachExpr {
10904                            block,
10905                            list: Box::new(list),
10906                        },
10907                        line,
10908                    })
10909                } else if self.in_pipe_rhs() {
10910                    // `|> ep` — bare ep at end of pipe: default to `say $_`
10911                    // `|> fore say` / `|> e say` — blockless pipe form: wrap EXPR into a synthetic block
10912                    let is_terminal = matches!(
10913                        self.peek(),
10914                        Token::Semicolon
10915                            | Token::RParen
10916                            | Token::Eof
10917                            | Token::PipeForward
10918                            | Token::RBrace
10919                    );
10920                    let block = if name == "ep" && is_terminal {
10921                        vec![Statement {
10922                            label: None,
10923                            kind: StmtKind::Expression(Expr {
10924                                kind: ExprKind::Say {
10925                                    handle: None,
10926                                    args: vec![Expr {
10927                                        kind: ExprKind::ScalarVar("_".into()),
10928                                        line,
10929                                    }],
10930                                },
10931                                line,
10932                            }),
10933                            line,
10934                        }]
10935                    } else {
10936                        let expr = self.parse_assign_expr_stop_at_pipe()?;
10937                        let expr = Self::lift_bareword_to_topic_call(expr);
10938                        vec![Statement {
10939                            label: None,
10940                            kind: StmtKind::Expression(expr),
10941                            line,
10942                        }]
10943                    };
10944                    let list = self.pipe_placeholder_list(line);
10945                    Ok(Expr {
10946                        kind: ExprKind::ForEachExpr {
10947                            block,
10948                            list: Box::new(list),
10949                        },
10950                        line,
10951                    })
10952                } else {
10953                    // Two surface forms share this branch:
10954                    //   `fore EXPR, LIST` — comma form (explicit per-item EXPR + list)
10955                    //   `ep LIST`         — list-only form: print each item with `say $_`
10956                    // We disambiguate by peeking after the first parsed expression:
10957                    // if the next token is a comma we're in the EXPR-then-LIST form;
10958                    // otherwise the first parse *was* the LIST and we default the
10959                    // block to `say $_` (only for `ep` — `fore`/`e` keep their
10960                    // explicit-expression contract).
10961                    let expr = self.parse_assign_expr()?;
10962                    let expr = Self::lift_bareword_to_topic_call(expr);
10963                    if !matches!(self.peek(), Token::Comma) && name == "ep" {
10964                        let block = vec![Statement {
10965                            label: None,
10966                            kind: StmtKind::Expression(Expr {
10967                                kind: ExprKind::Say {
10968                                    handle: None,
10969                                    args: vec![Expr {
10970                                        kind: ExprKind::ScalarVar("_".into()),
10971                                        line,
10972                                    }],
10973                                },
10974                                line,
10975                            }),
10976                            line,
10977                        }];
10978                        return Ok(Expr {
10979                            kind: ExprKind::ForEachExpr {
10980                                block,
10981                                list: Box::new(expr),
10982                            },
10983                            line,
10984                        });
10985                    }
10986                    self.expect(&Token::Comma)?;
10987                    let list_parts = self.parse_list_until_terminator()?;
10988                    let list_expr = if list_parts.len() == 1 {
10989                        list_parts.into_iter().next().unwrap()
10990                    } else {
10991                        Expr {
10992                            kind: ExprKind::List(list_parts),
10993                            line,
10994                        }
10995                    };
10996                    let block = vec![Statement {
10997                        label: None,
10998                        kind: StmtKind::Expression(expr),
10999                        line,
11000                    }];
11001                    Ok(Expr {
11002                        kind: ExprKind::ForEachExpr {
11003                            block,
11004                            list: Box::new(list_expr),
11005                        },
11006                        line,
11007                    })
11008                }
11009            }
11010            "rev" => {
11011                // `rev` — context-aware reverse: string in scalar, list in list context.
11012                // List-operator precedence (so `rev 1..3` parses as `rev(1..3)`, not
11013                // `(rev 1)..3`). Defaults to $_ when no argument given.
11014                // Only use pipe placeholder when directly in pipe RHS (not inside a block).
11015                // RBrace means we're inside a block like `map { rev }` - use $_ default.
11016                let prev = self.prev_line();
11017                let a = if self.in_pipe_rhs()
11018                    && (matches!(
11019                        self.peek(),
11020                        Token::Semicolon | Token::RParen | Token::Eof | Token::PipeForward
11021                    ) || self.peek_line() > prev)
11022                {
11023                    self.pipe_placeholder_list(line)
11024                } else if self.peek_line() > prev {
11025                    // Newline boundary: argument is on a later line —
11026                    // default to `$_` so the next statement parses as
11027                    // its own thing instead of being slurped as the
11028                    // implicit operand. (Same rule as
11029                    // `parse_one_arg_or_default`.)
11030                    Expr {
11031                        kind: ExprKind::ScalarVar("_".into()),
11032                        line: prev,
11033                    }
11034                } else if matches!(
11035                    self.peek(),
11036                    Token::Semicolon
11037                        | Token::RBrace
11038                        | Token::RParen
11039                        | Token::RBracket
11040                        | Token::Eof
11041                        | Token::Comma
11042                        | Token::FatArrow
11043                        | Token::PipeForward
11044                ) {
11045                    Expr {
11046                        kind: ExprKind::ScalarVar("_".into()),
11047                        line: self.peek_line(),
11048                    }
11049                } else if matches!(self.peek(), Token::LParen)
11050                    && matches!(self.peek_at(1), Token::RParen)
11051                {
11052                    // `rev()` — empty parens default to `$_` (matches Perl's
11053                    // `length()` / `uc()` etc. and the `|> rev()` pipe form).
11054                    let pl = self.peek_line();
11055                    self.advance(); // (
11056                    self.advance(); // )
11057                    Expr {
11058                        kind: ExprKind::ScalarVar("_".into()),
11059                        line: pl,
11060                    }
11061                } else {
11062                    self.parse_one_arg()?
11063                };
11064                Ok(Expr {
11065                    kind: ExprKind::Rev(Box::new(a)),
11066                    line,
11067                })
11068            }
11069            "reverse" => {
11070                if crate::no_interop_mode() {
11071                    return Err(self.syntax_err(
11072                        "stryke uses `rev` instead of `reverse` (--no-interop)",
11073                        line,
11074                    ));
11075                }
11076                // On the RHS of `|>`, the operand is supplied by the piped LHS.
11077                let a = if self.in_pipe_rhs()
11078                    && matches!(
11079                        self.peek(),
11080                        Token::Semicolon
11081                            | Token::RBrace
11082                            | Token::RParen
11083                            | Token::Eof
11084                            | Token::PipeForward
11085                    ) {
11086                    self.pipe_placeholder_list(line)
11087                } else if matches!(self.peek(), Token::LParen)
11088                    && matches!(self.peek_at(1), Token::RParen)
11089                {
11090                    // `reverse()` — Perl-style empty list call returns the empty list.
11091                    self.advance();
11092                    self.advance();
11093                    Expr {
11094                        kind: ExprKind::List(Vec::new()),
11095                        line,
11096                    }
11097                } else {
11098                    self.parse_one_arg()?
11099                };
11100                Ok(Expr {
11101                    kind: ExprKind::ReverseExpr(Box::new(a)),
11102                    line,
11103                })
11104            }
11105            "reversed" | "rv" => {
11106                // On the RHS of `|>`, the operand is supplied by the piped LHS.
11107                let a = if self.in_pipe_rhs()
11108                    && matches!(
11109                        self.peek(),
11110                        Token::Semicolon
11111                            | Token::RBrace
11112                            | Token::RParen
11113                            | Token::Eof
11114                            | Token::PipeForward
11115                    ) {
11116                    self.pipe_placeholder_list(line)
11117                } else {
11118                    self.parse_one_arg()?
11119                };
11120                Ok(Expr {
11121                    kind: ExprKind::Rev(Box::new(a)),
11122                    line,
11123                })
11124            }
11125            "join" => {
11126                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11127                    return Ok(e);
11128                }
11129                let args = self.parse_builtin_args()?;
11130                if args.is_empty() {
11131                    return Err(self.syntax_err("join requires separator and list", line));
11132                }
11133                // `@list |> join(",")` — list slot is filled by the piped LHS.
11134                if args.len() < 2 && !self.in_pipe_rhs() {
11135                    return Err(self.syntax_err("join requires separator and list", line));
11136                }
11137                Ok(Expr {
11138                    kind: ExprKind::JoinExpr {
11139                        separator: Box::new(args[0].clone()),
11140                        list: Box::new(Expr {
11141                            kind: ExprKind::List(args[1..].to_vec()),
11142                            line,
11143                        }),
11144                    },
11145                    line,
11146                })
11147            }
11148            "split" => {
11149                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11150                    return Ok(e);
11151                }
11152                let args = self.parse_builtin_args()?;
11153                let pattern = args.first().cloned().unwrap_or(Expr {
11154                    kind: ExprKind::String(" ".into()),
11155                    line,
11156                });
11157                let string = args.get(1).cloned().unwrap_or(Expr {
11158                    kind: ExprKind::ScalarVar("_".into()),
11159                    line,
11160                });
11161                let limit = args.get(2).cloned().map(Box::new);
11162                Ok(Expr {
11163                    kind: ExprKind::SplitExpr {
11164                        pattern: Box::new(pattern),
11165                        string: Box::new(string),
11166                        limit,
11167                    },
11168                    line,
11169                })
11170            }
11171            "substr" => {
11172                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11173                    return Ok(e);
11174                }
11175                let args = self.parse_builtin_args()?;
11176                Ok(Expr {
11177                    kind: ExprKind::Substr {
11178                        string: Box::new(args[0].clone()),
11179                        offset: Box::new(args[1].clone()),
11180                        length: args.get(2).cloned().map(Box::new),
11181                        replacement: args.get(3).cloned().map(Box::new),
11182                    },
11183                    line,
11184                })
11185            }
11186            "index" => {
11187                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11188                    return Ok(e);
11189                }
11190                let args = self.parse_builtin_args()?;
11191                Ok(Expr {
11192                    kind: ExprKind::Index {
11193                        string: Box::new(args[0].clone()),
11194                        substr: Box::new(args[1].clone()),
11195                        position: args.get(2).cloned().map(Box::new),
11196                    },
11197                    line,
11198                })
11199            }
11200            "rindex" => {
11201                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11202                    return Ok(e);
11203                }
11204                let args = self.parse_builtin_args()?;
11205                Ok(Expr {
11206                    kind: ExprKind::Rindex {
11207                        string: Box::new(args[0].clone()),
11208                        substr: Box::new(args[1].clone()),
11209                        position: args.get(2).cloned().map(Box::new),
11210                    },
11211                    line,
11212                })
11213            }
11214            "sprintf" => {
11215                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
11216                    return Ok(e);
11217                }
11218                let args = self.parse_builtin_args()?;
11219                let (first, rest) = args
11220                    .split_first()
11221                    .ok_or_else(|| self.syntax_err("sprintf requires format", line))?;
11222                Ok(Expr {
11223                    kind: ExprKind::Sprintf {
11224                        format: Box::new(first.clone()),
11225                        args: rest.to_vec(),
11226                    },
11227                    line,
11228                })
11229            }
11230            "map" | "flat_map" | "maps" | "flat_maps" => {
11231                let flatten_array_refs = matches!(name.as_str(), "flat_map" | "flat_maps");
11232                let stream = matches!(name.as_str(), "maps" | "flat_maps");
11233                if matches!(self.peek(), Token::LBrace) {
11234                    let (block, list) = self.parse_block_list()?;
11235                    Ok(Expr {
11236                        kind: ExprKind::MapExpr {
11237                            block,
11238                            list: Box::new(list),
11239                            flatten_array_refs,
11240                            stream,
11241                        },
11242                        line,
11243                    })
11244                } else {
11245                    let expr = self.parse_assign_expr_stop_at_pipe()?;
11246                    // Lift bareword to FuncCall($_) so `map sha512, @list`
11247                    // calls sha512($_) for each element instead of stringifying.
11248                    let expr = Self::lift_bareword_to_topic_call(expr);
11249                    let list_expr = if self.pipe_supplies_slurped_list_operand() {
11250                        self.pipe_placeholder_list(line)
11251                    } else {
11252                        self.expect(&Token::Comma)?;
11253                        let list_parts = self.parse_list_until_terminator()?;
11254                        if list_parts.len() == 1 {
11255                            list_parts.into_iter().next().unwrap()
11256                        } else {
11257                            Expr {
11258                                kind: ExprKind::List(list_parts),
11259                                line,
11260                            }
11261                        }
11262                    };
11263                    Ok(Expr {
11264                        kind: ExprKind::MapExprComma {
11265                            expr: Box::new(expr),
11266                            list: Box::new(list_expr),
11267                            flatten_array_refs,
11268                            stream,
11269                        },
11270                        line,
11271                    })
11272                }
11273            }
11274            "cond" => {
11275                if crate::compat_mode() {
11276                    return Err(self
11277                        .syntax_err("`cond` is a stryke extension (disabled by --compat)", line));
11278                }
11279                self.parse_cond_expr(line)
11280            }
11281            "match" => {
11282                if crate::compat_mode() {
11283                    return Err(self.syntax_err(
11284                        "algebraic `match` is a stryke extension (disabled by --compat)",
11285                        line,
11286                    ));
11287                }
11288                self.parse_algebraic_match_expr(line)
11289            }
11290            "grep" | "greps" | "filter" | "fi" | "find_all" => {
11291                let keyword = match name.as_str() {
11292                    "grep" => crate::ast::GrepBuiltinKeyword::Grep,
11293                    "greps" => crate::ast::GrepBuiltinKeyword::Greps,
11294                    "filter" | "fi" => crate::ast::GrepBuiltinKeyword::Filter,
11295                    "find_all" => crate::ast::GrepBuiltinKeyword::FindAll,
11296                    _ => unreachable!(),
11297                };
11298                if matches!(self.peek(), Token::LBrace) {
11299                    let (block, list) = self.parse_block_list()?;
11300                    Ok(Expr {
11301                        kind: ExprKind::GrepExpr {
11302                            block,
11303                            list: Box::new(list),
11304                            keyword,
11305                        },
11306                        line,
11307                    })
11308                } else {
11309                    let expr = self.parse_assign_expr_stop_at_pipe()?;
11310                    if self.pipe_supplies_slurped_list_operand() {
11311                        // Pipe-RHS blockless form: `|> grep EXPR`
11312                        // For literals, desugar to `$_ eq/== EXPR` so
11313                        // `|> filter 't'` keeps only elements equal to 't'.
11314                        // For regexes, desugar to `$_ =~ EXPR`.
11315                        let list = self.pipe_placeholder_list(line);
11316                        let topic = Expr {
11317                            kind: ExprKind::ScalarVar("_".into()),
11318                            line,
11319                        };
11320                        let test = match &expr.kind {
11321                            ExprKind::Integer(_) | ExprKind::Float(_) => Expr {
11322                                kind: ExprKind::BinOp {
11323                                    op: BinOp::NumEq,
11324                                    left: Box::new(topic),
11325                                    right: Box::new(expr),
11326                                },
11327                                line,
11328                            },
11329                            ExprKind::String(_) | ExprKind::InterpolatedString(_) => Expr {
11330                                kind: ExprKind::BinOp {
11331                                    op: BinOp::StrEq,
11332                                    left: Box::new(topic),
11333                                    right: Box::new(expr),
11334                                },
11335                                line,
11336                            },
11337                            ExprKind::Regex { .. } => Expr {
11338                                kind: ExprKind::BinOp {
11339                                    op: BinOp::BindMatch,
11340                                    left: Box::new(topic),
11341                                    right: Box::new(expr),
11342                                },
11343                                line,
11344                            },
11345                            _ => {
11346                                // Non-literal (e.g. `defined`, scalar coderef var,
11347                                // hash slot): lift barewords to topic-call, then
11348                                // route through GrepExprComma so the runtime
11349                                // coderef-dispatch in Op::GrepWithExpr handles
11350                                // both truthiness AND coderef-call uniformly.
11351                                let expr = Self::lift_bareword_to_topic_call(expr);
11352                                return Ok(Expr {
11353                                    kind: ExprKind::GrepExprComma {
11354                                        expr: Box::new(expr),
11355                                        list: Box::new(list),
11356                                        keyword,
11357                                    },
11358                                    line,
11359                                });
11360                            }
11361                        };
11362                        let block = vec![Statement {
11363                            label: None,
11364                            kind: StmtKind::Expression(test),
11365                            line,
11366                        }];
11367                        Ok(Expr {
11368                            kind: ExprKind::GrepExpr {
11369                                block,
11370                                list: Box::new(list),
11371                                keyword,
11372                            },
11373                            line,
11374                        })
11375                    } else {
11376                        let expr = Self::lift_bareword_to_topic_call(expr);
11377                        self.expect(&Token::Comma)?;
11378                        let list_parts = self.parse_list_until_terminator()?;
11379                        let list_expr = if list_parts.len() == 1 {
11380                            list_parts.into_iter().next().unwrap()
11381                        } else {
11382                            Expr {
11383                                kind: ExprKind::List(list_parts),
11384                                line,
11385                            }
11386                        };
11387                        Ok(Expr {
11388                            kind: ExprKind::GrepExprComma {
11389                                expr: Box::new(expr),
11390                                list: Box::new(list_expr),
11391                                keyword,
11392                            },
11393                            line,
11394                        })
11395                    }
11396                }
11397            }
11398            "sort" => {
11399                use crate::ast::SortComparator;
11400                if matches!(self.peek(), Token::LBrace) {
11401                    let block = self.parse_block()?;
11402                    let block_end_line = self.prev_line();
11403                    let _ = self.eat(&Token::Comma);
11404                    let list = if self.in_pipe_rhs()
11405                        && (matches!(
11406                            self.peek(),
11407                            Token::Semicolon
11408                                | Token::RBrace
11409                                | Token::RParen
11410                                | Token::Eof
11411                                | Token::PipeForward
11412                        ) || self.peek_line() > block_end_line)
11413                    {
11414                        self.pipe_placeholder_list(line)
11415                    } else {
11416                        self.parse_expression()?
11417                    };
11418                    Ok(Expr {
11419                        kind: ExprKind::SortExpr {
11420                            cmp: Some(SortComparator::Block(block)),
11421                            list: Box::new(list),
11422                        },
11423                        line,
11424                    })
11425                } else if matches!(self.peek(), Token::ScalarVar(ref v) if v == "a" || v == "b") {
11426                    // Blockless comparator: `sort $a <=> $b, @list`
11427                    let block = self.parse_block_or_bareword_cmp_block()?;
11428                    let _ = self.eat(&Token::Comma);
11429                    let list = if self.in_pipe_rhs()
11430                        && matches!(
11431                            self.peek(),
11432                            Token::Semicolon
11433                                | Token::RBrace
11434                                | Token::RParen
11435                                | Token::Eof
11436                                | Token::PipeForward
11437                        ) {
11438                        self.pipe_placeholder_list(line)
11439                    } else {
11440                        self.parse_expression()?
11441                    };
11442                    Ok(Expr {
11443                        kind: ExprKind::SortExpr {
11444                            cmp: Some(SortComparator::Block(block)),
11445                            list: Box::new(list),
11446                        },
11447                        line,
11448                    })
11449                } else if matches!(self.peek(), Token::ScalarVar(_)) {
11450                    // `sort $coderef (LIST)` — comparator is first; list often parenthesized.
11451                    // Pipe-RHS form `|> sort $coderef` uses placeholder LHS as the list.
11452                    self.suppress_indirect_paren_call =
11453                        self.suppress_indirect_paren_call.saturating_add(1);
11454                    let code = self.parse_assign_expr()?;
11455                    self.suppress_indirect_paren_call =
11456                        self.suppress_indirect_paren_call.saturating_sub(1);
11457                    let _ = self.eat(&Token::Comma);
11458                    let list = if self.in_pipe_rhs()
11459                        && matches!(
11460                            self.peek(),
11461                            Token::Semicolon
11462                                | Token::RBrace
11463                                | Token::RParen
11464                                | Token::Eof
11465                                | Token::PipeForward
11466                        ) {
11467                        self.pipe_placeholder_list(line)
11468                    } else if matches!(self.peek(), Token::LParen) {
11469                        self.advance();
11470                        let e = self.parse_expression()?;
11471                        self.expect(&Token::RParen)?;
11472                        e
11473                    } else {
11474                        self.parse_expression()?
11475                    };
11476                    Ok(Expr {
11477                        kind: ExprKind::SortExpr {
11478                            cmp: Some(SortComparator::Code(Box::new(code))),
11479                            list: Box::new(list),
11480                        },
11481                        line,
11482                    })
11483                } else if matches!(self.peek(), Token::Ident(ref name) if !Self::is_known_bareword(name) && !is_post_style_decl(name) && name != "varsync")
11484                {
11485                    // Blockless comparator via bare sub name: `sort my_cmp @list`
11486                    // Reject `var`/`val`/`varsync` here even though they aren't
11487                    // in `is_known_bareword` (by design — see the comment in
11488                    // is_perl5_core). They are declarator keywords, never
11489                    // sub names; treating them as a comparator would (a)
11490                    // miss-parse `sort var @list` as `sort var(@list)`, and
11491                    // (b) greedy-consume the next statement on the new line
11492                    // — observed when `var @free_men = keys %$wp |> sort`
11493                    // was followed by `var %women_ranks`, the second var
11494                    // got swallowed as sort's comparator and then discarded
11495                    // by `pipe_forward_apply` (Bug 2).
11496                    let block = self.parse_block_or_bareword_cmp_block()?;
11497                    let _ = self.eat(&Token::Comma);
11498                    let list = if self.in_pipe_rhs()
11499                        && matches!(
11500                            self.peek(),
11501                            Token::Semicolon
11502                                | Token::RBrace
11503                                | Token::RParen
11504                                | Token::Eof
11505                                | Token::PipeForward
11506                        ) {
11507                        self.pipe_placeholder_list(line)
11508                    } else {
11509                        self.parse_expression()?
11510                    };
11511                    Ok(Expr {
11512                        kind: ExprKind::SortExpr {
11513                            cmp: Some(SortComparator::Block(block)),
11514                            list: Box::new(list),
11515                        },
11516                        line,
11517                    })
11518                } else {
11519                    // Bare `sort` with no comparator and no list: only allowed
11520                    // as the RHS of `|>`, where the list comes from the LHS.
11521                    // Treat a newline as an implicit pipeline terminator —
11522                    // `@a |> sort\nmy $x = ...` must NOT swallow the next
11523                    // `my` stmt as sort's argument list.
11524                    let list = if self.in_pipe_rhs()
11525                        && (matches!(
11526                            self.peek(),
11527                            Token::Semicolon
11528                                | Token::RBrace
11529                                | Token::RParen
11530                                | Token::Eof
11531                                | Token::PipeForward
11532                        ) || self.peek_line() > line)
11533                    {
11534                        self.pipe_placeholder_list(line)
11535                    } else {
11536                        self.parse_expression()?
11537                    };
11538                    Ok(Expr {
11539                        kind: ExprKind::SortExpr {
11540                            cmp: None,
11541                            list: Box::new(list),
11542                        },
11543                        line,
11544                    })
11545                }
11546            }
11547            "reduce" | "fold" | "inject" => {
11548                let (block, list) = self.parse_block_list()?;
11549                Ok(Expr {
11550                    kind: ExprKind::ReduceExpr {
11551                        block,
11552                        list: Box::new(list),
11553                    },
11554                    line,
11555                })
11556            }
11557            // Parallel extensions
11558            "pmap" => {
11559                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11560                Ok(Expr {
11561                    kind: ExprKind::PMapExpr {
11562                        block,
11563                        list: Box::new(list),
11564                        progress: progress.map(Box::new),
11565                        flat_outputs: false,
11566                        on_cluster: None,
11567                        stream: false,
11568                    },
11569                    line,
11570                })
11571            }
11572            "pmap_on" => {
11573                let (cluster, block, list, progress) =
11574                    self.parse_cluster_block_then_list_optional_progress()?;
11575                Ok(Expr {
11576                    kind: ExprKind::PMapExpr {
11577                        block,
11578                        list: Box::new(list),
11579                        progress: progress.map(Box::new),
11580                        flat_outputs: false,
11581                        on_cluster: Some(Box::new(cluster)),
11582                        stream: false,
11583                    },
11584                    line,
11585                })
11586            }
11587            "pflat_map" => {
11588                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11589                Ok(Expr {
11590                    kind: ExprKind::PMapExpr {
11591                        block,
11592                        list: Box::new(list),
11593                        progress: progress.map(Box::new),
11594                        flat_outputs: true,
11595                        on_cluster: None,
11596                        stream: false,
11597                    },
11598                    line,
11599                })
11600            }
11601            "pflat_map_on" => {
11602                let (cluster, block, list, progress) =
11603                    self.parse_cluster_block_then_list_optional_progress()?;
11604                Ok(Expr {
11605                    kind: ExprKind::PMapExpr {
11606                        block,
11607                        list: Box::new(list),
11608                        progress: progress.map(Box::new),
11609                        flat_outputs: true,
11610                        on_cluster: Some(Box::new(cluster)),
11611                        stream: false,
11612                    },
11613                    line,
11614                })
11615            }
11616            "pmaps" => {
11617                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11618                Ok(Expr {
11619                    kind: ExprKind::PMapExpr {
11620                        block,
11621                        list: Box::new(list),
11622                        progress: progress.map(Box::new),
11623                        flat_outputs: false,
11624                        on_cluster: None,
11625                        stream: true,
11626                    },
11627                    line,
11628                })
11629            }
11630            "pflat_maps" => {
11631                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11632                Ok(Expr {
11633                    kind: ExprKind::PMapExpr {
11634                        block,
11635                        list: Box::new(list),
11636                        progress: progress.map(Box::new),
11637                        flat_outputs: true,
11638                        on_cluster: None,
11639                        stream: true,
11640                    },
11641                    line,
11642                })
11643            }
11644            "pgreps" => {
11645                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11646                Ok(Expr {
11647                    kind: ExprKind::PGrepExpr {
11648                        block,
11649                        list: Box::new(list),
11650                        progress: progress.map(Box::new),
11651                        stream: true,
11652                    },
11653                    line,
11654                })
11655            }
11656            "pmap_chunked" => {
11657                let chunk_size = self.parse_assign_expr()?;
11658                let block = self.parse_block_or_bareword_block()?;
11659                self.eat(&Token::Comma);
11660                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
11661                Ok(Expr {
11662                    kind: ExprKind::PMapChunkedExpr {
11663                        chunk_size: Box::new(chunk_size),
11664                        block,
11665                        list: Box::new(list),
11666                        progress: progress.map(Box::new),
11667                    },
11668                    line,
11669                })
11670            }
11671            "pgrep" => {
11672                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11673                Ok(Expr {
11674                    kind: ExprKind::PGrepExpr {
11675                        block,
11676                        list: Box::new(list),
11677                        progress: progress.map(Box::new),
11678                        stream: false,
11679                    },
11680                    line,
11681                })
11682            }
11683            "pfor" | "pforeach" => {
11684                if matches!(self.peek(), Token::LParen) {
11685                    self.expect(&Token::LParen)?;
11686                    let list = self.parse_expression()?;
11687                    self.expect(&Token::RParen)?;
11688                    let block = self.parse_block()?;
11689                    Ok(Expr {
11690                        kind: ExprKind::PForExpr {
11691                            block,
11692                            list: Box::new(list),
11693                            progress: None,
11694                        },
11695                        line,
11696                    })
11697                } else {
11698                    let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
11699                    Ok(Expr {
11700                        kind: ExprKind::PForExpr {
11701                            block,
11702                            list: Box::new(list),
11703                            progress: progress.map(Box::new),
11704                        },
11705                        line,
11706                    })
11707                }
11708            }
11709            // `par { BLOCK } LIST` — generic parallel-chunk evaluator.
11710            // Splits LIST into chunks (UTF-8-aligned for strings,
11711            // element-aligned for arrays), runs BLOCK on each chunk in
11712            // parallel with `_` bound to the chunk, flattens results.
11713            // Available as a top-level expression, not just an `~>` stage.
11714            "par" => {
11715                let (block, list, _progress) = self.parse_block_then_list_optional_progress()?;
11716                Ok(Expr {
11717                    kind: ExprKind::ParExpr {
11718                        block,
11719                        list: Box::new(list),
11720                    },
11721                    line,
11722                })
11723            }
11724            "par_lines" | "par_walk" => {
11725                let args = self.parse_builtin_args()?;
11726                if args.len() < 2 {
11727                    return Err(
11728                        self.syntax_err(format!("{} requires at least two arguments", name), line)
11729                    );
11730                }
11731
11732                if name == "par_lines" {
11733                    Ok(Expr {
11734                        kind: ExprKind::ParLinesExpr {
11735                            path: Box::new(args[0].clone()),
11736                            callback: Box::new(args[1].clone()),
11737                            progress: None,
11738                        },
11739                        line,
11740                    })
11741                } else {
11742                    Ok(Expr {
11743                        kind: ExprKind::ParWalkExpr {
11744                            path: Box::new(args[0].clone()),
11745                            callback: Box::new(args[1].clone()),
11746                            progress: None,
11747                        },
11748                        line,
11749                    })
11750                }
11751            }
11752            "pwatch" | "watch" => {
11753                let args = self.parse_builtin_args()?;
11754                if args.len() < 2 {
11755                    return Err(
11756                        self.syntax_err(format!("{} requires at least two arguments", name), line)
11757                    );
11758                }
11759                Ok(Expr {
11760                    kind: ExprKind::PwatchExpr {
11761                        path: Box::new(args[0].clone()),
11762                        callback: Box::new(args[1].clone()),
11763                    },
11764                    line,
11765                })
11766            }
11767            "fan" => {
11768                // fan { BLOCK }            — no count, block body
11769                // fan COUNT { BLOCK }      — count + block body
11770                // fan EXPR;                — no count, blockless body (wrap EXPR as block)
11771                // fan COUNT EXPR;          — count + blockless body
11772                // Optional: `, progress => EXPR` or `progress => EXPR` (no comma before progress)
11773                let (count, block) = self.parse_fan_count_and_block(line)?;
11774                let progress = self.parse_fan_optional_progress("fan")?;
11775                Ok(Expr {
11776                    kind: ExprKind::FanExpr {
11777                        count,
11778                        block,
11779                        progress,
11780                        capture: false,
11781                    },
11782                    line,
11783                })
11784            }
11785            "fan_cap" => {
11786                let (count, block) = self.parse_fan_count_and_block(line)?;
11787                let progress = self.parse_fan_optional_progress("fan_cap")?;
11788                Ok(Expr {
11789                    kind: ExprKind::FanExpr {
11790                        count,
11791                        block,
11792                        progress,
11793                        capture: true,
11794                    },
11795                    line,
11796                })
11797            }
11798            "async" => {
11799                if !matches!(self.peek(), Token::LBrace) {
11800                    return Err(self.syntax_err("async must be followed by { BLOCK }", line));
11801                }
11802                let block = self.parse_block()?;
11803                Ok(Expr {
11804                    kind: ExprKind::AsyncBlock { body: block },
11805                    line,
11806                })
11807            }
11808            "spawn" => {
11809                if !matches!(self.peek(), Token::LBrace) {
11810                    return Err(self.syntax_err("spawn must be followed by { BLOCK }", line));
11811                }
11812                let block = self.parse_block()?;
11813                Ok(Expr {
11814                    kind: ExprKind::SpawnBlock { body: block },
11815                    line,
11816                })
11817            }
11818            "trace" => {
11819                if !matches!(self.peek(), Token::LBrace) {
11820                    return Err(self.syntax_err("trace must be followed by { BLOCK }", line));
11821                }
11822                let block = self.parse_block()?;
11823                Ok(Expr {
11824                    kind: ExprKind::Trace { body: block },
11825                    line,
11826                })
11827            }
11828            "timer" => {
11829                let block = self.parse_block_or_bareword_block_no_args()?;
11830                Ok(Expr {
11831                    kind: ExprKind::Timer { body: block },
11832                    line,
11833                })
11834            }
11835            "bench" => {
11836                let block = self.parse_block_or_bareword_block_no_args()?;
11837                let times = Box::new(self.parse_expression()?);
11838                Ok(Expr {
11839                    kind: ExprKind::Bench { body: block, times },
11840                    line,
11841                })
11842            }
11843            "spinner" => {
11844                // `spinner "msg" { BLOCK }` or `spinner { BLOCK }`
11845                let (message, body) = if matches!(self.peek(), Token::LBrace) {
11846                    let body = self.parse_block()?;
11847                    (
11848                        Box::new(Expr {
11849                            kind: ExprKind::String("working".to_string()),
11850                            line,
11851                        }),
11852                        body,
11853                    )
11854                } else {
11855                    let msg = self.parse_assign_expr()?;
11856                    let body = self.parse_block()?;
11857                    (Box::new(msg), body)
11858                };
11859                Ok(Expr {
11860                    kind: ExprKind::Spinner { message, body },
11861                    line,
11862                })
11863            }
11864            "thread" | "t" => {
11865                // `thread EXPR stage1 stage2 ...` — threading macro (thread-first)
11866                // `t` is a short alias for `thread`
11867                // Each stage is either:
11868                //   - `ident` — bare function call
11869                //   - `ident { block }` — function with block arg
11870                //   - `ident arg1 arg2 { block }` — function with args and optional block
11871                //   - `fn { block }` — standalone anonymous block
11872                //   - `>{ block }` — shorthand for standalone anonymous block
11873                // Desugars to: EXPR |> stage1 |> stage2 |> ...
11874                self.parse_thread_macro(line, false)
11875            }
11876            "retry" => {
11877                // `retry { BLOCK }` or `retry BAREWORD` — bareword becomes zero-arg call.
11878                // An optional comma before `times` is allowed in both forms.
11879                let body = if matches!(self.peek(), Token::LBrace) {
11880                    self.parse_block()?
11881                } else {
11882                    let bw_line = self.peek_line();
11883                    let Token::Ident(ref name) = self.peek().clone() else {
11884                        return Err(self
11885                            .syntax_err("retry: expected block or bareword function name", line));
11886                    };
11887                    let name = name.clone();
11888                    self.advance();
11889                    vec![Statement::new(
11890                        StmtKind::Expression(Expr {
11891                            kind: ExprKind::FuncCall { name, args: vec![] },
11892                            line: bw_line,
11893                        }),
11894                        bw_line,
11895                    )]
11896                };
11897                self.eat(&Token::Comma);
11898                match self.peek() {
11899                    Token::Ident(ref s) if s == "times" => {
11900                        self.advance();
11901                    }
11902                    _ => {
11903                        return Err(self.syntax_err("retry: expected `times =>` after block", line));
11904                    }
11905                }
11906                self.expect(&Token::FatArrow)?;
11907                let times = Box::new(self.parse_assign_expr()?);
11908                let mut backoff = RetryBackoff::None;
11909                if self.eat(&Token::Comma) {
11910                    match self.peek() {
11911                        Token::Ident(ref s) if s == "backoff" => {
11912                            self.advance();
11913                        }
11914                        _ => {
11915                            return Err(
11916                                self.syntax_err("retry: expected `backoff =>` after comma", line)
11917                            );
11918                        }
11919                    }
11920                    self.expect(&Token::FatArrow)?;
11921                    let Token::Ident(mode) = self.peek().clone() else {
11922                        return Err(self.syntax_err(
11923                            "retry: expected backoff mode (none, linear, exponential)",
11924                            line,
11925                        ));
11926                    };
11927                    backoff = match mode.as_str() {
11928                        "none" => RetryBackoff::None,
11929                        "linear" => RetryBackoff::Linear,
11930                        "exponential" => RetryBackoff::Exponential,
11931                        _ => {
11932                            return Err(
11933                                self.syntax_err(format!("retry: invalid backoff `{mode}`"), line)
11934                            );
11935                        }
11936                    };
11937                    self.advance();
11938                }
11939                Ok(Expr {
11940                    kind: ExprKind::RetryBlock {
11941                        body,
11942                        times,
11943                        backoff,
11944                    },
11945                    line,
11946                })
11947            }
11948            "rate_limit" => {
11949                self.expect(&Token::LParen)?;
11950                let max = Box::new(self.parse_assign_expr()?);
11951                self.expect(&Token::Comma)?;
11952                let window = Box::new(self.parse_assign_expr()?);
11953                self.expect(&Token::RParen)?;
11954                let body = self.parse_block_or_bareword_block_no_args()?;
11955                let slot = self.alloc_rate_limit_slot();
11956                Ok(Expr {
11957                    kind: ExprKind::RateLimitBlock {
11958                        slot,
11959                        max,
11960                        window,
11961                        body,
11962                    },
11963                    line,
11964                })
11965            }
11966            "every" => {
11967                // `every("500ms") { BLOCK }` or `every "500ms" BODY` — parens optional.
11968                // Body consumes `|>` (every is an infinite loop, not a pipeable source).
11969                let has_paren = self.eat(&Token::LParen);
11970                let interval = Box::new(self.parse_assign_expr()?);
11971                if has_paren {
11972                    self.expect(&Token::RParen)?;
11973                }
11974                let body = if matches!(self.peek(), Token::LBrace) {
11975                    self.parse_block()?
11976                } else {
11977                    let bline = self.peek_line();
11978                    let expr = self.parse_assign_expr()?;
11979                    vec![Statement::new(StmtKind::Expression(expr), bline)]
11980                };
11981                Ok(Expr {
11982                    kind: ExprKind::EveryBlock { interval, body },
11983                    line,
11984                })
11985            }
11986            "gen" => {
11987                if !matches!(self.peek(), Token::LBrace) {
11988                    return Err(self.syntax_err("gen must be followed by { BLOCK }", line));
11989                }
11990                let body = self.parse_block()?;
11991                Ok(Expr {
11992                    kind: ExprKind::GenBlock { body },
11993                    line,
11994                })
11995            }
11996            "yield" => {
11997                let e = self.parse_assign_expr()?;
11998                Ok(Expr {
11999                    kind: ExprKind::Yield(Box::new(e)),
12000                    line,
12001                })
12002            }
12003            "await" => {
12004                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12005                    return Ok(e);
12006                }
12007                // `await` defaults to `$_` so `map { await } @tasks` works
12008                // (Perl-style topic-defaulting unary).
12009                let a = self.parse_one_arg_or_default()?;
12010                Ok(Expr {
12011                    kind: ExprKind::Await(Box::new(a)),
12012                    line,
12013                })
12014            }
12015            "slurp" | "cat" | "c" => {
12016                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12017                    return Ok(e);
12018                }
12019                let a = self.parse_one_arg_or_default()?;
12020                Ok(Expr {
12021                    kind: ExprKind::Slurp(Box::new(a)),
12022                    line,
12023                })
12024            }
12025            "swallow" | "swa" => {
12026                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12027                    return Ok(e);
12028                }
12029                let a = self.parse_one_arg_or_default()?;
12030                Ok(Expr {
12031                    kind: ExprKind::Swallow(Box::new(a)),
12032                    line,
12033                })
12034            }
12035            "ingest" | "ing" => {
12036                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12037                    return Ok(e);
12038                }
12039                let a = self.parse_one_arg_or_default()?;
12040                Ok(Expr {
12041                    kind: ExprKind::Ingest(Box::new(a)),
12042                    line,
12043                })
12044            }
12045            "burp" => {
12046                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12047                    return Ok(e);
12048                }
12049                let a = self.parse_one_arg_or_default()?;
12050                Ok(Expr {
12051                    kind: ExprKind::Burp(Box::new(a)),
12052                    line,
12053                })
12054            }
12055            "god" => {
12056                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12057                    return Ok(e);
12058                }
12059                let a = self.parse_one_arg_or_default()?;
12060                Ok(Expr {
12061                    kind: ExprKind::God(Box::new(a)),
12062                    line,
12063                })
12064            }
12065            "capture" => {
12066                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12067                    return Ok(e);
12068                }
12069                let a = self.parse_one_arg()?;
12070                Ok(Expr {
12071                    kind: ExprKind::Capture(Box::new(a)),
12072                    line,
12073                })
12074            }
12075            "fetch_url" => {
12076                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12077                    return Ok(e);
12078                }
12079                let a = self.parse_one_arg()?;
12080                Ok(Expr {
12081                    kind: ExprKind::FetchUrl(Box::new(a)),
12082                    line,
12083                })
12084            }
12085            "pchannel" => {
12086                let capacity = if self.eat(&Token::LParen) {
12087                    if matches!(self.peek(), Token::RParen) {
12088                        self.advance();
12089                        None
12090                    } else {
12091                        let e = self.parse_expression()?;
12092                        self.expect(&Token::RParen)?;
12093                        Some(Box::new(e))
12094                    }
12095                } else {
12096                    None
12097                };
12098                Ok(Expr {
12099                    kind: ExprKind::Pchannel { capacity },
12100                    line,
12101                })
12102            }
12103            "psort" => {
12104                if matches!(self.peek(), Token::LBrace)
12105                    || matches!(self.peek(), Token::ScalarVar(ref v) if v == "a" || v == "b")
12106                    || matches!(self.peek(), Token::Ident(ref name) if !Self::is_known_bareword(name))
12107                {
12108                    let block = self.parse_block_or_bareword_cmp_block()?;
12109                    // Mirror `sort`'s pipe-RHS handling — after the block,
12110                    // a newline (or any standard terminator token) inside a
12111                    // `|> psort { ... }` chain means the list comes from the
12112                    // pipe LHS, not from continued parsing into the next
12113                    // statement. Without this check `(@list) |> psort {
12114                    // _0 <=> _1 }\nmy $n = ...` silently swallowed `my $n =
12115                    // ...` as the list operand.
12116                    let block_end_line = self.prev_line();
12117                    self.eat(&Token::Comma);
12118                    let use_placeholder = self.in_pipe_rhs()
12119                        && (matches!(
12120                            self.peek(),
12121                            Token::Semicolon
12122                                | Token::RBrace
12123                                | Token::RParen
12124                                | Token::Eof
12125                                | Token::PipeForward
12126                        ) || self.peek_line() > block_end_line);
12127                    let (list, progress) = if use_placeholder {
12128                        (self.pipe_placeholder_list(line), None)
12129                    } else {
12130                        self.parse_assign_expr_list_optional_progress()?
12131                    };
12132                    Ok(Expr {
12133                        kind: ExprKind::PSortExpr {
12134                            cmp: Some(block),
12135                            list: Box::new(list),
12136                            progress: progress.map(Box::new),
12137                        },
12138                        line,
12139                    })
12140                } else {
12141                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
12142                    Ok(Expr {
12143                        kind: ExprKind::PSortExpr {
12144                            cmp: None,
12145                            list: Box::new(list),
12146                            progress: progress.map(Box::new),
12147                        },
12148                        line,
12149                    })
12150                }
12151            }
12152            "preduce" => {
12153                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
12154                Ok(Expr {
12155                    kind: ExprKind::PReduceExpr {
12156                        block,
12157                        list: Box::new(list),
12158                        progress: progress.map(Box::new),
12159                    },
12160                    line,
12161                })
12162            }
12163            "preduce_init" => {
12164                let (init, block, list, progress) =
12165                    self.parse_init_block_then_list_optional_progress()?;
12166                Ok(Expr {
12167                    kind: ExprKind::PReduceInitExpr {
12168                        init: Box::new(init),
12169                        block,
12170                        list: Box::new(list),
12171                        progress: progress.map(Box::new),
12172                    },
12173                    line,
12174                })
12175            }
12176            "pmap_reduce" => {
12177                let map_block = self.parse_block_or_bareword_block()?;
12178                // After the map block, expect either a `{ REDUCE }` block, or
12179                // after an eaten comma, a blockless reduce expr (`$a + $b`).
12180                let reduce_block = if matches!(self.peek(), Token::LBrace) {
12181                    self.parse_block()?
12182                } else {
12183                    // comma separates blockless map from blockless reduce
12184                    self.expect(&Token::Comma)?;
12185                    self.parse_block_or_bareword_cmp_block()?
12186                };
12187                self.eat(&Token::Comma);
12188                let line = self.peek_line();
12189                if let Token::Ident(ref kw) = self.peek().clone() {
12190                    if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
12191                        self.advance();
12192                        self.expect(&Token::FatArrow)?;
12193                        let prog = self.parse_assign_expr()?;
12194                        return Ok(Expr {
12195                            kind: ExprKind::PMapReduceExpr {
12196                                map_block,
12197                                reduce_block,
12198                                list: Box::new(Expr {
12199                                    kind: ExprKind::List(vec![]),
12200                                    line,
12201                                }),
12202                                progress: Some(Box::new(prog)),
12203                            },
12204                            line,
12205                        });
12206                    }
12207                }
12208                // BUG-301 fix: in pipe-RHS context (after `LHS |> pmap_reduce { } { }`),
12209                // the list operand comes from the pipe placeholder — there is no inline
12210                // list. Without this guard, `parse_assign_expr` below would greedily
12211                // consume the next statement (e.g. `p "after"` or `my $x = …`) as the
12212                // list, silently absorbing it into the pmap_reduce call. Mirror the
12213                // terminator check above to also bail when the next token signals "no
12214                // list here" in pipe-RHS context.
12215                if self.pipe_supplies_slurped_list_operand()
12216                    || matches!(
12217                        self.peek(),
12218                        Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
12219                    )
12220                {
12221                    return Ok(Expr {
12222                        kind: ExprKind::PMapReduceExpr {
12223                            map_block,
12224                            reduce_block,
12225                            list: Box::new(Expr {
12226                                kind: ExprKind::List(vec![]),
12227                                line,
12228                            }),
12229                            progress: None,
12230                        },
12231                        line,
12232                    });
12233                }
12234                let mut parts = vec![self.parse_assign_expr()?];
12235                loop {
12236                    if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
12237                        break;
12238                    }
12239                    if matches!(
12240                        self.peek(),
12241                        Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
12242                    ) {
12243                        break;
12244                    }
12245                    if let Token::Ident(ref kw) = self.peek().clone() {
12246                        if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
12247                            self.advance();
12248                            self.expect(&Token::FatArrow)?;
12249                            let prog = self.parse_assign_expr()?;
12250                            return Ok(Expr {
12251                                kind: ExprKind::PMapReduceExpr {
12252                                    map_block,
12253                                    reduce_block,
12254                                    list: Box::new(merge_expr_list(parts)),
12255                                    progress: Some(Box::new(prog)),
12256                                },
12257                                line,
12258                            });
12259                        }
12260                    }
12261                    parts.push(self.parse_assign_expr()?);
12262                }
12263                Ok(Expr {
12264                    kind: ExprKind::PMapReduceExpr {
12265                        map_block,
12266                        reduce_block,
12267                        list: Box::new(merge_expr_list(parts)),
12268                        progress: None,
12269                    },
12270                    line,
12271                })
12272            }
12273            "puniq" => {
12274                if self.pipe_supplies_slurped_list_operand() {
12275                    return Ok(Expr {
12276                        kind: ExprKind::FuncCall {
12277                            name: "puniq".to_string(),
12278                            args: vec![],
12279                        },
12280                        line,
12281                    });
12282                }
12283                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
12284                let mut args = vec![list];
12285                if let Some(p) = progress {
12286                    args.push(p);
12287                }
12288                Ok(Expr {
12289                    kind: ExprKind::FuncCall {
12290                        name: "puniq".to_string(),
12291                        args,
12292                    },
12293                    line,
12294                })
12295            }
12296            "pfirst" => {
12297                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
12298                let cr = Expr {
12299                    kind: ExprKind::CodeRef {
12300                        params: vec![],
12301                        body: block,
12302                    },
12303                    line,
12304                };
12305                let mut args = vec![cr, list];
12306                if let Some(p) = progress {
12307                    args.push(p);
12308                }
12309                Ok(Expr {
12310                    kind: ExprKind::FuncCall {
12311                        name: "pfirst".to_string(),
12312                        args,
12313                    },
12314                    line,
12315                })
12316            }
12317            "pany" => {
12318                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
12319                let cr = Expr {
12320                    kind: ExprKind::CodeRef {
12321                        params: vec![],
12322                        body: block,
12323                    },
12324                    line,
12325                };
12326                let mut args = vec![cr, list];
12327                if let Some(p) = progress {
12328                    args.push(p);
12329                }
12330                Ok(Expr {
12331                    kind: ExprKind::FuncCall {
12332                        name: "pany".to_string(),
12333                        args,
12334                    },
12335                    line,
12336                })
12337            }
12338            "uniq" | "distinct" => {
12339                if self.pipe_supplies_slurped_list_operand() {
12340                    return Ok(Expr {
12341                        kind: ExprKind::FuncCall {
12342                            name: name.clone(),
12343                            args: vec![],
12344                        },
12345                        line,
12346                    });
12347                }
12348                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
12349                if progress.is_some() {
12350                    return Err(self.syntax_err(
12351                        "`progress =>` is not supported for uniq (use puniq for parallel + progress)",
12352                        line,
12353                    ));
12354                }
12355                Ok(Expr {
12356                    kind: ExprKind::FuncCall {
12357                        name: name.clone(),
12358                        args: vec![list],
12359                    },
12360                    line,
12361                })
12362            }
12363            "flatten" => {
12364                if self.pipe_supplies_slurped_list_operand() {
12365                    return Ok(Expr {
12366                        kind: ExprKind::FuncCall {
12367                            name: "flatten".to_string(),
12368                            args: vec![],
12369                        },
12370                        line,
12371                    });
12372                }
12373                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
12374                if progress.is_some() {
12375                    return Err(self.syntax_err("`progress =>` is not supported for flatten", line));
12376                }
12377                Ok(Expr {
12378                    kind: ExprKind::FuncCall {
12379                        name: "flatten".to_string(),
12380                        args: vec![list],
12381                    },
12382                    line,
12383                })
12384            }
12385            "set" => {
12386                if self.pipe_supplies_slurped_list_operand() {
12387                    return Ok(Expr {
12388                        kind: ExprKind::FuncCall {
12389                            name: "set".to_string(),
12390                            args: vec![],
12391                        },
12392                        line,
12393                    });
12394                }
12395                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
12396                if progress.is_some() {
12397                    return Err(self.syntax_err("`progress =>` is not supported for set", line));
12398                }
12399                Ok(Expr {
12400                    kind: ExprKind::FuncCall {
12401                        name: "set".to_string(),
12402                        args: vec![list],
12403                    },
12404                    line,
12405                })
12406            }
12407            // `size` is the file-size builtin (Perl `-s`), not a list-count alias.
12408            // Defaults to `$_` when no arg is given, like `length`. See
12409            // `builtin_file_size` in builtins.rs for the runtime behavior.
12410            "size" => {
12411                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12412                    return Ok(e);
12413                }
12414                if self.pipe_supplies_slurped_list_operand() {
12415                    return Ok(Expr {
12416                        kind: ExprKind::FuncCall {
12417                            name: "size".to_string(),
12418                            args: vec![],
12419                        },
12420                        line,
12421                    });
12422                }
12423                let a = self.parse_one_arg_or_default()?;
12424                Ok(Expr {
12425                    kind: ExprKind::FuncCall {
12426                        name: "size".to_string(),
12427                        args: vec![a],
12428                    },
12429                    line,
12430                })
12431            }
12432            "list_count" | "list_size" | "count" | "len" | "cnt" => {
12433                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12434                    return Ok(e);
12435                }
12436                if self.pipe_supplies_slurped_list_operand() {
12437                    return Ok(Expr {
12438                        kind: ExprKind::FuncCall {
12439                            name: name.clone(),
12440                            args: vec![],
12441                        },
12442                        line,
12443                    });
12444                }
12445                // `len(EXPR)` / `cnt(EXPR)` / `count(EXPR)` with a tight `(` —
12446                // the parens are function-call syntax, not a parenthesized
12447                // list: stop the argument at `)` so `len(@a) % 2 == 1` is
12448                // `(len(@a)) % 2 == 1`, not `len(@a % 2 == 1)`. Empty parens
12449                // `len()` collapse to a zero-arg call (use the piped operand
12450                // or `$_`). Bare `len` followed by a low-precedence operator
12451                // (`==`, `&&`, `?`, …) also defaults to a zero-arg call so
12452                // `{ len == 0 }` works as a block predicate on the topic.
12453                // Bare `len EXPR` (no parens, e.g. `len @arr`) goes through
12454                // the greedy list-arg parser; this means `len @a + len @b`
12455                // is `len(@a + len(@b))` (returning the length of the sum
12456                // string), not `(len @a) + (len @b)`. Use explicit parens
12457                // when combining `len` with `+`, `-`, comparisons, etc.
12458                let args = if matches!(self.peek(), Token::LParen) {
12459                    self.advance();
12460                    if matches!(self.peek(), Token::RParen) {
12461                        self.advance();
12462                        Vec::new()
12463                    } else {
12464                        let inner = self.parse_expression()?;
12465                        self.expect(&Token::RParen)?;
12466                        vec![inner]
12467                    }
12468                } else if self.peek_is_named_unary_terminator() {
12469                    Vec::new()
12470                } else {
12471                    let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
12472                    if progress.is_some() {
12473                        return Err(self.syntax_err(
12474                            "`progress =>` is not supported for list_count / list_size / count / cnt",
12475                            line,
12476                        ));
12477                    }
12478                    vec![list]
12479                };
12480                Ok(Expr {
12481                    kind: ExprKind::FuncCall {
12482                        name: name.clone(),
12483                        args,
12484                    },
12485                    line,
12486                })
12487            }
12488            "shuffle" | "shuffled" => {
12489                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12490                    return Ok(e);
12491                }
12492                if self.pipe_supplies_slurped_list_operand() {
12493                    return Ok(Expr {
12494                        kind: ExprKind::FuncCall {
12495                            name: "shuffle".to_string(),
12496                            args: vec![],
12497                        },
12498                        line,
12499                    });
12500                }
12501                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
12502                if progress.is_some() {
12503                    return Err(self.syntax_err("`progress =>` is not supported for shuffle", line));
12504                }
12505                Ok(Expr {
12506                    kind: ExprKind::FuncCall {
12507                        name: "shuffle".to_string(),
12508                        args: vec![list],
12509                    },
12510                    line,
12511                })
12512            }
12513            "chunked" => {
12514                let mut parts = Vec::new();
12515                if self.eat(&Token::LParen) {
12516                    if !matches!(self.peek(), Token::RParen) {
12517                        parts.push(self.parse_assign_expr()?);
12518                        while self.eat(&Token::Comma) {
12519                            if matches!(self.peek(), Token::RParen) {
12520                                break;
12521                            }
12522                            parts.push(self.parse_assign_expr()?);
12523                        }
12524                    }
12525                    self.expect(&Token::RParen)?;
12526                } else {
12527                    // Paren-less `chunked N`: `|>` is a hard terminator, not
12528                    // an operator inside the arg (see
12529                    // `parse_assign_expr_stop_at_pipe`).
12530                    parts.push(self.parse_assign_expr_stop_at_pipe()?);
12531                    loop {
12532                        if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
12533                            break;
12534                        }
12535                        if matches!(
12536                            self.peek(),
12537                            Token::Semicolon
12538                                | Token::RBrace
12539                                | Token::RParen
12540                                | Token::Eof
12541                                | Token::PipeForward
12542                        ) {
12543                            break;
12544                        }
12545                        if self.peek_is_postfix_stmt_modifier_keyword() {
12546                            break;
12547                        }
12548                        parts.push(self.parse_assign_expr_stop_at_pipe()?);
12549                    }
12550                }
12551                if parts.len() == 1 {
12552                    let n = parts.pop().unwrap();
12553                    return Ok(Expr {
12554                        kind: ExprKind::FuncCall {
12555                            name: "chunked".to_string(),
12556                            args: vec![n],
12557                        },
12558                        line,
12559                    });
12560                }
12561                if parts.is_empty() {
12562                    return Ok(Expr {
12563                        kind: ExprKind::FuncCall {
12564                            name: "chunked".to_string(),
12565                            args: parts,
12566                        },
12567                        line,
12568                    });
12569                }
12570                if parts.len() == 2 {
12571                    let n = parts.pop().unwrap();
12572                    let list = parts.pop().unwrap();
12573                    return Ok(Expr {
12574                        kind: ExprKind::FuncCall {
12575                            name: "chunked".to_string(),
12576                            args: vec![list, n],
12577                        },
12578                        line,
12579                    });
12580                }
12581                Err(self.syntax_err(
12582                    "chunked: use LIST |> chunked(N) or chunked((1,2,3), 2)",
12583                    line,
12584                ))
12585            }
12586            "windowed" => {
12587                let mut parts = Vec::new();
12588                if self.eat(&Token::LParen) {
12589                    if !matches!(self.peek(), Token::RParen) {
12590                        parts.push(self.parse_assign_expr()?);
12591                        while self.eat(&Token::Comma) {
12592                            if matches!(self.peek(), Token::RParen) {
12593                                break;
12594                            }
12595                            parts.push(self.parse_assign_expr()?);
12596                        }
12597                    }
12598                    self.expect(&Token::RParen)?;
12599                } else {
12600                    // Paren-less `windowed N`: same `|>`-terminator rule as
12601                    // `chunked` above.
12602                    parts.push(self.parse_assign_expr_stop_at_pipe()?);
12603                    loop {
12604                        if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
12605                            break;
12606                        }
12607                        if matches!(
12608                            self.peek(),
12609                            Token::Semicolon
12610                                | Token::RBrace
12611                                | Token::RParen
12612                                | Token::Eof
12613                                | Token::PipeForward
12614                        ) {
12615                            break;
12616                        }
12617                        if self.peek_is_postfix_stmt_modifier_keyword() {
12618                            break;
12619                        }
12620                        parts.push(self.parse_assign_expr_stop_at_pipe()?);
12621                    }
12622                }
12623                if parts.len() == 1 {
12624                    let n = parts.pop().unwrap();
12625                    return Ok(Expr {
12626                        kind: ExprKind::FuncCall {
12627                            name: "windowed".to_string(),
12628                            args: vec![n],
12629                        },
12630                        line,
12631                    });
12632                }
12633                if parts.is_empty() {
12634                    return Ok(Expr {
12635                        kind: ExprKind::FuncCall {
12636                            name: "windowed".to_string(),
12637                            args: parts,
12638                        },
12639                        line,
12640                    });
12641                }
12642                if parts.len() == 2 {
12643                    let n = parts.pop().unwrap();
12644                    let list = parts.pop().unwrap();
12645                    return Ok(Expr {
12646                        kind: ExprKind::FuncCall {
12647                            name: "windowed".to_string(),
12648                            args: vec![list, n],
12649                        },
12650                        line,
12651                    });
12652                }
12653                Err(self.syntax_err(
12654                    "windowed: use LIST |> windowed(N) or windowed((1,2,3), 2)",
12655                    line,
12656                ))
12657            }
12658            "any" | "all" | "none" => {
12659                // `any(CODEREF, LIST)` with parens — parse as normal call.
12660                if matches!(self.peek(), Token::LParen) {
12661                    self.advance();
12662                    let args = self.parse_arg_list()?;
12663                    self.expect(&Token::RParen)?;
12664                    return Ok(Expr {
12665                        kind: ExprKind::FuncCall {
12666                            name: name.clone(),
12667                            args,
12668                        },
12669                        line,
12670                    });
12671                }
12672                // Coderef-in-block-position: `any $f LIST` / `any $f, LIST` /
12673                // `LIST |> any $f`. Same shape as the block form but uses a
12674                // value expression where `{ BLOCK }` would go.
12675                if let Some(args) = self.try_parse_coderef_listop_args(line)? {
12676                    return Ok(Expr {
12677                        kind: ExprKind::FuncCall {
12678                            name: name.clone(),
12679                            args,
12680                        },
12681                        line,
12682                    });
12683                }
12684                // `any BLOCK LIST` without parens.
12685                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
12686                if progress.is_some() {
12687                    return Err(self.syntax_err(
12688                        "`progress =>` is not supported for any/all/none (use pany for parallel + progress)",
12689                        line,
12690                    ));
12691                }
12692                let cr = Expr {
12693                    kind: ExprKind::CodeRef {
12694                        params: vec![],
12695                        body: block,
12696                    },
12697                    line,
12698                };
12699                Ok(Expr {
12700                    kind: ExprKind::FuncCall {
12701                        name: name.clone(),
12702                        args: vec![cr, list],
12703                    },
12704                    line,
12705                })
12706            }
12707            // Ruby `detect` / `find` — same as `first` (first element matching block).
12708            "first" | "detect" | "find" | "find_index" | "firstidx" | "first_index" => {
12709                let canonical =
12710                    if matches!(name.as_str(), "find_index" | "firstidx" | "first_index") {
12711                        "find_index"
12712                    } else {
12713                        "first"
12714                    };
12715                // `first(CODEREF, LIST)` with parens — parse as normal call.
12716                if matches!(self.peek(), Token::LParen) {
12717                    self.advance();
12718                    let args = self.parse_arg_list()?;
12719                    self.expect(&Token::RParen)?;
12720                    return Ok(Expr {
12721                        kind: ExprKind::FuncCall {
12722                            name: canonical.to_string(),
12723                            args,
12724                        },
12725                        line,
12726                    });
12727                }
12728                // Coderef-in-block-position: `first $f LIST` / `LIST |> first $f`.
12729                if let Some(args) = self.try_parse_coderef_listop_args(line)? {
12730                    return Ok(Expr {
12731                        kind: ExprKind::FuncCall {
12732                            name: canonical.to_string(),
12733                            args,
12734                        },
12735                        line,
12736                    });
12737                }
12738                // `first BLOCK LIST` without parens.
12739                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
12740                if progress.is_some() {
12741                    return Err(self.syntax_err(
12742                        "`progress =>` is not supported for first/detect/find/find_index (use pfirst for parallel + progress)",
12743                        line,
12744                    ));
12745                }
12746                let cr = Expr {
12747                    kind: ExprKind::CodeRef {
12748                        params: vec![],
12749                        body: block,
12750                    },
12751                    line,
12752                };
12753                Ok(Expr {
12754                    kind: ExprKind::FuncCall {
12755                        name: canonical.to_string(),
12756                        args: vec![cr, list],
12757                    },
12758                    line,
12759                })
12760            }
12761            "take_while" | "drop_while" | "skip_while" | "reject" | "grepv" | "tap" | "peek"
12762            | "partition" | "min_by" | "max_by" | "zip_with" | "count_by" => {
12763                // Coderef-in-block-position: `take_while $f LIST` etc.
12764                if let Some(args) = self.try_parse_coderef_listop_args(line)? {
12765                    return Ok(Expr {
12766                        kind: ExprKind::FuncCall {
12767                            name: name.to_string(),
12768                            args,
12769                        },
12770                        line,
12771                    });
12772                }
12773                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
12774                if progress.is_some() {
12775                    return Err(
12776                        self.syntax_err(format!("`progress =>` is not supported for {name}"), line)
12777                    );
12778                }
12779                let cr = Expr {
12780                    kind: ExprKind::CodeRef {
12781                        params: vec![],
12782                        body: block,
12783                    },
12784                    line,
12785                };
12786                Ok(Expr {
12787                    kind: ExprKind::FuncCall {
12788                        name: name.to_string(),
12789                        args: vec![cr, list],
12790                    },
12791                    line,
12792                })
12793            }
12794            "group_by" | "chunk_by" => {
12795                if matches!(self.peek(), Token::LBrace) {
12796                    let (block, list) = self.parse_block_list()?;
12797                    let cr = Expr {
12798                        kind: ExprKind::CodeRef {
12799                            params: vec![],
12800                            body: block,
12801                        },
12802                        line,
12803                    };
12804                    Ok(Expr {
12805                        kind: ExprKind::FuncCall {
12806                            name: name.to_string(),
12807                            args: vec![cr, list],
12808                        },
12809                        line,
12810                    })
12811                } else {
12812                    let key_expr = self.parse_assign_expr()?;
12813                    self.expect(&Token::Comma)?;
12814                    let list_parts = self.parse_list_until_terminator()?;
12815                    let list_expr = if list_parts.len() == 1 {
12816                        list_parts.into_iter().next().unwrap()
12817                    } else {
12818                        Expr {
12819                            kind: ExprKind::List(list_parts),
12820                            line,
12821                        }
12822                    };
12823                    Ok(Expr {
12824                        kind: ExprKind::FuncCall {
12825                            name: name.to_string(),
12826                            args: vec![key_expr, list_expr],
12827                        },
12828                        line,
12829                    })
12830                }
12831            }
12832            "with_index" => {
12833                if self.pipe_supplies_slurped_list_operand() {
12834                    return Ok(Expr {
12835                        kind: ExprKind::FuncCall {
12836                            name: "with_index".to_string(),
12837                            args: vec![],
12838                        },
12839                        line,
12840                    });
12841                }
12842                let (list, progress) = self.parse_assign_expr_list_optional_progress()?;
12843                if progress.is_some() {
12844                    return Err(
12845                        self.syntax_err("`progress =>` is not supported for with_index", line)
12846                    );
12847                }
12848                Ok(Expr {
12849                    kind: ExprKind::FuncCall {
12850                        name: "with_index".to_string(),
12851                        args: vec![list],
12852                    },
12853                    line,
12854                })
12855            }
12856            "pcache" => {
12857                let (block, list, progress) = self.parse_block_then_list_optional_progress()?;
12858                Ok(Expr {
12859                    kind: ExprKind::PcacheExpr {
12860                        block,
12861                        list: Box::new(list),
12862                        progress: progress.map(Box::new),
12863                    },
12864                    line,
12865                })
12866            }
12867            "pselect" => {
12868                let paren = self.eat(&Token::LParen);
12869                let (receivers, timeout) = self.parse_comma_expr_list_with_timeout_tail(paren)?;
12870                if paren {
12871                    self.expect(&Token::RParen)?;
12872                }
12873                if receivers.is_empty() {
12874                    return Err(self.syntax_err("pselect needs at least one receiver", line));
12875                }
12876                Ok(Expr {
12877                    kind: ExprKind::PselectExpr {
12878                        receivers,
12879                        timeout: timeout.map(Box::new),
12880                    },
12881                    line,
12882                })
12883            }
12884            "open" => {
12885                let paren = matches!(self.peek(), Token::LParen);
12886                if paren {
12887                    self.advance();
12888                }
12889                if matches!(self.peek(), Token::Ident(ref s) if is_lexical_decl(s)) {
12890                    self.advance();
12891                    let name = self.parse_scalar_var_name()?;
12892                    self.expect(&Token::Comma)?;
12893                    let mode = self.parse_assign_expr()?;
12894                    let file = if self.eat(&Token::Comma) {
12895                        Some(self.parse_assign_expr()?)
12896                    } else {
12897                        None
12898                    };
12899                    if paren {
12900                        self.expect(&Token::RParen)?;
12901                    }
12902                    Ok(Expr {
12903                        kind: ExprKind::Open {
12904                            handle: Box::new(Expr {
12905                                kind: ExprKind::OpenMyHandle { name },
12906                                line,
12907                            }),
12908                            mode: Box::new(mode),
12909                            file: file.map(Box::new),
12910                        },
12911                        line,
12912                    })
12913                } else {
12914                    // Perl convention: `open FH, "<", path` — an all-uppercase
12915                    // (or `_`) bareword in the filehandle slot is a literal
12916                    // handle name, never a constant / sub / bareword
12917                    // expression. Without this special-case, registered
12918                    // constants like `PI` / `TAU` / `E` would override the
12919                    // documented Perl idiom and the handle would register
12920                    // under the constant's numeric value.
12921                    let handle_lit = self.take_bareword_filehandle();
12922                    if handle_lit.is_some() {
12923                        // Consume the comma after the bareword filehandle so
12924                        // the arg parser starts at the mode expression.
12925                        self.expect(&Token::Comma)?;
12926                    }
12927                    let args = if paren {
12928                        self.parse_arg_list()?
12929                    } else {
12930                        self.parse_list_until_terminator()?
12931                    };
12932                    if paren {
12933                        self.expect(&Token::RParen)?;
12934                    }
12935                    let total = handle_lit.is_some() as usize + args.len();
12936                    if total < 2 {
12937                        return Err(self.syntax_err("open requires at least 2 arguments", line));
12938                    }
12939                    let (handle_expr, mode_expr, file_expr) = match handle_lit {
12940                        Some(name) => {
12941                            let h = Expr {
12942                                kind: ExprKind::String(name),
12943                                line,
12944                            };
12945                            (h, args[0].clone(), args.get(1).cloned())
12946                        }
12947                        None => (args[0].clone(), args[1].clone(), args.get(2).cloned()),
12948                    };
12949                    Ok(Expr {
12950                        kind: ExprKind::Open {
12951                            handle: Box::new(handle_expr),
12952                            mode: Box::new(mode_expr),
12953                            file: file_expr.map(Box::new),
12954                        },
12955                        line,
12956                    })
12957                }
12958            }
12959            "close" => {
12960                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12961                    return Ok(e);
12962                }
12963                // `close FH` — bareword filehandle slot takes a literal name.
12964                let a = self
12965                    .take_bareword_filehandle_arg(line)
12966                    .map(Ok)
12967                    .unwrap_or_else(|| self.parse_one_arg_or_default())?;
12968                Ok(Expr {
12969                    kind: ExprKind::Close(Box::new(a)),
12970                    line,
12971                })
12972            }
12973            "opendir" => {
12974                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
12975                    return Ok(e);
12976                }
12977                let paren = matches!(self.peek(), Token::LParen);
12978                if paren {
12979                    self.advance();
12980                }
12981                if matches!(self.peek(), Token::Ident(ref s) if is_lexical_decl(s)) {
12982                    self.advance();
12983                    let hname = self.parse_scalar_var_name()?;
12984                    self.expect(&Token::Comma)?;
12985                    let path = self.parse_assign_expr()?;
12986                    if paren {
12987                        self.expect(&Token::RParen)?;
12988                    }
12989                    return Ok(Expr {
12990                        kind: ExprKind::Opendir {
12991                            handle: Box::new(Expr {
12992                                kind: ExprKind::OpendirMyHandle { name: hname },
12993                                line,
12994                            }),
12995                            path: Box::new(path),
12996                        },
12997                        line,
12998                    });
12999                }
13000                let args = if paren {
13001                    self.parse_arg_list()?
13002                } else {
13003                    self.parse_list_until_terminator()?
13004                };
13005                if paren {
13006                    self.expect(&Token::RParen)?;
13007                }
13008                if args.len() != 2 {
13009                    return Err(self.syntax_err("opendir requires two arguments", line));
13010                }
13011                Ok(Expr {
13012                    kind: ExprKind::Opendir {
13013                        handle: Box::new(args[0].clone()),
13014                        path: Box::new(args[1].clone()),
13015                    },
13016                    line,
13017                })
13018            }
13019            "readdir" => {
13020                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13021                    return Ok(e);
13022                }
13023                let a = self.parse_one_arg()?;
13024                Ok(Expr {
13025                    kind: ExprKind::Readdir(Box::new(a)),
13026                    line,
13027                })
13028            }
13029            "closedir" => {
13030                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13031                    return Ok(e);
13032                }
13033                let a = self.parse_one_arg()?;
13034                Ok(Expr {
13035                    kind: ExprKind::Closedir(Box::new(a)),
13036                    line,
13037                })
13038            }
13039            "rewinddir" => {
13040                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13041                    return Ok(e);
13042                }
13043                let a = self.parse_one_arg()?;
13044                Ok(Expr {
13045                    kind: ExprKind::Rewinddir(Box::new(a)),
13046                    line,
13047                })
13048            }
13049            "telldir" => {
13050                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13051                    return Ok(e);
13052                }
13053                let a = self.parse_one_arg()?;
13054                Ok(Expr {
13055                    kind: ExprKind::Telldir(Box::new(a)),
13056                    line,
13057                })
13058            }
13059            "seekdir" => {
13060                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13061                    return Ok(e);
13062                }
13063                let args = self.parse_builtin_args()?;
13064                if args.len() != 2 {
13065                    return Err(self.syntax_err("seekdir requires two arguments", line));
13066                }
13067                Ok(Expr {
13068                    kind: ExprKind::Seekdir {
13069                        handle: Box::new(args[0].clone()),
13070                        position: Box::new(args[1].clone()),
13071                    },
13072                    line,
13073                })
13074            }
13075            "eof" => {
13076                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13077                    return Ok(e);
13078                }
13079                // `eof FH` — bareword filehandle slot (no parens) takes a
13080                // literal name. `eof(FH)` / `eof($fh)` / `eof("FH")` keep
13081                // their general-expression handling.
13082                if let Some(a) = self.take_bareword_filehandle_arg(line) {
13083                    return Ok(Expr {
13084                        kind: ExprKind::Eof(Some(Box::new(a))),
13085                        line,
13086                    });
13087                }
13088                if matches!(self.peek(), Token::LParen) {
13089                    self.advance();
13090                    if matches!(self.peek(), Token::RParen) {
13091                        self.advance();
13092                        Ok(Expr {
13093                            kind: ExprKind::Eof(None),
13094                            line,
13095                        })
13096                    } else {
13097                        // Inside the parens, bareword still wins as a handle.
13098                        let a = self
13099                            .take_bareword_filehandle_arg(line)
13100                            .map(Ok)
13101                            .unwrap_or_else(|| self.parse_expression())?;
13102                        self.expect(&Token::RParen)?;
13103                        Ok(Expr {
13104                            kind: ExprKind::Eof(Some(Box::new(a))),
13105                            line,
13106                        })
13107                    }
13108                } else {
13109                    Ok(Expr {
13110                        kind: ExprKind::Eof(None),
13111                        line,
13112                    })
13113                }
13114            }
13115            "system" => {
13116                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13117                    return Ok(e);
13118                }
13119                let args = self.parse_builtin_args()?;
13120                Ok(Expr {
13121                    kind: ExprKind::System(args),
13122                    line,
13123                })
13124            }
13125            "exec" => {
13126                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13127                    return Ok(e);
13128                }
13129                let args = self.parse_builtin_args()?;
13130                Ok(Expr {
13131                    kind: ExprKind::Exec(args),
13132                    line,
13133                })
13134            }
13135            "eval" => {
13136                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13137                    return Ok(e);
13138                }
13139                let a = if matches!(self.peek(), Token::LBrace) {
13140                    let block = self.parse_block()?;
13141                    Expr {
13142                        kind: ExprKind::CodeRef {
13143                            params: vec![],
13144                            body: block,
13145                        },
13146                        line,
13147                    }
13148                } else {
13149                    self.parse_one_arg_or_default()?
13150                };
13151                Ok(Expr {
13152                    kind: ExprKind::Eval(Box::new(a)),
13153                    line,
13154                })
13155            }
13156            "do" => {
13157                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13158                    return Ok(e);
13159                }
13160                let a = self.parse_one_arg()?;
13161                Ok(Expr {
13162                    kind: ExprKind::Do(Box::new(a)),
13163                    line,
13164                })
13165            }
13166            "require" => {
13167                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13168                    return Ok(e);
13169                }
13170                let a = self.parse_one_arg()?;
13171                Ok(Expr {
13172                    kind: ExprKind::Require(Box::new(a)),
13173                    line,
13174                })
13175            }
13176            "exit" => {
13177                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13178                    return Ok(e);
13179                }
13180                if matches!(
13181                    self.peek(),
13182                    Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
13183                ) {
13184                    Ok(Expr {
13185                        kind: ExprKind::Exit(None),
13186                        line,
13187                    })
13188                } else {
13189                    let a = self.parse_one_arg()?;
13190                    Ok(Expr {
13191                        kind: ExprKind::Exit(Some(Box::new(a))),
13192                        line,
13193                    })
13194                }
13195            }
13196            "chdir" => {
13197                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13198                    return Ok(e);
13199                }
13200                let a = self.parse_one_arg_or_default()?;
13201                Ok(Expr {
13202                    kind: ExprKind::Chdir(Box::new(a)),
13203                    line,
13204                })
13205            }
13206            "mkdir" => {
13207                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13208                    return Ok(e);
13209                }
13210                let args = self.parse_builtin_args()?;
13211                Ok(Expr {
13212                    kind: ExprKind::Mkdir {
13213                        path: Box::new(args[0].clone()),
13214                        mode: args.get(1).cloned().map(Box::new),
13215                    },
13216                    line,
13217                })
13218            }
13219            "unlink" | "rm" => {
13220                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13221                    return Ok(e);
13222                }
13223                let args = self.parse_builtin_args()?;
13224                Ok(Expr {
13225                    kind: ExprKind::Unlink(args),
13226                    line,
13227                })
13228            }
13229            "rename" => {
13230                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13231                    return Ok(e);
13232                }
13233                let args = self.parse_builtin_args()?;
13234                if args.len() != 2 {
13235                    return Err(self.syntax_err("rename requires two arguments", line));
13236                }
13237                Ok(Expr {
13238                    kind: ExprKind::Rename {
13239                        old: Box::new(args[0].clone()),
13240                        new: Box::new(args[1].clone()),
13241                    },
13242                    line,
13243                })
13244            }
13245            "chmod" => {
13246                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13247                    return Ok(e);
13248                }
13249                let args = self.parse_builtin_args()?;
13250                if args.len() < 2 {
13251                    return Err(self.syntax_err("chmod requires mode and at least one file", line));
13252                }
13253                Ok(Expr {
13254                    kind: ExprKind::Chmod(args),
13255                    line,
13256                })
13257            }
13258            "chown" => {
13259                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13260                    return Ok(e);
13261                }
13262                let args = self.parse_builtin_args()?;
13263                if args.len() < 3 {
13264                    return Err(
13265                        self.syntax_err("chown requires uid, gid, and at least one file", line)
13266                    );
13267                }
13268                Ok(Expr {
13269                    kind: ExprKind::Chown(args),
13270                    line,
13271                })
13272            }
13273            "stat" => {
13274                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13275                    return Ok(e);
13276                }
13277                let args = self.parse_builtin_args()?;
13278                let arg = if args.len() == 1 {
13279                    args[0].clone()
13280                } else if args.is_empty() {
13281                    Expr {
13282                        kind: ExprKind::ScalarVar("_".into()),
13283                        line,
13284                    }
13285                } else {
13286                    return Err(self.syntax_err("stat requires zero or one argument", line));
13287                };
13288                Ok(Expr {
13289                    kind: ExprKind::Stat(Box::new(arg)),
13290                    line,
13291                })
13292            }
13293            "lstat" => {
13294                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13295                    return Ok(e);
13296                }
13297                let args = self.parse_builtin_args()?;
13298                let arg = if args.len() == 1 {
13299                    args[0].clone()
13300                } else if args.is_empty() {
13301                    Expr {
13302                        kind: ExprKind::ScalarVar("_".into()),
13303                        line,
13304                    }
13305                } else {
13306                    return Err(self.syntax_err("lstat requires zero or one argument", line));
13307                };
13308                Ok(Expr {
13309                    kind: ExprKind::Lstat(Box::new(arg)),
13310                    line,
13311                })
13312            }
13313            "link" => {
13314                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13315                    return Ok(e);
13316                }
13317                let args = self.parse_builtin_args()?;
13318                if args.len() != 2 {
13319                    return Err(self.syntax_err("link requires two arguments", line));
13320                }
13321                Ok(Expr {
13322                    kind: ExprKind::Link {
13323                        old: Box::new(args[0].clone()),
13324                        new: Box::new(args[1].clone()),
13325                    },
13326                    line,
13327                })
13328            }
13329            "symlink" => {
13330                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13331                    return Ok(e);
13332                }
13333                let args = self.parse_builtin_args()?;
13334                if args.len() != 2 {
13335                    return Err(self.syntax_err("symlink requires two arguments", line));
13336                }
13337                Ok(Expr {
13338                    kind: ExprKind::Symlink {
13339                        old: Box::new(args[0].clone()),
13340                        new: Box::new(args[1].clone()),
13341                    },
13342                    line,
13343                })
13344            }
13345            "readlink" => {
13346                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13347                    return Ok(e);
13348                }
13349                let args = self.parse_builtin_args()?;
13350                let arg = if args.len() == 1 {
13351                    args[0].clone()
13352                } else if args.is_empty() {
13353                    Expr {
13354                        kind: ExprKind::ScalarVar("_".into()),
13355                        line,
13356                    }
13357                } else {
13358                    return Err(self.syntax_err("readlink requires zero or one argument", line));
13359                };
13360                Ok(Expr {
13361                    kind: ExprKind::Readlink(Box::new(arg)),
13362                    line,
13363                })
13364            }
13365            "files" => {
13366                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13367                    return Ok(e);
13368                }
13369                let args = self.parse_builtin_args()?;
13370                Ok(Expr {
13371                    kind: ExprKind::Files(args),
13372                    line,
13373                })
13374            }
13375            "filesf" | "f" => {
13376                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13377                    return Ok(e);
13378                }
13379                let args = self.parse_builtin_args()?;
13380                Ok(Expr {
13381                    kind: ExprKind::Filesf(args),
13382                    line,
13383                })
13384            }
13385            "fr" => {
13386                let args = self.parse_builtin_args()?;
13387                Ok(Expr {
13388                    kind: ExprKind::FilesfRecursive(args),
13389                    line,
13390                })
13391            }
13392            "dirs" | "d" => {
13393                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13394                    return Ok(e);
13395                }
13396                let args = self.parse_builtin_args()?;
13397                Ok(Expr {
13398                    kind: ExprKind::Dirs(args),
13399                    line,
13400                })
13401            }
13402            "dr" => {
13403                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13404                    return Ok(e);
13405                }
13406                let args = self.parse_builtin_args()?;
13407                Ok(Expr {
13408                    kind: ExprKind::DirsRecursive(args),
13409                    line,
13410                })
13411            }
13412            "sym_links" => {
13413                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13414                    return Ok(e);
13415                }
13416                let args = self.parse_builtin_args()?;
13417                Ok(Expr {
13418                    kind: ExprKind::SymLinks(args),
13419                    line,
13420                })
13421            }
13422            "sockets" => {
13423                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13424                    return Ok(e);
13425                }
13426                let args = self.parse_builtin_args()?;
13427                Ok(Expr {
13428                    kind: ExprKind::Sockets(args),
13429                    line,
13430                })
13431            }
13432            "pipes" => {
13433                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13434                    return Ok(e);
13435                }
13436                let args = self.parse_builtin_args()?;
13437                Ok(Expr {
13438                    kind: ExprKind::Pipes(args),
13439                    line,
13440                })
13441            }
13442            "block_devices" => {
13443                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13444                    return Ok(e);
13445                }
13446                let args = self.parse_builtin_args()?;
13447                Ok(Expr {
13448                    kind: ExprKind::BlockDevices(args),
13449                    line,
13450                })
13451            }
13452            "char_devices" => {
13453                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13454                    return Ok(e);
13455                }
13456                let args = self.parse_builtin_args()?;
13457                Ok(Expr {
13458                    kind: ExprKind::CharDevices(args),
13459                    line,
13460                })
13461            }
13462            "exe" | "executables" => {
13463                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13464                    return Ok(e);
13465                }
13466                let args = self.parse_builtin_args()?;
13467                Ok(Expr {
13468                    kind: ExprKind::Executables(args),
13469                    line,
13470                })
13471            }
13472            "glob" => {
13473                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13474                    return Ok(e);
13475                }
13476                let args = self.parse_builtin_args()?;
13477                Ok(Expr {
13478                    kind: ExprKind::Glob(args),
13479                    line,
13480                })
13481            }
13482            "glob_par" => {
13483                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13484                    return Ok(e);
13485                }
13486                let (args, progress) = self.parse_glob_par_or_par_sed_args()?;
13487                Ok(Expr {
13488                    kind: ExprKind::GlobPar { args, progress },
13489                    line,
13490                })
13491            }
13492            "par_sed" => {
13493                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13494                    return Ok(e);
13495                }
13496                let (args, progress) = self.parse_glob_par_or_par_sed_args()?;
13497                Ok(Expr {
13498                    kind: ExprKind::ParSed { args, progress },
13499                    line,
13500                })
13501            }
13502            "bless" => {
13503                if let Some(e) = self.fat_arrow_autoquote(&name, line) {
13504                    return Ok(e);
13505                }
13506                let args = self.parse_builtin_args()?;
13507                Ok(Expr {
13508                    kind: ExprKind::Bless {
13509                        ref_expr: Box::new(args[0].clone()),
13510                        class: args.get(1).cloned().map(Box::new),
13511                    },
13512                    line,
13513                })
13514            }
13515            "caller" => {
13516                if matches!(self.peek(), Token::LParen) {
13517                    self.advance();
13518                    if matches!(self.peek(), Token::RParen) {
13519                        self.advance();
13520                        Ok(Expr {
13521                            kind: ExprKind::Caller(None),
13522                            line,
13523                        })
13524                    } else {
13525                        let a = self.parse_expression()?;
13526                        self.expect(&Token::RParen)?;
13527                        Ok(Expr {
13528                            kind: ExprKind::Caller(Some(Box::new(a))),
13529                            line,
13530                        })
13531                    }
13532                } else {
13533                    Ok(Expr {
13534                        kind: ExprKind::Caller(None),
13535                        line,
13536                    })
13537                }
13538            }
13539            "wantarray" => {
13540                if crate::no_interop_mode() {
13541                    return Err(self.syntax_err(
13542                        "stryke `wantarray` is rejected under --no-interop — \
13543                         use explicit return-shape (`@result` vs `$scalar`) \
13544                         or pass a flag arg instead of context-sniffing",
13545                        line,
13546                    ));
13547                }
13548                if matches!(self.peek(), Token::LParen) {
13549                    self.advance();
13550                    self.expect(&Token::RParen)?;
13551                }
13552                Ok(Expr {
13553                    kind: ExprKind::Wantarray,
13554                    line,
13555                })
13556            }
13557            "sub" => {
13558                // In no-interop mode, `sub {}` is not valid — must use `fn {}`
13559                if crate::no_interop_mode() {
13560                    return Err(self.syntax_err(
13561                        "stryke uses `fn {}` instead of `sub {}` (--no-interop)",
13562                        line,
13563                    ));
13564                }
13565                // Anonymous sub — optional prototype `sub () { }` (e.g. Carp.pm `*X = sub () { 1 }`)
13566                let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
13567                let body = self.parse_block()?;
13568                Ok(Expr {
13569                    kind: ExprKind::CodeRef { params, body },
13570                    line,
13571                })
13572            }
13573            "fn" => {
13574                // Anonymous fn — stryke syntax for anonymous subroutines
13575                let (params, _prototype) = self.parse_sub_sig_or_prototype_opt()?;
13576                self.parse_sub_attributes()?;
13577                let body = self.parse_fn_eq_body_or_block(false)?;
13578                Ok(Expr {
13579                    kind: ExprKind::CodeRef { params, body },
13580                    line,
13581                })
13582            }
13583            _ => {
13584                // Generic function call
13585                // Check for fat arrow (bareword string in hash) — except for
13586                // topic-slot barewords (`_`, `_<`, `_0`, `_0<`, …), which must
13587                // resolve to the topic value, not the literal name.
13588                if matches!(self.peek(), Token::FatArrow) && !Self::is_underscore_topic_slot(&name)
13589                {
13590                    return Ok(Expr {
13591                        kind: ExprKind::String(name),
13592                        line,
13593                    });
13594                }
13595                // Bare `_` in expression position → topic variable `$_`.
13596                // Allows concise blocks: `map { _ * 2 }`, `fi { _ > 5 }`.
13597                // Also handles the outer-topic chain: `_<`, `_<<`, `_<<<`,
13598                // `_<<<<` for 1..4 frames up — and the positional matrix:
13599                // `_0<<<<`, `_1<<<<`, `_N<<<<` (N positionals × 5 levels).
13600                // `_0` is canonically aliased to `_` at every level (see
13601                // `Scope::set_closure_args`).
13602                //
13603                // Stryke string-index sugar: `_[N]` (bareword, no sigil) is
13604                // an alias for `_!N!` — char-of-topic substring. The sigil
13605                // form `$_[N]` keeps Perl's `@_`-access semantics (first
13606                // positional arg). We dispatch here, before the generic
13607                // ArrayElement path, so the AST for `_[N]` carries the
13608                // synthetic `__topicstr__$NAME` flag the interpreter / VM
13609                // strip and route to char-of-string.
13610                if Self::is_underscore_topic_slot(&name) {
13611                    if matches!(self.peek(), Token::LBracket) && self.peek_line() == line {
13612                        self.advance(); // [
13613                        let index = self.parse_expression()?;
13614                        self.expect(&Token::RBracket)?;
13615                        return Ok(Expr {
13616                            kind: ExprKind::ArrayElement {
13617                                array: format!("__topicstr__{}", name),
13618                                index: Box::new(index),
13619                            },
13620                            line,
13621                        });
13622                    }
13623                    return Ok(Expr {
13624                        kind: ExprKind::ScalarVar(name.clone()),
13625                        line,
13626                    });
13627                }
13628                // Function call with optional parens
13629                if matches!(self.peek(), Token::LParen) {
13630                    self.advance();
13631                    let args = self.parse_arg_list()?;
13632                    self.expect(&Token::RParen)?;
13633                    Ok(Expr {
13634                        kind: ExprKind::FuncCall { name, args },
13635                        line,
13636                    })
13637                } else if self.peek().is_term_start()
13638                    && !(matches!(self.peek(), Token::Ident(ref kw) if kw == "sub")
13639                        && matches!(self.peek_at(1), Token::Ident(_)))
13640                    && !(self.suppress_parenless_call > 0 && matches!(self.peek(), Token::Ident(_)))
13641                    && !(matches!(self.peek(), Token::LBrace)
13642                        && self.peek_line() > self.prev_line())
13643                    && !(matches!(self.peek(), Token::BitNot)
13644                        && self.suppress_tilde_range == 0
13645                        && matches!(
13646                            self.peek_at(1),
13647                            Token::Ident(_) | Token::Integer(_) | Token::Float(_)
13648                        ))
13649                {
13650                    // Perl allows func arg without parens
13651                    // Guard: `sub <name> { }` is a named sub declaration (new
13652                    // statement), not an argument to the preceding call.
13653                    // Guard: suppress_parenless_call > 0 with Ident prevents consuming
13654                    // barewords (used by thread macro so `t Color::Red p` treats
13655                    // `p` as a stage, not an argument to the enum variant), but
13656                    // still allows `{` for struct/hash literals like `t Foo { x => 1 } p`.
13657                    // Guard: `{` on a new line is a new statement (hashref/block),
13658                    // not an argument to the preceding bareword call.
13659                    // Guard: `~Ident` / `~Integer` / `~Float` after a bareword is
13660                    // the universal-tilde range separator (`I~M~5`, `Mon~Fri`,
13661                    // `Jan~Dec~2`), not unary BitNot of an arg. Bail to Bareword
13662                    // so the outer `parse_range` consumes `~` as the range op.
13663                    let args = self.parse_list_until_terminator()?;
13664                    Ok(Expr {
13665                        kind: ExprKind::FuncCall { name, args },
13666                        line,
13667                    })
13668                } else {
13669                    // No parens, no visible arguments — emit a Bareword.
13670                    // At runtime, Bareword tries sub resolution first (zero-arg
13671                    // call) and falls back to a string value.  stryke extension
13672                    // contexts (pipe-forward, map/fore) lift Bareword → FuncCall
13673                    // with `$_` injection separately.
13674                    Ok(Expr {
13675                        kind: ExprKind::Bareword(name),
13676                        line,
13677                    })
13678                }
13679            }
13680        }
13681    }
13682
13683    /// `open FH, ...` / `close FH` / `eof FH` — Perl's convention is that an
13684    /// all-uppercase (letters / digits / `_`, starting with a letter or `_`)
13685    /// bareword in the filehandle slot is a literal handle name, never a
13686    /// constant or sub call. This shadows registered constants like `PI`,
13687    /// `TAU`, `E` and the rare uppercase-letter filehandles (`H`, `O`, …)
13688    /// that would otherwise route through the bareword resolver.
13689    ///
13690    /// Returns `Some(name)` when the next token is such a bareword and the
13691    /// token after it is one of the accepted terminators (any of `accept`,
13692    /// or — when `accept` is empty — any of `,`, `;`, `)`, `}`, `|>`, Eof).
13693    /// Otherwise returns `None` and leaves the cursor untouched.
13694    fn take_bareword_filehandle_if(&mut self, accept: &[Token]) -> Option<String> {
13695        let Token::Ident(h) = self.peek().clone() else {
13696            return None;
13697        };
13698        // `main::STDOUT` is the qualified form of `STDOUT` — `main` is
13699        // the default package for special filehandles per Perl. The
13700        // lexer emits this as three tokens (Ident("main"), PackageSep,
13701        // Ident("STDOUT")), so detect the trio and consume the prefix
13702        // up front, then check the leaf as a normal uppercase handle.
13703        let saved = self.pos;
13704        let qualified = h == "main"
13705            && matches!(self.peek_at(1), Token::PackageSep)
13706            && matches!(self.peek_at(2), Token::Ident(s) if !s.is_empty());
13707        if qualified {
13708            self.advance(); // main
13709            self.advance(); // ::
13710        }
13711        let leaf = match self.peek().clone() {
13712            Token::Ident(s) => s,
13713            _ => {
13714                self.pos = saved;
13715                return None;
13716            }
13717        };
13718        let mut chars = leaf.chars();
13719        let first = match chars.next() {
13720            Some(c) => c,
13721            None => {
13722                self.pos = saved;
13723                return None;
13724            }
13725        };
13726        if !(first.is_ascii_uppercase() || first == '_') {
13727            self.pos = saved;
13728            return None;
13729        }
13730        if !leaf
13731            .chars()
13732            .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')
13733        {
13734            self.pos = saved;
13735            return None;
13736        }
13737        let next = self.peek_at(1);
13738        let ok = if accept.is_empty() {
13739            matches!(
13740                next,
13741                Token::Comma
13742                    | Token::Semicolon
13743                    | Token::RParen
13744                    | Token::RBrace
13745                    | Token::Eof
13746                    | Token::PipeForward
13747            )
13748        } else {
13749            accept
13750                .iter()
13751                .any(|t| std::mem::discriminant(t) == std::mem::discriminant(next))
13752        };
13753        if !ok {
13754            self.pos = saved;
13755            return None;
13756        }
13757        self.advance();
13758        Some(leaf)
13759    }
13760
13761    /// `open FH, …` — bareword filehandle followed by a comma.
13762    fn take_bareword_filehandle(&mut self) -> Option<String> {
13763        self.take_bareword_filehandle_if(&[Token::Comma])
13764    }
13765
13766    /// `close FH` / `eof FH` — bareword filehandle followed by a statement
13767    /// terminator. Returns a `String` expression to splice into the arg
13768    /// slot, or `None` if the next token isn't a literal filehandle.
13769    fn take_bareword_filehandle_arg(&mut self, line: usize) -> Option<Expr> {
13770        self.take_bareword_filehandle_if(&[]).map(|name| Expr {
13771            kind: ExprKind::String(name),
13772            line,
13773        })
13774    }
13775
13776    fn parse_print_like(
13777        &mut self,
13778        make: impl FnOnce(Option<String>, Vec<Expr>) -> ExprKind,
13779    ) -> StrykeResult<Expr> {
13780        let line = self.peek_line();
13781        // Check for filehandle: print STDERR "msg"  /  print $fh "msg"
13782        let handle = if let Token::Ident(ref h) = self.peek().clone() {
13783            // `print main::STDERR "msg"` — `main` is the default package
13784            // for special filehandles per Perl. The lexer emits
13785            // `main::STDOUT` as three tokens (Ident("main"), PackageSep,
13786            // Ident("STDOUT")), so peek + lookahead for the qualified
13787            // form and consume the `main::` prefix up front so the
13788            // existing uppercase-bareword path picks up the bare leaf.
13789            if h == "main"
13790                && matches!(self.peek_at(1), Token::PackageSep)
13791                && matches!(
13792                    self.peek_at(2),
13793                    Token::Ident(s) if !s.is_empty()
13794                        && s.chars().all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')
13795                        && s.chars().next().is_some_and(|c| c.is_ascii_uppercase() || c == '_'))
13796            {
13797                self.advance(); // main
13798                self.advance(); // ::
13799            }
13800            let h = match self.peek().clone() {
13801                Token::Ident(s) => s,
13802                _ => h.clone(),
13803            };
13804            if h.chars().all(|c| c.is_uppercase() || c == '_')
13805                && !matches!(self.peek(), Token::LParen)
13806            {
13807                let saved = self.pos;
13808                self.advance();
13809                // Verify next token is a term start (not operator).
13810                // Guard: `~Ident` / `~Integer` / `~Float` is a universal-tilde
13811                // range separator (`p I~M~5`, `p Mon~Fri`), not unary BitNot of
13812                // an arg. Bail filehandle detection so the bareword `I` flows
13813                // into the regular expression path where `parse_range` consumes
13814                // `~` as the range op.
13815                let is_tilde_range_after = matches!(self.peek(), Token::BitNot)
13816                    && self.suppress_tilde_range == 0
13817                    && matches!(
13818                        self.peek_at(1),
13819                        Token::Ident(_) | Token::Integer(_) | Token::Float(_)
13820                    );
13821                if !is_tilde_range_after
13822                    && (self.peek().is_term_start()
13823                        || matches!(
13824                            self.peek(),
13825                            Token::DoubleString(_)
13826                                | Token::BacktickString(_)
13827                                | Token::SingleString(_)
13828                        ))
13829                {
13830                    Some(h)
13831                } else {
13832                    self.pos = saved;
13833                    None
13834                }
13835            } else {
13836                None
13837            }
13838        } else if let Token::ScalarVar(ref v) = self.peek().clone() {
13839            // `print $fh "msg"` — scalar variable as indirect filehandle.
13840            // Treat as handle when the next token (after $var) is a term-start or
13841            // string literal *without* a preceding comma/operator, matching Perl's
13842            // indirect-object heuristic.
13843            // Exclude `$_` — it's virtually always the topic variable, not a handle.
13844            // Exclude `[` and `{` — those are array/hash subscripts on the variable
13845            // itself (`print $F[0]`, `print $h{k}`), not separate print arguments.
13846            // Exclude `(` — `$f(...)` is a coderef call expression (`p $f(5)` is
13847            // `p( $f(5) )`, not `print $f ARGS`). Without this guard, calling
13848            // stored coderefs in print/say/printf/p position required parens
13849            // wrappers (`p ( $f(5) )`) or a temporary scalar.
13850            // Exclude statement modifiers (`if`/`unless`/`while`/`until`/`for`/`foreach`)
13851            // — `print $_ if COND` prints `$_` to STDOUT, not to a handle named `$_`.
13852            // Exclude tokens on a later line — a newline ends the print statement
13853            // in stryke, so `p $j\nmy $k = …` must not absorb the following `my`.
13854            let v = v.clone();
13855            if v == "_" || matches!(self.peek_at(1), Token::LParen) {
13856                None
13857            } else {
13858                let saved = self.pos;
13859                let var_line = self.peek_line();
13860                self.advance();
13861                let next = self.peek().clone();
13862                let next_line = self.peek_line();
13863                let is_stmt_modifier = matches!(&next, Token::Ident(kw)
13864                    if matches!(kw.as_str(), "if" | "unless" | "while" | "until" | "for" | "foreach"));
13865                if !is_stmt_modifier
13866                    && next_line == var_line
13867                    && !matches!(next, Token::LBracket | Token::LBrace)
13868                    && (next.is_term_start()
13869                        || matches!(
13870                            next,
13871                            Token::DoubleString(_)
13872                                | Token::BacktickString(_)
13873                                | Token::SingleString(_)
13874                        ))
13875                {
13876                    // Next token looks like a print argument — $var is the handle.
13877                    Some(format!("${v}"))
13878                } else {
13879                    self.pos = saved;
13880                    None
13881                }
13882            }
13883        } else {
13884            None
13885        };
13886        // `print()` / `say()` / `printf()` — empty parens default to `$_`,
13887        // matching Perl 5: `perldoc -f print` / `-f say` say "If no arguments
13888        // are given, prints $_." (Same convention as the topic-default unary
13889        // builtins handled in `parse_one_arg_or_default`.)
13890        // Use `parse_list_until_terminator_allow_pipe` so that `p @a |> sum`
13891        // parses as `p(sum(@a))`, matching `~>` thread-first behavior.
13892        let args =
13893            if matches!(self.peek(), Token::LParen) && matches!(self.peek_at(1), Token::RParen) {
13894                let line_topic = self.peek_line();
13895                self.advance(); // (
13896                self.advance(); // )
13897                vec![Expr {
13898                    kind: ExprKind::ScalarVar("_".into()),
13899                    line: line_topic,
13900                }]
13901            } else {
13902                self.parse_list_until_terminator_allow_pipe()?
13903            };
13904        Ok(Expr {
13905            kind: make(handle, args),
13906            line,
13907        })
13908    }
13909
13910    fn parse_block_list(&mut self) -> StrykeResult<(Block, Expr)> {
13911        let block = self.parse_block()?;
13912        let block_end_line = self.prev_line();
13913        self.eat(&Token::Comma);
13914        // On the RHS of `|>`, the list operand is supplied by the piped LHS
13915        // and will be substituted at desugar time — accept a placeholder when
13916        // we're at a terminator here or on a new line (implicit semicolon).
13917        if self.in_pipe_rhs()
13918            && (matches!(
13919                self.peek(),
13920                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
13921            ) || self.peek_line() > block_end_line)
13922        {
13923            let line = self.peek_line();
13924            return Ok((block, self.pipe_placeholder_list(line)));
13925        }
13926        let list = self.parse_expression()?;
13927        Ok((block, list))
13928    }
13929
13930    /// Comma-separated expressions with optional trailing `timeout => SECS` (for `pselect`).
13931    /// When `paren` is true, stops at `)` as well as normal terminators.
13932    fn parse_comma_expr_list_with_timeout_tail(
13933        &mut self,
13934        paren: bool,
13935    ) -> StrykeResult<(Vec<Expr>, Option<Expr>)> {
13936        let mut parts = vec![self.parse_assign_expr()?];
13937        loop {
13938            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
13939                break;
13940            }
13941            if paren && matches!(self.peek(), Token::RParen) {
13942                break;
13943            }
13944            if matches!(
13945                self.peek(),
13946                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
13947            ) {
13948                break;
13949            }
13950            if self.peek_is_postfix_stmt_modifier_keyword() {
13951                break;
13952            }
13953            if let Token::Ident(ref kw) = self.peek().clone() {
13954                if kw == "timeout" && matches!(self.peek_at(1), Token::FatArrow) {
13955                    self.advance();
13956                    self.expect(&Token::FatArrow)?;
13957                    let t = self.parse_assign_expr()?;
13958                    return Ok((parts, Some(t)));
13959                }
13960            }
13961            parts.push(self.parse_assign_expr()?);
13962        }
13963        Ok((parts, None))
13964    }
13965
13966    /// `preduce_init EXPR, BLOCK, LIST` with optional `, progress => EXPR`.
13967    fn parse_init_block_then_list_optional_progress(
13968        &mut self,
13969    ) -> StrykeResult<(Expr, Block, Expr, Option<Expr>)> {
13970        let init = self.parse_assign_expr()?;
13971        self.expect(&Token::Comma)?;
13972        let block = self.parse_block_or_bareword_block()?;
13973        self.eat(&Token::Comma);
13974        let line = self.peek_line();
13975        if let Token::Ident(ref kw) = self.peek().clone() {
13976            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
13977                self.advance();
13978                self.expect(&Token::FatArrow)?;
13979                let prog = self.parse_assign_expr()?;
13980                return Ok((
13981                    init,
13982                    block,
13983                    Expr {
13984                        kind: ExprKind::List(vec![]),
13985                        line,
13986                    },
13987                    Some(prog),
13988                ));
13989            }
13990        }
13991        if matches!(
13992            self.peek(),
13993            Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
13994        ) {
13995            return Ok((
13996                init,
13997                block,
13998                Expr {
13999                    kind: ExprKind::List(vec![]),
14000                    line,
14001                },
14002                None,
14003            ));
14004        }
14005        let mut parts = vec![self.parse_assign_expr()?];
14006        loop {
14007            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
14008                break;
14009            }
14010            if matches!(
14011                self.peek(),
14012                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
14013            ) {
14014                break;
14015            }
14016            if self.peek_is_postfix_stmt_modifier_keyword() {
14017                break;
14018            }
14019            if let Token::Ident(ref kw) = self.peek().clone() {
14020                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
14021                    self.advance();
14022                    self.expect(&Token::FatArrow)?;
14023                    let prog = self.parse_assign_expr()?;
14024                    return Ok((init, block, merge_expr_list(parts), Some(prog)));
14025                }
14026            }
14027            parts.push(self.parse_assign_expr()?);
14028        }
14029        Ok((init, block, merge_expr_list(parts), None))
14030    }
14031
14032    /// `pmap_on CLUSTER { BLOCK } LIST [, progress => EXPR]` — cluster expr, then same tail as [`Self::parse_block_then_list_optional_progress`].
14033    fn parse_cluster_block_then_list_optional_progress(
14034        &mut self,
14035    ) -> StrykeResult<(Expr, Block, Expr, Option<Expr>)> {
14036        // `pmap_on $c { BLOCK } @list` — suppress `$c { ... }` hash-subscript
14037        // auto-arrow so the brace opens the BLOCK, not a `$c->{...}` deref.
14038        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_add(1);
14039        let cluster = self.parse_assign_expr();
14040        self.suppress_scalar_hash_brace = self.suppress_scalar_hash_brace.saturating_sub(1);
14041        let cluster = cluster?;
14042        // Accept the canonical `pmap_on $c, { BLOCK } @list` LSP-doc form too.
14043        self.eat(&Token::Comma);
14044        let block = self.parse_block_or_bareword_block()?;
14045        let block_end_line = self.prev_line();
14046        self.eat(&Token::Comma);
14047        let line = self.peek_line();
14048        if let Token::Ident(ref kw) = self.peek().clone() {
14049            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
14050                self.advance();
14051                self.expect(&Token::FatArrow)?;
14052                let prog = self.parse_assign_expr_stop_at_pipe()?;
14053                return Ok((
14054                    cluster,
14055                    block,
14056                    Expr {
14057                        kind: ExprKind::List(vec![]),
14058                        line,
14059                    },
14060                    Some(prog),
14061                ));
14062            }
14063        }
14064        let empty_list_ok = matches!(
14065            self.peek(),
14066            Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
14067        ) || (self.in_pipe_rhs()
14068            && (matches!(self.peek(), Token::Comma) || self.peek_line() > block_end_line));
14069        if empty_list_ok {
14070            return Ok((
14071                cluster,
14072                block,
14073                Expr {
14074                    kind: ExprKind::List(vec![]),
14075                    line,
14076                },
14077                None,
14078            ));
14079        }
14080        let mut parts = vec![self.parse_assign_expr_stop_at_pipe()?];
14081        loop {
14082            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
14083                break;
14084            }
14085            if matches!(
14086                self.peek(),
14087                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
14088            ) {
14089                break;
14090            }
14091            if self.peek_is_postfix_stmt_modifier_keyword() {
14092                break;
14093            }
14094            if let Token::Ident(ref kw) = self.peek().clone() {
14095                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
14096                    self.advance();
14097                    self.expect(&Token::FatArrow)?;
14098                    let prog = self.parse_assign_expr_stop_at_pipe()?;
14099                    return Ok((cluster, block, merge_expr_list(parts), Some(prog)));
14100                }
14101            }
14102            parts.push(self.parse_assign_expr_stop_at_pipe()?);
14103        }
14104        Ok((cluster, block, merge_expr_list(parts), None))
14105    }
14106
14107    /// Like [`parse_block_list`] but supports a trailing `, progress => EXPR`
14108    /// (`pmap`, `pgrep`, `preduce`, `pfor`, `pcache`, `psort`, …).
14109    ///
14110    /// Always invoked for paren-less trailing forms (`pmap { … } LIST`,
14111    /// `pmap { … } LIST, progress => EXPR`), so `|>` must terminate the whole
14112    /// stage — individual list parts and the progress value parse through
14113    /// [`Self::parse_assign_expr_stop_at_pipe`] to keep pipe-forward
14114    /// left-associative in `@a |> pmap { $_ * 2 }, progress => 0 |> join ','`.
14115    fn parse_block_then_list_optional_progress(
14116        &mut self,
14117    ) -> StrykeResult<(Block, Expr, Option<Expr>)> {
14118        let block = self.parse_block_or_bareword_block()?;
14119        let block_end_line = self.prev_line();
14120        self.eat(&Token::Comma);
14121        let line = self.peek_line();
14122        if let Token::Ident(ref kw) = self.peek().clone() {
14123            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
14124                self.advance();
14125                self.expect(&Token::FatArrow)?;
14126                let prog = self.parse_assign_expr_stop_at_pipe()?;
14127                return Ok((
14128                    block,
14129                    Expr {
14130                        kind: ExprKind::List(vec![]),
14131                        line,
14132                    },
14133                    Some(prog),
14134                ));
14135            }
14136        }
14137        // An empty list operand is allowed when the next token terminates the
14138        // enclosing context. Inside a pipe-forward RHS, a trailing `,` also
14139        // counts — `foo(bar, @a |> pmap { $_ * 2 }, baz)`. `|>` is also a
14140        // terminator — left-associative chaining leaves the outer `|>` for
14141        // the enclosing pipe-forward loop. A newline after the block also
14142        // terminates in pipe-RHS — the LHS supplies the list, so we must NOT
14143        // greedily eat the next statement (matches `parse_block_list`).
14144        let empty_list_ok = matches!(
14145            self.peek(),
14146            Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
14147        ) || (self.in_pipe_rhs()
14148            && (matches!(self.peek(), Token::Comma) || self.peek_line() > block_end_line));
14149        if empty_list_ok {
14150            return Ok((
14151                block,
14152                Expr {
14153                    kind: ExprKind::List(vec![]),
14154                    line,
14155                },
14156                None,
14157            ));
14158        }
14159        let mut parts = vec![self.parse_assign_expr_stop_at_pipe()?];
14160        loop {
14161            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
14162                break;
14163            }
14164            if matches!(
14165                self.peek(),
14166                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
14167            ) {
14168                break;
14169            }
14170            if self.peek_is_postfix_stmt_modifier_keyword() {
14171                break;
14172            }
14173            if let Token::Ident(ref kw) = self.peek().clone() {
14174                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
14175                    self.advance();
14176                    self.expect(&Token::FatArrow)?;
14177                    let prog = self.parse_assign_expr_stop_at_pipe()?;
14178                    return Ok((block, merge_expr_list(parts), Some(prog)));
14179                }
14180            }
14181            parts.push(self.parse_assign_expr_stop_at_pipe()?);
14182        }
14183        Ok((block, merge_expr_list(parts), None))
14184    }
14185
14186    /// Parse fan/fan_cap arguments: optional count + block or blockless expression.
14187    fn parse_fan_count_and_block(
14188        &mut self,
14189        line: usize,
14190    ) -> StrykeResult<(Option<Box<Expr>>, Block)> {
14191        // `fan { BLOCK }` — no count
14192        if matches!(self.peek(), Token::LBrace) {
14193            let block = self.parse_block()?;
14194            return Ok((None, block));
14195        }
14196        let saved = self.pos;
14197        // Not a brace — first expr could be count or body
14198        let first = self.parse_postfix()?;
14199        if matches!(self.peek(), Token::LBrace) {
14200            // `fan COUNT { BLOCK }`
14201            let block = self.parse_block()?;
14202            Ok((Some(Box::new(first)), block))
14203        } else if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof)
14204            || (matches!(self.peek(), Token::Comma)
14205                && matches!(self.peek_at(1), Token::Ident(ref kw) if kw == "progress"))
14206        {
14207            // `fan EXPR;` — no count, first is the body
14208            let block = self.bareword_to_no_arg_block(first);
14209            Ok((None, block))
14210        } else if matches!(first.kind, ExprKind::Integer(_)) {
14211            // `fan COUNT EXPR` or `fan COUNT, EXPR` — integer count + body
14212            self.eat(&Token::Comma);
14213            let body = self.parse_fan_blockless_body(line)?;
14214            Ok((Some(Box::new(first)), body))
14215        } else {
14216            // Non-integer first (e.g. `$_`) followed by binary op (e.g. `* $_`)
14217            // — backtrack and re-parse as a full body expression.
14218            self.pos = saved;
14219            let body = self.parse_fan_blockless_body(line)?;
14220            Ok((None, body))
14221        }
14222    }
14223
14224    /// Parse a blockless fan/fan_cap body as a full expression (not just postfix).
14225    fn parse_fan_blockless_body(&mut self, line: usize) -> StrykeResult<Block> {
14226        if matches!(self.peek(), Token::LBrace) {
14227            return self.parse_block();
14228        }
14229        // Check for bareword (zero-arg sub call) terminated by ; } EOF , or pipe
14230        if let Token::Ident(ref name) = self.peek().clone() {
14231            if matches!(
14232                self.peek_at(1),
14233                Token::Comma | Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
14234            ) {
14235                let name = name.clone();
14236                self.advance();
14237                let body = Expr {
14238                    kind: ExprKind::FuncCall { name, args: vec![] },
14239                    line,
14240                };
14241                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
14242            }
14243        }
14244        // Full expression (handles `$_ * $_`, `$_ + 1`, etc.)
14245        let expr = self.parse_assign_expr_stop_at_pipe()?;
14246        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
14247    }
14248
14249    /// Wrap a parsed expression as a single-statement block, converting bare
14250    /// identifiers to zero-arg calls (`work` → `work()`).
14251    fn bareword_to_no_arg_block(&self, expr: Expr) -> Block {
14252        let line = expr.line;
14253        let body = match &expr.kind {
14254            ExprKind::Bareword(name) => Expr {
14255                kind: ExprKind::FuncCall {
14256                    name: name.clone(),
14257                    args: vec![],
14258                },
14259                line,
14260            },
14261            _ => expr,
14262        };
14263        vec![Statement::new(StmtKind::Expression(body), line)]
14264    }
14265
14266    /// Parse either a `{ BLOCK }` or a bare expression and wrap it as a synthetic block.
14267    ///
14268    /// When the next token is `{`, delegates to [`Self::parse_block`].
14269    /// Otherwise parses a single postfix expression and wraps it as a call
14270    /// with `$_` as argument (for barewords) or a plain expression statement:
14271    ///
14272    /// - Bareword `foo` → `{ foo($_) }`
14273    /// - Other expr     → `{ EXPR }`
14274    fn parse_block_or_bareword_block(&mut self) -> StrykeResult<Block> {
14275        if matches!(self.peek(), Token::LBrace) {
14276            return self.parse_block();
14277        }
14278        let line = self.peek_line();
14279        // A lone identifier followed by a list-terminator is a bare sub name:
14280        // `pmap double, @list` → block is `{ double($_) }`, rest is list.
14281        if let Token::Ident(ref name) = self.peek().clone() {
14282            if matches!(
14283                self.peek_at(1),
14284                Token::Comma | Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
14285            ) {
14286                let name = name.clone();
14287                self.advance();
14288                let body = Expr {
14289                    kind: ExprKind::FuncCall {
14290                        name,
14291                        args: vec![Expr {
14292                            kind: ExprKind::ScalarVar("_".to_string()),
14293                            line,
14294                        }],
14295                    },
14296                    line,
14297                };
14298                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
14299            }
14300        }
14301        // Not a simple bareword — parse as expression (e.g. `$_ * 2`, `uc $_`)
14302        let expr = self.parse_assign_expr_stop_at_pipe()?;
14303        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
14304    }
14305
14306    /// Like [`parse_block_or_bareword_block`] but for fan/timer/bench where the
14307    /// bare function takes no args (body runs stand-alone, not per-element).
14308    /// Only consumes a single bareword identifier — does NOT let `parse_primary`
14309    /// greedily swallow subsequent tokens as function arguments.
14310    fn parse_block_or_bareword_block_no_args(&mut self) -> StrykeResult<Block> {
14311        if matches!(self.peek(), Token::LBrace) {
14312            return self.parse_block();
14313        }
14314        let line = self.peek_line();
14315        if let Token::Ident(ref name) = self.peek().clone() {
14316            if matches!(
14317                self.peek_at(1),
14318                Token::Comma
14319                    | Token::Semicolon
14320                    | Token::RBrace
14321                    | Token::Eof
14322                    | Token::PipeForward
14323                    | Token::Integer(_)
14324            ) {
14325                let name = name.clone();
14326                self.advance();
14327                let body = Expr {
14328                    kind: ExprKind::FuncCall { name, args: vec![] },
14329                    line,
14330                };
14331                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
14332            }
14333        }
14334        let expr = self.parse_postfix()?;
14335        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
14336    }
14337
14338    /// Returns true if `name` is a Perl keyword/builtin that should NOT be
14339    /// treated as a bare sub name (e.g. inside `sort`).
14340    /// True for any bareword the parser treats as a known builtin / keyword —
14341    /// Perl 5 core *or* a stryke extension. Used to suppress "call as user
14342    /// sub" interpretations (e.g. `sort my_cmp @list` only treats `my_cmp`
14343    /// as a comparator name if it *isn't* a known bareword). Previously named
14344    /// `is_perl_keyword`, which was misleading.
14345    fn is_known_bareword(name: &str) -> bool {
14346        Self::is_perl5_core(name) || Self::stryke_extension_name(name).is_some()
14347    }
14348
14349    /// True iff `name` appears as any spelling (primary *or* alias) in a
14350    /// `try_builtin` match arm. Picks up the ~300 aliases that don't show
14351    /// up in the parser-level keyword lists but are still callable at
14352    /// runtime — so `map { tj }` can default to `tj($_)` the same way
14353    /// `map { to_json }` does.
14354    fn is_try_builtin_name(name: &str) -> bool {
14355        crate::builtins::BUILTIN_ARMS
14356            .iter()
14357            .any(|arm| arm.contains(&name))
14358    }
14359
14360    /// True iff `name` is a Perl 5 core keyword/builtin (as shipped in stock
14361    /// `perl`). Extensions (`pmap`, `fan`, `timer`, …) are *not* included
14362    /// here — those live in `stryke_extension_name`. `%stryke::perl_compats`
14363    /// is derived from this list by `build.rs`.
14364    fn is_perl5_core(name: &str) -> bool {
14365        matches!(
14366            name,
14367            // ── array / list ────────────────────────────────────────────
14368            "map" | "grep" | "sort" | "reverse" | "join" | "split"
14369            | "push" | "pop" | "shift" | "unshift" | "splice"
14370            | "splice_last" | "splice1" | "spl_last"
14371            | "pack" | "unpack"
14372            | "unpack_first" | "unpack1" | "up1"
14373            // ── hash ────────────────────────────────────────────────────
14374            | "keys" | "values" | "each"
14375            // ── string ──────────────────────────────────────────────────
14376            | "chomp" | "chop" | "chr" | "ord" | "hex" | "oct"
14377            | "lc" | "uc" | "lcfirst" | "ucfirst"
14378            | "length" | "substr" | "index" | "rindex"
14379            | "sprintf" | "printf" | "print" | "say"
14380            | "pos" | "quotemeta" | "study"
14381            // ── numeric ─────────────────────────────────────────────────
14382            | "abs" | "int" | "sqrt" | "sin" | "cos" | "atan2"
14383            | "exp" | "log" | "rand" | "srand"
14384            // ── time ────────────────────────────────────────────────────
14385            | "time" | "localtime" | "gmtime"
14386            // ── type / reflection ───────────────────────────────────────
14387            | "defined" | "undef" | "ref" | "scalar" | "wantarray"
14388            | "caller" | "delete" | "exists" | "bless" | "prototype"
14389            | "tie" | "untie" | "tied"
14390            // ── io ──────────────────────────────────────────────────────
14391            | "open" | "close" | "read" | "readline" | "write" | "seek" | "tell"
14392            | "eof" | "binmode" | "getc" | "fileno" | "truncate"
14393            | "format" | "formline" | "select" | "vec"
14394            | "sysopen" | "sysread" | "sysseek" | "syswrite"
14395            // ── filesystem ──────────────────────────────────────────────
14396            | "stat" | "lstat" | "rename" | "unlink" | "utime"
14397            | "mkdir" | "rmdir" | "chdir" | "chmod" | "chown"
14398            | "glob" | "opendir" | "readdir" | "closedir"
14399            | "link" | "readlink" | "symlink"
14400            // ── ipc ─────────────────────────────────────────────────────
14401            | "fcntl" | "flock" | "ioctl" | "pipe" | "dbmopen" | "dbmclose"
14402            // ── sysv ipc ────────────────────────────────────────────────
14403            | "msgctl" | "msgget" | "msgrcv" | "msgsnd"
14404            | "semctl" | "semget" | "semop"
14405            | "shmctl" | "shmget" | "shmread" | "shmwrite"
14406            // ── process / system ────────────────────────────────────────
14407            | "system" | "exec" | "exit" | "die" | "warn" | "dump"
14408            | "fork" | "wait" | "waitpid" | "kill" | "syscall" | "alarm" | "sleep"
14409            | "chroot" | "times" | "umask" | "reset"
14410            | "getpgrp" | "setpgrp" | "getppid"
14411            | "getpriority" | "setpriority"
14412            // ── socket ──────────────────────────────────────────────────
14413            | "socket" | "socketpair" | "connect" | "listen" | "accept" | "shutdown"
14414            | "send" | "recv" | "bind" | "setsockopt" | "getsockopt"
14415            | "getpeername" | "getsockname"
14416            // ── posix metadata ──────────────────────────────────────────
14417            | "getpwnam" | "getpwuid" | "getpwent" | "setpwent"
14418            | "getgrnam" | "getgrgid" | "getgrent" | "setgrent"
14419            | "getlogin"
14420            | "gethostbyname" | "gethostbyaddr" | "gethostent"
14421            | "getnetbyname" | "getnetent"
14422            | "getprotobyname" | "getprotoent"
14423            | "getservbyname" | "getservent"
14424            | "sethostent" | "setnetent" | "setprotoent" | "setservent"
14425            | "endpwent" | "endgrent"
14426            | "endhostent" | "endnetent" | "endprotoent" | "endservent"
14427            // ── control flow ────────────────────────────────────────────
14428            | "return" | "do" | "eval" | "require"
14429            // NB: `var` / `val` are stryke declarator keywords (surface
14430            // aliases for `my` / `const my`), NOT Perl 5 core — they're
14431            // handled in `RESERVED_FUNCTION_NAMES` for the fn-name rejection
14432            // path with the same "reserved word" message as `my`/`our`/`sub`
14433            // get. Keeping them out of `is_perl5_core` also keeps them out
14434            // of `is_known_bareword`, which is the right call: var/val are
14435            // not callable barewords (`map { val }` should not be promoted
14436            // to `val($_)`).
14437            | "my" | "our" | "local" | "use" | "import" | "no"
14438            | "sub" | "if" | "unless" | "while" | "until"
14439            | "for" | "foreach" | "last" | "next" | "redo" | "goto"
14440            | "not" | "and" | "or"
14441            // ── quoting ─────────────────────────────────────────────────
14442            | "qw" | "qq" | "q"
14443            // ── phase blocks ────────────────────────────────────────────
14444            | "BEGIN" | "END"
14445        )
14446    }
14447
14448    /// If `name` is a stryke-only extension keyword/builtin, return it; else `None`.
14449    /// Used by `--compat` to reject extensions at parse time.
14450    fn stryke_extension_name(name: &str) -> Option<&str> {
14451        match name {
14452            // ── persistence / introspection ───────────────────────────────
14453            // ExprKind-special builtins (parsed as Burp / God / Swallow /
14454            // Ingest variants, not routed through try_builtin dispatch).
14455            // Registered HERE so the reflection registry (%b, %all, %c)
14456            // sees them via build.rs's category extractor.
14457            // (User bug 2026-05-27: "burp doesnt show up in %all" —
14458            // these names lived only in is_reserved_special_var_name
14459            // (shadow protection) and never reached the categorizer.
14460            // Pinned by reflection::exprkind_special_builtins_present_in_
14461            // reflection_registry. Section label kept under 40 chars
14462            // per build.rs:parse_section_header's length cap.)
14463            | "burp" | "god" | "swallow" | "ingest"
14464            // ── core value aliases ─────────────────────────────────────────
14465            // `null` → `undef` (JS/SQL spelling), normalized in
14466            // parse_named_expr; listed here so the reflection registry and
14467            // --compat gating see it.
14468            | "null"
14469            // ── distributed / congregation ─────────────────────────────────
14470            // Scriptable distributed-compute primitives. Wrap the existing
14471            // controller.rs / agent.rs TCP+bincode infrastructure with a
14472            // scatter-gather API. See builtins.rs::builtin_congregation /
14473            // builtin_pray / builtin_annex and docs/killer-features-
14474            // brainstorm.md "Scriptable Master/Slave" for the full design.
14475            | "congregation" | "ordain" | "muster" | "pray" | "annex"
14476            | "harvest" | "excommunicate" | "smite" | "bestow"
14477            | "enshrine" | "exhume" | "smother" | "amen"
14478            | "anoint" | "welcome" | "pilgrimage" | "bow"
14479            | "lick" | "peruse"
14480            | "chant" | "profess" | "apostatize" | "cathedral" | "cloister"
14481            | "recant" | "martyr" | "resurrect" | "divine" | "interrogate"
14482            // ── numerical stability + modern IDs ───────────────────────────
14483            | "ulid" | "is_ulid" | "ulid_timestamp"
14484            | "kahan_sum" | "welford_mean" | "welford_variance"
14485            | "welford_stddev" | "welford_pop_variance"
14486            // ── Shell-like REPL (Tier S) ───────────────────────────────────
14487            | "clear" | "cls" | "whoami" | "groups"
14488            | "pushd" | "popd" | "dir_stack"
14489            | "history" | "repl_alias" | "repl_unalias" | "set_alias" | "unset_alias"
14490            | "term_size" | "term_width" | "term_height"
14491            | "set_title" | "beep" | "ring_bell" | "man" | "manpage"
14492            | "run" | "exec_script" | "source" | "src"
14493            // ── Shell-like REPL (Tier A) ───────────────────────────────────
14494            | "rm" | "mktemp" | "mktempdir" | "whereis"
14495            | "nice" | "renice"
14496            | "tree" | "comm" | "column" | "xargs"
14497            | "openurl" | "xdg_open"
14498            | "curl_get" | "curl_post"
14499            | "iconv" | "strftime"
14500            | "tac" | "rev_lines"
14501            | "tty_raw" | "tty_cooked"
14502            // ── probabilistic data structures ──────────────────────────────
14503            | "bloom_filter" | "bloom_add" | "bloom_contains" | "bloom_len"
14504            | "bloom_clear" | "bloom_merge" | "bloom_fpr" | "bloom_bits"
14505            | "bloom_serialize" | "bloom_deserialize"
14506            | "hll" | "hyperloglog" | "hll_add" | "hll_count" | "hll_merge"
14507            | "hll_clear" | "hll_precision" | "hll_serialize" | "hll_deserialize"
14508            | "cms" | "count_min_sketch" | "cms_add" | "cms_count" | "cms_query"
14509            | "cms_merge" | "cms_clear" | "cms_serialize" | "cms_deserialize"
14510            | "topk" | "top_k_sketch" | "topk_add" | "topk_heavies" | "topk_count"
14511            | "topk_size" | "topk_merge" | "topk_clear"
14512            | "topk_serialize" | "topk_deserialize"
14513            | "t_digest" | "tdg" | "tdigest" | "td_add" | "td_quantile" | "td_count"
14514            | "td_min" | "td_max" | "td_sum" | "td_mean" | "td_merge" | "td_clear"
14515            | "td_serialize" | "td_deserialize"
14516            | "roaring" | "roaring_bitmap" | "rbm" | "rb_add" | "rb_remove" | "rb_contains"
14517            | "rb_len" | "rb_min" | "rb_max" | "rb_to_array" | "rb_rank"
14518            | "rb_or" | "rb_and" | "rb_xor" | "rb_andnot" | "rb_clear"
14519            | "rb_serialize" | "rb_deserialize"
14520            // ── Rate limiters / hash ring / LSH / trees / diff ────────────
14521            | "token_bucket" | "leaky_bucket" | "rl_try_take" | "rl_available"
14522            | "hash_ring" | "consistent_hash" | "hr_add" | "hr_remove" | "hr_get" | "hr_nodes"
14523            | "simhash" | "sh_add" | "sh_digest" | "sh_similarity"
14524            | "minhash" | "mh_add" | "mh_jaccard" | "mh_merge"
14525            | "interval_tree" | "it_insert" | "it_query_point" | "it_query_range"
14526            | "it_remove" | "it_len"
14527            | "bk_tree" | "bk_insert" | "bk_query" | "bk_len"
14528            | "rope" | "rope_insert" | "rope_delete" | "rope_substring"
14529            | "rope_to_string" | "rope_len"
14530            | "myers_diff" | "patience_diff"
14531            // ── rkyv KV store ──────────────────────────────────────────────
14532            | "kv_open" | "kv_new" | "kv_put" | "kv_set" | "kv_get"
14533            | "kv_del" | "kv_delete" | "kv_remove" | "kv_exists" | "kv_has"
14534            | "kv_keys" | "kv_scan" | "kv_len" | "kv_count" | "kv_size"
14535            | "kv_commit" | "kv_flush" | "kv_batch" | "kv_close"
14536            | "kv_stats" | "kv_info"
14537            // ── aop ────────────────────────────────────────────────────────
14538            | "proceed" | "intercept_list" | "intercept_remove" | "intercept_clear"
14539            // ── parallel ────────────────────────────────────────────────────
14540            | "pmap" | "pmap_on" | "pflat_map" | "pflat_map_on" | "pmap_chunked"
14541            | "pgrep" | "pfor" | "pforeach" | "ploop" | "pwhile" | "psort" | "preduce" | "preduce_init" | "pmap_reduce"
14542            | "pcache" | "pchannel" | "pselect" | "puniq" | "pfirst" | "pany"
14543            | "fan" | "fan_cap" | "par_lines" | "par_walk" | "par_sed"
14544            | "par_find_files" | "par_line_count" | "pwatch" | "par_pipeline_stream"
14545            | "glob_par" | "ppool" | "barrier" | "pipeline" | "cluster"
14546            | "pmaps" | "pflat_maps" | "pgreps"
14547            // ── controller / agent (script-level mode entry) ────────────────
14548            | "controller" | "agent"
14549            // ── provenance / lineage ───────────────────────────────────────
14550            | "mark" | "provenance" | "unmark"
14551            // ── network probes (TCP knock, UDP send, active probes) ───────
14552            | "kick" | "udp_send"
14553            | "tcp_probe" | "tcp_banner" | "whois_query"
14554            // ── P2P NAT traversal (persistent socket pool + STUN + punch) ──
14555            | "udp_open" | "udp_send_to" | "udp_recv" | "udp_recv_from"
14556            | "udp_close" | "stun" | "stun_classify" | "punch"
14557            // ── TURN relay fallback (RFC 8656) for symmetric NATs ──────────
14558            | "turn_allocate" | "turn_permission" | "turn_send"
14559            | "turn_recv" | "turn_refresh"
14560            // ── ipc / shared memory teleport ───────────────────────────────
14561            | "teleport" | "arrive"
14562            // ── peer-pair keepalive ────────────────────────────────────────
14563            | "turnbuckle" | "tb_alive" | "tb_ping" | "tb_close"
14564            // ── slow-trickle emitter ───────────────────────────────────────
14565            | "weep"
14566            // ── functional / iterator ───────────────────────────────────────
14567            | "fore" | "e" | "ep" | "flat_map" | "flat_maps" | "maps" | "filter" | "fi" | "find_all" | "reduce" | "fold"
14568            | "inject" | "collect" | "uniq" | "distinct" | "any" | "all" | "none"
14569            | "first" | "detect" | "find" | "find_index" | "firstidx" | "first_index"
14570            | "compact" | "concat" | "chain" | "reject" | "grepv" | "flatten" | "set"
14571            | "min_by" | "max_by" | "sort_by" | "tally"
14572            | "each_with_index" | "count" | "cnt" |"len" | "group_by" | "chunk_by"
14573            | "zip" | "chunk" | "chunked" | "sliding_window" | "windowed"
14574            | "enumerate" | "with_index" | "shuffle" | "shuffled"| "heap"
14575            | "take_while" | "drop_while" | "skip_while" | "tap" | "peek" | "partition"
14576            | "zip_with" | "count_by" | "skip" | "first_or"
14577            // ── cli / argv ──────────────────────────────────────────────────
14578            | "getopts"
14579            // ── pipeline / string helpers ───────────────────────────────────
14580            | "input" | "lines" | "words" | "chars" | "cindex" | "crindex"
14581            | "digits" | "letters" | "letters_uc" | "letters_lc"
14582            | "punctuation" | "punct"
14583            | "sentences" | "sents"
14584            | "paragraphs" | "paras" | "sections" | "sects"
14585            | "numbers" | "nums" | "graphemes" | "grs" | "columns" | "cols"
14586            | "trim" | "avg" | "stddev"
14587            | "squared" | "sq" | "square" | "cubed" | "cb" | "cube" | "expt" | "pow" | "pw"
14588            | "normalize" | "snake_case" | "camel_case" | "kebab_case"
14589            | "frequencies" | "freq" | "pfrequencies" | "pfreq"
14590            | "template" | "deburr"
14591            | "shell_quote" | "shell_split" | "shellwords"
14592            | "from_jsonl" | "to_jsonl" | "from_ini" | "to_ini"
14593            | "base64url_encode" | "base64url_decode"
14594            | "interleave" | "ddump" | "stringify" | "str" | "top"
14595            | "to_json" | "to_csv" | "to_toml" | "to_yaml" | "to_xml"
14596            | "to_html" | "to_markdown" | "to_table" | "xopen"
14597            | "from_json" | "from_csv" | "from_toml" | "from_yaml" | "from_xml"
14598            | "clip" | "clipboard" | "paste" | "pbcopy" | "pbpaste" | "preview"
14599            | "sparkline" | "spark" | "bar_chart" | "bars" | "flame" | "flamechart"
14600            | "histo" | "gauge" | "spinner" | "spinner_start" | "spinner_stop"
14601            | "to_hash" | "to_set"
14602            | "to_file" | "read_lines" | "append_file" | "write_json" | "read_json"
14603            | "tempfile" | "tempdir" | "list_count" | "list_size" | "size"
14604            | "clamp" | "grep_v" | "select_keys" | "pluck" | "glob_match" | "which_all"
14605            | "dedup" | "nth" | "tail" | "take" | "drop" | "tee" | "range"
14606            | "inc" | "dec" | "elapsed"
14607            // ── filesystem extensions ───────────────────────────────────────
14608            | "files" | "filesf" | "f" | "fr" | "dirs" | "d" | "dr" | "sym_links"
14609            | "sockets" | "pipes" | "block_devices" | "char_devices" | "exe" | "executables"
14610            | "basename" | "dirname" | "fileparse" | "realpath" | "canonpath"
14611            | "copy" | "cp" | "move" | "spurt" | "spit" | "read_bytes" | "which"
14612            | "getcwd" | "cd" | "ls" | "touch" | "gethostname" | "uname"
14613            | "file" | "xxd"
14614            // ── data / network ──────────────────────────────────────────────
14615            | "csv_read" | "csv_write" | "dataframe" | "sqlite"
14616            | "fetch" | "fetch_json" | "fetch_async" | "fetch_async_json"
14617            | "par_fetch" | "par_csv_read" | "par_pipeline"
14618            | "json_encode" | "json_decode" | "json_jq"
14619            | "http_request" | "serve" | "ssh"
14620            | "html_parse" | "css_select" | "xml_parse" | "xpath"
14621            | "smtp_send"
14622            | "net_interfaces" | "net_ipv4" | "net_ipv6" | "net_mac"
14623            | "net_public_ip" | "net_dns" | "net_reverse_dns"
14624            | "net_ping" | "net_port_open" | "net_ports_scan"
14625            | "net_latency" | "net_download" | "net_headers"
14626            | "net_dns_servers" | "net_gateway" | "net_whois" | "net_hostname"
14627            // ── git ─────────────────────────────────────────────────────────
14628            | "git_log" | "git_status" | "git_diff" | "git_branches"
14629            | "git_tags" | "git_blame" | "git_authors" | "git_files"
14630            | "git_show" | "git_root"
14631            // ── github / gh REST API ─────────────────────────────────────────
14632            | "gh_get" | "gh_user" | "gh_org" | "gh_followers" | "gh_following"
14633            | "gh_repo" | "gh_repos" | "gh_org_repos" | "gh_starred"
14634            | "gh_gists" | "gh_gist"
14635            | "gh_issues" | "gh_prs" | "gh_commits" | "gh_branches"
14636            | "gh_tags" | "gh_releases" | "gh_contributors" | "gh_forks"
14637            | "gh_stargazers" | "gh_topics" | "gh_languages"
14638            | "gh_readme" | "gh_workflows" | "gh_runs"
14639            | "gh_search_repos" | "gh_search_users" | "gh_search_code" | "gh_search_issues"
14640            | "gh_rate_limit" | "gh_meta" | "gh_emojis" | "gh_zen"
14641            // ── audio / media ───────────────────────────────────────────────
14642            | "audio_convert" | "audio_info" | "id3_read" | "id3_write"
14643            // ── pdf ─────────────────────────────────────────────────────────
14644            | "to_pdf" | "pdf_text" | "pdf_pages"
14645            // ── serialization (stryke-only encoders) ────────────────────────
14646            | "toml_encode" | "toml_decode"
14647            | "yaml_encode" | "yaml_decode"
14648            | "xml_encode" | "xml_decode"
14649            // ── crypto / encoding ───────────────────────────────────────────
14650            | "md5" | "sha1" | "sha224" | "sha256" | "sha384" | "sha512"
14651            | "sha3_256" | "s3_256" | "sha3_512" | "s3_512"
14652            | "shake128" | "shake256"
14653            | "hmac_sha256" | "hmac_sha1" | "hmac_sha384" | "hmac_sha512" | "hmac_md5"
14654            | "uuid" | "crc32"
14655            | "blake2b" | "b2b" | "blake2s" | "b2s" | "blake3" | "b3"
14656            | "ripemd160" | "rmd160" | "md4"
14657            | "xxh32" | "xxhash32" | "xxh64" | "xxhash64" | "xxh3" | "xxhash3" | "xxh3_128" | "xxhash3_128"
14658            | "murmur3" | "murmur3_32" | "murmur3_128"
14659            | "siphash" | "siphash_keyed"
14660            | "hkdf_sha256" | "hkdf" | "hkdf_sha512"
14661            | "poly1305" | "poly1305_mac"
14662            | "base32_encode" | "b32e" | "base32_decode" | "b32d"
14663            | "base58_encode" | "b58e" | "base58_decode" | "b58d"
14664            | "totp" | "totp_generate" | "totp_verify" | "hotp" | "hotp_generate"
14665            | "aes_cbc_encrypt" | "aes_cbc_enc" | "aes_cbc_decrypt" | "aes_cbc_dec"
14666            | "blowfish_encrypt" | "bf_enc" | "blowfish_decrypt" | "bf_dec"
14667            | "des3_encrypt" | "3des_enc" | "tdes_enc" | "des3_decrypt" | "3des_dec" | "tdes_dec"
14668            | "twofish_encrypt" | "tf_enc" | "twofish_decrypt" | "tf_dec"
14669            | "camellia_encrypt" | "cam_enc" | "camellia_decrypt" | "cam_dec"
14670            | "cast5_encrypt" | "cast5_enc" | "cast5_decrypt" | "cast5_dec"
14671            | "salsa20" | "salsa20_encrypt" | "salsa20_decrypt"
14672            | "xsalsa20" | "xsalsa20_encrypt" | "xsalsa20_decrypt"
14673            | "secretbox" | "secretbox_seal" | "secretbox_open"
14674            | "nacl_box_keygen" | "box_keygen" | "nacl_box" | "nacl_box_seal" | "box_seal"
14675            | "nacl_box_open" | "box_open"
14676            | "qr_ascii" | "qr" | "qr_png" | "qr_svg"
14677            | "barcode_code128" | "code128" | "barcode_code39" | "code39"
14678            | "barcode_ean13" | "ean13" | "barcode_svg"
14679            | "argon2_hash" | "argon2" | "argon2_verify"
14680            | "bcrypt_hash" | "bcrypt" | "bcrypt_verify"
14681            | "scrypt_hash" | "scrypt" | "scrypt_verify"
14682            | "pbkdf2" | "pbkdf2_derive"
14683            | "random_bytes" | "randbytes" | "random_bytes_hex" | "randhex"
14684            | "aes_encrypt" | "aes_enc" | "aes_decrypt" | "aes_dec"
14685            | "chacha_encrypt" | "chacha_enc" | "chacha_decrypt" | "chacha_dec"
14686            | "rsa_keygen" | "rsa_encrypt" | "rsa_enc" | "rsa_decrypt" | "rsa_dec"
14687            | "rsa_encrypt_pkcs1" | "rsa_decrypt_pkcs1" | "rsa_sign" | "rsa_verify"
14688            | "ecdsa_p256_keygen" | "p256_keygen" | "ecdsa_p256_sign" | "p256_sign"
14689            | "ecdsa_p256_verify" | "p256_verify"
14690            | "ecdsa_p384_keygen" | "p384_keygen" | "ecdsa_p384_sign" | "p384_sign"
14691            | "ecdsa_p384_verify" | "p384_verify"
14692            | "ecdsa_secp256k1_keygen" | "secp256k1_keygen"
14693            | "ecdsa_secp256k1_sign" | "secp256k1_sign"
14694            | "ecdsa_secp256k1_verify" | "secp256k1_verify"
14695            | "ecdh_p256" | "p256_dh" | "ecdh_p384" | "p384_dh"
14696            | "ed25519_keygen" | "ed_keygen" | "ed25519_sign" | "ed_sign"
14697            | "ed25519_verify" | "ed_verify"
14698            | "x25519_keygen" | "x_keygen" | "x25519_dh" | "x_dh"
14699            | "base64_encode" | "base64_decode"
14700            | "hex_encode" | "hex_decode"
14701            // ── steganography ───────────────────────────────────────────────
14702            | "hide" | "reveal" | "hide_capacity"
14703            | "url_encode" | "url_decode"
14704            | "gzip" | "gunzip" | "gz" | "ugz" | "zstd" | "zstd_decode" | "zst" | "uzst"
14705            | "brotli" | "br" | "brotli_decode" | "ubr"
14706            | "xz" | "lzma" | "xz_decode" | "unxz" | "unlzma"
14707            | "bzip2" | "bz2" | "bzip2_decode" | "bunzip2" | "ubz2"
14708            | "lz4" | "lz4_decode" | "unlz4"
14709            | "snappy" | "snp" | "snappy_decode" | "unsnappy"
14710            | "lzw" | "lzw_decode" | "unlzw"
14711            | "tar_create" | "tar" | "tar_extract" | "untar" | "tar_list"
14712            | "tar_gz_create" | "tgz" | "tar_gz_extract" | "untgz"
14713            | "zip_create" | "zip_archive" | "zip_extract" | "unzip_archive" | "zip_list"
14714            // ── special math functions ────────────────────────────────────────
14715            | "erf" | "erfc" | "gamma" | "tgamma" | "lgamma" | "ln_gamma"
14716            | "digamma" | "psi" | "beta_fn" | "lbeta" | "ln_beta"
14717            | "betainc" | "beta_reg" | "gammainc" | "gamma_li"
14718            | "gammaincc" | "gamma_ui" | "gammainc_reg" | "gamma_lr"
14719            | "gammaincc_reg" | "gamma_ur"
14720            // ── date / time ─────────────────────────────────────────────────
14721            | "datetime_utc" | "datetime_now_tz" | "now"
14722            | "datetime_format_tz" | "datetime_add_seconds"
14723            | "datetime_from_epoch"
14724            | "datetime_parse_rfc3339" | "datetime_parse_local"
14725            | "datetime_strftime" | "strptime" | "time_ago" | "from_now"
14726            | "dateseq" | "dategrep" | "dateround" | "datesort"
14727            // ── jwt ─────────────────────────────────────────────────────────
14728            | "jwt_encode" | "jwt_decode" | "jwt_decode_unsafe"
14729            // ── logging ─────────────────────────────────────────────────────
14730            | "log_info" | "log_warn" | "log_error"
14731            | "log_debug" | "log_trace" | "log_json" | "log_level"
14732            // ── concurrency / timing ────────────────────────────────────────
14733            | "async" | "spawn" | "trace" | "timer" | "bench"
14734            | "eval_timeout" | "retry" | "rate_limit" | "every"
14735            | "gen" | "watch"
14736            // ── caching ────────────────────────────────────────────────────────
14737            | "cache_clear" | "cache_exists" | "cache_stats" | "cacheview"
14738            // ── testing framework ────────────────────────────────────────────
14739            | "assert_eq" | "assert_ne" | "assert_ok" | "assert_err"
14740            | "assert_true" | "assert_false"
14741            | "assert_gt" | "assert_lt" | "assert_ge" | "assert_le"
14742            | "assert_match" | "assert_contains" | "assert_near" | "assert_dies"
14743            | "test_run" | "run_tests" | "test_skip" | "skip_test" | "skip_assert"
14744            // ── system info ─────────────────────────────────────────────────
14745            | "mounts" | "du" | "du_tree" | "process_list"
14746            | "thread_count" | "pool_info" | "par_bench"
14747            | "perfview" | "pfv"
14748            | "docs" | "help" | "h"
14749            | "banner"
14750            // ── network / ip / cidr ─────────────────────────────────────────
14751            | "ip_parse" | "ip_is_valid" | "ip_version" | "ip_family"
14752            | "ip_to_int" | "int_to_ip" | "ip_to_bytes" | "bytes_to_ip"
14753            | "ip_to_bits" | "bits_to_ip"
14754            | "ip_is_private" | "ip_is_loopback" | "ip_is_multicast"
14755            | "ip_is_link_local" | "ip_is_unspecified" | "ip_is_global"
14756            | "ip_is_documentation" | "ip_is_benchmarking" | "ip_is_shared"
14757            | "ip_is_reserved" | "ip_is_broadcast"
14758            | "ip_canonical" | "ip_reverse" | "ip_arpa"
14759            | "ip_compare" | "ip_sort" | "ip_random"
14760            | "ipv4_parse" | "ipv4_is_valid" | "ipv4_classful_class"
14761            | "ipv6_parse" | "ipv6_is_valid" | "ipv6_canonical"
14762            | "ipv6_expand" | "ipv6_compress" | "ipv6_strip_zone" | "ipv6_zone_id"
14763            | "ipv6_link_local" | "ipv6_unique_local" | "ipv6_solicited_node"
14764            | "ipv6_eui64_addr" | "ipv6_link_local_from_mac"
14765            | "ipv4_to_ipv6_mapped" | "ipv4_to_ipv6_6to4" | "ipv6_to_ipv4_compat"
14766            | "ipv6_is_6to4" | "ipv6_6to4_extract"
14767            | "ipv6_is_teredo" | "ipv6_teredo_extract"
14768            | "ipv6_is_isatap" | "ipv6_isatap_extract"
14769            | "cidr_parse" | "cidr_valid_subnet" | "cidr_format"
14770            | "cidr_prefix_len" | "cidr_class"
14771            | "cidr_network" | "cidr_broadcast" | "cidr_netmask"
14772            | "cidr_hostmask" | "cidr_wildcard"
14773            | "cidr_to_netmask" | "netmask_to_prefix"
14774            | "cidr_first_host" | "cidr_last_host" | "cidr_num_hosts"
14775            | "cidr_size" | "cidr_hosts" | "cidr_iterate"
14776            | "cidr_contains" | "ip_in_cidr" | "ip_in_subnet"
14777            | "cidr_subnet" | "cidr_supernet" | "cidr_subnets" | "cidr_split"
14778            | "cidr_overlaps" | "cidr_aggregate" | "cidr_summarize"
14779            | "cidr_intersection" | "cidr_difference" | "cidr_union"
14780            | "cidr_minimum_covering" | "cidr_is_aggregable"
14781            | "cidr_next" | "cidr_prev" | "cidr_distance"
14782            | "cidr_random_ip" | "ip_random_in_cidr"
14783            | "cidr_compare" | "cidr_sort"
14784            | "mac_parse" | "mac_is_valid" | "mac_normalize" | "mac_format"
14785            | "mac_to_int" | "int_to_mac" | "mac_to_bytes" | "bytes_to_mac"
14786            | "mac_oui" | "mac_vendor_lookup" | "mac_lookup_vendor"
14787            | "mac_is_unicast" | "mac_is_multicast" | "mac_is_broadcast"
14788            | "mac_is_locally_administered" | "mac_is_universally_administered"
14789            | "mac_random" | "mac_random_local" | "mac_compare"
14790            | "eui48_to_eui64" | "eui64_to_eui48" | "eui64_from_mac"
14791            | "port_name" | "port_is_well_known" | "port_is_assigned"
14792            | "port_is_registered" | "port_is_ephemeral" | "port_is_dynamic"
14793            | "port_to_service" | "port_service_lookup"
14794            | "port_parse_range" | "port_random_ephemeral"
14795            | "ws_handshake_key" | "ws_handshake_accept"
14796            | "ws_mask" | "ws_unmask" | "ws_frame_encode" | "ws_frame_decode"
14797            | "ws_close_frame"
14798            | "cookie_parse" | "cookie_format"
14799            | "cookie_jar_new" | "cookie_jar_add" | "cookie_jar_get"
14800            | "cookie_is_session" | "cookie_is_expired"
14801            | "cookie_domain_matches" | "cookie_path_matches"
14802            | "cookie_set_max_age"
14803            | "http_method_is_idempotent" | "http_method_is_safe"
14804            | "http_method_has_body"
14805            | "http_status_class"
14806            | "http_status_is_informational" | "http_status_is_success"
14807            | "http_status_is_redirect" | "http_status_is_client_error"
14808            | "http_status_is_server_error"
14809            | "http_status_text" | "http_date_parse" | "http_date_format"
14810            | "mime_type_for_extension" | "mime_extension_for_type"
14811            | "mime_is_text" | "mime_is_image" | "mime_is_audio"
14812            | "mime_is_video" | "mime_is_application"
14813            | "bandwidth_format" | "bandwidth_parse"
14814            | "latency_ms" | "packet_loss" | "jitter_ms"
14815            | "rtt_min" | "rtt_max" | "rtt_avg"
14816            // ── validation / input checks ──
14817            | "is_alpha_only" | "is_alphanumeric_only" | "is_numeric_only"
14818            | "is_ascii_only" | "is_printable_ascii" | "is_utf8"
14819            | "is_lowercase" | "is_uppercase" | "is_titlecase"
14820            | "is_palindrome_str"
14821            | "is_hex" | "is_octal" | "is_binary" | "is_base32"
14822            | "is_md5_hash" | "is_sha1_hash" | "is_sha256_hash"
14823            | "is_ipv6" | "is_cidr" | "is_mac"
14824            | "is_url_http" | "is_url_https"
14825            | "is_uuid_v4" | "is_uuid_v7"
14826            | "is_jwt" | "is_email_strict"
14827            | "luhn_digit" | "is_imei" | "is_imsi"
14828            | "is_vin" | "vin_decode"
14829            | "is_ean13" | "is_upc"
14830            | "is_isbn" | "isbn10_to_isbn13" | "isbn13_to_isbn10"
14831            | "iban_format" | "iban_country" | "is_bic" | "is_swift"
14832            | "is_phone" | "is_phone_e164"
14833            | "is_zip_us" | "is_zip_plus4" | "is_postal_code" | "is_ssn_us"
14834            | "semver_compare" | "semver_satisfies"
14835            | "semver_increment_major" | "semver_increment_minor" | "semver_increment_patch"
14836            // ── math / number theory extras ──
14837            | "extended_gcd" | "modinverse" | "modpow" | "modular_sqrt"
14838            | "stirling_1" | "stirling_2" | "catalan_number" | "lucas_n"
14839            | "prime_count_below" | "divisor_count" | "divisor_sum" | "sigma_divisors"
14840            | "sum_digits" | "product_digits" | "collatz_steps"
14841            | "hyperoperation" | "busy_beaver"
14842            | "quadratic_residue" | "is_quadratic_residue"
14843            | "discrete_log" | "order_modulo" | "square_free"
14844            | "perfect_number" | "abundant" | "deficient"
14845            // ── random / sampling extras ──
14846            | "random_bernoulli" | "random_normal" | "random_lognormal"
14847            | "random_exponential" | "random_poisson" | "random_gamma" | "random_beta"
14848            | "random_alphanumeric" | "random_alphabetic" | "random_password"
14849            | "random_choices_weighted"
14850            | "sample_weighted_unique" | "reservoir_sample_weighted"
14851            | "seeded_rng" | "save_random_state" | "restore_random_state"
14852            // ── complex / geom / color / trig ──
14853            | "complex_new" | "complex_real" | "complex_imag"
14854            | "complex_polar" | "complex_from_polar"
14855            | "complex_magnitude" | "complex_abs" | "complex_phase" | "complex_angle"
14856            | "complex_conjugate"
14857            | "complex_add" | "complex_sub" | "complex_mul" | "complex_div"
14858            | "complex_pow" | "complex_sqrt" | "complex_exp" | "complex_log"
14859            | "complex_sin" | "complex_cos" | "complex_tan"
14860            | "complex_sinh" | "complex_cosh" | "complex_tanh"
14861            | "complex_equal"
14862            | "point_angle"
14863            | "line_intersect" | "line_segment_intersect" | "line_distance_point"
14864            | "polygon_signed_area" | "polygon_orientation" | "polygon_reverse"
14865            | "polygon_contains_point" | "polygon_convex"
14866            | "polygon_simplify_dp" | "polygon_convex_hull_2d"
14867            | "triangle_area" | "triangle_centroid"
14868            | "triangle_circumcircle" | "triangle_incircle"
14869            | "triangle_contains_point"
14870            | "circle_circumference" | "circle_area"
14871            | "circle_intersects_line" | "circle_intersects_circle"
14872            | "rect_area" | "rect_perimeter" | "rect_intersect"
14873            | "rect_contains_point" | "rect_union"
14874            | "ellipse_area"
14875            | "sphere_surface_area" | "cylinder_surface_area"
14876            | "cone_surface_area" | "torus_surface_area"
14877            | "srgb_to_rgb" | "rgb_to_srgb"
14878            | "rgb_to_p3" | "p3_to_rgb"
14879            | "rgb_to_adobe_rgb" | "adobe_rgb_to_rgb"
14880            | "xyz_d65_to_d50" | "xyz_d50_to_d65"
14881            | "gamma_apply" | "gamma_remove"
14882            | "white_point_d65" | "white_point_d50"
14883            | "color_temperature_to_rgb" | "rgb_to_color_temperature"
14884            | "chromatic_adaptation"
14885            | "color_interpolate_rgb" | "color_interpolate_hsl"
14886            | "color_interpolate_lab" | "color_interpolate_oklab"
14887            | "color_blend_screen"
14888            | "atan2_deg" | "atan2_quadrant"
14889            | "polar_to_cartesian" | "cartesian_to_polar"
14890            | "spherical_to_cartesian" | "cartesian_to_spherical"
14891            | "cylindrical_to_cartesian" | "cartesian_to_cylindrical"
14892            | "versine_fn"
14893            // ── iterator + string-distance extras ──
14894            | "triples" | "n_tuples" | "peekable" | "runs" | "unique_by"
14895            | "multipeek" | "lookahead_n"
14896            | "sliding_average" | "sliding_sum" | "sliding_max" | "sliding_min"
14897            | "top_n_by" | "bottom_n_by" | "all_equal" | "take_n_random"
14898            | "unzip3" | "roundrobin" | "mode_iter" | "distinct_sample"
14899            | "ranked_choice" | "boyer_moore_majority"
14900            | "quickselect_nth" | "quickselect_median"
14901            | "top_k_min_heap" | "bottom_k_max_heap"
14902            | "unique_consecutive" | "exclude" | "exclude_first" | "exclude_last"
14903            | "weave_n" | "pad_left_n" | "pad_right_n"
14904            | "collect_into_string" | "collect_into_hashset" | "collect_into_btreeset"
14905            | "collect_into_hashmap" | "collect_into_btreemap"
14906            | "foldl1_iter" | "foldr1_iter"
14907            | "sort_by_cached_key"
14908            | "position_max" | "position_min" | "position_max_by" | "position_min_by"
14909            | "group_map"
14910            | "levenshtein_normalized" | "ratcliff_obershelp" | "match_rating"
14911            | "str_lcs" | "str_lcs_length" | "str_longest_common_substring"
14912            | "str_kmp" | "str_boyer_moore" | "str_rabin_karp"
14913            | "str_aho_corasick" | "str_z_array" | "str_suffix_array"
14914            | "str_rotations" | "str_compress_rle" | "str_decompress_rle"
14915            | "str_huffman_encode" | "str_huffman_decode"
14916            | "str_compress_lzss" | "str_decompress_lzss"
14917            | "str_isogram" | "fold_case"
14918            // ── extras ──
14919            | "bignum_new" | "bignum_from_str" | "bignum_to_str" | "bignum_to_int"
14920            | "bignum_add" | "bignum_sub" | "bignum_mul" | "bignum_div" | "bignum_mod"
14921            | "bignum_pow" | "bignum_modpow" | "bignum_gcd" | "bignum_lcm"
14922            | "bignum_factorial" | "bignum_sqrt" | "bignum_bit_length"
14923            | "bignum_set_bit" | "bignum_clear_bit" | "bignum_test_bit"
14924            | "bignum_and" | "bignum_or" | "bignum_xor" | "bignum_not"
14925            | "bignum_shl" | "bignum_shr" | "bignum_compare"
14926            | "bignum_negate" | "bignum_abs" | "bignum_sign"
14927            | "bignum_is_zero" | "bignum_is_negative" | "bignum_is_prime"
14928            | "bignum_random"
14929            | "gravity_constant" | "physics_apply_force" | "physics_apply_impulse"
14930            | "physics_collide_aabb" | "physics_collide_sphere"
14931            | "physics_raycast" | "physics_step"
14932            | "particle_emit" | "particle_update"
14933            | "vector2_new" | "vector2_add" | "vector2_sub" | "vector2_scale"
14934            | "vector2_dot" | "vector2_cross" | "vector2_length"
14935            | "vector2_normalize" | "vector2_distance" | "vector2_rotate"
14936            | "quaternion_new" | "quaternion_from_axis_angle"
14937            | "quaternion_multiply" | "quaternion_normalize" | "quaternion_to_matrix"
14938            | "freq_to_note" | "note_to_freq" | "midi_note_to_name"
14939            | "chord_notes" | "scale_notes" | "transpose_note"
14940            | "window_tukey" | "zero_crossing_rate" | "peak_db"
14941            | "audio_normalize" | "audio_fade_in" | "audio_fade_out"
14942            | "audio_to_mono" | "audio_to_stereo"
14943            | "biquad_lowpass" | "biquad_highpass" | "biquad_bandpass" | "biquad_notch"
14944            | "oscillator_sine" | "oscillator_square"
14945            | "oscillator_sawtooth" | "oscillator_triangle"
14946            | "adsr_envelope" | "ar_envelope" | "crossfade"
14947            | "fade_curve_linear" | "fade_curve_logarithmic" | "fade_curve_exponential"
14948            | "bbox_contains" | "bbox_union" | "bbox_intersect"
14949            | "bbox_center" | "bbox_area"
14950            | "mercator_unproject" | "geohash_precision"
14951            // ── extras ──
14952            | "jq_get" | "jq_set" | "jq_delete" | "jq_select"
14953            | "jq_keys_at" | "jq_values_at" | "jq_length_at"
14954            | "jq_type" | "jq_has" | "jq_paths" | "jq_leaf_paths"
14955            | "jq_walk" | "jq_map_values" | "jq_filter"
14956            | "jq_to_entries" | "jq_from_entries" | "jq_with_entries"
14957            | "jq_recurse" | "jq_min_by" | "jq_max_by"
14958            | "jq_sort_by" | "jq_group_by" | "jq_unique_by"
14959            | "jq_any" | "jq_all" | "jq_flatten"
14960            | "jq_index" | "jq_indices" | "jq_first" | "jq_last"
14961            | "jq_split_at" | "jq_chunks" | "jq_zip" | "jq_combinations"
14962            | "json_diff" | "json_patch" | "json_merge_patch"
14963            | "json_pointer_resolve" | "json_pointer_set"
14964 | "html_to_text" | "html_pretty" | "html_minify"
14965            | "html_sanitize" | "html_strip_tags" | "html_strip_scripts" | "html_strip_styles"
14966            | "html_extract_links" | "html_extract_images" | "html_extract_text"
14967            | "html_extract_meta" | "html_extract_title"
14968            | "html_extract_headings" | "html_extract_tables"
14969            | "html_inner_text" | "html_canonical_url"
14970            | "html_meta_charset" | "html_meta_keywords" | "html_meta_description"
14971            | "html_meta_og" | "html_meta_twitter"
14972            | "html_to_markdown" | "markdown_to_html" | "markdown_render"
14973 | "xml_pretty" | "xml_minify"
14974            | "xml_namespace" | "xml_text" | "xml_attrs"
14975            | "xml_children_by_tag" | "xml_root"
14976            | "xpath_select_one" | "xpath_attribute" | "xpath_text"
14977            | "xml_to_json" | "json_to_xml" | "xml_canonicalize"
14978            | "css_parse" | "css_minify" | "css_pretty"
14979            | "css_selector_parse" | "css_rule_extract" | "css_specificity"
14980            | "css_var_resolve" | "css_property_set" | "css_property_get"
14981            | "css_url_extract" | "css_import_extract" | "css_font_extract"
14982            | "selector_to_xpath" | "xpath_to_selector"
14983            // ── extras ──
14984            | "http_status_continue" | "http_status_switching_protocols"
14985            | "http_status_ok" | "http_status_created" | "http_status_accepted"
14986            | "http_status_no_content" | "http_status_partial_content"
14987            | "http_status_multiple_choices" | "http_status_moved_permanently"
14988            | "http_status_found" | "http_status_see_other" | "http_status_not_modified"
14989            | "http_status_temporary_redirect" | "http_status_permanent_redirect"
14990            | "http_status_bad_request" | "http_status_unauthorized"
14991            | "http_status_payment_required" | "http_status_forbidden"
14992            | "http_status_not_found" | "http_status_method_not_allowed"
14993            | "http_status_not_acceptable" | "http_status_conflict" | "http_status_gone"
14994            | "http_status_length_required" | "http_status_precondition_failed"
14995            | "http_status_payload_too_large" | "http_status_uri_too_long"
14996            | "http_status_unsupported_media_type" | "http_status_range_not_satisfiable"
14997            | "http_status_expectation_failed" | "http_status_im_a_teapot"
14998            | "http_status_unprocessable_entity" | "http_status_too_many_requests"
14999            | "http_status_internal_server_error" | "http_status_not_implemented"
15000            | "http_status_bad_gateway" | "http_status_service_unavailable"
15001            | "http_status_gateway_timeout" | "http_status_http_version_not_supported"
15002            | "http_method_get" | "http_method_post" | "http_method_put"
15003            | "http_method_delete" | "http_method_patch" | "http_method_head"
15004            | "http_method_options" | "http_method_trace" | "http_method_connect"
15005            | "dbeta" | "qbeta" | "rbeta" | "dcauchy" | "qcauchy" | "rcauchy"
15006            | "dexp" | "qexp" | "rexp" | "dgamma" | "qgamma" | "rgamma"
15007            | "dlnorm" | "qlnorm" | "rlnorm" | "dlogis" | "qlogis" | "rlogis"
15008            | "dpois" | "qpois" | "rpois" | "dweibull" | "qweibull" | "rweibull"
15009            | "qnorm" | "rnorm" | "qunif" | "runif"
15010            | "qbinom" | "rbinom" | "qgeom" | "rgeom" | "qhyper" | "rhyper"
15011            | "qchisq" | "rchisq" | "qf" | "rf" | "qt" | "rt"
15012            // ── extras ──
15013            | "currency_format" | "currency_parse" | "currency_round"
15014            | "currency_split_thousands" | "currency_code_to_symbol"
15015            | "currency_symbol_to_code" | "currency_convert" | "currency_rate"
15016            | "currency_iso_4217" | "currency_decimal_places"
15017            | "money_add" | "money_sub" | "money_mul" | "money_div" | "money_compare"
15018            | "tokenize_simple" | "tokenize_word" | "tokenize_subword"
15019            | "tokenize_bpe" | "tokenize_sentencepiece" | "embed_text"
15020            | "cosine_similarity" | "euclidean_distance" | "manhattan_distance"
15021            | "dot_product" | "normalize_vector"
15022            | "vector_add" | "vector_sub" | "vector_scale" | "vector_mean"
15023            | "top_k_indices" | "softmax" | "sigmoid" | "log_softmax" | "cross_entropy"
15024            | "path_canonical" | "path_relative_to" | "path_components"
15025            | "path_filename" | "path_stem" | "path_extension"
15026            | "path_join_many" | "path_with_extension" | "path_with_filename"
15027            | "path_is_subdirectory" | "path_common_ancestor" | "path_strip_prefix"
15028            | "path_glob_match_regex"
15029            | "file_mime" | "file_kind" | "file_attr_get" | "file_attr_set"
15030            | "xattr_get" | "xattr_set" | "xattr_list"
15031            | "file_chmod_string" | "file_chmod_octal" | "file_locked"
15032            | "file_acl_get" | "file_acl_set"
15033            | "locale_parse" | "locale_format" | "locale_language"
15034            | "locale_region" | "locale_script" | "locale_variant" | "locale_canonical"
15035            | "bcp47_parse" | "bcp47_format" | "bcp47_validate"
15036            | "language_tag_match" | "language_tag_subtags"
15037            | "locale_likely_subtags" | "locale_minimize" | "locale_collation"
15038            | "locale_calendar" | "locale_currency"
15039            | "locale_number_format" | "locale_date_format" | "locale_time_format"
15040            | "locale_decimal_separator" | "locale_group_separator"
15041            | "locale_first_day_of_week" | "locale_measurement_system"
15042            | "country_code_alpha2" | "country_code_alpha3" | "country_code_numeric"
15043            | "country_name" | "country_phone_prefix" | "country_currency"
15044            | "country_languages"
15045            | "language_iso_639_1" | "language_iso_639_2" | "language_iso_639_3"
15046            | "language_name"
15047            | "channel_unbounded" | "channel_bounded" | "channel_sync"
15048            | "channel_send_timeout" | "channel_recv_timeout"
15049            | "channel_try_recv" | "channel_try_send"
15050            | "channel_drain" | "channel_close" | "channel_is_closed"
15051            | "broadcast_channel_new" | "broadcast_channel_subscribe"
15052            | "broadcast_channel_publish"
15053            | "mpsc_new" | "mpmc_new" | "spmc_new" | "oneshot_new"
15054            // ── mutex + counting semaphore ─────────────────────────────────
15055            | "mutex" | "mutex_lock" | "mutex_unlock" | "mutex_try_lock" | "mutex_is_locked"
15056            | "semaphore" | "sem"
15057            | "semaphore_acquire" | "sem_acquire"
15058            | "semaphore_release" | "sem_release"
15059            | "semaphore_try_acquire" | "sem_try_acquire"
15060            | "semaphore_permits" | "sem_permits"
15061            | "semaphore_limit" | "sem_limit"
15062            // ── stress testing ──────────────────────────────────────────────
15063            | "stress_cpu" | "scpu" | "stress_mem" | "smem"
15064            | "stress_io" | "sio" | "stress_test" | "st"
15065            | "heat" | "fire" | "fire_and_forget" | "pin"
15066            // ── I/O extensions ──────────────────────────────────────────────
15067            | "slurp" | "cat" | "c" | "capture" | "pager" | "pg" | "less"
15068            | "stdin"
15069            // ── internal ────────────────────────────────────────────────────
15070            | "__stryke_rust_compile"
15071            | "vec_set_value"
15072            // ── short aliases ───────────────────────────────────────────────
15073            | "p" | "rev"
15074            // ── trivial numeric / predicate builtins ────────────────────────
15075            | "even" | "odd" | "zero" | "nonzero"
15076            | "positive" | "pos_n" | "negative" | "neg_n"
15077            | "sign" | "negate" | "double" | "triple" | "half"
15078            | "identity" | "id"
15079            | "round" | "floor" | "ceil" | "ceiling" | "trunc" | "truncn"
15080            | "gcd" | "lcm" | "min2" | "max2"
15081            | "log2" | "log10" | "hypot"
15082            | "rad_to_deg" | "r2d" | "deg_to_rad" | "d2r"
15083            | "pow2" | "abs_diff"
15084            | "factorial" | "fact" | "fibonacci" | "fib"
15085            | "is_prime" | "is_square" | "is_power_of_two" | "is_pow2"
15086            | "cbrt" | "exp2" | "percent" | "pct" | "inverse"
15087            | "median" | "mode_val" | "variance"
15088            // ── trivial string ops ──────────────────────────────────────────
15089            | "is_empty" | "is_blank" | "is_numeric"
15090            | "is_upper" | "is_lower" | "is_alpha" | "is_digit" | "is_alnum"
15091            | "is_space" | "is_whitespace"
15092            | "starts_with" | "sw" | "ends_with" | "ew" | "contains"
15093            | "capitalize" | "cap" | "swap_case" | "repeat"
15094            | "title_case" | "title" | "squish"
15095            | "pad_left" | "lpad" | "pad_right" | "rpad" | "center"
15096            | "truncate_at" | "shorten" | "reverse_str" | "rev_str"
15097            | "char_count" | "word_count" | "wc" | "line_count" | "lc_lines"
15098            // ── trivial type predicates ─────────────────────────────────────
15099            | "is_array" | "is_arrayref" | "is_hash" | "is_hashref"
15100            | "is_code" | "is_coderef" | "is_ref"
15101            | "is_undef" | "is_defined" | "is_def"
15102            | "is_string" | "is_str" | "is_int" | "is_integer" | "is_float"
15103            // ── hash helpers ────────────────────────────────────────────────
15104            | "invert" | "merge_hash"
15105            | "hash_map_values" | "hash_filter_keys" | "hash_filter_values"
15106            | "has_key" | "hk" | "has_any_key" | "has_all_keys"
15107            // ── boolean combinators ─────────────────────────────────────────
15108            | "both" | "either" | "neither" | "xor_bool" | "bool_to_int" | "b2i"
15109            // ── collection helpers (trivial) ────────────────────────────────
15110            | "riffle" | "intersperse" | "every_nth"
15111            | "drop_n" | "take_n" | "rotate" | "swap_pairs"
15112            // ── base conversion ─────────────────────────────────────────────
15113            | "to_bin" | "bin_of" | "to_hex" | "hex_of" | "to_oct" | "oct_of"
15114            | "from_bin" | "from_hex" | "from_oct" | "to_base" | "from_base"
15115            | "bits_count" | "popcount" | "leading_zeros" | "lz"
15116            | "trailing_zeros" | "tz" | "bit_length" | "bitlen"
15117            // ── bit ops ─────────────────────────────────────────────────────
15118            | "bit_and" | "bit_or" | "bit_xor" | "bit_not"
15119            | "shift_left" | "shl" | "shift_right" | "shr"
15120            | "bit_set" | "bit_clear" | "bit_toggle" | "bit_test"
15121            // ── unit conversions: temperature ───────────────────────────────
15122            | "c_to_f" | "f_to_c" | "c_to_k" | "k_to_c" | "f_to_k" | "k_to_f"
15123            // ── unit conversions: distance ──────────────────────────────────
15124            | "miles_to_km" | "km_to_miles" | "miles_to_m" | "m_to_miles"
15125            | "feet_to_m" | "m_to_feet" | "inches_to_cm" | "cm_to_inches"
15126            | "yards_to_m" | "m_to_yards"
15127            // ── unit conversions: mass ──────────────────────────────────────
15128            | "kg_to_lbs" | "lbs_to_kg" | "g_to_oz" | "oz_to_g"
15129            | "stone_to_kg" | "kg_to_stone"
15130            // ── unit conversions: digital ───────────────────────────────────
15131            | "bytes_to_kb" | "b_to_kb" | "kb_to_bytes" | "kb_to_b"
15132            | "bytes_to_mb" | "mb_to_bytes" | "bytes_to_gb" | "gb_to_bytes"
15133            | "kb_to_mb" | "mb_to_gb"
15134            | "bits_to_bytes" | "bytes_to_bits"
15135            // ── unit conversions: time ──────────────────────────────────────
15136            | "seconds_to_minutes" | "s_to_m" | "minutes_to_seconds" | "m_to_s"
15137            | "seconds_to_hours" | "hours_to_seconds"
15138            | "seconds_to_days" | "days_to_seconds"
15139            | "minutes_to_hours" | "hours_to_minutes"
15140            | "hours_to_days" | "days_to_hours"
15141            // ── date helpers ────────────────────────────────────────────────
15142            | "is_leap_year" | "is_leap" | "days_in_month"
15143            | "month_name" | "month_short"
15144            | "weekday_name" | "weekday_short" | "quarter_of"
15145            // ── now / timestamp ─────────────────────────────────────────────
15146            | "now_ms" | "now_us" | "now_ns"
15147            | "unix_epoch" | "epoch" | "unix_epoch_ms" | "epoch_ms"
15148            // ── color / ANSI ────────────────────────────────────────────────
15149            | "rgb_to_hex" | "hex_to_rgb"
15150            | "ansi_red" | "ansi_green" | "ansi_yellow" | "ansi_blue"
15151            | "ansi_magenta" | "ansi_cyan" | "ansi_white" | "ansi_black"
15152            | "ansi_bold" | "ansi_dim" | "ansi_underline" | "ansi_reverse"
15153            | "strip_ansi"
15154            | "red" | "green" | "yellow" | "blue" | "magenta" | "purple" | "cyan"
15155            | "white" | "black" | "bold" | "dim" | "italic" | "underline"
15156            | "strikethrough" | "ansi_off" | "off" | "gray" | "grey"
15157            | "bright_red" | "bright_green" | "bright_yellow" | "bright_blue"
15158            | "bright_magenta" | "bright_cyan" | "bright_white"
15159            | "bg_red" | "bg_green" | "bg_yellow" | "bg_blue"
15160            | "bg_magenta" | "bg_cyan" | "bg_white" | "bg_black"
15161            | "red_bold" | "bold_red" | "green_bold" | "bold_green"
15162            | "yellow_bold" | "bold_yellow" | "blue_bold" | "bold_blue"
15163            | "magenta_bold" | "bold_magenta" | "cyan_bold" | "bold_cyan"
15164            | "white_bold" | "bold_white"
15165            | "blink" | "rapid_blink" | "hidden" | "overline"
15166            | "bg_bright_red" | "bg_bright_green" | "bg_bright_yellow" | "bg_bright_blue"
15167            | "bg_bright_magenta" | "bg_bright_cyan" | "bg_bright_white"
15168            | "rgb" | "bg_rgb" | "color256" | "c256" | "bg_color256" | "bg_c256"
15169            // ── network / validation ────────────────────────────────────────
15170            | "ipv4_to_int" | "int_to_ipv4"
15171            | "is_valid_ipv4" | "is_valid_ipv6" | "is_valid_email" | "is_valid_url"
15172            // ── path helpers ────────────────────────────────────────────────
15173            | "path_ext" | "path_parent" | "path_join" | "path_split"
15174            | "strip_prefix" | "strip_suffix" | "ensure_prefix" | "ensure_suffix"
15175            // ── functional primitives ───────────────────────────────────────
15176            | "const_fn" | "always_true" | "always_false"
15177            | "flip_args" | "first_arg" | "second_arg" | "last_arg"
15178            // ── more list helpers ───────────────────────────────────────────
15179            | "count_eq" | "count_ne" | "all_eq"
15180            | "all_distinct" | "all_unique" | "has_duplicates"
15181            | "sum_of" | "product_of" | "max_of" | "min_of" | "range_of"
15182            // ── string quote / escape ───────────────────────────────────────
15183            | "quote" | "single_quote" | "unquote"
15184            | "extract_between" | "ellipsis"
15185            // ── random ──────────────────────────────────────────────────────
15186            | "coin_flip" | "dice_roll"
15187            | "random_int" | "random_float" | "random_bool"
15188            | "random_choice" | "random_between"
15189            | "random_string" | "random_alpha" | "random_digit"
15190            // ── symbol table ────────────────────────────────────────────────
15191            | "refresh_stashes"
15192            // ── system introspection ────────────────────────────────────────
15193            | "os_name" | "os_arch" | "num_cpus"
15194            | "pid" | "ppid" | "uid" | "gid"
15195            | "username" | "home_dir" | "temp_dir"
15196            | "mem_total" | "mem_free" | "mem_used"
15197            | "swap_total" | "swap_free" | "swap_used"
15198            | "disk_total" | "disk_free" | "disk_avail" | "disk_used"
15199            | "load_avg" | "sys_uptime" | "page_size"
15200            | "os_version" | "os_family" | "endianness" | "pointer_width"
15201            | "proc_mem" | "rss"
15202            // ── collection more ─────────────────────────────────────────────
15203            | "transpose" | "unzip"
15204            | "run_length_encode" | "rle" | "run_length_decode" | "rld"
15205            | "sliding_pairs" | "consecutive_eq" | "flatten_deep"
15206            // ── trig / math ───────────────────────────────────────
15207            | "tan" | "asin" | "acos" | "atan"
15208            | "sinh" | "cosh" | "tanh" | "asinh" | "acosh" | "atanh"
15209            | "sqr" | "cube_fn"
15210            | "mod_op" | "ceil_div" | "floor_div"
15211            | "is_finite" | "is_infinite" | "is_inf" | "is_nan"
15212            | "degrees" | "radians"
15213            | "min_abs" | "max_abs"
15214            | "saturate" | "sat01" | "wrap_around"
15215            // ── string ────────────────────────────────────────────
15216            | "rot13" | "rot47" | "caesar_shift" | "reverse_words"
15217            | "count_vowels" | "count_consonants" | "is_vowel" | "is_consonant"
15218            | "first_word" | "last_word"
15219            | "left_str" | "head_str" | "right_str" | "tail_str" | "mid_str"
15220            | "lowercase" | "uppercase"
15221            | "pascal_case" | "pc_case"
15222            | "constant_case" | "upper_snake" | "dot_case" | "path_case"
15223            | "is_palindrome" | "hamming_distance"
15224            | "longest_common_prefix" | "lcp"
15225            | "ascii_ord" | "ascii_chr" | "count_char" | "indexes_of"
15226            | "replace_first" | "replace_all_str"
15227            | "contains_any" | "contains_all"
15228            | "starts_with_any" | "ends_with_any"
15229            // ── predicates ────────────────────────────────────────
15230            | "is_pair" | "is_triple"
15231            | "is_sorted" | "is_asc" | "is_sorted_desc" | "is_desc"
15232            | "is_empty_arr" | "is_empty_hash"
15233            | "is_subset" | "is_superset" | "is_permutation"
15234            // ── collection ────────────────────────────────────────
15235            | "first_eq" | "last_eq"
15236            | "index_of" | "last_index_of" | "positions_of"
15237            | "batch" | "binary_search" | "bsearch" | "linear_search" | "lsearch"
15238            | "distinct_count" | "longest" | "shortest"
15239            | "array_union" | "list_union"
15240            | "array_intersection" | "list_intersection"
15241            | "array_difference" | "list_difference"
15242            | "symmetric_diff" | "group_of_n" | "chunk_n"
15243            | "repeat_list" | "cycle_n" | "random_sample" | "sample_n"
15244            // ── hash ops ──────────────────────────────────────────
15245            | "pick_keys" | "pick" | "omit_keys" | "omit"
15246            | "map_keys_fn" | "map_values_fn"
15247            | "hash_size" | "hash_from_pairs" | "pairs_from_hash"
15248            | "hash_eq" | "keys_sorted" | "values_sorted" | "remove_keys"
15249            // ── date ──────────────────────────────────────────────
15250            | "today" | "yesterday" | "tomorrow" | "is_weekend" | "is_weekday"
15251            // ── json helpers ────────────────────────────────────────────────
15252            | "json_pretty" | "json_minify" | "escape_json" | "json_escape"
15253            // ── process / env ───────────────────────────────────────────────
15254            | "cmd_exists" | "env_get" | "env_has" | "env_keys"
15255            | "argc" | "script_name"
15256            | "has_stdin_tty" | "has_stdout_tty" | "has_stderr_tty"
15257            // ── id helpers ──────────────────────────────────────────────────
15258            | "uuid_v4" | "nanoid" | "short_id" | "is_uuid" | "token"
15259            // ── url / email parts ───────────────────────────────────────────
15260            | "email_domain" | "email_local"
15261            | "url_host" | "url_path" | "url_query" | "url_scheme"
15262            // ── file stat / path ────────────────────────────────────────────
15263            | "file_size" | "fsize" | "file_mtime" | "mtime"
15264            | "file_atime" | "atime" | "file_ctime" | "ctime"
15265            | "is_symlink" | "is_readable" | "is_writable" | "is_executable"
15266            | "path_is_abs" | "path_is_rel"
15267            // ── stats / sort / array / format / cmp / regex / time conv / volume / force ──
15268            | "min_max" | "percentile" | "harmonic_mean" | "geometric_mean" | "zscore"
15269            | "sorted" | "sorted_desc" | "sorted_nums" | "sorted_by_length"
15270            | "reverse_list" | "list_reverse"
15271            | "without" | "without_nth" | "take_last" | "drop_last"
15272            | "pairwise" | "zipmap"
15273            | "format_bytes" | "human_bytes"
15274            | "format_duration" | "human_duration"
15275            | "format_number" | "group_number"
15276            | "format_percent" | "pad_number"
15277            | "spaceship" | "cmp_num" | "cmp_str"
15278            | "compare_versions" | "version_cmp"
15279            | "hash_insert" | "hash_update" | "hash_delete"
15280            | "matches_regex" | "re_match"
15281            | "count_regex_matches" | "regex_extract"
15282            | "regex_split_str" | "regex_replace_str"
15283            | "shuffle_chars" | "random_char" | "nth_word"
15284            | "head_lines" | "tail_lines" | "count_substring"
15285            | "is_valid_hex" | "hex_upper" | "hex_lower"
15286            | "ms_to_s" | "s_to_ms" | "ms_to_ns" | "ns_to_ms"
15287            | "us_to_ns" | "ns_to_us"
15288            | "liters_to_gallons" | "gallons_to_liters"
15289            | "liters_to_ml" | "ml_to_liters"
15290            | "cups_to_ml" | "ml_to_cups"
15291            | "newtons_to_lbf" | "lbf_to_newtons"
15292            | "joules_to_cal" | "cal_to_joules"
15293            | "watts_to_hp" | "hp_to_watts"
15294            | "pascals_to_psi" | "psi_to_pascals"
15295            | "bar_to_pascals" | "pascals_to_bar"
15296            // ── algebraic match ─────────────────────────────────────────────
15297            | "match"
15298            // ── clojure stdlib (only names not matched above) ─────────────────
15299            | "fst" | "rest" | "rst" | "second" | "snd"
15300            | "last_clj" | "lastc" | "butlast" | "bl"
15301            | "ffirst" | "ffs" | "fnext" | "fne" | "nfirst" | "nfs" | "nnext" | "nne"
15302            | "cons" | "conj"
15303            | "peek_clj" | "pkc" | "pop_clj" | "popc"
15304            | "some" | "not_any" | "not_every"
15305            | "comp" | "compose" | "partial" | "constantly" | "complement" | "compl"
15306            | "fnil" | "juxt"
15307            | "memoize" | "memo" | "curry" | "once"
15308            | "deep_clone" | "dclone" | "deep_merge" | "dmerge" | "deep_equal" | "deq"
15309            | "iterate" | "iter" | "repeatedly" | "rptd" | "cycle" | "cyc"
15310            | "mapcat" | "mcat" | "keep" | "kp" | "remove_clj" | "remc"
15311            | "reductions" | "rdcs"
15312            | "partition_by" | "pby" | "partition_all" | "pall"
15313            | "split_at" | "spat" | "split_with" | "spw"
15314            | "assoc" | "dissoc" | "get_in" | "gin" | "assoc_in" | "ain" | "update_in" | "uin"
15315            | "into" | "empty_clj" | "empc" | "seq" | "vec_clj" | "vecc"
15316            | "apply" | "appl"
15317            // ── python/ruby stdlib ───────────────────────────────────────────
15318            | "divmod" | "dm" | "accumulate" | "accum" | "starmap" | "smap"
15319            | "zip_longest" | "zipl" | "zip_fill" | "zipf" | "combinations" | "comb" | "permutations" | "perm"
15320            | "cartesian_product" | "cprod" | "compress" | "cmpr" | "filterfalse" | "falf"
15321            | "islice" | "isl" | "chain_from" | "chfr" | "pairwise_iter" | "pwi"
15322            | "tee_iter" | "teei" | "groupby_iter" | "gbi"
15323            | "each_slice" | "eslice" | "each_cons" | "econs"
15324            | "one" | "none_match" | "nonem"
15325            | "find_index_fn" | "fidx" | "rindex_fn" | "ridx"
15326            | "minmax" | "mmx" | "minmax_by" | "mmxb"
15327            | "dig" | "values_at" | "vat" | "fetch_val" | "fv" | "slice_arr" | "sla"
15328            | "transform_keys" | "tkeys" | "transform_values" | "tvals"
15329            | "sum_by" | "sumb" | "uniq_by" | "uqb"
15330            | "flat_map_fn" | "fmf" | "then_fn" | "thfn" | "times_fn" | "timf"
15331            | "step" | "upto" | "downto"
15332            // ── javascript array/object methods ─────────────────────────────
15333            | "find_last" | "fndl" | "find_last_index" | "fndli"
15334            | "at_index" | "ati" | "replace_at" | "repa"
15335            | "to_sorted" | "tsrt" | "to_reversed" | "trev" | "to_spliced" | "tspl"
15336            | "flat_depth" | "fltd" | "fill_arr" | "filla" | "includes_val" | "incv"
15337            | "object_keys" | "okeys" | "object_values" | "ovals"
15338            | "object_entries" | "oents" | "object_from_entries" | "ofents"
15339            // ── haskell list functions ──────────────────────────────────────
15340            | "span_fn" | "spanf" | "break_fn" | "brkf" | "group_runs" | "gruns"
15341            | "nub" | "sort_on" | "srton"
15342            | "intersperse_val" | "isp" | "intercalate" | "ical"
15343            | "replicate_val" | "repv" | "elem_of" | "elof" | "not_elem" | "ntelm"
15344            | "lookup_assoc" | "lkpa" | "scanl" | "scanr" | "unfoldr" | "unfr"
15345            // ── rust iterator methods ───────────────────────────────────────
15346            | "find_map" | "fndm" | "filter_map" | "fltm" | "fold_right" | "fldr"
15347            | "partition_either" | "peith" | "try_fold" | "tfld"
15348            | "map_while" | "mapw" | "inspect" | "insp"
15349            // ── ruby enumerable extras ──────────────────────────────────────
15350            | "tally_by" | "talb" | "sole" | "chunk_while" | "chkw" | "count_while" | "cntw"
15351            // ── go/general functional utilities ─────────────────────────────
15352            | "insert_at" | "insa" | "delete_at" | "dela" | "update_at" | "upda"
15353            | "split_on" | "spon" | "words_from" | "wfrm" | "unwords" | "unwds"
15354            | "lines_from" | "lfrm" | "unlines" | "unlns"
15355            | "window_n" | "winn" | "adjacent_pairs" | "adjp"
15356            | "zip_all" | "zall" | "unzip_pairs" | "uzp"
15357            | "interpose" | "ipos" | "partition_n" | "partn"
15358            | "map_indexed" | "mapi" | "reduce_indexed" | "redi" | "filter_indexed" | "flti"
15359            | "group_by_fn" | "gbf" | "index_by" | "idxb" | "associate" | "assoc_fn"
15360            // ── additional missing stdlib functions ─────────────────────────
15361            | "combinations_rep" | "combrep" | "inits" | "tails" | "subsequences" | "subseqs"
15362            | "nub_by" | "nubb" | "slice_when" | "slcw" | "slice_before" | "slcb" | "slice_after" | "slca"
15363            | "each_with_object" | "ewo" | "reduce_right" | "redr"
15364            | "is_sorted_by" | "issrtb" | "intersperse_with" | "ispw"
15365            | "running_reduce" | "runred" | "windowed_circular" | "wincirc"
15366            | "distinct_by" | "distb" | "average" | "mean" | "copy_within" | "cpyw"
15367            | "and_list" | "andl" | "or_list" | "orl" | "concat_map" | "cmap"
15368            | "elem_index" | "elidx" | "elem_indices" | "elidxs" | "find_indices" | "fndidxs"
15369            | "delete_first" | "delfst" | "delete_by" | "delby" | "insert_sorted" | "inssrt"
15370            | "union_list" | "unionl" | "intersect_list" | "intl"
15371            | "maximum_by" | "maxby" | "minimum_by" | "minby" | "batched" | "btch"
15372            // ── Extended stdlib: Text Processing ─────────────────────────────
15373            | "match_all" | "mall" | "capture_groups" | "capg" | "is_match" | "ism"
15374            | "split_regex" | "splre" | "replace_regex" | "replre"
15375            | "is_ascii" | "isasc" | "to_ascii" | "toasc"
15376            | "char_at" | "chat" | "code_point_at" | "cpat" | "from_code_point" | "fcp"
15377            | "normalize_spaces" | "nrmsp" | "remove_whitespace" | "rmws"
15378            | "pluralize" | "plur" | "ordinalize" | "ordn"
15379            | "parse_int" | "pint" | "parse_float" | "pflt" | "parse_bool" | "pbool"
15380            | "levenshtein" | "lev" | "soundex" | "sdx" | "similarity" | "sim"
15381            | "common_prefix" | "cpfx" | "common_suffix" | "csfx"
15382            | "wrap_text" | "wrpt" | "dedent" | "ddt" | "indent" | "idt"
15383            // ── Extended stdlib: Advanced Numeric ────────────────────────────
15384            | "lerp" | "inv_lerp" | "ilerp" | "smoothstep" | "smst" | "remap"
15385 | "dotp" | "cross_product" | "crossp"
15386            | "matrix_mul" | "matmul" | "mm"
15387            | "magnitude" | "mag" | "normalize_vec" | "nrmv"
15388            | "distance" | "dist" | "mdist"
15389            | "covariance" | "cov" | "correlation" | "corr"
15390            | "iqr" | "quantile" | "qntl" | "quantiles" | "qntls"
15391            | "lsp_completion_words" | "lsp_words"
15392            | "doctor" | "health"
15393            | "clamp_int" | "clpi"
15394            | "in_range" | "inrng" | "wrap_range" | "wrprng"
15395            | "sum_squares" | "sumsq" | "rms" | "cumsum" | "csum" | "cumprod" | "cprod_acc" | "diff"
15396            // ── Extended stdlib: Date/Time ───────────────────────────────────
15397            | "add_days" | "addd" | "add_hours" | "addh" | "add_minutes" | "addm"
15398            | "diff_days" | "diffd" | "diff_hours" | "diffh"
15399            | "start_of_day" | "sod" | "end_of_day" | "eod"
15400            | "start_of_hour" | "soh" | "start_of_minute" | "som"
15401            // ── Extended stdlib: Encoding/Hashing ────────────────────────────
15402            | "urle" | "urld"
15403            | "html_encode" | "htmle" | "html_decode" | "htmld"
15404            | "adler32" | "adl32" | "fnv1a" | "djb2"
15405            // ── Extended stdlib: Validation ──────────────────────────────────
15406            | "is_credit_card" | "iscc" | "is_isbn10" | "isbn10" | "is_isbn13" | "isbn13"
15407            | "is_iban" | "isiban" | "is_hex_str" | "ishex" | "is_binary_str" | "isbin"
15408            | "is_octal_str" | "isoct" | "is_json" | "isjson" | "is_base64" | "isb64"
15409            | "is_semver" | "issv" | "is_slug" | "isslug" | "slugify" | "slug"
15410            // ── Extended stdlib: Collection Advanced ─────────────────────────
15411            | "mode_stat" | "mstat" | "sampn" | "weighted_sample" | "wsamp"
15412            | "shuffle_arr" | "shuf" | "argmax" | "amax" | "argmin" | "amin"
15413            | "argsort" | "asrt" | "rank" | "rnk" | "dense_rank" | "drnk"
15414            | "partition_point" | "ppt" | "lower_bound" | "lbound"
15415            | "upper_bound" | "ubound" | "equal_range" | "eqrng"
15416            // ── Extended stdlib: Matrix Operations ───────────────────────────
15417            | "matrix_add" | "madd" | "matrix_sub" | "msub" | "matrix_mult" | "mmult"
15418            | "matrix_scalar" | "mscal" | "matrix_identity" | "mident"
15419            | "matrix_zeros" | "mzeros" | "matrix_ones" | "mones"
15420            | "matrix_diag" | "mdiag" | "matrix_trace" | "mtrace"
15421            | "matrix_row" | "mrow" | "matrix_col" | "mcol"
15422            | "matrix_shape" | "mshape" | "matrix_det" | "mdet"
15423            | "matrix_scale" | "mat_scale" | "diagonal" | "diag"
15424            // ── Extended stdlib: Graph Algorithms ────────────────────────────
15425            | "topological_sort" | "toposort" | "bfs_traverse" | "bfs"
15426            | "dfs_traverse" | "dfs" | "shortest_path_bfs" | "spbfs"
15427            | "connected_components_graph" | "ccgraph"
15428            | "has_cycle_graph" | "hascyc" | "is_bipartite_graph" | "isbip"
15429            // ── Extended stdlib: Data Validation ─────────────────────────────
15430            | "is_ipv4_addr" | "isip4" | "is_ipv6_addr" | "isip6"
15431            | "is_mac_addr" | "ismac" | "is_port_num" | "isport"
15432            | "is_hostname_valid" | "ishost"
15433            | "is_iso_date" | "isisodt" | "is_iso_time" | "isisotm"
15434            | "is_iso_datetime" | "isisodtm"
15435            | "is_phone_num" | "isphone" | "is_us_zip" | "iszip"
15436            // ── Extended stdlib: String Utilities Novel ──────────────────────
15437            | "word_wrap_text" | "wwrap" | "center_text" | "ctxt"
15438            | "ljust_text" | "ljt" | "rjust_text" | "rjt" | "zfill_num" | "zfill"
15439            | "remove_all_str" | "rmall" | "replace_n_times" | "repln"
15440            | "find_all_indices" | "fndalli"
15441            | "text_between" | "txbtwn" | "text_before" | "txbef" | "text_after" | "txaft"
15442            | "text_before_last" | "txbefl" | "text_after_last" | "txaftl"
15443            // ── Extended stdlib: Math Novel ──────────────────────────────────
15444            | "is_even_num" | "iseven" | "is_odd_num" | "isodd"
15445            | "is_positive_num" | "ispos" | "is_negative_num" | "isneg"
15446            | "is_zero_num" | "iszero" | "is_whole_num" | "iswhole"
15447            | "log_with_base" | "logb" | "nth_root_of" | "nroot"
15448            | "frac_part" | "fracp" | "reciprocal_of" | "recip"
15449            | "copy_sign" | "cpsgn" | "fused_mul_add" | "fmadd"
15450            | "floor_mod" | "fmod" | "floor_div_op" | "fdivop"
15451            | "signum_of" | "sgnum" | "midpoint_of" | "midpt"
15452            // ── Extended stdlib: Array Analysis ──────────────────────
15453            | "longest_run" | "lrun" | "longest_increasing" | "linc"
15454            | "longest_decreasing" | "ldec" | "max_sum_subarray" | "maxsub"
15455            | "majority_element" | "majority" | "kth_largest" | "kthl"
15456            | "kth_smallest" | "kths" | "count_inversions" | "cinv"
15457            | "is_monotonic" | "ismono" | "equilibrium_index" | "eqidx"
15458            // ── Extended stdlib: Set Operations ──────────────────────
15459            | "jaccard_index" | "jaccard" | "dice_coefficient" | "dicecoef"
15460            | "overlap_coefficient" | "overlapcoef"
15461            | "power_set" | "powerset" | "cartesian_power" | "cartpow"
15462            // ── Extended stdlib: Advanced String ─────────────────────
15463            | "is_isogram" | "isiso" | "is_heterogram" | "ishet"
15464            | "hamdist" | "jaro_similarity" | "jarosim"
15465            | "longest_common_substring" | "lcsub"
15466            | "longest_common_subsequence" | "lcseq"
15467            | "count_words" | "wcount" | "count_lines" | "lcount"
15468            | "count_chars" | "ccount" | "count_bytes" | "bcount"
15469            // ── Extended stdlib: More Math ───────────────────────────
15470            | "binomial" | "binom" | "catalan" | "catn" | "pascal_row" | "pascrow"
15471            | "is_coprime" | "iscopr" | "euler_totient" | "etot"
15472            | "mobius" | "mob" | "is_squarefree" | "issqfr"
15473            | "digital_root" | "digroot" | "is_narcissistic" | "isnarc"
15474            | "is_harshad" | "isharsh" | "is_kaprekar" | "iskap"
15475            // ── Extended stdlib: Date/Time Additional ────────────────
15476            | "day_of_year" | "doy" | "week_of_year" | "woy"
15477            | "days_in_month_fn" | "daysinmo" | "is_valid_date" | "isvdate"
15478            | "age_in_years" | "ageyrs"
15479            // ── functional combinators ──────────────────────────────────────
15480
15481            | "when_true" | "when_false" | "if_else" | "clamp_fn"
15482            | "attempt" | "try_fn" | "safe_div" | "safe_mod" | "safe_sqrt" | "safe_log"
15483            | "juxt2" | "juxt3" | "tap_val" | "debug_val" | "converge"
15484            | "iterate_n" | "unfold" | "arity_of" | "is_callable"
15485            | "coalesce" | "default_to" | "fallback"
15486            | "apply_list" | "zip_apply" | "scan"
15487            | "keep_if" | "reject_if" | "group_consecutive"
15488            | "after_n" | "before_n" | "clamp_list" | "normalize_list"
15489
15490            // ── matrix / linear algebra ─────────────────────────────────────
15491
15492
15493            | "matrix_multiply" | "mat_mul"
15494            | "identity_matrix" | "eye" | "zeros_matrix" | "zeros" | "ones_matrix" | "ones"
15495
15496
15497
15498            | "vec_normalize" | "unit_vec" | "vec_add" | "vec_sub" | "vec_scale"
15499            | "linspace" | "arange"
15500            // ── more regex ──────────────────────────────────────────────────
15501            | "re_test" | "re_find_all" | "re_groups" | "re_escape"
15502            | "re_split_limit" | "glob_to_regex" | "is_regex_valid"
15503            // ── more process / system ───────────────────────────────────────
15504            | "cwd" | "pwd_str" | "cpu_count" | "is_root" | "uptime_secs"
15505            | "env_pairs" | "env_set" | "env_remove" | "hostname_str" | "is_tty" | "signal_name"
15506            // ── data structure helpers ───────────────────────────────────────
15507            | "stack_new" | "queue_new" | "lru_new"
15508            | "counter" | "counter_most_common" | "defaultdict" | "ordered_set"
15509            | "bitset_new" | "bitset_set" | "bitset_test" | "bitset_clear"
15510            // ── trivial numeric helpers ─────────────────────────────
15511            | "abs_ceil" | "abs_each" | "abs_floor" | "ceil_each" | "dec_each"
15512            | "double_each" | "floor_each" | "half_each" | "inc_each" | "length_each"
15513            | "negate_each" | "not_each" | "offset_each" | "reverse_each" | "round_each"
15514            | "scale_each" | "sqrt_each" | "square_each" | "to_float_each" | "to_int_each"
15515            | "trim_each" | "type_each" | "upcase_each" | "downcase_each" | "bool_each"
15516            // ── math / physics constants ──────────────────────────────────────
15517            | "avogadro" | "boltzmann" | "golden_ratio" | "gravity" | "ln10" | "ln2"
15518            | "planck" | "speed_of_light" | "sqrt2"
15519            // ── physics formulas ──────────────────────────────────────────────
15520            | "bmi_calc" | "compound_interest" | "dew_point" | "discount_amount"
15521            | "force_mass_acc" | "freq_wavelength" | "future_value" | "haversine"
15522            | "heat_index" | "kinetic_energy" | "margin_price" | "markup_price"
15523            | "mortgage_payment" | "ohms_law_i" | "ohms_law_r" | "ohms_law_v"
15524            | "potential_energy" | "present_value" | "simple_interest" | "speed_distance_time"
15525            | "tax_amount" | "tip_amount" | "wavelength_freq" | "wind_chill"
15526            // ── math functions ────────────────────────────────────────────────
15527            | "angle_between_deg" | "approx_eq" | "chebyshev_distance" | "copysign"
15528 | "cube_root" | "entropy" | "float_bits" | "fma"
15529            | "int_bits" | "jaccard_similarity" | "log_base" | "mae" | "mse" | "nth_root"
15530            | "r_squared" | "reciprocal" | "relu" | "rmse" | "rotate_point" | "round_to"
15531 | "signum" | "square_root"
15532            // ── sequences ─────────────────────────────────────────────────────
15533            | "cubes_seq" | "fibonacci_seq" | "powers_of_seq" | "primes_seq"
15534            | "squares_seq" | "triangular_seq"
15535            // ── string helpers ──────────────────────────────────────
15536            | "alternate_case" | "angle_bracket" | "bracket" | "byte_length"
15537            | "bytes_to_hex_str" | "camel_words" | "char_length" | "chars_to_string"
15538            | "chomp_str" | "chop_str" | "filter_chars" | "from_csv_line" | "hex_to_bytes"
15539            | "insert_str" | "intersperse_char" | "ljust" | "map_chars" | "mirror_string"
15540            | "normalize_whitespace" | "only_alnum" | "only_alpha" | "only_ascii"
15541            | "only_digits" | "parenthesize" | "remove_str" | "repeat_string" | "rjust"
15542            | "sentence_case" | "string_count" | "string_sort" | "string_to_chars"
15543            | "string_unique_chars" | "substring" | "to_csv_line" | "trim_left" | "trim_right"
15544            | "xor_strings"
15545            // ── list helpers ─────────────────────────────────────────
15546            | "adjacent_difference" | "append_elem" | "consecutive_pairs" | "contains_elem"
15547            | "count_elem" | "drop_every" | "duplicate_count" | "elem_at" | "find_first"
15548            | "first_elem" | "flatten_once" | "fold_left" | "from_digits" | "from_pairs"
15549            | "group_by_size" | "hash_from_list"
15550            | "hash_merge_deep" | "hash_to_list" | "hash_zip" | "head_n" | "histogram_bins"
15551            | "index_of_elem" | "init_list" | "interleave_lists" | "last_elem" | "least_common"
15552            | "list_compact" | "list_eq" | "list_flatten_deep" | "max_list" | "mean_list"
15553            | "min_list" | "mode_list" | "most_common" | "partition_two" | "prefix_sums"
15554            | "prepend" | "product_list" | "remove_at" | "remove_elem" | "remove_first_elem"
15555            | "repeat_elem" | "running_max" | "running_min" | "sample_one" | "scan_left"
15556            | "second_elem" | "span" | "suffix_sums" | "sum_list" | "tail_n" | "take_every"
15557            | "third_elem" | "to_array" | "to_pairs" | "trimmed_mean" | "unique_count_of"
15558            | "wrap_index" | "digits_of"
15559            // ── predicates ──────────────────────────────────────────
15560            | "all_match" | "any_match" | "is_between" | "is_blank_or_nil" | "is_divisible_by"
15561            | "is_email" | "is_even" | "is_falsy" | "is_fibonacci" | "is_hex_color"
15562            | "is_in_range" | "is_ipv4" | "is_multiple_of" | "is_negative" | "is_nil"
15563            | "is_nonzero" | "is_odd" | "is_perfect_square" | "is_positive" | "is_power_of"
15564            | "is_prefix" | "is_present" | "is_strictly_decreasing" | "is_strictly_increasing"
15565            | "is_suffix" | "is_triangular" | "is_truthy" | "is_url" | "is_whole" | "is_zero"
15566            // ── counters ────────────────────────────────────────────
15567            | "count_digits" | "count_letters" | "count_lower" | "count_match"
15568            | "count_punctuation" | "count_spaces" | "count_upper" | "defined_count"
15569            | "empty_count" | "falsy_count" | "nonempty_count" | "numeric_count"
15570            | "truthy_count" | "undef_count"
15571            // ── conversion / utility ────────────────────────────────
15572            | "assert_type" | "between" | "clamp_each" | "die_if" | "die_unless"
15573            | "join_colons" | "join_commas" | "join_dashes" | "join_dots" | "join_lines"
15574            | "join_pipes" | "join_slashes" | "join_spaces" | "join_tabs" | "measure"
15575            | "max_float" | "min_float" | "noop_val" | "nop" | "pass" | "pred" | "succ"
15576            | "tap_debug" | "to_bool" | "to_float" | "to_int" | "to_string" | "void"
15577            | "range_exclusive" | "range_inclusive"
15578            // ── math / numeric extras ─────────────────────────────────────────
15579            | "aliquot_sum" | "autocorrelation" | "bell_number" | "cagr" | "coeff_of_variation"
15580            | "collatz_length" | "collatz_sequence" | "convolution"
15581            | "depreciation_double" | "depreciation_linear" | "discount" | "divisors"
15582            | "epsilon" | "euler_number" | "exponential_moving_average"
15583            | "f64_max" | "f64_min" | "fft_magnitude" | "goldbach" | "i64_max" | "i64_min"
15584            | "kurtosis" | "linear_regression" | "look_and_say" | "lucas" | "luhn_check"
15585            | "mean_absolute_error" | "mean_squared_error" | "median_absolute_deviation"
15586            | "minkowski_distance" | "moving_average" | "multinomial" | "neg_inf" | "npv"
15587            | "num_divisors" | "partition_number" | "pascals_triangle" | "skewness"
15588            | "standard_error" | "subfactorial" | "sum_divisors" | "totient_sum"
15589            | "tribonacci" | "weighted_mean" | "winsorize"
15590            // ── statistics (extended) ─────────────────────────────────────────
15591            | "chi_square_stat" | "describe" | "five_number_summary"
15592            | "gini" | "gini_coefficient" | "lorenz_curve" | "outliers_iqr"
15593            | "percentile_rank" | "quartiles" | "sample_stddev" | "sample_variance"
15594            | "spearman_correlation" | "t_test_one_sample" | "t_test_two_sample"
15595            | "z_score" | "z_scores"
15596            // ── number theory / primes ──────────────────────────────────────────
15597            | "abundant_numbers" | "deficient_numbers" | "is_abundant" | "is_deficient"
15598            | "is_pentagonal" | "is_perfect" | "is_smith" | "next_prime" | "nth_prime"
15599            | "pentagonal_number" | "perfect_numbers" | "prev_prime" | "prime_factors"
15600            | "prime_pi" | "primes_up_to" | "triangular_number" | "twin_primes"
15601            // ── geometry / physics ──────────────────────────────────────────────
15602            | "area_circle" | "area_ellipse" | "area_rectangle" | "area_trapezoid" | "area_triangle"
15603            | "bearing" | "circumference" | "cone_volume" | "cylinder_volume" | "heron_area"
15604            | "midpoint" | "perimeter_rectangle" | "perimeter_triangle" | "point_distance"
15605            | "polygon_area" | "slope" | "sphere_surface" | "sphere_volume" | "triangle_hypotenuse"
15606            // ── geometry (extended) ───────────────────────────────────────────
15607            | "angle_between" | "arc_length" | "bounding_box" | "centroid"
15608            | "circle_from_three_points" | "circ3" | "convex_hull" | "ellipse_perimeter" | "ellper"
15609            | "frustum_volume" | "haversine_distance" | "line_intersection"
15610            | "point_in_polygon" | "pip" | "polygon_perimeter" | "polyper" | "pyramid_volume"
15611            | "reflect_point" | "scale_point" | "sector_area"
15612            | "torus_surface" | "torus_volume" | "translate_point"
15613            | "vector_angle" | "vector_cross" | "vector_dot" | "vector_magnitude" | "vector_normalize"
15614            // ── constants ───────────────────────────────────────────────────────
15615            | "avogadro_number" | "boltzmann_constant" | "electron_mass" | "elementary_charge"
15616            | "gravitational_constant" | "phi" | "pi" | "PI" | "planck_constant"
15617            | "proton_mass" | "sol" | "tau" | "TAU" | "E"
15618            // ── finance ─────────────────────────────────────────────────────────
15619            | "bac_estimate" | "bmi" | "break_even" | "margin" | "markup" | "roi" | "tax" | "tip"
15620            // ── finance (extended) ────────────────────────────────────────────
15621            | "amortization_schedule" | "black_scholes_call" | "black_scholes_put"
15622            | "bond_price" | "bond_yield" | "capm" | "continuous_compound" | "ccomp"
15623            | "discounted_payback" | "duration" | "irr"
15624            | "max_drawdown" | "mdd" | "modified_duration" | "mod_dur" | "nper" | "num_periods" | "payback_period"
15625            | "pmt" | "pv" | "rule_of_72" | "sharpe_ratio" | "sortino_ratio"
15626            | "wacc" | "xirr"
15627            // ── string processing extras ──────────────────────────────────────
15628            | "acronym" | "atbash" | "bigrams" | "camel_to_snake" | "char_frequencies"
15629            | "chunk_string" | "collapse_whitespace" | "dedent_text" | "indent_text"
15630            | "initials" | "leetspeak" | "mask_string" | "ngrams" | "pig_latin"
15631            | "remove_consonants" | "remove_vowels" | "reverse_each_word" | "snake_to_camel"
15632            | "sort_words" | "string_distance" | "string_multiply" | "strip_html"
15633            | "trigrams" | "unique_words" | "word_frequencies" | "zalgo"
15634            // ── encoding / phonetics ────────────────────────────────────────────
15635            | "braille_encode" | "double_metaphone" | "metaphone" | "morse_decode"
15636            | "morse_encode" | "nato_phonetic" | "phonetic_digit" | "subscript" | "superscript"
15637            | "to_emoji_num"
15638            // ── roman numerals ──────────────────────────────────────────────────
15639            | "int_to_roman" | "roman_add" | "roman_numeral_list" | "roman_to_int"
15640            // ── base / gray code ────────────────────────────────────────────────
15641            | "base_convert" | "binary_to_gray" | "gray_code_sequence" | "gray_to_binary"
15642            // ── color operations ────────────────────────────────────────────────
15643            | "ansi_256" | "ansi_truecolor" | "color_blend" | "color_complement"
15644            | "color_darken" | "color_distance" | "color_grayscale" | "color_invert"
15645            | "color_lighten" | "hsl_to_rgb" | "hsv_to_rgb" | "random_color"
15646            | "rgb_to_hsl" | "rgb_to_hsv"
15647            // ── matrix operations extras ──────────────────────────────────────
15648            | "matrix_flatten" | "matrix_from_rows" | "matrix_hadamard" | "matrix_inverse"
15649            | "matrix_map" | "matrix_max" | "matrix_min" | "matrix_power" | "matrix_sum"
15650            | "matrix_transpose"
15651            // ── array / list operations extras ────────────────────────────────
15652            | "binary_insert" | "bucket" | "clamp_array" | "group_consecutive_by"
15653            | "histogram" | "merge_sorted" | "next_permutation" | "normalize_array"
15654            | "normalize_range" | "peak_detect" | "range_compress" | "range_expand"
15655            | "reservoir_sample" | "run_length_decode_str" | "run_length_encode_str"
15656            | "zero_crossings"
15657            // ── DSP / signal (extended) ───────────────────────────────────────
15658            | "apply_window" | "bandpass_filter" | "cross_correlation" | "dft"
15659            | "downsample" | "decimate" | "energy" | "envelope" | "hilbert_env" | "highpass_filter" | "idft"
15660            | "lowpass_filter" | "median_filter" | "normalize_signal" | "phase_spectrum"
15661            | "power_spectrum" | "psd" | "resample" | "spectral_centroid" | "spectrogram" | "stft" | "upsample" | "interpolate"
15662            | "window_blackman" | "window_hamming" | "window_hann" | "window_kaiser"
15663            // ── validation predicates extras ──────────────────────────────────
15664            | "is_anagram" | "is_balanced_parens" | "is_control" | "is_numeric_string"
15665            | "is_pangram" | "is_printable" | "is_valid_cidr" | "is_valid_cron"
15666            | "is_valid_hex_color" | "is_valid_latitude" | "is_valid_longitude" | "is_valid_mime"
15667            // ── algorithms / puzzles ────────────────────────────────────────────
15668            | "eval_rpn" | "fizzbuzz" | "game_of_life_step" | "mandelbrot_char"
15669            | "sierpinski" | "tower_of_hanoi" | "truth_table"
15670            // ── misc / utility ──────────────────────────────────────────────────
15671            | "byte_size" | "degrees_to_compass" | "to_string_val" | "type_of"
15672            // ── math formulas ───────────────────────────────────────────────────
15673            | "quadratic_roots" | "quadratic_discriminant" | "arithmetic_series"
15674            | "geometric_series" | "stirling_approx"
15675            | "double_factorial" | "rising_factorial" | "falling_factorial"
15676            | "gamma_approx" | "erf_approx" | "normal_pdf" | "normal_cdf"
15677            | "poisson_pmf" | "exponential_pdf" | "inverse_lerp"
15678            | "map_range"
15679            // ── physics formulas ────────────────────────────────────────────────
15680            | "momentum" | "impulse" | "work" | "power_phys" | "torque" | "angular_velocity"
15681            | "centripetal_force" | "escape_velocity" | "orbital_velocity" | "orbital_period"
15682            | "gravitational_force" | "coulomb_force" | "electric_field" | "capacitance"
15683            | "capacitor_energy" | "inductor_energy" | "resonant_frequency"
15684            | "rc_time_constant" | "rl_time_constant" | "impedance_rlc"
15685            | "relativistic_mass" | "lorentz_factor" | "time_dilation" | "length_contraction"
15686            | "relativistic_energy" | "rest_energy" | "de_broglie_wavelength"
15687            | "photon_energy" | "photon_energy_wavelength" | "schwarzschild_radius"
15688            | "stefan_boltzmann" | "wien_displacement" | "ideal_gas_pressure" | "ideal_gas_volume"
15689            | "projectile_range" | "projectile_max_height" | "projectile_time"
15690            | "spring_force" | "spring_energy" | "pendulum_period" | "doppler_frequency"
15691            | "decibel_ratio" | "snells_law" | "brewster_angle" | "critical_angle"
15692            | "lens_power" | "thin_lens" | "magnification_lens"
15693            // ── math constants ──────────────────────────────────────────────────
15694            | "euler_mascheroni" | "apery_constant" | "feigenbaum_delta" | "feigenbaum_alpha"
15695            | "catalan_constant" | "khinchin_constant" | "glaisher_constant"
15696            | "plastic_number" | "silver_ratio" | "supergolden_ratio"
15697            // ── physics constants ───────────────────────────────────────────────
15698            | "vacuum_permittivity" | "vacuum_permeability" | "coulomb_constant"
15699            | "fine_structure_constant" | "rydberg_constant" | "bohr_radius"
15700            | "bohr_magneton" | "nuclear_magneton" | "stefan_boltzmann_constant"
15701            | "wien_constant" | "gas_constant" | "faraday_constant" | "neutron_mass"
15702            | "atomic_mass_unit" | "earth_mass" | "earth_radius" | "sun_mass" | "sun_radius"
15703            | "astronomical_unit" | "light_year" | "parsec" | "hubble_constant"
15704            | "planck_length" | "planck_time" | "planck_mass" | "planck_temperature"
15705            // ── linear algebra (extended) ──────────────────────────────────
15706            | "matrix_solve" | "msolve" | "solve"
15707            | "matrix_lu" | "mlu" | "matrix_qr" | "mqr"
15708            | "matrix_eigenvalues" | "meig" | "eigenvalues" | "eig"
15709            | "matrix_norm" | "mnorm" | "matrix_cond" | "mcond" | "cond"
15710            | "matrix_pinv" | "mpinv" | "pinv"
15711            | "matrix_cholesky" | "mchol" | "cholesky"
15712            | "matrix_det_general" | "mdetg" | "det"
15713            // ── statistics tests (extended) ────────────────────────────────
15714            | "welch_ttest" | "welcht" | "paired_ttest" | "pairedt"
15715            | "cohen_d" | "cohend" | "anova_oneway" | "anova" | "anova1"
15716            | "spearman_corr" | "rho" | "kendall_tau" | "kendall" | "ktau"
15717            | "confidence_interval" | "ci"
15718            // ── distributions (extended) ──────────────────────────────────
15719            | "beta_pdf" | "betapdf" | "gamma_pdf" | "gammapdf"
15720            | "chi2_pdf" | "chi2pdf" | "chi_squared_pdf"
15721            | "t_pdf" | "tpdf" | "student_pdf"
15722            | "f_pdf" | "fpdf" | "fisher_pdf"
15723            | "lognormal_pdf" | "lnormpdf" | "weibull_pdf" | "weibpdf"
15724            | "cauchy_pdf" | "cauchypdf" | "laplace_pdf" | "laplacepdf"
15725            | "pareto_pdf" | "paretopdf"
15726            // ── interpolation & curve fitting ─────────────────────────────
15727            | "lagrange_interp" | "lagrange" | "linterp"
15728            | "cubic_spline" | "cspline" | "spline"
15729            | "poly_eval" | "polyval" | "polynomial_fit" | "polyfit"
15730            // ── numerical integration & differentiation ───────────────────
15731            | "trapz" | "trapezoid" | "simpson" | "simps"
15732            | "numerical_diff" | "numdiff" | "diff_array"
15733            | "cumtrapz" | "cumulative_trapz"
15734            // ── optimization / root finding ────────────────────────────────
15735            | "bisection" | "bisect" | "newton_method" | "newton" | "newton_raphson"
15736            | "golden_section" | "golden" | "gss"
15737            // ── ODE solvers ───────────────────────────────────────────────
15738            | "rk4" | "runge_kutta" | "rk4_ode" | "euler_ode" | "euler_method"
15739            // ── graph algorithms (extended) ────────────────────────────────
15740            | "dijkstra" | "shortest_path" | "bellman_ford" | "bellmanford"
15741            | "floyd_warshall" | "floydwarshall" | "apsp"
15742            | "prim_mst" | "mst" | "prim"
15743            // ── trig extensions ───────────────────────────────────────────
15744            | "cot" | "sec" | "csc" | "acot" | "asec" | "acsc" | "sinc" | "versin" | "versine"
15745            // ── ML activation functions ───────────────────────────────────
15746            | "leaky_relu" | "lrelu" | "elu" | "selu" | "gelu"
15747            | "silu" | "swish" | "mish" | "softplus"
15748            | "hard_sigmoid" | "hardsigmoid" | "hard_swish" | "hardswish"
15749            // ── special functions ─────────────────────────────────────────
15750            | "bessel_j0" | "j0" | "bessel_j1" | "j1"
15751            | "lambert_w" | "lambertw" | "productlog"
15752            // ── Wolfram-Math parity: Bessel/Airy/Hankel/Struve/Kelvin ─────
15753            | "bessel_j" | "bessel_y" | "bessel_i" | "bessel_k"
15754            | "hankel_h1" | "hankel_h2" | "bessel_j_zero"
15755            | "airy_ai" | "airy_bi" | "airy_ai_prime" | "airy_bi_prime"
15756            | "spherical_bessel_j" | "spherical_bessel_y"
15757            | "struve_h" | "struve_l" | "kelvin_ber" | "kelvin_bei"
15758            // ── orthogonal polynomials ────────────────────────────────────
15759            | "legendre_p" | "legendre_q" | "assoc_legendre_p"
15760            | "hermite_h" | "hermite_he" | "laguerre_l" | "assoc_laguerre_l"
15761            | "jacobi_p" | "gegenbauer_c" | "chebyshev_t" | "chebyshev_u"
15762            | "spherical_harmonic_y" | "zernike_r"
15763            // ── elliptic integrals + Jacobi/Weierstrass/theta ─────────────
15764            | "elliptic_k" | "elliptic_e" | "elliptic_pi" | "elliptic_f"
15765            | "elliptic_e_inc" | "elliptic_pi_inc"
15766            | "carlson_rf" | "carlson_rd" | "carlson_rj"
15767            | "jacobi_sn" | "jacobi_cn" | "jacobi_dn" | "jacobi_am"
15768            | "elliptic_theta"
15769            | "weierstrass_p" | "weierstrass_zeta" | "weierstrass_sigma"
15770            // ── zeta / polylog / Lerch ────────────────────────────────────
15771            | "zeta" | "riemann_zeta" | "hurwitz_zeta"
15772            | "polylog" | "dilog" | "lerch_phi"
15773            | "riemann_siegel_z" | "riemann_siegel_theta"
15774            | "dirichlet_eta" | "dirichlet_beta"
15775            // ── hypergeometric ────────────────────────────────────────────
15776            | "hypergeometric_2f1" | "hyper_2f1"
15777            | "hypergeometric_1f1" | "hyper_1f1" | "kummer_m"
15778            | "hypergeometric_0f1" | "hyper_0f1"
15779            | "hypergeometric_pfq" | "hyper_pfq"
15780            | "hypergeometric_u" | "tricomi_u"
15781            // ── modular forms ─────────────────────────────────────────────
15782            | "dedekind_eta" | "klein_j" | "klein_invariant_j"
15783            | "modular_lambda" | "ramanujan_tau"
15784            // ── integrals: Si / Ci / Ei / Li / Fresnel ────────────────────
15785            | "sin_integral" | "si_int" | "cos_integral" | "ci_int"
15786            | "sinh_integral" | "shi_int" | "cosh_integral" | "chi_int"
15787            | "exp_integral_e" | "ei_n" | "exp_integral_ei" | "ei_int"
15788            | "log_integral" | "li_int" | "fresnel_s" | "fresnel_c"
15789            // ── number-theory gaps ────────────────────────────────────────
15790            | "jacobi_symbol" | "kronecker_symbol"
15791            | "primitive_root" | "multiplicative_order"
15792            | "mangoldt_lambda" | "von_mangoldt" | "carmichael_lambda"
15793            | "squares_r" | "thue_morse" | "rudin_shapiro"
15794            | "farey_sequence" | "farey"
15795            | "frobenius_number" | "frobenius_solve" | "stern_brocot"
15796            // ── combinatorial gaps ────────────────────────────────────────
15797            | "stirling_s1" | "stirling_first" | "bell_polynomial_b" | "bell_y"
15798            | "clebsch_gordan" | "three_j_symbol" | "wigner_3j"
15799            | "six_j_symbol" | "wigner_6j" | "nine_j_symbol" | "wigner_9j"
15800            | "debruijn_sequence" | "debruijn" | "wigner_d"
15801            // ── q-series, Mittag-Leffler, Coulomb wave ────────────────────
15802            | "q_pochhammer" | "q_factorial" | "q_binomial"
15803            | "q_hypergeometric_pfq"
15804            | "mittag_leffler_e" | "mittag_leffler"
15805            | "coulomb_wave_f" | "coulomb_wave_g"
15806            // ── inverse special functions ─────────────────────────────────
15807            | "inverse_erf" | "erfinv" | "inverse_erfc" | "erfcinv"
15808            | "inverse_gamma_regularized" | "gamma_lr_inv"
15809            | "inverse_beta_regularized" | "beta_reg_inv"
15810            | "inverse_jacobi_sn"
15811            // ── piecewise / symbolic primitives ───────────────────────────
15812            | "dirac_delta" | "heaviside_theta" | "heaviside"
15813            | "unit_box" | "unit_triangle"
15814            | "square_wave" | "triangle_wave" | "sawtooth_wave" | "dirac_comb"
15815            // ── Tier A: number theory extensions ──────────────────────────
15816            | "liouville_lambda" | "jordan_totient" | "ramanujan_sum"
15817            | "cyclotomic_polynomial" | "cyclotomic" | "legendre_symbol"
15818            | "pythagorean_triple_q" | "gen_pythagorean_triple"
15819            | "sophie_germain_q" | "mersenne_q"
15820            | "lucas_lehmer_test" | "lucas_lehmer"
15821            | "continued_fraction" | "from_continued_fraction" | "convergents"
15822            | "best_rational_approximation" | "best_rational"
15823            // ── Tier B: combinatorial sequences ───────────────────────────
15824            | "motzkin_number" | "motzkin"
15825            | "narayana_number" | "narayana"
15826            | "delannoy_number" | "delannoy"
15827            | "schroder_number" | "schroder" | "large_schroder"
15828            | "small_schroder_number" | "small_schroder"
15829            | "eulerian_number"
15830            | "bernoulli_polynomial" | "euler_polynomial"
15831            | "pell_number" | "pell" | "pell_lucas_number" | "pell_lucas"
15832            | "perrin_number" | "perrin" | "padovan_number" | "padovan"
15833            // ── Tier C: linear algebra extras ─────────────────────────────
15834            | "kronecker_product" | "tensor_product" | "tensor_contract"
15835            | "matrix_rank" | "mrank"
15836            | "companion_matrix" | "companion"
15837            | "characteristic_polynomial" | "charpoly"
15838            | "singular_values" | "svals"
15839            | "nullspace" | "null_space" | "kernel"
15840            // ── Tier D: polynomial algebra ────────────────────────────────
15841            | "polynomial_gcd" | "polygcd"
15842            | "polynomial_quotient" | "polyquot"
15843            | "polynomial_remainder" | "polyrem"
15844            | "polynomial_resultant" | "resultant"
15845            | "polynomial_discriminant" | "discriminant"
15846            | "polynomial_roots" | "polyroots"
15847            // ── Tier E: more distributions ────────────────────────────────
15848            | "gumbel_pdf" | "gumbel_cdf" | "gumbel_quantile"
15849            | "frechet_pdf" | "frechet_cdf" | "frechet_quantile"
15850            | "logistic_pdf" | "logistic_cdf" | "logistic_quantile"
15851            | "rayleigh_pdf" | "rayleigh_cdf" | "rayleigh_quantile"
15852            | "inverse_gamma_pdf" | "inverse_gamma_cdf" | "inverse_gamma_quantile"
15853            | "kumaraswamy_pdf" | "kumaraswamy_cdf" | "kumaraswamy_quantile"
15854            // ── Tier F: Mathieu ───────────────────────────────────────────
15855            | "mathieu_a" | "mathieu_characteristic_a"
15856            | "mathieu_ce" | "mathieu_se"
15857            // ── Tier G: Heun general ──────────────────────────────────────
15858            | "heun_g"
15859            // ── Tier H: wavelets ──────────────────────────────────────────
15860            | "haar_transform" | "haar" | "haar_inverse" | "ihaar"
15861            | "daubechies_db4" | "db4" | "daubechies_db4_inverse" | "idb4"
15862            // ── Tier I: graph algorithms ──────────────────────────────────
15863            | "topo_sort_adj"
15864            | "scc_tarjan" | "tarjan_scc" | "strongly_connected"
15865            | "bipartite_q" | "is_bipartite"
15866            | "max_flow_edmonds_karp" | "max_flow" | "edmonds_karp"
15867            | "min_cut" | "eccentricity"
15868            | "graph_diameter" | "graph_radius"
15869            // ── Tier J: misc fillers ──────────────────────────────────────
15870            | "stieltjes_constant" | "stieltjes"
15871            | "gauss_sum" | "kloosterman_sum"
15872            | "eta_quotient" | "root_approximant"
15873            // ── vector calculus ──────────────────────────────────
15874            | "numerical_gradient" | "ngrad"
15875            | "numerical_jacobian" | "njac"
15876            | "numerical_hessian" | "nhess"
15877            | "numerical_divergence" | "ndiv"
15878            | "numerical_curl" | "ncurl"
15879            | "numerical_laplacian" | "nlap"
15880            // ── optimization ─────────────────────────────────────
15881            | "nelder_mead" | "simplex_min"
15882            | "gradient_descent" | "gd_min"
15883            | "bfgs_minimize" | "bfgs"
15884            | "levenberg_marquardt" | "lev_marq" | "lm_min"
15885            | "conjugate_gradient" | "cg_solve"
15886            | "least_squares" | "lstsq"
15887            // ── integration extras ───────────────────────────────
15888            | "romberg" | "romberg_int"
15889            | "gauss_legendre_quad" | "glquad" | "gl_quad"
15890            | "monte_carlo_integrate" | "mc_int"
15891            | "adaptive_simpson" | "asimp"
15892            // ── LA extras ────────────────────────────────────────
15893            | "lu_decompose" | "ludec"
15894            | "qr_decompose" | "qrdec"
15895            | "householder_reflector" | "householder"
15896            | "givens_rotation" | "givens"
15897            | "forward_substitute" | "fwdsub"
15898            | "back_substitute" | "backsub"
15899            | "hessenberg_reduce" | "hessen"
15900            // ── polynomial helpers ───────────────────────────────
15901            | "poly_derivative" | "polyder"
15902            | "poly_integrate" | "polyint"
15903            | "poly_compose" | "poly_eval_horner" | "horner"
15904            | "pade_approximant" | "pade"
15905            // ── quaternions ──────────────────────────────────────
15906            | "quat_mul" | "quat_conj" | "quat_norm" | "quat_inv"
15907            | "quat_from_axis_angle" | "axis_angle_to_quat"
15908            | "quat_to_axis_angle"
15909            | "quat_to_matrix" | "quat_from_matrix" | "matrix_to_quat"
15910            | "quat_slerp" | "slerp"
15911            | "euler_zyx_to_matrix" | "matrix_to_euler_zyx"
15912            | "rotate_3d_vec"
15913            // ── information theory ───────────────────────────────
15914            | "kl_divergence" | "kl_div"
15915            | "js_divergence" | "js_div"
15916            | "mutual_information" | "mi"
15917            | "cross_entropy_arr" | "cross_entropy_dist"
15918            | "renyi_entropy" | "tsallis_entropy"
15919            // ── quantum ──────────────────────────────────────────
15920            | "pauli_x" | "pauli_y" | "pauli_z"
15921            | "pauli_id" | "pauli_i" | "pauli_identity"
15922            | "ket_bra" | "density_matrix" | "expectation_value" | "expval"
15923            | "commutator" | "anticommutator"
15924            | "partial_trace" | "ptrace"
15925            | "von_neumann_entropy" | "vn_entropy"
15926            // ── stat mech ────────────────────────────────────────
15927            | "bose_einstein" | "fermi_dirac"
15928            | "maxwell_boltzmann_speed" | "mb_speed"
15929            | "partition_function" | "z_partition"
15930            | "helmholtz_free_energy" | "free_energy_f"
15931            | "boltzmann_factor"
15932            | "einstein_specific_heat" | "einstein_cv"
15933            // ── optics ───────────────────────────────────────────
15934            | "fresnel_reflection_te" | "fresnel_reflection_tm"
15935            | "fresnel_transmission_te" | "fresnel_transmission_tm"
15936            | "abcd_thin_lens" | "abcd_free_space"
15937            | "gaussian_beam_q"
15938            // ── astrodynamics ────────────────────────────────────
15939            | "kepler_solve"
15940            | "true_to_eccentric" | "eccentric_to_mean"
15941            | "julian_date" | "j_date"
15942            | "jd_to_gregorian" | "jd_to_date"
15943            | "sidereal_time_gmst" | "gmst"
15944            | "vis_viva" | "orbital_period_kepler"
15945            | "orbital_elements_to_state" | "elem_to_state"
15946            // ── time series ──────────────────────────────────────
15947            | "kalman_step" | "kalman_filter"
15948            | "exponential_smoothing" | "exp_smooth"
15949            | "holt_winters" | "arma_yw_fit" | "ar_yw"
15950            // ── graph centrality ─────────────────────────────────
15951            | "pagerank" | "betweenness_centrality" | "closeness_centrality"
15952            | "eigenvector_centrality" | "degree_centrality" | "triangle_count"
15953            // ── random samplers ──────────────────────────────────
15954            | "rgumbel" | "rfrechet" | "rrayleigh"
15955            | "rlogistic" | "rkumaraswamy" | "rinverse_gamma" | "rinvgamma"
15956            // ── 2D geometry ──────────────────────────────────────
15957            | "graham_scan" | "convex_hull_2d"
15958            | "line_line_intersect_2d" | "ll_intersect_2d"
15959            | "point_segment_distance" | "p_seg_dist"
15960            // ── auto-diff ────────────────────────────────────────
15961            | "forward_diff" | "fdiff"
15962            | "forward_diff_grad" | "fdiff_grad"
15963            // ── stat tests ───────────────────────────────────────
15964            | "bartlett_test" | "levene_test"
15965            | "fishers_exact_test_2x2" | "fishers_exact"
15966            | "mcnemar_test"
15967            | "runs_test" | "wald_wolfowitz"
15968            | "friedman_test" | "kruskal_wallis_test" | "kruskal"
15969            | "sign_test"
15970            | "anderson_darling_normality" | "ad_normality"
15971            | "jarque_bera_test" | "jb_test"
15972            | "ljung_box_test" | "ljung_box"
15973            | "durbin_watson_stat" | "durbin_watson"
15974            // ── distance metrics ─────────────────────────────────
15975            | "mahalanobis_distance" | "mahalanobis_dist"
15976            | "cosine_distance" | "canberra_distance"
15977            | "bray_curtis_distance" | "bray_curtis"
15978            | "l1_distance"
15979            | "chi_squared_distance"
15980            // ── more distributions ───────────────────────────────
15981            | "multivariate_normal_pdf" | "mvn_pdf"
15982            | "multivariate_normal_sample" | "rmvn"
15983            | "dirichlet_pdf" | "dirichlet_sample" | "rdirichlet"
15984            | "skellam_pmf"
15985            | "inverse_gaussian_pdf" | "wald_pdf"
15986            | "inverse_gaussian_cdf" | "wald_cdf"
15987            | "inverse_gaussian_sample" | "rwald"
15988            | "non_central_chi2_pdf" | "ncchi2_pdf"
15989            // ── matrix functions ─────────────────────────────────
15990            | "matrix_exp" | "expm" | "matrix_log" | "logm"
15991            | "matrix_sqrt" | "sqrtm" | "matrix_sin" | "sinm"
15992            | "matrix_cos" | "cosm"
15993            // ── adaptive ODE ─────────────────────────────────────
15994            | "rk45_dormand_prince" | "rk45" | "dopri5"
15995            | "midpoint_step" | "ode_midpoint"
15996            | "heun_step" | "ode_heun"
15997            | "verlet_step" | "ode_verlet"
15998            // ── GLM ──────────────────────────────────────────────
15999            | "logistic_regression" | "logit_fit"
16000            | "poisson_regression"
16001            | "ridge_regression" | "ridge"
16002            | "lasso_coord" | "lasso"
16003            // ── bootstrap/resampling ─────────────────────────────
16004            | "bootstrap_mean_ci" | "boot_mean_ci"
16005            | "jackknife_estimate" | "jackknife"
16006            | "permutation_test_diff" | "perm_test_diff"
16007            // ── time series extras ───────────────────────────────
16008            | "acf_at_lag" | "diff_op" | "lag_op"
16009            | "decompose_classical" | "decompose_ts"
16010            // ── combinatorial generators ─────────────────────────
16011            | "combinations_list" | "permutations_list"
16012            | "cyclic_permutations" | "subsets_of_size"
16013            // ── DP utilities ─────────────────────────────────────
16014            | "longest_increasing_subseq" | "lis"
16015            | "knapsack_01" | "knapsack"
16016            | "subset_sum_target" | "subset_sum"
16017            | "coin_change_min" | "coin_change_minimum"
16018            | "edit_distance_levenshtein" | "edit_distance"
16019            // ── ML metrics ───────────────────────────────────────
16020            | "one_hot_encode" | "onehot" | "label_encode"
16021            | "categorical_cross_entropy" | "cce"
16022            | "classification_metrics" | "binary_metrics"
16023            | "roc_auc" | "auroc"
16024            // ── DSP / image filters ──────────────────────────────
16025            | "gaussian_blur_kernel" | "sobel_x" | "sobel_y"
16026            | "prewitt_x" | "prewitt_y"
16027            | "laplacian_of_gaussian" | "log_kernel"
16028            // ── stochastic processes ─────────────────────────────
16029            | "brownian_path" | "wiener_path"
16030            | "geometric_brownian_path" | "gbm_path"
16031            | "poisson_process" | "random_walk_1d"
16032            // ── compression / info ───────────────────────────────
16033            | "lempel_ziv_complexity" | "lz_complexity"
16034            | "huffman_code_lengths" | "huffman"
16035            | "shannon_entropy_rate" | "block_entropy_rate"
16036            // ── physics / quantum ────────────────────────────────
16037            | "planck_blackbody" | "blackbody"
16038            | "rayleigh_jeans" | "compton_shift"
16039            | "rydberg_energy"
16040            | "hydrogen_radial_wavefunction" | "h_rad_psi"
16041            // ── number theory / algebra ──────────────────────────
16042            | "integer_log" | "ilog"
16043            | "aks_primality" | "aks"
16044            | "elliptic_curve_add" | "ec_add"
16045            | "berlekamp_massey" | "bm_lfsr"
16046            | "bezout_coefficients" | "bezout" | "extended_euclid"
16047            // ── CAS-lite ─────────────────────────────────────────
16048            | "factor_quadratic" | "complete_square"
16049            | "partial_fraction_simple" | "partial_fraction"
16050            // ── more quadrature ──────────────────────────────────
16051            | "gauss_chebyshev_quad" | "gc_quad"
16052            | "gauss_hermite_quad" | "gh_quad"
16053            | "gauss_laguerre_quad" | "glag_quad"
16054            | "clenshaw_curtis_quad" | "cc_quad"
16055            | "tanh_sinh_quad" | "ts_quad"
16056            | "gauss_legendre_2d" | "gl_2d"
16057            | "monte_carlo_2d" | "mc_2d"
16058            // ── more optimization ────────────────────────────────
16059            | "simulated_annealing" | "sa_min"
16060            | "simplex_lp" | "lp_simplex"
16061            | "particle_swarm" | "pso_min"
16062            // ── distributions ────────────────────────────────────
16063            | "gev_pdf" | "gev_cdf" | "gev_sample" | "rgev"
16064            | "gen_pareto_pdf" | "gen_pareto_cdf"
16065            | "gen_pareto_sample" | "rgenpareto"
16066            | "skew_normal_pdf" | "skew_normal_cdf"
16067            | "mixture_normal_pdf"
16068            | "categorical_sample" | "rcat"
16069            | "multinomial_pmf" | "multinomial_sample" | "rmultinom"
16070            | "truncated_normal_pdf"
16071            | "truncated_normal_sample" | "rtnorm"
16072            // ── clustering ───────────────────────────────────────
16073            | "dbscan" | "gmm_em_1d" | "gmm_1d"
16074            | "silhouette_score"
16075            | "davies_bouldin_index" | "db_index"
16076            | "calinski_harabasz_index" | "ch_index"
16077            | "mds_2d" | "pcoa_2d" | "mean_shift"
16078            // ── NN primitives ────────────────────────────────────
16079            | "batch_norm" | "layer_norm"
16080            | "dropout_mask"
16081            | "max_pool_1d" | "avg_pool_1d"
16082            | "attention_softmax" | "positional_encoding"
16083            | "glorot_init" | "xavier_init"
16084            | "he_init" | "kaiming_init"
16085            | "adam_step" | "rmsprop_step"
16086            // ── time series ──────────────────────────────────────
16087            | "ewma" | "ccf" | "periodogram"
16088            | "welch_psd" | "welch"
16089            | "lag_features"
16090            // ── image processing ─────────────────────────────────
16091            | "median_filter_2d"
16092            | "threshold_otsu" | "otsu"
16093            | "histogram_equalize" | "hist_eq"
16094            | "erode_2d" | "dilate_2d"
16095            // ── losses ───────────────────────────────────────────
16096            | "mse_loss" | "mae_loss" | "huber_loss"
16097            // ── spatial ──────────────────────────────────────────
16098            | "vincenty_distance" | "vincenty"
16099            | "mercator_project"
16100            | "destination_from_bearing" | "dest_bearing"
16101            // ── integer sequences ────────────────────────────────
16102            | "recaman" | "recaman_seq"
16103            | "sylvester" | "sylvester_seq"
16104            | "happy_q" | "is_happy"
16105            | "amicable_pair_q"
16106            | "aliquot_sequence"
16107            | "magic_constant"
16108            // ── graph metrics ────────────────────────────────────
16109            | "clustering_coefficient_local" | "cc_local"
16110            | "clustering_coefficient_global" | "cc_global"
16111            | "assortativity" | "common_neighbors" | "jaccard_neighbors"
16112            | "adamic_adar"
16113            | "preferential_attachment_score" | "pa_score"
16114            // ── 3D geometry ──────────────────────────────────────
16115            | "triangle_3d_normal" | "triangle_3d_area"
16116            | "tetrahedron_volume"
16117            | "plane_from_3_points" | "plane_from_pts"
16118            | "point_to_plane_distance" | "pt_plane_dist"
16119            | "ray_triangle_intersect" | "moller_trumbore"
16120            | "ray_sphere_intersect" | "aabb_overlap"
16121            // ── iterative solvers ────────────────────────────────
16122            | "gauss_seidel"
16123            | "jacobi_iteration" | "jacobi_solve"
16124            | "sor_solve" | "sor"
16125            | "thomas_tridiag_solve" | "thomas"
16126            | "richardson_extrapolation" | "richardson"
16127            | "finite_difference_5pt" | "fd5pt"
16128            // ── crypto / algebra ─────────────────────────────────
16129            | "tonelli_shanks_sqrt" | "tonelli_shanks"
16130            | "baby_step_giant_step" | "bsgs"
16131            | "pollard_rho_factor" | "pollard_rho"
16132            | "modular_lcm" | "mlcm"
16133            | "crt_general" | "crt_arbitrary"
16134            // ── physics / chemistry ──────────────────────────────
16135            | "van_der_waals_p" | "vdw_pressure"
16136            | "nernst_equation" | "nernst"
16137            | "arrhenius_rate" | "arrhenius"
16138            | "reduced_mass"
16139            | "ph_to_concentration" | "ph_to_h"
16140            // ── MCMC / SDE / HMM ─────────────────────────────────
16141            | "metropolis_hastings" | "mh_sampler"
16142            | "gibbs_sampler_step" | "gibbs_step"
16143            | "euler_maruyama" | "em_sde"
16144            | "milstein" | "milstein_sde"
16145            | "ornstein_uhlenbeck_path" | "ou_path"
16146            | "hmm_forward" | "hmm_viterbi" | "hmm_backward"
16147            // ── survival / alignment ─────────────────────────────
16148            | "kaplan_meier" | "km_estimator" | "log_rank_test"
16149            | "needleman_wunsch" | "nw_align"
16150            | "smith_waterman" | "sw_align"
16151            // ── chemistry ────────────────────────────────────────
16152            | "gibbs_free_energy" | "delta_g"
16153            | "henderson_hasselbalch" | "hh_eq"
16154            | "radioactive_decay"
16155            | "half_life_to_constant" | "hl_to_lambda"
16156            // ── control theory ───────────────────────────────────
16157            | "pid_step"
16158            | "transfer_function_eval" | "tf_eval"
16159            | "bode_magnitude_db" | "bode_mag_db"
16160            | "bode_phase_deg"
16161            | "lqr_2x2"
16162            // ── game theory ──────────────────────────────────────
16163            | "nash_eq_2x2" | "nash_2x2"
16164            | "shapley_value" | "expected_utility"
16165            // ── operations research ──────────────────────────────
16166            | "hungarian_assignment" | "hungarian"
16167            | "tsp_nearest_neighbor" | "tsp_nn"
16168            | "vertex_cover_2approx" | "vc_2approx"
16169            // ── PDE ──────────────────────────────────────────────
16170            | "heat_eq_1d" | "wave_eq_1d"
16171            | "laplace_2d_jacobi" | "laplace_jacobi"
16172            // ── Bayesian conjugate ───────────────────────────────
16173            | "beta_binomial_update"
16174            | "normal_normal_update"
16175            | "gamma_poisson_update"
16176            | "dirichlet_multinomial_update"
16177            // ── quantum gates ────────────────────────────────────
16178            | "hadamard_gate" | "h_gate"
16179            | "cnot_gate" | "cx_gate"
16180            | "swap_gate" | "cz_gate"
16181            | "qft_matrix" | "phase_gate"
16182            | "s_gate" | "t_gate"
16183            // ── splines ──────────────────────────────────────────
16184            | "bezier_eval"
16185            | "catmull_rom_eval" | "cmr_eval"
16186            | "cubic_hermite_eval" | "ch_eval"
16187            | "bspline_basis" | "nik_basis"
16188            // ── music ────────────────────────────────────────────
16189            | "freq_to_midi" | "midi_to_freq"
16190            | "equal_temperament_freq"
16191            | "cents_difference" | "cents_diff"
16192            // ── astronomy ────────────────────────────────────────
16193            | "redshift_z" | "hubble_distance" | "luminosity_distance"
16194            // ── fluid dynamics ───────────────────────────────────
16195            | "reynolds_number" | "mach_number"
16196            | "prandtl_number" | "bernoulli_velocity"
16197            // ── distributions ────────────────────────────────────
16198            | "negative_binomial_pmf" | "nb_pmf"
16199            | "hypergeometric_pmf"
16200            | "beta_binomial_pmf" | "bb_pmf"
16201            | "von_mises_pdf" | "vmf_pdf"
16202            // ── random graphs ────────────────────────────────────
16203            | "erdos_renyi_random" | "erdos_renyi"
16204            | "barabasi_albert_random" | "barabasi_albert"
16205            | "watts_strogatz_random" | "watts_strogatz"
16206            // ── color science ────────────────────────────────────
16207            | "rgb_to_lab" | "lab_to_rgb"
16208            | "kelvin_to_rgb" | "color_temp_rgb"
16209            // ── integer sequences ────────────────────────────────
16210            | "bell_triangle" | "surjection_count"
16211            | "distinct_partition_count" | "q_partition"
16212            | "fibonacci_q" | "is_fib_number"
16213            // ── stats / divergences / distribs / physics / astro / chem ──
16214            | "bonferroni_correction" | "bonferroni"
16215            | "benjamini_hochberg" | "bh_fdr"
16216            | "tukey_hsd"
16217            | "hellinger_distance"
16218            | "wasserstein_1d" | "earth_movers_1d"
16219            | "chi_squared_divergence"
16220            | "beta_geometric_pmf"
16221            | "generalized_gamma_pdf" | "gengamma_pdf"
16222            | "zip_pmf" | "zero_inflated_poisson_pmf"
16223            | "stefan_boltzmann_luminosity" | "stellar_luminosity"
16224            | "photon_momentum" | "photon_energy_ev"
16225            | "dipole_radiation_power" | "larmor_power"
16226            | "parallax_to_distance" | "hawking_temperature"
16227            | "roche_limit" | "apparent_magnitude" | "distance_modulus"
16228            | "beer_lambert" | "absorbance"
16229            | "rate_law_n"
16230            | "freezing_point_depression" | "fpd"
16231            | "mixed_nash_2x2" | "minimax_2x2"
16232            // ── graphics / DSP / image / clustering / combinatorics / NT ─
16233            | "barycentric_coords_2d" | "barycentric_2d"
16234            | "bresenham_line" | "bilinear_interp_2d"
16235            | "point_in_polygon_2d"
16236            | "hilbert_transform" | "cepstrum"
16237            | "butterworth_lowpass_coeffs" | "butter_lp"
16238            | "savitzky_golay_coeffs" | "sg_coeffs"
16239            | "savitzky_golay_filter" | "sg_filter"
16240            | "canny_edge_intensity" | "canny_intensity"
16241            | "bilateral_filter_basic" | "bilateral_filter"
16242            | "kmeans_pp_init" | "kpp_init"
16243            | "elbow_score" | "wcss"
16244            | "young_tableaux_count" | "syt_count"
16245            | "euler_alt_permutation" | "euler_zigzag"
16246            | "genocchi_number" | "lattice_paths_count"
16247            | "tetration"
16248            | "ackermann_limited" | "ackermann"
16249            | "perfect_power_q" | "b_smooth_q"
16250            // ── networks / crypto / quantum / geom / TS ──────────
16251            | "k_core"
16252            | "rich_club_coefficient" | "rich_club"
16253            | "rsa_basic_encrypt" | "rsa_enc_int"
16254            | "rsa_basic_decrypt" | "rsa_dec_int"
16255            | "dh_shared_secret"
16256            | "bell_state_phi_plus" | "bell_phi_plus"
16257            | "bell_state_psi_minus" | "bell_psi_minus"
16258            | "density_matrix_purity" | "rho_purity"
16259            | "concurrence_2qubit"
16260            | "point_in_circle"
16261            | "circle_circle_intersect_2d"
16262            | "polygon_centroid"
16263            | "sutherland_hodgman_clip" | "sh_clip"
16264            | "kalman_rts_smoother" | "rts_smoother"
16265            // ── bioinformatics ───────────────────────────────────
16266            | "gc_content" | "codon_to_aa"
16267            | "reverse_complement_dna" | "rev_comp_dna"
16268            | "hamming_dna"
16269            | "blosum62_pair_score" | "blosum62"
16270            | "kmer_count"
16271            // ── geographic ───────────────────────────────────────
16272            | "great_circle_bearing" | "gc_bearing"
16273            | "midpoint_lat_lon" | "mid_geo"
16274            | "utm_zone_for"
16275            | "area_polygon_lat_lon" | "geo_polygon_area"
16276            // ── finance ──────────────────────────────────────────
16277            | "crr_binomial_option" | "crr_option"
16278            | "bond_price_clean"
16279            | "bond_yield_to_maturity" | "bond_ytm"
16280            | "modified_duration_bond"
16281            | "convexity_bond" | "bond_convexity"
16282            // ── image quality ────────────────────────────────────
16283            | "ssim" | "psnr" | "mssim"
16284            // ── acoustics ────────────────────────────────────────
16285            | "db_spl_from_pa" | "db_spl"
16286            | "a_weighting_factor" | "a_weight"
16287            | "octave_band_center" | "octave_center"
16288            | "semitone_ratio"
16289            // ── genetics ─────────────────────────────────────────
16290            | "hardy_weinberg"
16291            | "expected_heterozygosity" | "het_e"
16292            | "fst_simple"
16293            | "allele_frequencies"
16294            // ── epidemiology ─────────────────────────────────────
16295            | "sir_step" | "sir_r0" | "doubling_time"
16296            // ── economics ────────────────────────────────────────
16297            | "theil_index"
16298            | "herfindahl_hirschman" | "hhi"
16299            | "atkinson_index"
16300            | "lorenz_curve_points"
16301            // ── APL/J primitives ─────────────────────────────────
16302            | "iota_range" | "iota"
16303            | "reshape_array" | "reshape"
16304            | "grade_up" | "grade_asc"
16305            | "grade_down" | "grade_desc"
16306            // ── plasma physics ───────────────────────────────────
16307            | "plasma_frequency" | "omega_p"
16308            | "debye_length" | "lambda_d"
16309            | "cyclotron_frequency" | "omega_c"
16310            | "larmor_radius" | "gyroradius"
16311            // ── string similarity ────────────────────────────────
16312            | "jaro_winkler_similarity" | "jaro_winkler"
16313            | "metaphone_simple"
16314            // ── rating systems ───────────────────────────────────
16315            | "elo_rating_update" | "elo"
16316            | "glicko_rating_update" | "glicko"
16317            | "dice_sum_pmf"
16318            // ── effect sizes ─────────────────────────────────────
16319            | "cohens_d" | "effect_size_d"
16320            | "cliff_delta"
16321            | "vargha_delaney_a12" | "a12"
16322            // ── control transient ────────────────────────────────
16323            | "step_response_2nd_order" | "step_2nd"
16324            | "overshoot_2nd_order" | "overshoot_pct"
16325            // ── matrix norms ─────────────────────────────────────
16326            | "frobenius_norm"
16327            | "spectral_norm" | "operator_norm_2"
16328            | "trace_matrix" | "tr_mat"
16329            // ── networks ─────────────────────────────────────────
16330            | "homophily_index" | "homophily"
16331            | "dyad_census" | "triad_census"
16332            // ── misc ─────────────────────────────────────────────
16333            | "sigmoid_inverse" | "logit"
16334            // ── list / string / date / color / music / astro / perm / linguistics / regression / combinatorics / PRNG ──
16335            | "partition_at" | "drop_at" | "insert_at_idx"
16336            | "replace_at_index" | "set_at"
16337            | "swap_indices" | "nth_largest" | "nth_smallest"
16338            | "position_of_all_matching" | "positions_of_all"
16339            | "string_take_first" | "string_take_last"
16340            | "string_drop_first" | "string_drop_last"
16341            | "pluralize_simple"
16342            | "singularize_simple" | "singularize"
16343            | "capitalize_words" | "title_words"
16344            | "format_table_simple" | "ascii_table"
16345            | "days_between" | "weeks_between"
16346            | "months_between" | "years_between"
16347            | "first_of_month" | "last_of_month"
16348            | "day_of_week_iso" | "iso_dow"
16349            | "easter_sunday" | "chinese_zodiac"
16350            | "iso_week_number" | "iso_week"
16351            | "relative_luminance" | "wcag_luminance"
16352            | "contrast_ratio_wcag" | "wcag_contrast"
16353            | "delta_e_76" | "delta_e"
16354            | "color_blend_t" | "lerp_color"
16355            | "chord_to_freqs" | "scale_to_intervals"
16356            | "interval_semitones"
16357            | "transpose_freq_semitones" | "transpose_semi"
16358            | "bpm_to_period" | "midi_to_pitch_class"
16359            | "key_signature_for" | "circle_of_fifths_step"
16360            | "moon_phase" | "equation_of_time"
16361            | "solar_declination" | "sidereal_day_period" | "ecliptic_obliquity"
16362            | "permutation_order"
16363            | "permutation_parity" | "perm_sign"
16364            | "identity_permutation"
16365            | "permutation_compose" | "perm_mul"
16366            | "flesch_reading_ease" | "flesch_kincaid_grade"
16367            | "gunning_fog"
16368            | "automated_readability_index" | "ari"
16369            | "lix"
16370            | "adjusted_r_squared" | "adj_r2"
16371            | "aic" | "bic"
16372            | "residuals_compute" | "compute_residuals"
16373            | "composition_count" | "weak_composition_count"
16374            | "necklace_count" | "bracelet_count"
16375            | "multiset_permutations_count" | "multinomial_count"
16376            | "pearson_hash_byte" | "pearson_hash"
16377            | "xorshift32_step" | "lcg_next_u32"
16378            | "fisher_yates_shuffle"
16379            // ── ──────────────────────────────────────────────────
16380            | "tetrahedral_number" | "square_pyramidal_number"
16381            | "octahedral_number" | "pentagonal_pyramidal_number"
16382            | "cake_number" | "cuban_number" | "centered_hexagonal_number"
16383            | "carmichael_q" | "is_carmichael"
16384            | "sphenic_q" | "is_sphenic"
16385            | "seven_smooth_q" | "is_7_smooth"
16386            | "cartesian_product_n" | "cart_n"
16387            | "multiset_union" | "multiset_intersection" | "multiset_difference"
16388            | "polynomial_roots_dk" | "durand_kerner"
16389            | "lin_bairstow_step" | "bairstow"
16390            | "heap_sift_down"
16391            | "fenwick_build" | "bit_build"
16392            | "fenwick_query" | "bit_query"
16393            | "segment_tree_sum" | "seg_sum"
16394            | "kmp_failure" | "kmp"
16395            | "z_array" | "z_func"
16396            | "suffix_array_naive"
16397            | "manacher_radii" | "manacher"
16398            | "rabin_karp_hash" | "lcp_array"
16399            | "regex_escape_simple"
16400            | "horspool_search" | "bm_horspool"
16401            | "lpt_schedule" | "lpt"
16402            | "johnsons_rule" | "johnson_2m"
16403            | "bit_reverse_32" | "bit_reverse"
16404            | "bin_to_gray" | "gray_to_bin"
16405            | "swap_bits_pos" | "swap_bits"
16406            | "hamming_weight" | "popcnt"
16407            | "hamming_distance_int" | "hamdist_int"
16408            | "internal_rate_of_return"
16409            | "modified_irr" | "mirr"
16410            | "payback_period_simple" | "payback_simple"
16411            | "rfc3339_format" | "rfc3339"
16412            | "rfc3339_parse"
16413            | "iso_ordinal_date" | "ordinal_date"
16414            // ── ──────────────────────────────────────────────────
16415            | "lazy_caterer" | "central_polygonal"
16416            | "centered_square" | "centered_triangular" | "centered_pentagonal"
16417            | "star_number" | "dodecahedral_number" | "icosahedral_number"
16418            | "pronic_number" | "squared_triangular"
16419            | "woodall_number" | "cullen_number"
16420            | "repunit" | "repdigit" | "kaprekar_routine_step"
16421            | "smith_q"
16422            | "keith_q" | "is_keith"
16423            | "armstrong_q" | "is_armstrong"
16424            | "fnv1a_hash" | "djb2_hash"
16425            | "jenkins_one_at_a_time" | "jenkins_oat"
16426            | "murmurhash3_x32"
16427            | "adler32_hash" | "crc16_ccitt"
16428            | "vec_dot"
16429            | "l1_norm" | "l2_norm" | "vec_l2"
16430            | "linf_norm" | "max_norm" | "lp_norm"
16431            | "unit_vector"
16432            | "vector_project" | "proj" | "vector_reject"
16433            | "orthogonalize_vectors" | "gram_schmidt"
16434            | "outer_product" | "vec_outer"
16435            | "matrix_diagonal" | "mdiagvec"
16436            | "matrix_anti_diagonal"
16437            | "matrix_symmetric_q" | "matrix_orthogonal_q"
16438            | "geometric_mean_arr" | "harmonic_mean_arr"
16439            | "quadratic_mean_arr" | "lehmer_mean"
16440            | "running_mean" | "running_variance"
16441            | "outlier_iqr_q" | "z_score_robust"
16442            | "geometric_sequence" | "arithmetic_sequence"
16443            | "log_sum_exp" | "lse"
16444            | "log_sigmoid" | "log1p_exp"
16445            | "string_chars"
16446            | "string_words_count" | "word_count_simple"
16447            | "string_lines_count" | "line_count_simple"
16448            | "string_intersperse" | "string_replicate"
16449            | "string_uniq_chars" | "string_letter_frequency"
16450            | "anagram_q" | "is_anagram_q"
16451            | "string_take_while" | "string_drop_while"
16452            | "string_split_at_first" | "string_partition_at_word"
16453            // ── ──────────────────────────────────────────────────
16454 | "relativistic_kinetic"
16455            | "lorentz_factor_v" | "doppler_relativistic"
16456            | "drag_force_quadratic" | "terminal_velocity"
16457            | "carnot_efficiency" | "otto_efficiency"
16458            | "brayton_efficiency" | "diesel_efficiency"
16459            | "specific_heat_const_v" | "speed_of_sound_ideal"
16460            | "kepler_period_au" | "synodic_period"
16461            | "hill_radius" | "jeans_length"
16462            | "chandrasekhar_mass" | "eddington_luminosity"
16463            | "schwarzschild_radius_m" | "gravity_at_radius"
16464            | "gravitational_pe"
16465            | "freefall_time" | "pendulum_freq" | "spring_period"
16466            | "centripetal_accel" | "lens_focal_length"
16467            | "avogadros_number" | "boltzmann_const"
16468            | "planck_const_h" | "gas_constant_r"
16469            | "concentration_dilute" | "partial_pressure"
16470            | "mole_fraction" | "molarity" | "molality"
16471            | "normality_chem" | "ionic_strength"
16472 | "titration_volume"
16473            | "atomic_radius_pm" | "de_broglie_wavelength_kg"
16474 | "lotka_volterra_step"
16475            | "michaelis_menten" | "hill_equation"
16476            | "lineweaver_burk" | "eadie_hofstee_y"
16477            | "arrhenius_temp_q10"
16478            | "body_surface_area_dubois" | "bsa_dubois"
16479            | "bmr_harris_benedict_male" | "bmr_harris_benedict_female"
16480            | "max_heart_rate" | "target_heart_rate"
16481            | "vo2_max_estimate" | "pulse_pressure"
16482            | "mean_arterial_pressure" | "map_bp"
16483            | "dew_point_magnus" | "heat_index_celsius"
16484            | "wind_chill_celsius" | "pressure_altitude_m"
16485            | "density_altitude_m" | "saturation_vapor_pressure"
16486            | "humidex" | "utci_simple"
16487            | "resistance_parallel" | "r_parallel"
16488            | "resistance_series" | "r_series"
16489            | "capacitance_parallel" | "c_parallel"
16490            | "capacitance_series" | "c_series"
16491            | "inductance_parallel" | "l_parallel"
16492            | "inductance_series" | "l_series"
16493            | "voltage_divider" | "current_divider"
16494            | "lc_resonant" | "q_factor_rlc"
16495            | "skin_depth" | "wire_resistance"
16496            | "motor_torque" | "efficiency_ratio"
16497            | "dB_voltage" | "db_voltage"
16498            | "dB_power" | "db_power"
16499            // ── ──────────────────────────────────────────────────
16500            | "bfs_distances" | "dfs_preorder" | "connected_components"
16501            | "graph_is_tree" | "graph_density"
16502            | "graph_average_degree" | "graph_max_degree" | "graph_min_degree"
16503            | "graph_complement"
16504            | "in_degree_directed" | "out_degree_directed"
16505            | "graph_eccentricity_all" | "is_connected"
16506            | "articulation_points" | "bridges_edges"
16507            | "eulerian_path_q" | "hamiltonian_brute"
16508            | "string_to_charcodes" | "charcodes_to_string"
16509            | "string_xor"
16510            | "string_camel_to_snake" | "string_snake_to_camel"
16511            | "string_kebab_to_snake" | "string_snake_to_kebab"
16512            | "palindromic_q" | "substring_count"
16513            | "string_truncate_ellipsis" | "string_expand_tabs"
16514            | "string_normalize_spaces"
16515 | "days_in_year" | "quarter_of_year"
16516            | "zeller_day_of_week" | "age_from_birthdate"
16517            | "business_days_between" | "unix_epoch_to_iso"
16518            | "loan_payment_pmt" | "loan_balance"
16519            | "amortization_total_interest"
16520            | "apr_to_apy" | "apy_to_apr"
16521            | "compound_interest_periods" | "simple_interest_compute"
16522
16523            | "perpetuity_value" | "growing_perpetuity"
16524            | "annuity_present_value" | "annuity_future_value"
16525            | "capm_expected_return"
16526            | "treynor_ratio"
16527            | "jensens_alpha" | "information_ratio"
16528            | "friction_factor_laminar" | "swamee_jain_factor"
16529            | "pipe_pressure_drop" | "orifice_velocity"
16530            | "chezy_velocity" | "manning_velocity"
16531            | "froude_number" | "weber_number" | "grashof_number"
16532            | "nusselt_dittus_boelter"
16533            // ── more extensions ────────────────────────────────────────────
16534            | "mollweide_project" | "robinson_project" | "sinusoidal_project"
16535            | "equirectangular_project" | "lambert_azimuthal_project" | "albers_conic_project"
16536            | "geohash_encode" | "geohash_decode" | "geohash_neighbor" | "geohash_bbox"
16537            | "gabor_kernel" | "unsharp_mask_kernel" | "emboss_kernel"
16538            | "box_blur_kernel" | "motion_blur_kernel" | "sharpen_kernel"
16539            | "edge_detect_kernel" | "sobel_diagonal_kernel" | "haar_2d_step"
16540            | "db4_coeffs" | "db6_coeffs" | "sym4_coeffs" | "coif1_coeffs"
16541            | "aes_sbox_byte" | "aes_inv_sbox_byte"
16542            | "chacha20_qround" | "xtea_round" | "speck_round" | "simon_round"
16543            | "kepler_hyperbolic" | "hohmann_dv1" | "hohmann_dv2" | "hohmann_total"
16544            | "bielliptic_total" | "lambert_simple"
16545            | "horizon_distance" | "solar_zenith_angle" | "air_mass_kasten"
16546            | "solar_constant" | "julian_centuries_j2000"
16547            | "mean_solar_longitude" | "mean_solar_anomaly" | "lst_to_solar"
16548            | "ra_dec_to_az_alt" | "ecliptic_to_equatorial" | "equatorial_to_galactic"
16549            | "orbital_eccentricity" | "semi_major_axis"
16550            | "specific_orbital_energy" | "specific_angular_momentum"
16551            | "toffoli_gate" | "ccx_gate" | "fredkin_gate" | "cswap_gate"
16552            | "iswap_gate" | "sqrt_swap_gate"
16553            | "rx_gate" | "ry_gate" | "rz_gate"
16554            | "ghz_state_n" | "w_state_n"
16555            | "depolarizing_channel" | "dephasing_channel" | "amplitude_damping_channel"
16556            | "quantum_fidelity_pure" | "trace_distance"
16557            | "bell_inequality_chsh" | "pauli_decomposition_2x2"
16558            | "quantum_relative_entropy" | "qft_4_real"
16559            | "bwt_encode" | "bwt_decode" | "mtf_encode" | "mtf_decode"
16560
16561            | "lyndon_factorize" | "christoffel_word" | "sturmian_word"
16562            | "z_function_alt" | "period_of_string" | "borders_of_string"
16563            | "thue_morse_string" | "fibonacci_word"
16564            | "mann_kendall_tau" | "theil_sen_slope" | "hodges_lehmann"
16565            | "huber_m_estimator" | "winsorized_variance_arr"
16566            | "bowley_skewness" | "pearson_skewness_2"
16567            | "concordance_correlation" | "quantile_p"
16568            | "label_propagation_step" | "modularity_q"
16569            | "clique_count_3" | "local_efficiency" | "global_efficiency"
16570            | "diameter_unweighted"
16571            | "aitken_delta_squared" | "wynn_epsilon"
16572            | "shanks_transform" | "levin_t_transform"
16573            | "harmonic_seq_sum" | "alternating_seq_sum"
16574            // ── more extensions (2) ────────────────────────────────────────
16575            | "sparse_csr_build" | "sparse_csr_mul_vec" | "sparse_density"
16576            | "lower_triangular_q" | "upper_triangular_q"
16577            | "diagonal_dominance_q" | "matrix_zero_q" | "matrix_identity_q"
16578            | "matrix_random_uniform" | "matrix_random_normal"
16579            | "andrew_monotone_chain" | "polygon_area_signed"
16580            | "polygon_convex_q" | "iou_2d_axis_aligned" | "hausdorff_distance_2d"
16581            | "minkowski_sum_simple" | "circle_3_points"
16582            | "polygon_winding_number" | "segment_length"
16583            | "segments_parallel_q" | "segments_perpendicular_q"
16584            | "burr_xii_pdf" | "burr_xii_cdf" | "dagum_pdf" | "lomax_pdf"
16585            | "birnbaum_saunders_pdf" | "tukey_lambda_quantile"
16586            | "half_cauchy_pdf" | "half_logistic_pdf" | "reciprocal_pdf"
16587            | "levy_pdf" | "voigt_profile_simple"
16588            | "gompertz_pdf" | "inverse_weibull_pdf"
16589            | "log_gamma_simple" | "inverse_chi2_pdf"
16590            | "poly1305_block_step" | "x25519_field_mul" | "curve25519_mul_simple"
16591            | "secp256k1_y_recover" | "hmac_step_xor"
16592            | "pkcs7_pad" | "pkcs7_unpad" | "xor_byte_string"
16593 | "atbash_cipher"
16594            | "vigenere_encrypt" | "vigenere_decrypt" | "xor_brute_keylen"
16595            | "arima_diff" | "seasonal_diff"
16596            | "garch_step" | "egarch_step"
16597            | "realized_volatility" | "max_drawdown_arr"
16598            | "calmar_ratio" | "omega_ratio" | "kelly_criterion"
16599            | "var_historical" | "cvar_historical"
16600            | "graph_degree_distribution" | "graph_count_edges"
16601            | "graph_bipartite_match_simple" | "graph_count_triangles"
16602            | "graph_avg_clustering" | "graph_transitivity"
16603            | "graph_max_clique_brute" | "graph_independent_set_brute"
16604            | "graph_count_paths_length_k" | "graph_pagerank_simple"
16605            // ── integration / ODE / root finding / optimization ─
16606            | "boole_rule" | "boole_int"
16607            | "gauss_legendre_5" | "gl5"
16608            | "gauss_kronrod_15" | "gk15"
16609
16610            | "midpoint_rule"
16611            | "adams_bashforth_4" | "ab4"
16612            | "heun_method" | "rk45_cash_karp" | "rkck"
16613            | "milne_pc" | "milne"
16614            | "modified_midpoint_ode" | "modmidpoint"
16615            | "backward_euler" | "implicit_euler"
16616            | "crank_nicolson_ode" | "cn_ode"
16617            | "brent_root" | "brent" | "ridders_root" | "ridders"
16618            | "steffensen_root" | "steffensen" | "halley_root" | "halley"
16619            | "householder_root" | "muller_root" | "muller"
16620            | "regula_falsi" | "false_position"
16621            | "secant_root" | "secant"
16622            | "anderson_step" | "aberth_step" | "inverse_quad_interp"
16623            | "lm_step" | "gradient_descent_step"
16624 | "nesterov_step" | "adagrad_step"
16625            | "cg_beta_pr" | "cg_beta_fr" | "bfgs_h_update_1d"
16626            | "wolfe_strong_q" | "dogleg_step"
16627            | "nelder_mead_reflect" | "nelder_mead_expand" | "nelder_mead_contract"
16628            | "sa_accept_prob" | "sa_boltzmann_temp" | "sa_cauchy_temp"
16629            | "sa_geometric_temp" | "acceptance_target"
16630            // ── financial pricing models ────────────────────────
16631            | "bs_call" | "blackscholes_call" | "bs_put" | "blackscholes_put"
16632 | "bs_theta_call" | "bs_rho_call"
16633 | "bachelier_call" | "black76_call"
16634            | "crr_american_call" | "crr_american_put" | "jr_european_call"
16635            | "trinomial_call" | "heston_price_simple" | "sabr_implied_vol"
16636            | "merton_jump_call" | "asian_call_mc" | "barrier_up_out_call"
16637            | "digital_call" | "lookback_call"
16638            | "macaulay_duration" | "forward_rate"
16639            | "discount_continuous" | "ytm_newton"
16640            | "vasicek_bond" | "cir_bond" | "hull_white_drift"
16641            | "cds_upfront" | "black_karasinski_drift" | "quanto_adjustment"
16642            | "fx_forward" | "garman_kohlhagen_call" | "margrabe" | "stulz_min_call"
16643            | "sharpe_annualized"
16644            | "jensen_alpha" | "modified_sharpe"
16645            // ── chemistry ───────────────────────────────────────
16646            | "ph_from_h" | "poh_from_oh" | "pka_from_ka"
16647 | "henderson_base"
16648            | "arrhenius_k" | "eyring_k"
16649            | "first_order_concentration" | "first_order_half_life"
16650            | "second_order_concentration" | "second_order_half_life"
16651            | "zero_order_concentration"
16652
16653            | "ideal_gas_n" | "redlich_kwong_p"
16654            | "compressibility_z"
16655            | "kc_from_rates" | "kp_from_kc" | "reaction_quotient" | "rxn_q"
16656            | "le_chatelier_dir"
16657            | "dg_from_k" | "k_from_dg" | "vant_hoff" | "clausius_clapeyron" | "antoine_p"
16658 | "emf_from_half_cells" | "faraday_mass_deposited"
16659 | "transmittance" | "ksp_from_concs"
16660 | "debye_huckel"
16661            | "cp_monatomic_ideal" | "cv_monatomic_ideal"
16662            | "heat_capacity_q" | "calorimeter_dt" | "enthalpy_reaction"
16663            | "avogadro_count" | "moles_from_mass"
16664            | "dilution_v2" | "raoult_law" | "bp_elevation" | "fp_depression"
16665            | "osmotic_pressure" | "rydberg_lambda" | "bohr_radius_n"
16666            | "bohr_energy_ev" | "photon_energy_freq" | "photon_energy_lambda"
16667            | "de_broglie"
16668            // ── biology / ecology ───────────────────────────────
16669 | "logistic_growth_step" | "logistic_growth_analytic"
16670            | "gompertz_growth_step" | "allee_growth_step"
16671 | "growth_rate_from_ratio"
16672 | "seir_step" | "seird_step" | "sis_step"
16673            | "r0_basic" | "rt_effective" | "herd_immunity_threshold" | "generation_time"
16674 | "inverse_simpson"
16675            | "pielou_evenness" | "margalef_richness" | "menhinick_richness"
16676            | "berger_parker" | "sorensen_dice"
16677            | "rao_quadratic_entropy"
16678 | "selection_step" | "nei_genetic_distance"
16679            | "effective_pop_size" | "carrying_capacity_from_data"
16680            | "petersen_estimator" | "chapman_estimator"
16681            | "lv_competition_step"
16682            | "holling_type1" | "holling_type2" | "holling_type3"
16683            | "leslie_step" | "net_reproductive_rate" | "generation_time_demo"
16684            | "finite_rate_lambda" | "kleibers_law" | "bergmann_adjust"
16685            | "q10" | "species_area" | "intrinsic_growth_rate"
16686            | "macarthur_wilson_immigration" | "macarthur_wilson_extinction"
16687            | "island_equilibrium"
16688            // ── EM / optics / relativity ────────────────────────
16689 | "efield_point" | "epotential_point"
16690 | "capacitor_charge"
16691            | "ohm_voltage" | "power_vi" | "power_i2r"
16692
16693 | "capacitance_parallel_sum"
16694            | "bfield_wire" | "bfield_solenoid" | "lorentz_force_mag"
16695 | "faraday_emf"
16696 | "lc_frequency" | "lc_omega"
16697            | "rc_tau" | "rl_tau"
16698            | "poynting_magnitude" | "em_intensity" | "radiation_pressure"
16699            | "em_wavelength" | "em_frequency"
16700            | "snell_theta2"
16701            | "index_from_speed" | "fresnel_reflection_normal"
16702            | "fresnel_rs" | "fresnel_rp"
16703            | "lensmaker" | "thin_lens_v" | "mirror_equation_v"
16704            | "lens_magnification" | "diffraction_grating_angle"
16705            | "single_slit_min" | "rayleigh_resolution"
16706            | "lorentz_gamma"
16707            | "rel_momentum" | "rel_ke" | "rel_total_energy" | "rel_energy_pm"
16708            | "relativistic_doppler" | "rel_velocity_add"
16709
16710            | "wave_string_speed" | "sound_solid" | "sound_gas"
16711            | "doppler_classical" | "standing_wave_fundamental"
16712            | "open_pipe_harmonic" | "closed_pipe_harmonic"
16713            | "sound_db"
16714            | "alfven_speed"
16715            | "grav_time_dilation" | "grav_redshift"
16716            // ── graph algorithms ────────────────────────────────
16717            | "kosaraju_scc" | "bridges"
16718            | "max_flow_ek" | "min_cut_value" | "hopcroft_karp"
16719
16720 | "katz_centrality" | "hits_simple"
16721            | "pagerank_damped" | "cc_count" | "cc_labels"
16722            | "topological_sort_kahn" | "has_cycle_directed" | "has_cycle_undirected"
16723 | "diameter_bfs" | "radius_bfs"
16724            | "num_edges" | "k_coreness"
16725            | "greedy_coloring" | "chromatic_number_greedy"
16726            | "sum_degrees" | "avg_degree" | "max_degree"
16727            | "is_tree" | "girth"
16728            // ── signal processing ───────────────────────────────
16729            | "hamming_window" | "hann_window" | "blackman_window"
16730            | "blackman_harris_window" | "bartlett_window" | "welch_window"
16731            | "kaiser_window" | "tukey_window" | "gaussian_window"
16732            | "hilbert_envelope"
16733            | "biquad_step" | "biquad_lowpass_coeffs" | "biquad_highpass_coeffs"
16734            | "biquad_bandpass_coeffs" | "biquad_notch_coeffs" | "biquad_allpass_coeffs"
16735            | "biquad_peak_coeffs" | "biquad_lowshelf_coeffs" | "biquad_highshelf_coeffs"
16736            | "butterworth_prewarp" | "butterworth_order"
16737            | "fir_moving_average" | "fir_lowpass_design"
16738 | "spectrogram_simple"
16739            | "zero_pad" | "resample_nearest" | "resample_linear" | "quantize"
16740            | "mu_law_encode" | "mu_law_decode" | "a_law_encode" | "a_law_decode"
16741            | "chirp_linear"
16742            // ── cryptography deep ───────────────────────────────
16743            | "fnv1a_32" | "fnv1a_64" | "sdbm_hash"
16744            | "siphash24"
16745            | "pbkdf2_hmac_step" | "scrypt_round" | "bcrypt_cost_iters"
16746            | "argon2_block_mix" | "hkdf_expand_step"
16747            | "lfsr_galois_step" | "mt19937_temper" | "xorshift64" | "xorshift32"
16748            | "pcg32_step" | "lcg_numrec_step" | "splitmix64_step" | "wyhash_mix"
16749
16750            | "xor_cipher_byte"
16751            | "railfence_encrypt" | "beaufort" | "affine_encrypt" | "substitution_encrypt"
16752            | "letter_frequency" | "english_chi2" | "index_of_coincidence" | "kasiski_repeats"
16753            | "deterministic_prime" | "dh_shared" | "rsa_encrypt_simple"
16754            | "monobit_test" | "approximate_entropy"
16755            // ── ML extensions ───────────────────────────────────
16756            | "gini_impurity" | "entropy_bits" | "information_gain" | "gain_ratio"
16757            | "nb_gaussian_likelihood" | "nb_bernoulli_likelihood" | "nb_multinomial_log_likelihood"
16758            | "adaboost_alpha" | "hinge_loss" | "squared_hinge"
16759            | "logistic_loss"
16760 | "sigmoid_grad" | "tanh_grad"
16761 | "relu_grad"
16762 | "softsign" | "prelu" | "threshold_act"
16763            | "confusion_counts" | "mcc" | "f_beta" | "specificity"
16764            | "balanced_accuracy" | "cohen_kappa" | "brier_score" | "log_loss"
16765            | "tversky" | "mahalanobis_1d"
16766 | "one_hot" | "topk_indices"
16767            | "minmax_scale" | "zscore_norm" | "robust_scale"
16768            // ── geometry / topology ─────────────────────────────
16769            | "triangle_area_heron" | "triangle_area_pts"
16770            | "triangle_inradius" | "triangle_circumradius"
16771            | "regular_ngon_area" | "regular_ngon_inradius" | "regular_ngon_circumradius"
16772 | "n_ball_volume"
16773 | "cylinder_surface" | "cone_surface"
16774
16775            | "ellipsoid_volume" | "ellipsoid_surface_approx"
16776            | "dist_point_line_2d" | "dist_point_plane_3d" | "closest_pt_segment_2d"
16777            | "bbox_from_points"
16778 | "euclidean_distance_nd"
16779 | "hamming_distance_str"
16780 | "great_circle_law_of_cos"
16781            | "initial_bearing" | "midpoint_great_circle"
16782            | "shoelace_area" | "polygon_is_convex" | "convex_hull_jarvis"
16783            | "euler_characteristic" | "genus_from_euler"
16784            | "spherical_triangle_area" | "polygon_with_holes_area" | "picks_theorem"
16785            | "centroid_nd" | "covariance_matrix_pts" | "simplex_volume_3d"
16786            // ── special functions extra ─────────────────────────
16787            | "hyper2f1" | "hyper1f1" | "hyper0f1" | "pochhammer"
16788            | "mathieu_ce0" | "mathieu_se1" | "parabolic_d0" | "parabolic_d1"
16789            | "whittaker_m" | "struve_h0" | "struve_h1"
16790            | "lambert_w0" | "wright_omega"
16791            | "sinhc" | "cosh_minus1_over_x2"
16792            | "sine_integral_si" | "cosine_integral_ci" | "exp_integral_e1"
16793 | "dawson_function" | "owen_t"
16794            | "spherical_bessel_j0" | "spherical_bessel_j1"
16795            | "spherical_bessel_y0" | "spherical_bessel_y1"
16796            | "mod_sph_bessel_i0" | "mod_sph_bessel_i1" | "mod_sph_bessel_k0"
16797            | "coulomb_f0"
16798            | "polylog_li2" | "polylog_n"
16799
16800 | "ti2" | "clausen_cl2"
16801            | "bose_einstein_g" | "fermi_dirac_int"
16802            | "theta3" | "theta2"
16803            | "jacobi_sn_small_q" | "jacobi_cn_small_q" | "jacobi_dn_small_q"
16804            | "riemann_xi" | "bessel_jn_general" | "bessel_in_general"
16805            // ── astronomy / music / color / units ───────────────
16806 | "absolute_magnitude"
16807            | "pc_to_ly" | "ly_to_pc" | "pc_to_au" | "au_to_m"
16808            | "solar_mass_to_kg" | "solar_luminosity_to_w"
16809            | "hubble_distance_mpc" | "comoving_distance_approx" | "critical_density"
16810            | "et_freq_ratio" | "midi_to_hz" | "hz_to_midi" | "cents_between"
16811            | "just_intonation_ratio" | "pythagorean_ratio"
16812            | "beat_frequency" | "bpm_to_spb" | "note_name_to_midi"
16813 | "rgb_to_yiq" | "rgb_to_yuv601"
16814            | "srgb_to_xyz" | "xyz_to_lab" | "delta_e_94"
16815
16816
16817            | "feet_to_meters" | "meters_to_feet"
16818            | "lb_to_kg" | "kg_to_lb"
16819            | "mph_to_kmh" | "kmh_to_mph" | "mps_to_kmh" | "kmh_to_mps" | "knots_to_kmh"
16820 | "atm_to_pa" | "pa_to_atm" | "mmhg_to_pa"
16821            | "ev_to_joules" | "joules_to_ev" | "btu_to_joules" | "kwh_to_joules"
16822            | "bpm_to_midi_tick_us" | "iso226_phon_adjustment"
16823            | "db_to_amp" | "amp_to_db"
16824            | "roman_encode" | "roman_decode" | "number_to_english"
16825            // ── cosmology / GR / FLRW ───────────────────────────
16826            | "hubble_lcdm" | "hubble_time" | "hubble_distance_si" | "critical_density_si"
16827            | "comoving_distance" | "angular_diameter_distance"
16828            | "lookback_time" | "age_at_z" | "scale_factor" | "redshift_from_a"
16829            | "omega_m_at_z" | "lcdm_eos" | "cpl_w" | "deceleration_q"
16830            | "schwarzschild_radius_kg" | "kerr_ergosphere_eq" | "kerr_horizon"
16831 | "bh_entropy" | "bh_evaporation_time"
16832            | "schwarzschild_isco" | "photon_sphere_radius"
16833            | "tidal_force" | "grav_dilation_factor" | "lense_thirring_omega"
16834            | "gw_strain_amplitude" | "chirp_mass" | "grav_binding_energy"
16835            | "roche_limit_rigid" | "roche_limit_fluid"
16836            | "lagrange_l1" | "sphere_of_influence"
16837            | "freefall_velocity_schwarzschild" | "einstein_ring_radius"
16838            | "microlensing_magnification" | "cosmic_distance_modulus_si"
16839            | "cmb_temperature" | "cmb_temperature_at_z"
16840 | "stefan_boltzmann_si" | "planck_spectral_radiance"
16841            | "schwarzschild_g_tt" | "schwarzschild_g_rr" | "kretschmann_schwarzschild"
16842            | "hill_velocity" | "vacuum_energy_density"
16843            | "sound_horizon_recomb" | "bao_scale_today" | "sigma8_default"
16844            | "lensing_convergence" | "sigma_crit"
16845            | "perihelion_precession" | "shapiro_delay" | "light_deflection_angle"
16846 | "tov_mass_limit"
16847            | "main_sequence_lifetime" | "schwarzschild_freefall_time"
16848            | "friedmann_density_total" | "cosmological_constant"
16849
16850 | "planck_energy"
16851            // ── quantum mechanics deep ──────────────────────────
16852            | "pure_state_density" | "purity"
16853            | "linear_entropy" | "quantum_mutual_info"
16854 | "eof_from_concurrence"
16855            | "bell_state_index" | "chsh_expectation" | "tsirelson_bound"
16856            | "pauli_real_part" | "pauli_y_imag"
16857            | "bloch_to_density_real" | "bloch_purity_check"
16858            | "fidelity_pure_real" | "l1_coherence" | "relative_entropy_coherence"
16859            | "kraus_apply" | "bit_flip_prob" | "phase_flip_prob"
16860            | "depolarizing_density_2x2" | "amplitude_damping_excited"
16861            | "quantum_fisher_info" | "cramer_rao_bound" | "squeezing_db" | "heisenberg_min"
16862            | "coherent_mean_photons" | "thermal_mean_photons" | "poisson_photon_pmf"
16863            | "bose_einstein_pmf" | "mandel_q" | "g2_zero"
16864            | "free_particle_energy" | "infinite_well_energy" | "harmonic_oscillator_energy"
16865            | "hydrogen_energy_n" | "stark_shift_linear"
16866            | "zeeman_energy" | "larmor_frequency" | "rabi_frequency"
16867            | "schrodinger_step_real" | "probability_density" | "state_norm" | "state_normalize"
16868 | "quantum_variance" | "spin_casimir"
16869            | "cg_simple" | "wigner_3j_bound" | "qho_ground_state"
16870            | "tunneling_prob" | "gamow_factor" | "compton_wavelength" | "uncertainty_position"
16871            | "berry_phase_spin_half" | "zeno_survival" | "decoherence_time"
16872            | "ramsey_visibility" | "fermi_golden_rule"
16873            // ── bioinformatics deep ─────────────────────────────
16874            | "needleman_wunsch_score" | "smith_waterman_score" | "pam250_score"
16875            | "tanimoto_bits" | "translate_dna" | "transcribe_dna_rna" | "reverse_transcribe"
16876            | "at_content" | "tm_wallace" | "tm_marmur" | "codon_adaptation_index"
16877            | "kmer_jaccard" | "sequence_shannon_info" | "pwm_score"
16878            | "msa_column_entropy" | "seq_logo_information"
16879 | "damerau_levenshtein" | "lcs_length"
16880 | "hirschberg_lcs_length" | "common_kmers"
16881            | "jukes_cantor_distance" | "kimura_2p_distance" | "felsenstein_step"
16882            | "branch_length_substitutions" | "num_unrooted_trees" | "bayes_posterior"
16883            | "hw_expected_counts" | "allele_frequency" | "ld_d" | "ld_r_squared"
16884 | "heterozygosity" | "ne_from_variance"
16885            | "expected_coverage" | "lander_waterman_gaps"
16886            | "bh_adjusted_p" | "zscore_count"
16887 | "go_enrichment_p" | "blosum45_score"
16888            | "henikoff_weight" | "hamming_protein" | "codon_usage_variance"
16889            | "dnds_ratio" | "mutation_rate" | "tajimas_d" | "wattersons_theta"
16890            | "coalescent_expected_time" | "coalescent_tree_length" | "nm_from_fst"
16891            // ── ODE advanced ────────────────────────────────────
16892            | "bdf1_step" | "bdf2_step" | "bdf3_step" | "bdf4_step" | "bdf5_step" | "bdf6_step"
16893            | "ab1_step" | "ab2_step" | "ab3_step"
16894            | "am2_step" | "am3_step" | "am4_step"
16895            | "ros2_step" | "imex_euler_step" | "symplectic_euler_step"
16896            | "leapfrog_step" | "stormer_verlet_step"
16897            | "rk4_single" | "dopri5_combine" | "rkf45_error"
16898            | "lobatto_iiia_2" | "lobatto_iiic_3" | "gauss_irk_2_stage" | "magnus_1st"
16899            | "euler_lte" | "trapezoidal_lte" | "pi_step_size"
16900            | "stiffness_ratio" | "spectral_radius"
16901            | "heun_euler_step" | "bogacki_shampine_step" | "verner_8_combine"
16902            | "rk_combine" | "ab_coeff_sum"
16903            | "newmark_beta_step" | "wilson_theta_step"
16904            | "strang_split" | "lie_split"
16905            | "exp_euler_step" | "etd_rk2" | "dde_euler_step"
16906            | "em_step" | "milstein_step" | "heun_sde_step" | "stratonovich_correction"
16907            | "predictor_corrector" | "numerical_jacobian_col"
16908            | "cn_coefficient" | "imex_theta_split" | "bulirsch_stoer_step"
16909            | "cfl_number" | "diffusion_stability"
16910            | "lax_friedrichs_flux" | "lax_wendroff_flux"
16911            | "van_leer_limiter" | "minmod_limiter" | "superbee_limiter" | "mc_limiter"
16912            // ── cryptanalysis & number theory deep ──────────────
16913            | "pollard_p_minus_1" | "fermat_factor"
16914            | "trial_smallest_factor" | "bsgs_discrete_log"
16915            | "mertens" | "liouville"
16916            | "is_b_smooth" | "primorial_n"
16917            | "pseudoprime_base2" | "strong_pseudoprime"
16918            | "aks_witness_count" | "qs_relation"
16919            | "index_calculus_naive" | "lll_2x2_step" | "coppersmith_bound"
16920            | "shor_period_prob" | "rsa_d_from_e" | "dh_secret"
16921            | "elgamal_encrypt" | "ecc_point_double" | "continued_fraction_sqrt"
16922            | "pell_fundamental" | "sum_two_squares" | "class_number_bound"
16923            | "smith_normal_2x2_step" | "regulator_naive"
16924            | "power_residue_check" | "wieferich_check" | "wilson_test"
16925            | "goldbach_pair" | "english_likeness" | "xor_break_singlebyte"
16926            | "bit_reverse_64"
16927            | "gf256_multiply" | "hash_combine"
16928            // ── econometrics ────────────────────────────────────
16929            | "arch_lm_test" | "breusch_pagan_test" | "white_robust_se"
16930            | "newey_west_se" | "hansen_j_test" | "gmm_moment_condition"
16931            | "hausman_test" | "breusch_godfrey_test" | "box_pierce_test"
16932            | "adf_test_stat" | "pp_test_stat" | "kpss_test_stat"
16933            | "dickey_fuller_critical" | "engle_granger_step"
16934            | "johansen_trace_step" | "vecm_alpha_beta"
16935            | "panel_within_estimator" | "panel_between_estimator"
16936            | "panel_random_effects" | "arellano_bond_step"
16937            | "ols_estimator" | "ols_residual_variance" | "ols_r_squared"
16938            | "ols_adjusted_r2" | "akaike_info_crit" | "bayesian_info_crit"
16939            | "hannan_quinn_ic" | "f_statistic_pooled" | "breusch_pagan_lm"
16940            | "ramsey_reset_test" | "chow_test_stat" | "white_test_stat"
16941            | "goldfeld_quandt" | "wald_test_stat" | "score_test_stat"
16942            | "likelihood_ratio_test" | "two_sls_iv" | "iv_estimator"
16943            | "mle_normal_log_lik" | "mle_exponential_log_lik"
16944            | "mle_poisson_log_lik" | "gmm_moment_function"
16945            | "pooling_test_stat" | "heteroskedasticity_test"
16946            | "robust_se_huber_white" | "bootstrap_se_estimate"
16947            | "heckman_correction" | "tobit_log_likelihood"
16948            | "probit_log_likelihood" | "logit_log_likelihood"
16949            | "multinomial_logit_prob" | "ordered_probit_threshold"
16950            | "panel_var_step" | "impulse_response_step"
16951            | "variance_decomposition" | "granger_causality_chi2"
16952            | "cointegration_residual" | "error_correction_step"
16953            | "random_walk_innovation" | "random_walk_drift_step"
16954            | "ar_model_likelihood" | "ma_model_likelihood"
16955            | "arma_model_innovation"
16956            // ── algebraic topology, knot theory, lie algebras ───
16957            | "euler_char_complex" | "betti_zero" | "betti_one" | "betti_two"
16958            | "genus_surface" | "chern_first_2d" | "genus_curve_arith"
16959            | "genus_curve_geo" | "hodge_diamond_value" | "poincare_duality"
16960            | "fundamental_group_zn" | "homology_rank" | "cohomology_rank"
16961            | "homotopy_group_sphere_pi" | "mapping_class_torus"
16962            | "linking_number_two" | "writhe_polygon" | "torsion_coefficient"
16963            | "simplex_volume_n" | "simplicial_volume" | "nerve_complex_count"
16964            | "cech_zero_cohomology" | "de_rham_zero"
16965            | "poincare_polynomial_eval" | "chromatic_homology_rank"
16966            | "khovanov_q_grading" | "hochschild_zero" | "cyclic_homology_step"
16967            | "group_cohomology_dim" | "group_homology_dim"
16968            | "abelianization_quotient" | "free_group_rank_lower"
16969            | "nilpotency_class_lower" | "solvable_length_upper"
16970            | "schreier_index" | "todd_genus_eval" | "hirzebruch_signature"
16971            | "chern_simons_action" | "gauss_bonnet_total"
16972            | "seifert_genus_lower" | "alexander_polynomial_at_one"
16973            | "jones_polynomial_at_minus_one" | "jones_polynomial_at_i"
16974            | "homfly_evaluation" | "kauffman_bracket_eval"
16975            | "cabling_pair_signature" | "seifert_form_2x2"
16976            | "turaev_alexander_step" | "v_polynomial_eval"
16977            | "polynomial_jones_skein" | "delta_complex_count"
16978            | "poset_zeta_two" | "mobius_poset_two" | "mobius_function_pair"
16979            | "mobius_inversion_step" | "incidence_algebra_dim"
16980            | "quiver_path_count" | "representation_dim_step"
16981            | "weyl_group_order" | "root_system_count"
16982            | "cartan_determinant_a2" | "cartan_matrix_b2"
16983            | "killing_form_su2" | "casimir_eigenvalue_su2"
16984            | "universal_enveloping_dim" | "verma_character_step"
16985            | "plethystic_substitution_value" | "schur_polynomial_eval"
16986            | "hall_inner_product_two" | "plactic_class_size"
16987            | "robinson_schensted_pair" | "yamanouchi_word_count"
16988            | "rsk_size" | "character_su2" | "character_sun"
16989            | "quantum_dimension_su2" | "quantum_dimension_q"
16990            | "fusion_rule_su2_step" | "modular_data_s_value"
16991            | "modular_data_t_value" | "verlinde_count_step"
16992            | "quantum_invariant_eval" | "operad_count_two"
16993            | "moduli_dimension_curves" | "hodge_polynomial_eval"
16994            | "mirror_symmetry_check" | "gromov_witten_invariant"
16995            | "donaldson_invariant" | "seiberg_witten_value"
16996            | "floer_homology_rank" | "khovanov_rasmussen_s"
16997            | "ozsvath_szabo_tau" | "heegaard_genus_lower"
16998            | "fintushel_stern_step" | "bauer_furuta_step"
16999            | "geometric_intersection_number"
17000            | "algebraic_intersection_number"
17001            // ── electrochemistry, batteries, fuel cells ─────────
17002            | "nernst_potential_full" | "electrode_potential_step"
17003            | "exchange_current_density" | "butler_volmer_current"
17004            | "tafel_anodic_current" | "tafel_cathodic_current"
17005            | "mass_transport_overpotential" | "limiting_current_density"
17006            | "diffusion_layer_thickness" | "faradaic_efficiency"
17007            | "coulombic_efficiency_cell" | "energy_efficiency_cell"
17008            | "voltaic_efficiency" | "charge_capacity_battery"
17009            | "energy_density_battery" | "power_density_battery"
17010            | "specific_capacity_active" | "columbic_capacity_lihalfcell"
17011            | "ragone_point" | "peukert_capacity" | "peukert_exponent_fit"
17012            | "shepherd_voltage_step" | "nernst_planck_flux"
17013            | "debye_length_electrolyte" | "debye_huckel_activity"
17014            | "gouy_chapman_potential" | "stern_layer_capacitance"
17015            | "double_layer_capacitance" | "helmholtz_capacitance"
17016            | "zeta_potential_estimate" | "electroosmotic_velocity"
17017            | "hagen_poiseuille_eo" | "diffuse_layer_thickness"
17018            | "poisson_boltzmann_step" | "linearized_pb_step"
17019            | "electrochem_impedance_z" | "randles_circuit_z"
17020            | "warburg_impedance" | "cole_cole_eis" | "nyquist_phase"
17021            | "charge_transfer_resistance" | "solution_resistance_estimate"
17022            | "ionic_conductivity_arrhenius" | "nernst_einstein_diffusivity"
17023            | "walden_product" | "kohlrausch_law"
17024            | "onsager_relation_two_species" | "trasatti_voltammetry_charge"
17025            | "randles_sevcik_peak" | "levich_current_rde"
17026            | "koutecky_levich_intercept" | "mott_schottky_capacitance"
17027            | "flat_band_potential" | "schottky_barrier_height"
17028            | "photocurrent_density" | "quantum_efficiency_photo"
17029            | "overall_efficiency_pec" | "fuel_cell_polarization"
17030            | "electrolyzer_voltage" | "faraday_efficiency_h2"
17031            | "overpotential_oer" | "overpotential_her"
17032            | "electrocrystallization_step" | "nucleation_rate_constant"
17033            | "metal_corrosion_rate" | "pourbaix_line_value"
17034            | "mixed_potential_step" | "electrochemiluminescence_yield"
17035            | "solid_electrolyte_capacity" | "ionic_liquid_viscosity_step"
17036            | "lithium_ion_diffusivity" | "soc_estimate_coulomb"
17037            | "soh_capacity_fade" | "ocv_lithium_ion_step"
17038            | "state_of_charge_kalman" | "thermal_runaway_threshold"
17039            | "joule_heating_battery" | "calorimetric_heat_battery"
17040            | "abuse_test_voltage" | "swelling_strain_step"
17041            | "sei_resistance_growth" | "binder_content_optimal"
17042            | "porosity_active_layer" | "tortuosity_estimate_bruggeman"
17043            | "electrolyte_decomposition_temp" | "gibbs_thomson_undercooling"
17044            | "nernst_diffusion_layer" | "diff_coeff_aqueous_estimate"
17045            | "salt_activity_coefficient" | "mean_activity_coeff_pitzer"
17046            | "osmotic_coefficient_pitzer" | "debye_huckel_screening_factor"
17047            | "ph_at_isoelectric" | "buffer_capacity_acid_base"
17048            | "henderson_hasselbalch_solve" | "titration_endpoint_index"
17049            // ── tensor calculus, GR, differential geometry ──────
17050            | "tensor_contract_two" | "tensor_outer_two" | "tensor_trace_index"
17051            | "tensor_symmetrize_two" | "tensor_antisymmetrize_two"
17052            | "levi_civita_three" | "levi_civita_four"
17053            | "kronecker_three" | "kronecker_four"
17054            | "metric_minkowski_eta_step" | "metric_schwarzschild_step"
17055            | "metric_kerr_step_simple" | "metric_frw_lapse"
17056            | "christoffel_first_kind_step" | "christoffel_second_kind_step"
17057            | "riemann_tensor_step_zero" | "riemann_curvature_normal_form"
17058            | "ricci_tensor_step_zero" | "scalar_curvature_step"
17059            | "einstein_tensor_step" | "weyl_tensor_step_zero"
17060            | "schouten_tensor_step" | "geodesic_equation_step_zero"
17061            | "parallel_transport_step" | "covariant_derivative_step"
17062            | "christoffel_symbol_normalize" | "ricci_identity_step"
17063            | "bianchi_first_identity_check" | "bianchi_second_identity_check"
17064            | "killing_vector_lie_step" | "lie_derivative_scalar_step"
17065            | "lie_derivative_vector_step" | "exterior_derivative_one_form"
17066            | "hodge_star_one_form" | "codifferential_step"
17067            | "laplace_de_rham_step" | "volume_form_riemannian"
17068            | "hodge_inner_product_one" | "sectional_curvature_two_plane"
17069            | "gauss_codazzi_step" | "mainardi_codazzi_step"
17070            | "weingarten_map_step" | "shape_operator_eig"
17071            | "mean_curvature_step" | "gaussian_curvature_step"
17072            | "extrinsic_principal_curv" | "intrinsic_principal_curv"
17073            | "geodesic_curvature_step" | "darboux_frame_step"
17074            | "fermi_normal_step" | "synge_world_function"
17075            | "raychaudhuri_step" | "expansion_scalar_step"
17076            | "shear_tensor_step" | "twist_tensor_step"
17077            | "optical_scalars_step" | "peeling_step_psi4"
17078            | "ads_metric_step" | "de_sitter_metric_step"
17079            | "warped_product_step_zero" | "kaluza_klein_step"
17080            | "brans_dicke_step" | "horndeski_step"
17081            | "einstein_dilaton_step" | "gauss_bonnet_term_2d"
17082            | "chern_pontryagin_4d_step" | "adm_mass_step"
17083            | "komar_mass_step" | "bondi_mass_step"
17084            | "brown_york_quasilocal" | "isolated_horizon_charge"
17085            | "trapped_surface_check" | "apparent_horizon_step"
17086            | "event_horizon_check" | "cosmological_constant_term"
17087            | "de_sitter_radius_step" | "anti_de_sitter_radius_step"
17088            | "penrose_diagram_factor" | "conformal_compactification_step"
17089            | "schwarzschild_kruskal_step" | "gullstrand_painleve_step"
17090            | "kerr_newman_charge_term" | "boyer_lindquist_step"
17091            | "hartle_thorne_metric" | "oppenheimer_volkoff_step"
17092            | "post_newtonian_step" | "shapiro_delay_step"
17093            | "mercury_perihelion_advance"
17094            | "gravitational_wave_quadrupole"
17095            | "plus_polarization_amp" | "cross_polarization_amp"
17096            | "chirp_mass_inspiral_step" | "isco_radius_kerr_step"
17097            | "spin_orbit_coupling_term" | "spin_spin_coupling_term"
17098            | "hawking_area_increase" | "unruh_temperature_full"
17099            | "bekenstein_entropy_step" | "holographic_entanglement_step"
17100            | "ryu_takayanagi_step" | "swampland_distance_check"
17101            // ── information theory, coding, signal processing ──
17102            | "conditional_entropy_step" | "joint_entropy_step"
17103            | "relative_entropy_kl" | "mutual_information_step"
17104            | "chain_rule_entropy" | "fano_inequality_bound"
17105            | "data_processing_inequality" | "arithmetic_coding_interval"
17106            | "range_coding_step" | "golomb_rice_code"
17107            | "elias_gamma_code" | "elias_delta_code" | "exp_golomb_code"
17108            | "fibonacci_code" | "shannon_fano_elias_code"
17109            | "huffman_balanced_step" | "arithmetic_decode_interval"
17110            | "range_decode_step" | "universal_code_length"
17111            | "ziv_lempel_estimate" | "lz77_match_length"
17112            | "lz78_dictionary_growth" | "lzw_step_dict"
17113            | "ppm_predict_prob" | "deflate_huffman_lit"
17114            | "brotli_distance_code_count" | "zstd_window_size_log"
17115            | "mpeg_quant_value" | "jpeg_zig_zag_index"
17116            | "jpeg_dct_8x8_quant" | "hadamard_walsh_transform_step"
17117            | "karhunen_loeve_step" | "discrete_haar_step"
17118            | "db4_wavelet_step" | "biorthogonal_step"
17119            | "beylkin_wavelet_step" | "coiflet_wavelet_step"
17120            | "mallat_pyramid_step" | "threshold_soft_value"
17121            | "threshold_hard_value" | "median_filter_window"
17122            | "mean_filter_window" | "gaussian_filter_window"
17123            | "unsharp_mask_step" | "sobel_kernel_value"
17124            | "prewitt_kernel_value" | "roberts_kernel_value"
17125            | "laplacian_kernel_value" | "canny_threshold_step"
17126            | "hough_accumulator_step" | "ransac_iteration_count"
17127            | "optical_flow_lk_step" | "horn_schunck_step"
17128            | "kalman_predict_state" | "kalman_update_state"
17129            | "particle_filter_resample" | "unscented_sigma_point"
17130            | "ekf_jacobian_step" | "markov_decision_value"
17131            | "bellman_equation_step" | "q_learning_update"
17132            | "policy_iteration_step" | "value_iteration_step"
17133            | "sarsa_update" | "double_q_learning_step"
17134            | "ucb1_action_value" | "thompson_sample_beta"
17135            | "boltzmann_softmax_action" | "explore_exploit_epsilon"
17136            | "montecarlo_returns_step" | "td_zero_update"
17137            | "td_lambda_update" | "gradient_temporal_diff"
17138            | "deep_q_target" | "ddpg_critic_loss_step"
17139            | "ppo_clip_term" | "trpo_kl_constraint"
17140            | "a3c_advantage_step" | "ppo_advantage_step"
17141            | "gae_advantage_step" | "generalized_advantage"
17142            | "information_bottleneck_step" | "free_energy_principle"
17143            | "fisher_info_metric" | "kullback_jensen_div"
17144            | "hellinger_distance_step" | "total_variation_distance"
17145            | "bhattacharyya_coefficient" | "wasserstein_dist_emp"
17146            | "chisquare_metric" | "hellinger_kernel"
17147            | "jensen_shannon_div" | "renyi_divergence_step"
17148            | "amari_alpha_div" | "csiszar_phi_div"
17149            | "sinkhorn_iteration_step" | "sliced_wasserstein"
17150            | "gromov_wasserstein_step" | "spectral_signature_match"
17151            | "mfcc_coeff_step" | "chroma_feature_step"
17152            // ── combinatorial optimization, scheduling ──────────
17153            | "tsp_lower_bound_mst" | "tsp_held_karp_step"
17154            | "christofides_ratio_bound" | "two_opt_swap_delta"
17155            | "or_opt_delta" | "three_opt_delta" | "lin_kernighan_step"
17156            | "nearest_neighbor_tour_step" | "greedy_edge_tour"
17157            | "nearest_insertion_step" | "farthest_insertion_step"
17158            | "cheapest_insertion_step" | "max_flow_ford_fulkerson_step"
17159            | "edmonds_karp_step" | "dinic_blocking_flow"
17160            | "push_relabel_step" | "boykov_kolmogorov_step"
17161            | "mincut_stoer_wagner" | "gomory_hu_step"
17162            | "karger_contract_edge" | "karger_min_cut_count"
17163            | "maximum_bipartite_matching" | "hopcroft_karp_phase"
17164            | "blossom_match_step" | "weighted_match_kuhn_step"
17165            | "hungarian_method_step" | "ap_jonker_volgenant_step"
17166            | "assignment_lower_bound" | "job_shop_makespan_lower"
17167            | "flow_shop_johnson_step" | "parallel_machine_lpt"
17168            | "parallel_machine_spt" | "list_scheduling_step"
17169            | "graham_2approx_bound" | "chc_bound_makespan"
17170            | "bin_packing_first_fit" | "bin_packing_best_fit"
17171            | "bin_packing_next_fit" | "bin_packing_lower_bound_l1"
17172            | "multidim_packing_step" | "knapsack_01_dp_value"
17173            | "knapsack_unbounded_dp" | "knapsack_fractional_step"
17174            | "knapsack_branch_bound" | "knapsack_lp_relaxation"
17175            | "multi_knapsack_step" | "quadratic_assignment_step"
17176            | "qap_lower_bound" | "graph_coloring_dsatur_step"
17177            | "graph_coloring_welsh_powell"
17178            | "graph_coloring_brooks_bound" | "graph_coloring_lp_bound"
17179            | "fractional_chromatic_lower" | "list_coloring_step"
17180            | "edge_coloring_vizing_step" | "clique_number_lower"
17181            | "independence_number_upper" | "vertex_cover_lp_round"
17182            | "dominating_set_greedy_step" | "dominating_set_lp_bound"
17183            | "set_cover_greedy_step" | "set_cover_lp_round"
17184            | "hitting_set_greedy" | "weighted_set_cover_step"
17185            | "matroid_greedy_step" | "matroid_intersection_step"
17186            | "submodular_greedy_step" | "submodular_curvature_bound"
17187            | "nemhauser_wolsey_bound" | "lp_relax_round"
17188            | "branch_and_bound_step" | "cutting_plane_step"
17189            | "gomory_cut_step" | "chvatal_gomory_cut"
17190            | "mixed_integer_round_up" | "mixed_integer_round_down"
17191            | "sos_constraint_check" | "column_generation_step"
17192            | "benders_decomposition_step" | "dantzig_wolfe_step"
17193            | "lagrangian_relax_step" | "lagrangian_dual_step"
17194            | "subgradient_step_size" | "nonlinear_dual_step"
17195            | "augmented_lagrangian_step" | "admm_primal_step"
17196            | "admm_dual_step" | "proximal_gradient_step"
17197            | "nesterov_accelerate_step" | "fista_step" | "ista_step"
17198            | "mirror_descent_step" | "frank_wolfe_step"
17199            | "conditional_gradient_step" | "greedy_set_cover_round"
17200            | "local_search_swap_step" | "tabu_search_move_score"
17201            | "simulated_annealing_step" | "genetic_crossover_one_point"
17202            | "mutation_bit_flip_prob" | "roulette_wheel_select_index"
17203            // ── climate, fluids, atmospheric ────────────────────
17204            | "stefan_boltzmann_radiation" | "emissivity_grey_body"
17205            | "albedo_blackbody_balance" | "solar_constant_at_distance"
17206            | "total_solar_irradiance_step" | "absorbed_short_wave"
17207            | "emitted_long_wave" | "clausius_clapeyron_full"
17208            | "relative_humidity_step" | "dewpoint_temperature_full"
17209            | "wet_bulb_potential" | "virtual_temperature_full"
17210            | "density_altitude_full" | "geopotential_height_full"
17211            | "geometric_height_full" | "adiabatic_lapse_rate_dry"
17212            | "adiabatic_lapse_rate_moist" | "brunt_vaisala_full"
17213            | "richardson_number_step" | "gradient_richardson_full"
17214            | "flux_richardson_full" | "turbulent_kinetic_energy_step"
17215            | "mixing_length_prandtl" | "monin_obukhov_length"
17216            | "similarity_function_phi" | "log_law_wind_profile"
17217            | "power_law_wind_profile" | "ekman_layer_depth"
17218            | "ekman_pumping_step" | "geostrophic_wind_step"
17219            | "gradient_wind_step" | "thermal_wind_step"
17220            | "quasi_geostrophic_omega" | "omega_equation_step"
17221            | "potential_temperature_step" | "equivalent_potential_temp"
17222            | "saturation_equivalent_pt" | "ipv_potential_vorticity"
17223            | "ertel_pv_step" | "absolute_vorticity_step"
17224            | "relative_vorticity_step" | "divergence_omega_step"
17225            | "streamfunction_step" | "velocity_potential_step"
17226            | "helmholtz_decomp_step" | "courant_friedrichs_lewy"
17227            | "peclet_number_step" | "prandtl_number_step"
17228            | "reynolds_full_number" | "schmidt_number_step"
17229            | "sherwood_number_step" | "nusselt_full_number"
17230            | "grashof_number_step" | "rayleigh_number_step"
17231            | "weber_number_step" | "froude_number_step"
17232            | "strouhal_full" | "mach_full_step"
17233            | "biot_number_step" | "fourier_number_step"
17234            | "turbulence_intensity_step" | "hurst_exponent_estimate"
17235            | "detrended_fluct_alpha" | "power_spectrum_slope"
17236            | "spectral_kappa_minus53" | "batchelor_scale_step"
17237            | "kolmogorov_microscale" | "taylor_microscale_step"
17238            | "integral_length_scale" | "turbulent_dissipation_eps"
17239            | "isotropic_relation_check" | "sst_anomaly_step"
17240            | "enso_index_step" | "amo_index_step" | "nao_index_step"
17241            | "soi_oscillation_index" | "pdo_index_step" | "mjo_phase_step"
17242            | "walker_circulation_step" | "hadley_cell_max_lat"
17243            | "ferrel_cell_step" | "itcz_position_lat" | "trade_wind_speed"
17244            | "westerlies_jet_speed" | "polar_vortex_radius"
17245            | "arctic_oscillation_step" | "indian_monsoon_index"
17246            | "african_monsoon_index" | "qbo_oscillation_step"
17247            | "solar_cycle_phase" | "sunspot_relative_number"
17248            | "geomagnetic_kp_index" | "ozone_dobson_total"
17249            | "chlorine_radical_decay" | "montreal_protocol_track"
17250            | "co2_growth_rate_step" | "methane_growth_rate"
17251            | "aerosol_optical_depth" | "ice_age_milankovitch"
17252            | "greenhouse_forcing_step"
17253            // ── game theory, mechanism design, social choice ────
17254            | "game_two_player_value" | "nash_equilibrium_pair"
17255            | "mixed_strategy_value" | "zero_sum_minmax"
17256            | "saddle_point_check" | "correlated_equilibrium_value"
17257            | "shapley_value_two_step" | "banzhaf_index_two"
17258            | "nucleolus_lp_step" | "core_membership_check"
17259            | "imputation_efficient_check" | "imputation_individual_rational"
17260            | "prisoners_dilemma_payoff" | "matching_pennies_payoff"
17261            | "chicken_game_payoff" | "stag_hunt_payoff"
17262            | "battle_sexes_payoff" | "public_goods_game_payoff"
17263            | "tragedy_commons_metric" | "ultimatum_acceptance_prob"
17264            | "dictator_game_share" | "trust_game_repayment"
17265            | "cooperative_game_value" | "characteristic_function"
17266            | "bargaining_set_check" | "kalai_smorodinsky_step"
17267            | "nash_bargaining_solution" | "egalitarian_solution"
17268            | "utilitarian_solution" | "social_welfare_sum"
17269            | "arrow_impossibility_check" | "gibbard_satterthwaite_check"
17270            | "borda_count_step" | "condorcet_winner_check"
17271            | "plurality_winner_step" | "kemeny_score_step"
17272            | "dodgson_swap_count" | "coombs_runoff_step"
17273            | "single_transferable_vote" | "range_voting_score"
17274            | "approval_voting_max" | "schulze_method_step"
17275            | "copeland_score_step" | "black_method_winner"
17276            | "median_voter_step" | "hotelling_location_step"
17277            | "arrow_pareto_check" | "fair_division_envy_free"
17278            | "proportional_share" | "maximin_share"
17279            | "egalitarian_split" | "nash_social_welfare"
17280            | "divisible_goods_proportional" | "indivisible_envy_free_check"
17281            | "adjusted_winner_pct" | "sealed_bid_first_price"
17282            | "sealed_bid_second_price" | "english_auction_step"
17283            | "dutch_auction_step" | "all_pay_auction_step"
17284            | "vcg_payment_step" | "revenue_equivalence_check"
17285            | "truthful_mechanism_check" | "incentive_compatibility_check"
17286            | "mechanism_design_obj" | "double_auction_step"
17287            | "combinatorial_auction_step" | "posted_price_offer_accept"
17288            | "matching_market_step" | "deferred_acceptance_step"
17289            | "boston_mechanism_step" | "top_trading_cycles_step"
17290            | "school_choice_match" | "roommate_match_step"
17291            | "network_formation_step" | "coordination_game_payoff"
17292            | "evolutionary_stable_strategy" | "replicator_dynamics_step"
17293            | "hawk_dove_payoff" | "fictitious_play_step"
17294            | "best_response_dynamic" | "quantal_response_logit"
17295            | "level_k_step" | "cognitive_hierarchy_step"
17296            | "sequential_eq_check" | "subgame_perfect_eq"
17297            | "stackelberg_step" | "cournot_quantity_step"
17298            | "bertrand_price_step" | "hotelling_price_step"
17299            | "collusion_payoff_step" | "folk_theorem_value"
17300            | "repeated_game_avg_payoff" | "discount_factor_step"
17301            | "trigger_strategy_payoff" | "grim_trigger_step"
17302            | "tit_for_tat_step" | "prisoners_repeated_eq"
17303            | "mertens_zamir_step" | "ex_post_value_check"
17304            | "ex_ante_value_check" | "common_knowledge_iterations"
17305            // ── symbolic CAS, decompositions, projections ───────
17306            | "cas_simplify_term" | "cas_expand_two_terms"
17307            | "cas_factor_quadratic" | "cas_partial_fraction_simple"
17308            | "cas_polynomial_gcd_step" | "cas_polynomial_div_step"
17309            | "cas_lagrange_interpolate" | "cas_chebyshev_eval"
17310            | "cas_legendre_eval" | "cas_hermite_eval"
17311            | "cas_laguerre_eval" | "cas_jacobi_eval"
17312            | "cas_gegenbauer_eval" | "cas_taylor_coefficient"
17313            | "cas_padé_diagonal" | "cas_continued_fraction_step"
17314            | "cas_resultant_two" | "cas_subresultant_two"
17315            | "cas_groebner_lt_step" | "cas_buchberger_step"
17316            | "cas_macaulay_matrix_step" | "cas_modular_inverse"
17317            | "cas_extended_euclid_step" | "cas_smith_normal_step"
17318            | "cas_hermite_normal_step" | "cas_radical_simplify"
17319            | "cas_minimal_polynomial" | "cas_gcd_polynomial_step"
17320            | "cas_resultant_x_y" | "cas_solve_linear"
17321            | "cas_solve_quadratic" | "cas_solve_cubic"
17322            | "cas_solve_quartic" | "cas_solve_polynomial_n"
17323            | "cas_root_isolate_step" | "cas_sturm_sequence_step"
17324            | "cas_descartes_rule_count" | "cas_companion_matrix_root"
17325            | "cas_polynomial_roots_kahan"
17326            | "cas_eigenvalue_inverse_iteration" | "cas_qr_iteration_step"
17327            | "cas_jacobi_eigen_step" | "cas_lanczos_iteration_step"
17328            | "cas_arnoldi_iteration_step" | "cas_givens_rotation_apply"
17329            | "cas_householder_reflection" | "cas_modified_gram_schmidt"
17330            | "cas_classical_gram_schmidt" | "cas_rank_revealing_qr"
17331            | "cas_pivoted_lu_step" | "cas_block_lu_step"
17332            | "cas_cholesky_step" | "cas_modified_cholesky"
17333            | "cas_ldlt_step" | "cas_bunch_kaufman_step"
17334            | "cas_woodbury_identity" | "cas_matrix_pencil_step"
17335            | "cas_generalized_eigen" | "cas_singular_value_step"
17336            | "cas_truncated_svd_value" | "cas_pseudoinverse_step"
17337            | "cas_polar_decomposition" | "cas_schur_decomposition_step"
17338            | "cas_quasi_triangular" | "cas_riccati_continuous_step"
17339            | "cas_riccati_discrete_step" | "cas_lyapunov_continuous_step"
17340            | "cas_lyapunov_discrete_step" | "cas_sylvester_equation_step"
17341            | "cas_kronecker_product_step" | "cas_vec_operator_step"
17342            | "cas_matrix_function_step" | "cas_matrix_log_step"
17343            | "cas_matrix_exp_pade" | "cas_matrix_sqrt_step"
17344            | "cas_drazin_inverse_step" | "cas_moore_penrose_step"
17345            | "cas_least_squares_solve" | "cas_total_least_squares"
17346            | "cas_constrained_ls_step" | "cas_truncated_lsq"
17347            | "cas_regularized_lsq_tikhonov" | "cas_basis_pursuit_step"
17348            | "cas_lasso_soft_threshold" | "cas_elastic_net_step"
17349            | "cas_omp_step" | "cas_iht_iteration"
17350            | "cas_cosamp_step" | "cas_admm_lasso_step"
17351            | "cas_proximal_l1_step" | "cas_proximal_l2_step"
17352            | "cas_proximal_l_inf_step" | "cas_indicator_simplex_proj"
17353            | "cas_proj_l1_ball" | "cas_proj_l2_ball"
17354            | "cas_proj_box" | "cas_proj_psd_cone"
17355            | "cas_proj_soc_step" | "cas_proj_exp_cone"
17356            | "cas_dykstra_step" | "cas_alternating_projection"
17357            | "cas_polya_enumeration_step" | "cas_burnside_count_step"
17358            // ── ML primitives — activations, losses, optimizers ─
17359            | "ml_relu_step" | "ml_leaky_relu_step" | "ml_elu_step"
17360            | "ml_selu_step" | "ml_gelu_step" | "ml_swish_step"
17361            | "ml_mish_step" | "ml_softplus_step" | "ml_softsign_step"
17362            | "ml_hard_sigmoid" | "ml_hard_tanh" | "ml_prelu_step"
17363            | "ml_celu_step" | "ml_silu_step" | "ml_logsumexp_step"
17364            | "ml_log_softmax_step" | "ml_log_sigmoid"
17365            | "ml_glu_step" | "ml_geglu_step" | "ml_swiglu_step"
17366            | "ml_attention_score_step" | "ml_scaled_dot_product"
17367            | "ml_multihead_avg" | "ml_softmax_temperature"
17368            | "ml_dropout_mask_prob" | "ml_layer_norm_step"
17369            | "ml_batch_norm_step" | "ml_group_norm_step"
17370            | "ml_rms_norm_step" | "ml_instance_norm_step"
17371            | "ml_weight_norm_step" | "ml_spectral_norm_step"
17372            | "ml_l2_normalize_step" | "ml_huber_loss_step"
17373            | "ml_smooth_l1_loss" | "ml_focal_loss_step"
17374            | "ml_dice_loss_step" | "ml_iou_loss_step"
17375            | "ml_giou_loss_step" | "ml_diou_loss_step"
17376            | "ml_ciou_loss_step" | "ml_contrastive_loss"
17377            | "ml_triplet_loss_step" | "ml_arcface_loss_step"
17378            | "ml_center_loss_step" | "ml_kl_divergence_loss"
17379            | "ml_cross_entropy_loss" | "ml_binary_cross_entropy"
17380            | "ml_label_smoothing" | "ml_mixup_lambda"
17381            | "ml_cutmix_box_iou" | "ml_random_erasing_step"
17382            | "ml_cosine_lr_schedule" | "ml_warmup_lr_step"
17383            | "ml_step_lr_schedule" | "ml_exponential_lr"
17384            | "ml_polynomial_lr" | "ml_one_cycle_lr"
17385            | "ml_inverse_sqrt_lr" | "ml_cyclic_lr_step"
17386            | "ml_sgd_step" | "ml_momentum_step"
17387            | "ml_nesterov_momentum" | "ml_adagrad_step"
17388            | "ml_rmsprop_step" | "ml_adam_step"
17389            | "ml_adamw_step" | "ml_adamax_step"
17390            | "ml_nadam_step" | "ml_radam_step"
17391            | "ml_lookahead_step" | "ml_lamb_step"
17392            | "ml_lars_step" | "ml_yogi_step"
17393            | "ml_amsgrad_step" | "ml_adabelief_step"
17394            | "ml_shampoo_step" | "ml_lion_step"
17395            | "ml_sophia_step" | "ml_gradient_clip_norm"
17396            | "ml_gradient_clip_value" | "ml_gradient_accumulate"
17397            | "ml_gradient_centralize" | "ml_weight_decay_step"
17398            | "ml_he_init_value" | "ml_xavier_init_value"
17399            | "ml_glorot_init_value" | "ml_orthogonal_init"
17400            | "ml_truncnormal_init" | "ml_kaiming_init"
17401            | "ml_lecun_init_value" | "ml_zero_init"
17402            | "ml_constant_init" | "ml_uniform_init"
17403            | "ml_one_hot_index" | "ml_label_to_id"
17404            | "ml_id_to_label_step" | "ml_token_logit_top_k"
17405            | "ml_topk_argmax" | "ml_nucleus_sample_p"
17406            | "ml_temperature_decay" | "ml_repetition_penalty"
17407            | "ml_eos_logit_boost"
17408            // ── NLP — ranking, similarity, language models ──────
17409            | "nlp_bm25_score" | "nlp_tf_idf_step" | "nlp_okapi_score"
17410            | "nlp_word_freq_value" | "nlp_doc_freq_step"
17411            | "nlp_inverse_doc_freq" | "nlp_cosine_similarity_two"
17412            | "nlp_jaccard_similarity_two" | "nlp_overlap_coefficient"
17413            | "nlp_dice_coefficient_two" | "nlp_simpson_coefficient"
17414            | "nlp_levenshtein_dist" | "nlp_damerau_levenshtein"
17415            | "nlp_jaro_distance" | "nlp_jaro_winkler"
17416            | "nlp_hamming_distance" | "nlp_lcs_length" | "nlp_lcs_ratio"
17417            | "nlp_meteor_score" | "nlp_bleu_score_n"
17418            | "nlp_rouge_score_n" | "nlp_chrf_score" | "nlp_ter_score"
17419            | "nlp_wer_score" | "nlp_cer_score" | "nlp_perplexity_value"
17420            | "nlp_bits_per_character" | "nlp_char_ngram_count"
17421            | "nlp_word_ngram_count" | "nlp_skip_gram_count"
17422            | "nlp_byte_pair_merge_step" | "nlp_wordpiece_score"
17423            | "nlp_unigram_lm_score" | "nlp_kneser_ney_step"
17424            | "nlp_witten_bell_step" | "nlp_good_turing_count"
17425            | "nlp_laplace_smoothing" | "nlp_lidstone_smoothing"
17426            | "nlp_jelinek_mercer" | "nlp_dirichlet_smoothing"
17427            | "nlp_query_likelihood_step" | "nlp_kl_lm_div"
17428            | "nlp_pmi_score" | "nlp_npmi_score"
17429            | "nlp_chi2_collocation" | "nlp_loglikelihood_collocation"
17430            | "nlp_t_score_collocation" | "nlp_dunning_log_likelihood"
17431            | "nlp_lda_alpha_step" | "nlp_lda_beta_step"
17432            | "nlp_lda_topic_dist" | "nlp_plsa_step"
17433            | "nlp_word2vec_skipgram_loss" | "nlp_word2vec_cbow_loss"
17434            | "nlp_glove_loss_step" | "nlp_fasttext_subword_count"
17435            | "nlp_byte_level_bpe_step" | "nlp_sentencepiece_score"
17436            | "nlp_unigram_subword_loss" | "nlp_subword_regularization"
17437            | "nlp_pointwise_attn_score" | "nlp_relative_position_bias"
17438            | "nlp_alibi_position_bias" | "nlp_rope_rotary_angle"
17439            | "nlp_rope_apply_step" | "nlp_position_encoding_sin"
17440            | "nlp_position_encoding_cos" | "nlp_pe_freq_band"
17441            | "nlp_max_seq_len_check" | "nlp_token_drop_rate"
17442            | "nlp_byte_frequency" | "nlp_char_frequency"
17443            | "nlp_punct_ratio" | "nlp_uppercase_ratio"
17444            | "nlp_digit_ratio" | "nlp_emoji_ratio"
17445            | "nlp_url_count" | "nlp_email_count" | "nlp_phone_count"
17446            | "nlp_hashtag_count" | "nlp_mention_count"
17447            | "nlp_token_overlap_two" | "nlp_word_mover_dist"
17448            | "nlp_sif_weight_step" | "nlp_doc_embedding_avg"
17449            | "nlp_attention_pool_step" | "nlp_max_pool_step"
17450            | "nlp_avg_pool_step" | "nlp_sum_pool_step"
17451            | "nlp_self_attn_compute_step" | "nlp_cross_attn_compute_step"
17452            | "nlp_window_attn_step" | "nlp_strided_attn_step"
17453            | "nlp_block_attn_step" | "nlp_sliding_window_step"
17454            | "nlp_local_attn_step" | "nlp_dilated_attn_step"
17455            | "nlp_global_attn_step" | "nlp_sparse_attn_score"
17456            | "nlp_linformer_step" | "nlp_performer_step"
17457            | "nlp_reformer_step" | "nlp_longformer_step"
17458            | "nlp_bigbird_step" | "nlp_routing_attn_step"
17459            // ── graphics, geometry, ray tracing, BRDF, color ────
17460            | "gfx_perspective_proj_x" | "gfx_perspective_proj_y"
17461            | "gfx_orthographic_proj" | "gfx_view_matrix_step"
17462            | "gfx_lookat_forward" | "gfx_lookat_right" | "gfx_lookat_up"
17463            | "gfx_quat_to_axis_angle" | "gfx_axis_angle_to_quat"
17464            | "gfx_quat_slerp_step" | "gfx_quat_nlerp_step"
17465            | "gfx_quat_dot_two" | "gfx_quat_inverse_step"
17466            | "gfx_quat_to_euler_pitch" | "gfx_quat_to_euler_yaw"
17467            | "gfx_quat_to_euler_roll" | "gfx_euler_to_quat_x"
17468            | "gfx_euler_to_quat_y" | "gfx_euler_to_quat_z"
17469            | "gfx_euler_to_quat_w" | "gfx_rotation_matrix_xx"
17470            | "gfx_rotation_matrix_yy" | "gfx_rotation_matrix_zz"
17471            | "gfx_translation_matrix_step" | "gfx_scale_matrix_step"
17472            | "gfx_shear_matrix_xy" | "gfx_homogeneous_divide"
17473            | "gfx_screen_space_x" | "gfx_screen_space_y"
17474            | "gfx_ndc_to_screen_x" | "gfx_ndc_to_screen_y"
17475            | "gfx_screen_to_ndc_x" | "gfx_screen_to_ndc_y"
17476            | "gfx_clip_polygon_step" | "gfx_sutherland_hodgman"
17477            | "gfx_cohen_sutherland_code" | "gfx_liang_barsky_t"
17478            | "gfx_bresenham_step_x" | "gfx_bresenham_step_y"
17479            | "gfx_xiaolin_wu_intensity" | "gfx_aabb_intersect_check"
17480            | "gfx_obb_overlap_step" | "gfx_sphere_intersect_t"
17481            | "gfx_ray_triangle_t" | "gfx_ray_plane_t" | "gfx_ray_box_t"
17482            | "gfx_ray_sphere_t" | "gfx_ray_disk_t"
17483            | "gfx_ray_cylinder_t" | "gfx_ray_cone_t"
17484            | "gfx_ray_ellipsoid_t" | "gfx_ray_torus_t_approx"
17485            | "gfx_barycentric_alpha" | "gfx_barycentric_beta"
17486            | "gfx_barycentric_gamma" | "gfx_phong_diffuse_step"
17487            | "gfx_phong_specular_step" | "gfx_phong_ambient_step"
17488            | "gfx_blinn_specular_step" | "gfx_lambert_term"
17489            | "gfx_oren_nayar_term" | "gfx_cook_torrance_d_ggx"
17490            | "gfx_cook_torrance_g_smith" | "gfx_cook_torrance_f_schlick"
17491            | "gfx_disney_principled_d" | "gfx_microfacet_brdf_step"
17492            | "gfx_subsurface_scattering_term" | "gfx_translucent_falloff"
17493            | "gfx_normal_distribution_ggx"
17494            | "gfx_geometric_attenuation_smith"
17495            | "gfx_fresnel_dielectric_step" | "gfx_fresnel_conductor_step"
17496            | "gfx_index_of_refraction" | "gfx_snells_law_angle"
17497            | "gfx_total_internal_reflection" | "gfx_refract_direction_x"
17498            | "gfx_reflect_direction_x" | "gfx_environment_map_uv_u"
17499            | "gfx_environment_map_uv_v" | "gfx_cube_map_face_index"
17500            | "gfx_octahedral_encode_x" | "gfx_octahedral_encode_y"
17501            | "gfx_spherical_harmonic_y00" | "gfx_spherical_harmonic_y10"
17502            | "gfx_spherical_harmonic_y11" | "gfx_spherical_harmonic_y20"
17503            | "gfx_zonal_harmonic_step" | "gfx_irradiance_sh_eval"
17504            | "gfx_radiance_sh_eval" | "gfx_skybox_uv_u" | "gfx_skybox_uv_v"
17505            | "gfx_tonemap_reinhard" | "gfx_tonemap_aces"
17506            | "gfx_tonemap_uncharted2" | "gfx_tonemap_filmic"
17507            | "gfx_gamma_correct_step" | "gfx_srgb_to_linear"
17508            | "gfx_linear_to_srgb" | "gfx_dither_bayer_4x4"
17509            | "gfx_dither_floyd_steinberg" | "gfx_oklab_l_step"
17510            | "gfx_oklab_a_step" | "gfx_oklab_b_step"
17511            | "gfx_oklch_chroma" | "gfx_oklch_hue"
17512            | "gfx_pcg_hash_step" | "gfx_xorshift_step"
17513            | "gfx_halton_step" | "gfx_sobol_step"
17514            | "gfx_van_der_corput" | "gfx_low_discrepancy_step"
17515            | "gfx_blue_noise_value" | "gfx_perlin_noise_step"
17516            | "gfx_simplex_noise_step" | "gfx_fbm_noise_step"
17517            | "gfx_worley_noise_step" | "gfx_voronoi_distance"
17518            | "gfx_curl_noise_step" | "gfx_gradient_noise_step"
17519            | "gfx_value_noise_step" | "gfx_signed_distance_box"
17520            | "gfx_signed_distance_sphere" | "gfx_signed_distance_capsule"
17521            // ── database internals, distributed systems ─────────
17522            | "db_b_tree_split" | "db_b_tree_merge"
17523            | "db_lsm_compaction_step" | "db_skiplist_height_pick"
17524            | "db_bloom_filter_bit_index" | "db_cuckoo_filter_fingerprint"
17525            | "db_quotient_filter_canonical" | "db_count_min_sketch_bin"
17526            | "db_hyperloglog_register_max" | "db_min_hash_value"
17527            | "db_simhash_bit" | "db_consistent_hash_index"
17528            | "db_rendezvous_hash_score" | "db_jump_hash_bucket"
17529            | "db_maglev_hash_step" | "db_lru_cache_eviction_age"
17530            | "db_lfu_cache_decay" | "db_arc_cache_score"
17531            | "db_clock_cache_hand" | "db_tinylfu_admit_score"
17532            | "db_w_tinylfu_freq" | "db_buffer_pool_score"
17533            | "db_query_plan_cost_step" | "db_join_selectivity_step"
17534            | "db_index_seek_cost" | "db_seq_scan_cost"
17535            | "db_index_scan_cost" | "db_sort_cost_estimate"
17536            | "db_hash_join_cost" | "db_merge_join_cost"
17537            | "db_nested_loop_cost" | "db_query_cardinality"
17538            | "db_histogram_bucket_index" | "db_quantile_estimate_p99"
17539            | "db_t_digest_centroid" | "db_kll_quantile_step"
17540            | "db_dd_sketch_bin" | "db_reservoir_sample_index"
17541            | "db_chao_estimator_step" | "db_jaccard_minhash_estimate"
17542            | "db_distinct_estimate_lpc" | "db_distinct_estimate_hll"
17543            | "db_throttle_token_step" | "db_leaky_bucket_step"
17544            | "db_token_bucket_step" | "db_circuit_breaker_step"
17545            | "db_two_phase_commit_step" | "db_three_phase_commit_step"
17546            | "db_paxos_propose_id" | "db_raft_term_advance"
17547            | "db_raft_log_match_check" | "db_zab_epoch_step"
17548            | "db_chubby_lease_step" | "db_logical_clock_step"
17549            | "db_lamport_timestamp" | "db_vector_clock_merge"
17550            | "db_hybrid_logical_clock" | "db_crdt_g_counter_merge"
17551            | "db_crdt_pn_counter_merge" | "db_crdt_lww_register_merge"
17552            | "db_crdt_set_or_merge" | "db_consensus_quorum_size"
17553            | "db_replication_lag_step" | "db_partitions_for_n"
17554            | "db_consistent_lookup_id" | "db_chord_finger_index"
17555            | "db_kademlia_xor_distance" | "db_pastry_routing_step"
17556            | "db_dht_replicate_factor" | "db_partition_failure_check"
17557            | "db_byzantine_quorum_size" | "db_pbft_view_change"
17558            | "db_honey_badger_step" | "db_avalanche_query_step"
17559            | "db_quorum_intersection_check" | "db_anti_entropy_step"
17560            | "db_merkle_node_hash" | "db_merkle_path_verify"
17561            | "db_gossip_fanout_step" | "db_anti_entropy_pull_step"
17562            | "db_split_brain_check" | "db_clock_skew_estimate"
17563            | "db_freshness_score" | "db_read_repair_step"
17564            | "db_hinted_handoff_step" | "db_compaction_score"
17565            | "db_levelled_compaction_step" | "db_size_tiered_compaction"
17566            | "db_universal_compaction_step" | "db_write_amplification"
17567            | "db_read_amplification" | "db_space_amplification"
17568            | "db_block_cache_hit_rate" | "db_page_cache_eviction_age"
17569            | "db_wal_fsync_cost" | "db_group_commit_count"
17570            | "db_replica_lag_threshold" | "db_synchronous_commit_check"
17571            | "db_async_commit_check" | "db_eventual_consistency_check"
17572            | "db_strong_consistency_check" | "db_linearizability_check"
17573            | "db_causal_consistency_check"
17574            // ── networking — TCP, AQM, MIMO, queueing ───────────
17575            | "net_tcp_cwnd_step" | "net_tcp_ssthresh_update"
17576            | "net_tcp_reno_step" | "net_tcp_cubic_step"
17577            | "net_tcp_bbr_step" | "net_tcp_vegas_step"
17578            | "net_tcp_westwood_step" | "net_tcp_compound_step"
17579            | "net_tcp_dctcp_step" | "net_tcp_yeah_step"
17580            | "net_tcp_htcp_step" | "net_tcp_hybla_step"
17581            | "net_tcp_illinois_step" | "net_tcp_lp_step"
17582            | "net_tcp_scalable_step" | "net_tcp_veno_step"
17583            | "net_aiad_step" | "net_aimd_step"
17584            | "net_miad_step" | "net_mimd_step"
17585            | "net_aqm_red_drop_prob" | "net_aqm_codel_target"
17586            | "net_aqm_pie_drop_rate" | "net_aqm_fq_codel_step"
17587            | "net_aqm_blue_step" | "net_aqm_choke_step"
17588            | "net_aqm_sfq_step" | "net_aqm_drr_step"
17589            | "net_aqm_wrr_step" | "net_token_rate_limit"
17590            | "net_traffic_shaper_step" | "net_priority_queue_index"
17591            | "net_packet_loss_estimate" | "net_jitter_estimate"
17592            | "net_latency_avg" | "net_rtt_smoothed"
17593            | "net_rtt_variation" | "net_rto_compute"
17594            | "net_bandwidth_delay_product" | "net_path_capacity_kleinrock"
17595            | "net_loss_rate_to_throughput" | "net_throughput_padhye"
17596            | "net_throughput_mathis" | "net_throughput_response"
17597            | "net_router_buffer_size" | "net_drop_tail_check"
17598            | "net_burst_size_compute" | "net_packet_pacing_step"
17599            | "net_link_capacity_share" | "net_proportional_fair_share"
17600            | "net_max_min_fair_step" | "net_alpha_fair_step"
17601            | "net_kelly_pricing_step" | "net_network_utility_max"
17602            | "net_lyapunov_drift_plus_penalty" | "net_backpressure_step"
17603            | "net_max_weight_match" | "net_qcsma_propose"
17604            | "net_csma_back_off" | "net_alohanet_throughput"
17605            | "net_slotted_aloha_throughput" | "net_csma_efficiency"
17606            | "net_token_ring_efficiency" | "net_polling_efficiency"
17607            | "net_radio_path_loss" | "net_friis_received_power"
17608            | "net_two_ray_ground_loss" | "net_okumura_hata_loss"
17609            | "net_log_distance_path" | "net_shadowing_normal"
17610            | "net_rician_k_factor" | "net_rayleigh_envelope"
17611            | "net_doppler_shift" | "net_capacity_shannon"
17612            | "net_mimo_capacity_step" | "net_zero_forcing_beam"
17613            | "net_mmse_beam_step" | "net_water_filling_power"
17614            | "net_amc_threshold_index" | "net_harq_combining_gain"
17615            | "net_turbo_decode_iter" | "net_ldpc_iteration_step"
17616            | "net_polar_decode_step" | "net_viterbi_step"
17617            | "net_bcjr_step" | "net_outage_probability"
17618            | "net_diversity_gain" | "net_array_gain"
17619            | "net_multiplexing_gain" | "net_coding_gain"
17620            | "net_pruning_gain" | "net_macro_diversity_step"
17621            | "net_micro_diversity_step" | "net_handoff_threshold"
17622            | "net_call_admission_check" | "net_blocking_probability"
17623            | "net_erlang_b_formula" | "net_erlang_c_formula"
17624            | "net_engset_formula" | "net_little_law_l"
17625            | "net_throughput_law" | "net_response_time_law"
17626            | "net_utilization_law" | "net_forced_flow_law"
17627            // ── OS internals — schedulers, I/O, memory ──────────
17628            | "os_priority_aging_step" | "os_mlfq_demote_step"
17629            | "os_mlfq_promote_step" | "os_round_robin_quantum"
17630            | "os_completely_fair_vruntime" | "os_lottery_ticket_count"
17631            | "os_stride_pass_step" | "os_eevdf_eligible"
17632            | "os_cfs_load_balance_step" | "os_eas_energy_estimate"
17633            | "os_smt_threading_share" | "os_numa_node_distance"
17634            | "os_cpu_affinity_score" | "os_thread_migration_cost"
17635            | "os_load_average_decay" | "os_runqueue_depth"
17636            | "os_io_scheduler_deadline" | "os_io_scheduler_cfq_step"
17637            | "os_io_scheduler_noop_step" | "os_io_scheduler_bfq_step"
17638            | "os_io_scheduler_kyber_step" | "os_io_scheduler_mq_deadline"
17639            | "os_anticipation_window" | "os_elevator_step"
17640            | "os_disk_seek_time" | "os_disk_rotational_lat"
17641            | "os_disk_transfer_time" | "os_pre_fetch_window"
17642            | "os_buffer_cache_pages" | "os_dirty_page_threshold"
17643            | "os_writeback_step" | "os_swappiness_factor"
17644            | "os_kswapd_wake_threshold" | "os_oom_score_step"
17645            | "os_page_replacement_lru" | "os_page_replacement_clock"
17646            | "os_page_replacement_2q" | "os_working_set_size"
17647            | "os_thrashing_threshold" | "os_demand_paging_step"
17648            | "os_copy_on_write_check" | "os_zero_page_optimization"
17649            | "os_huge_page_threshold" | "os_transparent_hugepage"
17650            | "os_kasan_shadow_offset" | "os_kfence_check"
17651            | "os_kfence_alloc_index" | "os_slub_object_size_round"
17652            | "os_slab_color_offset" | "os_per_cpu_cache_size"
17653            | "os_buddy_order_pick" | "os_compact_memory_step"
17654            | "os_kvm_vmcs_field_offset" | "os_apic_irq_priority"
17655            | "os_msi_x_vector_count" | "os_iommu_domain_step"
17656            | "os_pci_bus_address" | "os_acpi_state_transition"
17657            | "os_cpufreq_governor_step" | "os_intel_pstate_target"
17658            | "os_amd_pstate_target" | "os_thermal_zone_trip"
17659            | "os_throttle_temperature" | "os_battery_capacity_pct"
17660            | "os_powertop_score" | "os_idle_state_select"
17661            | "os_c_state_residency" | "os_p_state_voltage"
17662            | "os_dvfs_step" | "os_voltage_scaling_step"
17663            | "os_frequency_scaling_step" | "os_inotify_event_count"
17664            | "os_epoll_ctl_count" | "os_io_uring_sqe_count"
17665            | "os_io_uring_cqe_count" | "os_kqueue_event_count"
17666            | "os_systemd_journal_size" | "os_dmesg_severity_level"
17667            | "os_audit_event_priority" | "os_apparmor_profile_active"
17668            | "os_selinux_context_match" | "os_smack_label_compare"
17669            | "os_capability_check" | "os_seccomp_filter_step"
17670            | "os_namespace_isolation" | "os_cgroup_v1_count"
17671            | "os_cgroup_v2_count" | "os_pid_max_value"
17672            | "os_thread_max_value" | "os_file_max_value"
17673            | "os_open_files_count" | "os_socket_max_value"
17674            | "os_inotify_max_watches" | "os_oom_kill_score"
17675            | "os_zswap_compress_ratio" | "os_zram_compress_ratio"
17676            | "os_swap_pressure_score" | "os_pressure_stall_step"
17677            | "os_psi_avg10_step" | "os_psi_avg60_step"
17678            | "os_psi_avg300_step" | "os_load_proc_avg"
17679            | "os_load_user_avg" | "os_load_iowait_avg"
17680            // ── security — KDFs, MFA, PKI, web sec, TLS ─────────
17681            | "sec_argon2_memcost" | "sec_argon2_timecost"
17682            | "sec_argon2_parallelism" | "sec_argon2_block_step"
17683            | "sec_pbkdf2_iter" | "sec_scrypt_n_param"
17684            | "sec_scrypt_r_param" | "sec_scrypt_p_param"
17685            | "sec_balloon_hash_step" | "sec_yescrypt_step"
17686            | "sec_bcrypt_cost_factor" | "sec_bcrypt_round_step"
17687            | "sec_password_strength_zxcvbn" | "sec_haveibeenpwned_check"
17688            | "sec_diceware_word_index" | "sec_xkcd_passphrase_score"
17689            | "sec_passphrase_entropy" | "sec_chosen_charset_strength"
17690            | "sec_keystroke_timing_var" | "sec_2fa_totp_window"
17691            | "sec_totp_drift_check" | "sec_hotp_counter_step"
17692            | "sec_yubikey_otp_check" | "sec_webauthn_attestation_check"
17693            | "sec_fido2_assertion_check" | "sec_certificate_chain_depth"
17694            | "sec_revocation_ocsp_check" | "sec_crl_age_seconds"
17695            | "sec_pki_path_validate" | "sec_x509_subject_match"
17696            | "sec_san_match_count" | "sec_basic_constraints_ca"
17697            | "sec_pinning_compare" | "sec_certificate_transparency"
17698            | "sec_dane_tlsa_match" | "sec_hpkp_pin_match"
17699            | "sec_csp_directive_match" | "sec_csrf_token_match"
17700            | "sec_cors_origin_match" | "sec_xss_filter_score"
17701            | "sec_html_escape_check" | "sec_url_safe_encode_check"
17702            | "sec_path_traversal_detect" | "sec_sqli_pattern_score"
17703            | "sec_xxe_pattern_score" | "sec_xxe_dtd_check"
17704            | "sec_command_injection_score" | "sec_idor_check"
17705            | "sec_jwt_alg_safe" | "sec_jwt_kid_match"
17706            | "sec_jwt_signature_verify" | "sec_oauth2_state_validate"
17707            | "sec_oauth2_pkce_step" | "sec_oauth_nonce_check"
17708            | "sec_session_lifetime" | "sec_idle_timeout_step"
17709            | "sec_login_throttle_step" | "sec_account_lockout_step"
17710            | "sec_password_history_check" | "sec_complexity_policy_score"
17711            | "sec_dictionary_attack_check" | "sec_brute_force_attempts"
17712            | "sec_credential_stuffing_score" | "sec_kerberos_ticket_age"
17713            | "sec_kerberos_pac_check" | "sec_kerberos_pre_auth"
17714            | "sec_ldap_bind_step" | "sec_radius_auth_step"
17715            | "sec_diameter_avp_step" | "sec_saml_assertion_age"
17716            | "sec_oidc_id_token_age" | "sec_acme_dns_challenge"
17717            | "sec_dnssec_signature_check" | "sec_spf_pass_check"
17718            | "sec_dkim_signature_check" | "sec_dmarc_policy_check"
17719            | "sec_arc_chain_step" | "sec_smtp_ssl_check"
17720            | "sec_imap_starttls_check" | "sec_pop3_security_step"
17721            | "sec_tls_alert_severity" | "sec_tls13_handshake_step"
17722            | "sec_tls12_handshake_step" | "sec_tls11_deprecation_check"
17723            | "sec_ssl3_disabled_check" | "sec_cipher_suite_strength"
17724            | "sec_cbc_mac_block_count" | "sec_gcm_iv_unique_check"
17725            | "sec_chachapoly_nonce_check" | "sec_x25519_clamping_step"
17726            | "sec_ed25519_signature_step" | "sec_ed448_signature_step"
17727            | "sec_p384_curve_step" | "sec_secp256k1_step"
17728            | "sec_blake3_chunk_step" | "sec_keccak_round_step"
17729            | "sec_sha3_padding_step" | "sec_argon2_state_advance"
17730            | "sec_chacha20_quarterround" | "sec_aes_round_step"
17731            | "sec_aes_keyschedule_step" | "sec_des_round_step"
17732            | "sec_blowfish_round_step" | "sec_serpent_round_step"
17733            | "sec_twofish_round_step"
17734            // ── calendrical algorithms ──────────────────────────
17735            | "fixed_from_gregorian" | "gregorian_from_fixed"
17736            | "fixed_from_julian" | "julian_from_fixed"
17737            | "iso_week_date" | "hebrew_leap_year"
17738            | "hebrew_year_length" | "fixed_from_hebrew"
17739            | "islamic_leap_year" | "fixed_from_islamic"
17740            | "persian_arithmetic_leap" | "fixed_from_persian"
17741            | "coptic_from_fixed" | "ethiopic_from_fixed"
17742            | "french_revolutionary_leap" | "fixed_from_french"
17743            | "chinese_year_zodiac" | "chinese_lunation_winter"
17744            | "hindu_solar_year" | "hindu_lunisolar_month"
17745            | "maya_long_count_from_fixed" | "mayan_haab_from_fixed"
17746            | "mayan_tzolkin_from_fixed" | "badi_year_from_fixed"
17747            | "bahai_from_fixed" | "easter_gregorian_year"
17748            | "easter_orthodox_year" | "easter_julian_year"
17749            | "day_of_week_zeller" | "iso_day_number"
17750            | "weekday_name_short" | "leap_year_gregorian"
17751
17752            // ── R / SciPy distributions and tests ───────────────
17753            | "dnorm" | "dt" | "df_dist" | "dchisq"
17754            | "glm" | "aov" | "shapiro_wilk" | "anderson_darling"
17755            | "kolmogorov_smirnov" | "spearmanr" | "kendalltau" | "pearsonr"
17756            | "mannwhitneyu" | "wilcoxon" | "kruskal_h"
17757
17758            // ── APL/J/K array primitives ────────────────────────
17759            | "iota_n" | "reduce_axis" | "scan_axis" | "fold_axis"
17760            | "rotate_axis" | "transpose_axis" | "reshape_dim"
17761            | "encode_base" | "decode_base" | "nub_list" | "nub_count"
17762            | "membership_idx" | "deal_n_k" | "roll_n"
17763            | "permute_idx" | "invert_perm"
17764
17765            // ── astronomy / astrometry ──────────────────────────
17766            | "julian_day" | "jd_to_calendar" | "tt_to_tdb"
17767            | "ra_dec_to_alt_az" | "alt_az_to_ra_dec"
17768            | "precession_iau2006" | "nutation_iau2000a"
17769            | "aberration_annual" | "proper_motion_apply"
17770            | "parallax_correction" | "sun_position_low" | "sun_distance_au"
17771            | "moon_position_low" | "moon_phase_age" | "lunation_index"
17772            | "eclipse_magnitude" | "saros_cycle" | "metonic_cycle"
17773            | "orbit_kepler3" | "orbital_period_au" | "orbit_eccentric_anomaly"
17774            | "escape_velocity_body" | "hill_sphere_radius" | "tisserand_param"
17775            | "tle_mean_motion" | "sgp4_propagate_step" | "airy_disk_radius"
17776            | "rayleigh_criterion" | "strehl_ratio" | "au_to_km"
17777
17778            // ── sports analytics — ratings & sabermetric ────────
17779            | "elo_expected" | "elo_update" | "glicko_rating"
17780            | "trueskill_update" | "trueskill_match_quality"
17781            | "pythagorean_expectation" | "war_above_replacement"
17782            | "woba_weight" | "wrc_plus" | "ops_plus" | "era_plus"
17783            | "fip" | "xfip" | "siera" | "babip" | "wpa"
17784            | "win_probability" | "leverage_index" | "clutch_score"
17785            | "shooting_pct" | "save_pct" | "corsi_for" | "fenwick_for"
17786            | "goals_above_avg" | "tackle_efficiency" | "yards_per_attempt"
17787            | "qbr_metric" | "epa_per_play"
17788
17789            // ── Excel/Sheets + bond/loan financial ──────────────
17790            | "vlookup" | "hlookup" | "xlookup" | "index_match"
17791            | "indirect" | "choose" | "offset"
17792            | "sumif" | "countif" | "averageif"
17793            | "sumifs" | "countifs" | "averageifs"
17794            | "sumproduct" | "rank_eq" | "rank_avg" | "percentrank"
17795            | "quartile_inc" | "quartile_exc"
17796            | "xnpv" | "ppmt" | "ipmt" | "rate"
17797            | "macauley_duration" | "convexity" | "yield_to_maturity"
17798            | "accrued_interest" | "clean_price" | "dirty_price"
17799            | "coupon_count" | "skill_score" | "reliability_diagram"
17800            | "taylor_diagram_score"
17801
17802            // ── GIS — geohash, H3, S2, UTM, projections ─────────
17803            | "geohash_neighbors" | "h3_index" | "h3_geo_to_h3"
17804            | "h3_h3_to_geo" | "h3_k_ring" | "h3_neighbor" | "h3_resolution"
17805            | "s2_cell_id" | "s2_cell_at_lat_lng" | "s2_cell_neighbors"
17806            | "utm_from_lat_lng" | "utm_to_lat_lng"
17807            | "mgrs_encode" | "mgrs_decode"
17808            | "lat_lng_to_xy_mercator" | "lat_lng_to_xy_lambert"
17809            | "haversine_dist" | "vincenty_dist" | "andoyer_dist"
17810            | "rhumb_line_bearing"
17811            | "destination_point" | "tile_xyz_to_lat_lng" | "lat_lng_to_tile_xyz"
17812            | "polygon_winding_order" | "point_in_polygon_ray"
17813            | "point_in_polygon_winding" | "segment_intersection"
17814            | "segment_distance_point" | "convex_hull_chan"
17815
17816            // ── robotics & control ──────────────────────────────
17817            | "pid_anti_windup" | "pid_ziegler_nichols"
17818            | "smith_predictor_step" | "lqr_gain_continuous"
17819            | "lqr_gain_discrete" | "lqg_step" | "h_infinity_norm"
17820            | "bode_gain_margin" | "bode_phase_margin"
17821            | "nyquist_encirclement" | "nichols_chart_step"
17822            | "servo_position_velocity" | "servo_torque_step"
17823            | "imu_madgwick_step" | "imu_mahony_step" | "quaternion_from_imu"
17824            | "denavit_hartenberg_h" | "forward_kinematics_dh"
17825            | "inverse_kinematics_2link" | "jacobian_2dof"
17826            | "manipulability_yoshikawa" | "singularity_check_2link"
17827            | "path_dubins_lsl" | "path_dubins_rsr" | "path_reeds_shepp"
17828            | "rrt_extend" | "rrt_star_rewire" | "prm_node_connect"
17829
17830            // ── actuarial science ───────────────────────────────
17831            | "life_expectancy_e0" | "force_of_mortality" | "select_ultimate"
17832            | "annuity_due_an" | "annuity_immediate_an"
17833            | "term_life_a_n_t" | "whole_life_a"
17834            | "endowment_pure_e" | "endowment_combined_a"
17835            | "premium_net" | "level_premium"
17836            | "reserve_prospective" | "reserve_retrospective"
17837            | "gross_premium_load" | "experience_factor"
17838            | "mortality_table_q" | "select_period_step"
17839            | "multi_decrement_q" | "multi_state_pij"
17840            | "credibility_buhlmann" | "loss_severity_lognormal"
17841            | "loss_frequency_poisson" | "ruin_probability_lundberg"
17842            | "cramer_lundberg_step" | "bornhuetter_ferguson"
17843            | "chain_ladder_step" | "ibnr_estimate" | "run_off_triangle_step"
17844
17845            // ── epidemiology / public health ────────────────────
17846            | "r_naught_basic" | "r_effective_t" | "doubling_time_growth"
17847            | "sirs_step" | "seirs_step" | "susceptible_to_infected"
17848            | "attack_rate" | "vaccination_coverage_required"
17849            | "cfr_case_fatality" | "ifr_infection_fatality"
17850            | "dalys_disability_weight" | "qaly_lifetime" | "ylll_pml"
17851            | "rt_serial_interval" | "generation_time_step"
17852            | "gini_inequality_health" | "standardized_mortality_smr"
17853            | "indirect_age_adjusted" | "direct_age_adjusted"
17854            | "odds_ratio_2x2" | "risk_ratio_2x2" | "number_needed_to_treat"
17855            | "attributable_fraction_pop" | "preventive_fraction"
17856            | "contact_tracing_eff" | "cluster_attack_rate"
17857            | "transmission_pair_index"
17858
17859            // ── archive/encoding format primitives ──────────────
17860            | "tar_header_checksum" | "tar_pad_512" | "tar_member_record"
17861            | "zip_local_header" | "zip_central_dir" | "zip_eocd"
17862            | "gzip_member_step" | "gzip_crc32_init" | "gzip_isize"
17863            | "deflate_dynamic_huffman" | "deflate_static_block"
17864            | "lz4_block_step" | "lz4_match_offset"
17865            | "zstd_frame_header" | "brotli_huffman_table"
17866            | "brotli_meta_block" | "lzma_range_step"
17867            | "quoted_printable_encode" | "uuencode_step"
17868            | "modhex_encode" | "percent_encode_full"
17869            | "punycode_encode" | "idn_to_ascii" | "idn_to_unicode"
17870            | "msgpack_pack_int" | "msgpack_pack_str"
17871            | "cbor_encode_uint" | "cbor_encode_str"
17872
17873            // ── chemistry & biochemistry ────────────────────────
17874            | "molecular_weight_compound" | "molarity_dilution"
17875            | "gas_constant_value" | "eyring_rate" | "van_t_hoff_kp"
17876            | "henderson_buffer" | "titration_ph_endpoint"
17877            | "isoelectric_point_protein" | "ka_to_pka" | "pkb_to_kb"
17878            | "amphoteric_check" | "oxidation_number"
17879            | "half_reaction_balance" | "redox_potential_cell"
17880            | "electrolysis_mass" | "spectrophotometer_beer_lambert"
17881            | "epsilon_extinction" | "transmittance_to_a"
17882            | "crystal_field_ligand" | "jahn_teller_check"
17883            | "vsepr_geometry" | "lewis_dot_count"
17884            | "formal_charge" | "resonance_count"
17885            | "ramachandran_phi_psi" | "rg_radius_of_gyration"
17886            | "spectroscopic_factor" | "avogadro_constant"
17887
17888            // ── music theory ────────────────────────────────────
17889            | "cents_between_freqs" | "note_name_from_midi"
17890            | "interval_quality_size" | "scale_pitches_major"
17891            | "scale_pitches_minor" | "mode_pitches_dorian"
17892            | "mode_pitches_phrygian" | "mode_pitches_lydian"
17893            | "chord_root_inversion" | "chord_quality_classify"
17894            | "chord_voicing_close" | "key_signature_sharps"
17895            | "key_signature_flats" | "tempo_to_ms" | "beat_to_seconds"
17896            | "time_sig_subdivision" | "equal_tempered_freq"
17897            | "just_intonation_freq" | "pythagorean_freq"
17898            | "mean_tone_freq" | "werckmeister_iii" | "kirnberger_iii"
17899            | "dynamics_db_level" | "harmonics_partial"
17900
17901            // ── geology, seismology, mineralogy ─────────────────
17902            | "moment_magnitude_mw" | "richter_local_ml"
17903            | "surface_wave_ms" | "body_wave_mb"
17904            | "gutenberg_richter_b" | "omori_aftershock"
17905            | "pga_attenuation" | "arias_intensity" | "shake_map_pga"
17906            | "liquefaction_potential_index" | "spt_n_correction"
17907            | "mineral_mohs_hardness" | "streak_color_index"
17908            | "specific_gravity_water" | "feldspar_classify"
17909            | "silicate_classify" | "igneous_qapf"
17910            | "metamorphic_grade" | "crustal_density_depth"
17911            | "pwave_velocity_depth" | "swave_velocity_depth"
17912            | "gradient_geothermal" | "heat_flow_radiogenic"
17913
17914            // ── BLAS / LAPACK ───────────────────────────────────
17915            | "dgemm" | "sgemm" | "zgemm" | "cgemm"
17916            | "dgemv" | "sgemv" | "dtrsm" | "strsm"
17917            | "dgesv" | "dgetrf" | "dgeqrf" | "dgesvd"
17918            | "dsyevd" | "dpotrf" | "daxpy" | "ddot"
17919            | "dnrm2" | "dscal" | "dasum" | "idamax"
17920            | "dsyrk" | "dgerqf" | "dorgqr" | "dorglq"
17921            | "drot" | "drotg" | "dpbsv" | "dgbsv"
17922            | "dtbsv" | "dtrsv" | "ddrot" | "dgemm3m"
17923            | "dgels" | "dgelsd"
17924
17925            // ── logic, proof, SAT/SMT, type theory ──────────────
17926            | "cnf_unit_propagate" | "cnf_pure_literal_elim"
17927            | "cnf_dpll_branch" | "dpll_clause_learning"
17928            | "two_watched_literals" | "walksat_step"
17929            | "resolution_step" | "subsumption_check"
17930            | "tableau_branch_close" | "sequent_left_intro"
17931            | "sequent_right_intro" | "nbe_normalize"
17932            | "church_numeral_n" | "encode_pair" | "encode_succ"
17933            | "simply_typed_check" | "hindley_milner_step"
17934            | "unification_robinson" | "bdd_apply" | "bdd_restrict"
17935            | "bdd_quantify" | "aig_simplify_step"
17936            | "smt_qf_lia_solve_step" | "smt_qf_uf_combine"
17937            | "model_checking_ctl" | "model_checking_ltl"
17938            | "bisimulation_step" | "coq_tactic_apply"
17939            | "coq_unify_term" | "refl_check" | "sym_check" | "trans_check"
17940
17941            // ── compilers / parsing ─────────────────────────────
17942            | "nfa_to_dfa" | "subset_construction"
17943            | "dfa_minimize_hopcroft" | "regex_to_nfa_thompson"
17944            | "glushkov_construction" | "brzozowski_derivative"
17945            | "ll1_first_set" | "ll1_follow_set" | "ll1_predict_table"
17946            | "lr0_items_step" | "lalr_lookahead_compute"
17947            | "lr1_canonical_collection"
17948            | "earley_scan" | "earley_predict" | "earley_complete"
17949            | "packrat_parse_step" | "ascent_parser_step"
17950            | "pratt_parse_step" | "shunting_yard_step"
17951            | "regex_compile_thompson" | "regex_match_dfa"
17952            | "lex_keyword_classify"
17953            | "peg_seq" | "peg_choice" | "peg_repeat" | "peg_lookahead"
17954            | "dfa_simulate_step" | "bytecode_disasm_step"
17955            | "ssa_phi_insert" | "dom_tree_idom" | "dominance_frontier"
17956
17957            // ── computational linguistics ───────────────────────
17958            | "porter_stem_step" | "snowball_stem_english"
17959            | "snowball_stem_french" | "lemmatize_wordnet"
17960            | "lemmatize_lemmy" | "stem_lancaster"
17961            | "soundex_phonetic" | "metaphone_phonetic"
17962            | "caverphone_2" | "nysiis_phonetic"
17963            | "match_rating_codex" | "daitch_mokotoff"
17964            | "viterbi_pos_tag" | "forward_backward_pos"
17965            | "crf_log_likelihood" | "bigram_perplexity"
17966            | "trigram_perplexity" | "ner_bilou_decode"
17967            | "constituency_cyk" | "dependency_parse_eisner"
17968            | "transition_arc_eager" | "transition_arc_standard"
17969            | "word_alignment_ibm1" | "word_alignment_ibm2"
17970            | "lexicalized_parse" | "coreference_singleton"
17971            | "anaphora_distance" | "head_finding_collins"
17972            | "tree_kernel_collins"
17973
17974            // ── Postgres SQL strings, JSON, aggregates ─────────
17975            | "btrim" | "translate" | "ascii"
17976            | "regexp_split" | "regexp_matches" | "regexp_replace"
17977            | "json_build_object" | "jsonb_set"
17978            | "json_array_length" | "json_extract_path"
17979            | "json_strip_nulls" | "jsonb_pretty"
17980            | "jsonb_path_query" | "json_each"
17981            | "jsonb_array_length" | "jsonb_object_keys"
17982            | "jsonb_typeof" | "array_to_jsonb"
17983            | "ts_match" | "ts_rank" | "ts_headline"
17984            | "substring_similarity" | "levenshtein_dist"
17985            | "word_similarity" | "strict_word_similarity"
17986            | "hstore_to_array" | "array_to_hstore"
17987            | "string_agg" | "array_agg"
17988            | "corr_agg" | "covar_pop" | "covar_samp"
17989            | "regr_slope" | "regr_intercept" | "regr_r2"
17990            | "percentile_cont" | "percentile_disc" | "mode_agg"
17991            | "array_to_string" | "array_position" | "array_positions"
17992            | "array_remove" | "array_replace"
17993            | "xmlforest" | "xmlagg"
17994
17995            // ── Redis-flavour primitives ────────────────────────
17996            | "zadd" | "zrem" | "zrangebyscore"
17997            | "zrank" | "zrevrank" | "zincrby"
17998            | "zcard" | "zcount" | "zlexcount"
17999            | "lpush" | "rpush" | "lrange" | "lrem"
18000            | "hset" | "hget" | "hgetall" | "hlen"
18001            | "hkeys" | "hvals" | "hmset" | "hincrby"
18002            | "sadd" | "srem" | "smembers"
18003            | "sinter" | "sunion" | "sdiff"
18004            | "scard" | "sismember" | "spop"
18005            | "setex" | "setnx" | "expire"
18006            | "ttl" | "pttl" | "persist"
18007            | "incr" | "decr" | "incrby" | "decrby"
18008            | "getset" | "mset" | "mget" | "renamenx"
18009            | "dbsize" | "type_redis" | "exists_key"
18010            | "strlen" | "getrange" | "setrange" | "append_redis"
18011            | "bitcount" | "bitop" | "bitpos"
18012            | "pfadd" | "pfcount"
18013            | "geoadd" | "geodist" | "geohash"
18014            | "xadd" | "xlen" | "xrange"
18015            | "object_encoding" | "debug_object" | "cluster_slots"
18016
18017            // ── NumPy + scipy.special ──────────────────────────
18018            | "argpartition" | "bincount" | "nonzero_count"
18019            | "flatnonzero" | "searchsorted" | "digitize"
18020            | "histogram_bin_edges" | "unique_count"
18021            | "polyfit_rmse"
18022            | "ellipk" | "ellipe"
18023            | "hyp1f1" | "hyp2f1" | "mathieu_b"
18024            | "spherical_jn" | "spherical_yn"
18025            | "jv" | "yn" | "iv" | "kv"
18026            | "airyai" | "airybi"
18027            | "polygamma" | "trigamma" | "loggamma"
18028            | "factorial2" | "factorialk"
18029            | "owens_t" | "marcum_q" | "voigt_profile"
18030            | "chebyt" | "chebyu" | "sph_harm"
18031            | "wofz" | "erfcx" | "erfi" | "dawsn"
18032            | "interp1d"
18033            | "convolve_full" | "convolve_valid" | "correlate_full"
18034            | "kron_product"
18035            | "simpson_rule" | "romberg_quad" | "fixed_quad"
18036            | "ode45_step" | "ode_lsoda" | "solve_ivp_step"
18037            | "root_brentq" | "root_newton" | "root_secant"
18038            | "fmin_powell" | "fmin_cobyla"
18039
18040            // ── economics + game theory ─────────────────────────
18041            | "cobb_douglas" | "ces_production"
18042            | "leontief_input" | "leontief_output"
18043            | "slutsky_decompose"
18044            | "marshallian_demand" | "hicksian_demand"
18045            | "expenditure_function" | "indirect_utility"
18046            | "gale_shapley_step" | "deferred_acceptance"
18047            | "top_trading_cycle" | "vcg_payment" | "myerson_optimal"
18048            | "gini_market" | "hhi_concentration"
18049            | "cournot_eq" | "stackelberg_eq" | "bertrand_eq"
18050            | "monopoly_lerner"
18051            | "consumer_surplus" | "producer_surplus"
18052            | "deadweight_loss" | "tax_incidence"
18053            | "pareto_efficiency" | "edgeworth_box_alloc"
18054            | "social_welfare_utilitarian"
18055            | "social_welfare_rawls" | "social_welfare_nash"
18056            | "arrow_independence"
18057            | "vickrey_auction" | "first_price_seal"
18058            | "english_auction" | "dutch_auction"
18059            | "core_coalition" | "stable_matching_count"
18060            | "gale_optimal" | "pareto_dominance"
18061            | "lerner_index"
18062            | "price_elasticity" | "supply_elasticity"
18063            | "income_elasticity" | "engel_curve" | "cross_elasticity"
18064            | "diff_in_diff" | "did_estimator" | "rdd_estimate"
18065            // ── SciPy.signal — DSP filters, windows, transforms ──
18066            | "hann_w" | "hamming_w" | "blackman_w" | "barthann_w"
18067            | "nuttall_w" | "flattop_w" | "parzen_window" | "tukey_w"
18068            | "taylor_window" | "dpss_window" | "kaiserord_step"
18069            | "butter_lp_re" | "butter_hp_mag"
18070            | "cheby1_lp" | "cheby2_lp" | "ellip_lp" | "bessel_lp"
18071            | "notch_filter"
18072            | "sosfilt_step" | "lfilter_zi_init" | "filtfilt_pad"
18073            | "freqz_eval" | "freqs_eval" | "group_delay_eval"
18074            | "impulse_response_n"
18075            | "tf2zpk_step" | "zpk2tf_step" | "tf2sos_step"
18076            | "zpk2sos_step" | "sos2tf_step"
18077            | "bilinear_xform" | "bilinear_zpk_xform"
18078            | "firwin_lowpass" | "firwin_highpass"
18079            | "firwin_bandpass" | "firwin_bandstop"
18080            | "firwin2_freq" | "remez_design"
18081            | "stft_step" | "istft_step"
18082            | "cwt_morlet" | "ricker_wavelet" | "mexican_hat_wavelet"
18083            | "coherence_xy" | "csd_xy" | "welch_psd_avg"
18084            | "periodogram_basic" | "lombscargle_freq"
18085            | "hilbert_signal" | "envelope_amplitude"
18086            | "deconvolve_step" | "fftconvolve_step" | "oaconvolve_step"
18087            | "upfirdn_step" | "resample_poly_step" | "decimate_step"
18088            | "savgol_coef" | "detrend_linear"
18089            | "wiener_filter" | "medfilt_1d" | "peak_widths_at"
18090            // ── NetworkX graph algorithms ───────────────────────
18091            | "dijkstra_relax" | "bellman_ford_relax"
18092            | "floyd_warshall_step" | "johnson_reweight"
18093            | "astar_search" | "bidirectional_dijkstra"
18094            | "yen_k_shortest" | "ida_star"
18095            | "bfs_count" | "dfs_postorder_done" | "topo_kahn_step"
18096            | "tarjan_scc_step" | "kosaraju_step"
18097            | "kruskal_step" | "prim_step" | "boruvka_step"
18098            | "reverse_delete_step"
18099            | "ford_fulkerson_step" | "edmonds_karp_bfs"
18100            | "dinic_step" | "push_relabel_relabel"
18101            | "stoer_wagner_step" | "karger_step"
18102            | "pagerank_iter" | "hits_authority" | "hits_hub"
18103            | "personalized_pagerank"
18104            | "centrality_degree" | "centrality_closeness"
18105            | "centrality_betweenness" | "centrality_eigenvector"
18106            | "centrality_katz" | "harmonic_centrality" | "load_centrality"
18107            | "clustering_coefficient" | "triangles_count" | "transitivity"
18108            | "modularity_score" | "louvain_gain"
18109            | "label_propagation" | "girvan_newman"
18110            | "articulation_point" | "bridge_edge"
18111            | "edge_connectivity" | "vertex_connectivity"
18112            | "biconnected_components"
18113            | "gx_diameter" | "gx_radius" | "gx_eccentricity"
18114            | "warshall_step"
18115            | "tsp_held_karp" | "tsp_nn_step" | "tsp_christofides"
18116            | "graph_coloring_greedy" | "welsh_powell"
18117            | "vf2_consistent" | "subgraph_isomorphism"
18118            | "hungarian_step" | "hopcroft_karp_step"
18119            | "bron_kerbosch"
18120            | "min_vertex_cover" | "max_independent_set"
18121            | "dominating_set_greedy" | "hamiltonian_path"
18122            | "min_steiner_tree" | "k_shortest_spanning"
18123            | "random_walk_hitting" | "simrank"
18124            // ── Pandas DataFrame ops ────────────────────────────
18125            | "df_groupby" | "df_aggregate" | "df_apply"
18126            | "df_transform" | "df_pivot" | "df_pivot_table"
18127            | "df_melt" | "df_stack" | "df_unstack"
18128            | "df_explode" | "df_get_dummies" | "df_crosstab"
18129            | "df_merge" | "df_join" | "df_concat"
18130            | "df_resample" | "df_rolling" | "df_expanding"
18131            | "df_ewm" | "df_shift" | "df_diff"
18132            | "df_pct_change" | "df_corr" | "df_cov"
18133            | "df_corrwith" | "df_describe" | "df_kurtosis"
18134            | "df_skew" | "df_sem" | "df_mad"
18135            | "df_dropna" | "df_fillna" | "df_interpolate"
18136            | "df_replace" | "df_isnull" | "df_notnull"
18137            | "df_sort_values" | "df_rank" | "df_quantile"
18138            | "df_value_counts" | "df_sample" | "df_nlargest"
18139            | "df_nsmallest" | "df_idxmax" | "df_idxmin"
18140            | "df_clip" | "df_round" | "df_to_datetime"
18141            | "df_to_timedelta" | "df_to_numeric" | "df_eval"
18142            | "df_query" | "df_filter" | "df_drop_duplicates"
18143            | "df_duplicated" | "df_set_index" | "df_reset_index"
18144            // ── PIL/OpenCV image processing ─────────────────────
18145            | "image_resize" | "image_grayscale" | "image_threshold"
18146            | "image_blur_gaussian" | "image_blur_box" | "image_sharpen"
18147            | "image_edge_canny" | "image_edge_sobel" | "image_edge_laplacian"
18148            | "image_dilate" | "image_erode" | "image_morphology_open"
18149            | "image_morphology_close" | "image_histogram" | "image_equalize"
18150            | "image_clahe" | "image_contrast" | "image_brightness"
18151            | "image_gamma" | "image_invert" | "image_sepia"
18152            | "image_posterize" | "image_solarize" | "convolve_2d"
18153            | "filter_median" | "filter_bilateral" | "filter_nlmeans"
18154            | "gabor_filter" | "hog_features" | "harris_corners"
18155            | "shi_tomasi_corners" | "sift_keypoints" | "orb_keypoints"
18156            | "surf_keypoints" | "template_match" | "face_detect_haar"
18157            | "watershed_segment" | "slic_superpixels" | "felzenszwalb_segment"
18158            | "graph_cut_segment" | "hough_lines" | "hough_circles"
18159            | "ransac_homography" | "optical_flow_lk" | "optical_flow_farneback"
18160            | "corner_subpix" | "image_rotate" | "image_flip_h"
18161            | "image_flip_v" | "image_emboss" | "image_motion_blur"
18162            // ── statsmodels ─
18163            | "arima_fit" | "arima_forecast" | "arma_order_select"
18164            | "sarimax_fit" | "garch_fit" | "ewma_smooth"
18165            | "holt_winters_additive" | "holt_winters_multiplicative" | "kalman_filter_step"
18166            | "kalman_smoother_step" | "var_fit" | "vecm_fit"
18167            | "johansen_test" | "phillips_perron" | "adfuller"
18168            | "kpss_test" | "breusch_godfrey" | "ljung_box_q"
18169            | "durbin_watson_d" | "granger_causality" | "cointegration_eg"
18170            | "seasonal_decompose" | "stl_decompose" | "acf_basis"
18171            | "pacf_basis" | "moving_average_filter" | "exp_smooth_simple"
18172            | "exp_smooth_double" | "markov_switching_ar" | "markov_switching_mr"
18173            | "arch_lm" | "state_space_kalman" | "ucm_unobserved_components"
18174            | "spectral_density_estimate" | "bayesian_step" | "pivoted_cholesky_var"
18175            // ── sklearn ─
18176            | "sk_logistic_predict" | "sk_logistic_fit" | "sk_random_forest_fit"
18177            | "sk_gbt_fit" | "sk_xgb_fit" | "sk_lightgbm_fit"
18178            | "sk_svm_fit" | "sk_kmeans_fit" | "sk_dbscan_fit"
18179            | "sk_agglomerative_fit" | "sk_pca_fit" | "sk_tsne_fit"
18180            | "sk_umap_fit" | "sk_isolation_forest_fit" | "sk_lof_fit"
18181            | "sk_kfold_split" | "sk_stratified_kfold" | "sk_cross_val_score"
18182            | "sk_grid_search" | "sk_random_search" | "sk_bayes_search"
18183            | "sk_pipeline_fit" | "sk_standard_scaler" | "sk_min_max_scaler"
18184            | "sk_robust_scaler" | "sk_quantile_transform" | "sk_power_transform"
18185            | "sk_one_hot" | "sk_ordinal_encode" | "sk_label_encode"
18186            | "sk_tfidf" | "sk_count_vectorize" | "sk_silhouette"
18187            | "sk_calinski_harabasz" | "sk_davies_bouldin" | "sk_adjusted_rand"
18188            | "sk_mutual_info" | "sk_lda_topic" | "sk_nmf_topic"
18189            | "sk_word2vec_train" | "sk_doc2vec_train" | "sk_naive_bayes_predict"
18190            | "sk_knn_predict" | "sk_decision_tree_split"
18191            // ── quantum ─
18192            | "qubit_x" | "qubit_y" | "qubit_z"
18193            | "qubit_h" | "qubit_s" | "qubit_t"
18194            | "qubit_rx" | "qubit_ry" | "qubit_rz"
18195            | "qubit_u3" | "qubit_u2" | "qubit_u1"
18196            | "qubit_phase" | "qubit_cnot" | "qubit_cz"
18197            | "qubit_swap" | "qubit_ccx" | "qubit_measure"
18198            | "qubit_reset" | "bell_state" | "ghz_state"
18199            | "w_state" | "qft" | "inverse_qft"
18200            | "grover_iter" | "shor_period" | "vqe_step"
18201            | "qaoa_step" | "qpe_iteration" | "pauli_string_expect"
18202            | "circuit_depth" | "circuit_width" | "gate_decompose"
18203            | "ancilla_alloc" | "bloch_sphere_x" | "bloch_sphere_z"
18204            | "density_matrix_purity_q" | "entanglement_entropy" | "quantum_teleportation"
18205            | "superdense_coding" | "noise_model_depolarize"
18206            // ── b81-misc-utility ─
18207            | "mirr_excel" | "accrint" | "cumipmt"
18208            | "cumprinc" | "dollarde" | "dollarfr"
18209            | "received" | "yieldmat" | "yielddisc"
18210            | "duration_macaulay" | "mduration" | "odddyield"
18211            | "disc_excel" | "effect" | "nominal"
18212            | "intrate" | "price_disc" | "cityhash64"
18213            | "farmhash_64" | "metro_hash_64" | "spookyhash_128"
18214            | "t1ha" | "highway_hash" | "fnv0_32"
18215            | "lose_lose"
18216            | "oat_hash" | "lz4_encode_block" | "snappy_encode"
18217            | "zstd_encode_step" | "brotli_encode_meta" | "lzma_encode_step"
18218            | "bz2_encode_step" | "lzo_encode_step" | "deflate_encode_huffman"
18219            | "lzw_encode" | "gzip_encode_step" | "uri_template_expand"
18220            | "uri_resolve" | "uri_normalize" | "percent_decode_url"
18221            | "url_encode_form" | "url_decode_form" | "punycode_decode_step"
18222            | "idn_normalize" | "url_origin" | "etag_validate"
18223            | "cache_control_parse" | "vary_match" | "content_negotiate"
18224            | "accept_lang_pick" | "range_header_parse" | "if_match_check"
18225            | "if_none_match_check" | "digest_auth_quote" | "www_auth_parse"
18226            // ── b82-misc-utility ─
18227            | "iso8601_duration_parse" | "iso8601_duration_to_seconds" | "rrule_next_occurrence"
18228            | "cron_next_fire" | "date_round_iso" | "week_number_iso"
18229            | "fiscal_year_us" | "age_at_date" | "easter_western"
18230            | "easter_orthodox_year_2" | "chinese_new_year" | "solstice_winter"
18231            | "equinox_spring" | "rgb_to_oklab" | "oklab_to_rgb"
18232            | "rgb_to_cmyk" | "cmyk_to_rgb" | "rgb_to_xyz"
18233            | "xyz_to_rgb" | "rgb_to_yuv" | "yuv_to_rgb"
18234            | "luminance_relative" | "contrast_ratio" | "wcag_pass"
18235            | "color_temperature_kelvin" | "delta_e76" | "delta_e94"
18236            | "delta_e2000" | "color_blend_alpha" | "isbn10_check"
18237            | "isbn13_check" | "ean13_check" | "upc_check"
18238            | "eth_addr_check" | "btc_addr_check" | "ssn_check"
18239            | "vin_check" | "imei_check" | "iban_check"
18240            | "cusip_check" | "kde_silverman_bw" | "kde_scott_bw"
18241            | "kde_bandwidth_lscv" | "kde_epanechnikov" | "kde_gaussian_2d"
18242            | "kde_uniform" | "kde_triangular" | "kde_biweight"
18243            | "kde_triweight" | "kde_cosine" | "kde_logistic_kernel"
18244            // ── number theory (extended) ──────────────────────────────────
18245            | "mod_exp" | "modexp" | "powmod"
18246            | "mod_inv" | "modinv" | "chinese_remainder" | "crt"
18247            | "miller_rabin" | "millerrabin" | "is_probable_prime"
18248            // ── combinatorics (extended) ──────────────────────────────────
18249            | "derangements" | "stirling2" | "stirling_second"
18250            | "bernoulli_number" | "bernoulli" | "harmonic_number" | "harmonic"
18251            // ── physics (new) ─────────────────────────────────────────────
18252            | "drag_force" | "fdrag" | "ideal_gas" | "pv_nrt"
18253            // ── financial greeks & risk ───────────────────────────────────
18254            | "bs_delta" | "bsdelta" | "option_delta"
18255            | "bs_gamma" | "bsgamma" | "option_gamma"
18256            | "bs_vega" | "bsvega" | "option_vega"
18257            | "bs_theta" | "bstheta" | "option_theta"
18258            | "bs_rho" | "bsrho" | "option_rho"
18259            | "bond_duration" | "mac_duration"
18260            // ── DSP extensions ────────────────────────────────────────────
18261            | "dct" | "idct" | "goertzel" | "chirp" | "chirp_signal"
18262            // ── encoding extensions ───────────────────────────────────────
18263            | "base85_encode" | "b85e" | "ascii85_encode" | "a85e"
18264            | "base85_decode" | "b85d" | "ascii85_decode" | "a85d"
18265            // ── R base: distributions ─────────────────────────────────────
18266            | "pnorm" | "pbinom" | "dbinom" | "ppois"
18267            | "punif" | "pexp" | "pweibull" | "plnorm" | "pcauchy"
18268            // ── R base: matrix ops ────────────────────────────────────────
18269            | "rbind" | "cbind"
18270            | "row_sums" | "rowSums" | "col_sums" | "colSums"
18271            | "row_means" | "rowMeans" | "col_means" | "colMeans"
18272            | "outer" | "crossprod" | "tcrossprod"
18273            | "nrow" | "ncol" | "prop_table" | "proptable"
18274            // ── R base: vector ops ────────────────────────────────────────
18275            | "cummax" | "cummin" | "scale_vec" | "scale"
18276            | "which_fn" | "tabulate"
18277            | "duplicated" | "duped" | "rev_vec"
18278            | "seq_fn" | "rep_fn" | "rep"
18279            | "cut_bins" | "cut" | "find_interval" | "findInterval"
18280            | "ecdf_fn" | "ecdf" | "density_est" | "density"
18281            | "embed_ts" | "embed"
18282            // ── R base: stats tests ───────────────────────────────────────
18283            | "shapiro_test" | "shapiro" | "ks_test" | "ks"
18284            | "wilcox_test" | "wilcox" | "mann_whitney"
18285            | "prop_test" | "proptest" | "binom_test" | "binomtest"
18286            // ── R base: apply / functional ────────────────────────────────
18287            | "sapply" | "tapply" | "do_call" | "docall"
18288            // ── R base: ML / clustering ───────────────────────────────────
18289            | "kmeans" | "prcomp" | "pca"
18290            // ── R base: random generators ─────────────────────────────────
18291
18292
18293
18294            // ── R base: quantile functions ────────────────────────────────
18295
18296            // ── R base: additional CDFs ───────────────────────────────────
18297            | "pgamma" | "pbeta" | "pchisq" | "pt_cdf" | "pt" | "pf_cdf" | "pf"
18298            // ── R base: additional PMFs ───────────────────────────────────
18299            | "dgeom" | "dunif" | "dnbinom" | "dhyper"
18300            // ── R base: smoothing / interpolation ─────────────────────────
18301            | "lowess" | "loess" | "approx_fn" | "approx"
18302            // ── R base: linear models ─────────────────────────────────────
18303            | "lm_fit" | "lm"
18304            // ── R base: remaining quantiles ───────────────────────────────
18305 | "qt_fn" | "qf_fn"
18306
18307            // ── R base: time series ───────────────────────────────────────
18308            | "acf_fn" | "acf" | "pacf_fn" | "pacf"
18309            | "diff_lag" | "diff_ts" | "ts_filter" | "filter_ts"
18310            // ── R base: regression diagnostics ────────────────────────────
18311            | "predict_lm" | "predict" | "confint_lm" | "confint"
18312            // ── R base: multivariate stats ────────────────────────────────
18313            | "cor_matrix" | "cor_mat" | "cov_matrix" | "cov_mat"
18314            | "mahalanobis" | "mahal" | "dist_matrix" | "dist_mat"
18315            | "hclust" | "cutree" | "weighted_var" | "wvar" | "cov2cor"
18316            // ── SVG plotting ──────────────────────────────────────────────
18317            | "scatter_svg" | "scatter_plot" | "line_svg" | "line_plot"
18318            | "plot_svg" | "hist_svg" | "histogram_svg"
18319            | "boxplot_svg" | "box_plot" | "bar_svg" | "barchart_svg"
18320            | "pie_svg" | "pie_chart" | "heatmap_svg" | "heatmap"
18321            | "donut_svg" | "donut" | "area_svg" | "area_chart"
18322            | "hbar_svg" | "hbar" | "radar_svg" | "radar" | "spider"
18323            | "candlestick_svg" | "candlestick" | "ohlc"
18324            | "violin_svg" | "violin" | "cor_heatmap" | "cor_matrix_svg"
18325            | "stacked_bar_svg" | "stacked_bar"
18326            | "wordcloud_svg" | "wordcloud" | "wcloud"
18327            | "treemap_svg" | "treemap"
18328            | "pvw"
18329            // ── Cyberpunk terminal art ────────────────────────────────
18330            | "cyber_city" | "cyber_grid" | "cyber_rain" | "matrix_rain"
18331            | "cyber_glitch" | "glitch_text" | "cyber_banner" | "neon_banner"
18332            | "cyber_circuit" | "cyber_skull" | "cyber_eye"
18333            // ── AI primitives (docs/AI_PRIMITIVES.md) ─────────────────
18334            | "ai" | "ai_agent" | "prompt" | "stream_prompt" | "stream_prompt_cb"
18335            | "tokens_of"
18336            | "ai_estimate" | "ai_cost" | "ai_history" | "ai_history_clear"
18337            | "ai_cache_clear" | "ai_cache_size"
18338            | "ai_mock_install" | "ai_mock_clear"
18339            | "ai_config_get" | "ai_config_set" | "ai_routing_get" | "ai_routing_set"
18340            | "ai_register_tool" | "ai_unregister_tool" | "ai_clear_tools" | "ai_tools_list"
18341            | "ai_filter" | "ai_map" | "ai_classify" | "ai_match" | "ai_sort" | "ai_dedupe"
18342            | "ai_extract" | "ai_summarize" | "ai_translate" | "ai_template"
18343            | "ai_session_new" | "ai_session_send" | "ai_session_history"
18344            | "ai_session_close" | "ai_session_reset"
18345            | "ai_session_export" | "ai_session_import"
18346            | "ai_memory_save" | "ai_memory_recall" | "ai_memory_forget"
18347            | "ai_memory_count" | "ai_memory_clear"
18348            | "ai_vision" | "ai_pdf" | "ai_grounded" | "ai_citations"
18349            | "ai_transcribe" | "ai_speak" | "ai_image" | "ai_image_edit" | "ai_image_variation"
18350            | "ai_models" | "ai_describe" | "ai_pricing" | "ai_dashboard"
18351            | "ai_moderate" | "ai_chunk" | "ai_warm" | "ai_compare"
18352            | "ai_last_thinking" | "ai_budget" | "ai_batch" | "ai_pmap"
18353            | "ai_file_upload" | "ai_file_list" | "ai_file_get" | "ai_file_delete"
18354            | "ai_file_anthropic_upload" | "ai_file_anthropic_list" | "ai_file_anthropic_delete"
18355            | "vec_cosine" | "vec_search" | "vec_topk"
18356            // ── AI tool specs ────────────────────────────────────────
18357            | "web_search_tool" | "fetch_url_tool" | "read_file_tool" | "run_code_tool"
18358            // ── MCP (Model Context Protocol) ─────────────────────────
18359            | "mcp_connect" | "mcp_close" | "mcp_tools" | "mcp_call"
18360            | "mcp_resource" | "mcp_resources" | "mcp_prompt" | "mcp_prompts"
18361            | "mcp_attach_to_ai" | "mcp_detach_from_ai" | "mcp_attached"
18362            | "mcp_server_start" | "mcp_serve_registered_tools"
18363            // ── PTY / expect (docs/expect-feature-idea.md) ────────────
18364            | "pty_spawn" | "pty_send" | "pty_read" | "pty_expect" | "pty_expect_table"
18365            | "pty_buffer" | "pty_alive" | "pty_eof" | "pty_close" | "pty_interact"
18366            | "pty_strip_ansi" | "pty_after_eof" | "pty_pending_events"
18367            // ── Stress / telemetry extensions ─────────────────────────
18368            | "stress_fp" | "stress_int" | "stress_cache" | "stress_branch"
18369            | "stress_sort" | "stress_alloc" | "stress_mmap" | "stress_disk"
18370            | "stress_iops" | "stress_net" | "stress_http" | "stress_dns"
18371            | "stress_fork" | "stress_thread" | "stress_aes" | "stress_compress"
18372            | "stress_regex" | "stress_json" | "stress_burst" | "stress_ramp"
18373            | "stress_oscillate" | "stress_all" | "stress_temp" | "stress_thermal_zones"
18374            | "stress_freq" | "stress_throttled" | "stress_load" | "stress_meminfo"
18375            | "stress_cores" | "stress_arm_kill_switch" | "stress_killed"
18376            | "stress_disarm_kill_switch"
18377            | "stress_metrics_record" | "stress_metrics_clear" | "stress_metrics_count"
18378            | "stress_metrics_export" | "stress_metrics_prometheus"
18379            | "stress_metrics_json" | "stress_metrics_csv" | "stress_metrics_watch"
18380            // ── Compliance / secrets ─────────────────────────────────
18381            | "audit_log" | "audit_log_path"
18382            | "secrets_encrypt" | "secrets_decrypt" | "secrets_random_key" | "secrets_kdf"
18383            // ── Web framework (docs/WEB_FRAMEWORK.md) ─────────────────
18384            | "web_route" | "web_resources" | "web_root" | "web_routes_table"
18385            | "web_application_config" | "web_boot_application"
18386            | "web_render" | "web_render_partial" | "web_redirect"
18387            | "web_json" | "web_text" | "web_csv" | "web_markdown"
18388            | "web_params" | "web_request" | "web_set_header" | "web_status"
18389            | "web_before_action" | "web_after_action"
18390            | "web_session" | "web_session_set" | "web_session_get" | "web_session_clear"
18391            | "web_signed" | "web_unsigned"
18392            | "web_cookies" | "web_set_cookie"
18393            | "web_flash" | "web_flash_set" | "web_flash_get"
18394            | "web_validate" | "web_permit"
18395            | "web_password_hash" | "web_password_verify"
18396            | "web_token_for" | "web_token_consume" | "web_csrf_meta_tag"
18397            | "web_security_headers" | "web_can"
18398            | "web_h" | "web_truncate" | "web_pluralize" | "web_time_ago_in_words"
18399            | "web_image_tag" | "web_link_to" | "web_button_to"
18400            | "web_form_with" | "web_form_close"
18401            | "web_text_field" | "web_text_area" | "web_check_box"
18402            | "web_stylesheet_link_tag" | "web_javascript_link_tag"
18403            | "web_yield_content" | "web_content_for"
18404            | "web_etag" | "web_cache_get" | "web_cache_set"
18405            | "web_cache_delete" | "web_cache_clear"
18406            | "web_db_connect" | "web_db_execute" | "web_db_query"
18407            | "web_db_begin" | "web_db_commit" | "web_db_rollback"
18408            | "web_create_table" | "web_drop_table"
18409            | "web_add_column" | "web_remove_column"
18410            | "web_migrate" | "web_rollback"
18411            | "web_model_all" | "web_model_find" | "web_model_first" | "web_model_last"
18412            | "web_model_where" | "web_model_create" | "web_model_update"
18413            | "web_model_destroy" | "web_model_count" | "web_model_increment"
18414            | "web_model_paginate" | "web_model_search" | "web_model_soft_destroy"
18415            | "web_model_with"
18416            | "web_jobs_init" | "web_job_enqueue" | "web_job_dequeue"
18417            | "web_job_complete" | "web_job_fail"
18418            | "web_jobs_list" | "web_jobs_stats" | "web_job_purge"
18419            | "web_jsonapi_resource" | "web_jsonapi_collection" | "web_jsonapi_error"
18420            | "web_bearer_token" | "web_jwt_encode" | "web_jwt_decode"
18421            | "web_otp_secret" | "web_otp_generate" | "web_otp_verify"
18422            | "web_uuid" | "web_now" | "web_log" | "web_rate_limit"
18423            | "web_t" | "web_load_locale" | "web_openapi"
18424            | "web_faker_int" | "web_faker_email" | "web_faker_name"
18425            | "web_faker_sentence" | "web_faker_paragraph"
18426            // ── test runner ─────────────────────────────────────────────────
18427            // In-process equivalents of `stryke check` / `stryke test`. The
18428            // builtin form lets stryke programs (e.g. `exercism_run_all.stk`)
18429            // call them directly without `system "stryke check $f"`, so
18430            // `check_no_interop` from inside `pmaps` is fork-free and
18431            // race-free (per-thread no-interop TLS override).
18432            | "check" | "check_no_interop" | "check_ni"
18433            | "test" | "test_no_interop" | "test_ni"
18434            // ── linear algebra / graphs / dates / special math ──
18435            // ── bits / music theory / hashes / text / statistical tests ──
18436            // ── phonetic / geo projections / base58/91/z85 / astronomy / crc / color blends / compression ──
18437            // ── bioinformatics / 3d geometry / sequence alignment / file headers / hmm ──
18438            // ── game theory / ml inference / chemistry / ops research / info theory ──
18439            // ── cv kernels / information retrieval / rl / color spaces / windows / trie/fenwick/uf / network ──
18440            // ── combinatorics / audio synthesis / search / physics 2d / noise / rng variants ──
18441            // ── ratings / image morphology / computational geometry 2d / crypto / constants / case conversions / photography / unit conversions ──
18442            | "andrew_monotone_hull" | "aperture_stop_to_fnumber" | "arpad_predict" | "bilateral_filter_2d"
18443            | "black_hat_transform" | "canny_edges_full" | "case_alternating" | "case_constant"
18444            | "case_dot" | "case_pascal" | "case_path" | "case_sentence"
18445            | "case_swap" | "case_title_proper" | "case_train" | "closing_2d"
18446            | "cohen_sutherland_clip" | "constants_au_meters" | "constants_avogadro_n" | "constants_bohr_radius"
18447            | "constants_boltzmann_k" | "constants_earth_mass" | "constants_earth_radius" | "constants_electron_charge"
18448            | "constants_electron_mass" | "constants_faraday" | "constants_gas_r" | "constants_gravitational_g"
18449            | "constants_lightyear_meters" | "constants_neutron_mass" | "constants_parsec_meters" | "constants_planck_h"
18450            | "constants_planck_hbar" | "constants_planck" | "constants_proton_mass" | "constants_rydberg"
18451            | "constants_solar_mass" | "constants_solar_radius" | "constants_speed_of_light" | "constants_stefan_boltzmann"
18452            | "contour_area" | "contour_centroid" | "contour_find" | "contour_perimeter"
18453            | "convex_hull_3d" | "convex_hull_3d_simple" | "crop_factor" | "delaunay_triangulate_2d" | "depth_of_field_far"
18454            | "depth_of_field_near" | "dh_compute_shared" | "dilation_2d" | "ec_point_add"
18455            | "ec_point_double" | "ed25519_keypair_simple" | "ed25519_sign_simple" | "ed25519_verify_simple"
18456            | "erosion_2d" | "exposure_value" | "field_of_view" | "focal_length_35mm_equiv"
18457            | "glicko_rd_update" | "glicko_volatility" | "graham_scan_hull" | "hu_moments"
18458            | "hyperfocal_distance" | "liang_barsky_clip" | "minkowski_sum_2d" | "moment_image"
18459            | "morphological_gradient" | "opening_2d" | "pagerank_tournament" | "polygon_inflate"
18460            | "polygon_offset" | "polygon_self_intersects" | "polygon_shrink" | "polygon_simple_check"
18461            | "polygon_winding" | "prewitt_x_kernel" | "prewitt_y_kernel" | "ranking_average"
18462            | "ranking_kendall_tau" | "ranking_spearman_rho" | "roberts_cross_kernel" | "rsa_keypair_simple"
18463            | "rsa_modular_exp" | "scharr_x_kernel" | "scharr_y_kernel" | "schnorr_sign_simple"
18464            | "schnorr_verify_simple" | "shutter_speed_reciprocal" | "sobel_magnitude" | "sunny_16_rule"
18465            | "swiss_pairing" | "top_hat_transform" | "tournament_score" | "trueskill_simple"
18466            | "unit_energy" | "unit_pressure" | "unit_temperature" | "unit_volume_metric_to_us"
18467            | "unit_volume_us_to_metric" | "voronoi_cell_2d" | "weiler_atherton_clip" | "zernike_radial"
18468
18469            | "a_star_grid" | "all_pass_filter" | "am_synth" | "bidirectional_bfs"
18470            | "buoyancy_force" | "center_of_mass_2d" | "center_of_mass_3d" | "centered_polygonal"
18471            | "chorus_simple" | "collision_response_2d" | "comb_filter" | "compositions_count"
18472            | "critical_damping" | "cube_number" | "damping_factor" | "decagonal_number"
18473            | "derangement_count" | "dodecahedral" | "elastic_collision_1d" | "exponential_search"
18474            | "fbm_noise_2d" | "fibonacci_matrix" | "fibonacci_nth_fast" | "fir_filter"
18475            | "flanger_simple" | "floyd_cycle_detect" | "fm_synth_2op" | "freeverb_lite"
18476            | "gnomonic_number" | "greedy_best_first" | "hash_2d_int" | "heptagonal_number"
18477            | "hexagonal_number" | "hyperfactorial" | "icosahedral" | "ida_star_search"
18478            | "inelastic_collision_1d" | "interpolation_search" | "lattice_paths" | "lift_force"
18479            | "lucas_nth" | "moment_of_inertia_cylinder" | "moment_of_inertia_disc" | "moment_of_inertia_rod"
18480            | "moment_of_inertia_sphere" | "mulberry32_next" | "multinomial_coefficient" | "narayana_cow"
18481            | "nonagonal_number" | "octagonal_number" | "partitions_count" | "pcg32_next"
18482            | "pell_nth" | "perlin_2d" | "perlin_3d" | "phaser_simple"
18483            | "plate_reverb_simple" | "poisson_brackets" | "primorial" | "projectile_position"
18484            | "projectile_velocity" | "ridge_noise_2d" | "ring_modulate" | "schroeder_reverb"
18485            | "simplex_2d" | "splitmix64_next" | "spring_oscillator_pos" | "square_pyramidal"
18486            | "super_factorial" | "ternary_search" | "tetrahedral" | "tetranacci"
18487            | "torque_arm" | "turbulence_noise_2d" | "value_noise_2d" | "wavetable_synth"
18488            | "worley_2d" | "xorshift32_next"
18489
18490            | "adaptive_threshold" | "bayes_factor" | "bayesian_beta_update" | "bayesian_normal_update"
18491            | "bm25_score" | "boltzmann_choose" | "braycurtis_dist" | "canberra_dist"
18492            | "canny_edges_simple" | "chebyshev_norm" | "cidr_to_range" | "ciede2000_color_distance"
18493            | "ciede76_color_distance" | "ciede94_color_distance" | "conv1d_apply" | "conv2d_apply"
18494            | "correlate2d" | "cosine_sim_sparse" | "credible_interval_beta" | "credible_interval_normal"
18495            | "dice_coeff" | "earth_mover_1d" | "epsilon_greedy_choose" | "fenwick_new"
18496            | "fenwick_query_prefix" | "fenwick_query_range" | "fenwick_update" | "gaussian_kernel"
18497            | "gradient_magnitude_2d" | "harris_response" | "integral_image" | "ip_subnet_split"
18498            | "ipv6_global_unicast" | "jaccard_sim" | "laplacian_kernel" | "lch_to_rgb"
18499            | "mahalanobis_sq" | "manhattan_norm" | "maximum_a_posteriori" | "minkowski_norm"
18500            | "non_max_suppression" | "oklch_to_rgb" | "otsu_threshold" | "overlap_coeff"
18501            | "posterior_predictive_beta" | "posterior_predictive_normal" | "prior_jeffreys_uniform" | "qlearning_step"
18502            | "range_to_cidr" | "rgb_to_lch" | "rgb_to_oklch" | "rl_discount_returns"
18503            | "rl_n_step_return" | "rl_td_error" | "sarsa_step" | "sliding_dot_product"
18504            | "sobel_x_kernel" | "sobel_y_kernel" | "softmax_choose" | "tanimoto_coeff"
18505            | "tfidf_compute" | "thompson_beta_choose" | "trie_count" | "trie_insert"
18506            | "trie_keys" | "trie_lookup" | "trie_new" | "trie_prefix_search"
18507            | "trie_remove" | "tversky_index" | "ucb1_choose" | "union_find_components"
18508            | "union_find_find" | "union_find_new" | "union_find_union" | "window_bartlett"
18509            | "window_blackman_harris" | "window_flat_top" | "window_gaussian" | "window_welch"
18510
18511            | "alphabeta_value" | "chem_arrhenius_k" | "chem_avogadro" | "chem_balance_check"
18512            | "chem_boiling_point_elevation" | "chem_buffer_capacity" | "chem_celsius_to_fahrenheit" | "chem_celsius_to_kelvin"
18513            | "chem_concentration_to_molarity" | "chem_dilution" | "chem_fahrenheit_to_celsius" | "chem_fahrenheit_to_kelvin"
18514            | "chem_formula_parse" | "chem_freezing_point_depression" | "chem_h_from_ph" | "chem_henderson_hasselbalch"
18515            | "chem_ideal_gas_volume" | "chem_isoelectric_estimate" | "chem_kelvin_to_celsius" | "chem_kelvin_to_fahrenheit"
18516            | "chem_kelvin_to_rankine" | "chem_molality" | "chem_molar_mass" | "chem_molarity_to_normality"
18517            | "chem_partial_pressure" | "chem_ph_from_h" | "chem_pka_lookup" | "chem_rankine_to_kelvin"
18518            | "conditional_entropy" | "edmonds_karp_max_flow" | "expectiminimax_value" | "ford_fulkerson_max_flow"
18519            | "job_schedule_ljf" | "job_schedule_spt" | "joint_entropy" | "js_divergence_distributions"
18520            | "kl_divergence_distributions" | "knapsack_fractional" | "knapsack_unbounded" | "lp_simplex_max"
18521            | "lp_simplex_min" | "matching_bipartite_greedy" | "matching_bipartite_hungarian" | "minimax_value"
18522            | "mixed_strategy_2x2" | "ml_attention_score" | "ml_batch_norm" | "ml_dot_product_attention"
18523            | "ml_dropout_mask" | "ml_elu_layer" | "ml_gelu_layer" | "ml_hinge_loss"
18524            | "ml_huber_loss" | "ml_kl_div_loss" | "ml_label_smooth" | "ml_layer_norm"
18525            | "ml_leaky_relu_layer" | "ml_mae_loss" | "ml_mish_layer" | "ml_mse_loss"
18526            | "ml_one_hot_encode" | "ml_position_encoding" | "ml_relu_layer" | "ml_self_attention"
18527            | "ml_sigmoid_layer" | "ml_softmax_layer" | "ml_softplus_layer" | "ml_swish_layer"
18528            | "ml_tanh_layer" | "ngram_perplexity" | "ngram_prob" | "ngram_top_k_next"
18529            | "ngram_train" | "payoff_matrix" | "relative_entropy" | "tsp_2opt"
18530            | "zero_sum_value"
18531
18532            | "aabb_contains_point" | "aabb_intersects" | "aabb_new" | "aabb_union"
18533            | "aabb_volume" | "backward_algorithm" | "blast_kmer_index" | "bmp_header_read"
18534            | "bootstrap_resample" | "codon_optimize" | "codon_to_amino_acid" | "codon_usage_table"
18535            | "dna_at_content" | "dna_complement" | "dna_gc_content" | "dna_kmer_count"
18536            | "dna_kmer_index" | "dna_melting_temp" | "dna_reverse_complement" | "dna_transcribe"
18537            | "dna_translate" | "elf_header_read" | "forward_algorithm" | "gif_header_read"
18538            | "ico_header_read"
18539            | "jpeg_markers" | "levenshtein_edit_path" | "mach_o_header_read" | "markov_stationary"
18540            | "markov_transition_matrix" | "mat4_determinant" | "mat4_identity" | "mat4_inverse"
18541            | "mat4_look_at" | "mat4_multiply" | "mat4_orthographic" | "mat4_perspective"
18542            | "mat4_rotate_axis" | "mat4_rotate_x" | "mat4_rotate_y" | "mat4_rotate_z"
18543            | "mat4_scale" | "mat4_translate" | "mat4_transpose" | "nw_score"
18544            | "permutation_test" | "plane_distance_to_point" | "plane_normalize" | "png_header_read"
18545            | "profile_hmm_score" | "protein_charge_at_ph" | "protein_hydrophobicity" | "protein_molecular_weight"
18546            | "protein_pI" | "quat_conjugate" | "quat_dot" | "quat_from_euler"
18547            | "quat_identity" | "quat_inverse" | "quat_multiply" | "quat_normalize"
18548            | "quat_to_euler" | "quat_to_mat4" | "ray_aabb_intersect" | "ray_plane_intersect_2"
18549            | "ray_plane_intersect" | "rna_gc_content" | "rna_hamming" | "rna_reverse_complement"
18550            | "rna_to_dna" | "sequence_identity_pct" | "sequence_similarity_pct" | "shuffle_resample"
18551            | "sphere_aabb_intersect" | "sphere_sphere_intersect" | "sw_score" | "tar_header_read"
18552            | "triangle_area_3d" | "triangle_normal" | "vec3_add" | "vec3_cross"
18553            | "vec3_distance" | "vec3_dot" | "vec3_length" | "vec3_lerp"
18554            | "vec3_normalize" | "vec3_project" | "vec3_reflect" | "vec3_refract"
18555            | "vec3_scale" | "vec3_sub" | "vec4_add" | "vec4_dot"
18556            | "vec4_length" | "vec4_scale" | "vec4_sub" | "viterbi_decode"
18557            | "wav_header_read" | "zip_central_directory" | "zip_local_file_header"
18558
18559            | "adler32_combine" | "ase_palette_extract" | "base58check_decode" | "base58check_encode"
18560            | "base91_decode" | "basE91_decode" | "base91_encode" | "basE91_encode"
18561            | "bwt_invert" | "bwt_transform" | "caverphone" | "caverphone2"
18562            | "crc10_atm" | "crc12_dect" | "crc24" | "crc32_bzip2"
18563            | "crc32_jamcrc" | "crc32_mpeg2" | "crc32_xfer" | "crc6_itu"
18564            | "crc64_ecma" | "crc64_xz" | "delta_decode" | "delta_encode"
18565            | "destination_lat_lon" | "double_metaphone_primary" | "double_metaphone_secondary" | "fletcher16"
18566            | "fletcher32" | "fletcher64" | "full_moon_julian" | "fuzzy_substring_match"
18567            | "gamma_correct" | "gamma_uncorrect" | "geomag_declination" | "huffman_decode"
18568            | "huffman_encode" | "julian_to_unix" | "lambert_project" | "lat_lon_to_utm"
18569            | "match_rating_compare" | "mercator_project_x" | "mercator_project_y" | "mercator_unproject_lat"
18570            | "mercator_unproject_lon" | "modified_julian_date" | "moon_age_days" | "moon_distance_km"
18571            | "new_moon_julian" | "nysiis" | "phonex" | "rgb_blend_color_burn"
18572            | "rgb_blend_color_dodge" | "rgb_blend_darken" | "rgb_blend_lighten" | "rgb_blend_multiply"
18573            | "rgb_blend_normal" | "rgb_blend_overlay" | "rgb_blend_screen" | "rle_compress"
18574            | "rle_decompress" | "season_of_year" | "sidereal_time_greenwich" | "sidereal_time_local"
18575            | "solar_noon_unix" | "soundex_v1" | "soundex_v2" | "unix_to_julian"
18576            | "utm_to_lat_lon" | "utm_zone" | "varint_decode" | "varint_encode"
18577            | "vincenty_bearing" | "z85_decode" | "z85_encode" | "zigzag_decode"
18578            | "zigzag_encode"
18579
18580            | "anova_one_way" | "binomial_test" | "bit_clz"
18581            | "bit_count_ones" | "bit_count_zeros" | "bit_ctz" | "bit_extract"
18582            | "bit_first_clear" | "bit_first_set" | "bit_insert" | "bit_last_clear"
18583            | "bit_last_set" | "bit_log2_int" | "bit_parity" | "bit_reverse_u16"
18584            | "bit_reverse_u32" | "bit_reverse_u64" | "bit_reverse_u8" | "bit_rotate_left"
18585            | "bit_rotate_right" | "bit_swap_bytes" | "chi_square_goodness_fit" | "chi_square_independence"
18586            | "chord_augmented" | "chord_diminished" | "chord_diminished7" | "chord_dominant7"
18587            | "chord_major" | "chord_major7" | "chord_minor" | "chord_minor7"
18588            | "crc16_xmodem" | "crc16" | "crc32_zlib" | "crc32c"
18589            | "crc8" | "detab" | "entab" | "fisher_exact_2x2"
18590            | "gray_code_decode" | "gray_code_encode" | "hmac_md5_hex" | "hmac_sha1_hex"
18591            | "hmac_sha256_hex" | "hmac_sha384_hex" | "hmac_sha512_hex" | "indent_block"
18592            | "interval_name" | "jenkins_hash" | "justify_center" | "justify_left"
18593            | "justify_right" | "kruskal_wallis" | "ks_test_one_sample" | "ks_test_two_sample"
18594            | "loose_hash" | "mann_whitney_u" | "midi_to_note_name" | "popcount_u32"
18595            | "popcount_u64" | "proportion_test" | "rank_data" | "scale_blues"
18596            | "scale_chromatic" | "scale_dorian" | "scale_harmonic_minor" | "scale_locrian"
18597            | "scale_lydian" | "scale_major" | "scale_melodic_minor" | "scale_minor"
18598            | "scale_mixolydian" | "scale_pentatonic" | "scale_phrygian" | "seconds_per_beat"
18599            | "strip_indent" | "t_test_paired" | "tempo_to_ms_per_beat" | "truncate_middle"
18600            | "unicode_codepoints" | "wilcoxon_signed_rank" | "word_wrap"
18601
18602            | "beta_function" | "beta_incomplete" | "date_add_days" | "date_add_months"
18603            | "date_add_years" | "date_business_days_between" | "date_day" | "date_dayofweek"
18604            | "date_dayofyear" | "date_days_in_month" | "date_diff_days" | "date_diff_hours"
18605            | "date_diff_minutes" | "date_diff_seconds" | "date_easter" | "date_first_of_month"
18606            | "date_hour" | "date_is_leap" | "date_is_weekend" | "date_iso_format"
18607            | "date_iso_week" | "date_last_of_month" | "date_minute" | "date_month"
18608            | "date_quarter" | "date_second" | "date_str_to_unix" | "date_unix_to_str"
18609            | "date_weekofyear" | "date_year" | "ei" | "expint"
18610            | "gamma_regularized_p" | "gamma_regularized_q" | "graph_articulation_points" | "graph_bellman_ford"
18611            | "graph_betweenness" | "graph_bfs" | "graph_bridges" | "graph_closeness"
18612            | "graph_clustering_coefficient" | "graph_color_greedy" | "graph_connected_components" | "graph_cycle_detect"
18613            | "graph_degree" | "graph_dfs" | "graph_dijkstra" | "graph_eccentricity"
18614            | "graph_eigenvector_centrality" | "graph_floyd_warshall" | "graph_from_edges" | "graph_has_path"
18615            | "graph_in_degree" | "graph_is_bipartite" | "graph_is_connected" | "graph_kosaraju"
18616            | "graph_kruskal_mst" | "graph_out_degree" | "graph_pagerank" | "graph_prim_mst"
18617            | "graph_shortest_path" | "graph_strongly_connected_components" | "graph_tarjan" | "graph_to_adj_list"
18618            | "graph_to_adj_matrix" | "graph_topological_sort" | "hypergeom_1f1" | "hypergeom_2f1"
18619            | "li" | "matrix_adjugate" | "matrix_cholesky_decompose" | "matrix_cofactor"
18620            | "matrix_cols" | "matrix_concat_h" | "matrix_concat_v" | "matrix_determinant"
18621            | "matrix_from_cols" | "matrix_get" | "matrix_kronecker" | "matrix_lu_decompose"
18622            | "matrix_minor" | "matrix_new" | "matrix_norm_frobenius" | "matrix_norm_l1"
18623            | "matrix_norm_linf" | "matrix_outer_product" | "matrix_qr_decompose" | "matrix_reshape"
18624            | "matrix_rows" | "matrix_set" | "matrix_submatrix" | "matrix_swap_cols"
18625            | "matrix_swap_rows" | "matrix_to_string" | "matrix_vec_mul" | "si"
18626            | "sun_rise_unix" | "sun_set_unix" | "zeta_riemann" | "zodiac_sign"
18627            // ── quant / technical indicators / time-series / finance / optimization ──
18628            | "add_seasonality" | "trapezoidal_integrate" | "simpson_integrate"
18629            | "ode_euler" | "fit_curve_least_squares"
18630            | "adf_test" | "adx" | "atr" | "bollinger_lower" | "bollinger_middle"
18631            | "bollinger_upper" | "break_even_price" | "break_even_qty" | "candlestick_pattern_doji"
18632            | "candlestick_pattern_engulfing" | "candlestick_pattern_evening_star" | "candlestick_pattern_hammer" | "candlestick_pattern_morning_star"
18633            | "candlestick_pattern_three_black_crows" | "candlestick_pattern_three_white_soldiers" | "cci"
18634            | "dema" | "diff_pct" | "diff_series"
18635            | "discount_pct" | "donchian_lower" | "donchian_upper" | "double_exponential_smoothing"
18636            | "duration_modified" | "ema" | "expanding_mean" | "expanding_sum"
18637            | "fibonacci_extension" | "fibonacci_retracement" | "finite_difference_central"
18638            | "finite_difference_forward" | "hma"
18639            | "hurst_exponent" | "interp_lagrange"
18640            | "interp_linear" | "kama"
18641            | "keltner_lower" | "keltner_upper" | "lag_series"
18642            | "loan_interest_total" | "loan_payment" | "loan_remaining" | "log_returns"
18643            | "macd_histogram" | "macd_signal" | "macd" | "markup_pct" | "net_present_value" | "obv" | "parabolic_sar" | "pivot_points" | "profit_margin_pct" | "remove_seasonality" | "resistance_level"
18644            | "roc" | "rolling_kurtosis" | "rolling_max"
18645            | "rolling_mean" | "rolling_median" | "rolling_min" | "rolling_skew"
18646            | "rolling_std" | "rolling_sum" | "rolling_var"
18647            | "rsi" | "shift_series" | "simple_returns" | "sma" | "stoch_rsi" | "support_level"
18648            | "tema" | "trend_line" | "treynor"
18649            | "trix" | "true_range" | "twap" | "ulcer_index"
18650            | "volatility_annualized" | "volatility_realized" | "vwap" | "williams_r"
18651            | "wma"
18652            => Some(name),
18653            _ => None,
18654        }
18655    }
18656
18657    /// Reserved hash names that cannot be shadowed by user declarations.
18658    /// These are stryke's reflection hashes populated from builtins metadata.
18659    fn is_reserved_hash_name(name: &str) -> bool {
18660        matches!(
18661            name,
18662            "b" | "pc"
18663                | "e"
18664                | "a"
18665                | "d"
18666                | "c"
18667                | "p"
18668                | "k"
18669                | "all"
18670                | "stryke::builtins"
18671                | "stryke::perl_compats"
18672                | "stryke::extensions"
18673                | "stryke::aliases"
18674                | "stryke::descriptions"
18675                | "stryke::categories"
18676                | "stryke::primaries"
18677                | "stryke::keywords"
18678                | "stryke::all"
18679        )
18680    }
18681
18682    /// Check if a UDF name shadows a stryke builtin and error if so.
18683    /// Called only in non-compat mode — compat mode allows shadowing for Perl 5 parity.
18684    /// Reserved words that cannot be used as function names because they are
18685    /// lexer-level operators or language keywords that would be mis-tokenized.
18686    const RESERVED_FUNCTION_NAMES: &'static [&'static str] = &[
18687        "y",
18688        "tr",
18689        "s",
18690        "m",
18691        "q",
18692        "qq",
18693        "qw",
18694        "qx",
18695        "qr",
18696        "if",
18697        "unless",
18698        "while",
18699        "until",
18700        "for",
18701        "foreach",
18702        "loop",
18703        "ploop",
18704        "pwhile",
18705        "null",
18706        "given",
18707        "when",
18708        "else",
18709        "elsif",
18710        "do",
18711        "eval",
18712        "return",
18713        "last",
18714        "next",
18715        "redo",
18716        "goto",
18717        "my",
18718        "var",
18719        "val",
18720        "our",
18721        "local",
18722        "state",
18723        "sub",
18724        "fn",
18725        "class",
18726        "struct",
18727        "enum",
18728        "trait",
18729        "use",
18730        "import",
18731        "no",
18732        "require",
18733        "package",
18734        "BEGIN",
18735        "END",
18736        "CHECK",
18737        "INIT",
18738        "UNITCHECK",
18739        "and",
18740        "or",
18741        "not",
18742        "x",
18743        "eq",
18744        "ne",
18745        "lt",
18746        "gt",
18747        "le",
18748        "ge",
18749        "cmp",
18750    ];
18751
18752    fn check_udf_shadows_builtin(&self, name: &str, line: usize) -> StrykeResult<()> {
18753        // Already namespaced (e.g. `Foo::y`) — package context makes the
18754        // name unambiguous, so it can never shadow a builtin.
18755        if name.contains("::") {
18756            return Ok(());
18757        }
18758        // Reserved syntactic words (`if`, `while`, `package`, …) break
18759        // parsing as function names regardless of package.
18760        if Self::RESERVED_FUNCTION_NAMES.contains(&name) {
18761            return Err(self.syntax_err(
18762                format!("`{name}` is a reserved word and cannot be used as a function name"),
18763                line,
18764            ));
18765        }
18766        // Bare `fn name(...)` inside a non-main `package Foo` registers
18767        // under `Foo::name`. The user sub is callable only via the
18768        // fully-qualified `Foo::name(...)` spelling — bare calls always
18769        // dispatch to the global builtin. Allow the declaration.
18770        if self.current_package != "main" {
18771            return Ok(());
18772        }
18773        // In `package main` (the default), there's no qualified spelling
18774        // to "escape" a builtin name. Reject `fn sum {}` here so callers
18775        // never wonder why bare `sum(1,2,3)` ignored their definition.
18776        if Self::is_known_bareword(name)
18777            || Self::is_try_builtin_name(name)
18778            || crate::list_builtins::is_list_builtin_name(name)
18779        {
18780            return Err(self.syntax_err(
18781                format!(
18782"`{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)"
18783                ),
18784                line,
18785            ));
18786        }
18787        Ok(())
18788    }
18789
18790    /// Check if a hash name shadows a reserved stryke hash and error if so.
18791    /// Called only in non-compat mode.
18792    fn check_hash_shadows_reserved(&self, name: &str, line: usize) -> StrykeResult<()> {
18793        if Self::is_reserved_hash_name(name) {
18794            return Err(self.syntax_err(
18795                format!(
18796"`%{name}` is a stryke reserved hash and cannot be redefined (this is not Perl 5; pass --compat for Perl 5 mode)"
18797                ),
18798                line,
18799            ));
18800        }
18801        Ok(())
18802    }
18803
18804    /// Validate assignment to %hash in non-compat mode.
18805    /// Rejects: scalar, string, arrayref, hashref, coderef, undef, odd-length list.
18806    fn validate_hash_assignment(&self, value: &Expr, line: usize) -> StrykeResult<()> {
18807        match &value.kind {
18808            ExprKind::Integer(_) | ExprKind::Float(_) => {
18809                return Err(self.syntax_err(
18810                    "cannot assign scalar to hash — use %h = (key => value) or %h = %{$hashref}",
18811                    line,
18812                ));
18813            }
18814            ExprKind::String(_) | ExprKind::InterpolatedString(_) | ExprKind::Bareword(_) => {
18815                return Err(self.syntax_err(
18816                    "cannot assign string to hash — use %h = (key => value) or %h = %{$hashref}",
18817                    line,
18818                ));
18819            }
18820            ExprKind::ArrayRef(_) => {
18821                return Err(self.syntax_err(
18822                    "cannot assign arrayref to hash — use %h = @{$arrayref} for even-length list",
18823                    line,
18824                ));
18825            }
18826            ExprKind::ScalarRef(inner) => {
18827                if matches!(inner.kind, ExprKind::ArrayVar(_)) {
18828                    return Err(self.syntax_err(
18829                        "cannot assign \\@array to hash — use %h = @array for even-length list",
18830                        line,
18831                    ));
18832                }
18833                if matches!(inner.kind, ExprKind::HashVar(_)) {
18834                    return Err(self.syntax_err(
18835                        "cannot assign \\%hash to hash — use %h = %other directly",
18836                        line,
18837                    ));
18838                }
18839            }
18840            ExprKind::HashRef(_) => {
18841                return Err(self.syntax_err(
18842                    "cannot assign hashref to hash — use %h = %{$hashref} to dereference",
18843                    line,
18844                ));
18845            }
18846            ExprKind::CodeRef { .. } => {
18847                return Err(self.syntax_err("cannot assign coderef to hash", line));
18848            }
18849            ExprKind::Undef => {
18850                return Err(
18851                    self.syntax_err("cannot assign undef to hash — use %h = () to empty", line)
18852                );
18853            }
18854            ExprKind::List(items)
18855                if items.len() % 2 != 0
18856                    && !items.iter().any(|e| {
18857                        matches!(
18858                            e.kind,
18859                            ExprKind::ArrayVar(_)
18860                                | ExprKind::HashVar(_)
18861                                | ExprKind::FuncCall { .. }
18862                                | ExprKind::Deref { .. }
18863                                | ExprKind::ScalarVar(_)
18864                        )
18865                    }) =>
18866            {
18867                return Err(self.syntax_err(
18868                        format!(
18869                            "odd-length list ({} elements) in hash assignment — missing value for last key",
18870                            items.len()
18871                        ),
18872                        line,
18873                    ));
18874            }
18875            _ => {}
18876        }
18877        Ok(())
18878    }
18879
18880    /// Validate assignment to @array in non-compat mode.
18881    /// Rejects: undef (likely a mistake — use `@a = ()` to empty).
18882    /// Note: bare scalars like `@a = 2` are allowed since Perl coerces them to single-element lists.
18883    /// Note: `@a = {hashref}` is allowed as a common pattern for single-element arrays.
18884    fn validate_array_assignment(&self, value: &Expr, line: usize) -> StrykeResult<()> {
18885        if let ExprKind::Undef = &value.kind {
18886            return Err(
18887                self.syntax_err("cannot assign undef to array — use @a = () to empty", line)
18888            );
18889        }
18890        Ok(())
18891    }
18892
18893    /// Validate assignment to $scalar in non-compat mode.
18894    /// Rejects: list literals (Perl 5 silently returns last element — footgun).
18895    fn validate_scalar_assignment(&self, value: &Expr, line: usize) -> StrykeResult<()> {
18896        if let ExprKind::List(items) = &value.kind {
18897            if items.len() > 1 {
18898                return Err(self.syntax_err(
18899                    format!(
18900                        "cannot assign {}-element list to scalar — Perl 5 silently takes last element; use ($x) = (list) or $x = $list[-1]",
18901                        items.len()
18902                    ),
18903                    line,
18904                ));
18905            }
18906        }
18907        Ok(())
18908    }
18909
18910    /// Validate an assignment based on target type (in non-compat mode only).
18911    fn validate_assignment(&self, target: &Expr, value: &Expr, line: usize) -> StrykeResult<()> {
18912        if crate::compat_mode() {
18913            return Ok(());
18914        }
18915        match &target.kind {
18916            ExprKind::HashVar(_) => self.validate_hash_assignment(value, line),
18917            ExprKind::ArrayVar(_) => self.validate_array_assignment(value, line),
18918            ExprKind::ScalarVar(_) => self.validate_scalar_assignment(value, line),
18919            _ => Ok(()),
18920        }
18921    }
18922
18923    /// Parse a block OR a blockless comparison expression for sort/psort/heap.
18924    /// Blockless: `$a <=> $b` or `$a cmp $b` or any expression → wrapped as a Block.
18925    /// Also accepts a bare function name: `psort my_cmp, @list`.
18926    fn parse_block_or_bareword_cmp_block(&mut self) -> StrykeResult<Block> {
18927        if matches!(self.peek(), Token::LBrace) {
18928            return self.parse_block();
18929        }
18930        let line = self.peek_line();
18931        // Bare sub name: `psort my_cmp, @list`
18932        if let Token::Ident(ref name) = self.peek().clone() {
18933            if matches!(
18934                self.peek_at(1),
18935                Token::Comma | Token::Semicolon | Token::RBrace | Token::Eof | Token::PipeForward
18936            ) {
18937                let name = name.clone();
18938                self.advance();
18939                let body = Expr {
18940                    kind: ExprKind::FuncCall {
18941                        name,
18942                        args: vec![
18943                            Expr {
18944                                kind: ExprKind::ScalarVar("a".to_string()),
18945                                line,
18946                            },
18947                            Expr {
18948                                kind: ExprKind::ScalarVar("b".to_string()),
18949                                line,
18950                            },
18951                        ],
18952                    },
18953                    line,
18954                };
18955                return Ok(vec![Statement::new(StmtKind::Expression(body), line)]);
18956            }
18957        }
18958        // Blockless expression: `$a <=> $b`, `$b cmp $a`, etc.
18959        let expr = self.parse_assign_expr_stop_at_pipe()?;
18960        Ok(vec![Statement::new(StmtKind::Expression(expr), line)])
18961    }
18962
18963    /// After `fan` / `fan_cap` `{ BLOCK }`, optional `, progress => EXPR` or `progress => EXPR` (no comma).
18964    fn parse_fan_optional_progress(
18965        &mut self,
18966        which: &'static str,
18967    ) -> StrykeResult<Option<Box<Expr>>> {
18968        let line = self.peek_line();
18969        if self.eat(&Token::Comma) {
18970            match self.peek() {
18971                Token::Ident(ref kw)
18972                    if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) =>
18973                {
18974                    self.advance();
18975                    self.expect(&Token::FatArrow)?;
18976                    return Ok(Some(Box::new(self.parse_assign_expr()?)));
18977                }
18978                _ => {
18979                    return Err(self.syntax_err(
18980                        format!("{which}: expected `progress => EXPR` after comma"),
18981                        line,
18982                    ));
18983                }
18984            }
18985        }
18986        if let Token::Ident(ref kw) = self.peek().clone() {
18987            if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
18988                self.advance();
18989                self.expect(&Token::FatArrow)?;
18990                return Ok(Some(Box::new(self.parse_assign_expr()?)));
18991            }
18992        }
18993        Ok(None)
18994    }
18995
18996    /// Comma-separated assign expressions with optional trailing `, progress => EXPR`
18997    /// (for `pmap_chunked`, `psort`, etc.).
18998    ///
18999    /// Paren-less — individual parts parse through
19000    /// [`Self::parse_assign_expr_stop_at_pipe`] so a trailing `|>` is left for
19001    /// the enclosing pipe-forward loop (left-associative chaining).
19002    fn parse_assign_expr_list_optional_progress(&mut self) -> StrykeResult<(Expr, Option<Expr>)> {
19003        // On the RHS of `|>`, list-taking builtins may be written bare with no
19004        // operand — `@a |> uniq`, `@a |> flatten`, `foo(bar, @a |> psort)`, etc.
19005        // When the next token is a list-terminator, yield an empty placeholder
19006        // list; [`Self::pipe_forward_apply`] substitutes the piped LHS at
19007        // desugar time, so the placeholder is never evaluated.
19008        if self.in_pipe_rhs()
19009            && matches!(
19010                self.peek(),
19011                Token::Semicolon
19012                    | Token::RBrace
19013                    | Token::RParen
19014                    | Token::Eof
19015                    | Token::PipeForward
19016                    | Token::Comma
19017            )
19018        {
19019            return Ok((self.pipe_placeholder_list(self.peek_line()), None));
19020        }
19021        let mut parts = vec![self.parse_assign_expr_stop_at_pipe()?];
19022        loop {
19023            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
19024                break;
19025            }
19026            if matches!(
19027                self.peek(),
19028                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
19029            ) {
19030                break;
19031            }
19032            if self.peek_is_postfix_stmt_modifier_keyword() {
19033                break;
19034            }
19035            if let Token::Ident(ref kw) = self.peek().clone() {
19036                if kw == "progress" && matches!(self.peek_at(1), Token::FatArrow) {
19037                    self.advance();
19038                    self.expect(&Token::FatArrow)?;
19039                    let prog = self.parse_assign_expr_stop_at_pipe()?;
19040                    return Ok((merge_expr_list(parts), Some(prog)));
19041                }
19042            }
19043            parts.push(self.parse_assign_expr_stop_at_pipe()?);
19044        }
19045        Ok((merge_expr_list(parts), None))
19046    }
19047
19048    fn parse_one_arg(&mut self) -> StrykeResult<Expr> {
19049        if matches!(self.peek(), Token::LParen) {
19050            self.advance();
19051            let expr = self.parse_expression()?;
19052            self.expect(&Token::RParen)?;
19053            Ok(expr)
19054        } else {
19055            self.parse_assign_expr_stop_at_pipe()
19056        }
19057    }
19058
19059    /// Bare argument for a Perl-5 named unary operator (`defined`, `length`,
19060    /// `abs`, `scalar`, `ref`, `keys`, `values`, etc.). Named unary precedence
19061    /// sits between shift (`<<`/`>>`) and comparison (`<`/`>`), so we parse
19062    /// only down to shift level. The surrounding `&&` / `||` / `==` / `<` /
19063    /// equality / logical / ternary stay outside the unary's argument.
19064    /// Without this `defined $x && Y` mis-parsed as `defined($x && Y)` and
19065    /// silently returned true whenever `$x` was defined — see the skip-list
19066    /// debugging write-up. Same scope rule for `length` etc.
19067    fn parse_named_unary_arg(&mut self) -> StrykeResult<Expr> {
19068        if matches!(self.peek(), Token::LParen) {
19069            self.advance();
19070            let expr = self.parse_expression()?;
19071            self.expect(&Token::RParen)?;
19072            Ok(expr)
19073        } else {
19074            self.parse_shift()
19075        }
19076    }
19077
19078    fn parse_one_arg_or_default(&mut self) -> StrykeResult<Expr> {
19079        // Treat a line boundary as a hard arg terminator: if the next
19080        // token is on a *later* line than the named-unary keyword we
19081        // just consumed, default the operand to `$_` and stop. Without
19082        // this, `my $x = uc` followed by `my $y = 5` on the next line
19083        // mis-parses by silently swallowing `my $y = 5` as the implicit
19084        // argument to `uc`. Stryke (like Perl/shell) terminates
19085        // statements at newline; continuation requires explicit `\`.
19086        // The check skips when the *next* token is itself a binary /
19087        // postfix operator that legitimately continues the expression
19088        // (handled by the existing operator stop-list below).
19089        let prev = self.prev_line();
19090        if self.peek_line() > prev {
19091            return Ok(Expr {
19092                kind: ExprKind::ScalarVar("_".into()),
19093                line: prev,
19094            });
19095        }
19096        // Default to `$_` when the next token cannot start an argument expression
19097        // because it has lower precedence than a named unary operator. Perl 5
19098        // named unary precedence sits above ternary / comparison / logical / bitwise
19099        // / assignment / list ops; everything below should terminate the implicit
19100        // argument and let the surrounding expression continue.
19101        // See `perldoc perlop` ("Named Unary Operators").
19102        if matches!(
19103            self.peek(),
19104            // Statement / list / call boundaries
19105            Token::Semicolon
19106                | Token::RBrace
19107                | Token::RParen
19108                | Token::RBracket
19109                | Token::Eof
19110                | Token::Comma
19111                | Token::FatArrow
19112                | Token::PipeForward
19113            // Ternary `? :`
19114                | Token::Question
19115                | Token::Colon
19116            // Comparison / equality (numeric + string)
19117                | Token::NumEq | Token::NumNe | Token::NumLt | Token::NumGt
19118                | Token::NumLe | Token::NumGe | Token::Spaceship
19119                | Token::StrEq | Token::StrNe | Token::StrLt | Token::StrGt
19120                | Token::StrLe | Token::StrGe | Token::StrCmp
19121            // Logical (symbolic and word forms) + defined-or
19122                | Token::LogAnd | Token::LogOr | Token::LogNot
19123                | Token::LogAndWord | Token::LogOrWord | Token::LogNotWord
19124                | Token::DefinedOr
19125            // Range (lower precedence than named unary)
19126                | Token::Range | Token::RangeExclusive
19127            // Assignment (any compound form)
19128                | Token::Assign | Token::PlusAssign | Token::MinusAssign
19129                | Token::MulAssign | Token::DivAssign | Token::ModAssign
19130                | Token::PowAssign | Token::DotAssign | Token::AndAssign
19131                | Token::OrAssign | Token::XorAssign | Token::DefinedOrAssign
19132                | Token::ShiftLeftAssign | Token::ShiftRightAssign
19133                | Token::BitAndAssign | Token::BitOrAssign
19134        ) {
19135            return Ok(Expr {
19136                kind: ExprKind::ScalarVar("_".into()),
19137                line: self.peek_line(),
19138            });
19139        }
19140        // `f()` — empty parens default to `$_`, matching Perl 5 semantics.
19141        // `perldoc -f length`: "If EXPR is omitted, returns the length of $_."
19142        // Perl accepts both `length` and `length()` as `length($_)`.
19143        if matches!(self.peek(), Token::LParen) && matches!(self.peek_at(1), Token::RParen) {
19144            let line = self.peek_line();
19145            self.advance(); // (
19146            self.advance(); // )
19147            return Ok(Expr {
19148                kind: ExprKind::ScalarVar("_".into()),
19149                line,
19150            });
19151        }
19152        // Named-unary precedence: parenless arg only goes down to shift level,
19153        // so surrounding `eq` / `==` / `?:` / `&&` / `||` stay outside. Without
19154        // this, `ref $x eq "FOO"` mis-parses as `ref ($x eq "FOO")`.
19155        // (PARITY-016 — also fixes `length $s == 3 ? "Y" : "N"` etc.)
19156        self.parse_named_unary_arg()
19157    }
19158
19159    /// Array operand for `shift` / `pop`: default `@_`, or `shift(@a)` / `shift()` (empty parens = `@_`).
19160    fn parse_one_arg_or_argv(&mut self) -> StrykeResult<Expr> {
19161        let line = self.prev_line(); // line where shift/pop keyword was
19162        if matches!(self.peek(), Token::LParen) {
19163            self.advance();
19164            if matches!(self.peek(), Token::RParen) {
19165                self.advance();
19166                return Ok(Expr {
19167                    kind: ExprKind::ArrayVar("_".into()),
19168                    line: self.peek_line(),
19169                });
19170            }
19171            let expr = self.parse_expression()?;
19172            self.expect(&Token::RParen)?;
19173            return Ok(expr);
19174        }
19175        // Implicit semicolon: if next token is on a different line, don't consume it
19176        if matches!(
19177            self.peek(),
19178            Token::Semicolon
19179                | Token::RBrace
19180                | Token::RParen
19181                | Token::Eof
19182                | Token::Comma
19183                | Token::PipeForward
19184        ) || self.peek_line() > line
19185        {
19186            Ok(Expr {
19187                kind: ExprKind::ArrayVar("_".into()),
19188                line,
19189            })
19190        } else {
19191            self.parse_assign_expr()
19192        }
19193    }
19194
19195    fn parse_builtin_args(&mut self) -> StrykeResult<Vec<Expr>> {
19196        if matches!(self.peek(), Token::LParen) {
19197            self.advance();
19198            let args = self.parse_arg_list()?;
19199            self.expect(&Token::RParen)?;
19200            Ok(args)
19201        } else if self.suppress_parenless_call > 0 && matches!(self.peek(), Token::Ident(_)) {
19202            // In thread context, don't consume barewords as arguments
19203            // so `t filesf sorted ep` parses `sorted` as a stage, not an arg to filesf
19204            Ok(vec![])
19205        } else {
19206            self.parse_list_until_terminator()
19207        }
19208    }
19209
19210    /// Check if the next token is `=>` (fat arrow). If so, the preceding bareword
19211    /// should be treated as an auto-quoted string (hash key), not a function call.
19212    /// Returns `Some(Expr::String(name))` if fat arrow follows, `None` otherwise.
19213    #[inline]
19214    fn fat_arrow_autoquote(&self, name: &str, line: usize) -> Option<Expr> {
19215        if matches!(self.peek(), Token::FatArrow) {
19216            Some(Expr {
19217                kind: ExprKind::String(name.to_string()),
19218                line,
19219            })
19220        } else {
19221            None
19222        }
19223    }
19224
19225    /// Parse a hash subscript key inside `{…}`.
19226    ///
19227    /// Perl auto-quotes a single bareword before `}`, even for keywords:
19228    /// `$h{print}`, `$r->{f}` etc. all yield the string key. Stryke also
19229    /// auto-quotes the string-comparison and word-logical operator tokens
19230    /// (`eq`, `ne`, `lt`, `gt`, `le`, `ge`, `cmp`, `and`, `or`, `not`, `x`)
19231    /// here — the lexer eagerly converts those identifiers to operator tokens,
19232    /// but inside `{…}` followed by `}` they're plainly hash keys.
19233    /// Stryke exception: topic-slot barewords (`_`, `_<`, `_0`, `_0<`, …)
19234    /// resolve to the topic value, not the literal name — `$h{_<}` ≡ `$h{$_<}`.
19235    fn parse_hash_subscript_key(&mut self) -> StrykeResult<Expr> {
19236        let line = self.peek_line();
19237        if let Token::Ident(ref k) = self.peek().clone() {
19238            if matches!(self.peek_at(1), Token::RBrace) && !Self::is_underscore_topic_slot(k) {
19239                let s = k.clone();
19240                self.advance();
19241                return Ok(Expr {
19242                    kind: ExprKind::String(s),
19243                    line,
19244                });
19245            }
19246        }
19247        if matches!(self.peek_at(1), Token::RBrace) {
19248            if let Some(s) = Self::operator_keyword_to_ident_str(self.peek()) {
19249                self.advance();
19250                return Ok(Expr {
19251                    kind: ExprKind::String(s.to_string()),
19252                    line,
19253                });
19254            }
19255        }
19256        self.parse_expression()
19257    }
19258
19259    /// `progress` introducing the optional `progress => EXPR` suffix for `glob_par` / `par_sed`.
19260    #[inline]
19261    fn peek_is_glob_par_progress_kw(&self) -> bool {
19262        matches!(self.peek(), Token::Ident(ref kw) if kw == "progress")
19263            && matches!(self.peek_at(1), Token::FatArrow)
19264    }
19265
19266    /// Pattern list for `glob_par` / `par_sed` inside `(...)`, stopping before `)` or `progress =>`.
19267    fn parse_pattern_list_until_rparen_or_progress(&mut self) -> StrykeResult<Vec<Expr>> {
19268        let mut args = Vec::new();
19269        loop {
19270            if matches!(self.peek(), Token::RParen | Token::Eof) {
19271                break;
19272            }
19273            if self.peek_is_glob_par_progress_kw() {
19274                break;
19275            }
19276            args.push(self.parse_assign_expr()?);
19277            match self.peek() {
19278                Token::RParen => break,
19279                Token::Comma => {
19280                    self.advance();
19281                    if matches!(self.peek(), Token::RParen) {
19282                        break;
19283                    }
19284                    if self.peek_is_glob_par_progress_kw() {
19285                        break;
19286                    }
19287                }
19288                _ => {
19289                    return Err(self.syntax_err(
19290                        "expected `,`, `)`, or `progress =>` after argument in `glob_par` / `par_sed`",
19291                        self.peek_line(),
19292                    ));
19293                }
19294            }
19295        }
19296        Ok(args)
19297    }
19298
19299    /// Paren-less pattern list for `glob_par` / `par_sed`, stopping before stmt end or `progress =>`.
19300    fn parse_pattern_list_glob_par_bare(&mut self) -> StrykeResult<Vec<Expr>> {
19301        let mut args = Vec::new();
19302        loop {
19303            if matches!(
19304                self.peek(),
19305                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
19306            ) {
19307                break;
19308            }
19309            if self.peek_is_postfix_stmt_modifier_keyword() {
19310                break;
19311            }
19312            if self.peek_is_glob_par_progress_kw() {
19313                break;
19314            }
19315            args.push(self.parse_assign_expr()?);
19316            if !self.eat(&Token::Comma) {
19317                break;
19318            }
19319            if self.peek_is_glob_par_progress_kw() {
19320                break;
19321            }
19322        }
19323        Ok(args)
19324    }
19325
19326    /// `glob_pat EXPR, ...` or `glob_pat(...)` plus optional `, progress => EXPR` / inner `progress =>`.
19327    fn parse_glob_par_or_par_sed_args(&mut self) -> StrykeResult<(Vec<Expr>, Option<Box<Expr>>)> {
19328        if matches!(self.peek(), Token::LParen) {
19329            self.advance();
19330            let args = self.parse_pattern_list_until_rparen_or_progress()?;
19331            let progress = if self.peek_is_glob_par_progress_kw() {
19332                self.advance();
19333                self.expect(&Token::FatArrow)?;
19334                Some(Box::new(self.parse_assign_expr()?))
19335            } else {
19336                None
19337            };
19338            self.expect(&Token::RParen)?;
19339            Ok((args, progress))
19340        } else {
19341            let args = self.parse_pattern_list_glob_par_bare()?;
19342            // Comma after the last pattern was consumed inside `parse_pattern_list_glob_par_bare`.
19343            let progress = if self.peek_is_glob_par_progress_kw() {
19344                self.advance();
19345                self.expect(&Token::FatArrow)?;
19346                Some(Box::new(self.parse_assign_expr()?))
19347            } else {
19348                None
19349            };
19350            Ok((args, progress))
19351        }
19352    }
19353
19354    pub(crate) fn parse_arg_list(&mut self) -> StrykeResult<Vec<Expr>> {
19355        let mut args = Vec::new();
19356        // Inside `(...)`, `|>` is a normal operator again (e.g. `f(2 |> g, 3)`),
19357        // so shadow any outer paren-less-arg suppression from
19358        // `no_pipe_forward_depth`. Saturating so nested mixes are safe.
19359        let saved_no_pf = self.no_pipe_forward_depth;
19360        self.no_pipe_forward_depth = 0;
19361        while !matches!(
19362            self.peek(),
19363            Token::RParen | Token::RBracket | Token::RBrace | Token::Eof
19364        ) {
19365            let arg = match self.parse_assign_expr() {
19366                Ok(e) => e,
19367                Err(err) => {
19368                    self.no_pipe_forward_depth = saved_no_pf;
19369                    return Err(err);
19370                }
19371            };
19372            args.push(arg);
19373            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
19374                break;
19375            }
19376        }
19377        self.no_pipe_forward_depth = saved_no_pf;
19378        Ok(args)
19379    }
19380
19381    /// Parse a comma-separated list of slice subscript args. Each arg may be a regular
19382    /// expression, a closed range (`1:3`, `1..3:2`), or an open-ended Python-style colon
19383    /// range (`:`, `::`, `:N`, `N:`, `::-1`, `:N:M`, `N::M`, `::M`). Open-ended forms
19384    /// produce `ExprKind::SliceRange`; closed `1:3` produces `ExprKind::Range` (legacy).
19385    ///
19386    /// `is_hash` enables fat-comma-style bareword auto-quoting for endpoints — `{a:c:1}`
19387    /// treats `a` and `c` as string keys without quoting (cannot be a function call;
19388    /// use `func():other` if you actually want to invoke).
19389    pub(crate) fn parse_slice_arg_list(&mut self, is_hash: bool) -> StrykeResult<Vec<Expr>> {
19390        let mut args = Vec::new();
19391        let saved_no_pf = self.no_pipe_forward_depth;
19392        self.no_pipe_forward_depth = 0;
19393        while !matches!(
19394            self.peek(),
19395            Token::RParen | Token::RBracket | Token::RBrace | Token::Eof
19396        ) {
19397            let arg = match self.parse_slice_arg(is_hash) {
19398                Ok(e) => e,
19399                Err(err) => {
19400                    self.no_pipe_forward_depth = saved_no_pf;
19401                    return Err(err);
19402                }
19403            };
19404            args.push(arg);
19405            if !self.eat(&Token::Comma) && !self.eat(&Token::FatArrow) {
19406                break;
19407            }
19408        }
19409        self.no_pipe_forward_depth = saved_no_pf;
19410        Ok(args)
19411    }
19412
19413    /// Parse one slice subscript argument (see [`Self::parse_slice_arg_list`]).
19414    fn parse_slice_arg(&mut self, is_hash: bool) -> StrykeResult<Expr> {
19415        let line = self.peek_line();
19416
19417        // Open-start: `:` or `::` immediately
19418        if matches!(self.peek(), Token::Colon) {
19419            self.advance();
19420            return self.finish_slice_range(None, false, is_hash, line);
19421        }
19422        if matches!(self.peek(), Token::PackageSep) {
19423            self.advance();
19424            return self.finish_slice_range(None, true, is_hash, line);
19425        }
19426
19427        // Parse FROM with `:` suppressed inside `parse_range` so it doesn't get
19428        // consumed as a colon-range there — we want to handle the colon ourselves.
19429        self.suppress_colon_range = self.suppress_colon_range.saturating_add(1);
19430        let result = self.parse_slice_endpoint(is_hash);
19431        self.suppress_colon_range = self.suppress_colon_range.saturating_sub(1);
19432        let from_expr = result?;
19433
19434        // Trailing `:` or `::` after the FROM endpoint?
19435        if matches!(self.peek(), Token::Colon) {
19436            self.advance();
19437            return self.finish_slice_range(Some(Box::new(from_expr)), false, is_hash, line);
19438        }
19439        if matches!(self.peek(), Token::PackageSep) {
19440            self.advance();
19441            return self.finish_slice_range(Some(Box::new(from_expr)), true, is_hash, line);
19442        }
19443
19444        Ok(from_expr)
19445    }
19446
19447    /// After consuming the first colon (or `::` pair), parse the rest of the slice range.
19448    /// `double` is true if we just consumed `::` — TO is implicit `None`, the next
19449    /// expression (if any) is STEP.
19450    ///
19451    /// Returns `ExprKind::Range` for fully-closed forms (legacy compatibility) and
19452    /// `ExprKind::SliceRange` whenever any endpoint is omitted (open-ended).
19453    fn finish_slice_range(
19454        &mut self,
19455        from: Option<Box<Expr>>,
19456        double: bool,
19457        is_hash: bool,
19458        line: usize,
19459    ) -> StrykeResult<Expr> {
19460        let (to, step) = if double {
19461            // `::` so TO is implicit; STEP is whatever (if anything) follows.
19462            let step_v = self.parse_slice_optional_endpoint(is_hash)?;
19463            (None, step_v)
19464        } else {
19465            // single `:` — parse TO, then optional `:STEP`.
19466            let to_v = self.parse_slice_optional_endpoint(is_hash)?;
19467            let step_v = if matches!(self.peek(), Token::Colon) {
19468                self.advance();
19469                self.parse_slice_optional_endpoint(is_hash)?
19470            } else if matches!(self.peek(), Token::PackageSep) {
19471                return Err(
19472                    self.syntax_err("Unexpected `::` after slice TO endpoint".to_string(), line)
19473                );
19474            } else {
19475                None
19476            };
19477            (to_v, step_v)
19478        };
19479
19480        // Closed form (both endpoints present) — produce a regular `Range` so the
19481        // rest of the compiler/VM keeps reusing existing range-expansion paths.
19482        if let (Some(f), Some(t)) = (from.as_ref(), to.as_ref()) {
19483            return Ok(Expr {
19484                kind: ExprKind::Range {
19485                    from: f.clone(),
19486                    to: t.clone(),
19487                    exclusive: false,
19488                    step,
19489                },
19490                line,
19491            });
19492        }
19493
19494        Ok(Expr {
19495            kind: ExprKind::SliceRange { from, to, step },
19496            line,
19497        })
19498    }
19499
19500    /// Parse an optional slice endpoint: returns `None` if the next token closes the slice
19501    /// arg (`,`, `]`, `}`, or another `:`). Otherwise parses an endpoint expression.
19502    fn parse_slice_optional_endpoint(&mut self, is_hash: bool) -> StrykeResult<Option<Box<Expr>>> {
19503        if matches!(
19504            self.peek(),
19505            Token::Colon
19506                | Token::PackageSep
19507                | Token::Comma
19508                | Token::RBracket
19509                | Token::RBrace
19510                | Token::Eof
19511        ) {
19512            return Ok(None);
19513        }
19514        self.suppress_colon_range = self.suppress_colon_range.saturating_add(1);
19515        let r = self.parse_slice_endpoint(is_hash);
19516        self.suppress_colon_range = self.suppress_colon_range.saturating_sub(1);
19517        Ok(Some(Box::new(r?)))
19518    }
19519
19520    /// Parse a single slice endpoint expression. For hash slices, a bareword `Ident`
19521    /// followed by `:`, `::`, `,`, `]`, or `}` auto-quotes (fat-comma style); otherwise
19522    /// fall through to standard expression parsing. For array slices, no auto-quote.
19523    fn parse_slice_endpoint(&mut self, is_hash: bool) -> StrykeResult<Expr> {
19524        if is_hash {
19525            if let Token::Ident(name) = self.peek().clone() {
19526                if matches!(
19527                    self.peek_at(1),
19528                    Token::Colon
19529                        | Token::PackageSep
19530                        | Token::Comma
19531                        | Token::RBracket
19532                        | Token::RBrace
19533                ) {
19534                    let line = self.peek_line();
19535                    self.advance();
19536                    return Ok(Expr {
19537                        kind: ExprKind::String(name),
19538                        line,
19539                    });
19540                }
19541            }
19542        }
19543        self.parse_assign_expr()
19544    }
19545
19546    /// Arguments for `->name` / `->SUPER::name` **without** `(...)`. Unlike `die foo + 1`
19547    /// (unary `+` on `1` passed to `foo`), Perl treats `$o->meth + 5` as infix `+` after a
19548    /// no-arg method call; we must not consume that `+` as the start of a first argument.
19549    fn parse_method_arg_list_no_paren(&mut self) -> StrykeResult<Vec<Expr>> {
19550        let mut args = Vec::new();
19551        let call_line = self.prev_line();
19552        loop {
19553            // `$g->next { ... }` — `{` starts the enclosing statement's block, not an anonymous
19554            // hash argument to `next` (paren-less method call has no args here).
19555            if args.is_empty() && matches!(self.peek(), Token::LBrace) {
19556                break;
19557            }
19558            if matches!(
19559                self.peek(),
19560                Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof | Token::PipeForward
19561            ) {
19562                break;
19563            }
19564            if let Token::Ident(ref kw) = self.peek().clone() {
19565                if matches!(
19566                    kw.as_str(),
19567                    "if" | "unless" | "while" | "until" | "for" | "foreach"
19568                ) {
19569                    break;
19570                }
19571            }
19572            // `foo($obj->meth, $x)` — comma separates *outer* args; it is not the start of a
19573            // paren-less method argument (those use spaces: `$obj->meth $a, $b`).
19574            if args.is_empty()
19575                && (self.peek_method_arg_infix_terminator() || matches!(self.peek(), Token::Comma))
19576            {
19577                break;
19578            }
19579            // Implicit semicolon: if no args collected yet and next token is on a different
19580            // line, treat newline as statement boundary. Allows `$p->method\nnext_stmt`.
19581            if args.is_empty() && self.peek_line() > call_line {
19582                break;
19583            }
19584            args.push(self.parse_assign_expr()?);
19585            if !self.eat(&Token::Comma) {
19586                break;
19587            }
19588        }
19589        Ok(args)
19590    }
19591
19592    /// Tokens that end a paren-less method arg list when no comma-separated args yet (infix on
19593    /// the whole `->meth` expression).
19594    fn peek_method_arg_infix_terminator(&self) -> bool {
19595        matches!(
19596            self.peek(),
19597            Token::Plus
19598                | Token::Minus
19599                | Token::Star
19600                | Token::Slash
19601                | Token::Percent
19602                | Token::Power
19603                | Token::Dot
19604                | Token::X
19605                | Token::NumEq
19606                | Token::NumNe
19607                | Token::NumLt
19608                | Token::NumGt
19609                | Token::NumLe
19610                | Token::NumGe
19611                | Token::Spaceship
19612                | Token::StrEq
19613                | Token::StrNe
19614                | Token::StrLt
19615                | Token::StrGt
19616                | Token::StrLe
19617                | Token::StrGe
19618                | Token::StrCmp
19619                | Token::LogAnd
19620                | Token::LogOr
19621                | Token::LogAndWord
19622                | Token::LogOrWord
19623                | Token::DefinedOr
19624                | Token::BitAnd
19625                | Token::BitOr
19626                | Token::BitXor
19627                | Token::ShiftLeft
19628                | Token::ShiftRight
19629                | Token::Range
19630                | Token::RangeExclusive
19631                | Token::BindMatch
19632                | Token::BindNotMatch
19633                | Token::Arrow
19634                // `($a->b) ? $a->c : $a->d` — `->c` must not slurp the ternary `:` / `?`.
19635                | Token::Question
19636                | Token::Colon
19637                // Assignment operators: `$obj->field = val` is setter sugar, not method arg.
19638                | Token::Assign
19639                | Token::PlusAssign
19640                | Token::MinusAssign
19641                | Token::MulAssign
19642                | Token::DivAssign
19643                | Token::ModAssign
19644                | Token::PowAssign
19645                | Token::DotAssign
19646                | Token::AndAssign
19647                | Token::OrAssign
19648                | Token::XorAssign
19649                | Token::DefinedOrAssign
19650                | Token::ShiftLeftAssign
19651                | Token::ShiftRightAssign
19652                | Token::BitAndAssign
19653                | Token::BitOrAssign
19654        )
19655    }
19656
19657    fn parse_list_until_terminator(&mut self) -> StrykeResult<Vec<Expr>> {
19658        self.parse_list_until_terminator_inner(false)
19659    }
19660
19661    /// Variant of `parse_list_until_terminator` that allows `|>` within arguments.
19662    /// Used by print-like statements (`p`, `say`, `print`, `printf`) so that
19663    /// `p @a |> sum` parses as `p(sum(@a))` rather than `sum(p(@a))`, matching
19664    /// the behavior of `~>` thread-first macro.
19665    fn parse_list_until_terminator_allow_pipe(&mut self) -> StrykeResult<Vec<Expr>> {
19666        self.parse_list_until_terminator_inner(true)
19667    }
19668
19669    fn parse_list_until_terminator_inner(&mut self, allow_pipe: bool) -> StrykeResult<Vec<Expr>> {
19670        let mut args = Vec::new();
19671        // Line of the last consumed token (the keyword / function name that
19672        // triggered this arg parse).  Used for implicit-semicolon: if no args
19673        // have been parsed yet and the next token is on a *different* line,
19674        // treat the newline as a statement boundary and stop.
19675        let call_line = self.prev_line();
19676        loop {
19677            // When `allow_pipe` is false, `|>` terminates the list (preserving
19678            // left-associativity for chains like `@a |> head 2 |> join "-"`).
19679            // When true (print-like statements), `|>` is allowed within args.
19680            let is_terminator = if allow_pipe {
19681                matches!(
19682                    self.peek(),
19683                    Token::Semicolon | Token::RBrace | Token::RParen | Token::Eof
19684                )
19685            } else {
19686                matches!(
19687                    self.peek(),
19688                    Token::Semicolon
19689                        | Token::RBrace
19690                        | Token::RParen
19691                        | Token::Eof
19692                        | Token::PipeForward
19693                )
19694            };
19695            if is_terminator {
19696                break;
19697            }
19698            // Check for postfix modifiers — stop before `expr for LIST` / `expr if COND` etc.
19699            if let Token::Ident(ref kw) = self.peek().clone() {
19700                if matches!(
19701                    kw.as_str(),
19702                    "if" | "unless" | "while" | "until" | "for" | "foreach"
19703                ) {
19704                    break;
19705                }
19706            }
19707            // Implicit semicolons: if no args have been collected yet and the
19708            // next token is on a different line from the call keyword, treat
19709            // the newline as a statement boundary.  This prevents paren-less
19710            // calls (`say`, `print`, user subs) from greedily swallowing the
19711            // *next* statement when the author omitted a semicolon.
19712            // After a comma continuation, multi-line arg lists still work.
19713            if args.is_empty() && self.peek_line() > call_line {
19714                break;
19715            }
19716            // When `allow_pipe` is true, pipe chains are consumed within each
19717            // argument. When false, `|>` terminates the whole call list, so
19718            // individual args must not absorb a following `|>`.
19719            if allow_pipe {
19720                args.push(self.parse_assign_expr()?);
19721            } else {
19722                args.push(self.parse_assign_expr_stop_at_pipe()?);
19723            }
19724            if !self.eat(&Token::Comma) {
19725                break;
19726            }
19727        }
19728        Ok(args)
19729    }
19730
19731    /// Body of `+{ ... }` — Perl's force-hashref idiom. The opening `+` and `{`
19732    /// have already been consumed. Tries the normal `KEY => VAL, …` shape first
19733    /// (so `+{ a => 1, b => 2 }` is identical to `{ a => 1, b => 2 }`); on
19734    /// failure falls back to "single list-yielding expression treated as a
19735    /// flat key/value spread" so `+{ map { (k, v) } LIST }` works without
19736    /// the user needing a temp `my %h = ...; \%h` shuffle.
19737    fn parse_forced_hashref_body(&mut self, line: usize) -> StrykeResult<Expr> {
19738        let saved = self.pos;
19739        if let Ok(pairs) = self.try_parse_hash_ref() {
19740            return Ok(Expr {
19741                kind: ExprKind::HashRef(pairs),
19742                line,
19743            });
19744        }
19745        // Empty `+{}` is the empty hashref.
19746        self.pos = saved;
19747        if matches!(self.peek(), Token::RBrace) {
19748            self.advance();
19749            return Ok(Expr {
19750                kind: ExprKind::HashRef(vec![]),
19751                line,
19752            });
19753        }
19754        // Single expression — eval as list, flatten into key/value pairs via the
19755        // existing __HASH_SPREAD__ sentinel that `ExprKind::HashRef` already
19756        // handles in [`Interpreter::eval_expr`].
19757        let inner = self.parse_expression()?;
19758        self.expect(&Token::RBrace)?;
19759        let sentinel_key = Expr {
19760            kind: ExprKind::String("__HASH_SPREAD__".into()),
19761            line,
19762        };
19763        Ok(Expr {
19764            kind: ExprKind::HashRef(vec![(sentinel_key, inner)]),
19765            line,
19766        })
19767    }
19768
19769    fn try_parse_hash_ref(&mut self) -> StrykeResult<Vec<(Expr, Expr)>> {
19770        let mut pairs = Vec::new();
19771        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
19772            // Perl autoquotes a bareword immediately before `=>` (hash key), even for keywords like
19773            // `pos`, `bless`, `return` — see Text::Balanced `_failmsg` (`pos => $pos`).
19774            // Stryke exception: topic-slot barewords (`_`, `_<`, `_0`, `_0<`, `_!N!`, …)
19775            // resolve to the topic value, not the literal name — `{ _ => 1 }` ≡ `{ $_ => 1 }`.
19776            let line = self.peek_line();
19777            let key = if let Token::Ident(ref name) = self.peek().clone() {
19778                if matches!(self.peek_at(1), Token::FatArrow)
19779                    && !Self::is_underscore_topic_slot(name)
19780                {
19781                    self.advance();
19782                    Expr {
19783                        kind: ExprKind::String(name.clone()),
19784                        line,
19785                    }
19786                } else {
19787                    self.parse_assign_expr()?
19788                }
19789            } else {
19790                self.parse_assign_expr()?
19791            };
19792            // If the key expression is a hash/array variable and is followed by `}` or `,`
19793            // with no `=>`, treat the whole thing as a hash-from-expression construction.
19794            // This handles `{ %a }`, `{ %a, key => val }`, etc.
19795            if matches!(self.peek(), Token::RBrace | Token::Comma)
19796                && matches!(
19797                    key.kind,
19798                    ExprKind::HashVar(_)
19799                        | ExprKind::Deref {
19800                            kind: Sigil::Hash,
19801                            ..
19802                        }
19803                )
19804            {
19805                // Synthesize a pair whose key/value is spread from the hash expression.
19806                // Use a sentinel "spread" pair: key=the hash expr, value=undef.
19807                // The evaluator will flatten this.
19808                let sentinel_key = Expr {
19809                    kind: ExprKind::String("__HASH_SPREAD__".into()),
19810                    line,
19811                };
19812                pairs.push((sentinel_key, key));
19813                self.eat(&Token::Comma);
19814                continue;
19815            }
19816            // Expect => or , after key
19817            if self.eat(&Token::FatArrow) || self.eat(&Token::Comma) {
19818                let val = self.parse_assign_expr()?;
19819                pairs.push((key, val));
19820                self.eat(&Token::Comma);
19821            } else {
19822                return Err(self.syntax_err("Expected => or , in hash ref", key.line));
19823            }
19824        }
19825        self.expect(&Token::RBrace)?;
19826        Ok(pairs)
19827    }
19828
19829    /// Parse `key => val, key => val, ...` up to (but not consuming) `term`.
19830    /// Used by the `%[…]` and `%{k=>v,…}` sugar to build an inline hashref
19831    /// AST node, sidestepping the block/hashref ambiguity that `try_parse_hash_ref`
19832    /// navigates. Caller expects and consumes `term` itself.
19833    fn parse_hashref_pairs_until(&mut self, term: &Token) -> StrykeResult<Vec<(Expr, Expr)>> {
19834        let mut pairs = Vec::new();
19835        while !matches!(&self.peek(), t if std::mem::discriminant(*t) == std::mem::discriminant(term))
19836            && !matches!(self.peek(), Token::Eof)
19837        {
19838            let line = self.peek_line();
19839            let key = if let Token::Ident(ref name) = self.peek().clone() {
19840                if matches!(self.peek_at(1), Token::FatArrow)
19841                    && !Self::is_underscore_topic_slot(name)
19842                {
19843                    self.advance();
19844                    Expr {
19845                        kind: ExprKind::String(name.clone()),
19846                        line,
19847                    }
19848                } else {
19849                    self.parse_assign_expr()?
19850                }
19851            } else {
19852                self.parse_assign_expr()?
19853            };
19854            if self.eat(&Token::FatArrow) || self.eat(&Token::Comma) {
19855                let val = self.parse_assign_expr()?;
19856                pairs.push((key, val));
19857                self.eat(&Token::Comma);
19858            } else {
19859                return Err(self.syntax_err("Expected => or , in hash ref", key.line));
19860            }
19861        }
19862        Ok(pairs)
19863    }
19864
19865    /// Inside an interpolated string, after a `$name`/`${EXPR}`/`$name[i]`/`$name{k}` base
19866    /// expression, consume any chain of `->[…]`, `->{…}`, **adjacent** `[…]`, or `{…}`
19867    /// subscripts. Perl auto-implies `->` between consecutive subscripts, so
19868    /// `$matrix[1][1]` is `$matrix[1]->[1]` and `$h{a}{b}` is `$h{a}->{b}`.
19869    /// Each step wraps the current expression in an `ArrowDeref`.
19870    fn interp_chain_subscripts(
19871        &self,
19872        chars: &[char],
19873        i: &mut usize,
19874        mut base: Expr,
19875        line: usize,
19876    ) -> Expr {
19877        loop {
19878            // Optional `->` connector
19879            let (after, requires_subscript) =
19880                if *i + 1 < chars.len() && chars[*i] == '-' && chars[*i + 1] == '>' {
19881                    (*i + 2, true)
19882                } else {
19883                    (*i, false)
19884                };
19885            if after >= chars.len() {
19886                break;
19887            }
19888            match chars[after] {
19889                '[' => {
19890                    *i = after + 1;
19891                    let mut idx_str = String::new();
19892                    while *i < chars.len() && chars[*i] != ']' {
19893                        idx_str.push(chars[*i]);
19894                        *i += 1;
19895                    }
19896                    if *i < chars.len() {
19897                        *i += 1;
19898                    }
19899                    let idx_expr = if let Some(rest) = idx_str.strip_prefix('$') {
19900                        Expr {
19901                            kind: ExprKind::ScalarVar(rest.to_string()),
19902                            line,
19903                        }
19904                    } else if let Ok(n) = idx_str.parse::<i64>() {
19905                        Expr {
19906                            kind: ExprKind::Integer(n),
19907                            line,
19908                        }
19909                    } else {
19910                        Expr {
19911                            kind: ExprKind::String(idx_str),
19912                            line,
19913                        }
19914                    };
19915                    base = Expr {
19916                        kind: ExprKind::ArrowDeref {
19917                            expr: Box::new(base),
19918                            index: Box::new(idx_expr),
19919                            kind: DerefKind::Array,
19920                        },
19921                        line,
19922                    };
19923                }
19924                '{' => {
19925                    *i = after + 1;
19926                    let mut key = String::new();
19927                    let mut depth = 1usize;
19928                    while *i < chars.len() && depth > 0 {
19929                        if chars[*i] == '{' {
19930                            depth += 1;
19931                        } else if chars[*i] == '}' {
19932                            depth -= 1;
19933                            if depth == 0 {
19934                                break;
19935                            }
19936                        }
19937                        key.push(chars[*i]);
19938                        *i += 1;
19939                    }
19940                    if *i < chars.len() {
19941                        *i += 1;
19942                    }
19943                    let key_expr = if let Some(rest) = key.strip_prefix('$') {
19944                        Expr {
19945                            kind: ExprKind::ScalarVar(rest.to_string()),
19946                            line,
19947                        }
19948                    } else {
19949                        Expr {
19950                            kind: ExprKind::String(key),
19951                            line,
19952                        }
19953                    };
19954                    base = Expr {
19955                        kind: ExprKind::ArrowDeref {
19956                            expr: Box::new(base),
19957                            index: Box::new(key_expr),
19958                            kind: DerefKind::Hash,
19959                        },
19960                        line,
19961                    };
19962                }
19963                _ => {
19964                    if requires_subscript {
19965                        // `->method()` etc — not interpolated, leave for literal output.
19966                    }
19967                    break;
19968                }
19969            }
19970        }
19971        base
19972    }
19973
19974    /// Reject `$a` / `$b` references in `--no-interop` mode (lexer catches them
19975    /// outside double-quoted strings; this catches the in-string interpolation
19976    /// path which has its own parser bypassing `Token::ScalarVar`).
19977    fn no_interop_check_scalar_var_name(&self, name: &str, line: usize) -> StrykeResult<()> {
19978        if crate::no_interop_mode() && (name == "a" || name == "b") {
19979            return Err(self.syntax_err(
19980                format!(
19981                    "stryke uses `_` / `_1` (bareword in code) or `$_` / `$_1` \
19982                     (sigil inside string interpolation / when whitespace would \
19983                     change parsing) instead of `${}` (--no-interop is active)",
19984                    name
19985                ),
19986                line,
19987            ));
19988        }
19989        Ok(())
19990    }
19991
19992    /// Quote-aware balanced-brace scan for `${…}` / `@{…}` / `#{…}`
19993    /// interpolation bodies. `chars[start]` is the first char after the
19994    /// opening `{`; depth starts at 1. `{` / `}` inside nested '…' / "…"
19995    /// literals (with `\` escapes) don't count toward depth, so
19996    /// `"#{"}"}"` finds the right closer. Returns `(inner, index of the
19997    /// closing brace)`, or `None` when unterminated.
19998    ///
19999    /// The quote tracker can't tell string delimiters from quote chars in
20000    /// regex content — `#{ _ =~ s/"/""/gr }` has an odd quote count, so the
20001    /// quote-aware pass never sees the closer. When that pass fails, retry
20002    /// quote-blind (plain depth counting): a region the quote tracker calls
20003    /// unterminated means the heuristic mis-read non-string quotes.
20004    fn scan_interp_braces(chars: &[char], start: usize) -> Option<(String, usize)> {
20005        Self::scan_interp_braces_pass(chars, start, true)
20006            .or_else(|| Self::scan_interp_braces_pass(chars, start, false))
20007    }
20008
20009    fn scan_interp_braces_pass(
20010        chars: &[char],
20011        start: usize,
20012        quote_aware: bool,
20013    ) -> Option<(String, usize)> {
20014        let mut depth = 1usize;
20015        let mut quote: Option<char> = None;
20016        let mut esc = false;
20017        let mut i = start;
20018        while i < chars.len() {
20019            let c = chars[i];
20020            if let Some(q) = quote {
20021                if esc {
20022                    esc = false;
20023                } else if c == '\\' {
20024                    esc = true;
20025                } else if c == q {
20026                    quote = None;
20027                }
20028            } else {
20029                match c {
20030                    '\'' | '"' if quote_aware => quote = Some(c),
20031                    '{' => depth += 1,
20032                    '}' => {
20033                        depth -= 1;
20034                        if depth == 0 {
20035                            return Some((chars[start..i].iter().collect(), i));
20036                        }
20037                    }
20038                    _ => {}
20039                }
20040            }
20041            i += 1;
20042        }
20043        None
20044    }
20045
20046    fn parse_interpolated_string(&self, s: &str, line: usize) -> StrykeResult<Expr> {
20047        // Parse $var and @var inside double-quoted strings
20048        let mut parts = Vec::new();
20049        let mut literal = String::new();
20050        let chars: Vec<char> = s.chars().collect();
20051        let mut i = 0;
20052
20053        'istr: while i < chars.len() {
20054            if chars[i] == LITERAL_DOLLAR_IN_DQUOTE {
20055                literal.push('$');
20056                i += 1;
20057                continue;
20058            }
20059            if chars[i] == LITERAL_AT_IN_DQUOTE {
20060                literal.push('@');
20061                i += 1;
20062                continue;
20063            }
20064            // "\\$x" in source: one backslash in the string, then interpolate $x (Perl double-quoted string).
20065            if chars[i] == '\\' && i + 1 < chars.len() && chars[i + 1] == '$' {
20066                literal.push('\\');
20067                i += 1;
20068                // i now points at '$' — fall through to $ handling below
20069            }
20070            if chars[i] == '$' && i + 1 < chars.len() {
20071                if !literal.is_empty() {
20072                    parts.push(StringPart::Literal(std::mem::take(&mut literal)));
20073                }
20074                i += 1; // past `$`
20075                        // Perl allows whitespace between `$` and the variable name (`$ foo` → `$foo`).
20076                while i < chars.len() && chars[i].is_whitespace() {
20077                    i += 1;
20078                }
20079                if i >= chars.len() {
20080                    return Err(self.syntax_err("Final $ should be \\$ or $name", line));
20081                }
20082                // `$#name` — last index of `@name` (Perl `$#array`).
20083                if chars[i] == '#' {
20084                    i += 1;
20085                    let mut sname = String::from("#");
20086                    while i < chars.len()
20087                        && (chars[i].is_alphanumeric() || chars[i] == '_' || chars[i] == ':')
20088                    {
20089                        sname.push(chars[i]);
20090                        i += 1;
20091                    }
20092                    while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
20093                        sname.push_str("::");
20094                        i += 2;
20095                        while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
20096                            sname.push(chars[i]);
20097                            i += 1;
20098                        }
20099                    }
20100                    self.no_interop_check_scalar_var_name(&sname, line)?;
20101                    parts.push(StringPart::ScalarVar(sname));
20102                    continue;
20103                }
20104                // `$$` — process id (Perl `$$`), only when the two `$` are adjacent (no whitespace
20105                // between) and the second `$` is not followed by a word character or digit (`$$x`
20106                // / `$$_` / `$$0` are `$` + `$x` / `$_` / `$0`).
20107                if chars[i] == '$' {
20108                    let next_c = chars.get(i + 1).copied();
20109                    let is_pid = match next_c {
20110                        None => true,
20111                        Some(c)
20112                            if !c.is_ascii_digit() && !matches!(c, 'A'..='Z' | 'a'..='z' | '_') =>
20113                        {
20114                            true
20115                        }
20116                        _ => false,
20117                    };
20118                    if is_pid {
20119                        parts.push(StringPart::ScalarVar("$$".to_string()));
20120                        i += 1; // consume second `$`
20121                        continue;
20122                    }
20123                    i += 1; // skip second `$` — same as a single `$` before the identifier
20124                }
20125                if chars[i] == '{' {
20126                    // `${…}` — braced variable OR expression interpolation.
20127                    //   `${name}`              → ScalarVar(name)        (Perl standard)
20128                    //   `${$ref}` / `${\EXPR}` → deref the expression   (Perl standard)
20129                    //   `${name}[idx]` / `${name}{k}` / `${$r}[i]` …    chain after `}`
20130                    // stryke's prior `#{expr}` form remains supported elsewhere.
20131                    i += 1;
20132                    let inner = match Self::scan_interp_braces(&chars, i) {
20133                        Some((inner, close)) => {
20134                            i = close + 1; // past closing `}`
20135                            inner
20136                        }
20137                        None => {
20138                            // Unterminated: consume the rest (historical behavior).
20139                            let rest: String = chars[i..].iter().collect();
20140                            i = chars.len();
20141                            rest
20142                        }
20143                    };
20144
20145                    // Distinguish "name" from "expression". If trimmed inner starts with
20146                    // `$`, `\`, or contains operator/punctuation chars, treat as Perl
20147                    // expression and emit a scalar deref. Otherwise, plain variable name.
20148                    let trimmed = inner.trim();
20149                    if !crate::compat_mode() && zsh_param_form(trimmed) {
20150                        // zsh parameter expansion (`${x:-d}`, `${x#p}`, `${(U)x}`,
20151                        // `${#x}`, …) — world-first: stryke supports the full zsh
20152                        // `${ … }` grammar inside double-quoted strings by
20153                        // delegating to zshrs's `paramsubst` engine at runtime via
20154                        // the `zpexpand` builtin (no reimplementation). Gated off
20155                        // under `--compat`, where `${ … }` keeps pure Perl 5
20156                        // semantics.
20157                        parts.push(StringPart::Expr(build_zpexpand_expr(trimmed, line)));
20158                    } else {
20159                    let mut base: Expr = if trimmed.starts_with('$')
20160                        || trimmed.starts_with('\\')
20161                        || trimmed.starts_with('@')   // `${@arr}` rare but valid
20162                        || trimmed.starts_with('%')   // `${%h}`   rare but valid
20163                        || trimmed.contains(['(', '+', '-', '*', '/', '.', '?', '&', '|'])
20164                    {
20165                        // Re-parse the inner content as a Perl expression. Wrap in
20166                        // `Deref { kind: Sigil::Scalar }` to dereference the resulting
20167                        // scalar reference (Perl: `${$r}` ≡ `$$r`).
20168                        match parse_expression_from_str(trimmed, "<interp>") {
20169                            Ok(e) => Expr {
20170                                kind: ExprKind::Deref {
20171                                    expr: Box::new(e),
20172                                    kind: Sigil::Scalar,
20173                                },
20174                                line,
20175                            },
20176                            Err(_) => Expr {
20177                                kind: ExprKind::ScalarVar(inner.clone()),
20178                                line,
20179                            },
20180                        }
20181                    } else {
20182                        // Treat as a plain (possibly qualified) variable name.
20183                        self.no_interop_check_scalar_var_name(&inner, line)?;
20184                        Expr {
20185                            kind: ExprKind::ScalarVar(inner),
20186                            line,
20187                        }
20188                    };
20189
20190                    // After `${…}` we may see `[idx]` / `{key}` for indexing into the
20191                    // dereferenced array/hash (`${$ar}[1]`, `${$hr}{k}`), and arrow
20192                    // chains thereafter.
20193                    base = self.interp_chain_subscripts(&chars, &mut i, base, line);
20194                    parts.push(StringPart::Expr(base));
20195                    }
20196                } else if chars[i] == '^' {
20197                    // `$^V`, `$^O`, … — name stored as `^V`, `^O`, … (see [`Interpreter::get_special_var`]).
20198                    let mut name = String::from("^");
20199                    i += 1;
20200                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
20201                        name.push(chars[i]);
20202                        i += 1;
20203                    }
20204                    if i < chars.len() && chars[i] == '{' {
20205                        i += 1; // skip {
20206                        let mut key = String::new();
20207                        let mut depth = 1;
20208                        while i < chars.len() && depth > 0 {
20209                            if chars[i] == '{' {
20210                                depth += 1;
20211                            } else if chars[i] == '}' {
20212                                depth -= 1;
20213                                if depth == 0 {
20214                                    break;
20215                                }
20216                            }
20217                            key.push(chars[i]);
20218                            i += 1;
20219                        }
20220                        if i < chars.len() {
20221                            i += 1;
20222                        }
20223                        let key_expr = if let Some(rest) = key.strip_prefix('$') {
20224                            Expr {
20225                                kind: ExprKind::ScalarVar(rest.to_string()),
20226                                line,
20227                            }
20228                        } else {
20229                            Expr {
20230                                kind: ExprKind::String(key),
20231                                line,
20232                            }
20233                        };
20234                        parts.push(StringPart::Expr(Expr {
20235                            kind: ExprKind::HashElement {
20236                                hash: name,
20237                                key: Box::new(key_expr),
20238                            },
20239                            line,
20240                        }));
20241                    } else if i < chars.len() && chars[i] == '[' {
20242                        i += 1;
20243                        let mut idx_str = String::new();
20244                        while i < chars.len() && chars[i] != ']' {
20245                            idx_str.push(chars[i]);
20246                            i += 1;
20247                        }
20248                        if i < chars.len() {
20249                            i += 1;
20250                        }
20251                        let idx_expr = if let Some(rest) = idx_str.strip_prefix('$') {
20252                            Expr {
20253                                kind: ExprKind::ScalarVar(rest.to_string()),
20254                                line,
20255                            }
20256                        } else if let Ok(n) = idx_str.parse::<i64>() {
20257                            Expr {
20258                                kind: ExprKind::Integer(n),
20259                                line,
20260                            }
20261                        } else {
20262                            Expr {
20263                                kind: ExprKind::String(idx_str),
20264                                line,
20265                            }
20266                        };
20267                        parts.push(StringPart::Expr(Expr {
20268                            kind: ExprKind::ArrayElement {
20269                                array: name,
20270                                index: Box::new(idx_expr),
20271                            },
20272                            line,
20273                        }));
20274                    } else {
20275                        self.no_interop_check_scalar_var_name(&name, line)?;
20276                        parts.push(StringPart::ScalarVar(name));
20277                    }
20278                } else if chars[i].is_alphabetic() || chars[i] == '_' {
20279                    let mut name = String::new();
20280                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
20281                        name.push(chars[i]);
20282                        i += 1;
20283                    }
20284                    // Package-qualified names: `$Foo::x`, `$Foo::Bar::baz`. Mirror
20285                    // the `$#Foo::a` continuation logic. Without this, `"$Foo::x"`
20286                    // captures only `Foo` and leaves `::x` as literal text — the
20287                    // interpolation reads bare `$Foo`, which is undef.
20288                    while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
20289                        name.push_str("::");
20290                        i += 2;
20291                        while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
20292                            name.push(chars[i]);
20293                            i += 1;
20294                        }
20295                    }
20296                    // `"$main::!"`, `"$main::@"`, `"$main::?"` — stryke
20297                    // implements the Perl docs faithfully ("All
20298                    // punctuation variables like $_ reside in main"):
20299                    // after the `::` chain stops, accept a single
20300                    // punctuation / digit char (or `^LETTER`) as the
20301                    // leaf so the interp sees one ScalarVar token.
20302                    // The runtime canonicalizes `main::PUNCT` →
20303                    // `PUNCT`.
20304                    //
20305                    // Disabled in `--compat`: Perl 5.42 doesn't parse
20306                    // `$main::!` as a var; the trailing punct is left
20307                    // as literal string text. Match that behavior in
20308                    // compat mode.
20309                    if name.ends_with("::") && i < chars.len() && !crate::compat_mode() {
20310                        if chars[i] == '^'
20311                            && i + 1 < chars.len()
20312                            && chars[i + 1].is_ascii_alphabetic()
20313                        {
20314                            name.push('^');
20315                            name.push(chars[i + 1]);
20316                            i += 2;
20317                        } else if "!@$&*+;',\"\\|?/<>.0123456789~%-=()[]{}".contains(chars[i]) {
20318                            name.push(chars[i]);
20319                            i += 1;
20320                        }
20321                    }
20322                    // `$_<`, `$_<<`, … — outer topic (stryke extension). Also
20323                    // `$_N<`, `$_N<<` for positional aliases. And the indexed
20324                    // shortcut `$_<N` ≡ `$_<<<...<` (N chevrons), so `"$_<3"`
20325                    // and `"$_<<<"` interpolate identically.
20326                    let is_topic_slot = name == "_"
20327                        || (name.len() > 1
20328                            && name.starts_with('_')
20329                            && name[1..].bytes().all(|b| b.is_ascii_digit()));
20330                    if is_topic_slot {
20331                        // Try indexed-ascent first: `<` immediately followed by digits.
20332                        let try_indexed = chars.get(i) == Some(&'<')
20333                            && chars.get(i + 1).is_some_and(|c| c.is_ascii_digit());
20334                        let mut handled_indexed = false;
20335                        if try_indexed {
20336                            let mut j = i + 1;
20337                            while j < chars.len() && chars[j].is_ascii_digit() {
20338                                j += 1;
20339                            }
20340                            let digits: String = chars[i + 1..j].iter().collect();
20341                            if let Ok(n) = digits.parse::<usize>() {
20342                                if n >= 1 {
20343                                    for _ in 0..n {
20344                                        name.push('<');
20345                                    }
20346                                    i = j;
20347                                    handled_indexed = true;
20348                                }
20349                            }
20350                        }
20351                        if !handled_indexed {
20352                            while i < chars.len() && chars[i] == '<' {
20353                                name.push('<');
20354                                i += 1;
20355                            }
20356                        }
20357                    }
20358                    // `--no-interop`: `$a` / `$b` are Perl-isms; reject inside
20359                    // string interpolation too. Catches both `"$a"` and `"$a[0]"`
20360                    // / `"$a{k}"` / `"$a->[0]"` because every branch below uses
20361                    // `name` to build the expression.
20362                    self.no_interop_check_scalar_var_name(&name, line)?;
20363                    // Build the base expression, then thread arrow-deref chains
20364                    // (`->[…]` / `->{…}`) onto it so things like `$ar->[2]`,
20365                    // `$href->{k}`, and chained `$x->{a}[1]->{b}` interpolate
20366                    // correctly inside double-quoted strings (Perl convention).
20367                    let mut base = if i < chars.len() && chars[i] == '{' {
20368                        // $hash{key}
20369                        i += 1; // skip {
20370                        let mut key = String::new();
20371                        let mut depth = 1;
20372                        while i < chars.len() && depth > 0 {
20373                            if chars[i] == '{' {
20374                                depth += 1;
20375                            } else if chars[i] == '}' {
20376                                depth -= 1;
20377                                if depth == 0 {
20378                                    break;
20379                                }
20380                            }
20381                            key.push(chars[i]);
20382                            i += 1;
20383                        }
20384                        if i < chars.len() {
20385                            i += 1;
20386                        } // skip }
20387                        let key_expr = if let Some(rest) = key.strip_prefix('$') {
20388                            Expr {
20389                                kind: ExprKind::ScalarVar(rest.to_string()),
20390                                line,
20391                            }
20392                        } else {
20393                            Expr {
20394                                kind: ExprKind::String(key),
20395                                line,
20396                            }
20397                        };
20398                        Expr {
20399                            kind: ExprKind::HashElement {
20400                                hash: name,
20401                                key: Box::new(key_expr),
20402                            },
20403                            line,
20404                        }
20405                    } else if i < chars.len() && chars[i] == '[' {
20406                        // $array[idx]
20407                        i += 1;
20408                        let mut idx_str = String::new();
20409                        while i < chars.len() && chars[i] != ']' {
20410                            idx_str.push(chars[i]);
20411                            i += 1;
20412                        }
20413                        if i < chars.len() {
20414                            i += 1;
20415                        }
20416                        let idx_expr = if let Some(rest) = idx_str.strip_prefix('$') {
20417                            Expr {
20418                                kind: ExprKind::ScalarVar(rest.to_string()),
20419                                line,
20420                            }
20421                        } else if let Ok(n) = idx_str.parse::<i64>() {
20422                            Expr {
20423                                kind: ExprKind::Integer(n),
20424                                line,
20425                            }
20426                        } else {
20427                            Expr {
20428                                kind: ExprKind::String(idx_str),
20429                                line,
20430                            }
20431                        };
20432                        Expr {
20433                            kind: ExprKind::ArrayElement {
20434                                array: name,
20435                                index: Box::new(idx_expr),
20436                            },
20437                            line,
20438                        }
20439                    } else {
20440                        // Bare $name — defer to the chain-extension loop below.
20441                        Expr {
20442                            kind: ExprKind::ScalarVar(name),
20443                            line,
20444                        }
20445                    };
20446
20447                    // Chain `->[…]` / `->{…}` AND adjacent `[…]` / `{…}` — Perl
20448                    // implies `->` between consecutive subscripts (`$m[1][2]`
20449                    // ≡ `$m[1]->[2]`).  See `interp_chain_subscripts`.
20450                    base = self.interp_chain_subscripts(&chars, &mut i, base, line);
20451                    parts.push(StringPart::Expr(base));
20452                } else if chars[i].is_ascii_digit() {
20453                    // $0 (program name), $1…$n (regexp captures). Perl disallows $01, $02, …
20454                    if chars[i] == '0' {
20455                        i += 1;
20456                        if i < chars.len() && chars[i].is_ascii_digit() {
20457                            return Err(self.syntax_err(
20458                                "Numeric variables with more than one digit may not start with '0'",
20459                                line,
20460                            ));
20461                        }
20462                        parts.push(StringPart::ScalarVar("0".into()));
20463                    } else {
20464                        let start = i;
20465                        while i < chars.len() && chars[i].is_ascii_digit() {
20466                            i += 1;
20467                        }
20468                        parts.push(StringPart::ScalarVar(chars[start..i].iter().collect()));
20469                    }
20470                } else {
20471                    let c = chars[i];
20472                    let probe = c.to_string();
20473                    // `&` is the regex-match special var — semantically symmetric with
20474                    // backtick (`$``) prematch and apostrophe (`$'`) postmatch which
20475                    // are already handled here. `is_special_scalar_name_for_get` doesn't
20476                    // currently list `&`/`'`/`` ` `` (those have separate runtime paths
20477                    // for set/clear under regex updates), so we add them inline.
20478                    if VMHelper::is_special_scalar_name_for_get(&probe)
20479                        || matches!(c, '\'' | '`' | '&')
20480                    {
20481                        i += 1;
20482                        // Check for hash element access: `$+{key}`, `$-{key}`, etc.
20483                        if i < chars.len() && chars[i] == '{' {
20484                            i += 1; // skip {
20485                            let mut key = String::new();
20486                            let mut depth = 1;
20487                            while i < chars.len() && depth > 0 {
20488                                if chars[i] == '{' {
20489                                    depth += 1;
20490                                } else if chars[i] == '}' {
20491                                    depth -= 1;
20492                                    if depth == 0 {
20493                                        break;
20494                                    }
20495                                }
20496                                key.push(chars[i]);
20497                                i += 1;
20498                            }
20499                            if i < chars.len() {
20500                                i += 1;
20501                            } // skip }
20502                            let key_expr = if let Some(rest) = key.strip_prefix('$') {
20503                                Expr {
20504                                    kind: ExprKind::ScalarVar(rest.to_string()),
20505                                    line,
20506                                }
20507                            } else {
20508                                Expr {
20509                                    kind: ExprKind::String(key),
20510                                    line,
20511                                }
20512                            };
20513                            let mut base = Expr {
20514                                kind: ExprKind::HashElement {
20515                                    hash: probe,
20516                                    key: Box::new(key_expr),
20517                                },
20518                                line,
20519                            };
20520                            base = self.interp_chain_subscripts(&chars, &mut i, base, line);
20521                            parts.push(StringPart::Expr(base));
20522                        } else {
20523                            // Check for arrow deref chain: `$@->{key}`, etc.
20524                            let mut base = Expr {
20525                                kind: ExprKind::ScalarVar(probe),
20526                                line,
20527                            };
20528                            base = self.interp_chain_subscripts(&chars, &mut i, base, line);
20529                            if matches!(base.kind, ExprKind::ScalarVar(_)) {
20530                                // No chain extension — use the simpler ScalarVar part
20531                                if let ExprKind::ScalarVar(name) = base.kind {
20532                                    self.no_interop_check_scalar_var_name(&name, line)?;
20533                                    parts.push(StringPart::ScalarVar(name));
20534                                }
20535                            } else {
20536                                parts.push(StringPart::Expr(base));
20537                            }
20538                        }
20539                    } else {
20540                        literal.push('$');
20541                        literal.push(c);
20542                        i += 1;
20543                    }
20544                }
20545            } else if chars[i] == '@' && i + 1 < chars.len() {
20546                let next = chars[i + 1];
20547                // `@$aref` / `@${expr}` — array dereference in interpolation (Perl `"@$r"` → elements of @$r).
20548                if next == '$' {
20549                    if !literal.is_empty() {
20550                        parts.push(StringPart::Literal(std::mem::take(&mut literal)));
20551                    }
20552                    i += 1; // past `@`
20553                    debug_assert_eq!(chars[i], '$');
20554                    i += 1; // past `$`
20555                    while i < chars.len() && chars[i].is_whitespace() {
20556                        i += 1;
20557                    }
20558                    if i >= chars.len() {
20559                        return Err(self.syntax_err(
20560                            "Expected variable or block after `@$` in double-quoted string",
20561                            line,
20562                        ));
20563                    }
20564                    let inner_expr = if chars[i] == '{' {
20565                        i += 1;
20566                        let start = i;
20567                        let mut depth = 1usize;
20568                        while i < chars.len() && depth > 0 {
20569                            match chars[i] {
20570                                '{' => depth += 1,
20571                                '}' => {
20572                                    depth -= 1;
20573                                    if depth == 0 {
20574                                        break;
20575                                    }
20576                                }
20577                                _ => {}
20578                            }
20579                            i += 1;
20580                        }
20581                        if depth != 0 {
20582                            return Err(self.syntax_err(
20583                                "Unterminated `${ ... }` after `@` in double-quoted string",
20584                                line,
20585                            ));
20586                        }
20587                        let inner: String = chars[start..i].iter().collect();
20588                        i += 1; // closing `}`
20589                        parse_expression_from_str(inner.trim(), "-e")?
20590                    } else {
20591                        let mut name = String::new();
20592                        if chars[i] == '^' {
20593                            name.push('^');
20594                            i += 1;
20595                            while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_')
20596                            {
20597                                name.push(chars[i]);
20598                                i += 1;
20599                            }
20600                        } else {
20601                            while i < chars.len()
20602                                && (chars[i].is_alphanumeric()
20603                                    || chars[i] == '_'
20604                                    || chars[i] == ':')
20605                            {
20606                                name.push(chars[i]);
20607                                i += 1;
20608                            }
20609                            while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
20610                                name.push_str("::");
20611                                i += 2;
20612                                while i < chars.len()
20613                                    && (chars[i].is_alphanumeric() || chars[i] == '_')
20614                                {
20615                                    name.push(chars[i]);
20616                                    i += 1;
20617                                }
20618                            }
20619                        }
20620                        if name.is_empty() {
20621                            return Err(self.syntax_err(
20622                                "Expected identifier after `@$` in double-quoted string",
20623                                line,
20624                            ));
20625                        }
20626                        Expr {
20627                            kind: ExprKind::ScalarVar(name),
20628                            line,
20629                        }
20630                    };
20631                    parts.push(StringPart::Expr(Expr {
20632                        kind: ExprKind::Deref {
20633                            expr: Box::new(inner_expr),
20634                            kind: Sigil::Array,
20635                        },
20636                        line,
20637                    }));
20638                    continue 'istr;
20639                }
20640                if next == '{' {
20641                    if !literal.is_empty() {
20642                        parts.push(StringPart::Literal(std::mem::take(&mut literal)));
20643                    }
20644                    i += 2; // `@{`
20645                    let (inner, close) =
20646                        Self::scan_interp_braces(&chars, i).ok_or_else(|| {
20647                            self.syntax_err("Unterminated @{ ... } in double-quoted string", line)
20648                        })?;
20649                    i = close + 1; // past closing `}`
20650                    let inner_expr = parse_expression_from_str(inner.trim(), "-e")?;
20651                    parts.push(StringPart::Expr(Expr {
20652                        kind: ExprKind::Deref {
20653                            expr: Box::new(inner_expr),
20654                            kind: Sigil::Array,
20655                        },
20656                        line,
20657                    }));
20658                    continue 'istr;
20659                }
20660                if !(next.is_alphabetic() || next == '_' || next == '+' || next == '-') {
20661                    literal.push(chars[i]);
20662                    i += 1;
20663                } else {
20664                    if !literal.is_empty() {
20665                        parts.push(StringPart::Literal(std::mem::take(&mut literal)));
20666                    }
20667                    i += 1;
20668                    let mut name = String::new();
20669                    if i < chars.len() && (chars[i] == '+' || chars[i] == '-') {
20670                        name.push(chars[i]);
20671                        i += 1;
20672                    } else {
20673                        while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
20674                            name.push(chars[i]);
20675                            i += 1;
20676                        }
20677                        while i + 1 < chars.len() && chars[i] == ':' && chars[i + 1] == ':' {
20678                            name.push_str("::");
20679                            i += 2;
20680                            while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_')
20681                            {
20682                                name.push(chars[i]);
20683                                i += 1;
20684                            }
20685                        }
20686                    }
20687                    if i < chars.len() && chars[i] == '[' {
20688                        i += 1;
20689                        let start_inner = i;
20690                        let mut depth = 1usize;
20691                        while i < chars.len() && depth > 0 {
20692                            match chars[i] {
20693                                '[' => depth += 1,
20694                                ']' => depth -= 1,
20695                                _ => {}
20696                            }
20697                            if depth == 0 {
20698                                let inner: String = chars[start_inner..i].iter().collect();
20699                                i += 1; // closing ]
20700                                let indices = parse_slice_indices_from_str(inner.trim(), "-e")?;
20701                                parts.push(StringPart::Expr(Expr {
20702                                    kind: ExprKind::ArraySlice {
20703                                        array: name.clone(),
20704                                        indices,
20705                                    },
20706                                    line,
20707                                }));
20708                                continue 'istr;
20709                            }
20710                            i += 1;
20711                        }
20712                        return Err(self.syntax_err(
20713                            "Unterminated [ in array slice inside quoted string",
20714                            line,
20715                        ));
20716                    }
20717                    parts.push(StringPart::ArrayVar(name));
20718                }
20719            } else if chars[i] == '#'
20720                && i + 1 < chars.len()
20721                && chars[i + 1] == '{'
20722                && !crate::compat_mode()
20723            {
20724                // #{expr} — Ruby-style expression interpolation (stryke extension).
20725                if !literal.is_empty() {
20726                    parts.push(StringPart::Literal(std::mem::take(&mut literal)));
20727                }
20728                i += 2; // skip `#{`
20729                let inner = match Self::scan_interp_braces(&chars, i) {
20730                    Some((inner, close)) => {
20731                        i = close + 1; // past closing `}`
20732                        inner
20733                    }
20734                    None => {
20735                        // Unterminated: consume the rest (historical behavior).
20736                        let rest: String = chars[i..].iter().collect();
20737                        i = chars.len();
20738                        rest
20739                    }
20740                };
20741                let expr = parse_block_from_str(inner.trim(), "-e", line)?;
20742                parts.push(StringPart::Expr(expr));
20743            } else {
20744                literal.push(chars[i]);
20745                i += 1;
20746            }
20747        }
20748        if !literal.is_empty() {
20749            parts.push(StringPart::Literal(literal));
20750        }
20751
20752        if parts.len() == 1 {
20753            if let StringPart::Literal(s) = &parts[0] {
20754                return Ok(Expr {
20755                    kind: ExprKind::String(s.clone()),
20756                    line,
20757                });
20758            }
20759        }
20760        if parts.is_empty() {
20761            return Ok(Expr {
20762                kind: ExprKind::String(String::new()),
20763                line,
20764            });
20765        }
20766
20767        Ok(Expr {
20768            kind: ExprKind::InterpolatedString(parts),
20769            line,
20770        })
20771    }
20772
20773    fn expr_to_overload_key(&self, e: &Expr) -> StrykeResult<String> {
20774        match &e.kind {
20775            ExprKind::String(s) => Ok(s.clone()),
20776            _ => Err(self.syntax_err(
20777                "overload key must be a string literal (e.g. '\"\"' or '+')",
20778                e.line,
20779            )),
20780        }
20781    }
20782
20783    fn expr_to_overload_sub(&mut self, e: &Expr) -> StrykeResult<String> {
20784        match &e.kind {
20785            ExprKind::String(s) => Ok(s.clone()),
20786            ExprKind::Integer(n) => Ok(n.to_string()),
20787            ExprKind::SubroutineRef(s) | ExprKind::SubroutineCodeRef(s) => Ok(s.clone()),
20788            // Anonymous sub: `use overload "+" => sub { ... };` — promote the
20789            // anon body into a synthetic top-level SubDecl so the overload
20790            // table can hold the name like the named-sub case. (PARITY-012)
20791            ExprKind::CodeRef { params, body } => {
20792                let id = self.next_overload_anon_id;
20793                self.next_overload_anon_id = self.next_overload_anon_id.saturating_add(1);
20794                let name = format!("__overload_anon_{}", id);
20795                self.pending_synthetic_subs.push(Statement {
20796                    label: None,
20797                    kind: StmtKind::SubDecl {
20798                        name: name.clone(),
20799                        params: params.clone(),
20800                        body: body.clone(),
20801                        prototype: None,
20802                    },
20803                    line: e.line,
20804                });
20805                Ok(name)
20806            }
20807            _ => Err(self.syntax_err(
20808                "overload handler must be a string literal, number (e.g. fallback => 1), or \\&subname (method in current package)",
20809                e.line,
20810            )),
20811        }
20812    }
20813}
20814
20815/// Stringify a Perl-style `use Module VERSION` float pin so the resolver
20816/// gets the literal the user typed (`"2.13"`), not an f64-roundtrip
20817/// `"2.13000000000000034"`. We only have the parsed `f64` here — the
20818/// lexer collapses the source slice into `Token::Float(v)` — so format
20819/// shortest round-trippable decimal via Rust's `{}`. For `2.13` /
20820/// `0.20` / `1.0` this matches the source; for genuinely-imprecise
20821/// f64s it gives the nearest representation, which still resolves
20822/// store dirs whose names were generated through the same path.
20823fn format_version_float(v: f64) -> String {
20824    let s = format!("{}", v);
20825    // Trailing `.0` on whole numbers — `1.0` should stay `1.0` so a
20826    // store entry `pkg@1.0/` matches, but `2` should not become `2.0`.
20827    // Rust's default formatter already preserves the `.0` for floats
20828    // (`format!("{}", 1.0_f64)` → `"1"` on stable, hence the explicit
20829    // re-add). Without this, `use Foo 1.0` would pin to `Foo@1` which
20830    // never matches `Foo@1.0/` on disk.
20831    if !s.contains('.') && !s.contains('e') && !s.contains('E') {
20832        return format!("{}.0", s);
20833    }
20834    s
20835}
20836
20837fn merge_expr_list(parts: Vec<Expr>) -> Expr {
20838    if parts.len() == 1 {
20839        parts.into_iter().next().unwrap()
20840    } else {
20841        let line = parts.first().map(|e| e.line).unwrap_or(0);
20842        Expr {
20843            kind: ExprKind::List(parts),
20844            line,
20845        }
20846    }
20847}
20848
20849/// Parse a single expression from `s` (e.g. contents of `@{ ... }` inside a double-quoted string).
20850pub fn parse_expression_from_str(s: &str, file: &str) -> StrykeResult<Expr> {
20851    let mut lexer = Lexer::new_with_file(s, file);
20852    let tokens = lexer.tokenize()?;
20853    let mut parser = Parser::new_with_file(tokens, file);
20854    let e = parser.parse_expression()?;
20855    if !parser.at_eof() {
20856        return Err(parser.syntax_err(
20857            "Extra tokens in embedded string expression",
20858            parser.peek_line(),
20859        ));
20860    }
20861    Ok(e)
20862}
20863
20864/// Parse a statement list from `s` and wrap as `do { ... }` (for `#{...}` interpolation).
20865pub fn parse_block_from_str(s: &str, file: &str, line: usize) -> StrykeResult<Expr> {
20866    let mut lexer = Lexer::new_with_file(s, file);
20867    let tokens = lexer.tokenize()?;
20868    let mut parser = Parser::new_with_file(tokens, file);
20869    let stmts = parser.parse_statements()?;
20870    let inner_line = stmts.first().map(|st| st.line).unwrap_or(line);
20871    let inner = Expr {
20872        kind: ExprKind::CodeRef {
20873            params: vec![],
20874            body: stmts,
20875        },
20876        line: inner_line,
20877    };
20878    Ok(Expr {
20879        kind: ExprKind::Do(Box::new(inner)),
20880        line,
20881    })
20882}
20883
20884/// Comma-separated expressions on a `format` value line (below a picture line).
20885/// Parse `[ ... ]` contents for `@a[...]` (same rules as `parse_arg_list` / comma-separated indices).
20886pub fn parse_slice_indices_from_str(s: &str, file: &str) -> StrykeResult<Vec<Expr>> {
20887    let mut lexer = Lexer::new_with_file(s, file);
20888    let tokens = lexer.tokenize()?;
20889    let mut parser = Parser::new_with_file(tokens, file);
20890    parser.parse_arg_list()
20891}
20892/// `parse_format_value_line` — see implementation.
20893pub fn parse_format_value_line(line: &str) -> StrykeResult<Vec<Expr>> {
20894    let trimmed = line.trim();
20895    if trimmed.is_empty() {
20896        return Ok(vec![]);
20897    }
20898    let mut lexer = Lexer::new(trimmed);
20899    let tokens = lexer.tokenize()?;
20900    let mut parser = Parser::new(tokens);
20901    let mut exprs = Vec::new();
20902    loop {
20903        if parser.at_eof() {
20904            break;
20905        }
20906        // Assignment-level expressions so `a, b` yields two fields (not one comma list).
20907        exprs.push(parser.parse_assign_expr()?);
20908        if parser.eat(&Token::Comma) {
20909            continue;
20910        }
20911        if !parser.at_eof() {
20912            return Err(parser.syntax_err("Extra tokens in format value line", parser.peek_line()));
20913        }
20914        break;
20915    }
20916    Ok(exprs)
20917}
20918
20919#[cfg(test)]
20920mod tests {
20921    use super::*;
20922
20923    fn parse_ok(code: &str) -> Program {
20924        let mut lexer = Lexer::new(code);
20925        let tokens = lexer.tokenize().expect("tokenize");
20926        let mut parser = Parser::new(tokens);
20927        parser.parse_program().expect("parse")
20928    }
20929
20930    fn parse_err(code: &str) -> String {
20931        let mut lexer = Lexer::new(code);
20932        let tokens = match lexer.tokenize() {
20933            Ok(t) => t,
20934            Err(e) => return e.message,
20935        };
20936        let mut parser = Parser::new(tokens);
20937        parser.parse_program().unwrap_err().message
20938    }
20939
20940    #[test]
20941    fn parse_empty_program() {
20942        let p = parse_ok("");
20943        assert!(p.statements.is_empty());
20944    }
20945
20946    #[test]
20947    fn parse_semicolons_only() {
20948        let p = parse_ok(";;");
20949        assert!(p.statements.len() <= 3);
20950    }
20951
20952    #[test]
20953    fn parse_simple_scalar_assignment() {
20954        let p = parse_ok("$x = 1");
20955        assert_eq!(p.statements.len(), 1);
20956    }
20957
20958    #[test]
20959    fn parse_simple_array_assignment() {
20960        let p = parse_ok("@arr = (1, 2, 3)");
20961        assert_eq!(p.statements.len(), 1);
20962    }
20963
20964    #[test]
20965    fn parse_simple_hash_assignment() {
20966        let p = parse_ok("%h = (a => 1, b => 2)");
20967        assert_eq!(p.statements.len(), 1);
20968    }
20969
20970    #[test]
20971    fn parse_subroutine_decl() {
20972        let p = parse_ok("fn foo { 1 }");
20973        assert_eq!(p.statements.len(), 1);
20974        match &p.statements[0].kind {
20975            StmtKind::SubDecl { name, .. } => assert_eq!(name, "foo"),
20976            _ => panic!("expected SubDecl"),
20977        }
20978    }
20979
20980    #[test]
20981    fn parse_class_method_expr_body_shorthand() {
20982        let p = parse_ok("class X { fn adg = \"\" }");
20983        match &p.statements[0].kind {
20984            StmtKind::ClassDecl { def } => {
20985                let m = def.method("adg").expect("adg method");
20986                let body = m.body.as_ref().expect("body");
20987                assert_eq!(body.len(), 1);
20988                match &body[0].kind {
20989                    StmtKind::Expression(e) => match &e.kind {
20990                        ExprKind::String(s) => assert!(s.is_empty()),
20991                        _ => panic!("expected string expr, got {:?}", e.kind),
20992                    },
20993                    _ => panic!("expected expression stmt"),
20994                }
20995            }
20996            _ => panic!("expected ClassDecl"),
20997        }
20998    }
20999
21000    #[test]
21001    fn parse_named_fn_eq_shorthand_with_sig() {
21002        let p = parse_ok("fn add_one($x) = $x + 1");
21003        match &p.statements[0].kind {
21004            StmtKind::SubDecl {
21005                name, params, body, ..
21006            } => {
21007                assert_eq!(name, "add_one");
21008                assert_eq!(params.len(), 1);
21009                assert_eq!(body.len(), 1);
21010            }
21011            _ => panic!("expected SubDecl"),
21012        }
21013    }
21014
21015    #[test]
21016    fn parse_anon_fn_eq_shorthand_with_sig() {
21017        let p = parse_ok("my $f = fn($x) = 23");
21018        match &p.statements[0].kind {
21019            StmtKind::My(decls) => {
21020                let init = decls[0].initializer.as_ref().expect("initializer");
21021                match &init.kind {
21022                    ExprKind::CodeRef { params, body } => {
21023                        assert_eq!(params.len(), 1);
21024                        assert_eq!(body.len(), 1);
21025                    }
21026                    _ => panic!("expected CodeRef"),
21027                }
21028            }
21029            _ => panic!("expected My"),
21030        }
21031    }
21032
21033    #[test]
21034    fn parse_struct_method_eq_shorthand() {
21035        let p = parse_ok("struct S { fn double($a) = $a * 2 }");
21036        match &p.statements[0].kind {
21037            StmtKind::StructDecl { def } => {
21038                assert_eq!(def.methods.len(), 1);
21039                assert_eq!(def.methods[0].name, "double");
21040                assert_eq!(def.methods[0].body.len(), 1);
21041            }
21042            _ => panic!("expected StructDecl"),
21043        }
21044    }
21045
21046    #[test]
21047    fn parse_trait_method_eq_shorthand() {
21048        let p = parse_ok("trait T { fn k = 0 }");
21049        match &p.statements[0].kind {
21050            StmtKind::TraitDecl { def } => {
21051                let m = def.method("k").expect("k");
21052                let body = m.body.as_ref().expect("default body");
21053                assert_eq!(body.len(), 1);
21054            }
21055            _ => panic!("expected TraitDecl"),
21056        }
21057    }
21058
21059    #[test]
21060    fn parse_fn_eq_shorthand_rejects_top_level_comma() {
21061        let msg = parse_err("fn z = 1, 2");
21062        assert!(
21063            msg.contains("single expression") || msg.contains("comma"),
21064            "{}",
21065            msg
21066        );
21067    }
21068
21069    #[test]
21070    fn parse_subroutine_with_prototype() {
21071        let p = parse_ok("fn foo ($$) { 1 }");
21072        assert_eq!(p.statements.len(), 1);
21073        match &p.statements[0].kind {
21074            StmtKind::SubDecl { prototype, .. } => {
21075                assert!(prototype.is_some());
21076            }
21077            _ => panic!("expected SubDecl"),
21078        }
21079    }
21080
21081    #[test]
21082    fn parse_anonymous_fn() {
21083        let p = parse_ok("my $f = fn { 1 }");
21084        assert_eq!(p.statements.len(), 1);
21085    }
21086
21087    #[test]
21088    fn parse_if_statement() {
21089        let p = parse_ok("if (1) { 2 }");
21090        assert_eq!(p.statements.len(), 1);
21091        matches!(&p.statements[0].kind, StmtKind::If { .. });
21092    }
21093
21094    #[test]
21095    fn parse_if_elsif_else() {
21096        let p = parse_ok("if (0) { 1 } elsif (1) { 2 } else { 3 }");
21097        assert_eq!(p.statements.len(), 1);
21098    }
21099
21100    #[test]
21101    fn parse_unless_statement() {
21102        let p = parse_ok("unless (0) { 1 }");
21103        assert_eq!(p.statements.len(), 1);
21104    }
21105
21106    #[test]
21107    fn parse_while_loop() {
21108        let p = parse_ok("while ($x) { $x-- }");
21109        assert_eq!(p.statements.len(), 1);
21110    }
21111
21112    #[test]
21113    fn parse_until_loop() {
21114        let p = parse_ok("until ($x) { $x++ }");
21115        assert_eq!(p.statements.len(), 1);
21116    }
21117
21118    #[test]
21119    fn parse_for_c_style() {
21120        let p = parse_ok("for (my $i=0; $i<10; $i++) { 1 }");
21121        assert_eq!(p.statements.len(), 1);
21122    }
21123
21124    #[test]
21125    fn parse_foreach_loop() {
21126        let p = parse_ok("foreach my $x (@arr) { 1 }");
21127        assert_eq!(p.statements.len(), 1);
21128    }
21129
21130    #[test]
21131    fn parse_loop_with_label() {
21132        let p = parse_ok("OUTER: for my $i (1..10) { last OUTER }");
21133        assert_eq!(p.statements.len(), 1);
21134        assert_eq!(p.statements[0].label.as_deref(), Some("OUTER"));
21135    }
21136
21137    #[test]
21138    fn parse_begin_block() {
21139        let p = parse_ok("BEGIN { 1 }");
21140        assert_eq!(p.statements.len(), 1);
21141        matches!(&p.statements[0].kind, StmtKind::Begin(_));
21142    }
21143
21144    #[test]
21145    fn parse_end_block() {
21146        let p = parse_ok("END { 1 }");
21147        assert_eq!(p.statements.len(), 1);
21148        matches!(&p.statements[0].kind, StmtKind::End(_));
21149    }
21150
21151    #[test]
21152    fn parse_package_statement() {
21153        let p = parse_ok("package Foo::Bar");
21154        assert_eq!(p.statements.len(), 1);
21155        match &p.statements[0].kind {
21156            StmtKind::Package { name } => assert_eq!(name, "Foo::Bar"),
21157            _ => panic!("expected Package"),
21158        }
21159    }
21160
21161    #[test]
21162    fn parse_use_statement() {
21163        let p = parse_ok("use strict");
21164        assert_eq!(p.statements.len(), 1);
21165    }
21166
21167    /// `use Module VERSION` — Perl-style version pin captures the
21168    /// numeric first arg as `StmtKind::Use::version`, not an import.
21169    #[test]
21170    fn parse_use_module_version_pin() {
21171        let p = parse_ok("use Some::Module 2.13");
21172        assert_eq!(p.statements.len(), 1);
21173        match &p.statements[0].kind {
21174            StmtKind::Use { module, imports, version } => {
21175                assert_eq!(module, "Some::Module");
21176                assert!(imports.is_empty(), "version must not leak into imports");
21177                assert_eq!(version.as_deref(), Some("2.13"));
21178            }
21179            other => panic!("expected Use, got {:?}", other),
21180        }
21181    }
21182
21183    /// `use Module VERSION, LIST` — version pin AND import list both
21184    /// captured. Comma between the two is consumed by parse_use.
21185    /// `parse_expression` consumes its own comma-list, so the imports
21186    /// vec ends up with a single list-expr — the point of this test is
21187    /// the version pin survives the LIST tail, not the import shape.
21188    #[test]
21189    fn parse_use_module_version_then_imports() {
21190        let p = parse_ok("use Some::Module 1.0, 'foo', 'bar'");
21191        assert_eq!(p.statements.len(), 1);
21192        match &p.statements[0].kind {
21193            StmtKind::Use { module, imports, version } => {
21194                assert_eq!(module, "Some::Module");
21195                assert_eq!(version.as_deref(), Some("1.0"));
21196                assert!(!imports.is_empty(), "import LIST must reach the imports vec");
21197            }
21198            other => panic!("expected Use, got {:?}", other),
21199        }
21200    }
21201
21202    /// Plain `use Foo` — no version, no imports. version field is None.
21203    #[test]
21204    fn parse_use_module_no_version() {
21205        let p = parse_ok("use Foo");
21206        match &p.statements[0].kind {
21207            StmtKind::Use { version, .. } => {
21208                assert_eq!(version.as_deref(), None);
21209            }
21210            other => panic!("expected Use, got {:?}", other),
21211        }
21212    }
21213
21214    /// `use Foo 1.0.0` — three-segment semver doesn't parse today
21215    /// because the lexer treats `1.0` as a `Float`, then chokes on
21216    /// the next `.0`. This test pins the current limitation so a
21217    /// future parser change that adds v-string or string-literal
21218    /// pin support updates the test alongside. Workaround for
21219    /// users today: pin to a 2-segment version (`use Foo 1.0`) or
21220    /// rely on the lockfile / highest-installed scan.
21221    #[test]
21222    fn parse_use_module_three_segment_version_rejected() {
21223        let parsed = std::panic::catch_unwind(|| parse_ok("use Foo 1.0.0"));
21224        assert!(
21225            parsed.is_err(),
21226            "3-segment version pins are NOT supported today; if this passes \
21227             the parser was extended — update the test (and probably the \
21228             resolver's pin-arm too)"
21229        );
21230    }
21231
21232    /// `use Foo "1.0.0"` parses cleanly but the string becomes an
21233    /// IMPORT, not a version pin. Documents the current Perl 5
21234    /// semantics — Perl's `use Module VERSION LIST` grammar only
21235    /// recognises a numeric VERSION; a quoted string in that slot
21236    /// is treated as part of the import list. Pinned in case a
21237    /// future change conflates the two.
21238    #[test]
21239    fn parse_use_module_string_literal_is_import_not_version() {
21240        let p = parse_ok(r#"use Foo "1.0.0""#);
21241        match &p.statements[0].kind {
21242            StmtKind::Use { version, imports, .. } => {
21243                assert_eq!(version.as_deref(), None, "string literal must not become version pin");
21244                assert!(!imports.is_empty(), "string literal must reach the import list");
21245            }
21246            other => panic!("expected Use, got {:?}", other),
21247        }
21248    }
21249
21250    #[test]
21251    fn parse_no_statement() {
21252        let p = parse_ok("no warnings");
21253        assert_eq!(p.statements.len(), 1);
21254    }
21255
21256    #[test]
21257    fn parse_require_bareword() {
21258        let p = parse_ok("require Foo::Bar");
21259        assert_eq!(p.statements.len(), 1);
21260    }
21261
21262    #[test]
21263    fn parse_require_string() {
21264        let p = parse_ok(r#"require "foo.pl""#);
21265        assert_eq!(p.statements.len(), 1);
21266    }
21267
21268    #[test]
21269    fn parse_eval_block() {
21270        let p = parse_ok("eval { 1 }");
21271        assert_eq!(p.statements.len(), 1);
21272    }
21273
21274    #[test]
21275    fn parse_eval_string() {
21276        let p = parse_ok(r#"eval "1 + 2""#);
21277        assert_eq!(p.statements.len(), 1);
21278    }
21279
21280    #[test]
21281    fn parse_qw_word_list() {
21282        let p = parse_ok("my @a = qw(foo bar baz)");
21283        assert_eq!(p.statements.len(), 1);
21284    }
21285
21286    #[test]
21287    fn parse_q_string() {
21288        let p = parse_ok("my $s = q{hello}");
21289        assert_eq!(p.statements.len(), 1);
21290    }
21291
21292    #[test]
21293    fn parse_qq_string() {
21294        let p = parse_ok(r#"my $s = qq(hello $x)"#);
21295        assert_eq!(p.statements.len(), 1);
21296    }
21297
21298    #[test]
21299    fn parse_regex_match() {
21300        let p = parse_ok(r#"$x =~ /foo/"#);
21301        assert_eq!(p.statements.len(), 1);
21302    }
21303
21304    #[test]
21305    fn parse_regex_substitution() {
21306        let p = parse_ok(r#"$x =~ s/foo/bar/g"#);
21307        assert_eq!(p.statements.len(), 1);
21308    }
21309
21310    #[test]
21311    fn parse_transliterate() {
21312        let p = parse_ok(r#"$x =~ tr/a-z/A-Z/"#);
21313        assert_eq!(p.statements.len(), 1);
21314    }
21315
21316    #[test]
21317    fn parse_ternary_operator() {
21318        let p = parse_ok("my $x = $a ? 1 : 2");
21319        assert_eq!(p.statements.len(), 1);
21320    }
21321
21322    #[test]
21323    fn parse_arrow_method_call() {
21324        let p = parse_ok("$obj->method()");
21325        assert_eq!(p.statements.len(), 1);
21326    }
21327
21328    #[test]
21329    fn parse_arrow_deref_hash() {
21330        let p = parse_ok("$r->{key}");
21331        assert_eq!(p.statements.len(), 1);
21332    }
21333
21334    #[test]
21335    fn parse_arrow_deref_array() {
21336        let p = parse_ok("$r->[0]");
21337        assert_eq!(p.statements.len(), 1);
21338    }
21339
21340    #[test]
21341    fn parse_chained_arrow_deref() {
21342        let p = parse_ok("$r->{a}[0]{b}");
21343        assert_eq!(p.statements.len(), 1);
21344    }
21345
21346    #[test]
21347    fn parse_my_multiple_vars() {
21348        let p = parse_ok("my ($a, $b, $c) = (1, 2, 3)");
21349        assert_eq!(p.statements.len(), 1);
21350    }
21351
21352    #[test]
21353    fn parse_our_scalar() {
21354        let p = parse_ok("our $VERSION = '1.0'");
21355        assert_eq!(p.statements.len(), 1);
21356    }
21357
21358    #[test]
21359    fn parse_local_scalar() {
21360        let p = parse_ok("local $/ = undef");
21361        assert_eq!(p.statements.len(), 1);
21362    }
21363
21364    #[test]
21365    fn parse_state_variable() {
21366        let p = parse_ok("fn Test::counter { state $n = 0; $n++ }");
21367        assert_eq!(p.statements.len(), 1);
21368    }
21369
21370    #[test]
21371    fn parse_postfix_if() {
21372        let p = parse_ok("print 1 if $x");
21373        assert_eq!(p.statements.len(), 1);
21374    }
21375
21376    #[test]
21377    fn parse_postfix_unless() {
21378        let p = parse_ok("die 'error' unless $ok");
21379        assert_eq!(p.statements.len(), 1);
21380    }
21381
21382    #[test]
21383    fn parse_postfix_while() {
21384        let p = parse_ok("$x++ while $x < 10");
21385        assert_eq!(p.statements.len(), 1);
21386    }
21387
21388    #[test]
21389    fn parse_postfix_for() {
21390        let p = parse_ok("print for @arr");
21391        assert_eq!(p.statements.len(), 1);
21392    }
21393
21394    #[test]
21395    fn parse_last_next_redo() {
21396        let p = parse_ok("for (@a) { next if $_ < 0; last if $_ > 10 }");
21397        assert_eq!(p.statements.len(), 1);
21398    }
21399
21400    #[test]
21401    fn parse_return_statement() {
21402        let p = parse_ok("fn foo { return 42 }");
21403        assert_eq!(p.statements.len(), 1);
21404    }
21405
21406    #[test]
21407    fn parse_wantarray() {
21408        let p = parse_ok("fn foo { wantarray ? @a : $a }");
21409        assert_eq!(p.statements.len(), 1);
21410    }
21411
21412    #[test]
21413    fn parse_caller_builtin() {
21414        let p = parse_ok("my @c = caller");
21415        assert_eq!(p.statements.len(), 1);
21416    }
21417
21418    #[test]
21419    fn parse_ref_to_array() {
21420        let p = parse_ok("my $r = \\@arr");
21421        assert_eq!(p.statements.len(), 1);
21422    }
21423
21424    #[test]
21425    fn parse_ref_to_hash() {
21426        let p = parse_ok("my $r = \\%hash");
21427        assert_eq!(p.statements.len(), 1);
21428    }
21429
21430    #[test]
21431    fn parse_ref_to_scalar() {
21432        let p = parse_ok("my $r = \\$x");
21433        assert_eq!(p.statements.len(), 1);
21434    }
21435
21436    #[test]
21437    fn parse_deref_scalar() {
21438        let p = parse_ok("my $v = $$r");
21439        assert_eq!(p.statements.len(), 1);
21440    }
21441
21442    #[test]
21443    fn parse_deref_array() {
21444        let p = parse_ok("my @a = @$r");
21445        assert_eq!(p.statements.len(), 1);
21446    }
21447
21448    #[test]
21449    fn parse_deref_hash() {
21450        let p = parse_ok("my %h = %$r");
21451        assert_eq!(p.statements.len(), 1);
21452    }
21453
21454    #[test]
21455    fn parse_blessed_ref() {
21456        let p = parse_ok("bless $r, 'Foo'");
21457        assert_eq!(p.statements.len(), 1);
21458    }
21459
21460    #[test]
21461    fn parse_heredoc_basic() {
21462        let p = parse_ok("my $s = <<END;\nfoo\nEND");
21463        assert_eq!(p.statements.len(), 1);
21464    }
21465
21466    #[test]
21467    fn parse_heredoc_quoted() {
21468        let p = parse_ok("my $s = <<'END';\nfoo\nEND");
21469        assert_eq!(p.statements.len(), 1);
21470    }
21471
21472    #[test]
21473    fn parse_do_block() {
21474        let p = parse_ok("my $x = do { 1 + 2 }");
21475        assert_eq!(p.statements.len(), 1);
21476    }
21477
21478    #[test]
21479    fn parse_do_file() {
21480        let p = parse_ok(r#"do "foo.pl""#);
21481        assert_eq!(p.statements.len(), 1);
21482    }
21483
21484    #[test]
21485    fn parse_map_expression() {
21486        let p = parse_ok("my @b = map { $_ * 2 } @a");
21487        assert_eq!(p.statements.len(), 1);
21488    }
21489
21490    /// `on $cluster () …` must keep `()` as SOURCE (empty list), not postfix
21491    /// indirect `($cluster)()` which leaves `map` as SOURCE and breaks parsing.
21492    #[test]
21493    fn parse_dist_thread_on_scalar_empty_list_source() {
21494        let p = parse_ok("~d> on $c () map { _ * 2 }");
21495        assert_eq!(p.statements.len(), 1);
21496        let StmtKind::Expression(root) = &p.statements[0].kind else {
21497            panic!("expected Expression statement");
21498        };
21499        let ExprKind::DistReduceExpr { cluster, list, .. } = &root.kind else {
21500            panic!("expected DistReduceExpr, got {:?}", root.kind);
21501        };
21502        assert!(
21503            matches!(cluster.kind, ExprKind::ScalarVar(ref s) if s == "c"),
21504            "expected cluster $c, got {:?}",
21505            cluster.kind
21506        );
21507        assert!(
21508            matches!(list.kind, ExprKind::List(ref v) if v.is_empty()),
21509            "expected empty list source, got {:?}",
21510            list.kind
21511        );
21512    }
21513
21514    #[test]
21515    fn parse_grep_expression() {
21516        let p = parse_ok("my @b = grep { $_ > 0 } @a");
21517        assert_eq!(p.statements.len(), 1);
21518    }
21519
21520    #[test]
21521    fn parse_sort_expression() {
21522        let p = parse_ok("my @b = sort { $a <=> $b } @a");
21523        assert_eq!(p.statements.len(), 1);
21524    }
21525
21526    #[test]
21527    fn pipe_sort_does_not_swallow_next_my_decl() {
21528        // Regression: bare `|> sort` followed by `\n my $x = ...` used
21529        // to eat the next stmt as sort's argument list. After the fix,
21530        // both statements must appear in the AST.
21531        let p = parse_ok("my @s = @data |> sort\nmy $j = join(\",\", @s)");
21532        assert_eq!(
21533            p.statements.len(),
21534            2,
21535            "expected 2 stmts (sort + join decl), got {}: {:?}",
21536            p.statements.len(),
21537            p.statements
21538                .iter()
21539                .map(|s| format!("{:?}", s.kind).chars().take(60).collect::<String>())
21540                .collect::<Vec<_>>(),
21541        );
21542    }
21543
21544    #[test]
21545    fn pipe_sort_multiline_pipeline_preserves_next_decl() {
21546        // Same shape but with maps/grep stages between the source and
21547        // `sort` — mirrors the original `test_oop_inventory_threaded_pin`
21548        // bug fixture.
21549        let p = parse_ok(
21550            "my @bk = @{$inv->by_cat(\"bakery\")} |> maps { _->label() } |> sort\nmy $j = join(\"|\", @bk)",
21551        );
21552        assert_eq!(p.statements.len(), 2);
21553    }
21554
21555    #[test]
21556    fn parse_pipe_forward() {
21557        let p = parse_ok("@a |> map { $_ * 2 }");
21558        assert_eq!(p.statements.len(), 1);
21559    }
21560
21561    #[test]
21562    fn parse_expression_from_str_simple() {
21563        let e = parse_expression_from_str("$x + 1", "-e").unwrap();
21564        assert!(matches!(e.kind, ExprKind::BinOp { .. }));
21565    }
21566
21567    #[test]
21568    fn parse_expression_from_str_extra_tokens_error() {
21569        let err = parse_expression_from_str("$x; $y", "-e").unwrap_err();
21570        assert!(err.message.contains("Extra tokens"));
21571    }
21572
21573    #[test]
21574    fn parse_slice_indices_from_str_basic() {
21575        let indices = parse_slice_indices_from_str("0, 1, 2", "-e").unwrap();
21576        assert_eq!(indices.len(), 3);
21577    }
21578
21579    #[test]
21580    fn parse_format_value_line_empty() {
21581        let exprs = parse_format_value_line("").unwrap();
21582        assert!(exprs.is_empty());
21583    }
21584
21585    #[test]
21586    fn parse_format_value_line_single() {
21587        let exprs = parse_format_value_line("$x").unwrap();
21588        assert_eq!(exprs.len(), 1);
21589    }
21590
21591    #[test]
21592    fn parse_format_value_line_multiple() {
21593        let exprs = parse_format_value_line("$a, $b, $c").unwrap();
21594        assert_eq!(exprs.len(), 3);
21595    }
21596
21597    #[test]
21598    fn parse_unclosed_brace_error() {
21599        let err = parse_err("fn foo {");
21600        assert!(!err.is_empty());
21601    }
21602
21603    #[test]
21604    fn parse_unclosed_paren_error() {
21605        let err = parse_err("print (1, 2");
21606        assert!(!err.is_empty());
21607    }
21608
21609    #[test]
21610    fn parse_invalid_statement_error() {
21611        let err = parse_err("???");
21612        assert!(!err.is_empty());
21613    }
21614
21615    #[test]
21616    fn merge_expr_list_single() {
21617        let e = Expr {
21618            kind: ExprKind::Integer(1),
21619            line: 1,
21620        };
21621        let merged = merge_expr_list(vec![e.clone()]);
21622        matches!(merged.kind, ExprKind::Integer(1));
21623    }
21624
21625    #[test]
21626    fn merge_expr_list_multiple() {
21627        let e1 = Expr {
21628            kind: ExprKind::Integer(1),
21629            line: 1,
21630        };
21631        let e2 = Expr {
21632            kind: ExprKind::Integer(2),
21633            line: 1,
21634        };
21635        let merged = merge_expr_list(vec![e1, e2]);
21636        matches!(merged.kind, ExprKind::List(_));
21637    }
21638
21639    // ── --no-interop strict-mode rejections ─────────────────────────────
21640    //
21641    // `--no-interop` is the bot firewall: it rejects Perl 5 idioms the
21642    // parser would otherwise accept, forcing stryke-only spellings. Each
21643    // of these pins one rejection rule so a later refactor can't silently
21644    // accept the un-idiomatic form. We RAII the TLS flag so sibling tests
21645    // running in parallel don't see the override.
21646
21647    struct NoInteropGuard {
21648        saved: Option<bool>,
21649    }
21650    impl NoInteropGuard {
21651        fn on() -> Self {
21652            let saved = crate::no_interop_mode_tls();
21653            crate::set_no_interop_mode_tls(Some(true));
21654            Self { saved }
21655        }
21656    }
21657    impl Drop for NoInteropGuard {
21658        fn drop(&mut self) {
21659            crate::set_no_interop_mode_tls(self.saved);
21660        }
21661    }
21662
21663    #[test]
21664    fn no_interop_rejects_sub_keyword() {
21665        let _g = NoInteropGuard::on();
21666        let err = parse_err("sub foo { 1 }");
21667        assert!(
21668            err.contains("--no-interop") && err.contains("fn"),
21669            "sub rejected with fn hint: got {err:?}"
21670        );
21671    }
21672
21673    #[test]
21674    fn no_interop_rejects_say() {
21675        let _g = NoInteropGuard::on();
21676        let err = parse_err("say 1");
21677        assert!(
21678            err.contains("--no-interop") && err.contains("`p`"),
21679            "say rejected with p hint: got {err:?}"
21680        );
21681    }
21682
21683    #[test]
21684    fn no_interop_rejects_scalar_keyword() {
21685        let _g = NoInteropGuard::on();
21686        let err = parse_err("my $n = scalar @x");
21687        assert!(
21688            err.contains("--no-interop") && (err.contains("len") || err.contains("cnt")),
21689            "scalar rejected with len/cnt hint: got {err:?}"
21690        );
21691    }
21692
21693    #[test]
21694    fn no_interop_rejects_reverse() {
21695        let _g = NoInteropGuard::on();
21696        let err = parse_err("my @y = reverse @x");
21697        assert!(
21698            err.contains("--no-interop") && err.contains("rev"),
21699            "reverse rejected with rev hint: got {err:?}"
21700        );
21701    }
21702
21703    /// And the inverse — the stryke spellings (`fn`, `p`, `len`, `rev`)
21704    /// must parse cleanly under the same flag. A regression that
21705    /// accidentally rejects the canonical form is just as bad as one
21706    /// that accepts the Perl 5 form.
21707    #[test]
21708    fn no_interop_accepts_stryke_idioms() {
21709        let _g = NoInteropGuard::on();
21710        // Each of these used to be the Perl 5 form; stryke's equivalent
21711        // must parse without error.
21712        parse_ok("fn foo { 1 }");
21713        parse_ok("p 1");
21714        parse_ok("my @x = (1, 2, 3); my $n = len(@x)");
21715        parse_ok("my @x = (1, 2, 3); my @y = rev(@x)");
21716    }
21717
21718    /// `--no-interop` must NOT affect default-mode parsing. Tests run in
21719    /// parallel; the guard's Drop restores the flag, but verify the
21720    /// happy-path Perl 5 forms still parse with the flag *off* so we know
21721    /// the guard mechanics actually restore.
21722    #[test]
21723    fn default_mode_still_accepts_perl5_forms() {
21724        // No guard installed — process default (off in tests).
21725        parse_ok("sub foo { 1 }");
21726        parse_ok("say 1");
21727    }
21728
21729    /// `$a` / `$b` outside a sort or reduce block is rejected under
21730    /// `--no-interop` — stryke routes the user to `$_0` / `$_1`
21731    /// implicit-positional names which work everywhere (including in
21732    /// sort blocks). Pins the lexer arm at `parser.rs::18964` shape.
21733    #[test]
21734    fn no_interop_rejects_bare_dollar_a_dollar_b() {
21735        let _g = NoInteropGuard::on();
21736        // Bare reference outside a block context.
21737        let err = parse_err("my $x = $a + $b");
21738        assert!(
21739            err.contains("--no-interop") || err.contains("$_0") || err.contains("$_1"),
21740            "$a/$b rejected with positional hint: got {err:?}"
21741        );
21742    }
21743
21744    /// And inside a sort block too: stryke's strict mode wants
21745    /// `$_0` / `$_1` even there (per the temperature_converter and
21746    /// quicksort_no_interop examples).
21747    #[test]
21748    fn no_interop_rejects_dollar_a_inside_sort_block() {
21749        let _g = NoInteropGuard::on();
21750        let err = parse_err("my @s = sort { $a <=> $b } (3, 1, 2)");
21751        assert!(
21752            err.contains("--no-interop") || err.contains("$_0") || err.contains("$_1"),
21753            "$a in sort block rejected: got {err:?}"
21754        );
21755    }
21756
21757    /// And the inverse — `$_0` / `$_1` inside a sort block parses
21758    /// clean under `--no-interop`.
21759    #[test]
21760    fn no_interop_accepts_positional_underscore_in_sort_block() {
21761        let _g = NoInteropGuard::on();
21762        parse_ok("my @s = sort { $_0 <=> $_1 } (3, 1, 2)");
21763    }
21764
21765    // ── stryke-specific grammar pins (parse with the flag on or off) ────
21766
21767    /// Colon ranges `start:end` are stryke-canonical (not `..`).
21768    /// `1:10`, `0:N`, `-5:5` all parse.
21769    #[test]
21770    fn colon_range_parses_in_for_loop() {
21771        let _g = NoInteropGuard::on();
21772        parse_ok("for my $i (1:10) { p $i }");
21773        parse_ok("my @r = 0:99");
21774        parse_ok("my @r = -5:5");
21775    }
21776
21777    /// Postfix `if` / `unless` / `for` modifiers parse on a statement.
21778    #[test]
21779    fn postfix_statement_modifiers_parse() {
21780        let _g = NoInteropGuard::on();
21781        parse_ok("p _ for (1, 2, 3)");
21782        parse_ok("p 1 if 1");
21783        parse_ok("p 0 unless 0");
21784    }
21785
21786    /// Pipe-forward `|>` desugars at parse time — verify it accepts both
21787    /// the bare-function (`x |> f`) and the block (`x |> { _ * 2 }`) forms.
21788    #[test]
21789    fn pipe_forward_accepts_both_function_and_block_rhs() {
21790        let _g = NoInteropGuard::on();
21791        parse_ok("my $r = 1:10 |> sum");
21792        parse_ok("my @r = 1:10 |> maps { _ * 2 }");
21793        parse_ok("my @r = 1:10 |> grep { _ % 2 == 0 } |> maps { _ + 1 }");
21794    }
21795
21796    /// `_0`, `_1`, ... bareword positional params (no sigil).
21797    #[test]
21798    fn bareword_positional_underscore_n_parses_in_blocks() {
21799        let _g = NoInteropGuard::on();
21800        // `_0` / `_1` inside a sort block: the canonical strict spelling.
21801        parse_ok("my @s = sort { _0 <=> _1 } (3, 1, 2)");
21802        // And inside a maps block as the per-item topic.
21803        parse_ok("my @r = maps { _0 * 2 } (1, 2, 3)");
21804    }
21805
21806    /// Declarative types — `struct`, `enum`, `class`, `trait` — must
21807    /// parse under `--no-interop` (they're stryke extensions, not Perl 5
21808    /// shapes). Pin each via a minimal declaration.
21809    #[test]
21810    fn no_interop_accepts_struct_decl() {
21811        let _g = NoInteropGuard::on();
21812        parse_ok("struct Point { x => Int, y => Int }");
21813    }
21814
21815    #[test]
21816    fn no_interop_accepts_enum_decl() {
21817        let _g = NoInteropGuard::on();
21818        parse_ok("enum Color { Red, Green, Blue }");
21819        // Data-carrying variants use the `Variant => Type` shape, not
21820        // `Variant(Type)` — the latter is reserved for pattern
21821        // destructuring in match arms.
21822        parse_ok("enum Maybe { Just => Int, Nothing }");
21823    }
21824
21825    #[test]
21826    fn no_interop_accepts_class_decl_with_methods() {
21827        let _g = NoInteropGuard::on();
21828        parse_ok(
21829            "class Rect {\n    width: Float\n    height: Float\n\n    fn area { $self->width * $self->height }\n}",
21830        );
21831    }
21832
21833    #[test]
21834    fn no_interop_accepts_trait_decl() {
21835        let _g = NoInteropGuard::on();
21836        parse_ok("trait Greeter { fn greet; fn loudly { p \"GREET\" } }");
21837    }
21838
21839    /// Compound-assign operators — `||=` defined-or-assign, `//=` exists-
21840    /// or-assign — are stryke- and Perl-compat and should round-trip.
21841    /// These are the lazy-init idiom for hash-of-array buckets used in
21842    /// `csv_summary_no_interop.stk`.
21843    #[test]
21844    fn defined_or_assign_compound_operators_parse() {
21845        let _g = NoInteropGuard::on();
21846        parse_ok("my $h = {}; $h->{x} ||= []");
21847        parse_ok("my $v; $v //= 0");
21848    }
21849
21850    /// `+{ ... }` is the unambiguous hashref literal (vs `{ ... }` block).
21851    /// Used in the CSV demo to push rows.
21852    #[test]
21853    fn explicit_hashref_literal_parses() {
21854        let _g = NoInteropGuard::on();
21855        parse_ok("my $row = +{ region => \"north\", qty => 10 }");
21856        parse_ok("my @rows = (+{ a => 1 }, +{ a => 2 })");
21857    }
21858
21859    /// `eval { … }` block + `$@` error-variable inspection is the
21860    /// canonical exception form used in `rpn_calc_no_interop.stk`.
21861    #[test]
21862    fn eval_block_and_dollar_at_parse() {
21863        let _g = NoInteropGuard::on();
21864        parse_ok("my $r = eval { 1 + 2 }; p $@ if $@");
21865        parse_ok("eval { die \"boom\" }; p $@");
21866    }
21867
21868    /// `try { … } catch ($e) { … }` is the stryke-extension exception
21869    /// shape (Perl-5-on-steroids); must parse under `--no-interop`.
21870    #[test]
21871    fn try_catch_parses() {
21872        let _g = NoInteropGuard::on();
21873        parse_ok("try { die \"boom\" } catch ($e) { p $e }");
21874    }
21875
21876    /// File-test operators (`-d`, `-f`, `-r`, `-e`, …) are unary prefix
21877    /// ops on a filename or filehandle. Stryke inherits the Perl shape.
21878    #[test]
21879    fn file_test_operators_parse() {
21880        let _g = NoInteropGuard::on();
21881        parse_ok("p 1 if -d \"/tmp\"");
21882        parse_ok("p 2 if -f \"/etc/hosts\"");
21883        parse_ok("p 3 if -e $0");
21884        parse_ok("my $sz = -s \"/etc/hosts\"");
21885    }
21886
21887    /// `~>` (thread-first) and `~>>` (thread-last) macros — stryke's
21888    /// signature pipeline operators alongside `|>`. Pin the basic
21889    /// parsing shape.
21890    #[test]
21891    fn thread_macros_parse() {
21892        let _g = NoInteropGuard::on();
21893        parse_ok("my $r = ~> 5 +1 *2");
21894        parse_ok("my @r = ~> (1,2,3) maps { _ * 2 }");
21895    }
21896
21897    /// Hash-destructure parameter — `fn f({ a => $a, b => $b })` —
21898    /// is a stryke-extension signature shape used to unpack a hashref
21899    /// at call time.
21900    #[test]
21901    fn hash_destructure_sub_signature_parses() {
21902        let _g = NoInteropGuard::on();
21903        parse_ok("fn handle({ name => $name, qty => $qty }) { p \"$name x $qty\" }");
21904    }
21905
21906    /// Anonymous fn (`fn { ... }`) — the implicit-positional closure
21907    /// shape. Used as a first-class value: assigned, returned, passed.
21908    #[test]
21909    fn anonymous_fn_parses() {
21910        let _g = NoInteropGuard::on();
21911        parse_ok("my $f = fn { _0 * 2 }");
21912        parse_ok("my @doubled = maps { _0 * 2 } (1, 2, 3)");
21913    }
21914
21915    /// Ternary `cond ? a : b` chains for inline branching.
21916    #[test]
21917    fn ternary_and_chained_ternary_parse() {
21918        let _g = NoInteropGuard::on();
21919        parse_ok("my $r = $x > 0 ? \"pos\" : \"neg\"");
21920        parse_ok("my $r = $x > 0 ? \"pos\" : $x < 0 ? \"neg\" : \"zero\"");
21921    }
21922
21923    /// Negative indices on array slice — `@arr[-3:-1]` for the last
21924    /// three elements. Used in the parallel_primes demo.
21925    #[test]
21926    fn array_slice_with_negative_indices_parses() {
21927        let _g = NoInteropGuard::on();
21928        parse_ok("my @arr = (1,2,3,4,5); my @tail = @arr[-3:-1]");
21929        parse_ok("my @arr = (1,2,3); my @last_two = @arr[-2:]");
21930    }
21931
21932    /// `state $x` declarations (function-local persistent storage)
21933    /// used by the memoised-fib demo for the cache hash.
21934    #[test]
21935    fn state_variable_declaration_parses() {
21936        let _g = NoInteropGuard::on();
21937        // Use clearly-non-builtin fn names (`counter` / `memo` clash
21938        // with stryke builtins).
21939        parse_ok("fn my_counter { state $n = 0; $n++; $n }");
21940        parse_ok("fn my_memo($k) { state %cache; $cache{$k} //= compute($k) }");
21941    }
21942
21943    /// `our` declarations are package-globals — still legal in strict
21944    /// mode (just sub/say/scalar/reverse are rejected, not `our`).
21945    #[test]
21946    fn our_declaration_parses() {
21947        let _g = NoInteropGuard::on();
21948        parse_ok("our $VERSION = 1.0");
21949        parse_ok("our @EXPORT = (1, 2, 3)");
21950    }
21951
21952    /// Regex binding operators — `=~` (match) and `!~` (negated match).
21953    /// Used in roman_numerals_no_interop for input validation.
21954    #[test]
21955    fn regex_binding_operators_parse() {
21956        let _g = NoInteropGuard::on();
21957        parse_ok("p 1 if $s =~ /^\\d+$/");
21958        parse_ok("p 0 unless $s !~ /[A-Z]/");
21959        parse_ok("my @m = $s =~ /(\\w+)/g");
21960    }
21961
21962    /// Nested data-structure literals — hash of array of hash, the
21963    /// shape used by csv_summary_no_interop (`%by_region` is a
21964    /// hash of arrays of hashrefs).
21965    #[test]
21966    fn nested_data_structure_literals_parse() {
21967        let _g = NoInteropGuard::on();
21968        parse_ok("my %h = (a => [1, 2, 3], b => [4, 5])");
21969        parse_ok("my @rows = (+{ x => 1 }, +{ x => 2 })");
21970        parse_ok("my %grid = (cells => [+{ row => 1 }, +{ row => 2 }])");
21971    }
21972
21973    /// Anonymous fn with explicit params — `fn ($x, $y) { ... }`.
21974    /// Complements the `fn { _0 + _1 }` implicit-positional form.
21975    #[test]
21976    fn anonymous_fn_with_explicit_params_parses() {
21977        let _g = NoInteropGuard::on();
21978        parse_ok("my $add = fn ($x, $y) { $x + $y }");
21979        parse_ok("my $h = fn ($v, %opts) { p $v; p %opts }");
21980    }
21981
21982    /// `package Foo;` and `package Foo::Bar;` declarations — qualified
21983    /// namespace setup. Still legal in strict mode.
21984    #[test]
21985    fn package_declaration_parses() {
21986        let _g = NoInteropGuard::on();
21987        parse_ok("package Foo; my $x = 1");
21988        parse_ok("package Foo::Bar::Baz; our $VERSION = 0.01");
21989    }
21990
21991    /// `next` / `last` / `redo` loop-control statements (used in
21992    /// balanced_brackets / brainfuck / sieve demos to break out of
21993    /// inner loops).
21994    #[test]
21995    fn loop_control_keywords_parse() {
21996        let _g = NoInteropGuard::on();
21997        parse_ok("for my $i (1:10) { next if $i % 2; p $i }");
21998        parse_ok("while (1) { last if $done }");
21999        parse_ok("my $rerun = 0; for (1:5) { if ($rerun) { redo } }");
22000    }
22001
22002    /// Labelled loops + labelled `next` / `last`. Used when you need
22003    /// to break out of nested loops.
22004    #[test]
22005    fn labelled_loops_parse() {
22006        let _g = NoInteropGuard::on();
22007        parse_ok("OUTER: for my $i (1:10) { last OUTER if $i > 5 }");
22008        parse_ok("OUTER: for my $i (1:3) { INNER: for my $j (1:3) { next OUTER if $j > $i } }");
22009    }
22010
22011    /// String-repeat operator `x` — `\"-\" x 40` for a separator line,
22012    /// `(0) x N` for an initialized array. Both shapes appear in the
22013    /// histogram / sieve / brainfuck demos.
22014    #[test]
22015    fn string_repeat_x_operator_parses() {
22016        let _g = NoInteropGuard::on();
22017        parse_ok("my $sep = \"-\" x 40");
22018        parse_ok("my @zeros = (0) x 100");
22019        parse_ok("my $bar = \"#\" x $count");
22020    }
22021
22022    /// `chomp` / `chop` builtins — mutate-in-place string ops on the
22023    /// topic or an explicit lvalue. Used in stdin-reading scripts.
22024    #[test]
22025    fn chomp_chop_parse() {
22026        let _g = NoInteropGuard::on();
22027        parse_ok("chomp(my $line = <STDIN>)");
22028        parse_ok("my $s = \"hi\\n\"; chomp $s");
22029        parse_ok("my $t = \"hi\"; chop $t");
22030    }
22031
22032    /// Substitution operator `s/pat/repl/flags` — used by
22033    /// palindrome_no_interop to strip non-alphanumerics.
22034    #[test]
22035    fn substitution_operator_parses() {
22036        let _g = NoInteropGuard::on();
22037        parse_ok("my $s = \"abc\"; $s =~ s/b/X/");
22038        parse_ok("my $s = \"AaBb\"; $s =~ s/[a-z]//g");
22039        parse_ok("my $s = \"Hello\"; $s =~ s/(.)/\\1\\1/g");
22040    }
22041
22042    /// `sprintf` for column-aligned strings — used by every demo's
22043    /// table output.
22044    #[test]
22045    fn sprintf_parses_with_format_specs() {
22046        let _g = NoInteropGuard::on();
22047        parse_ok("my $row = sprintf(\"%-10s %5d\", \"foo\", 42)");
22048        parse_ok("p sprintf(\"%.3f ms\", 1.234)");
22049        parse_ok("p sprintf(\"%04x\", 255)");
22050    }
22051
22052    /// `<STDIN>` diamond reads — list and scalar context. Used by
22053    /// wordcount / csv_summary / anagram demos.
22054    #[test]
22055    fn diamond_stdin_reads_parse() {
22056        let _g = NoInteropGuard::on();
22057        parse_ok("my $line = <STDIN>");
22058        parse_ok("my @lines = <STDIN>");
22059        parse_ok("while (my $line = <STDIN>) { p $line }");
22060    }
22061
22062    /// Chained method calls — `$obj->foo->bar(arg)`. Used in
22063    /// build_destroy and class demos.
22064    #[test]
22065    fn chained_method_calls_parse() {
22066        let _g = NoInteropGuard::on();
22067        parse_ok("$obj->foo->bar");
22068        parse_ok("my $r = $row->{region}");
22069        parse_ok("$obj->set(1)->get");
22070    }
22071
22072    /// Hash dereference syntaxes — `%$href`, `keys %$href`,
22073    /// `$href->{key}`, `%{ expr }`. Used by set_ops_no_interop.
22074    #[test]
22075    fn hash_deref_forms_parse() {
22076        let _g = NoInteropGuard::on();
22077        parse_ok("my $h = +{a=>1}; my %copy = %$h");
22078        parse_ok("my $h = +{a=>1}; my @k = keys %$h");
22079        parse_ok("my $h = +{a=>1}; p $h->{a}");
22080        parse_ok("my $h = +{a=>1, b=>2}; p len(keys %{$h})");
22081    }
22082
22083    /// `unless` postfix on a `die` — the canonical assertion shape
22084    /// every demo uses for self-tests.
22085    #[test]
22086    fn die_unless_assertion_parses() {
22087        let _g = NoInteropGuard::on();
22088        parse_ok("die \"x must be 1\" unless 1 == 1");
22089        parse_ok("die \"empty list\" if len(@x) == 0");
22090        parse_ok("my $x = 1; die \"hi\" unless $x");
22091    }
22092
22093    /// `qw(...)` quote-words literal — the bareword-list shape used in
22094    /// the older example scripts.
22095    #[test]
22096    fn qw_literal_parses() {
22097        let _g = NoInteropGuard::on();
22098        parse_ok("my @w = qw(red green blue)");
22099        parse_ok("for my $name (qw(Alice Bob Carol)) { p $name }");
22100    }
22101
22102    /// Array slice with explicit indices — `@arr[0, 2, 4]`. Different
22103    /// from range slice `@arr[1:3]`.
22104    #[test]
22105    fn array_slice_with_explicit_indices_parses() {
22106        let _g = NoInteropGuard::on();
22107        parse_ok("my @a = (10, 20, 30, 40); my @s = @a[0, 2]");
22108        parse_ok("my @a = (10, 20, 30); my @s = @a[2, 0, 1]");
22109    }
22110
22111    /// Hash slice — `@h{'a', 'b'}` returns the list of values at keys
22112    /// 'a' and 'b'. Different sigil context than scalar hash access.
22113    #[test]
22114    fn hash_slice_with_keys_parses() {
22115        let _g = NoInteropGuard::on();
22116        parse_ok("my %h = (a=>1, b=>2, c=>3); my @v = @h{'a','c'}");
22117    }
22118
22119    /// Bitwise operators — `&`, `|`, `^`, `~`, `<<`, `>>`. Used by
22120    /// bitops_no_interop. Each binds tighter than the comparison
22121    /// operators, looser than the arithmetic ones (Perl precedence).
22122    #[test]
22123    fn bitwise_operators_parse() {
22124        let _g = NoInteropGuard::on();
22125        parse_ok("my $x = 0xAA; my $y = $x & 0x0F");
22126        parse_ok("my $x = 1; my $y = $x | 2 | 4");
22127        parse_ok("my $x = 0xFF; my $y = $x ^ 0x80");
22128        parse_ok("my $x = 0xAA; my $y = ~$x & 0xff");
22129        parse_ok("my $x = 1; my $y = $x << 4");
22130        parse_ok("my $x = 0xF0; my $y = $x >> 2");
22131    }
22132
22133    /// `0x`, `0b`, `0o` numeric literal prefixes — hex / binary /
22134    /// octal. Used freely in bitops + numeric-conversion demos.
22135    #[test]
22136    fn numeric_literal_prefixes_parse() {
22137        let _g = NoInteropGuard::on();
22138        parse_ok("my $hex = 0xCAFEBABE");
22139        parse_ok("my $bin = 0b1101");
22140        parse_ok("my $hex8 = 0xff & 0x0f");
22141    }
22142
22143    /// Negative array index — `$arr[-1]`, `$arr[-2]`. Used by
22144    /// shunting_yard (`$ops[-1]` to peek stack top).
22145    #[test]
22146    fn negative_array_index_parses() {
22147        let _g = NoInteropGuard::on();
22148        // `@a` / `@b` array names share the `$a` / `$b` reservation in
22149        // strict mode — use `@arr` instead.
22150        parse_ok("my @arr = (1, 2, 3); p $arr[-1]");
22151        parse_ok("my @stack = (10, 20, 30); p $stack[-1] if len(@stack) > 0");
22152    }
22153
22154    /// `last` / `next` / `return` as standalone statements (already
22155    /// pinned alongside `last LABEL` but not as the lone-line form).
22156    #[test]
22157    fn loop_control_standalone_parses() {
22158        let _g = NoInteropGuard::on();
22159        parse_ok("while (1) { last }");
22160        parse_ok("for my $i (1:10) { next if $i == 3 }");
22161        parse_ok("fn ret { return 42 }");
22162    }
22163
22164    /// `shift @args` / `pop @args` — common destructuring shape inside
22165    /// functions for "first arg" / "last arg" pickoff (used by morse
22166    /// to peel the mode off `@ARGV`).
22167    #[test]
22168    fn shift_pop_on_array_parses() {
22169        let _g = NoInteropGuard::on();
22170        parse_ok("my @a = (1, 2, 3); my $first = shift @a");
22171        parse_ok("my @a = (1, 2, 3); my $last  = pop @a");
22172        parse_ok("fn drop_first(@xs) { shift @xs; @xs }");
22173    }
22174
22175    /// Special internal names — `__FILE__`, `__LINE__`, `__PACKAGE__`
22176    /// — accessed by tooling and error reporting.
22177    #[test]
22178    fn special_internal_names_parse() {
22179        let _g = NoInteropGuard::on();
22180        parse_ok("p __FILE__");
22181        parse_ok("p __LINE__");
22182        parse_ok("p __PACKAGE__");
22183        parse_ok("p __FILE__ . \":\" . __LINE__");
22184    }
22185
22186    /// Nested ternary RHS with parens / chained alternatives. Used by
22187    /// the conway demo's pattern dispatch.
22188    #[test]
22189    fn deeply_nested_ternary_parses() {
22190        let _g = NoInteropGuard::on();
22191        parse_ok("my $g = ($p eq \"a\") ? 1 : ($p eq \"b\") ? 2 : ($p eq \"c\") ? 3 : 4");
22192    }
22193
22194    /// Array-of-hashref + index-into-hash subscript chain:
22195    /// `$rows[0]->{name}`, `$rows[-1]->{score}`. Used in csv_summary
22196    /// and ranking demos.
22197    #[test]
22198    fn array_of_hashref_chained_subscript_parses() {
22199        let _g = NoInteropGuard::on();
22200        parse_ok("my @rows = (+{name=>'a',sc=>1}); p $rows[0]->{name}");
22201        parse_ok("my @rows = (+{n=>10},+{n=>20}); p $rows[-1]->{n}");
22202    }
22203
22204    /// Nested `for` loops with two index variables — the 2D grid walk
22205    /// shape used in conway.
22206    #[test]
22207    fn nested_for_loops_parse() {
22208        let _g = NoInteropGuard::on();
22209        parse_ok("for my $r (0:5) { for my $c (0:5) { p \"$r,$c\" } }");
22210    }
22211
22212    /// Compound assignment `$x .= ...` (string append). Used by every
22213    /// demo that builds output strings incrementally (brainfuck,
22214    /// RLE encode, Caesar).
22215    #[test]
22216    fn dot_assign_string_append_parses() {
22217        let _g = NoInteropGuard::on();
22218        parse_ok("my $s = \"a\"; $s .= \"b\"");
22219        parse_ok("my $out = \"\"; $out .= \"x\" for (1:3)");
22220    }
22221
22222    /// `unshift @arr, $v` — push to the FRONT of an array; used by
22223    /// graph_bfs for back-tracking the path.
22224    #[test]
22225    fn unshift_parses() {
22226        let _g = NoInteropGuard::on();
22227        parse_ok("my @path = (3, 4); unshift @path, 2; unshift @path, 1");
22228        parse_ok("my @q; unshift @q, $_ for (1:5)");
22229    }
22230
22231    /// `defined` and `// 0` defined-or fallback. Used by the
22232    /// graph_bfs neighbour lookup (`$REVERSE{$ch} // 0`).
22233    #[test]
22234    fn defined_or_fallback_parses() {
22235        let _g = NoInteropGuard::on();
22236        parse_ok("my $x; my $y = $x // 0");
22237        parse_ok("my %h = (a=>1); my $v = $h{missing} // -1");
22238        parse_ok("p 1 if defined $foo");
22239    }
22240
22241    /// `for my $x (rev 0:N)` — reverse a range with the stryke `rev`
22242    /// keyword. Used by knapsack back-tracking.
22243    #[test]
22244    fn rev_over_range_parses() {
22245        let _g = NoInteropGuard::on();
22246        parse_ok("for my $i (rev 0:9) { p $i }");
22247        parse_ok("my @r = rev (1, 2, 3, 4)");
22248    }
22249
22250    /// Array deref `@$ref`, `@{$expr}`. Used freely in graph_bfs +
22251    /// knapsack to walk arrays-of-arrayrefs.
22252    #[test]
22253    fn array_deref_forms_parse() {
22254        let _g = NoInteropGuard::on();
22255        parse_ok("my $r = [1, 2, 3]; my @copy = @$r");
22256        parse_ok("my $r = [1, 2, 3]; my @copy = @{$r}");
22257        parse_ok("my @rs = ([1], [2, 3]); my @flat; push @flat, @$_ for @rs");
22258    }
22259
22260    /// Hashref via `[k]` doesn't exist — but `$ref->{k}` and `${$ref}{k}`
22261    /// are the two equivalent shapes. Pin both.
22262    #[test]
22263    fn hashref_subscript_alt_forms_parse() {
22264        let _g = NoInteropGuard::on();
22265        parse_ok("my $h = +{a=>1}; p $h->{a}");
22266        parse_ok("my $h = +{a=>1}; p ${$h}{a}");
22267    }
22268
22269    /// `abs($x)`, `int($x)`, `sqrt($x)` — common numeric builtins
22270    /// invoked as functions.
22271    #[test]
22272    fn numeric_builtins_parse() {
22273        let _g = NoInteropGuard::on();
22274        parse_ok("my $x = abs(-5)");
22275        parse_ok("my $f = int(3.7)");
22276        parse_ok("my $s = sqrt(2)");
22277        parse_ok("my $n = int($x * 100 + 0.5) / 100");
22278    }
22279
22280    /// `local $var` declaration — dynamic-scoped binding restored
22281    /// on block exit. Different from `my` (lexical) and `our`
22282    /// (package-global).
22283    #[test]
22284    fn local_declaration_parses() {
22285        let _g = NoInteropGuard::on();
22286        // Strict mode rejects `sub` — use anonymous `fn` for the scope.
22287        parse_ok("our $g = 1; (fn { local $g = 99; p $g })->()");
22288    }
22289
22290    /// `srand($seed)` and `rand($n)` — deterministic random with
22291    /// optional bound. Used by dice + histogram demos for repeatable
22292    /// CI output.
22293    #[test]
22294    fn srand_rand_parse() {
22295        let _g = NoInteropGuard::on();
22296        parse_ok("srand(42); my $r = rand(6)");
22297        parse_ok("srand(); my $r = int(rand(100))");
22298    }
22299
22300    /// `substr($s, $i, $n)` for slicing strings (2-arg + 3-arg forms).
22301    /// Used by base64 + soundex + RPN demos.
22302    #[test]
22303    fn substr_parses() {
22304        let _g = NoInteropGuard::on();
22305        parse_ok("my $s = \"hello\"; p substr($s, 0, 1)");
22306        parse_ok("my $s = \"hello\"; p substr($s, 1)");
22307        parse_ok("my $s = \"hello\"; p substr($s, -2)");
22308    }
22309
22310    /// Underscore separator in numeric literals — `1_000_000` for
22311    /// readability. Used in `pmap { ... } 1:1_000` style code.
22312    #[test]
22313    fn underscore_separators_in_numbers_parse() {
22314        let _g = NoInteropGuard::on();
22315        parse_ok("my $n = 1_000_000");
22316        parse_ok("my $r = 1_000_000 / 365");
22317        parse_ok("my $hex = 0xff_ff");
22318    }
22319
22320    /// 2D array-of-arrayref construction `[[a,b], [c,d]]` and access
22321    /// `$grid->[0]->[1]`. Used by interval_merge and dijkstra demos.
22322    #[test]
22323    fn arrayref_of_arrayref_access_parses() {
22324        let _g = NoInteropGuard::on();
22325        parse_ok("my $grid = [[1, 2], [3, 4]]");
22326        parse_ok("my $grid = [[1, 2], [3, 4]]; p $grid->[0]->[1]");
22327        parse_ok("my $grid = [[1, 2], [3, 4]]; p $grid->[1][0]");
22328    }
22329
22330    /// Mutating array element via arrow-arrow chain — `$ref->[0]->[1] = $v`.
22331    /// Used by interval_merge to extend an interval's end in place.
22332    #[test]
22333    fn arrow_chain_assignment_parses() {
22334        let _g = NoInteropGuard::on();
22335        parse_ok("my $g = [[0, 0]]; $g->[0]->[1] = 99");
22336        parse_ok("my $g = [[0, 0]]; $g->[0][1] = 99");
22337    }
22338
22339    /// Tuple destructure from arrayref element — `my ($a, $b) = @$e`.
22340    /// Pin the no-interop renames (`$a` reserved).
22341    #[test]
22342    fn tuple_destructure_from_arrayref_parses() {
22343        let _g = NoInteropGuard::on();
22344        parse_ok("my $e = [10, 20]; my ($lhs, $rhs) = @$e");
22345        parse_ok("my $e = [\"x\", 1]; my ($name, $weight) = ($e->[0], $e->[1])");
22346    }
22347
22348    /// `next unless` / `last unless` postfix on a loop body. Common
22349    /// guard shape inside the rolling-stats sliding loops.
22350    #[test]
22351    fn next_last_unless_postfix_parses() {
22352        let _g = NoInteropGuard::on();
22353        parse_ok("for my $i (1:10) { next unless $i % 2 == 0; p $i }");
22354        parse_ok("while (1) { last unless $live }");
22355    }
22356
22357    /// `keys %{ ... }` with parenthesised hash dereference — the
22358    /// shape used by deepish hash-of-hash access.
22359    #[test]
22360    fn keys_on_braced_hash_deref_parses() {
22361        let _g = NoInteropGuard::on();
22362        parse_ok("my $h = +{a=>1}; my @k = keys %{$h}");
22363        parse_ok("my $h = +{a=>+{b=>1}}; my @k = keys %{$h->{a}}");
22364    }
22365
22366    /// Namespaced fn definition — `fn Module::method($x) { ... }`.
22367    /// All UDFs in the examples/ demos use this form so future stryke
22368    /// stdlib additions don't shadow user code (or vice versa).
22369    #[test]
22370    fn namespaced_fn_decl_parses() {
22371        let _g = NoInteropGuard::on();
22372        parse_ok("fn Module::method($x) { $x * 2 }");
22373        parse_ok("fn Foo::Bar::helper { 42 }");
22374        parse_ok("fn Demo::run { p \"running\" }");
22375    }
22376
22377    /// Calling a namespaced fn — `Module::method($arg)`. Also used as
22378    /// a method on an explicit invocant in some contexts.
22379    #[test]
22380    fn namespaced_fn_call_parses() {
22381        let _g = NoInteropGuard::on();
22382        parse_ok("fn Module::add($x, $y) { $x + $y } p Module::add(2, 3)");
22383        // `caller` itself is a stryke builtin — use a namespaced caller
22384        // to avoid the clash.
22385        parse_ok("fn Foo::Bar::baz { 1 } fn Demo::main { Foo::Bar::baz() + Foo::Bar::baz() }");
22386    }
22387
22388    /// `index($haystack, $needle)` builtin — the canonical string
22389    /// substring-position lookup that KMP cross-checks against.
22390    #[test]
22391    fn index_builtin_parses() {
22392        let _g = NoInteropGuard::on();
22393        parse_ok("my $i = index(\"hello world\", \"world\")");
22394        parse_ok("my $i = index($s, $pat, 0)");
22395    }
22396
22397    /// `for my $i (rev 1:N)` — reverse iteration over a colon range.
22398    /// Already pinned in `rev_over_range_parses` but here for the
22399    /// `rev (list)` form on an array literal.
22400    #[test]
22401    fn rev_on_array_literal_parses() {
22402        let _g = NoInteropGuard::on();
22403        parse_ok("my @r = rev (1, 2, 3, 4)");
22404        parse_ok("for my $x (rev (\"a\", \"b\", \"c\")) { p $x }");
22405    }
22406
22407    /// Numeric comparison + string comparison side by side — the
22408    /// pattern used by sort blocks with secondary tie-breaking
22409    /// (numeric primary, string secondary). `$a` / `$b` are reserved
22410    /// under --no-interop; use `$_0` / `$_1` for sort comparator args.
22411    #[test]
22412    fn numeric_and_string_comparison_in_one_expr_parses() {
22413        let _g = NoInteropGuard::on();
22414        parse_ok("my $r = $_0 <=> $_1 || $name cmp $other");
22415        parse_ok("p 1 if $x == $y && $name eq \"foo\"");
22416    }
22417
22418    /// Bareword positional names — `_0`, `_1`, `_N` without sigil —
22419    /// are stryke's idiomatic spelling in code contexts. The sigil
22420    /// form (`$_0`, `$_1`) is reserved for string interpolation where
22421    /// a bareword would just be a literal substring. Pin the bareword
22422    /// shape in every common closure position.
22423    #[test]
22424    fn bareword_positional_in_sort_reduce_blocks_parses() {
22425        let _g = NoInteropGuard::on();
22426        // Sort comparator with bareword positional names.
22427        parse_ok("my @s = sort { _0 <=> _1 } (3, 1, 2)");
22428        // Reduce accumulator + element.
22429        parse_ok("my $r = (1, 2, 3) |> reduce { _0 + _1 }");
22430        // Reduce on string concat — _0 acc, _1 next.
22431        parse_ok("my $s = (\"a\", \"b\") |> reduce { _0 . _1 }");
22432    }
22433
22434    /// Bareword topic `_` inside `maps` / `grep` / `pmap` / `pgrep`
22435    /// closure bodies. Tightest form; no sigil needed.
22436    #[test]
22437    fn bareword_topic_in_maps_grep_parses() {
22438        let _g = NoInteropGuard::on();
22439        parse_ok("my @r = (1, 2, 3) |> maps { _ * 2 }");
22440        parse_ok("my @r = (1, 2, 3, 4) |> grep { _ % 2 == 0 }");
22441        parse_ok("my @r = 1:100 |> pmap { _ ** 3 }");
22442        parse_ok("my @r = 1:100 |> pgrep { _ % 7 == 0 }");
22443    }
22444
22445    /// `_` and `_1` in arithmetic expression context (no sigil).
22446    /// Inside string interpolation, only the sigil form `${_}` /
22447    /// `$_0` / `$_1` works — pin the contrast.
22448    #[test]
22449    fn bareword_vs_sigil_in_string_interp_parses() {
22450        let _g = NoInteropGuard::on();
22451        // Bareword in code: tight, idiomatic.
22452        parse_ok("my @r = (5, 10) |> maps { _ + 1 }");
22453        // Sigil in string interp: the bareword would just be the
22454        // literal characters underscore-zero, so the sigil form is
22455        // required here.
22456        parse_ok("p \"first=$_0 second=$_1\"");
22457        parse_ok("p \"got: $_\"");
22458    }
22459
22460    /// Bareword `_` topic inside a `for $_` -style postfix-for body.
22461    /// Used in Conway's life-counter — `$n += _ for @$g`.
22462    #[test]
22463    fn bareword_topic_in_postfix_for_parses() {
22464        let _g = NoInteropGuard::on();
22465        parse_ok("my $n = 0; $n += _ for (1, 2, 3, 4)");
22466        parse_ok("my %h; $h{_}++ for (\"a\", \"b\", \"a\")");
22467    }
22468
22469    /// Bareword `_` with hash-subscript on the outer side —
22470    /// `$h{_}++` is tighter than `$h{$_}++` (no sigil on key).
22471    #[test]
22472    fn bareword_topic_as_hash_key_parses() {
22473        let _g = NoInteropGuard::on();
22474        parse_ok("my %h; $h{_}++ for (\"a\", \"b\", \"a\")");
22475        parse_ok("my @arr = (1, 2, 3); my %seen; $seen{$arr[_]}++ for (0, 1, 2)");
22476    }
22477
22478    /// Hashref subscript inside a `grep` block using the bareword
22479    /// topic — the FirstU::pipe_find shape: `grep { $seen{$c[_]} == 1 }`.
22480    #[test]
22481    fn bareword_topic_inside_subscript_chain_parses() {
22482        let _g = NoInteropGuard::on();
22483        parse_ok(
22484            "my @c = (\"a\", \"b\", \"c\"); my %seen = (a => 1, b => 2, c => 1); \
22485             my @hits = grep { $seen{$c[_]} == 1 } (0, 1, 2)",
22486        );
22487    }
22488
22489    /// `ref($x)` builtin — used by flatten to discriminate arrayref
22490    /// from scalar leaves.
22491    #[test]
22492    fn ref_builtin_parses() {
22493        let _g = NoInteropGuard::on();
22494        parse_ok("my $x = [1, 2]; p ref($x)");
22495        parse_ok("my $r = +{a => 1}; p 1 if ref($r) eq \"HASH\"");
22496    }
22497
22498    /// Expression-bodied recursive `fn` — style guide rule 6. The body
22499    /// is a single ternary that re-invokes the same fn; Kadane / gcd /
22500    /// lcm demos all use this shape.
22501    #[test]
22502    fn expression_bodied_recursive_fn_parses() {
22503        let _g = NoInteropGuard::on();
22504        parse_ok("fn N::gcd = _1 == 0 ? _0 : N::gcd(_1, _0 % _1)");
22505        parse_ok("fn N::lcm = _0 * _1 / N::gcd(_0, _1)");
22506    }
22507
22508    /// Expression-bodied `fn` whose body is a `|> reduce` over the
22509    /// implicit topic — used by gcd_list / lcm_list.
22510    #[test]
22511    fn expression_bodied_pipe_reduce_parses() {
22512        let _g = NoInteropGuard::on();
22513        parse_ok(
22514            "fn N::gcd = _1 == 0 ? _0 : N::gcd(_1, _0 % _1); \
22515                  fn N::gcd_list = @{_} |> reduce { N::gcd(_0, _1) }",
22516        );
22517    }
22518
22519    /// Reduce-fold with a hashref accumulator seeded by prepending the
22520    /// init to the input — Boyer-Moore / Kadane idiom.
22521    #[test]
22522    fn reduce_fold_with_hashref_accumulator_parses() {
22523        let _g = NoInteropGuard::on();
22524        parse_ok(
22525            "my @xs = (3, 2, 3); \
22526             my $st = (+{cur => 0, best => -100}, @xs) |> reduce { \
22527                +{ cur => _1, best => _0->{best} } \
22528             }",
22529        );
22530    }
22531
22532    /// Native array-deref slicing `@$arr[$lo:$hi]` — used by
22533    /// rolling_stats to slice the input window.
22534    #[test]
22535    fn array_deref_slice_with_variable_bounds_parses() {
22536        let _g = NoInteropGuard::on();
22537        parse_ok(
22538            "my @a = (10, 20, 30, 40, 50); my $r = \\@a; \
22539             my $lo = 1; my $hi = 3; \
22540             my @s = @$r[$lo:$hi]",
22541        );
22542    }
22543
22544    /// `min(@$v[$lo:$hi])` / `max(@$v[$lo:$hi])` — rolling_stats hot
22545    /// path. Paren-less `min @$v[..]` parses wrong; pinned with parens.
22546    #[test]
22547    fn min_max_over_array_deref_slice_parses() {
22548        let _g = NoInteropGuard::on();
22549        parse_ok(
22550            "my @a = (1, 3, 2, 5); my $r = \\@a; \
22551             my $lo = 0; my $hi = 2; \
22552             my $m1 = min(@$r[$lo:$hi]); \
22553             my $m2 = max(@$r[$lo:$hi])",
22554        );
22555    }
22556
22557    /// `flat_maps` with recursive call inside the block — flatten
22558    /// demo's central idiom; verifies the recursive call inside a
22559    /// pipeline stage parses cleanly.
22560    #[test]
22561    fn flat_maps_with_recursive_call_parses() {
22562        let _g = NoInteropGuard::on();
22563        parse_ok(
22564            "fn Flat::flatten($r) = @$r |> flat_maps { \
22565                ref(_) eq \"ARRAY\" ? Flat::flatten(_) : (_) \
22566             }",
22567        );
22568    }
22569
22570    /// Inner `map { _->[$i] }` capturing outer lexical `$i` — zip's
22571    /// N-list helper. Bareword topic inside, `$i` is the lexical.
22572    #[test]
22573    fn nested_map_with_outer_lexical_capture_parses() {
22574        let _g = NoInteropGuard::on();
22575        parse_ok(
22576            "my @lists = ([1,2,3], [10,20,30]); \
22577             my @r = 0:2 |> maps { my $i = _; [map { _->[$i] } @lists] }",
22578        );
22579    }
22580
22581    /// `@{_}` deref of the topic — required when sigil-form `@$_` is
22582    /// being avoided per style guide. zip's `len(@{_})` shape.
22583    #[test]
22584    fn array_deref_of_bareword_topic_parses() {
22585        let _g = NoInteropGuard::on();
22586        parse_ok("my @lists = ([1,2,3], [10,20,30]); p min(map { len(@{_}) } @lists)");
22587    }
22588
22589    /// `~>` thread-macro stages for `glob`, `rand`, `srand` — these
22590    /// builtins have their own `ExprKind` (Glob, Rand, Srand), not the
22591    /// generic FuncCall path, so they previously fell through to the
22592    /// default arm and produced "Undefined subroutine" at runtime.
22593    #[test]
22594    fn thread_macro_accepts_glob_rand_srand_stages() {
22595        parse_ok("my @r = ~> \"/tmp/*\" glob sort");
22596        parse_ok("my $i = ~> 100 rand int");
22597        parse_ok("~> 42 srand");
22598    }
22599
22600    /// Recursive expression-bodied `fn` with path compression — the
22601    /// union-find demo idiom: `fn UF::find($uf, $x) { ... }` body
22602    /// re-invokes itself and writes the result into a hashref slot.
22603    #[test]
22604    fn recursive_fn_with_arrayref_assignment_parses() {
22605        let _g = NoInteropGuard::on();
22606        parse_ok(
22607            "fn UF::find($uf, $x) { \
22608                return $x if $uf->{parent}[$x] == $x; \
22609                $uf->{parent}[$x] = UF::find($uf, $uf->{parent}[$x]); \
22610                $uf->{parent}[$x] \
22611             }",
22612        );
22613    }
22614
22615    /// Hash-initialised with computed list inside arrayref literal —
22616    /// `[0:$n - 1]` and `[(0) x $n]` as field values. Used by UF::new.
22617    #[test]
22618    fn hashref_init_with_range_and_repeat_parses() {
22619        let _g = NoInteropGuard::on();
22620        parse_ok("fn UF::new($n) = +{ parent => [0:$n - 1], rank => [(0) x $n], count => $n }");
22621    }
22622
22623    /// Postfix-for over an arrayref-deref: `Trie::insert($t, $_) for @$words`.
22624    /// Used by Trie::from_words.
22625    #[test]
22626    fn postfix_for_arrayref_deref_parses() {
22627        let _g = NoInteropGuard::on();
22628        parse_ok("my @words = (\"a\", \"b\"); my $r = \\@words; my @out; push @out, $_ for @$r");
22629    }
22630
22631    /// Tuple-swap destructure inside a block — UF::union flips ra/rb
22632    /// for union-by-rank.
22633    #[test]
22634    fn tuple_swap_destructure_parses() {
22635        let _g = NoInteropGuard::on();
22636        parse_ok("my $ra = 1; my $rb = 2; ($ra, $rb) = ($rb, $ra)");
22637    }
22638
22639    /// 2-D array initialised with explicit row arrayrefs — the
22640    /// `--no-interop` mode rejects 2-D autoviv (`$d[$i][$j] = X`
22641    /// on un-initialized `@d`), so each row must be an arrayref
22642    /// literal first. Damerau-Levenshtein demo's matrix setup.
22643    #[test]
22644    fn explicit_2d_array_row_init_parses() {
22645        let _g = NoInteropGuard::on();
22646        parse_ok(
22647            "my @d; my $m = 3; my $n = 4; \
22648             for my $i (0:$m) { $d[$i] = [(0) x ($n + 1)] }",
22649        );
22650    }
22651
22652    /// `min()` with 3 arguments — Damerau-Levenshtein uses this for the
22653    /// deletion/insertion/substitution step.
22654    #[test]
22655    fn min_with_three_args_parses() {
22656        let _g = NoInteropGuard::on();
22657        parse_ok("my $x = min(1, 2, 3)");
22658        parse_ok("my @d; $d[0][0] = 5; my $r = min($d[0][0] + 1, $d[0][0] + 1, $d[0][0] + 0)");
22659    }
22660
22661    /// String slice with `$s[N:M]` where M is `len(...)`-based.
22662    /// Trie::count_with_prefix shape.
22663    #[test]
22664    fn string_slice_with_len_bound_parses() {
22665        let _g = NoInteropGuard::on();
22666        parse_ok(
22667            "my $w = \"apple\"; my $pre = \"app\"; \
22668             my $ok = len($w) >= len($pre) && $w[0:len($pre) - 1] eq $pre",
22669        );
22670    }
22671
22672    /// Sort comparator with `_0->[N] <=> _1->[N]` — sorting an
22673    /// array-of-arrayrefs by a positional field. Kruskal MST pattern.
22674    #[test]
22675    fn sort_block_with_arrow_deref_topic_parses() {
22676        let _g = NoInteropGuard::on();
22677        parse_ok(
22678            "my @edges = ([0,1,4], [2,3,1], [1,2,2]); \
22679             my @sorted = sort { _0->[2] <=> _1->[2] } @edges",
22680        );
22681    }
22682
22683    /// C-style `for` header with declarations and post-decrement —
22684    /// Knuth shuffle's inner loop walks high-to-low.
22685    #[test]
22686    fn cstyle_for_with_postdecrement_parses() {
22687        let _g = NoInteropGuard::on();
22688        parse_ok(
22689            "my @arr = (1, 2, 3, 4, 5); \
22690             my $n = len(@arr); \
22691             for (my $i = $n - 1; $i > 0; $i--) { p $arr[$i] }",
22692        );
22693    }
22694
22695    /// Tuple-swap on array-deref index pairs — Knuth-shuffle inner
22696    /// step swaps `$r->[$i]` and `$r->[$j]` via destructure.
22697    #[test]
22698    fn tuple_swap_arrayref_index_parses() {
22699        let _g = NoInteropGuard::on();
22700        parse_ok(
22701            "my @arr = (1, 2, 3); my $r = \\@arr; my $i = 0; my $j = 2; \
22702             ($r->[$i], $r->[$j]) = ($r->[$j], $r->[$i])",
22703        );
22704    }
22705
22706    /// Recursive backtracking — N-queens pushes a column, recurses,
22707    /// then pops. Verifies array mutation inside recursive fn calls.
22708    #[test]
22709    fn recursive_backtracking_arrayref_mutation_parses() {
22710        let _g = NoInteropGuard::on();
22711        parse_ok(
22712            "fn Q::go($n, $cols, $count_ref) { \
22713                my $r = len(@$cols); \
22714                if ($r == $n) { $$count_ref++; return } \
22715                for my $c (0:$n - 1) { \
22716                    push @$cols, $c; \
22717                    Q::go($n, $cols, $count_ref); \
22718                    pop @$cols \
22719                } \
22720             }",
22721        );
22722    }
22723
22724    /// Doubly-linked list as a hash-of-{prev,next} — LRU cache's
22725    /// node-table pattern. `$nodes->{$k}{prev}` chain.
22726    #[test]
22727    fn nested_hashref_chain_parses() {
22728        let _g = NoInteropGuard::on();
22729        parse_ok(
22730            "my $c = +{ nodes => +{ a => +{ val => 1, prev => undef, next => \"b\" } } }; \
22731             my $p = $c->{nodes}{a}{prev}; \
22732             my $n = $c->{nodes}{a}{next}",
22733        );
22734    }
22735
22736    /// `$$count_ref++` — dereference a scalar-ref and post-increment.
22737    /// Used in Queens::recur to update a shared counter.
22738    #[test]
22739    fn scalar_ref_postincrement_parses() {
22740        let _g = NoInteropGuard::on();
22741        parse_ok("my $n = 0; my $ref = \\$n; $$ref++; p $n");
22742    }
22743
22744    /// `$h{$k}` autoviv chain — Markov bigram table builds a
22745    /// hash-of-hash where the inner is created on first access.
22746    #[test]
22747    fn hash_of_hash_autoviv_increment_parses() {
22748        let _g = NoInteropGuard::on();
22749        parse_ok(
22750            "my %table; \
22751             my $prev = \"the\"; my $next = \"quick\"; \
22752             $table{$prev} //= +{}; \
22753             $table{$prev}{$next}++",
22754        );
22755    }
22756
22757    /// `for (... ; ...) { ... }` C-style with literal counter — Knuth
22758    /// shuffle's high-to-low walk and similar.
22759    #[test]
22760    fn cstyle_for_with_literal_bounds_parses() {
22761        let _g = NoInteropGuard::on();
22762        parse_ok("my $sum = 0; for (my $i = 0; $i < 10; $i++) { $sum += $i }");
22763    }
22764
22765    /// Recursive expression-bodied `fn` with ternary base case —
22766    /// Josephus closed-form. Single-letter tail segments in namespaced
22767    /// names (`J::s`, `Foo::m`, `Foo::q`, `Foo::qx`, `Foo::qr`) are
22768    /// identifiers — the `::` prefix disambiguates from the
22769    /// `s/.../.../`, `m//`, `q//`, etc. quote-like operators.
22770    #[test]
22771    fn recursive_expression_body_with_ternary_parses() {
22772        let _g = NoInteropGuard::on();
22773        parse_ok("fn J::s($n, $k) = $n == 1 ? 0 : (J::s($n - 1, $k) + $k) % $n");
22774    }
22775
22776    /// All quote-like single/two-letter operators (`s`, `m`, `q`, `qq`,
22777    /// `qx`, `qr`, `tr`, `y`) are valid namespaced identifier tails
22778    /// after `::` — they're not lexed as their regex/quote forms.
22779    #[test]
22780    fn namespaced_quote_like_tail_segments_parse() {
22781        let _g = NoInteropGuard::on();
22782        parse_ok("fn Foo::s($x) = $x + 1");
22783        parse_ok("fn Foo::m($x) = $x * 2");
22784        parse_ok("fn Foo::q($x) = $x");
22785        parse_ok("fn Foo::qq($x) = $x");
22786        parse_ok("fn Foo::qx($x) = $x");
22787        parse_ok("fn Foo::qr($x) = $x");
22788        parse_ok("fn Foo::tr($x) = $x");
22789        parse_ok("fn Foo::y($x) = $x");
22790    }
22791
22792    /// `splice @arr, $i, 1` — remove a single element from middle of
22793    /// an array. Josephus simulate uses this.
22794    #[test]
22795    fn splice_single_remove_parses() {
22796        let _g = NoInteropGuard::on();
22797        parse_ok("my @circle = 0:5; splice @circle, 2, 1");
22798    }
22799
22800    /// `atan2(0, -1)` for π — Monte Carlo's true-reference value.
22801    #[test]
22802    fn atan2_call_parses() {
22803        let _g = NoInteropGuard::on();
22804        parse_ok("fn MC::true_pi = atan2(0, -1)");
22805        parse_ok("my $pi = atan2(0, -1)");
22806    }
22807
22808    /// Flat 1-D array indexed as 2-D via `r * COLS + c` — Sudoku
22809    /// board layout. Arithmetic inside subscripts.
22810    #[test]
22811    fn flat_2d_array_indexing_parses() {
22812        let _g = NoInteropGuard::on();
22813        parse_ok(
22814            "my @board = (0) x 81; \
22815             my $r = 3; my $c = 5; \
22816             $board[$r * 9 + $c] = 7; \
22817             my $v = $board[$r * 9 + $c]",
22818        );
22819    }
22820
22821    /// `par` is callable as a top-level expression, not just an
22822    /// `~>` thread-macro stage. Prefix form: `par { BLOCK } LIST`.
22823    /// (Previously parser only accepted `par` inside thread macros,
22824    /// emitting "Undefined subroutine &par" at runtime for any other
22825    /// call site.)
22826    #[test]
22827    fn par_top_level_prefix_form_parses() {
22828        let _g = NoInteropGuard::on();
22829        parse_ok("my @r = par { _ * 2 } (1, 2, 3, 4)");
22830        parse_ok("par { p _ } @big");
22831    }
22832
22833    /// 2-D DP table with `max()` step — LCS / Levenshtein / Damerau /
22834    /// general edit-distance pattern. Validates that 3-way max +
22835    /// nested arrayref subscript chain parses cleanly.
22836    #[test]
22837    fn dp_max_step_chained_subscript_parses() {
22838        let _g = NoInteropGuard::on();
22839        parse_ok(
22840            "my @d; for my $i (0:3) { $d[$i] = [(0) x 4] } \
22841             $d[1][1] = max($d[0][1], $d[1][0]); \
22842             my $r = $d[1][1]",
22843        );
22844    }
22845
22846    /// Rolling polynomial hash arithmetic — Rabin-Karp's window update.
22847    #[test]
22848    fn rolling_hash_arithmetic_parses() {
22849        let _g = NoInteropGuard::on();
22850        parse_ok(
22851            "my $h = 0; my $base = 257; my $mod = 1000000007; my $high = 256; \
22852             my $drop = 65; my $add = 90; \
22853             $h = (($h - $drop * $high) * $base + $add) % $mod; \
22854             $h = ($h + $mod) % $mod",
22855        );
22856    }
22857
22858    /// Triple-nested loop with index expressions on a 2-D arrayref —
22859    /// Floyd-Warshall's k/i/j signature.
22860    #[test]
22861    fn triple_nested_2d_via_k_parses() {
22862        let _g = NoInteropGuard::on();
22863        parse_ok(
22864            "my @d; for my $i (0:3) { $d[$i] = [(0) x 4] } \
22865             for my $k (0:3) { for my $i (0:3) { for my $j (0:3) { \
22866                $d[$i][$j] = $d[$i][$k] + $d[$k][$j] \
22867                    if $d[$i][$k] + $d[$k][$j] < $d[$i][$j] \
22868             } } }",
22869        );
22870    }
22871
22872    /// DP fill with `@dp = ($INF) x ($amount + 1)` repeat-init.
22873    /// Coin-change shape.
22874    #[test]
22875    fn dp_array_repeat_init_parses() {
22876        let _g = NoInteropGuard::on();
22877        parse_ok("my $amount = 11; my $INF = 1e18; my @dp = ($INF) x ($amount + 1); $dp[0] = 0");
22878    }
22879
22880    /// `join("", rev split //, $s)` — palindrome check pipeline.
22881    #[test]
22882    fn rev_split_join_chain_parses() {
22883        let _g = NoInteropGuard::on();
22884        parse_ok("my $s = \"abc\"; my $r = join(\"\", rev split //, $s)");
22885    }
22886
22887    /// C-style `for` with explicit init / cond / decrement step —
22888    /// heap-sort's sift-down walks the heap children from end down.
22889    #[test]
22890    fn cstyle_for_decrement_with_arrayref_swap_parses() {
22891        let _g = NoInteropGuard::on();
22892        parse_ok(
22893            "my @arr = (1, 2, 3); my $r = \\@arr; \
22894             for (my $end = len(@arr) - 1; $end > 0; $end--) { \
22895                ($r->[0], $r->[$end]) = ($r->[$end], $r->[0]) \
22896             }",
22897        );
22898    }
22899
22900    /// `shift @q` inside a while-loop driving a BFS-style queue —
22901    /// Kahn's topological sort.
22902    #[test]
22903    fn shift_in_while_loop_parses() {
22904        let _g = NoInteropGuard::on();
22905        parse_ok(
22906            "my @q = (0, 1, 2); my @out; \
22907             while (len(@q) > 0) { my $u = shift @q; push @out, $u }",
22908        );
22909    }
22910
22911    /// Modular exponentiation by squaring — Miller-Rabin's core. While
22912    /// loop with `int($e / 2)`, modular multiply, and conditional update.
22913    #[test]
22914    fn mod_pow_squaring_loop_parses() {
22915        let _g = NoInteropGuard::on();
22916        parse_ok(
22917            "fn MR::mod_pow($base, $exp, $m) { \
22918                my $result = 1; my $bs = $base % $m; my $e = $exp; \
22919                while ($e > 0) { \
22920                    $result = ($result * $bs) % $m if $e % 2 == 1; \
22921                    $e = int($e / 2); \
22922                    $bs = ($bs * $bs) % $m \
22923                } \
22924                $result \
22925             }",
22926        );
22927    }
22928
22929    /// 2-D DP traceback: walk back from dp[n][target], conditionally
22930    /// taking or skipping each item. Subset-sum reconstruct shape.
22931    #[test]
22932    fn dp_traceback_walk_parses() {
22933        let _g = NoInteropGuard::on();
22934        parse_ok(
22935            "my @xs = (1, 2, 3); my $n = 3; my $target = 4; \
22936             my @dp; for my $i (0:$n) { $dp[$i] = [(0) x ($target + 1)] } \
22937             my @out; my $j = $target; \
22938             for (my $i = $n; $i > 0; $i--) { \
22939                my $v = $xs[$i - 1]; \
22940                if ($j >= $v && $dp[$i - 1][$j - $v] == 1) { \
22941                    unshift @out, $v; $j -= $v \
22942                } \
22943             }",
22944        );
22945    }
22946
22947    /// Binary search inside a `for` loop — LIS patience-sort variant
22948    /// using `tails` array. while-loop with `int(($lo+$hi)/2)`.
22949    #[test]
22950    fn binary_search_in_for_loop_parses() {
22951        let _g = NoInteropGuard::on();
22952        parse_ok(
22953            "my @xs = (3, 1, 4, 1, 5); my @tails; \
22954             for my $x (@xs) { \
22955                my $lo = 0; my $hi = len @tails; \
22956                while ($lo < $hi) { \
22957                    my $mid = int(($lo + $hi) / 2); \
22958                    if ($tails[$mid] < $x) { $lo = $mid + 1 } else { $hi = $mid } \
22959                } \
22960                if ($lo == len @tails) { push @tails, $x } else { $tails[$lo] = $x } \
22961             }",
22962        );
22963    }
22964
22965    /// Edge-relaxation loop with destructure + early-skip — Bellman-Ford
22966    /// shape. Tests `my ($u, $w, $cost) = @$e` inside a for-loop.
22967    #[test]
22968    fn edge_relaxation_destructure_parses() {
22969        let _g = NoInteropGuard::on();
22970        parse_ok(
22971            "my @edges = ([0, 1, 5], [1, 2, -3]); \
22972             my @dist = (0, 1e18, 1e18); my $INF = 1e18; \
22973             for my $e (@edges) { \
22974                my ($u, $w, $cost) = @$e; \
22975                next if $dist[$u] >= $INF; \
22976                $dist[$w] = $dist[$u] + $cost if $dist[$u] + $cost < $dist[$w] \
22977             }",
22978        );
22979    }
22980
22981    /// Bitwise `&` with negation: `$x & -$x` — Fenwick tree's
22982    /// lowest-set-bit isolation.
22983    #[test]
22984    fn bitwise_and_with_negation_parses() {
22985        let _g = NoInteropGuard::on();
22986        parse_ok("fn Fenwick::lsb($x) = $x & -$x");
22987        parse_ok("my $k = 12; my $lo_bit = $k & -$k");
22988    }
22989
22990    /// `@count[v] = old_v; running += c` — counting-sort cumulative
22991    /// transform. Tests serial assign-and-update inside a for loop.
22992    #[test]
22993    fn counting_sort_cumulative_loop_parses() {
22994        let _g = NoInteropGuard::on();
22995        parse_ok(
22996            "my @count = (3, 1, 2, 4); my $running = 0; \
22997             for my $v (0:3) { \
22998                my $c = $count[$v]; \
22999                $count[$v] = $running; \
23000                $running += $c \
23001             }",
23002        );
23003    }
23004
23005    /// Recursive tree walk with hashref nodes — Huffman tree traversal
23006    /// pattern. Validates that `defined $node` + `exists $node->{sym}`
23007    /// + child-recursion all parse cleanly.
23008    #[test]
23009    fn recursive_tree_walk_with_hashref_parses() {
23010        let _g = NoInteropGuard::on();
23011        parse_ok(
23012            "fn Huff::walk($node, $prefix, $codes) { \
23013                return unless defined $node; \
23014                if (exists $node->{sym}) { \
23015                    $codes->{$node->{sym}} = $prefix eq \"\" ? \"0\" : $prefix; \
23016                    return \
23017                } \
23018                Huff::walk($node->{left},  $prefix . \"0\", $codes); \
23019                Huff::walk($node->{right}, $prefix . \"1\", $codes) \
23020             }",
23021        );
23022    }
23023
23024    /// Z-array maintained-window arithmetic — three-way min via
23025    /// ternary, increment-while-match. Z-algorithm core.
23026    #[test]
23027    fn z_array_window_arithmetic_parses() {
23028        let _g = NoInteropGuard::on();
23029        parse_ok(
23030            "my @c = (\"a\", \"b\", \"a\"); my $n = 3; \
23031             my @z = (0) x $n; $z[0] = $n; \
23032             my $l = 0; my $r = 0; my $i = 1; \
23033             if ($i < $r) { \
23034                my $inside = $r - $i < $z[$i - $l] ? $r - $i : $z[$i - $l]; \
23035                $z[$i] = $inside \
23036             } \
23037             while ($i + $z[$i] < $n && $c[$z[$i]] eq $c[$i + $z[$i]]) { $z[$i]++ }",
23038        );
23039    }
23040
23041    /// BFS expansion with parent-linked hashref nodes — A* /
23042    /// general pathfinding shape. Inner `for` over neighbor offsets.
23043    #[test]
23044    fn bfs_with_parent_link_node_parses() {
23045        let _g = NoInteropGuard::on();
23046        parse_ok(
23047            "my @open = (+{ r => 0, c => 0, g => 0, parent => undef }); \
23048             while (len(@open) > 0) { \
23049                my $cur = shift @open; \
23050                for my $d ([-1, 0], [1, 0], [0, -1], [0, 1]) { \
23051                    my ($dr, $dc) = @$d; \
23052                    push @open, +{ \
23053                        r => $cur->{r} + $dr, c => $cur->{c} + $dc, \
23054                        g => $cur->{g} + 1, parent => $cur \
23055                    } \
23056                } \
23057                last \
23058             }",
23059        );
23060    }
23061
23062    /// Cross-product sort comparator with collinear tiebreak —
23063    /// Graham scan's polar sort.
23064    #[test]
23065    fn cross_product_sort_comparator_parses() {
23066        let _g = NoInteropGuard::on();
23067        parse_ok(
23068            "fn Hull::cross($p1, $p2, $p3) = \
23069                ($p2->[0] - $p1->[0]) * ($p3->[1] - $p1->[1]) - \
23070                ($p2->[1] - $p1->[1]) * ($p3->[0] - $p1->[0]); \
23071             my $pivot = [0, 0]; my @pts = ([1, 1], [2, 0]); \
23072             my @sorted = sort { \
23073                my $c = Hull::cross($pivot, _0, _1); \
23074                $c == 0 ? 0 : ($c < 0 ? 1 : -1) \
23075             } @pts",
23076        );
23077    }
23078
23079    /// Lomuto partition + recursive bisection — quickselect's loop.
23080    /// Inner loop with `$i++` + swap on each match.
23081    #[test]
23082    fn lomuto_partition_loop_parses() {
23083        let _g = NoInteropGuard::on();
23084        parse_ok(
23085            "fn QS::partition($arr, $lo, $hi) { \
23086                my $pivot = $arr->[$hi]; \
23087                my $i = $lo - 1; \
23088                for my $j ($lo:$hi - 1) { \
23089                    if ($arr->[$j] <= $pivot) { \
23090                        $i++; \
23091                        ($arr->[$i], $arr->[$j]) = ($arr->[$j], $arr->[$i]) \
23092                    } \
23093                } \
23094                ($arr->[$i + 1], $arr->[$hi]) = ($arr->[$hi], $arr->[$i + 1]); \
23095                $i + 1 \
23096             }",
23097        );
23098    }
23099
23100    /// `do { x } while (cond)` shape with diff-then-gcd — Pollard rho's
23101    /// tortoise-and-hare loop. Tests absolute-difference via ternary.
23102    #[test]
23103    fn tortoise_hare_diff_loop_parses() {
23104        let _g = NoInteropGuard::on();
23105        parse_ok(
23106            "my $x = 2; my $y = 2; my $n = 35; my $d = 1; \
23107             while ($d == 1) { \
23108                $x = ($x * $x + 1) % $n; \
23109                $y = ($y * $y + 1) % $n; \
23110                $y = ($y * $y + 1) % $n; \
23111                my $diff = $x > $y ? $x - $y : $y - $x; \
23112                $d = $diff \
23113             }",
23114        );
23115    }
23116
23117    /// Recursive ext_gcd returning a 3-tuple via arrayref destructure.
23118    /// Modular-inverse pattern.
23119    #[test]
23120    fn ext_gcd_recursive_destructure_parses() {
23121        let _g = NoInteropGuard::on();
23122        parse_ok(
23123            "fn Mod::ext_gcd($va, $vb) { \
23124                return [$va, 1, 0] if $vb == 0; \
23125                my $r = Mod::ext_gcd($vb, $va % $vb); \
23126                my ($g, $x1, $y1) = @$r; \
23127                [$g, $y1, $x1 - int($va / $vb) * $y1] \
23128             }",
23129        );
23130    }
23131
23132    /// Convolution-recurrence DP — Catalan number computation.
23133    /// Inner accumulator with mult on each iteration.
23134    #[test]
23135    fn convolution_recurrence_dp_parses() {
23136        let _g = NoInteropGuard::on();
23137        parse_ok(
23138            "my @c = (1); my $n = 5; \
23139             for my $i (1:$n) { \
23140                my $sum = 0; \
23141                for my $j (0:$i - 1) { $sum += $c[$j] * $c[$i - 1 - $j] } \
23142                push @c, $sum \
23143             }",
23144        );
23145    }
23146
23147    /// Tarjan SCC state: shared mutable hashref carries the entire
23148    /// algorithm's bookkeeping (`index`, `idx_of`, `low_of`,
23149    /// `on_stack`, `stack`, `sccs`) — passed by reference to the
23150    /// recursive worker.
23151    #[test]
23152    fn tarjan_scc_shared_state_parses() {
23153        let _g = NoInteropGuard::on();
23154        parse_ok(
23155            "fn SCC::strong_connect($s, $v) { \
23156                $s->{idx_of}{$v} = $s->{index}; \
23157                $s->{low_of}{$v} = $s->{index}; \
23158                $s->{index}++; \
23159                push @{$s->{stack}}, $v; \
23160                $s->{on_stack}{$v} = 1; \
23161                for my $w (@{$s->{adj}{$v}}) { \
23162                    if (!exists $s->{idx_of}{$w}) { \
23163                        SCC::strong_connect($s, $w); \
23164                        $s->{low_of}{$v} = $s->{low_of}{$w} if $s->{low_of}{$w} < $s->{low_of}{$v} \
23165                    } \
23166                } \
23167             }",
23168        );
23169    }
23170
23171    /// Heap's algorithm permutation generator — recursive in-place
23172    /// swap with parity-conditional pivot choice.
23173    #[test]
23174    fn heaps_algorithm_recursive_parses() {
23175        let _g = NoInteropGuard::on();
23176        parse_ok(
23177            "fn Perm::heaps_inner($arr, $n, $out) { \
23178                if ($n == 1) { my @snap = @$arr; push @$out, \\@snap; return } \
23179                for my $i (0:$n - 1) { \
23180                    Perm::heaps_inner($arr, $n - 1, $out); \
23181                    if ($n % 2 == 0) { \
23182                        ($arr->[$i], $arr->[$n - 1]) = ($arr->[$n - 1], $arr->[$i]) \
23183                    } else { \
23184                        ($arr->[0], $arr->[$n - 1]) = ($arr->[$n - 1], $arr->[0]) \
23185                    } \
23186                } \
23187             }",
23188        );
23189    }
23190
23191    /// `format` is a Perl FORMAT-declaration keyword. The lexer must
23192    /// NOT eat `format` when it appears as a hash key
23193    /// (`$h{format}`, `{format => ...}`), a method name
23194    /// (`$obj->format`), a namespaced tail (`Foo::format`), or a
23195    /// list/expr item with terminator follow-up. Previously
23196    /// `$opts{format} = "csv"` triggered "Expected '=' after format
23197    /// name" because the lexer greedily entered format-decl mode.
23198    #[test]
23199    fn format_as_hash_key_parses() {
23200        parse_ok("my %opts; $opts{format} = \"csv\"");
23201        parse_ok("my %opts = (format => \"csv\", level => 9)");
23202        parse_ok("my $h = +{ format => \"csv\" }");
23203        parse_ok("my @keys = ($h->{format}, $h->{level})");
23204    }
23205
23206    /// `format` after `->` is a method name, not the format keyword.
23207    #[test]
23208    fn format_as_method_call_parses() {
23209        parse_ok("class Foo { val: Str; fn format($self) { \"x\" } } my $f = Foo(val => \"y\"); my $s = $f->format()");
23210    }
23211
23212    /// `format` after `::` is a namespaced fn name tail.
23213    #[test]
23214    fn format_as_namespaced_tail_parses() {
23215        parse_ok("fn Foo::format($x) = $x . \"!\"");
23216        parse_ok("fn Foo::format($x) = $x . \"!\"; my $r = Foo::format(\"hi\")");
23217    }
23218
23219    /// Compound-assign on hash arrow-deref leaves the new value on the
23220    /// stack (uses `SetArrowHashKeep`, not `SetArrowHash`). Previously
23221    /// the no-keep variant left nothing for the statement-level Pop,
23222    /// which then ate a slot from the CALLER's stack frame — corrupting
23223    /// `dec($h) + dec($h) + dec($h)`-style multi-call expressions.
23224    /// See tests/suite/hashref_assignment_pin.rs for runtime pins.
23225    #[test]
23226    fn arrow_hash_compound_assign_parses_all_ops() {
23227        let _g = NoInteropGuard::on();
23228        parse_ok("my $h = +{n=>10}; $h->{n} -= 1");
23229        parse_ok("my $h = +{n=>10}; $h->{n} += 1");
23230        parse_ok("my $h = +{n=>10}; $h->{n} *= 2");
23231        parse_ok("my $h = +{n=>10}; $h->{n} /= 2");
23232        parse_ok("my $h = +{n=>10}; $h->{n} %= 3");
23233        parse_ok("my $h = +{n=>\"x\"}; $h->{n} .= \"y\"");
23234    }
23235
23236    /// The compound-assign yields the NEW value as its expression
23237    /// result, same as plain `$h->{k} = v` does. Validated at parse
23238    /// time by accepting the use-as-rvalue shape `my $v = $h->{n} -= 1`.
23239    #[test]
23240    fn arrow_hash_compound_assign_value_chains_parses() {
23241        let _g = NoInteropGuard::on();
23242        parse_ok("my $h = +{n=>10}; my $v = $h->{n} -= 1");
23243        parse_ok("my $h = +{n=>10}; my @list = ($h->{n} += 5, $h->{n} += 5)");
23244        parse_ok("my $h = +{n=>10}; my $double = ($h->{n} -= 1) * 2");
23245    }
23246}
23247
23248// ── zsh parameter expansion inside double-quoted strings ─────────────────────
23249// Detection + lowering for `${ … }` zsh parameter-expansion forms. The actual
23250// expansion is performed at runtime by zshrs's `paramsubst` (via the `zpexpand`
23251// builtin) — these helpers only recognize the form and gather which variables
23252// to bridge. Gated off under `--compat` at the call site.
23253
23254/// True when a `${ … }` body is a zsh parameter expansion rather than a Perl
23255/// `${name}` / `${$ref}` / `${EXPR}`: a `(flags)name` or `${#name}` length
23256/// prefix, or a name immediately followed by a zsh operator (`:`, `#`, `%`, `/`).
23257fn zsh_param_form(body: &str) -> bool {
23258    let b = body.as_bytes();
23259    if b.is_empty() || b[0] == b'$' || b[0] == b'\\' {
23260        return false;
23261    }
23262    if b[0] == b'(' {
23263        // `(flags)name` — flags then an identifier / positional / `$`.
23264        if let Some(rp) = body.find(')') {
23265            return body[rp + 1..]
23266                .bytes()
23267                .next()
23268                .map_or(false, |c| c.is_ascii_alphanumeric() || c == b'_' || c == b'$');
23269        }
23270        return false;
23271    }
23272    if b[0] == b'#' {
23273        // `${#name}` length.
23274        return body[1..]
23275            .bytes()
23276            .next()
23277            .map_or(false, |c| c.is_ascii_alphanumeric() || c == b'_');
23278    }
23279    let mut i = 0;
23280    while i < b.len() && (b[i].is_ascii_alphanumeric() || b[i] == b'_') {
23281        i += 1;
23282    }
23283    if i == 0 {
23284        return false;
23285    }
23286    match b.get(i) {
23287        // `:-`/`:=`/`:?`/`:+`/`:off` — but NOT `::` (Perl package qualifier, e.g.
23288        // `${main::cfg}`, which must stay Perl interpolation).
23289        Some(b':') => b.get(i + 1) != Some(&b':'),
23290        // `#`/`##` trim, `%`/`%%` trim, `/`//` substitution, `[…]` subscript.
23291        Some(b'#') | Some(b'%') | Some(b'/') | Some(b'[') => true,
23292        _ => false,
23293    }
23294}
23295
23296/// Lower a zsh `${ body }` form to `zpexpand('${body}', "name", $name, …)` — the
23297/// raw template (single-quoted so stryke leaves it intact) plus each referenced
23298/// variable so the builtin can bridge their values into zshrs's parameter table.
23299fn build_zpexpand_expr(body: &str, line: usize) -> Expr {
23300    let mut args = vec![Expr {
23301        kind: ExprKind::String(format!("${{{}}}", body)),
23302        line,
23303    }];
23304    // A name with a `[…]` subscript is an array — bridge it as `@name` so the
23305    // subscript / subscript-flag (`(i)`,`(r)`,…) has an array to index.
23306    let arrays = subscripted_names(body);
23307    for name in zsh_referenced_params(body) {
23308        args.push(Expr {
23309            kind: ExprKind::String(name.clone()),
23310            line,
23311        });
23312        // Arrays are passed by reference (`\@name`, lowered to `ScalarRef`) so
23313        // the list does not flatten into the call's argument pairs; the builtin
23314        // derefs them.
23315        let val = if arrays.contains(&name) {
23316            ExprKind::ScalarRef(Box::new(Expr {
23317                kind: ExprKind::ArrayVar(name),
23318                line,
23319            }))
23320        } else {
23321            ExprKind::ScalarVar(name)
23322        };
23323        args.push(Expr { kind: val, line });
23324    }
23325    Expr {
23326        kind: ExprKind::FuncCall {
23327            name: "zpexpand".to_string(),
23328            args,
23329        },
23330        line,
23331    }
23332}
23333
23334/// Names that appear with a `[` subscript in a `${ … }` body (so they must be
23335/// bridged as arrays).
23336fn subscripted_names(body: &str) -> Vec<String> {
23337    let b = body.as_bytes();
23338    let mut out = Vec::new();
23339    let mut i = 0;
23340    while i < b.len() {
23341        if b[i].is_ascii_alphabetic() || b[i] == b'_' {
23342            let s = i;
23343            while i < b.len() && (b[i].is_ascii_alphanumeric() || b[i] == b'_') {
23344                i += 1;
23345            }
23346            if b.get(i) == Some(&b'[') {
23347                out.push(body[s..i].to_string());
23348            }
23349        } else {
23350            i += 1;
23351        }
23352    }
23353    out
23354}
23355
23356/// Variables referenced by a `${ … }` body: the leading parameter (after any
23357/// `(flags)` / `#` prefix) plus every `$name` mentioned in the operator/word.
23358fn zsh_referenced_params(inner: &str) -> Vec<String> {
23359    let b = inner.as_bytes();
23360    let mut out = Vec::new();
23361    let mut i = 0;
23362    if b.first() == Some(&b'(') {
23363        if let Some(rp) = inner.find(')') {
23364            i = rp + 1;
23365        }
23366    }
23367    while i < b.len() && b[i] == b'#' {
23368        i += 1;
23369    }
23370    let s = i;
23371    while i < b.len() && (b[i].is_ascii_alphanumeric() || b[i] == b'_') {
23372        i += 1;
23373    }
23374    if i > s {
23375        out.push(inner[s..i].to_string());
23376    }
23377    while i < b.len() {
23378        if b[i] == b'$' && i + 1 < b.len() && (b[i + 1].is_ascii_alphabetic() || b[i + 1] == b'_') {
23379            let ds = i + 1;
23380            let mut de = ds;
23381            while de < b.len() && (b[de].is_ascii_alphanumeric() || b[de] == b'_') {
23382                de += 1;
23383            }
23384            out.push(inner[ds..de].to_string());
23385            i = de;
23386        } else {
23387            i += 1;
23388        }
23389    }
23390    out.sort();
23391    out.dedup();
23392    out
23393}