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/// `Program` — see fields for layout.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Program {
12    /// `statements` field.
13    pub statements: Vec<Statement>,
14}
15/// `Statement` — see fields for layout.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct Statement {
18    /// Leading `LABEL:` on this statement (Perl convention: `FOO:`).
19    pub label: Option<String>,
20    /// `kind` field.
21    pub kind: StmtKind,
22    /// `line` field.
23    pub line: usize,
24}
25
26impl Statement {
27    /// `new` — see implementation.
28    pub fn new(kind: StmtKind, line: usize) -> Self {
29        Self {
30            label: None,
31            kind,
32            line,
33        }
34    }
35}
36
37/// Surface spelling for `grep` / `greps` / `filter` (`fi`) / `find_all`.
38/// `grep` is eager (Perl-compatible); `greps` / `filter` / `find_all` are lazy (streaming).
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
40#[serde(rename_all = "snake_case")]
41#[derive(Default)]
42pub enum GrepBuiltinKeyword {
43    /// `Grep` variant.
44    #[default]
45    Grep,
46    /// `Greps` variant.
47    Greps,
48    /// `Filter` variant.
49    Filter,
50    /// `FindAll` variant.
51    FindAll,
52}
53
54impl GrepBuiltinKeyword {
55    /// `as_str` — see implementation.
56    pub const fn as_str(self) -> &'static str {
57        match self {
58            Self::Grep => "grep",
59            Self::Greps => "greps",
60            Self::Filter => "filter",
61            Self::FindAll => "find_all",
62        }
63    }
64
65    /// Returns `true` for streaming variants (`greps`, `filter`, `find_all`).
66    pub const fn is_stream(self) -> bool {
67        !matches!(self, Self::Grep)
68    }
69}
70
71/// Named parameter in `sub name (SIG ...) { }` — stryke extension (not Perl 5 prototype syntax).
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub enum SubSigParam {
74    /// `$name`, `$name: Type`, or `$name = default` — one positional scalar from `@_`,
75    /// optionally typed and/or with a default value.
76    Scalar(String, Option<PerlTypeName>, Option<Box<Expr>>),
77    /// `@name` or `@name = (default, list)` — slurps remaining positional args into an array.
78    Array(String, Option<Box<Expr>>),
79    /// `%name` or `%name = (key => val, ...)` — slurps remaining positional args into a hash.
80    Hash(String, Option<Box<Expr>>),
81    /// `[ $a, @tail, ... ]` — next argument must be array-like; same element rules as algebraic `match`.
82    ArrayDestruct(Vec<MatchArrayElem>),
83    /// `{ k => $v, ... }` — next argument must be a hash or hashref; keys bind to listed scalars.
84    HashDestruct(Vec<(String, String)>),
85}
86/// `StmtKind` — see variants.
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub enum StmtKind {
89    /// `Expression` variant.
90    Expression(Expr),
91    /// `If` variant.
92    If {
93        condition: Expr,
94        body: Block,
95        elsifs: Vec<(Expr, Block)>,
96        else_block: Option<Block>,
97    },
98    /// `Unless` variant.
99    Unless {
100        condition: Expr,
101        body: Block,
102        else_block: Option<Block>,
103    },
104    /// `While` variant.
105    While {
106        condition: Expr,
107        body: Block,
108        label: Option<String>,
109        /// `while (...) { } continue { }`
110        continue_block: Option<Block>,
111    },
112    /// `Until` variant.
113    Until {
114        condition: Expr,
115        body: Block,
116        label: Option<String>,
117        continue_block: Option<Block>,
118    },
119    /// `DoWhile` variant.
120    DoWhile { body: Block, condition: Expr },
121    /// `For` variant.
122    For {
123        init: Option<Box<Statement>>,
124        condition: Option<Expr>,
125        step: Option<Expr>,
126        body: Block,
127        label: Option<String>,
128        continue_block: Option<Block>,
129    },
130    /// `Foreach` variant.
131    Foreach {
132        var: String,
133        list: Expr,
134        body: Block,
135        label: Option<String>,
136        continue_block: Option<Block>,
137    },
138    /// `SubDecl` variant.
139    SubDecl {
140        name: String,
141        params: Vec<SubSigParam>,
142        body: Block,
143        /// Subroutine prototype text from `sub foo ($$) { }` (excluding parens).
144        /// `None` when using structured [`SubSigParam`] signatures instead.
145        prototype: Option<String>,
146    },
147    /// `Package` variant.
148    Package { name: String },
149    /// `Use` variant.
150    ///
151    /// `version` is `Some(v)` only for the use-site override syntax
152    /// `use Foo@VERSION` — pins resolution to `<store>/<name>@<version>/`
153    /// directly, bypassing lockfile / installed-index lookups. `None`
154    /// for plain `use Foo`, which respects the project's lockfile pin
155    /// (inside a stryke project) or the global installed.toml entry
156    /// (standalone scripts).
157    Use {
158        module: String,
159        imports: Vec<Expr>,
160        version: Option<String>,
161    },
162    /// `use 5.008;` / `use 5;` — Perl version requirement (no-op at runtime in stryke).
163    UsePerlVersion { version: f64 },
164    /// `use overload '""' => 'as_string', '+' => 'add';` — operator maps (method names in current package).
165    UseOverload { pairs: Vec<(String, String)> },
166    /// `No` variant.
167    No { module: String, imports: Vec<Expr> },
168    /// `Return` variant.
169    Return(Option<Expr>),
170    /// `Last` variant.
171    Last(Option<String>),
172    /// `Next` variant.
173    Next(Option<String>),
174    /// `Redo` variant.
175    Redo(Option<String>),
176    /// `My` variant.
177    My(Vec<VarDecl>),
178    /// `Our` variant.
179    Our(Vec<VarDecl>),
180    /// `Local` variant.
181    Local(Vec<VarDecl>),
182    /// `state $x = 0` — persistent lexical variable (initialized once per sub)
183    State(Vec<VarDecl>),
184    /// `local $h{k}` / `local $SIG{__WARN__}` — lvalues that are not plain `my`-style names.
185    LocalExpr {
186        target: Expr,
187        initializer: Option<Expr>,
188    },
189    /// `mysync $x = 0` — thread-safe atomic variable for parallel blocks
190    MySync(Vec<VarDecl>),
191    /// `oursync $x = 0` — package-global thread-safe atomic variable. Same as
192    /// `mysync` but the binding lives in the package stash (e.g. `main::x`)
193    /// so it is visible across packages and parallel workers share one cell.
194    OurSync(Vec<VarDecl>),
195    /// Bare block (for scoping or do {})
196    Block(Block),
197    /// Statements run in order without an extra scope frame (parser desugar).
198    StmtGroup(Block),
199    /// `BEGIN { ... }`
200    Begin(Block),
201    /// `END { ... }`
202    End(Block),
203    /// `UNITCHECK { ... }` — end of compilation unit (reverse order before CHECK).
204    UnitCheck(Block),
205    /// `CHECK { ... }` — end of compile phase (reverse order).
206    Check(Block),
207    /// `INIT { ... }` — before runtime main (forward order).
208    Init(Block),
209    /// Empty statement (bare semicolon)
210    Empty,
211    /// `goto EXPR` — expression evaluates to a label name in the same block.
212    Goto { target: Box<Expr> },
213    /// Standalone `continue { BLOCK }` (normally follows a loop; parsed for acceptance).
214    Continue(Block),
215    /// `struct Name { field => Type, ... }` — fixed-field records (`Name->new`, `$x->field`).
216    StructDecl { def: StructDef },
217    /// `enum Name { Variant1 => Type, Variant2, ... }` — algebraic data types.
218    EnumDecl { def: EnumDef },
219    /// `class Name extends Parent impl Trait { fields; methods }` — full OOP.
220    ClassDecl { def: ClassDef },
221    /// `trait Name { fn required; fn with_default { } }` — interface/mixin.
222    TraitDecl { def: TraitDef },
223    /// `eval_timeout SECS { ... }` — run block on a worker thread; main waits up to SECS (portable timeout).
224    EvalTimeout { timeout: Expr, body: Block },
225    /// `try { } catch ($err) { } [ finally { } ]` — catch runtime/die errors (not `last`/`next`/`return` flow).
226    /// `finally` runs after a successful `try` or after `catch` completes (including if `catch` rethrows).
227    TryCatch {
228        try_block: Block,
229        catch_var: String,
230        catch_block: Block,
231        finally_block: Option<Block>,
232    },
233    /// `given (EXPR) { when ... default ... }` — topic in `$_`, `when` matches with regex / eq / smartmatch.
234    Given { topic: Expr, body: Block },
235    /// `when (COND) { }` — only valid inside `given` (handled by given dispatcher).
236    When { cond: Expr, body: Block },
237    /// `default { }` — only valid inside `given`.
238    DefaultCase { body: Block },
239    /// `tie %hash` / `tie @arr` / `tie $x` — TIEHASH / TIEARRAY / TIESCALAR (FETCH/STORE).
240    Tie {
241        target: TieTarget,
242        class: Expr,
243        args: Vec<Expr>,
244    },
245    /// `format NAME =` picture/value lines … `.` — report templates for `write`.
246    FormatDecl { name: String, lines: Vec<String> },
247    /// `before|after|around "<glob>" { ... }` — register AOP advice on user subs.
248    /// Pattern is a glob (`*`, `?`) matched against the called sub's bare name.
249    AdviceDecl {
250        kind: AdviceKind,
251        pattern: String,
252        body: Block,
253    },
254}
255
256/// AOP advice kind for [`StmtKind::AdviceDecl`].
257#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
258pub enum AdviceKind {
259    /// Run before the matched sub; sees `INTERCEPT_NAME` / `INTERCEPT_ARGS`.
260    Before,
261    /// Run after the matched sub; sees `INTERCEPT_MS` / `INTERCEPT_US` and the retval in `$?`.
262    After,
263    /// Wrap the matched sub; must call `proceed()` to invoke the original.
264    Around,
265}
266
267/// Target of `tie` (hash, array, or scalar).
268#[derive(Debug, Clone, Serialize, Deserialize)]
269pub enum TieTarget {
270    /// `Hash` variant.
271    Hash(String),
272    /// `Array` variant.
273    Array(String),
274    /// `Scalar` variant.
275    Scalar(String),
276}
277
278/// Optional type for `typed my $x : Int` — enforced at assignment time (runtime).
279#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
280pub enum PerlTypeName {
281    /// `Int` variant.
282    Int,
283    /// `Str` variant.
284    Str,
285    /// `Float` variant.
286    Float,
287    /// `Bool` variant.
288    Bool,
289    /// `Array` variant.
290    Array,
291    /// `Hash` variant.
292    Hash,
293    /// `Ref` variant.
294    Ref,
295    /// Struct-typed field: `field => Point` where Point is a struct name.
296    Struct(String),
297    /// Enum-typed field: `field => Color` where Color is an enum name.
298    Enum(String),
299    /// Accepts any value (no runtime type check).
300    Any,
301}
302
303/// Single field in a struct definition.
304#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct StructField {
306    /// `name` field.
307    pub name: String,
308    /// `ty` field.
309    pub ty: PerlTypeName,
310    /// Optional default value expression (evaluated at construction time if field not provided).
311    #[serde(skip_serializing_if = "Option::is_none")]
312    pub default: Option<Expr>,
313}
314
315/// Method defined inside a struct: `fn name { ... }` or `fn name($self, ...) { ... }`.
316#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct StructMethod {
318    /// `name` field.
319    pub name: String,
320    /// `params` field.
321    pub params: Vec<SubSigParam>,
322    /// `body` field.
323    pub body: Block,
324}
325
326/// Single variant in an enum definition.
327#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct EnumVariant {
329    /// `name` field.
330    pub name: String,
331    /// Optional type for data carried by this variant. If None, it carries no data.
332    pub ty: Option<PerlTypeName>,
333}
334
335/// Compile-time algebraic data type: `enum Name { Variant1 => Type, Variant2, ... }`.
336#[derive(Debug, Clone, Serialize, Deserialize)]
337pub struct EnumDef {
338    /// `name` field.
339    pub name: String,
340    /// `variants` field.
341    pub variants: Vec<EnumVariant>,
342}
343
344impl EnumDef {
345    /// `variant_index` — see implementation.
346    #[inline]
347    pub fn variant_index(&self, name: &str) -> Option<usize> {
348        self.variants.iter().position(|v| v.name == name)
349    }
350    /// `variant` — see implementation.
351    #[inline]
352    pub fn variant(&self, name: &str) -> Option<&EnumVariant> {
353        self.variants.iter().find(|v| v.name == name)
354    }
355}
356
357/// Compile-time record type: `struct Name { field => Type, ... ; fn method { } }`.
358#[derive(Debug, Clone, Serialize, Deserialize)]
359pub struct StructDef {
360    /// `name` field.
361    pub name: String,
362    /// `fields` field.
363    pub fields: Vec<StructField>,
364    /// User-defined methods: `fn name { }` inside struct body.
365    #[serde(default, skip_serializing_if = "Vec::is_empty")]
366    pub methods: Vec<StructMethod>,
367}
368
369/// Visibility modifier for class fields and methods.
370#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
371pub enum Visibility {
372    /// `Public` variant.
373    #[default]
374    Public,
375    /// `Private` variant.
376    Private,
377    /// `Protected` variant.
378    Protected,
379}
380
381/// Single field in a class definition: `name: Type = default` or `pub name: Type`.
382#[derive(Debug, Clone, Serialize, Deserialize)]
383pub struct ClassField {
384    /// `name` field.
385    pub name: String,
386    /// `ty` field.
387    pub ty: PerlTypeName,
388    /// `visibility` field.
389    pub visibility: Visibility,
390    /// `default` field.
391    #[serde(skip_serializing_if = "Option::is_none")]
392    pub default: Option<Expr>,
393}
394
395/// Method defined inside a class: `fn name { }` or `pub fn name($self, ...) { }`.
396#[derive(Debug, Clone, Serialize, Deserialize)]
397pub struct ClassMethod {
398    /// `name` field.
399    pub name: String,
400    /// `params` field.
401    pub params: Vec<SubSigParam>,
402    /// `body` field.
403    pub body: Option<Block>,
404    /// `visibility` field.
405    pub visibility: Visibility,
406    /// `is_static` field.
407    pub is_static: bool,
408    /// `is_final` field.
409    #[serde(default, skip_serializing_if = "is_false")]
410    pub is_final: bool,
411}
412
413/// Trait definition: `trait Name { fn required; fn with_default { } }`.
414#[derive(Debug, Clone, Serialize, Deserialize)]
415pub struct TraitDef {
416    /// `name` field.
417    pub name: String,
418    /// `methods` field.
419    pub methods: Vec<ClassMethod>,
420}
421
422impl TraitDef {
423    /// `method` — see implementation.
424    #[inline]
425    pub fn method(&self, name: &str) -> Option<&ClassMethod> {
426        self.methods.iter().find(|m| m.name == name)
427    }
428    /// `required_methods` — see implementation.
429    #[inline]
430    pub fn required_methods(&self) -> impl Iterator<Item = &ClassMethod> {
431        self.methods.iter().filter(|m| m.body.is_none())
432    }
433}
434
435/// A static (class-level) variable: `static count: Int = 0`.
436#[derive(Debug, Clone, Serialize, Deserialize)]
437pub struct ClassStaticField {
438    /// `name` field.
439    pub name: String,
440    /// `ty` field.
441    pub ty: PerlTypeName,
442    /// `visibility` field.
443    pub visibility: Visibility,
444    /// `default` field.
445    #[serde(skip_serializing_if = "Option::is_none")]
446    pub default: Option<Expr>,
447}
448
449/// Class definition: `class Name extends Parent impl Trait { fields; methods }`.
450#[derive(Debug, Clone, Serialize, Deserialize)]
451pub struct ClassDef {
452    /// `name` field.
453    pub name: String,
454    /// `is_abstract` field.
455    #[serde(default, skip_serializing_if = "is_false")]
456    pub is_abstract: bool,
457    /// `is_final` field.
458    #[serde(default, skip_serializing_if = "is_false")]
459    pub is_final: bool,
460    /// `extends` field.
461    #[serde(default, skip_serializing_if = "Vec::is_empty")]
462    pub extends: Vec<String>,
463    /// `implements` field.
464    #[serde(default, skip_serializing_if = "Vec::is_empty")]
465    pub implements: Vec<String>,
466    /// `fields` field.
467    pub fields: Vec<ClassField>,
468    /// `methods` field.
469    pub methods: Vec<ClassMethod>,
470    /// `static_fields` field.
471    #[serde(default, skip_serializing_if = "Vec::is_empty")]
472    pub static_fields: Vec<ClassStaticField>,
473}
474
475fn is_false(v: &bool) -> bool {
476    !*v
477}
478
479impl ClassDef {
480    /// `field_index` — see implementation.
481    #[inline]
482    pub fn field_index(&self, name: &str) -> Option<usize> {
483        self.fields.iter().position(|f| f.name == name)
484    }
485    /// `field` — see implementation.
486    #[inline]
487    pub fn field(&self, name: &str) -> Option<&ClassField> {
488        self.fields.iter().find(|f| f.name == name)
489    }
490    /// `method` — see implementation.
491    #[inline]
492    pub fn method(&self, name: &str) -> Option<&ClassMethod> {
493        self.methods.iter().find(|m| m.name == name)
494    }
495    /// `static_methods` — see implementation.
496    #[inline]
497    pub fn static_methods(&self) -> impl Iterator<Item = &ClassMethod> {
498        self.methods.iter().filter(|m| m.is_static)
499    }
500    /// `instance_methods` — see implementation.
501    #[inline]
502    pub fn instance_methods(&self) -> impl Iterator<Item = &ClassMethod> {
503        self.methods.iter().filter(|m| !m.is_static)
504    }
505}
506
507impl StructDef {
508    /// `field_index` — see implementation.
509    #[inline]
510    pub fn field_index(&self, name: &str) -> Option<usize> {
511        self.fields.iter().position(|f| f.name == name)
512    }
513
514    /// Get field type by name.
515    #[inline]
516    pub fn field_type(&self, name: &str) -> Option<&PerlTypeName> {
517        self.fields.iter().find(|f| f.name == name).map(|f| &f.ty)
518    }
519
520    /// Get method by name.
521    #[inline]
522    pub fn method(&self, name: &str) -> Option<&StructMethod> {
523        self.methods.iter().find(|m| m.name == name)
524    }
525}
526
527impl PerlTypeName {
528    /// Bytecode encoding for `DeclareScalarTyped` / VM (only simple types; struct types use name pool).
529    #[inline]
530    pub fn from_byte(b: u8) -> Option<Self> {
531        match b {
532            0 => Some(Self::Int),
533            1 => Some(Self::Str),
534            2 => Some(Self::Float),
535            3 => Some(Self::Bool),
536            4 => Some(Self::Array),
537            5 => Some(Self::Hash),
538            6 => Some(Self::Ref),
539            7 => Some(Self::Any),
540            _ => None,
541        }
542    }
543
544    /// Bytecode encoding (simple types only; `Struct(name)` / `Enum(name)` requires separate name pool lookup).
545    #[inline]
546    pub fn as_byte(&self) -> Option<u8> {
547        match self {
548            Self::Int => Some(0),
549            Self::Str => Some(1),
550            Self::Float => Some(2),
551            Self::Bool => Some(3),
552            Self::Array => Some(4),
553            Self::Hash => Some(5),
554            Self::Ref => Some(6),
555            Self::Any => Some(7),
556            Self::Struct(_) | Self::Enum(_) => None,
557        }
558    }
559
560    /// Display name for error messages.
561    pub fn display_name(&self) -> String {
562        match self {
563            Self::Int => "Int".to_string(),
564            Self::Str => "Str".to_string(),
565            Self::Float => "Float".to_string(),
566            Self::Bool => "Bool".to_string(),
567            Self::Array => "Array".to_string(),
568            Self::Hash => "Hash".to_string(),
569            Self::Ref => "Ref".to_string(),
570            Self::Any => "Any".to_string(),
571            Self::Struct(name) => name.clone(),
572            Self::Enum(name) => name.clone(),
573        }
574    }
575
576    /// Strict runtime check: `Int` only integer-like [`StrykeValue`](crate::value::StrykeValue), `Str` only string, `Float` allows int or float.
577    pub fn check_value(&self, v: &crate::value::StrykeValue) -> Result<(), String> {
578        match self {
579            Self::Int => {
580                if v.is_integer_like() {
581                    Ok(())
582                } else {
583                    Err(format!("expected Int (INTEGER), got {}", v.type_name()))
584                }
585            }
586            Self::Str => {
587                if v.is_string_like() {
588                    Ok(())
589                } else {
590                    Err(format!("expected Str (STRING), got {}", v.type_name()))
591                }
592            }
593            Self::Float => {
594                if v.is_integer_like() || v.is_float_like() {
595                    Ok(())
596                } else {
597                    Err(format!(
598                        "expected Float (INTEGER or FLOAT), got {}",
599                        v.type_name()
600                    ))
601                }
602            }
603            Self::Bool => Ok(()),
604            Self::Array => {
605                if v.as_array_vec().is_some() || v.as_array_ref().is_some() {
606                    Ok(())
607                } else {
608                    Err(format!("expected Array, got {}", v.type_name()))
609                }
610            }
611            Self::Hash => {
612                if v.as_hash_map().is_some() || v.as_hash_ref().is_some() {
613                    Ok(())
614                } else {
615                    Err(format!("expected Hash, got {}", v.type_name()))
616                }
617            }
618            Self::Ref => {
619                if v.as_scalar_ref().is_some()
620                    || v.as_array_ref().is_some()
621                    || v.as_hash_ref().is_some()
622                    || v.as_code_ref().is_some()
623                {
624                    Ok(())
625                } else {
626                    Err(format!("expected Ref, got {}", v.type_name()))
627                }
628            }
629            Self::Struct(name) => {
630                // Allow undef for struct/class types (nullable pattern)
631                if v.is_undef() {
632                    return Ok(());
633                }
634                if let Some(s) = v.as_struct_inst() {
635                    if s.def.name == *name {
636                        Ok(())
637                    } else {
638                        Err(format!(
639                            "expected struct {}, got struct {}",
640                            name, s.def.name
641                        ))
642                    }
643                } else if let Some(e) = v.as_enum_inst() {
644                    if e.def.name == *name {
645                        Ok(())
646                    } else {
647                        Err(format!("expected {}, got enum {}", name, e.def.name))
648                    }
649                } else if let Some(c) = v.as_class_inst() {
650                    // Check class name and full inheritance hierarchy
651                    if c.isa(name) {
652                        Ok(())
653                    } else {
654                        Err(format!("expected {}, got {}", name, c.def.name))
655                    }
656                } else if let Some(b) = v.as_blessed_ref() {
657                    // Old-style `bless {...}, "Class"` — accept as the
658                    // nominal type if the class name matches. Lets typed-
659                    // my survive any escape hatch that reaches the value
660                    // through the Perl 5 OO path.
661                    if b.class == *name {
662                        Ok(())
663                    } else {
664                        Err(format!("expected {}, got {}", name, b.class))
665                    }
666                } else {
667                    Err(format!("expected {}, got {}", name, v.type_name()))
668                }
669            }
670            Self::Enum(name) => {
671                // Allow undef for enum types (nullable pattern)
672                if v.is_undef() {
673                    return Ok(());
674                }
675                if let Some(e) = v.as_enum_inst() {
676                    if e.def.name == *name {
677                        Ok(())
678                    } else {
679                        Err(format!("expected enum {}, got enum {}", name, e.def.name))
680                    }
681                } else {
682                    Err(format!("expected enum {}, got {}", name, v.type_name()))
683                }
684            }
685            Self::Any => Ok(()),
686        }
687    }
688}
689/// `VarDecl` — see fields for layout.
690#[derive(Debug, Clone, Serialize, Deserialize)]
691pub struct VarDecl {
692    /// `sigil` field.
693    pub sigil: Sigil,
694    /// `name` field.
695    pub name: String,
696    /// `initializer` field.
697    pub initializer: Option<Expr>,
698    /// Set by `frozen my ...` — reassignments are rejected at compile time (bytecode) or runtime.
699    pub frozen: bool,
700    /// Set by `typed my $x : Int` (scalar only).
701    pub type_annotation: Option<PerlTypeName>,
702    /// True when declared with parens: `my ($x) = @a` vs `my $x = @a`.
703    /// In list context, a scalar gets the first element; in scalar context, it gets the count.
704    #[serde(default)]
705    pub list_context: bool,
706}
707/// `Sigil` — see variants.
708#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
709pub enum Sigil {
710    /// `Scalar` variant.
711    Scalar,
712    /// `Array` variant.
713    Array,
714    /// `Hash` variant.
715    Hash,
716    /// `local *FH` — filehandle slot alias (limited typeglob).
717    Typeglob,
718}
719/// `Block` type alias.
720pub type Block = Vec<Statement>;
721
722/// Comparator for `sort` — `{ $a <=> $b }`, or a code ref / expression (Perl `sort $cmp LIST`).
723#[derive(Debug, Clone, Serialize, Deserialize)]
724pub enum SortComparator {
725    /// `Block` variant.
726    Block(Block),
727    /// `Code` variant.
728    Code(Box<Expr>),
729}
730
731// ── Algebraic `match` expression (stryke extension) ──
732
733/// One arm of [`ExprKind::AlgebraicMatch`]: `PATTERN [if EXPR] => EXPR`.
734#[derive(Debug, Clone, Serialize, Deserialize)]
735pub struct MatchArm {
736    /// `pattern` field.
737    pub pattern: MatchPattern,
738    /// Optional guard (`if EXPR`) evaluated after pattern match; `$_` is the match subject.
739    #[serde(skip_serializing_if = "Option::is_none")]
740    pub guard: Option<Box<Expr>>,
741    /// `body` field.
742    pub body: Expr,
743}
744
745/// `retry { } backoff => exponential` — sleep policy between attempts (after failure).
746#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
747pub enum RetryBackoff {
748    None,
749    /// Delay grows linearly: `base_ms * attempt` (attempt starts at 1).
750    Linear,
751    /// Delay doubles each failure: `base_ms * 2^(attempt-1)` (capped).
752    Exponential,
753}
754
755/// Pattern for algebraic `match` (distinct from the `=~` / regex [`ExprKind::Match`]).
756#[derive(Debug, Clone, Serialize, Deserialize)]
757pub enum MatchPattern {
758    /// `_` — matches anything.
759    Any,
760    /// `/regex/` — subject stringified; on success the arm body sets `$_` to the subject and
761    /// populates match variables (`$1`…, `$&`, `${^MATCH}`, `@-`/`@+`, `%+`, …) like `=~`.
762    Regex { pattern: String, flags: String },
763    /// Arbitrary expression compared for equality / smart-match against the subject.
764    Value(Box<Expr>),
765    /// `[1, 2, *]` — prefix elements match; optional `*` matches any tail (must be last).
766    Array(Vec<MatchArrayElem>),
767    /// `{ name => $n, ... }` — required keys; `$n` binds the value for the arm body.
768    Hash(Vec<MatchHashPair>),
769    /// `Some($x)` — matches array-like values with **at least two** elements where index `1` is
770    /// Perl-truthy (stryke: `$gen->next` yields `[value, more]` with `more` truthy while iterating).
771    OptionSome(String),
772}
773/// `MatchArrayElem` — see variants.
774#[derive(Debug, Clone, Serialize, Deserialize)]
775pub enum MatchArrayElem {
776    /// `Expr` variant.
777    Expr(Expr),
778    /// `$name` at the top of a pattern element — bind this position to a new lexical `$name`.
779    /// Use `[($x)]` if you need smartmatch against the current value of `$x` instead.
780    CaptureScalar(String),
781    /// Rest-of-array wildcard (only valid as the last element).
782    Rest,
783    /// `@name` — bind remaining elements as a new array to `@name` (only valid as the last element).
784    RestBind(String),
785}
786/// `MatchHashPair` — see variants.
787#[derive(Debug, Clone, Serialize, Deserialize)]
788pub enum MatchHashPair {
789    /// `key => _` — key must exist.
790    KeyOnly { key: Expr },
791    /// `key => $name` — key must exist; value is bound to `$name` in the arm.
792    Capture { key: Expr, name: String },
793}
794/// `MagicConstKind` — see variants.
795#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
796pub enum MagicConstKind {
797    /// Current source path (`$0`-style script name or `-e`).
798    File,
799    /// Line number of this token (1-based, same as lexer).
800    Line,
801    /// Reference to currently executing subroutine (for anonymous recursion).
802    Sub,
803}
804/// `Expr` — see fields for layout.
805#[derive(Debug, Clone, Serialize, Deserialize)]
806pub struct Expr {
807    /// `kind` field.
808    pub kind: ExprKind,
809    /// `line` field.
810    pub line: usize,
811}
812/// `ExprKind` — see variants.
813#[derive(Debug, Clone, Serialize, Deserialize)]
814pub enum ExprKind {
815    // Literals
816    /// `Integer` variant.
817    Integer(i64),
818    /// `Float` variant.
819    Float(f64),
820    /// `String` variant.
821    String(String),
822    /// Unquoted identifier used as an expression term (`if (FOO)`), distinct from quoted `'FOO'` / `"FOO"`.
823    /// Resolved at runtime: nullary subroutine if defined, otherwise stringifies like Perl barewords.
824    Bareword(String),
825    /// `Regex` variant.
826    Regex(String, String),
827    /// `QW` variant.
828    QW(Vec<String>),
829    /// `Undef` variant.
830    Undef,
831    /// `__FILE__` / `__LINE__` (Perl compile-time literals).
832    MagicConst(MagicConstKind),
833
834    // Interpolated string (mix of literal and variable parts)
835    /// `InterpolatedString` variant.
836    InterpolatedString(Vec<StringPart>),
837
838    // Variables
839    /// `ScalarVar` variant.
840    ScalarVar(String),
841    /// `ArrayVar` variant.
842    ArrayVar(String),
843    /// `HashVar` variant.
844    HashVar(String),
845    /// `ArrayElement` variant.
846    ArrayElement {
847        array: String,
848        index: Box<Expr>,
849    },
850    /// `HashElement` variant.
851    HashElement {
852        hash: String,
853        key: Box<Expr>,
854    },
855    /// `ArraySlice` variant.
856    ArraySlice {
857        array: String,
858        indices: Vec<Expr>,
859    },
860    /// `HashSlice` variant.
861    HashSlice {
862        hash: String,
863        keys: Vec<Expr>,
864    },
865    /// `%h{KEYS}` — Perl 5.20+ key-value slice: returns a flat list of
866    /// (key, value, key, value, ...) pairs instead of just values. (BUG-008)
867    HashKvSlice {
868        hash: String,
869        keys: Vec<Expr>,
870    },
871    /// `@$container{keys}` — hash slice when the hash is reached via a scalar ref (Perl `@$href{k1,k2}`).
872    HashSliceDeref {
873        container: Box<Expr>,
874        keys: Vec<Expr>,
875    },
876    /// `(LIST)[i,...]` / `(sort ...)[0]` — subscript after a non-arrow container (not `$a[i]` / `$r->[i]`).
877    AnonymousListSlice {
878        source: Box<Expr>,
879        indices: Vec<Expr>,
880    },
881
882    // References
883    /// `ScalarRef` variant.
884    ScalarRef(Box<Expr>),
885    /// `ArrayRef` variant.
886    ArrayRef(Vec<Expr>),
887    HashRef(Vec<(Expr, Expr)>),
888    /// `CodeRef` variant.
889    CodeRef {
890        params: Vec<SubSigParam>,
891        body: Block,
892    },
893    /// Unary `&name` — invoke subroutine `name` (Perl `&foo` / `&Foo::bar`).
894    SubroutineRef(String),
895    /// `\&name` — coderef to an existing named subroutine (Perl `\&foo`).
896    SubroutineCodeRef(String),
897    /// `\&{ EXPR }` — coderef to a subroutine whose name is given by `EXPR` (string or expression).
898    DynamicSubCodeRef(Box<Expr>),
899    /// `Deref` variant.
900    Deref {
901        expr: Box<Expr>,
902        kind: Sigil,
903    },
904    /// `ArrowDeref` variant.
905    ArrowDeref {
906        expr: Box<Expr>,
907        index: Box<Expr>,
908        kind: DerefKind,
909    },
910
911    // Operators
912    /// `BinOp` variant.
913    BinOp {
914        left: Box<Expr>,
915        op: BinOp,
916        right: Box<Expr>,
917    },
918    /// `UnaryOp` variant.
919    UnaryOp {
920        op: UnaryOp,
921        expr: Box<Expr>,
922    },
923    /// `PostfixOp` variant.
924    PostfixOp {
925        expr: Box<Expr>,
926        op: PostfixOp,
927    },
928    /// `Assign` variant.
929    Assign {
930        target: Box<Expr>,
931        value: Box<Expr>,
932    },
933    /// `CompoundAssign` variant.
934    CompoundAssign {
935        target: Box<Expr>,
936        op: BinOp,
937        value: Box<Expr>,
938    },
939    /// `Ternary` variant.
940    Ternary {
941        condition: Box<Expr>,
942        then_expr: Box<Expr>,
943        else_expr: Box<Expr>,
944    },
945
946    // Repetition operator `EXPR x N`.
947    //
948    // Perl distinguishes scalar string repetition (`"ab" x 3` → `"ababab"`) from
949    // list repetition (`(0) x 3` → `(0,0,0)`, `qw(a b) x 2` → `(a,b,a,b)`). The
950    // discriminator at parse time is the LHS shape: a top-level paren-list (or
951    // `qw(...)`) immediately before `x` is list-repeat; everything else is
952    // scalar-repeat. The parser sets `list_repeat=true` only in that case;
953    // `f(args) x N` (function-call parens, not list parens) stays scalar.
954    /// `Repeat` variant.
955    Repeat {
956        expr: Box<Expr>,
957        count: Box<Expr>,
958        list_repeat: bool,
959    },
960
961    // Range: `1..10` / `1...10` — in scalar context, `...` is the exclusive flip-flop (Perl `sed`-style).
962    // With step: `1..100:2` (1,3,5,...,99) or `100..1:-1` (100,99,...,1).
963    /// `Range` variant.
964    Range {
965        from: Box<Expr>,
966        to: Box<Expr>,
967        #[serde(default)]
968        exclusive: bool,
969        #[serde(default)]
970        step: Option<Box<Expr>>,
971    },
972
973    /// Slice subscript range with optional endpoints — Python-style `[start:stop:step]`.
974    /// Only emitted by the parser inside `@arr[...]` / `@h{...}` (and arrow-deref forms).
975    /// Open-ended forms: `[::-1]` (reverse), `[:N]`, `[N:]`, `[::M]`, `[N::M]`.
976    /// Compiler dispatches to typed integer-strict (array) or stringify-all (hash) ops.
977    SliceRange {
978        #[serde(default)]
979        from: Option<Box<Expr>>,
980        #[serde(default)]
981        to: Option<Box<Expr>>,
982        #[serde(default)]
983        step: Option<Box<Expr>>,
984    },
985
986    /// `my $x = EXPR` (or `our` / `state` / `local`) used as an *expression* —
987    /// e.g. inside `if (my $line = readline)` / `while (my $x = next())`.
988    /// Evaluation: declare each var in the current scope, evaluate the initializer
989    /// (or default to `undef`), then return the assigned value(s).
990    /// Distinct from `StmtKind::My` which only appears at statement level.
991    ///
992    /// `var $x = EXPR` and `val $x = EXPR` (Kotlin/Scala-style aliases for
993    /// `my` and `const my`) reach this node via parser-level normalization
994    /// — `parse_primary` rewrites `raw_kw` to `"my"` and threads the
995    /// `mark_frozen` bit through `VarDecl::frozen` on each decl. The
996    /// `keyword` field below therefore only ever holds the post-normalized
997    /// spelling; `"var"`/`"val"` do not appear at the AST layer.
998    MyExpr {
999        keyword: String, // "my" / "our" / "state" / "local" (post-normalization; `var`/`val` collapse into `"my"`)
1000        decls: Vec<VarDecl>,
1001    },
1002
1003    // Function call
1004    /// `FuncCall` variant.
1005    FuncCall {
1006        name: String,
1007        args: Vec<Expr>,
1008    },
1009
1010    // Method call: $obj->method(args) or $obj->SUPER::method(args)
1011    /// `MethodCall` variant.
1012    MethodCall {
1013        object: Box<Expr>,
1014        method: String,
1015        args: Vec<Expr>,
1016        /// When true, dispatch starts after the caller package in the linearized MRO.
1017        #[serde(default)]
1018        super_call: bool,
1019    },
1020    /// Call through a coderef or invokable scalar: `$cr->(...)` is [`MethodCall`]; this is
1021    /// `$coderef(...)` or `&$coderef(...)` (the latter sets `ampersand`).
1022    IndirectCall {
1023        target: Box<Expr>,
1024        args: Vec<Expr>,
1025        #[serde(default)]
1026        ampersand: bool,
1027        /// True for unary `&$cr` with no `(...)` — Perl passes the caller's `@_` to the invoked sub.
1028        #[serde(default)]
1029        pass_caller_arglist: bool,
1030    },
1031    /// Limited typeglob: `*FOO` → handle name `FOO` for `open` / I/O.
1032    Typeglob(String),
1033    /// `*{ EXPR }` — typeglob slot by dynamic name (e.g. `*{$pkg . '::import'}`).
1034    TypeglobExpr(Box<Expr>),
1035
1036    // Special forms
1037    /// `Print` variant.
1038    Print {
1039        handle: Option<String>,
1040        args: Vec<Expr>,
1041    },
1042    /// `Say` variant.
1043    Say {
1044        handle: Option<String>,
1045        args: Vec<Expr>,
1046    },
1047    /// `Printf` variant.
1048    Printf {
1049        handle: Option<String>,
1050        args: Vec<Expr>,
1051    },
1052    /// `Die` variant.
1053    Die(Vec<Expr>),
1054    /// `Warn` variant.
1055    Warn(Vec<Expr>),
1056
1057    // Regex operations
1058    /// `Match` variant.
1059    Match {
1060        expr: Box<Expr>,
1061        pattern: String,
1062        flags: String,
1063        /// When true, `/g` uses Perl scalar semantics (one match per eval, updates `pos`).
1064        scalar_g: bool,
1065        #[serde(default = "default_delim")]
1066        delim: char,
1067    },
1068    /// `Substitution` variant.
1069    Substitution {
1070        expr: Box<Expr>,
1071        pattern: String,
1072        replacement: String,
1073        flags: String,
1074        #[serde(default = "default_delim")]
1075        delim: char,
1076    },
1077    /// `Transliterate` variant.
1078    Transliterate {
1079        expr: Box<Expr>,
1080        from: String,
1081        to: String,
1082        flags: String,
1083        #[serde(default = "default_delim")]
1084        delim: char,
1085    },
1086
1087    // List operations
1088    /// `MapExpr` variant.
1089    MapExpr {
1090        block: Block,
1091        list: Box<Expr>,
1092        /// `flat_map { }` — peel one ARRAY ref from each iteration (stryke extension).
1093        flatten_array_refs: bool,
1094        /// `maps` / `flat_maps` — lazy iterator output (stryke); `map` / `flat_map` use `false`.
1095        #[serde(default)]
1096        stream: bool,
1097    },
1098    /// `map EXPR, LIST` — EXPR is evaluated in list context with `$_` set to each element.
1099    MapExprComma {
1100        expr: Box<Expr>,
1101        list: Box<Expr>,
1102        flatten_array_refs: bool,
1103        #[serde(default)]
1104        stream: bool,
1105    },
1106    /// `GrepExpr` variant.
1107    GrepExpr {
1108        block: Block,
1109        list: Box<Expr>,
1110        #[serde(default)]
1111        keyword: GrepBuiltinKeyword,
1112    },
1113    /// `grep EXPR, LIST` — EXPR is evaluated with `$_` set to each element (Perl list vs scalar context).
1114    GrepExprComma {
1115        expr: Box<Expr>,
1116        list: Box<Expr>,
1117        #[serde(default)]
1118        keyword: GrepBuiltinKeyword,
1119    },
1120    /// `sort BLOCK LIST`, `sort SUB LIST`, or `sort $coderef LIST` (Perl uses `$a`/`$b` in the comparator).
1121    SortExpr {
1122        cmp: Option<SortComparator>,
1123        list: Box<Expr>,
1124    },
1125    /// `ReverseExpr` variant.
1126    ReverseExpr(Box<Expr>),
1127    /// `rev EXPR` — always string-reverse (scalar reverse), stryke extension.
1128    Rev(Box<Expr>),
1129    /// `JoinExpr` variant.
1130    JoinExpr {
1131        separator: Box<Expr>,
1132        list: Box<Expr>,
1133    },
1134    /// `SplitExpr` variant.
1135    SplitExpr {
1136        pattern: Box<Expr>,
1137        string: Box<Expr>,
1138        limit: Option<Box<Expr>>,
1139    },
1140    /// `each { BLOCK } @list` — execute BLOCK for each element
1141    /// with `$_` aliased; void context (returns count in scalar context).
1142    ForEachExpr {
1143        block: Block,
1144        list: Box<Expr>,
1145    },
1146
1147    // Parallel extensions
1148    /// `PMapExpr` variant.
1149    PMapExpr {
1150        block: Block,
1151        list: Box<Expr>,
1152        /// `pmap { } @list, progress => EXPR` — when truthy, print a progress bar on stderr.
1153        progress: Option<Box<Expr>>,
1154        /// `pflat_map { }` — flatten each block result like [`ExprKind::MapExpr`] (arrays expand);
1155        /// parallel output is stitched in **input order** (unlike plain `pmap`, which is unordered).
1156        flat_outputs: bool,
1157        /// `pmap_on $cluster { } @list` — fan out over SSH (`stryke --remote-worker`); `None` = local rayon.
1158        #[serde(default, skip_serializing_if = "Option::is_none")]
1159        on_cluster: Option<Box<Expr>>,
1160        /// `pmaps` / `pflat_maps` — streaming variant: returns a lazy iterator that processes
1161        /// chunks in parallel via rayon instead of eagerly collecting all results.
1162        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1163        stream: bool,
1164    },
1165    /// `pmap_chunked N { BLOCK } @list [, progress => EXPR]` — parallel map in batches of N.
1166    PMapChunkedExpr {
1167        chunk_size: Box<Expr>,
1168        block: Block,
1169        list: Box<Expr>,
1170        progress: Option<Box<Expr>>,
1171    },
1172    /// `PGrepExpr` variant.
1173    PGrepExpr {
1174        block: Block,
1175        list: Box<Expr>,
1176        /// `pgrep { } @list, progress => EXPR` — stderr progress bar when truthy.
1177        progress: Option<Box<Expr>>,
1178        /// `pgreps` — streaming variant: returns a lazy iterator.
1179        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1180        stream: bool,
1181    },
1182    /// `pfor { BLOCK } @list [, progress => EXPR]` — stderr progress bar when truthy.
1183    PForExpr {
1184        block: Block,
1185        list: Box<Expr>,
1186        progress: Option<Box<Expr>>,
1187    },
1188    /// `par { BLOCK } INPUT` — generic parallel-chunk wrapper. Splits INPUT
1189    /// (string → UTF-8-aligned byte chunks; array/list → element-chunks)
1190    /// into N pieces (N = available rayon threads), evaluates BLOCK per
1191    /// chunk in parallel with `$_` bound to the chunk, then concatenates
1192    /// results. Lets any whole-input op (`letters`, `chars`, `uc`, `freq`,
1193    /// regex `//g`, etc.) parallelize without needing a `pX` variant.
1194    ParExpr {
1195        block: Block,
1196        list: Box<Expr>,
1197    },
1198    /// `par_reduce { extract } [ { merge } ] INPUT` — chunk-extract-merge.
1199    /// Same chunker as `par {}`, but each chunk's result is reduced
1200    /// pairwise across chunks instead of concatenated.
1201    ///
1202    /// - One block: auto-merger picks based on result type (number → `+`,
1203    ///   `hash<num>` → key-wise `+`, array → concat, string → concat).
1204    /// - Two blocks: explicit pairwise reducer with `$a`/`$b`.
1205    ParReduceExpr {
1206        extract_block: Block,
1207        reduce_block: Option<Block>,
1208        list: Box<Expr>,
1209    },
1210    /// Distributed counterpart of [`ExprKind::ParReduceExpr`]. Same chunk-block
1211    /// semantics (stages operate on `@_`) but chunks ship to a `RemoteCluster`
1212    /// of SSH workers via the existing `cluster::run_cluster` dispatcher.
1213    /// Built by `~d> on $cluster SOURCE stage1 stage2 ...`.
1214    DistReduceExpr {
1215        cluster: Box<Expr>,
1216        extract_block: Block,
1217        list: Box<Expr>,
1218    },
1219    /// `par_lines PATH, fn { ... } [, progress => EXPR]` — optional stderr progress (per line).
1220    ParLinesExpr {
1221        path: Box<Expr>,
1222        callback: Box<Expr>,
1223        progress: Option<Box<Expr>>,
1224    },
1225    /// `par_walk PATH, fn { ... } [, progress => EXPR]` — parallel recursive directory walk; `$_` is each path.
1226    ParWalkExpr {
1227        path: Box<Expr>,
1228        callback: Box<Expr>,
1229        progress: Option<Box<Expr>>,
1230    },
1231    /// `pwatch GLOB, fn { ... }` — notify-based watcher (evaluated by interpreter).
1232    PwatchExpr {
1233        path: Box<Expr>,
1234        callback: Box<Expr>,
1235    },
1236    /// `psort { } @list [, progress => EXPR]` — stderr progress when truthy (start/end phases).
1237    PSortExpr {
1238        cmp: Option<Block>,
1239        list: Box<Expr>,
1240        progress: Option<Box<Expr>>,
1241    },
1242    /// `reduce { $a + $b } @list` — sequential left fold over the list.
1243    /// `$a` is the accumulator; `$b` is the next list element.
1244    ReduceExpr {
1245        block: Block,
1246        list: Box<Expr>,
1247    },
1248    /// `preduce { $a + $b } @list` — parallel fold/reduce using rayon.
1249    /// $a and $b are set to the accumulator and current element.
1250    PReduceExpr {
1251        block: Block,
1252        list: Box<Expr>,
1253        /// `preduce { } @list, progress => EXPR` — stderr progress bar when truthy.
1254        progress: Option<Box<Expr>>,
1255    },
1256    /// `preduce_init EXPR, { $a / $b } @list` — parallel fold with explicit identity.
1257    /// Each chunk starts from a clone of `EXPR`; partials are merged (hash maps add counts per key;
1258    /// other types use the same block with `$a` / `$b` as partial accumulators). `$a` is the
1259    /// accumulator, `$b` is the next list element; `@_` is `($a, $b)` for `my ($acc, $item) = @_`.
1260    PReduceInitExpr {
1261        init: Box<Expr>,
1262        block: Block,
1263        list: Box<Expr>,
1264        progress: Option<Box<Expr>>,
1265    },
1266    /// `pmap_reduce { map } { reduce } @list` — fused parallel map + tree reduce (no full mapped array).
1267    PMapReduceExpr {
1268        map_block: Block,
1269        reduce_block: Block,
1270        list: Box<Expr>,
1271        progress: Option<Box<Expr>>,
1272    },
1273    /// `pcache { BLOCK } @list [, progress => EXPR]` — stderr progress bar when truthy.
1274    PcacheExpr {
1275        block: Block,
1276        list: Box<Expr>,
1277        progress: Option<Box<Expr>>,
1278    },
1279    /// `pselect($rx1, $rx2, ...)` — optional `timeout => SECS` for bounded wait.
1280    PselectExpr {
1281        receivers: Vec<Expr>,
1282        timeout: Option<Box<Expr>>,
1283    },
1284    /// `fan [COUNT] { BLOCK }` — execute BLOCK COUNT times in parallel (default COUNT = rayon pool size).
1285    /// `fan_cap [COUNT] { BLOCK }` — same, but return value is a **list** of each block's return value (index order).
1286    /// `$_` is set to the iteration index (0..COUNT-1).
1287    /// Optional `, progress => EXPR` — stderr progress bar (like `pmap`).
1288    FanExpr {
1289        count: Option<Box<Expr>>,
1290        block: Block,
1291        progress: Option<Box<Expr>>,
1292        capture: bool,
1293    },
1294
1295    /// `async { BLOCK }` — run BLOCK on a worker thread; returns a task handle.
1296    AsyncBlock {
1297        body: Block,
1298    },
1299    /// `spawn { BLOCK }` — same as [`ExprKind::AsyncBlock`] (Rust `thread::spawn`–style naming); join with `await`.
1300    SpawnBlock {
1301        body: Block,
1302    },
1303    /// `trace { BLOCK }` — print `mysync` scalar mutations to stderr (for parallel debugging).
1304    Trace {
1305        body: Block,
1306    },
1307    /// `timer { BLOCK }` — run BLOCK and return elapsed wall time in milliseconds (float).
1308    Timer {
1309        body: Block,
1310    },
1311    /// `bench { BLOCK } N` — run BLOCK `N` times (warmup + min/mean/p99 wall time, ms).
1312    Bench {
1313        body: Block,
1314        times: Box<Expr>,
1315    },
1316    /// `spinner "msg" { BLOCK }` — animated spinner on stderr while block runs.
1317    Spinner {
1318        message: Box<Expr>,
1319        body: Block,
1320    },
1321    /// `await EXPR` — join an async task, or return EXPR unchanged.
1322    Await(Box<Expr>),
1323    /// Read entire file as UTF-8 (`slurp $path`).
1324    Slurp(Box<Expr>),
1325    /// `swallow PATTERN` — expand a zsh-style glob and return a hash
1326    /// `{ canonicalized_abspath => raw_bytes }`. Per-file body never decodes,
1327    /// so binary files round-trip cleanly. Hard-fails on non-regular matches
1328    /// the same way `slurp` does; opt out with the `(N)` null-glob qualifier.
1329    Swallow(Box<Expr>),
1330    /// `burp HASH` — inverse of `swallow`. Take a hash `{ path => bytes }`,
1331    /// write each entry to disk (creates parent directories automatically),
1332    /// and return the number of files written. Hard-fails on the first I/O
1333    /// error. Accepts plain hashes and hash refs; values may be bytes or any
1334    /// scalar that stringifies (matches `spew`/`spurt` conventions).
1335    Burp(Box<Expr>),
1336    /// `god EXPR` — omniscient runtime introspection. Returns a structured
1337    /// multi-line dump showing the type tag, heap pointer, Arc strong/weak
1338    /// counts, byte hex previews, generator/pipeline state, and closure
1339    /// captures. Cycle-safe via per-pointer recursion tracking. Sibling to
1340    /// `pp` (human-friendly) and `ddump` (deep structure).
1341    God(Box<Expr>),
1342    /// `ingest PATTERN` — streaming variant of `swallow`: returns a lazy
1343    /// iterator yielding `[canonicalized_abspath, raw_bytes]` per file. Only
1344    /// one file's bytes are resident at a time. Path list and stat/canonicalize
1345    /// are eager (full zsh qualifier support); file reads are lazy. Hard-fails
1346    /// on non-regular matches up-front, matching `slurp`/`swallow` policy.
1347    Ingest(Box<Expr>),
1348    /// Run shell command and return structured output (`capture "cmd"`).
1349    Capture(Box<Expr>),
1350    /// `` `cmd` `` / `qx{cmd}` — run via `sh -c`, return **stdout as a string** (Perl); updates `$?`.
1351    Qx(Box<Expr>),
1352    /// Blocking HTTP GET (`fetch_url $url`).
1353    FetchUrl(Box<Expr>),
1354
1355    /// `pchannel()` — unbounded; `pchannel(N)` — bounded capacity N.
1356    Pchannel {
1357        capacity: Option<Box<Expr>>,
1358    },
1359
1360    // Array/Hash operations
1361    /// `Push` variant.
1362    Push {
1363        array: Box<Expr>,
1364        values: Vec<Expr>,
1365    },
1366    /// `Pop` variant.
1367    Pop(Box<Expr>),
1368    /// `Shift` variant.
1369    Shift(Box<Expr>),
1370    /// `Unshift` variant.
1371    Unshift {
1372        array: Box<Expr>,
1373        values: Vec<Expr>,
1374    },
1375    /// `Splice` variant.
1376    Splice {
1377        array: Box<Expr>,
1378        offset: Option<Box<Expr>>,
1379        length: Option<Box<Expr>>,
1380        replacement: Vec<Expr>,
1381    },
1382    /// `Delete` variant.
1383    Delete(Box<Expr>),
1384    /// `Exists` variant.
1385    Exists(Box<Expr>),
1386    /// `Keys` variant.
1387    Keys(Box<Expr>),
1388    /// `Values` variant.
1389    Values(Box<Expr>),
1390    /// `Each` variant.
1391    Each(Box<Expr>),
1392
1393    // String operations
1394    /// `Chomp` variant.
1395    Chomp(Box<Expr>),
1396    /// `Chop` variant.
1397    Chop(Box<Expr>),
1398    /// `Length` variant.
1399    Length(Box<Expr>),
1400    /// `Substr` variant.
1401    Substr {
1402        string: Box<Expr>,
1403        offset: Box<Expr>,
1404        length: Option<Box<Expr>>,
1405        replacement: Option<Box<Expr>>,
1406    },
1407    /// `Index` variant.
1408    Index {
1409        string: Box<Expr>,
1410        substr: Box<Expr>,
1411        position: Option<Box<Expr>>,
1412    },
1413    /// `Rindex` variant.
1414    Rindex {
1415        string: Box<Expr>,
1416        substr: Box<Expr>,
1417        position: Option<Box<Expr>>,
1418    },
1419    /// `Sprintf` variant.
1420    Sprintf {
1421        format: Box<Expr>,
1422        args: Vec<Expr>,
1423    },
1424
1425    // Numeric
1426    /// `Abs` variant.
1427    Abs(Box<Expr>),
1428    /// `Int` variant.
1429    Int(Box<Expr>),
1430    /// `Sqrt` variant.
1431    Sqrt(Box<Expr>),
1432    /// `Sin` variant.
1433    Sin(Box<Expr>),
1434    /// `Cos` variant.
1435    Cos(Box<Expr>),
1436    /// `Atan2` variant.
1437    Atan2 {
1438        y: Box<Expr>,
1439        x: Box<Expr>,
1440    },
1441    /// `Exp` variant.
1442    Exp(Box<Expr>),
1443    /// `Log` variant.
1444    Log(Box<Expr>),
1445    /// `rand` with optional upper bound (none = Perl default 1.0).
1446    Rand(Option<Box<Expr>>),
1447    /// `srand` with optional seed (none = time-based).
1448    Srand(Option<Box<Expr>>),
1449    /// `Hex` variant.
1450    Hex(Box<Expr>),
1451    /// `Oct` variant.
1452    Oct(Box<Expr>),
1453
1454    // Case
1455    /// `Lc` variant.
1456    Lc(Box<Expr>),
1457    /// `Uc` variant.
1458    Uc(Box<Expr>),
1459    /// `Lcfirst` variant.
1460    Lcfirst(Box<Expr>),
1461    /// `Ucfirst` variant.
1462    Ucfirst(Box<Expr>),
1463
1464    /// Unicode case fold (Perl `fc`).
1465    Fc(Box<Expr>),
1466    /// Regex-escape a string (Perl `quotemeta`, aliased `qm`). Lowers to
1467    /// `Op::CallBuiltin(BuiltinId::Quotemeta, 1)` for JIT lowering.
1468    Quotemeta(Box<Expr>),
1469    /// DES-style `crypt` (see libc `crypt(3)` on Unix; empty on other targets).
1470    Crypt {
1471        plaintext: Box<Expr>,
1472        salt: Box<Expr>,
1473    },
1474    /// `pos` — optional scalar lvalue target (`None` = `$_`).
1475    Pos(Option<Box<Expr>>),
1476    /// `study` — hint for repeated matching; returns byte length of the string.
1477    Study(Box<Expr>),
1478
1479    // Type
1480    /// `Defined` variant.
1481    Defined(Box<Expr>),
1482    /// `Ref` variant.
1483    Ref(Box<Expr>),
1484    /// `ScalarContext` variant.
1485    ScalarContext(Box<Expr>),
1486
1487    // Char
1488    /// `Chr` variant.
1489    Chr(Box<Expr>),
1490    /// `Ord` variant.
1491    Ord(Box<Expr>),
1492
1493    // I/O
1494    /// `open my $fh` — only valid as [`ExprKind::Open::handle`]; declares `$fh` and binds the handle.
1495    OpenMyHandle {
1496        name: String,
1497    },
1498    /// `Open` variant.
1499    Open {
1500        handle: Box<Expr>,
1501        mode: Box<Expr>,
1502        file: Option<Box<Expr>>,
1503    },
1504    /// `Close` variant.
1505    Close(Box<Expr>),
1506    /// `ReadLine` variant.
1507    ReadLine(Option<String>),
1508    /// `Eof` variant.
1509    Eof(Option<Box<Expr>>),
1510    /// `opendir my $dh` — only valid as [`ExprKind::Opendir::handle`]; declares `$dh` and binds the handle.
1511    OpendirMyHandle {
1512        name: String,
1513    },
1514    /// `Opendir` variant.
1515    Opendir {
1516        handle: Box<Expr>,
1517        path: Box<Expr>,
1518    },
1519    /// `Readdir` variant.
1520    Readdir(Box<Expr>),
1521    /// `Closedir` variant.
1522    Closedir(Box<Expr>),
1523    /// `Rewinddir` variant.
1524    Rewinddir(Box<Expr>),
1525    /// `Telldir` variant.
1526    Telldir(Box<Expr>),
1527    /// `Seekdir` variant.
1528    Seekdir {
1529        handle: Box<Expr>,
1530        position: Box<Expr>,
1531    },
1532
1533    // File tests
1534    /// `FileTest` variant.
1535    FileTest {
1536        op: char,
1537        expr: Box<Expr>,
1538    },
1539
1540    // System
1541    /// `System` variant.
1542    System(Vec<Expr>),
1543    /// `Exec` variant.
1544    Exec(Vec<Expr>),
1545    /// `Eval` variant.
1546    Eval(Box<Expr>),
1547    /// `Do` variant.
1548    Do(Box<Expr>),
1549    /// `Require` variant.
1550    Require(Box<Expr>),
1551    /// `Exit` variant.
1552    Exit(Option<Box<Expr>>),
1553    /// `Chdir` variant.
1554    Chdir(Box<Expr>),
1555    /// `Mkdir` variant.
1556    Mkdir {
1557        path: Box<Expr>,
1558        mode: Option<Box<Expr>>,
1559    },
1560    /// `Unlink` variant.
1561    Unlink(Vec<Expr>),
1562    /// `Rename` variant.
1563    Rename {
1564        old: Box<Expr>,
1565        new: Box<Expr>,
1566    },
1567    /// `chmod MODE, @files` — first expr is mode, rest are paths.
1568    Chmod(Vec<Expr>),
1569    /// `chown UID, GID, @files` — first two are uid/gid, rest are paths.
1570    Chown(Vec<Expr>),
1571    /// `Stat` variant.
1572    Stat(Box<Expr>),
1573    /// `Lstat` variant.
1574    Lstat(Box<Expr>),
1575    /// `Link` variant.
1576    Link {
1577        old: Box<Expr>,
1578        new: Box<Expr>,
1579    },
1580    /// `Symlink` variant.
1581    Symlink {
1582        old: Box<Expr>,
1583        new: Box<Expr>,
1584    },
1585    /// `Readlink` variant.
1586    Readlink(Box<Expr>),
1587    /// `files` / `files DIR` — list file names in a directory (default: `.`).
1588    Files(Vec<Expr>),
1589    /// `filesf` / `filesf DIR` / `f` — list only regular file names in a directory (default: `.`).
1590    Filesf(Vec<Expr>),
1591    /// `fr DIR` — list only regular file names recursively (default: `.`).
1592    FilesfRecursive(Vec<Expr>),
1593    /// `dirs` / `dirs DIR` / `d` — list subdirectory names in a directory (default: `.`).
1594    Dirs(Vec<Expr>),
1595    /// `dr DIR` — list subdirectory paths recursively (default: `.`).
1596    DirsRecursive(Vec<Expr>),
1597    /// `sym_links` / `sym_links DIR` — list symlink names in a directory (default: `.`).
1598    SymLinks(Vec<Expr>),
1599    /// `sockets` / `sockets DIR` — list Unix socket names in a directory (default: `.`).
1600    Sockets(Vec<Expr>),
1601    /// `pipes` / `pipes DIR` — list named-pipe (FIFO) names in a directory (default: `.`).
1602    Pipes(Vec<Expr>),
1603    /// `block_devices` / `block_devices DIR` — list block device names in a directory (default: `.`).
1604    BlockDevices(Vec<Expr>),
1605    /// `char_devices` / `char_devices DIR` — list character device names in a directory (default: `.`).
1606    CharDevices(Vec<Expr>),
1607    /// `exe` / `exe DIR` — list executable file names in a directory (default: `.`).
1608    Executables(Vec<Expr>),
1609    /// `Glob` variant.
1610    Glob(Vec<Expr>),
1611    /// Parallel recursive glob (rayon); same patterns as `glob`, different walk strategy.
1612    /// Optional `, progress => EXPR` — stderr progress bar (one tick per pattern).
1613    GlobPar {
1614        args: Vec<Expr>,
1615        progress: Option<Box<Expr>>,
1616    },
1617    /// `par_sed PATTERN, REPLACEMENT, FILES... [, progress => EXPR]` — parallel in-place regex replace per file (`g` semantics).
1618    ParSed {
1619        args: Vec<Expr>,
1620        progress: Option<Box<Expr>>,
1621    },
1622
1623    // Bless
1624    /// `Bless` variant.
1625    Bless {
1626        ref_expr: Box<Expr>,
1627        class: Option<Box<Expr>>,
1628    },
1629
1630    // Caller
1631    /// `Caller` variant.
1632    Caller(Option<Box<Expr>>),
1633
1634    // Wantarray
1635    /// `Wantarray` variant.
1636    Wantarray,
1637
1638    // List / Context
1639    /// `List` variant.
1640    List(Vec<Expr>),
1641
1642    // Postfix if/unless/while/until/for
1643    /// `PostfixIf` variant.
1644    PostfixIf {
1645        expr: Box<Expr>,
1646        condition: Box<Expr>,
1647    },
1648    /// `PostfixUnless` variant.
1649    PostfixUnless {
1650        expr: Box<Expr>,
1651        condition: Box<Expr>,
1652    },
1653    /// `PostfixWhile` variant.
1654    PostfixWhile {
1655        expr: Box<Expr>,
1656        condition: Box<Expr>,
1657    },
1658    /// `PostfixUntil` variant.
1659    PostfixUntil {
1660        expr: Box<Expr>,
1661        condition: Box<Expr>,
1662    },
1663    /// `PostfixForeach` variant.
1664    PostfixForeach {
1665        expr: Box<Expr>,
1666        list: Box<Expr>,
1667    },
1668
1669    /// `retry { BLOCK } times => N [, backoff => linear|exponential|none]` — re-run block until success or attempts exhausted.
1670    RetryBlock {
1671        body: Block,
1672        times: Box<Expr>,
1673        backoff: RetryBackoff,
1674    },
1675    /// `rate_limit(MAX, WINDOW) { BLOCK }` — sliding window: at most MAX runs per WINDOW (e.g. `"1s"`).
1676    /// `slot` is assigned at parse time for per-site state in the interpreter.
1677    RateLimitBlock {
1678        slot: u32,
1679        max: Box<Expr>,
1680        window: Box<Expr>,
1681        body: Block,
1682    },
1683    /// `every(INTERVAL) { BLOCK }` — repeat BLOCK forever with sleep (INTERVAL like `"5s"` or seconds).
1684    EveryBlock {
1685        interval: Box<Expr>,
1686        body: Block,
1687    },
1688    /// `gen { ... yield ... }` — lazy generator; call `->next` for each value.
1689    GenBlock {
1690        body: Block,
1691    },
1692    /// `yield EXPR` — only valid inside `gen { }` (and propagates through control flow).
1693    Yield(Box<Expr>),
1694
1695    /// `match (EXPR) { PATTERN => EXPR, ... }` — first matching arm; bindings scoped to the arm body.
1696    AlgebraicMatch {
1697        subject: Box<Expr>,
1698        arms: Vec<MatchArm>,
1699    },
1700}
1701/// `StringPart` — see variants.
1702#[derive(Debug, Clone, Serialize, Deserialize)]
1703pub enum StringPart {
1704    /// `Literal` variant.
1705    Literal(String),
1706    /// `ScalarVar` variant.
1707    ScalarVar(String),
1708    /// `ArrayVar` variant.
1709    ArrayVar(String),
1710    /// `Expr` variant.
1711    Expr(Expr),
1712}
1713/// `DerefKind` — see variants.
1714#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1715pub enum DerefKind {
1716    /// `Array` variant.
1717    Array,
1718    /// `Hash` variant.
1719    Hash,
1720    /// `Call` variant.
1721    Call,
1722}
1723/// `BinOp` — see variants.
1724#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1725pub enum BinOp {
1726    /// `Add` variant.
1727    Add,
1728    /// `Sub` variant.
1729    Sub,
1730    /// `Mul` variant.
1731    Mul,
1732    /// `Div` variant.
1733    Div,
1734    /// `Mod` variant.
1735    Mod,
1736    /// `Pow` variant.
1737    Pow,
1738    /// `Concat` variant.
1739    Concat,
1740    /// `NumEq` variant.
1741    NumEq,
1742    /// `NumNe` variant.
1743    NumNe,
1744    /// `NumLt` variant.
1745    NumLt,
1746    /// `NumGt` variant.
1747    NumGt,
1748    /// `NumLe` variant.
1749    NumLe,
1750    /// `NumGe` variant.
1751    NumGe,
1752    /// `Spaceship` variant.
1753    Spaceship,
1754    /// `StrEq` variant.
1755    StrEq,
1756    /// `StrNe` variant.
1757    StrNe,
1758    /// `StrLt` variant.
1759    StrLt,
1760    /// `StrGt` variant.
1761    StrGt,
1762    /// `StrLe` variant.
1763    StrLe,
1764    /// `StrGe` variant.
1765    StrGe,
1766    /// `StrCmp` variant.
1767    StrCmp,
1768    /// `LogAnd` variant.
1769    LogAnd,
1770    /// `LogOr` variant.
1771    LogOr,
1772    /// `DefinedOr` variant.
1773    DefinedOr,
1774    /// `BitAnd` variant.
1775    BitAnd,
1776    /// `BitOr` variant.
1777    BitOr,
1778    /// `BitXor` variant.
1779    BitXor,
1780    /// `ShiftLeft` variant.
1781    ShiftLeft,
1782    /// `ShiftRight` variant.
1783    ShiftRight,
1784    /// `LogAndWord` variant.
1785    LogAndWord,
1786    /// `LogOrWord` variant.
1787    LogOrWord,
1788    /// `BindMatch` variant.
1789    BindMatch,
1790    /// `BindNotMatch` variant.
1791    BindNotMatch,
1792}
1793/// `UnaryOp` — see variants.
1794#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1795pub enum UnaryOp {
1796    /// `Negate` variant.
1797    Negate,
1798    /// `LogNot` variant.
1799    LogNot,
1800    /// `BitNot` variant.
1801    BitNot,
1802    /// `LogNotWord` variant.
1803    LogNotWord,
1804    /// `PreIncrement` variant.
1805    PreIncrement,
1806    /// `PreDecrement` variant.
1807    PreDecrement,
1808    /// `Ref` variant.
1809    Ref,
1810}
1811/// `PostfixOp` — see variants.
1812#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1813pub enum PostfixOp {
1814    /// `Increment` variant.
1815    Increment,
1816    /// `Decrement` variant.
1817    Decrement,
1818}
1819
1820#[cfg(test)]
1821mod tests {
1822    use super::*;
1823
1824    #[test]
1825    fn binop_deref_kind_distinct() {
1826        assert_ne!(BinOp::Add, BinOp::Sub);
1827        assert_eq!(DerefKind::Call, DerefKind::Call);
1828    }
1829
1830    #[test]
1831    fn sigil_variants_exhaustive_in_tests() {
1832        let all = [Sigil::Scalar, Sigil::Array, Sigil::Hash];
1833        assert_eq!(all.len(), 3);
1834    }
1835
1836    #[test]
1837    fn program_empty_roundtrip_clone() {
1838        let p = Program { statements: vec![] };
1839        assert!(p.clone().statements.is_empty());
1840    }
1841
1842    #[test]
1843    fn program_serializes_to_json() {
1844        let p = crate::parse("1+2;").expect("parse");
1845        let s = serde_json::to_string(&p).expect("json");
1846        assert!(s.contains("\"statements\""));
1847        assert!(s.contains("BinOp"));
1848    }
1849
1850    // ─── GrepBuiltinKeyword ───────────────────────────────────────────
1851
1852    #[test]
1853    fn grep_keyword_as_str_matrix() {
1854        assert_eq!(GrepBuiltinKeyword::Grep.as_str(), "grep");
1855        assert_eq!(GrepBuiltinKeyword::Greps.as_str(), "greps");
1856        assert_eq!(GrepBuiltinKeyword::Filter.as_str(), "filter");
1857        assert_eq!(GrepBuiltinKeyword::FindAll.as_str(), "find_all");
1858    }
1859
1860    #[test]
1861    fn grep_keyword_is_stream_only_false_for_grep() {
1862        // `grep` is the collecting (non-streaming) variant; everything else streams.
1863        assert!(!GrepBuiltinKeyword::Grep.is_stream());
1864        assert!(GrepBuiltinKeyword::Greps.is_stream());
1865        assert!(GrepBuiltinKeyword::Filter.is_stream());
1866        assert!(GrepBuiltinKeyword::FindAll.is_stream());
1867    }
1868
1869    // ─── PerlTypeName byte encoding ───────────────────────────────────
1870
1871    #[test]
1872    fn perl_type_name_byte_roundtrip() {
1873        // 0..7 → simple types → back to same byte.
1874        for b in 0..=7u8 {
1875            let t = PerlTypeName::from_byte(b).unwrap_or_else(|| panic!("byte {b} unknown"));
1876            assert_eq!(t.as_byte(), Some(b), "round-trip failed for byte {b}");
1877        }
1878    }
1879
1880    #[test]
1881    fn perl_type_name_unknown_bytes_return_none() {
1882        assert!(PerlTypeName::from_byte(8).is_none());
1883        assert!(PerlTypeName::from_byte(255).is_none());
1884    }
1885
1886    #[test]
1887    fn perl_type_name_struct_and_enum_have_no_byte_encoding() {
1888        // Named types require name-pool lookup, not byte encoding.
1889        assert_eq!(PerlTypeName::Struct("Point".into()).as_byte(), None);
1890        assert_eq!(PerlTypeName::Enum("Color".into()).as_byte(), None);
1891    }
1892
1893    #[test]
1894    fn perl_type_name_simple_byte_assignments_are_stable() {
1895        // Pin the byte ordering so VM bytecode doesn't shift accidentally.
1896        assert_eq!(PerlTypeName::Int.as_byte(), Some(0));
1897        assert_eq!(PerlTypeName::Str.as_byte(), Some(1));
1898        assert_eq!(PerlTypeName::Float.as_byte(), Some(2));
1899        assert_eq!(PerlTypeName::Bool.as_byte(), Some(3));
1900        assert_eq!(PerlTypeName::Array.as_byte(), Some(4));
1901        assert_eq!(PerlTypeName::Hash.as_byte(), Some(5));
1902        assert_eq!(PerlTypeName::Ref.as_byte(), Some(6));
1903        assert_eq!(PerlTypeName::Any.as_byte(), Some(7));
1904    }
1905
1906    #[test]
1907    fn perl_type_name_display_name_simple_types() {
1908        assert_eq!(PerlTypeName::Int.display_name(), "Int");
1909        assert_eq!(PerlTypeName::Str.display_name(), "Str");
1910        assert_eq!(PerlTypeName::Float.display_name(), "Float");
1911        assert_eq!(PerlTypeName::Bool.display_name(), "Bool");
1912        assert_eq!(PerlTypeName::Array.display_name(), "Array");
1913        assert_eq!(PerlTypeName::Hash.display_name(), "Hash");
1914        assert_eq!(PerlTypeName::Ref.display_name(), "Ref");
1915        assert_eq!(PerlTypeName::Any.display_name(), "Any");
1916    }
1917
1918    #[test]
1919    fn perl_type_name_display_name_named_types() {
1920        assert_eq!(PerlTypeName::Struct("Point".into()).display_name(), "Point");
1921        assert_eq!(PerlTypeName::Enum("Color".into()).display_name(), "Color");
1922    }
1923
1924    // ─── PerlTypeName::check_value runtime type-check ─────────────────
1925
1926    #[test]
1927    fn perl_type_int_accepts_integer_like() {
1928        let v = crate::value::StrykeValue::integer(42);
1929        assert!(PerlTypeName::Int.check_value(&v).is_ok());
1930    }
1931
1932    #[test]
1933    fn perl_type_int_rejects_string() {
1934        let v = crate::value::StrykeValue::string("hi".into());
1935        let err = PerlTypeName::Int.check_value(&v);
1936        assert!(err.is_err());
1937        assert!(err.unwrap_err().contains("Int"));
1938    }
1939
1940    #[test]
1941    fn perl_type_str_accepts_string() {
1942        let v = crate::value::StrykeValue::string("hi".into());
1943        assert!(PerlTypeName::Str.check_value(&v).is_ok());
1944    }
1945
1946    #[test]
1947    fn perl_type_float_accepts_both_int_and_float() {
1948        // Float is permissive — accepts integer-like too (numeric promotion).
1949        assert!(PerlTypeName::Float
1950            .check_value(&crate::value::StrykeValue::integer(7))
1951            .is_ok());
1952        assert!(PerlTypeName::Float
1953            .check_value(&crate::value::StrykeValue::float(3.14))
1954            .is_ok());
1955    }
1956
1957    #[test]
1958    fn perl_type_bool_accepts_anything() {
1959        // Bool's check_value returns Ok(()) for everything (perl truthiness).
1960        assert!(PerlTypeName::Bool
1961            .check_value(&crate::value::StrykeValue::integer(0))
1962            .is_ok());
1963        assert!(PerlTypeName::Bool
1964            .check_value(&crate::value::StrykeValue::string("".into()))
1965            .is_ok());
1966        assert!(PerlTypeName::Bool
1967            .check_value(&crate::value::StrykeValue::UNDEF)
1968            .is_ok());
1969    }
1970
1971    // ─── Statement::new constructor ───────────────────────────────────
1972
1973    #[test]
1974    fn statement_new_preserves_line_and_kind() {
1975        let kind = StmtKind::Expression(Expr {
1976            kind: ExprKind::Integer(42),
1977            line: 7,
1978        });
1979        let s = Statement::new(kind, 7);
1980        assert_eq!(s.line, 7);
1981        // Round-trip the kind via debug formatting since pattern-match would
1982        // require StmtKind to be PartialEq.
1983        assert!(format!("{:?}", s.kind).contains("Expression"));
1984    }
1985}