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