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 [`PerlValue`](crate::value::PerlValue), `Str` only string, `Float` allows int or float.
526    pub fn check_value(&self, v: &crate::value::PerlValue) -> 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}
649
650#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
651pub enum Sigil {
652    Scalar,
653    Array,
654    Hash,
655    /// `local *FH` — filehandle slot alias (limited typeglob).
656    Typeglob,
657}
658
659pub type Block = Vec<Statement>;
660
661/// Comparator for `sort` — `{ $a <=> $b }`, or a code ref / expression (Perl `sort $cmp LIST`).
662#[derive(Debug, Clone, Serialize, Deserialize)]
663pub enum SortComparator {
664    Block(Block),
665    Code(Box<Expr>),
666}
667
668// ── Algebraic `match` expression (stryke extension) ──
669
670/// One arm of [`ExprKind::AlgebraicMatch`]: `PATTERN [if EXPR] => EXPR`.
671#[derive(Debug, Clone, Serialize, Deserialize)]
672pub struct MatchArm {
673    pub pattern: MatchPattern,
674    /// Optional guard (`if EXPR`) evaluated after pattern match; `$_` is the match subject.
675    #[serde(skip_serializing_if = "Option::is_none")]
676    pub guard: Option<Box<Expr>>,
677    pub body: Expr,
678}
679
680/// `retry { } backoff => exponential` — sleep policy between attempts (after failure).
681#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
682pub enum RetryBackoff {
683    /// No delay between attempts.
684    None,
685    /// Delay grows linearly: `base_ms * attempt` (attempt starts at 1).
686    Linear,
687    /// Delay doubles each failure: `base_ms * 2^(attempt-1)` (capped).
688    Exponential,
689}
690
691/// Pattern for algebraic `match` (distinct from the `=~` / regex [`ExprKind::Match`]).
692#[derive(Debug, Clone, Serialize, Deserialize)]
693pub enum MatchPattern {
694    /// `_` — matches anything.
695    Any,
696    /// `/regex/` — subject stringified; on success the arm body sets `$_` to the subject and
697    /// populates match variables (`$1`…, `$&`, `${^MATCH}`, `@-`/`@+`, `%+`, …) like `=~`.
698    Regex { pattern: String, flags: String },
699    /// Arbitrary expression compared for equality / smart-match against the subject.
700    Value(Box<Expr>),
701    /// `[1, 2, *]` — prefix elements match; optional `*` matches any tail (must be last).
702    Array(Vec<MatchArrayElem>),
703    /// `{ name => $n, ... }` — required keys; `$n` binds the value for the arm body.
704    Hash(Vec<MatchHashPair>),
705    /// `Some($x)` — matches array-like values with **at least two** elements where index `1` is
706    /// Perl-truthy (stryke: `$gen->next` yields `[value, more]` with `more` truthy while iterating).
707    OptionSome(String),
708}
709
710#[derive(Debug, Clone, Serialize, Deserialize)]
711pub enum MatchArrayElem {
712    Expr(Expr),
713    /// `$name` at the top of a pattern element — bind this position to a new lexical `$name`.
714    /// Use `[($x)]` if you need smartmatch against the current value of `$x` instead.
715    CaptureScalar(String),
716    /// Rest-of-array wildcard (only valid as the last element).
717    Rest,
718    /// `@name` — bind remaining elements as a new array to `@name` (only valid as the last element).
719    RestBind(String),
720}
721
722#[derive(Debug, Clone, Serialize, Deserialize)]
723pub enum MatchHashPair {
724    /// `key => _` — key must exist.
725    KeyOnly { key: Expr },
726    /// `key => $name` — key must exist; value is bound to `$name` in the arm.
727    Capture { key: Expr, name: String },
728}
729
730#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
731pub enum MagicConstKind {
732    /// Current source path (`$0`-style script name or `-e`).
733    File,
734    /// Line number of this token (1-based, same as lexer).
735    Line,
736    /// Reference to currently executing subroutine (for anonymous recursion).
737    Sub,
738}
739
740#[derive(Debug, Clone, Serialize, Deserialize)]
741pub struct Expr {
742    pub kind: ExprKind,
743    pub line: usize,
744}
745
746#[derive(Debug, Clone, Serialize, Deserialize)]
747pub enum ExprKind {
748    // Literals
749    Integer(i64),
750    Float(f64),
751    String(String),
752    /// Unquoted identifier used as an expression term (`if (FOO)`), distinct from quoted `'FOO'` / `"FOO"`.
753    /// Resolved at runtime: nullary subroutine if defined, otherwise stringifies like Perl barewords.
754    Bareword(String),
755    Regex(String, String),
756    QW(Vec<String>),
757    Undef,
758    /// `__FILE__` / `__LINE__` (Perl compile-time literals).
759    MagicConst(MagicConstKind),
760
761    // Interpolated string (mix of literal and variable parts)
762    InterpolatedString(Vec<StringPart>),
763
764    // Variables
765    ScalarVar(String),
766    ArrayVar(String),
767    HashVar(String),
768    ArrayElement {
769        array: String,
770        index: Box<Expr>,
771    },
772    HashElement {
773        hash: String,
774        key: Box<Expr>,
775    },
776    ArraySlice {
777        array: String,
778        indices: Vec<Expr>,
779    },
780    HashSlice {
781        hash: String,
782        keys: Vec<Expr>,
783    },
784    /// `%h{KEYS}` — Perl 5.20+ key-value slice: returns a flat list of
785    /// (key, value, key, value, ...) pairs instead of just values. (BUG-008)
786    HashKvSlice {
787        hash: String,
788        keys: Vec<Expr>,
789    },
790    /// `@$container{keys}` — hash slice when the hash is reached via a scalar ref (Perl `@$href{k1,k2}`).
791    HashSliceDeref {
792        container: Box<Expr>,
793        keys: Vec<Expr>,
794    },
795    /// `(LIST)[i,...]` / `(sort ...)[0]` — subscript after a non-arrow container (not `$a[i]` / `$r->[i]`).
796    AnonymousListSlice {
797        source: Box<Expr>,
798        indices: Vec<Expr>,
799    },
800
801    // References
802    ScalarRef(Box<Expr>),
803    ArrayRef(Vec<Expr>),
804    HashRef(Vec<(Expr, Expr)>),
805    CodeRef {
806        params: Vec<SubSigParam>,
807        body: Block,
808    },
809    /// Unary `&name` — invoke subroutine `name` (Perl `&foo` / `&Foo::bar`).
810    SubroutineRef(String),
811    /// `\&name` — coderef to an existing named subroutine (Perl `\&foo`).
812    SubroutineCodeRef(String),
813    /// `\&{ EXPR }` — coderef to a subroutine whose name is given by `EXPR` (string or expression).
814    DynamicSubCodeRef(Box<Expr>),
815    Deref {
816        expr: Box<Expr>,
817        kind: Sigil,
818    },
819    ArrowDeref {
820        expr: Box<Expr>,
821        index: Box<Expr>,
822        kind: DerefKind,
823    },
824
825    // Operators
826    BinOp {
827        left: Box<Expr>,
828        op: BinOp,
829        right: Box<Expr>,
830    },
831    UnaryOp {
832        op: UnaryOp,
833        expr: Box<Expr>,
834    },
835    PostfixOp {
836        expr: Box<Expr>,
837        op: PostfixOp,
838    },
839    Assign {
840        target: Box<Expr>,
841        value: Box<Expr>,
842    },
843    CompoundAssign {
844        target: Box<Expr>,
845        op: BinOp,
846        value: Box<Expr>,
847    },
848    Ternary {
849        condition: Box<Expr>,
850        then_expr: Box<Expr>,
851        else_expr: Box<Expr>,
852    },
853
854    // Repetition operator `EXPR x N`.
855    //
856    // Perl distinguishes scalar string repetition (`"ab" x 3` → `"ababab"`) from
857    // list repetition (`(0) x 3` → `(0,0,0)`, `qw(a b) x 2` → `(a,b,a,b)`). The
858    // discriminator at parse time is the LHS shape: a top-level paren-list (or
859    // `qw(...)`) immediately before `x` is list-repeat; everything else is
860    // scalar-repeat. The parser sets `list_repeat=true` only in that case;
861    // `f(args) x N` (function-call parens, not list parens) stays scalar.
862    Repeat {
863        expr: Box<Expr>,
864        count: Box<Expr>,
865        list_repeat: bool,
866    },
867
868    // Range: `1..10` / `1...10` — in scalar context, `...` is the exclusive flip-flop (Perl `sed`-style).
869    // With step: `1..100:2` (1,3,5,...,99) or `100..1:-1` (100,99,...,1).
870    Range {
871        from: Box<Expr>,
872        to: Box<Expr>,
873        #[serde(default)]
874        exclusive: bool,
875        #[serde(default)]
876        step: Option<Box<Expr>>,
877    },
878
879    /// Slice subscript range with optional endpoints — Python-style `[start:stop:step]`.
880    /// Only emitted by the parser inside `@arr[...]` / `@h{...}` (and arrow-deref forms).
881    /// Open-ended forms: `[::-1]` (reverse), `[:N]`, `[N:]`, `[::M]`, `[N::M]`.
882    /// Compiler dispatches to typed integer-strict (array) or stringify-all (hash) ops.
883    SliceRange {
884        #[serde(default)]
885        from: Option<Box<Expr>>,
886        #[serde(default)]
887        to: Option<Box<Expr>>,
888        #[serde(default)]
889        step: Option<Box<Expr>>,
890    },
891
892    /// `my $x = EXPR` (or `our` / `state` / `local`) used as an *expression* —
893    /// e.g. inside `if (my $line = readline)` / `while (my $x = next())`.
894    /// Evaluation: declare each var in the current scope, evaluate the initializer
895    /// (or default to `undef`), then return the assigned value(s).
896    /// Distinct from `StmtKind::My` which only appears at statement level.
897    MyExpr {
898        keyword: String, // "my" / "our" / "state" / "local"
899        decls: Vec<VarDecl>,
900    },
901
902    // Function call
903    FuncCall {
904        name: String,
905        args: Vec<Expr>,
906    },
907
908    // Method call: $obj->method(args) or $obj->SUPER::method(args)
909    MethodCall {
910        object: Box<Expr>,
911        method: String,
912        args: Vec<Expr>,
913        /// When true, dispatch starts after the caller package in the linearized MRO.
914        #[serde(default)]
915        super_call: bool,
916    },
917    /// Call through a coderef or invokable scalar: `$cr->(...)` is [`MethodCall`]; this is
918    /// `$coderef(...)` or `&$coderef(...)` (the latter sets `ampersand`).
919    IndirectCall {
920        target: Box<Expr>,
921        args: Vec<Expr>,
922        #[serde(default)]
923        ampersand: bool,
924        /// True for unary `&$cr` with no `(...)` — Perl passes the caller's `@_` to the invoked sub.
925        #[serde(default)]
926        pass_caller_arglist: bool,
927    },
928    /// Limited typeglob: `*FOO` → handle name `FOO` for `open` / I/O.
929    Typeglob(String),
930    /// `*{ EXPR }` — typeglob slot by dynamic name (e.g. `*{$pkg . '::import'}`).
931    TypeglobExpr(Box<Expr>),
932
933    // Special forms
934    Print {
935        handle: Option<String>,
936        args: Vec<Expr>,
937    },
938    Say {
939        handle: Option<String>,
940        args: Vec<Expr>,
941    },
942    Printf {
943        handle: Option<String>,
944        args: Vec<Expr>,
945    },
946    Die(Vec<Expr>),
947    Warn(Vec<Expr>),
948
949    // Regex operations
950    Match {
951        expr: Box<Expr>,
952        pattern: String,
953        flags: String,
954        /// When true, `/g` uses Perl scalar semantics (one match per eval, updates `pos`).
955        scalar_g: bool,
956        #[serde(default = "default_delim")]
957        delim: char,
958    },
959    Substitution {
960        expr: Box<Expr>,
961        pattern: String,
962        replacement: String,
963        flags: String,
964        #[serde(default = "default_delim")]
965        delim: char,
966    },
967    Transliterate {
968        expr: Box<Expr>,
969        from: String,
970        to: String,
971        flags: String,
972        #[serde(default = "default_delim")]
973        delim: char,
974    },
975
976    // List operations
977    MapExpr {
978        block: Block,
979        list: Box<Expr>,
980        /// `flat_map { }` — peel one ARRAY ref from each iteration (stryke extension).
981        flatten_array_refs: bool,
982        /// `maps` / `flat_maps` — lazy iterator output (stryke); `map` / `flat_map` use `false`.
983        #[serde(default)]
984        stream: bool,
985    },
986    /// `map EXPR, LIST` — EXPR is evaluated in list context with `$_` set to each element.
987    MapExprComma {
988        expr: Box<Expr>,
989        list: Box<Expr>,
990        flatten_array_refs: bool,
991        #[serde(default)]
992        stream: bool,
993    },
994    GrepExpr {
995        block: Block,
996        list: Box<Expr>,
997        #[serde(default)]
998        keyword: GrepBuiltinKeyword,
999    },
1000    /// `grep EXPR, LIST` — EXPR is evaluated with `$_` set to each element (Perl list vs scalar context).
1001    GrepExprComma {
1002        expr: Box<Expr>,
1003        list: Box<Expr>,
1004        #[serde(default)]
1005        keyword: GrepBuiltinKeyword,
1006    },
1007    /// `sort BLOCK LIST`, `sort SUB LIST`, or `sort $coderef LIST` (Perl uses `$a`/`$b` in the comparator).
1008    SortExpr {
1009        cmp: Option<SortComparator>,
1010        list: Box<Expr>,
1011    },
1012    ReverseExpr(Box<Expr>),
1013    /// `rev EXPR` — always string-reverse (scalar reverse), stryke extension.
1014    Rev(Box<Expr>),
1015    JoinExpr {
1016        separator: Box<Expr>,
1017        list: Box<Expr>,
1018    },
1019    SplitExpr {
1020        pattern: Box<Expr>,
1021        string: Box<Expr>,
1022        limit: Option<Box<Expr>>,
1023    },
1024    /// `each { BLOCK } @list` — execute BLOCK for each element
1025    /// with `$_` aliased; void context (returns count in scalar context).
1026    ForEachExpr {
1027        block: Block,
1028        list: Box<Expr>,
1029    },
1030
1031    // Parallel extensions
1032    PMapExpr {
1033        block: Block,
1034        list: Box<Expr>,
1035        /// `pmap { } @list, progress => EXPR` — when truthy, print a progress bar on stderr.
1036        progress: Option<Box<Expr>>,
1037        /// `pflat_map { }` — flatten each block result like [`ExprKind::MapExpr`] (arrays expand);
1038        /// parallel output is stitched in **input order** (unlike plain `pmap`, which is unordered).
1039        flat_outputs: bool,
1040        /// `pmap_on $cluster { } @list` — fan out over SSH (`stryke --remote-worker`); `None` = local rayon.
1041        #[serde(default, skip_serializing_if = "Option::is_none")]
1042        on_cluster: Option<Box<Expr>>,
1043        /// `pmaps` / `pflat_maps` — streaming variant: returns a lazy iterator that processes
1044        /// chunks in parallel via rayon instead of eagerly collecting all results.
1045        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1046        stream: bool,
1047    },
1048    /// `pmap_chunked N { BLOCK } @list [, progress => EXPR]` — parallel map in batches of N.
1049    PMapChunkedExpr {
1050        chunk_size: Box<Expr>,
1051        block: Block,
1052        list: Box<Expr>,
1053        progress: Option<Box<Expr>>,
1054    },
1055    PGrepExpr {
1056        block: Block,
1057        list: Box<Expr>,
1058        /// `pgrep { } @list, progress => EXPR` — stderr progress bar when truthy.
1059        progress: Option<Box<Expr>>,
1060        /// `pgreps` — streaming variant: returns a lazy iterator.
1061        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1062        stream: bool,
1063    },
1064    /// `pfor { BLOCK } @list [, progress => EXPR]` — stderr progress bar when truthy.
1065    PForExpr {
1066        block: Block,
1067        list: Box<Expr>,
1068        progress: Option<Box<Expr>>,
1069    },
1070    /// `par { BLOCK } INPUT` — generic parallel-chunk wrapper. Splits INPUT
1071    /// (string → UTF-8-aligned byte chunks; array/list → element-chunks)
1072    /// into N pieces (N = available rayon threads), evaluates BLOCK per
1073    /// chunk in parallel with `$_` bound to the chunk, then concatenates
1074    /// results. Lets any whole-input op (`letters`, `chars`, `uc`, `freq`,
1075    /// regex `//g`, etc.) parallelize without needing a `pX` variant.
1076    ParExpr {
1077        block: Block,
1078        list: Box<Expr>,
1079    },
1080    /// `par_reduce { extract } [ { merge } ] INPUT` — chunk-extract-merge.
1081    /// Same chunker as `par {}`, but each chunk's result is reduced
1082    /// pairwise across chunks instead of concatenated.
1083    ///
1084    /// - One block: auto-merger picks based on result type (number → `+`,
1085    ///   `hash<num>` → key-wise `+`, array → concat, string → concat).
1086    /// - Two blocks: explicit pairwise reducer with `$a`/`$b`.
1087    ParReduceExpr {
1088        extract_block: Block,
1089        reduce_block: Option<Block>,
1090        list: Box<Expr>,
1091    },
1092    /// `par_lines PATH, fn { ... } [, progress => EXPR]` — optional stderr progress (per line).
1093    ParLinesExpr {
1094        path: Box<Expr>,
1095        callback: Box<Expr>,
1096        progress: Option<Box<Expr>>,
1097    },
1098    /// `par_walk PATH, fn { ... } [, progress => EXPR]` — parallel recursive directory walk; `$_` is each path.
1099    ParWalkExpr {
1100        path: Box<Expr>,
1101        callback: Box<Expr>,
1102        progress: Option<Box<Expr>>,
1103    },
1104    /// `pwatch GLOB, fn { ... }` — notify-based watcher (evaluated by interpreter).
1105    PwatchExpr {
1106        path: Box<Expr>,
1107        callback: Box<Expr>,
1108    },
1109    /// `psort { } @list [, progress => EXPR]` — stderr progress when truthy (start/end phases).
1110    PSortExpr {
1111        cmp: Option<Block>,
1112        list: Box<Expr>,
1113        progress: Option<Box<Expr>>,
1114    },
1115    /// `reduce { $a + $b } @list` — sequential left fold over the list.
1116    /// `$a` is the accumulator; `$b` is the next list element.
1117    ReduceExpr {
1118        block: Block,
1119        list: Box<Expr>,
1120    },
1121    /// `preduce { $a + $b } @list` — parallel fold/reduce using rayon.
1122    /// $a and $b are set to the accumulator and current element.
1123    PReduceExpr {
1124        block: Block,
1125        list: Box<Expr>,
1126        /// `preduce { } @list, progress => EXPR` — stderr progress bar when truthy.
1127        progress: Option<Box<Expr>>,
1128    },
1129    /// `preduce_init EXPR, { $a / $b } @list` — parallel fold with explicit identity.
1130    /// Each chunk starts from a clone of `EXPR`; partials are merged (hash maps add counts per key;
1131    /// other types use the same block with `$a` / `$b` as partial accumulators). `$a` is the
1132    /// accumulator, `$b` is the next list element; `@_` is `($a, $b)` for `my ($acc, $item) = @_`.
1133    PReduceInitExpr {
1134        init: Box<Expr>,
1135        block: Block,
1136        list: Box<Expr>,
1137        progress: Option<Box<Expr>>,
1138    },
1139    /// `pmap_reduce { map } { reduce } @list` — fused parallel map + tree reduce (no full mapped array).
1140    PMapReduceExpr {
1141        map_block: Block,
1142        reduce_block: Block,
1143        list: Box<Expr>,
1144        progress: Option<Box<Expr>>,
1145    },
1146    /// `pcache { BLOCK } @list [, progress => EXPR]` — stderr progress bar when truthy.
1147    PcacheExpr {
1148        block: Block,
1149        list: Box<Expr>,
1150        progress: Option<Box<Expr>>,
1151    },
1152    /// `pselect($rx1, $rx2, ...)` — optional `timeout => SECS` for bounded wait.
1153    PselectExpr {
1154        receivers: Vec<Expr>,
1155        timeout: Option<Box<Expr>>,
1156    },
1157    /// `fan [COUNT] { BLOCK }` — execute BLOCK COUNT times in parallel (default COUNT = rayon pool size).
1158    /// `fan_cap [COUNT] { BLOCK }` — same, but return value is a **list** of each block's return value (index order).
1159    /// `$_` is set to the iteration index (0..COUNT-1).
1160    /// Optional `, progress => EXPR` — stderr progress bar (like `pmap`).
1161    FanExpr {
1162        count: Option<Box<Expr>>,
1163        block: Block,
1164        progress: Option<Box<Expr>>,
1165        capture: bool,
1166    },
1167
1168    /// `async { BLOCK }` — run BLOCK on a worker thread; returns a task handle.
1169    AsyncBlock {
1170        body: Block,
1171    },
1172    /// `spawn { BLOCK }` — same as [`ExprKind::AsyncBlock`] (Rust `thread::spawn`–style naming); join with `await`.
1173    SpawnBlock {
1174        body: Block,
1175    },
1176    /// `trace { BLOCK }` — print `mysync` scalar mutations to stderr (for parallel debugging).
1177    Trace {
1178        body: Block,
1179    },
1180    /// `timer { BLOCK }` — run BLOCK and return elapsed wall time in milliseconds (float).
1181    Timer {
1182        body: Block,
1183    },
1184    /// `bench { BLOCK } N` — run BLOCK `N` times (warmup + min/mean/p99 wall time, ms).
1185    Bench {
1186        body: Block,
1187        times: Box<Expr>,
1188    },
1189    /// `spinner "msg" { BLOCK }` — animated spinner on stderr while block runs.
1190    Spinner {
1191        message: Box<Expr>,
1192        body: Block,
1193    },
1194    /// `await EXPR` — join an async task, or return EXPR unchanged.
1195    Await(Box<Expr>),
1196    /// Read entire file as UTF-8 (`slurp $path`).
1197    Slurp(Box<Expr>),
1198    /// Run shell command and return structured output (`capture "cmd"`).
1199    Capture(Box<Expr>),
1200    /// `` `cmd` `` / `qx{cmd}` — run via `sh -c`, return **stdout as a string** (Perl); updates `$?`.
1201    Qx(Box<Expr>),
1202    /// Blocking HTTP GET (`fetch_url $url`).
1203    FetchUrl(Box<Expr>),
1204
1205    /// `pchannel()` — unbounded; `pchannel(N)` — bounded capacity N.
1206    Pchannel {
1207        capacity: Option<Box<Expr>>,
1208    },
1209
1210    // Array/Hash operations
1211    Push {
1212        array: Box<Expr>,
1213        values: Vec<Expr>,
1214    },
1215    Pop(Box<Expr>),
1216    Shift(Box<Expr>),
1217    Unshift {
1218        array: Box<Expr>,
1219        values: Vec<Expr>,
1220    },
1221    Splice {
1222        array: Box<Expr>,
1223        offset: Option<Box<Expr>>,
1224        length: Option<Box<Expr>>,
1225        replacement: Vec<Expr>,
1226    },
1227    Delete(Box<Expr>),
1228    Exists(Box<Expr>),
1229    Keys(Box<Expr>),
1230    Values(Box<Expr>),
1231    Each(Box<Expr>),
1232
1233    // String operations
1234    Chomp(Box<Expr>),
1235    Chop(Box<Expr>),
1236    Length(Box<Expr>),
1237    Substr {
1238        string: Box<Expr>,
1239        offset: Box<Expr>,
1240        length: Option<Box<Expr>>,
1241        replacement: Option<Box<Expr>>,
1242    },
1243    Index {
1244        string: Box<Expr>,
1245        substr: Box<Expr>,
1246        position: Option<Box<Expr>>,
1247    },
1248    Rindex {
1249        string: Box<Expr>,
1250        substr: Box<Expr>,
1251        position: Option<Box<Expr>>,
1252    },
1253    Sprintf {
1254        format: Box<Expr>,
1255        args: Vec<Expr>,
1256    },
1257
1258    // Numeric
1259    Abs(Box<Expr>),
1260    Int(Box<Expr>),
1261    Sqrt(Box<Expr>),
1262    Sin(Box<Expr>),
1263    Cos(Box<Expr>),
1264    Atan2 {
1265        y: Box<Expr>,
1266        x: Box<Expr>,
1267    },
1268    Exp(Box<Expr>),
1269    Log(Box<Expr>),
1270    /// `rand` with optional upper bound (none = Perl default 1.0).
1271    Rand(Option<Box<Expr>>),
1272    /// `srand` with optional seed (none = time-based).
1273    Srand(Option<Box<Expr>>),
1274    Hex(Box<Expr>),
1275    Oct(Box<Expr>),
1276
1277    // Case
1278    Lc(Box<Expr>),
1279    Uc(Box<Expr>),
1280    Lcfirst(Box<Expr>),
1281    Ucfirst(Box<Expr>),
1282
1283    /// Unicode case fold (Perl `fc`).
1284    Fc(Box<Expr>),
1285    /// DES-style `crypt` (see libc `crypt(3)` on Unix; empty on other targets).
1286    Crypt {
1287        plaintext: Box<Expr>,
1288        salt: Box<Expr>,
1289    },
1290    /// `pos` — optional scalar lvalue target (`None` = `$_`).
1291    Pos(Option<Box<Expr>>),
1292    /// `study` — hint for repeated matching; returns byte length of the string.
1293    Study(Box<Expr>),
1294
1295    // Type
1296    Defined(Box<Expr>),
1297    Ref(Box<Expr>),
1298    ScalarContext(Box<Expr>),
1299
1300    // Char
1301    Chr(Box<Expr>),
1302    Ord(Box<Expr>),
1303
1304    // I/O
1305    /// `open my $fh` — only valid as [`ExprKind::Open::handle`]; declares `$fh` and binds the handle.
1306    OpenMyHandle {
1307        name: String,
1308    },
1309    Open {
1310        handle: Box<Expr>,
1311        mode: Box<Expr>,
1312        file: Option<Box<Expr>>,
1313    },
1314    Close(Box<Expr>),
1315    ReadLine(Option<String>),
1316    Eof(Option<Box<Expr>>),
1317
1318    Opendir {
1319        handle: Box<Expr>,
1320        path: Box<Expr>,
1321    },
1322    Readdir(Box<Expr>),
1323    Closedir(Box<Expr>),
1324    Rewinddir(Box<Expr>),
1325    Telldir(Box<Expr>),
1326    Seekdir {
1327        handle: Box<Expr>,
1328        position: Box<Expr>,
1329    },
1330
1331    // File tests
1332    FileTest {
1333        op: char,
1334        expr: Box<Expr>,
1335    },
1336
1337    // System
1338    System(Vec<Expr>),
1339    Exec(Vec<Expr>),
1340    Eval(Box<Expr>),
1341    Do(Box<Expr>),
1342    Require(Box<Expr>),
1343    Exit(Option<Box<Expr>>),
1344    Chdir(Box<Expr>),
1345    Mkdir {
1346        path: Box<Expr>,
1347        mode: Option<Box<Expr>>,
1348    },
1349    Unlink(Vec<Expr>),
1350    Rename {
1351        old: Box<Expr>,
1352        new: Box<Expr>,
1353    },
1354    /// `chmod MODE, @files` — first expr is mode, rest are paths.
1355    Chmod(Vec<Expr>),
1356    /// `chown UID, GID, @files` — first two are uid/gid, rest are paths.
1357    Chown(Vec<Expr>),
1358
1359    Stat(Box<Expr>),
1360    Lstat(Box<Expr>),
1361    Link {
1362        old: Box<Expr>,
1363        new: Box<Expr>,
1364    },
1365    Symlink {
1366        old: Box<Expr>,
1367        new: Box<Expr>,
1368    },
1369    Readlink(Box<Expr>),
1370    /// `files` / `files DIR` — list file names in a directory (default: `.`).
1371    Files(Vec<Expr>),
1372    /// `filesf` / `filesf DIR` / `f` — list only regular file names in a directory (default: `.`).
1373    Filesf(Vec<Expr>),
1374    /// `fr DIR` — list only regular file names recursively (default: `.`).
1375    FilesfRecursive(Vec<Expr>),
1376    /// `dirs` / `dirs DIR` / `d` — list subdirectory names in a directory (default: `.`).
1377    Dirs(Vec<Expr>),
1378    /// `dr DIR` — list subdirectory paths recursively (default: `.`).
1379    DirsRecursive(Vec<Expr>),
1380    /// `sym_links` / `sym_links DIR` — list symlink names in a directory (default: `.`).
1381    SymLinks(Vec<Expr>),
1382    /// `sockets` / `sockets DIR` — list Unix socket names in a directory (default: `.`).
1383    Sockets(Vec<Expr>),
1384    /// `pipes` / `pipes DIR` — list named-pipe (FIFO) names in a directory (default: `.`).
1385    Pipes(Vec<Expr>),
1386    /// `block_devices` / `block_devices DIR` — list block device names in a directory (default: `.`).
1387    BlockDevices(Vec<Expr>),
1388    /// `char_devices` / `char_devices DIR` — list character device names in a directory (default: `.`).
1389    CharDevices(Vec<Expr>),
1390    /// `exe` / `exe DIR` — list executable file names in a directory (default: `.`).
1391    Executables(Vec<Expr>),
1392    Glob(Vec<Expr>),
1393    /// Parallel recursive glob (rayon); same patterns as `glob`, different walk strategy.
1394    /// Optional `, progress => EXPR` — stderr progress bar (one tick per pattern).
1395    GlobPar {
1396        args: Vec<Expr>,
1397        progress: Option<Box<Expr>>,
1398    },
1399    /// `par_sed PATTERN, REPLACEMENT, FILES... [, progress => EXPR]` — parallel in-place regex replace per file (`g` semantics).
1400    ParSed {
1401        args: Vec<Expr>,
1402        progress: Option<Box<Expr>>,
1403    },
1404
1405    // Bless
1406    Bless {
1407        ref_expr: Box<Expr>,
1408        class: Option<Box<Expr>>,
1409    },
1410
1411    // Caller
1412    Caller(Option<Box<Expr>>),
1413
1414    // Wantarray
1415    Wantarray,
1416
1417    // List / Context
1418    List(Vec<Expr>),
1419
1420    // Postfix if/unless/while/until/for
1421    PostfixIf {
1422        expr: Box<Expr>,
1423        condition: Box<Expr>,
1424    },
1425    PostfixUnless {
1426        expr: Box<Expr>,
1427        condition: Box<Expr>,
1428    },
1429    PostfixWhile {
1430        expr: Box<Expr>,
1431        condition: Box<Expr>,
1432    },
1433    PostfixUntil {
1434        expr: Box<Expr>,
1435        condition: Box<Expr>,
1436    },
1437    PostfixForeach {
1438        expr: Box<Expr>,
1439        list: Box<Expr>,
1440    },
1441
1442    /// `retry { BLOCK } times => N [, backoff => linear|exponential|none]` — re-run block until success or attempts exhausted.
1443    RetryBlock {
1444        body: Block,
1445        times: Box<Expr>,
1446        backoff: RetryBackoff,
1447    },
1448    /// `rate_limit(MAX, WINDOW) { BLOCK }` — sliding window: at most MAX runs per WINDOW (e.g. `"1s"`).
1449    /// `slot` is assigned at parse time for per-site state in the interpreter.
1450    RateLimitBlock {
1451        slot: u32,
1452        max: Box<Expr>,
1453        window: Box<Expr>,
1454        body: Block,
1455    },
1456    /// `every(INTERVAL) { BLOCK }` — repeat BLOCK forever with sleep (INTERVAL like `"5s"` or seconds).
1457    EveryBlock {
1458        interval: Box<Expr>,
1459        body: Block,
1460    },
1461    /// `gen { ... yield ... }` — lazy generator; call `->next` for each value.
1462    GenBlock {
1463        body: Block,
1464    },
1465    /// `yield EXPR` — only valid inside `gen { }` (and propagates through control flow).
1466    Yield(Box<Expr>),
1467
1468    /// `match (EXPR) { PATTERN => EXPR, ... }` — first matching arm; bindings scoped to the arm body.
1469    AlgebraicMatch {
1470        subject: Box<Expr>,
1471        arms: Vec<MatchArm>,
1472    },
1473}
1474
1475#[derive(Debug, Clone, Serialize, Deserialize)]
1476pub enum StringPart {
1477    Literal(String),
1478    ScalarVar(String),
1479    ArrayVar(String),
1480    Expr(Expr),
1481}
1482
1483#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1484pub enum DerefKind {
1485    Array,
1486    Hash,
1487    Call,
1488}
1489
1490#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1491pub enum BinOp {
1492    Add,
1493    Sub,
1494    Mul,
1495    Div,
1496    Mod,
1497    Pow,
1498    Concat,
1499    NumEq,
1500    NumNe,
1501    NumLt,
1502    NumGt,
1503    NumLe,
1504    NumGe,
1505    Spaceship,
1506    StrEq,
1507    StrNe,
1508    StrLt,
1509    StrGt,
1510    StrLe,
1511    StrGe,
1512    StrCmp,
1513    LogAnd,
1514    LogOr,
1515    DefinedOr,
1516    BitAnd,
1517    BitOr,
1518    BitXor,
1519    ShiftLeft,
1520    ShiftRight,
1521    LogAndWord,
1522    LogOrWord,
1523    BindMatch,
1524    BindNotMatch,
1525}
1526
1527#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1528pub enum UnaryOp {
1529    Negate,
1530    LogNot,
1531    BitNot,
1532    LogNotWord,
1533    PreIncrement,
1534    PreDecrement,
1535    Ref,
1536}
1537
1538#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1539pub enum PostfixOp {
1540    Increment,
1541    Decrement,
1542}
1543
1544#[cfg(test)]
1545mod tests {
1546    use super::*;
1547
1548    #[test]
1549    fn binop_deref_kind_distinct() {
1550        assert_ne!(BinOp::Add, BinOp::Sub);
1551        assert_eq!(DerefKind::Call, DerefKind::Call);
1552    }
1553
1554    #[test]
1555    fn sigil_variants_exhaustive_in_tests() {
1556        let all = [Sigil::Scalar, Sigil::Array, Sigil::Hash];
1557        assert_eq!(all.len(), 3);
1558    }
1559
1560    #[test]
1561    fn program_empty_roundtrip_clone() {
1562        let p = Program { statements: vec![] };
1563        assert!(p.clone().statements.is_empty());
1564    }
1565
1566    #[test]
1567    fn program_serializes_to_json() {
1568        let p = crate::parse("1+2;").expect("parse");
1569        let s = serde_json::to_string(&p).expect("json");
1570        assert!(s.contains("\"statements\""));
1571        assert!(s.contains("BinOp"));
1572    }
1573}