Skip to main content

stryke/
ast.rs

1//! AST node types for the Perl 5 interpreter.
2//! Every node carries a `line` field for error reporting.
3
4use serde::{Deserialize, Serialize};
5
6fn default_delim() -> char {
7    '/'
8}
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Program {
12    pub statements: Vec<Statement>,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Statement {
17    /// Leading `LABEL:` on this statement (Perl convention: `FOO:`).
18    pub label: Option<String>,
19    pub kind: StmtKind,
20    pub line: usize,
21}
22
23impl Statement {
24    pub fn new(kind: StmtKind, line: usize) -> Self {
25        Self {
26            label: None,
27            kind,
28            line,
29        }
30    }
31}
32
33/// Surface spelling for `grep` / `greps` / `filter` (`fi`) / `find_all`.
34/// `grep` is eager (Perl-compatible); `greps` / `filter` / `find_all` are lazy (streaming).
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "snake_case")]
37#[derive(Default)]
38pub enum GrepBuiltinKeyword {
39    #[default]
40    Grep,
41    Greps,
42    Filter,
43    FindAll,
44}
45
46impl GrepBuiltinKeyword {
47    pub const fn as_str(self) -> &'static str {
48        match self {
49            Self::Grep => "grep",
50            Self::Greps => "greps",
51            Self::Filter => "filter",
52            Self::FindAll => "find_all",
53        }
54    }
55
56    /// Returns `true` for streaming variants (`greps`, `filter`, `find_all`).
57    pub const fn is_stream(self) -> bool {
58        !matches!(self, Self::Grep)
59    }
60}
61
62/// Named parameter in `sub name (SIG ...) { }` — stryke extension (not Perl 5 prototype syntax).
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub enum SubSigParam {
65    /// `$name`, `$name: Type`, or `$name = default` — one positional scalar from `@_`,
66    /// optionally typed and/or with a default value.
67    Scalar(String, Option<PerlTypeName>, Option<Box<Expr>>),
68    /// `@name` or `@name = (default, list)` — slurps remaining positional args into an array.
69    Array(String, Option<Box<Expr>>),
70    /// `%name` or `%name = (key => val, ...)` — slurps remaining positional args into a hash.
71    Hash(String, Option<Box<Expr>>),
72    /// `[ $a, @tail, ... ]` — next argument must be array-like; same element rules as algebraic `match`.
73    ArrayDestruct(Vec<MatchArrayElem>),
74    /// `{ k => $v, ... }` — next argument must be a hash or hashref; keys bind to listed scalars.
75    HashDestruct(Vec<(String, String)>),
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub enum StmtKind {
80    Expression(Expr),
81    If {
82        condition: Expr,
83        body: Block,
84        elsifs: Vec<(Expr, Block)>,
85        else_block: Option<Block>,
86    },
87    Unless {
88        condition: Expr,
89        body: Block,
90        else_block: Option<Block>,
91    },
92    While {
93        condition: Expr,
94        body: Block,
95        label: Option<String>,
96        /// `while (...) { } continue { }`
97        continue_block: Option<Block>,
98    },
99    Until {
100        condition: Expr,
101        body: Block,
102        label: Option<String>,
103        continue_block: Option<Block>,
104    },
105    DoWhile {
106        body: Block,
107        condition: Expr,
108    },
109    For {
110        init: Option<Box<Statement>>,
111        condition: Option<Expr>,
112        step: Option<Expr>,
113        body: Block,
114        label: Option<String>,
115        continue_block: Option<Block>,
116    },
117    Foreach {
118        var: String,
119        list: Expr,
120        body: Block,
121        label: Option<String>,
122        continue_block: Option<Block>,
123    },
124    SubDecl {
125        name: String,
126        params: Vec<SubSigParam>,
127        body: Block,
128        /// Subroutine prototype text from `sub foo ($$) { }` (excluding parens).
129        /// `None` when using structured [`SubSigParam`] signatures instead.
130        prototype: Option<String>,
131    },
132    Package {
133        name: String,
134    },
135    Use {
136        module: String,
137        imports: Vec<Expr>,
138    },
139    /// `use 5.008;` / `use 5;` — Perl version requirement (no-op at runtime in stryke).
140    UsePerlVersion {
141        version: f64,
142    },
143    /// `use overload '""' => 'as_string', '+' => 'add';` — operator maps (method names in current package).
144    UseOverload {
145        pairs: Vec<(String, String)>,
146    },
147    No {
148        module: String,
149        imports: Vec<Expr>,
150    },
151    Return(Option<Expr>),
152    Last(Option<String>),
153    Next(Option<String>),
154    Redo(Option<String>),
155    My(Vec<VarDecl>),
156    Our(Vec<VarDecl>),
157    Local(Vec<VarDecl>),
158    /// `state $x = 0` — persistent lexical variable (initialized once per sub)
159    State(Vec<VarDecl>),
160    /// `local $h{k}` / `local $SIG{__WARN__}` — lvalues that are not plain `my`-style names.
161    LocalExpr {
162        target: Expr,
163        initializer: Option<Expr>,
164    },
165    /// `mysync $x = 0` — thread-safe atomic variable for parallel blocks
166    MySync(Vec<VarDecl>),
167    /// `oursync $x = 0` — package-global thread-safe atomic variable. Same as
168    /// `mysync` but the binding lives in the package stash (e.g. `main::x`)
169    /// so it is visible across packages and parallel workers share one cell.
170    OurSync(Vec<VarDecl>),
171    /// Bare block (for scoping or do {})
172    Block(Block),
173    /// Statements run in order without an extra scope frame (parser desugar).
174    StmtGroup(Block),
175    /// `BEGIN { ... }`
176    Begin(Block),
177    /// `END { ... }`
178    End(Block),
179    /// `UNITCHECK { ... }` — end of compilation unit (reverse order before CHECK).
180    UnitCheck(Block),
181    /// `CHECK { ... }` — end of compile phase (reverse order).
182    Check(Block),
183    /// `INIT { ... }` — before runtime main (forward order).
184    Init(Block),
185    /// Empty statement (bare semicolon)
186    Empty,
187    /// `goto EXPR` — expression evaluates to a label name in the same block.
188    Goto {
189        target: Box<Expr>,
190    },
191    /// Standalone `continue { BLOCK }` (normally follows a loop; parsed for acceptance).
192    Continue(Block),
193    /// `struct Name { field => Type, ... }` — fixed-field records (`Name->new`, `$x->field`).
194    StructDecl {
195        def: StructDef,
196    },
197    /// `enum Name { Variant1 => Type, Variant2, ... }` — algebraic data types.
198    EnumDecl {
199        def: EnumDef,
200    },
201    /// `class Name extends Parent impl Trait { fields; methods }` — full OOP.
202    ClassDecl {
203        def: ClassDef,
204    },
205    /// `trait Name { fn required; fn with_default { } }` — interface/mixin.
206    TraitDecl {
207        def: TraitDef,
208    },
209    /// `eval_timeout SECS { ... }` — run block on a worker thread; main waits up to SECS (portable timeout).
210    EvalTimeout {
211        timeout: Expr,
212        body: Block,
213    },
214    /// `try { } catch ($err) { } [ finally { } ]` — catch runtime/die errors (not `last`/`next`/`return` flow).
215    /// `finally` runs after a successful `try` or after `catch` completes (including if `catch` rethrows).
216    TryCatch {
217        try_block: Block,
218        catch_var: String,
219        catch_block: Block,
220        finally_block: Option<Block>,
221    },
222    /// `given (EXPR) { when ... default ... }` — topic in `$_`, `when` matches with regex / eq / smartmatch.
223    Given {
224        topic: Expr,
225        body: Block,
226    },
227    /// `when (COND) { }` — only valid inside `given` (handled by given dispatcher).
228    When {
229        cond: Expr,
230        body: Block,
231    },
232    /// `default { }` — only valid inside `given`.
233    DefaultCase {
234        body: Block,
235    },
236    /// `tie %hash` / `tie @arr` / `tie $x` — TIEHASH / TIEARRAY / TIESCALAR (FETCH/STORE).
237    Tie {
238        target: TieTarget,
239        class: Expr,
240        args: Vec<Expr>,
241    },
242    /// `format NAME =` picture/value lines … `.` — report templates for `write`.
243    FormatDecl {
244        name: String,
245        lines: Vec<String>,
246    },
247    /// `before|after|around "<glob>" { ... }` — register AOP advice on user subs.
248    /// Pattern is a glob (`*`, `?`) matched against the called sub's bare name.
249    AdviceDecl {
250        kind: AdviceKind,
251        pattern: String,
252        body: Block,
253    },
254}
255
256/// AOP advice kind for [`StmtKind::AdviceDecl`].
257#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
258pub enum AdviceKind {
259    /// Run before the matched sub; sees `INTERCEPT_NAME` / `INTERCEPT_ARGS`.
260    Before,
261    /// Run after the matched sub; sees `INTERCEPT_MS` / `INTERCEPT_US` and the retval in `$?`.
262    After,
263    /// Wrap the matched sub; must call `proceed()` to invoke the original.
264    Around,
265}
266
267/// Target of `tie` (hash, array, or scalar).
268#[derive(Debug, Clone, Serialize, Deserialize)]
269pub enum TieTarget {
270    Hash(String),
271    Array(String),
272    Scalar(String),
273}
274
275/// Optional type for `typed my $x : Int` — enforced at assignment time (runtime).
276#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
277pub enum PerlTypeName {
278    Int,
279    Str,
280    Float,
281    Bool,
282    Array,
283    Hash,
284    Ref,
285    /// Struct-typed field: `field => Point` where Point is a struct name.
286    Struct(String),
287    /// Enum-typed field: `field => Color` where Color is an enum name.
288    Enum(String),
289    /// Accepts any value (no runtime type check).
290    Any,
291}
292
293/// Single field in a struct definition.
294#[derive(Debug, Clone, Serialize, Deserialize)]
295pub struct StructField {
296    pub name: String,
297    pub ty: PerlTypeName,
298    /// Optional default value expression (evaluated at construction time if field not provided).
299    #[serde(skip_serializing_if = "Option::is_none")]
300    pub default: Option<Expr>,
301}
302
303/// Method defined inside a struct: `fn name { ... }` or `fn name($self, ...) { ... }`.
304#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct StructMethod {
306    pub name: String,
307    pub params: Vec<SubSigParam>,
308    pub body: Block,
309}
310
311/// Single variant in an enum definition.
312#[derive(Debug, Clone, Serialize, Deserialize)]
313pub struct EnumVariant {
314    pub name: String,
315    /// Optional type for data carried by this variant. If None, it carries no data.
316    pub ty: Option<PerlTypeName>,
317}
318
319/// Compile-time algebraic data type: `enum Name { Variant1 => Type, Variant2, ... }`.
320#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct EnumDef {
322    pub name: String,
323    pub variants: Vec<EnumVariant>,
324}
325
326impl EnumDef {
327    #[inline]
328    pub fn variant_index(&self, name: &str) -> Option<usize> {
329        self.variants.iter().position(|v| v.name == name)
330    }
331
332    #[inline]
333    pub fn variant(&self, name: &str) -> Option<&EnumVariant> {
334        self.variants.iter().find(|v| v.name == name)
335    }
336}
337
338/// Compile-time record type: `struct Name { field => Type, ... ; fn method { } }`.
339#[derive(Debug, Clone, Serialize, Deserialize)]
340pub struct StructDef {
341    pub name: String,
342    pub fields: Vec<StructField>,
343    /// User-defined methods: `fn name { }` inside struct body.
344    #[serde(default, skip_serializing_if = "Vec::is_empty")]
345    pub methods: Vec<StructMethod>,
346}
347
348/// Visibility modifier for class fields and methods.
349#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
350pub enum Visibility {
351    #[default]
352    Public,
353    Private,
354    Protected,
355}
356
357/// Single field in a class definition: `name: Type = default` or `pub name: Type`.
358#[derive(Debug, Clone, Serialize, Deserialize)]
359pub struct ClassField {
360    pub name: String,
361    pub ty: PerlTypeName,
362    pub visibility: Visibility,
363    #[serde(skip_serializing_if = "Option::is_none")]
364    pub default: Option<Expr>,
365}
366
367/// Method defined inside a class: `fn name { }` or `pub fn name($self, ...) { }`.
368#[derive(Debug, Clone, Serialize, Deserialize)]
369pub struct ClassMethod {
370    pub name: String,
371    pub params: Vec<SubSigParam>,
372    pub body: Option<Block>,
373    pub visibility: Visibility,
374    pub is_static: bool,
375    #[serde(default, skip_serializing_if = "is_false")]
376    pub is_final: bool,
377}
378
379/// Trait definition: `trait Name { fn required; fn with_default { } }`.
380#[derive(Debug, Clone, Serialize, Deserialize)]
381pub struct TraitDef {
382    pub name: String,
383    pub methods: Vec<ClassMethod>,
384}
385
386impl TraitDef {
387    #[inline]
388    pub fn method(&self, name: &str) -> Option<&ClassMethod> {
389        self.methods.iter().find(|m| m.name == name)
390    }
391
392    #[inline]
393    pub fn required_methods(&self) -> impl Iterator<Item = &ClassMethod> {
394        self.methods.iter().filter(|m| m.body.is_none())
395    }
396}
397
398/// A static (class-level) variable: `static count: Int = 0`.
399#[derive(Debug, Clone, Serialize, Deserialize)]
400pub struct ClassStaticField {
401    pub name: String,
402    pub ty: PerlTypeName,
403    pub visibility: Visibility,
404    #[serde(skip_serializing_if = "Option::is_none")]
405    pub default: Option<Expr>,
406}
407
408/// Class definition: `class Name extends Parent impl Trait { fields; methods }`.
409#[derive(Debug, Clone, Serialize, Deserialize)]
410pub struct ClassDef {
411    pub name: String,
412    #[serde(default, skip_serializing_if = "is_false")]
413    pub is_abstract: bool,
414    #[serde(default, skip_serializing_if = "is_false")]
415    pub is_final: bool,
416    #[serde(default, skip_serializing_if = "Vec::is_empty")]
417    pub extends: Vec<String>,
418    #[serde(default, skip_serializing_if = "Vec::is_empty")]
419    pub implements: Vec<String>,
420    pub fields: Vec<ClassField>,
421    pub methods: Vec<ClassMethod>,
422    #[serde(default, skip_serializing_if = "Vec::is_empty")]
423    pub static_fields: Vec<ClassStaticField>,
424}
425
426fn is_false(v: &bool) -> bool {
427    !*v
428}
429
430impl ClassDef {
431    #[inline]
432    pub fn field_index(&self, name: &str) -> Option<usize> {
433        self.fields.iter().position(|f| f.name == name)
434    }
435
436    #[inline]
437    pub fn field(&self, name: &str) -> Option<&ClassField> {
438        self.fields.iter().find(|f| f.name == name)
439    }
440
441    #[inline]
442    pub fn method(&self, name: &str) -> Option<&ClassMethod> {
443        self.methods.iter().find(|m| m.name == name)
444    }
445
446    #[inline]
447    pub fn static_methods(&self) -> impl Iterator<Item = &ClassMethod> {
448        self.methods.iter().filter(|m| m.is_static)
449    }
450
451    #[inline]
452    pub fn instance_methods(&self) -> impl Iterator<Item = &ClassMethod> {
453        self.methods.iter().filter(|m| !m.is_static)
454    }
455}
456
457impl StructDef {
458    #[inline]
459    pub fn field_index(&self, name: &str) -> Option<usize> {
460        self.fields.iter().position(|f| f.name == name)
461    }
462
463    /// Get field type by name.
464    #[inline]
465    pub fn field_type(&self, name: &str) -> Option<&PerlTypeName> {
466        self.fields.iter().find(|f| f.name == name).map(|f| &f.ty)
467    }
468
469    /// Get method by name.
470    #[inline]
471    pub fn method(&self, name: &str) -> Option<&StructMethod> {
472        self.methods.iter().find(|m| m.name == name)
473    }
474}
475
476impl PerlTypeName {
477    /// Bytecode encoding for `DeclareScalarTyped` / VM (only simple types; struct types use name pool).
478    #[inline]
479    pub fn from_byte(b: u8) -> Option<Self> {
480        match b {
481            0 => Some(Self::Int),
482            1 => Some(Self::Str),
483            2 => Some(Self::Float),
484            3 => Some(Self::Bool),
485            4 => Some(Self::Array),
486            5 => Some(Self::Hash),
487            6 => Some(Self::Ref),
488            7 => Some(Self::Any),
489            _ => None,
490        }
491    }
492
493    /// Bytecode encoding (simple types only; `Struct(name)` / `Enum(name)` requires separate name pool lookup).
494    #[inline]
495    pub fn as_byte(&self) -> Option<u8> {
496        match self {
497            Self::Int => Some(0),
498            Self::Str => Some(1),
499            Self::Float => Some(2),
500            Self::Bool => Some(3),
501            Self::Array => Some(4),
502            Self::Hash => Some(5),
503            Self::Ref => Some(6),
504            Self::Any => Some(7),
505            Self::Struct(_) | Self::Enum(_) => None,
506        }
507    }
508
509    /// Display name for error messages.
510    pub fn display_name(&self) -> String {
511        match self {
512            Self::Int => "Int".to_string(),
513            Self::Str => "Str".to_string(),
514            Self::Float => "Float".to_string(),
515            Self::Bool => "Bool".to_string(),
516            Self::Array => "Array".to_string(),
517            Self::Hash => "Hash".to_string(),
518            Self::Ref => "Ref".to_string(),
519            Self::Any => "Any".to_string(),
520            Self::Struct(name) => name.clone(),
521            Self::Enum(name) => name.clone(),
522        }
523    }
524
525    /// Strict runtime check: `Int` only integer-like [`StrykeValue`](crate::value::StrykeValue), `Str` only string, `Float` allows int or float.
526    pub fn check_value(&self, v: &crate::value::StrykeValue) -> Result<(), String> {
527        match self {
528            Self::Int => {
529                if v.is_integer_like() {
530                    Ok(())
531                } else {
532                    Err(format!("expected Int (INTEGER), got {}", v.type_name()))
533                }
534            }
535            Self::Str => {
536                if v.is_string_like() {
537                    Ok(())
538                } else {
539                    Err(format!("expected Str (STRING), got {}", v.type_name()))
540                }
541            }
542            Self::Float => {
543                if v.is_integer_like() || v.is_float_like() {
544                    Ok(())
545                } else {
546                    Err(format!(
547                        "expected Float (INTEGER or FLOAT), got {}",
548                        v.type_name()
549                    ))
550                }
551            }
552            Self::Bool => Ok(()),
553            Self::Array => {
554                if v.as_array_vec().is_some() || v.as_array_ref().is_some() {
555                    Ok(())
556                } else {
557                    Err(format!("expected Array, got {}", v.type_name()))
558                }
559            }
560            Self::Hash => {
561                if v.as_hash_map().is_some() || v.as_hash_ref().is_some() {
562                    Ok(())
563                } else {
564                    Err(format!("expected Hash, got {}", v.type_name()))
565                }
566            }
567            Self::Ref => {
568                if v.as_scalar_ref().is_some()
569                    || v.as_array_ref().is_some()
570                    || v.as_hash_ref().is_some()
571                    || v.as_code_ref().is_some()
572                {
573                    Ok(())
574                } else {
575                    Err(format!("expected Ref, got {}", v.type_name()))
576                }
577            }
578            Self::Struct(name) => {
579                // Allow undef for struct/class types (nullable pattern)
580                if v.is_undef() {
581                    return Ok(());
582                }
583                if let Some(s) = v.as_struct_inst() {
584                    if s.def.name == *name {
585                        Ok(())
586                    } else {
587                        Err(format!(
588                            "expected struct {}, got struct {}",
589                            name, s.def.name
590                        ))
591                    }
592                } else if let Some(e) = v.as_enum_inst() {
593                    if e.def.name == *name {
594                        Ok(())
595                    } else {
596                        Err(format!("expected {}, got enum {}", name, e.def.name))
597                    }
598                } else if let Some(c) = v.as_class_inst() {
599                    // Check class name and full inheritance hierarchy
600                    if c.isa(name) {
601                        Ok(())
602                    } else {
603                        Err(format!("expected {}, got {}", name, c.def.name))
604                    }
605                } else if let Some(b) = v.as_blessed_ref() {
606                    // Old-style `bless {...}, "Class"` — accept as the
607                    // nominal type if the class name matches. Lets typed-
608                    // my survive any escape hatch that reaches the value
609                    // through the Perl 5 OO path.
610                    if b.class == *name {
611                        Ok(())
612                    } else {
613                        Err(format!("expected {}, got {}", name, b.class))
614                    }
615                } else {
616                    Err(format!("expected {}, got {}", name, v.type_name()))
617                }
618            }
619            Self::Enum(name) => {
620                // Allow undef for enum types (nullable pattern)
621                if v.is_undef() {
622                    return Ok(());
623                }
624                if let Some(e) = v.as_enum_inst() {
625                    if e.def.name == *name {
626                        Ok(())
627                    } else {
628                        Err(format!("expected enum {}, got enum {}", name, e.def.name))
629                    }
630                } else {
631                    Err(format!("expected enum {}, got {}", name, v.type_name()))
632                }
633            }
634            Self::Any => Ok(()),
635        }
636    }
637}
638
639#[derive(Debug, Clone, Serialize, Deserialize)]
640pub struct VarDecl {
641    pub sigil: Sigil,
642    pub name: String,
643    pub initializer: Option<Expr>,
644    /// Set by `frozen my ...` — reassignments are rejected at compile time (bytecode) or runtime.
645    pub frozen: bool,
646    /// Set by `typed my $x : Int` (scalar only).
647    pub type_annotation: Option<PerlTypeName>,
648    /// True when declared with parens: `my ($x) = @a` vs `my $x = @a`.
649    /// In list context, a scalar gets the first element; in scalar context, it gets the count.
650    #[serde(default)]
651    pub list_context: bool,
652}
653
654#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
655pub enum Sigil {
656    Scalar,
657    Array,
658    Hash,
659    /// `local *FH` — filehandle slot alias (limited typeglob).
660    Typeglob,
661}
662
663pub type Block = Vec<Statement>;
664
665/// Comparator for `sort` — `{ $a <=> $b }`, or a code ref / expression (Perl `sort $cmp LIST`).
666#[derive(Debug, Clone, Serialize, Deserialize)]
667pub enum SortComparator {
668    Block(Block),
669    Code(Box<Expr>),
670}
671
672// ── Algebraic `match` expression (stryke extension) ──
673
674/// One arm of [`ExprKind::AlgebraicMatch`]: `PATTERN [if EXPR] => EXPR`.
675#[derive(Debug, Clone, Serialize, Deserialize)]
676pub struct MatchArm {
677    pub pattern: MatchPattern,
678    /// Optional guard (`if EXPR`) evaluated after pattern match; `$_` is the match subject.
679    #[serde(skip_serializing_if = "Option::is_none")]
680    pub guard: Option<Box<Expr>>,
681    pub body: Expr,
682}
683
684/// `retry { } backoff => exponential` — sleep policy between attempts (after failure).
685#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
686pub enum RetryBackoff {
687    /// No delay between attempts.
688    None,
689    /// Delay grows linearly: `base_ms * attempt` (attempt starts at 1).
690    Linear,
691    /// Delay doubles each failure: `base_ms * 2^(attempt-1)` (capped).
692    Exponential,
693}
694
695/// Pattern for algebraic `match` (distinct from the `=~` / regex [`ExprKind::Match`]).
696#[derive(Debug, Clone, Serialize, Deserialize)]
697pub enum MatchPattern {
698    /// `_` — matches anything.
699    Any,
700    /// `/regex/` — subject stringified; on success the arm body sets `$_` to the subject and
701    /// populates match variables (`$1`…, `$&`, `${^MATCH}`, `@-`/`@+`, `%+`, …) like `=~`.
702    Regex { pattern: String, flags: String },
703    /// Arbitrary expression compared for equality / smart-match against the subject.
704    Value(Box<Expr>),
705    /// `[1, 2, *]` — prefix elements match; optional `*` matches any tail (must be last).
706    Array(Vec<MatchArrayElem>),
707    /// `{ name => $n, ... }` — required keys; `$n` binds the value for the arm body.
708    Hash(Vec<MatchHashPair>),
709    /// `Some($x)` — matches array-like values with **at least two** elements where index `1` is
710    /// Perl-truthy (stryke: `$gen->next` yields `[value, more]` with `more` truthy while iterating).
711    OptionSome(String),
712}
713
714#[derive(Debug, Clone, Serialize, Deserialize)]
715pub enum MatchArrayElem {
716    Expr(Expr),
717    /// `$name` at the top of a pattern element — bind this position to a new lexical `$name`.
718    /// Use `[($x)]` if you need smartmatch against the current value of `$x` instead.
719    CaptureScalar(String),
720    /// Rest-of-array wildcard (only valid as the last element).
721    Rest,
722    /// `@name` — bind remaining elements as a new array to `@name` (only valid as the last element).
723    RestBind(String),
724}
725
726#[derive(Debug, Clone, Serialize, Deserialize)]
727pub enum MatchHashPair {
728    /// `key => _` — key must exist.
729    KeyOnly { key: Expr },
730    /// `key => $name` — key must exist; value is bound to `$name` in the arm.
731    Capture { key: Expr, name: String },
732}
733
734#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
735pub enum MagicConstKind {
736    /// Current source path (`$0`-style script name or `-e`).
737    File,
738    /// Line number of this token (1-based, same as lexer).
739    Line,
740    /// Reference to currently executing subroutine (for anonymous recursion).
741    Sub,
742}
743
744#[derive(Debug, Clone, Serialize, Deserialize)]
745pub struct Expr {
746    pub kind: ExprKind,
747    pub line: usize,
748}
749
750#[derive(Debug, Clone, Serialize, Deserialize)]
751pub enum ExprKind {
752    // Literals
753    Integer(i64),
754    Float(f64),
755    String(String),
756    /// Unquoted identifier used as an expression term (`if (FOO)`), distinct from quoted `'FOO'` / `"FOO"`.
757    /// Resolved at runtime: nullary subroutine if defined, otherwise stringifies like Perl barewords.
758    Bareword(String),
759    Regex(String, String),
760    QW(Vec<String>),
761    Undef,
762    /// `__FILE__` / `__LINE__` (Perl compile-time literals).
763    MagicConst(MagicConstKind),
764
765    // Interpolated string (mix of literal and variable parts)
766    InterpolatedString(Vec<StringPart>),
767
768    // Variables
769    ScalarVar(String),
770    ArrayVar(String),
771    HashVar(String),
772    ArrayElement {
773        array: String,
774        index: Box<Expr>,
775    },
776    HashElement {
777        hash: String,
778        key: Box<Expr>,
779    },
780    ArraySlice {
781        array: String,
782        indices: Vec<Expr>,
783    },
784    HashSlice {
785        hash: String,
786        keys: Vec<Expr>,
787    },
788    /// `%h{KEYS}` — Perl 5.20+ key-value slice: returns a flat list of
789    /// (key, value, key, value, ...) pairs instead of just values. (BUG-008)
790    HashKvSlice {
791        hash: String,
792        keys: Vec<Expr>,
793    },
794    /// `@$container{keys}` — hash slice when the hash is reached via a scalar ref (Perl `@$href{k1,k2}`).
795    HashSliceDeref {
796        container: Box<Expr>,
797        keys: Vec<Expr>,
798    },
799    /// `(LIST)[i,...]` / `(sort ...)[0]` — subscript after a non-arrow container (not `$a[i]` / `$r->[i]`).
800    AnonymousListSlice {
801        source: Box<Expr>,
802        indices: Vec<Expr>,
803    },
804
805    // References
806    ScalarRef(Box<Expr>),
807    ArrayRef(Vec<Expr>),
808    HashRef(Vec<(Expr, Expr)>),
809    CodeRef {
810        params: Vec<SubSigParam>,
811        body: Block,
812    },
813    /// Unary `&name` — invoke subroutine `name` (Perl `&foo` / `&Foo::bar`).
814    SubroutineRef(String),
815    /// `\&name` — coderef to an existing named subroutine (Perl `\&foo`).
816    SubroutineCodeRef(String),
817    /// `\&{ EXPR }` — coderef to a subroutine whose name is given by `EXPR` (string or expression).
818    DynamicSubCodeRef(Box<Expr>),
819    Deref {
820        expr: Box<Expr>,
821        kind: Sigil,
822    },
823    ArrowDeref {
824        expr: Box<Expr>,
825        index: Box<Expr>,
826        kind: DerefKind,
827    },
828
829    // Operators
830    BinOp {
831        left: Box<Expr>,
832        op: BinOp,
833        right: Box<Expr>,
834    },
835    UnaryOp {
836        op: UnaryOp,
837        expr: Box<Expr>,
838    },
839    PostfixOp {
840        expr: Box<Expr>,
841        op: PostfixOp,
842    },
843    Assign {
844        target: Box<Expr>,
845        value: Box<Expr>,
846    },
847    CompoundAssign {
848        target: Box<Expr>,
849        op: BinOp,
850        value: Box<Expr>,
851    },
852    Ternary {
853        condition: Box<Expr>,
854        then_expr: Box<Expr>,
855        else_expr: Box<Expr>,
856    },
857
858    // Repetition operator `EXPR x N`.
859    //
860    // Perl distinguishes scalar string repetition (`"ab" x 3` → `"ababab"`) from
861    // list repetition (`(0) x 3` → `(0,0,0)`, `qw(a b) x 2` → `(a,b,a,b)`). The
862    // discriminator at parse time is the LHS shape: a top-level paren-list (or
863    // `qw(...)`) immediately before `x` is list-repeat; everything else is
864    // scalar-repeat. The parser sets `list_repeat=true` only in that case;
865    // `f(args) x N` (function-call parens, not list parens) stays scalar.
866    Repeat {
867        expr: Box<Expr>,
868        count: Box<Expr>,
869        list_repeat: bool,
870    },
871
872    // Range: `1..10` / `1...10` — in scalar context, `...` is the exclusive flip-flop (Perl `sed`-style).
873    // With step: `1..100:2` (1,3,5,...,99) or `100..1:-1` (100,99,...,1).
874    Range {
875        from: Box<Expr>,
876        to: Box<Expr>,
877        #[serde(default)]
878        exclusive: bool,
879        #[serde(default)]
880        step: Option<Box<Expr>>,
881    },
882
883    /// Slice subscript range with optional endpoints — Python-style `[start:stop:step]`.
884    /// Only emitted by the parser inside `@arr[...]` / `@h{...}` (and arrow-deref forms).
885    /// Open-ended forms: `[::-1]` (reverse), `[:N]`, `[N:]`, `[::M]`, `[N::M]`.
886    /// Compiler dispatches to typed integer-strict (array) or stringify-all (hash) ops.
887    SliceRange {
888        #[serde(default)]
889        from: Option<Box<Expr>>,
890        #[serde(default)]
891        to: Option<Box<Expr>>,
892        #[serde(default)]
893        step: Option<Box<Expr>>,
894    },
895
896    /// `my $x = EXPR` (or `our` / `state` / `local`) used as an *expression* —
897    /// e.g. inside `if (my $line = readline)` / `while (my $x = next())`.
898    /// Evaluation: declare each var in the current scope, evaluate the initializer
899    /// (or default to `undef`), then return the assigned value(s).
900    /// Distinct from `StmtKind::My` which only appears at statement level.
901    MyExpr {
902        keyword: String, // "my" / "our" / "state" / "local"
903        decls: Vec<VarDecl>,
904    },
905
906    // Function call
907    FuncCall {
908        name: String,
909        args: Vec<Expr>,
910    },
911
912    // Method call: $obj->method(args) or $obj->SUPER::method(args)
913    MethodCall {
914        object: Box<Expr>,
915        method: String,
916        args: Vec<Expr>,
917        /// When true, dispatch starts after the caller package in the linearized MRO.
918        #[serde(default)]
919        super_call: bool,
920    },
921    /// Call through a coderef or invokable scalar: `$cr->(...)` is [`MethodCall`]; this is
922    /// `$coderef(...)` or `&$coderef(...)` (the latter sets `ampersand`).
923    IndirectCall {
924        target: Box<Expr>,
925        args: Vec<Expr>,
926        #[serde(default)]
927        ampersand: bool,
928        /// True for unary `&$cr` with no `(...)` — Perl passes the caller's `@_` to the invoked sub.
929        #[serde(default)]
930        pass_caller_arglist: bool,
931    },
932    /// Limited typeglob: `*FOO` → handle name `FOO` for `open` / I/O.
933    Typeglob(String),
934    /// `*{ EXPR }` — typeglob slot by dynamic name (e.g. `*{$pkg . '::import'}`).
935    TypeglobExpr(Box<Expr>),
936
937    // Special forms
938    Print {
939        handle: Option<String>,
940        args: Vec<Expr>,
941    },
942    Say {
943        handle: Option<String>,
944        args: Vec<Expr>,
945    },
946    Printf {
947        handle: Option<String>,
948        args: Vec<Expr>,
949    },
950    Die(Vec<Expr>),
951    Warn(Vec<Expr>),
952
953    // Regex operations
954    Match {
955        expr: Box<Expr>,
956        pattern: String,
957        flags: String,
958        /// When true, `/g` uses Perl scalar semantics (one match per eval, updates `pos`).
959        scalar_g: bool,
960        #[serde(default = "default_delim")]
961        delim: char,
962    },
963    Substitution {
964        expr: Box<Expr>,
965        pattern: String,
966        replacement: String,
967        flags: String,
968        #[serde(default = "default_delim")]
969        delim: char,
970    },
971    Transliterate {
972        expr: Box<Expr>,
973        from: String,
974        to: String,
975        flags: String,
976        #[serde(default = "default_delim")]
977        delim: char,
978    },
979
980    // List operations
981    MapExpr {
982        block: Block,
983        list: Box<Expr>,
984        /// `flat_map { }` — peel one ARRAY ref from each iteration (stryke extension).
985        flatten_array_refs: bool,
986        /// `maps` / `flat_maps` — lazy iterator output (stryke); `map` / `flat_map` use `false`.
987        #[serde(default)]
988        stream: bool,
989    },
990    /// `map EXPR, LIST` — EXPR is evaluated in list context with `$_` set to each element.
991    MapExprComma {
992        expr: Box<Expr>,
993        list: Box<Expr>,
994        flatten_array_refs: bool,
995        #[serde(default)]
996        stream: bool,
997    },
998    GrepExpr {
999        block: Block,
1000        list: Box<Expr>,
1001        #[serde(default)]
1002        keyword: GrepBuiltinKeyword,
1003    },
1004    /// `grep EXPR, LIST` — EXPR is evaluated with `$_` set to each element (Perl list vs scalar context).
1005    GrepExprComma {
1006        expr: Box<Expr>,
1007        list: Box<Expr>,
1008        #[serde(default)]
1009        keyword: GrepBuiltinKeyword,
1010    },
1011    /// `sort BLOCK LIST`, `sort SUB LIST`, or `sort $coderef LIST` (Perl uses `$a`/`$b` in the comparator).
1012    SortExpr {
1013        cmp: Option<SortComparator>,
1014        list: Box<Expr>,
1015    },
1016    ReverseExpr(Box<Expr>),
1017    /// `rev EXPR` — always string-reverse (scalar reverse), stryke extension.
1018    Rev(Box<Expr>),
1019    JoinExpr {
1020        separator: Box<Expr>,
1021        list: Box<Expr>,
1022    },
1023    SplitExpr {
1024        pattern: Box<Expr>,
1025        string: Box<Expr>,
1026        limit: Option<Box<Expr>>,
1027    },
1028    /// `each { BLOCK } @list` — execute BLOCK for each element
1029    /// with `$_` aliased; void context (returns count in scalar context).
1030    ForEachExpr {
1031        block: Block,
1032        list: Box<Expr>,
1033    },
1034
1035    // Parallel extensions
1036    PMapExpr {
1037        block: Block,
1038        list: Box<Expr>,
1039        /// `pmap { } @list, progress => EXPR` — when truthy, print a progress bar on stderr.
1040        progress: Option<Box<Expr>>,
1041        /// `pflat_map { }` — flatten each block result like [`ExprKind::MapExpr`] (arrays expand);
1042        /// parallel output is stitched in **input order** (unlike plain `pmap`, which is unordered).
1043        flat_outputs: bool,
1044        /// `pmap_on $cluster { } @list` — fan out over SSH (`stryke --remote-worker`); `None` = local rayon.
1045        #[serde(default, skip_serializing_if = "Option::is_none")]
1046        on_cluster: Option<Box<Expr>>,
1047        /// `pmaps` / `pflat_maps` — streaming variant: returns a lazy iterator that processes
1048        /// chunks in parallel via rayon instead of eagerly collecting all results.
1049        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1050        stream: bool,
1051    },
1052    /// `pmap_chunked N { BLOCK } @list [, progress => EXPR]` — parallel map in batches of N.
1053    PMapChunkedExpr {
1054        chunk_size: Box<Expr>,
1055        block: Block,
1056        list: Box<Expr>,
1057        progress: Option<Box<Expr>>,
1058    },
1059    PGrepExpr {
1060        block: Block,
1061        list: Box<Expr>,
1062        /// `pgrep { } @list, progress => EXPR` — stderr progress bar when truthy.
1063        progress: Option<Box<Expr>>,
1064        /// `pgreps` — streaming variant: returns a lazy iterator.
1065        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1066        stream: bool,
1067    },
1068    /// `pfor { BLOCK } @list [, progress => EXPR]` — stderr progress bar when truthy.
1069    PForExpr {
1070        block: Block,
1071        list: Box<Expr>,
1072        progress: Option<Box<Expr>>,
1073    },
1074    /// `par { BLOCK } INPUT` — generic parallel-chunk wrapper. Splits INPUT
1075    /// (string → UTF-8-aligned byte chunks; array/list → element-chunks)
1076    /// into N pieces (N = available rayon threads), evaluates BLOCK per
1077    /// chunk in parallel with `$_` bound to the chunk, then concatenates
1078    /// results. Lets any whole-input op (`letters`, `chars`, `uc`, `freq`,
1079    /// regex `//g`, etc.) parallelize without needing a `pX` variant.
1080    ParExpr {
1081        block: Block,
1082        list: Box<Expr>,
1083    },
1084    /// `par_reduce { extract } [ { merge } ] INPUT` — chunk-extract-merge.
1085    /// Same chunker as `par {}`, but each chunk's result is reduced
1086    /// pairwise across chunks instead of concatenated.
1087    ///
1088    /// - One block: auto-merger picks based on result type (number → `+`,
1089    ///   `hash<num>` → key-wise `+`, array → concat, string → concat).
1090    /// - Two blocks: explicit pairwise reducer with `$a`/`$b`.
1091    ParReduceExpr {
1092        extract_block: Block,
1093        reduce_block: Option<Block>,
1094        list: Box<Expr>,
1095    },
1096    /// Distributed counterpart of [`ExprKind::ParReduceExpr`]. Same chunk-block
1097    /// semantics (stages operate on `@_`) but chunks ship to a `RemoteCluster`
1098    /// of SSH workers via the existing `cluster::run_cluster` dispatcher.
1099    /// Built by `~d> on $cluster SOURCE stage1 stage2 ...`.
1100    DistReduceExpr {
1101        cluster: Box<Expr>,
1102        extract_block: Block,
1103        list: Box<Expr>,
1104    },
1105    /// `par_lines PATH, fn { ... } [, progress => EXPR]` — optional stderr progress (per line).
1106    ParLinesExpr {
1107        path: Box<Expr>,
1108        callback: Box<Expr>,
1109        progress: Option<Box<Expr>>,
1110    },
1111    /// `par_walk PATH, fn { ... } [, progress => EXPR]` — parallel recursive directory walk; `$_` is each path.
1112    ParWalkExpr {
1113        path: Box<Expr>,
1114        callback: Box<Expr>,
1115        progress: Option<Box<Expr>>,
1116    },
1117    /// `pwatch GLOB, fn { ... }` — notify-based watcher (evaluated by interpreter).
1118    PwatchExpr {
1119        path: Box<Expr>,
1120        callback: Box<Expr>,
1121    },
1122    /// `psort { } @list [, progress => EXPR]` — stderr progress when truthy (start/end phases).
1123    PSortExpr {
1124        cmp: Option<Block>,
1125        list: Box<Expr>,
1126        progress: Option<Box<Expr>>,
1127    },
1128    /// `reduce { $a + $b } @list` — sequential left fold over the list.
1129    /// `$a` is the accumulator; `$b` is the next list element.
1130    ReduceExpr {
1131        block: Block,
1132        list: Box<Expr>,
1133    },
1134    /// `preduce { $a + $b } @list` — parallel fold/reduce using rayon.
1135    /// $a and $b are set to the accumulator and current element.
1136    PReduceExpr {
1137        block: Block,
1138        list: Box<Expr>,
1139        /// `preduce { } @list, progress => EXPR` — stderr progress bar when truthy.
1140        progress: Option<Box<Expr>>,
1141    },
1142    /// `preduce_init EXPR, { $a / $b } @list` — parallel fold with explicit identity.
1143    /// Each chunk starts from a clone of `EXPR`; partials are merged (hash maps add counts per key;
1144    /// other types use the same block with `$a` / `$b` as partial accumulators). `$a` is the
1145    /// accumulator, `$b` is the next list element; `@_` is `($a, $b)` for `my ($acc, $item) = @_`.
1146    PReduceInitExpr {
1147        init: Box<Expr>,
1148        block: Block,
1149        list: Box<Expr>,
1150        progress: Option<Box<Expr>>,
1151    },
1152    /// `pmap_reduce { map } { reduce } @list` — fused parallel map + tree reduce (no full mapped array).
1153    PMapReduceExpr {
1154        map_block: Block,
1155        reduce_block: Block,
1156        list: Box<Expr>,
1157        progress: Option<Box<Expr>>,
1158    },
1159    /// `pcache { BLOCK } @list [, progress => EXPR]` — stderr progress bar when truthy.
1160    PcacheExpr {
1161        block: Block,
1162        list: Box<Expr>,
1163        progress: Option<Box<Expr>>,
1164    },
1165    /// `pselect($rx1, $rx2, ...)` — optional `timeout => SECS` for bounded wait.
1166    PselectExpr {
1167        receivers: Vec<Expr>,
1168        timeout: Option<Box<Expr>>,
1169    },
1170    /// `fan [COUNT] { BLOCK }` — execute BLOCK COUNT times in parallel (default COUNT = rayon pool size).
1171    /// `fan_cap [COUNT] { BLOCK }` — same, but return value is a **list** of each block's return value (index order).
1172    /// `$_` is set to the iteration index (0..COUNT-1).
1173    /// Optional `, progress => EXPR` — stderr progress bar (like `pmap`).
1174    FanExpr {
1175        count: Option<Box<Expr>>,
1176        block: Block,
1177        progress: Option<Box<Expr>>,
1178        capture: bool,
1179    },
1180
1181    /// `async { BLOCK }` — run BLOCK on a worker thread; returns a task handle.
1182    AsyncBlock {
1183        body: Block,
1184    },
1185    /// `spawn { BLOCK }` — same as [`ExprKind::AsyncBlock`] (Rust `thread::spawn`–style naming); join with `await`.
1186    SpawnBlock {
1187        body: Block,
1188    },
1189    /// `trace { BLOCK }` — print `mysync` scalar mutations to stderr (for parallel debugging).
1190    Trace {
1191        body: Block,
1192    },
1193    /// `timer { BLOCK }` — run BLOCK and return elapsed wall time in milliseconds (float).
1194    Timer {
1195        body: Block,
1196    },
1197    /// `bench { BLOCK } N` — run BLOCK `N` times (warmup + min/mean/p99 wall time, ms).
1198    Bench {
1199        body: Block,
1200        times: Box<Expr>,
1201    },
1202    /// `spinner "msg" { BLOCK }` — animated spinner on stderr while block runs.
1203    Spinner {
1204        message: Box<Expr>,
1205        body: Block,
1206    },
1207    /// `await EXPR` — join an async task, or return EXPR unchanged.
1208    Await(Box<Expr>),
1209    /// Read entire file as UTF-8 (`slurp $path`).
1210    Slurp(Box<Expr>),
1211    /// Run shell command and return structured output (`capture "cmd"`).
1212    Capture(Box<Expr>),
1213    /// `` `cmd` `` / `qx{cmd}` — run via `sh -c`, return **stdout as a string** (Perl); updates `$?`.
1214    Qx(Box<Expr>),
1215    /// Blocking HTTP GET (`fetch_url $url`).
1216    FetchUrl(Box<Expr>),
1217
1218    /// `pchannel()` — unbounded; `pchannel(N)` — bounded capacity N.
1219    Pchannel {
1220        capacity: Option<Box<Expr>>,
1221    },
1222
1223    // Array/Hash operations
1224    Push {
1225        array: Box<Expr>,
1226        values: Vec<Expr>,
1227    },
1228    Pop(Box<Expr>),
1229    Shift(Box<Expr>),
1230    Unshift {
1231        array: Box<Expr>,
1232        values: Vec<Expr>,
1233    },
1234    Splice {
1235        array: Box<Expr>,
1236        offset: Option<Box<Expr>>,
1237        length: Option<Box<Expr>>,
1238        replacement: Vec<Expr>,
1239    },
1240    Delete(Box<Expr>),
1241    Exists(Box<Expr>),
1242    Keys(Box<Expr>),
1243    Values(Box<Expr>),
1244    Each(Box<Expr>),
1245
1246    // String operations
1247    Chomp(Box<Expr>),
1248    Chop(Box<Expr>),
1249    Length(Box<Expr>),
1250    Substr {
1251        string: Box<Expr>,
1252        offset: Box<Expr>,
1253        length: Option<Box<Expr>>,
1254        replacement: Option<Box<Expr>>,
1255    },
1256    Index {
1257        string: Box<Expr>,
1258        substr: Box<Expr>,
1259        position: Option<Box<Expr>>,
1260    },
1261    Rindex {
1262        string: Box<Expr>,
1263        substr: Box<Expr>,
1264        position: Option<Box<Expr>>,
1265    },
1266    Sprintf {
1267        format: Box<Expr>,
1268        args: Vec<Expr>,
1269    },
1270
1271    // Numeric
1272    Abs(Box<Expr>),
1273    Int(Box<Expr>),
1274    Sqrt(Box<Expr>),
1275    Sin(Box<Expr>),
1276    Cos(Box<Expr>),
1277    Atan2 {
1278        y: Box<Expr>,
1279        x: Box<Expr>,
1280    },
1281    Exp(Box<Expr>),
1282    Log(Box<Expr>),
1283    /// `rand` with optional upper bound (none = Perl default 1.0).
1284    Rand(Option<Box<Expr>>),
1285    /// `srand` with optional seed (none = time-based).
1286    Srand(Option<Box<Expr>>),
1287    Hex(Box<Expr>),
1288    Oct(Box<Expr>),
1289
1290    // Case
1291    Lc(Box<Expr>),
1292    Uc(Box<Expr>),
1293    Lcfirst(Box<Expr>),
1294    Ucfirst(Box<Expr>),
1295
1296    /// Unicode case fold (Perl `fc`).
1297    Fc(Box<Expr>),
1298    /// DES-style `crypt` (see libc `crypt(3)` on Unix; empty on other targets).
1299    Crypt {
1300        plaintext: Box<Expr>,
1301        salt: Box<Expr>,
1302    },
1303    /// `pos` — optional scalar lvalue target (`None` = `$_`).
1304    Pos(Option<Box<Expr>>),
1305    /// `study` — hint for repeated matching; returns byte length of the string.
1306    Study(Box<Expr>),
1307
1308    // Type
1309    Defined(Box<Expr>),
1310    Ref(Box<Expr>),
1311    ScalarContext(Box<Expr>),
1312
1313    // Char
1314    Chr(Box<Expr>),
1315    Ord(Box<Expr>),
1316
1317    // I/O
1318    /// `open my $fh` — only valid as [`ExprKind::Open::handle`]; declares `$fh` and binds the handle.
1319    OpenMyHandle {
1320        name: String,
1321    },
1322    Open {
1323        handle: Box<Expr>,
1324        mode: Box<Expr>,
1325        file: Option<Box<Expr>>,
1326    },
1327    Close(Box<Expr>),
1328    ReadLine(Option<String>),
1329    Eof(Option<Box<Expr>>),
1330
1331    Opendir {
1332        handle: Box<Expr>,
1333        path: Box<Expr>,
1334    },
1335    Readdir(Box<Expr>),
1336    Closedir(Box<Expr>),
1337    Rewinddir(Box<Expr>),
1338    Telldir(Box<Expr>),
1339    Seekdir {
1340        handle: Box<Expr>,
1341        position: Box<Expr>,
1342    },
1343
1344    // File tests
1345    FileTest {
1346        op: char,
1347        expr: Box<Expr>,
1348    },
1349
1350    // System
1351    System(Vec<Expr>),
1352    Exec(Vec<Expr>),
1353    Eval(Box<Expr>),
1354    Do(Box<Expr>),
1355    Require(Box<Expr>),
1356    Exit(Option<Box<Expr>>),
1357    Chdir(Box<Expr>),
1358    Mkdir {
1359        path: Box<Expr>,
1360        mode: Option<Box<Expr>>,
1361    },
1362    Unlink(Vec<Expr>),
1363    Rename {
1364        old: Box<Expr>,
1365        new: Box<Expr>,
1366    },
1367    /// `chmod MODE, @files` — first expr is mode, rest are paths.
1368    Chmod(Vec<Expr>),
1369    /// `chown UID, GID, @files` — first two are uid/gid, rest are paths.
1370    Chown(Vec<Expr>),
1371
1372    Stat(Box<Expr>),
1373    Lstat(Box<Expr>),
1374    Link {
1375        old: Box<Expr>,
1376        new: Box<Expr>,
1377    },
1378    Symlink {
1379        old: Box<Expr>,
1380        new: Box<Expr>,
1381    },
1382    Readlink(Box<Expr>),
1383    /// `files` / `files DIR` — list file names in a directory (default: `.`).
1384    Files(Vec<Expr>),
1385    /// `filesf` / `filesf DIR` / `f` — list only regular file names in a directory (default: `.`).
1386    Filesf(Vec<Expr>),
1387    /// `fr DIR` — list only regular file names recursively (default: `.`).
1388    FilesfRecursive(Vec<Expr>),
1389    /// `dirs` / `dirs DIR` / `d` — list subdirectory names in a directory (default: `.`).
1390    Dirs(Vec<Expr>),
1391    /// `dr DIR` — list subdirectory paths recursively (default: `.`).
1392    DirsRecursive(Vec<Expr>),
1393    /// `sym_links` / `sym_links DIR` — list symlink names in a directory (default: `.`).
1394    SymLinks(Vec<Expr>),
1395    /// `sockets` / `sockets DIR` — list Unix socket names in a directory (default: `.`).
1396    Sockets(Vec<Expr>),
1397    /// `pipes` / `pipes DIR` — list named-pipe (FIFO) names in a directory (default: `.`).
1398    Pipes(Vec<Expr>),
1399    /// `block_devices` / `block_devices DIR` — list block device names in a directory (default: `.`).
1400    BlockDevices(Vec<Expr>),
1401    /// `char_devices` / `char_devices DIR` — list character device names in a directory (default: `.`).
1402    CharDevices(Vec<Expr>),
1403    /// `exe` / `exe DIR` — list executable file names in a directory (default: `.`).
1404    Executables(Vec<Expr>),
1405    Glob(Vec<Expr>),
1406    /// Parallel recursive glob (rayon); same patterns as `glob`, different walk strategy.
1407    /// Optional `, progress => EXPR` — stderr progress bar (one tick per pattern).
1408    GlobPar {
1409        args: Vec<Expr>,
1410        progress: Option<Box<Expr>>,
1411    },
1412    /// `par_sed PATTERN, REPLACEMENT, FILES... [, progress => EXPR]` — parallel in-place regex replace per file (`g` semantics).
1413    ParSed {
1414        args: Vec<Expr>,
1415        progress: Option<Box<Expr>>,
1416    },
1417
1418    // Bless
1419    Bless {
1420        ref_expr: Box<Expr>,
1421        class: Option<Box<Expr>>,
1422    },
1423
1424    // Caller
1425    Caller(Option<Box<Expr>>),
1426
1427    // Wantarray
1428    Wantarray,
1429
1430    // List / Context
1431    List(Vec<Expr>),
1432
1433    // Postfix if/unless/while/until/for
1434    PostfixIf {
1435        expr: Box<Expr>,
1436        condition: Box<Expr>,
1437    },
1438    PostfixUnless {
1439        expr: Box<Expr>,
1440        condition: Box<Expr>,
1441    },
1442    PostfixWhile {
1443        expr: Box<Expr>,
1444        condition: Box<Expr>,
1445    },
1446    PostfixUntil {
1447        expr: Box<Expr>,
1448        condition: Box<Expr>,
1449    },
1450    PostfixForeach {
1451        expr: Box<Expr>,
1452        list: Box<Expr>,
1453    },
1454
1455    /// `retry { BLOCK } times => N [, backoff => linear|exponential|none]` — re-run block until success or attempts exhausted.
1456    RetryBlock {
1457        body: Block,
1458        times: Box<Expr>,
1459        backoff: RetryBackoff,
1460    },
1461    /// `rate_limit(MAX, WINDOW) { BLOCK }` — sliding window: at most MAX runs per WINDOW (e.g. `"1s"`).
1462    /// `slot` is assigned at parse time for per-site state in the interpreter.
1463    RateLimitBlock {
1464        slot: u32,
1465        max: Box<Expr>,
1466        window: Box<Expr>,
1467        body: Block,
1468    },
1469    /// `every(INTERVAL) { BLOCK }` — repeat BLOCK forever with sleep (INTERVAL like `"5s"` or seconds).
1470    EveryBlock {
1471        interval: Box<Expr>,
1472        body: Block,
1473    },
1474    /// `gen { ... yield ... }` — lazy generator; call `->next` for each value.
1475    GenBlock {
1476        body: Block,
1477    },
1478    /// `yield EXPR` — only valid inside `gen { }` (and propagates through control flow).
1479    Yield(Box<Expr>),
1480
1481    /// `match (EXPR) { PATTERN => EXPR, ... }` — first matching arm; bindings scoped to the arm body.
1482    AlgebraicMatch {
1483        subject: Box<Expr>,
1484        arms: Vec<MatchArm>,
1485    },
1486}
1487
1488#[derive(Debug, Clone, Serialize, Deserialize)]
1489pub enum StringPart {
1490    Literal(String),
1491    ScalarVar(String),
1492    ArrayVar(String),
1493    Expr(Expr),
1494}
1495
1496#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1497pub enum DerefKind {
1498    Array,
1499    Hash,
1500    Call,
1501}
1502
1503#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1504pub enum BinOp {
1505    Add,
1506    Sub,
1507    Mul,
1508    Div,
1509    Mod,
1510    Pow,
1511    Concat,
1512    NumEq,
1513    NumNe,
1514    NumLt,
1515    NumGt,
1516    NumLe,
1517    NumGe,
1518    Spaceship,
1519    StrEq,
1520    StrNe,
1521    StrLt,
1522    StrGt,
1523    StrLe,
1524    StrGe,
1525    StrCmp,
1526    LogAnd,
1527    LogOr,
1528    DefinedOr,
1529    BitAnd,
1530    BitOr,
1531    BitXor,
1532    ShiftLeft,
1533    ShiftRight,
1534    LogAndWord,
1535    LogOrWord,
1536    BindMatch,
1537    BindNotMatch,
1538}
1539
1540#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1541pub enum UnaryOp {
1542    Negate,
1543    LogNot,
1544    BitNot,
1545    LogNotWord,
1546    PreIncrement,
1547    PreDecrement,
1548    Ref,
1549}
1550
1551#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1552pub enum PostfixOp {
1553    Increment,
1554    Decrement,
1555}
1556
1557#[cfg(test)]
1558mod tests {
1559    use super::*;
1560
1561    #[test]
1562    fn binop_deref_kind_distinct() {
1563        assert_ne!(BinOp::Add, BinOp::Sub);
1564        assert_eq!(DerefKind::Call, DerefKind::Call);
1565    }
1566
1567    #[test]
1568    fn sigil_variants_exhaustive_in_tests() {
1569        let all = [Sigil::Scalar, Sigil::Array, Sigil::Hash];
1570        assert_eq!(all.len(), 3);
1571    }
1572
1573    #[test]
1574    fn program_empty_roundtrip_clone() {
1575        let p = Program { statements: vec![] };
1576        assert!(p.clone().statements.is_empty());
1577    }
1578
1579    #[test]
1580    fn program_serializes_to_json() {
1581        let p = crate::parse("1+2;").expect("parse");
1582        let s = serde_json::to_string(&p).expect("json");
1583        assert!(s.contains("\"statements\""));
1584        assert!(s.contains("BinOp"));
1585    }
1586}