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