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