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