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