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