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    ///
981    /// `var $x = EXPR` and `val $x = EXPR` (Kotlin/Scala-style aliases for
982    /// `my` and `const my`) reach this node via parser-level normalization
983    /// — `parse_primary` rewrites `raw_kw` to `"my"` and threads the
984    /// `mark_frozen` bit through `VarDecl::frozen` on each decl. The
985    /// `keyword` field below therefore only ever holds the post-normalized
986    /// spelling; `"var"`/`"val"` do not appear at the AST layer.
987    MyExpr {
988        keyword: String, // "my" / "our" / "state" / "local" (post-normalization; `var`/`val` collapse into `"my"`)
989        decls: Vec<VarDecl>,
990    },
991
992    // Function call
993    /// `FuncCall` variant.
994    FuncCall {
995        name: String,
996        args: Vec<Expr>,
997    },
998
999    // Method call: $obj->method(args) or $obj->SUPER::method(args)
1000    /// `MethodCall` variant.
1001    MethodCall {
1002        object: Box<Expr>,
1003        method: String,
1004        args: Vec<Expr>,
1005        /// When true, dispatch starts after the caller package in the linearized MRO.
1006        #[serde(default)]
1007        super_call: bool,
1008    },
1009    /// Call through a coderef or invokable scalar: `$cr->(...)` is [`MethodCall`]; this is
1010    /// `$coderef(...)` or `&$coderef(...)` (the latter sets `ampersand`).
1011    IndirectCall {
1012        target: Box<Expr>,
1013        args: Vec<Expr>,
1014        #[serde(default)]
1015        ampersand: bool,
1016        /// True for unary `&$cr` with no `(...)` — Perl passes the caller's `@_` to the invoked sub.
1017        #[serde(default)]
1018        pass_caller_arglist: bool,
1019    },
1020    /// Limited typeglob: `*FOO` → handle name `FOO` for `open` / I/O.
1021    Typeglob(String),
1022    /// `*{ EXPR }` — typeglob slot by dynamic name (e.g. `*{$pkg . '::import'}`).
1023    TypeglobExpr(Box<Expr>),
1024
1025    // Special forms
1026    /// `Print` variant.
1027    Print {
1028        handle: Option<String>,
1029        args: Vec<Expr>,
1030    },
1031    /// `Say` variant.
1032    Say {
1033        handle: Option<String>,
1034        args: Vec<Expr>,
1035    },
1036    /// `Printf` variant.
1037    Printf {
1038        handle: Option<String>,
1039        args: Vec<Expr>,
1040    },
1041    /// `Die` variant.
1042    Die(Vec<Expr>),
1043    /// `Warn` variant.
1044    Warn(Vec<Expr>),
1045
1046    // Regex operations
1047    /// `Match` variant.
1048    Match {
1049        expr: Box<Expr>,
1050        pattern: String,
1051        flags: String,
1052        /// When true, `/g` uses Perl scalar semantics (one match per eval, updates `pos`).
1053        scalar_g: bool,
1054        #[serde(default = "default_delim")]
1055        delim: char,
1056    },
1057    /// `Substitution` variant.
1058    Substitution {
1059        expr: Box<Expr>,
1060        pattern: String,
1061        replacement: String,
1062        flags: String,
1063        #[serde(default = "default_delim")]
1064        delim: char,
1065    },
1066    /// `Transliterate` variant.
1067    Transliterate {
1068        expr: Box<Expr>,
1069        from: String,
1070        to: String,
1071        flags: String,
1072        #[serde(default = "default_delim")]
1073        delim: char,
1074    },
1075
1076    // List operations
1077    /// `MapExpr` variant.
1078    MapExpr {
1079        block: Block,
1080        list: Box<Expr>,
1081        /// `flat_map { }` — peel one ARRAY ref from each iteration (stryke extension).
1082        flatten_array_refs: bool,
1083        /// `maps` / `flat_maps` — lazy iterator output (stryke); `map` / `flat_map` use `false`.
1084        #[serde(default)]
1085        stream: bool,
1086    },
1087    /// `map EXPR, LIST` — EXPR is evaluated in list context with `$_` set to each element.
1088    MapExprComma {
1089        expr: Box<Expr>,
1090        list: Box<Expr>,
1091        flatten_array_refs: bool,
1092        #[serde(default)]
1093        stream: bool,
1094    },
1095    /// `GrepExpr` variant.
1096    GrepExpr {
1097        block: Block,
1098        list: Box<Expr>,
1099        #[serde(default)]
1100        keyword: GrepBuiltinKeyword,
1101    },
1102    /// `grep EXPR, LIST` — EXPR is evaluated with `$_` set to each element (Perl list vs scalar context).
1103    GrepExprComma {
1104        expr: Box<Expr>,
1105        list: Box<Expr>,
1106        #[serde(default)]
1107        keyword: GrepBuiltinKeyword,
1108    },
1109    /// `sort BLOCK LIST`, `sort SUB LIST`, or `sort $coderef LIST` (Perl uses `$a`/`$b` in the comparator).
1110    SortExpr {
1111        cmp: Option<SortComparator>,
1112        list: Box<Expr>,
1113    },
1114    /// `ReverseExpr` variant.
1115    ReverseExpr(Box<Expr>),
1116    /// `rev EXPR` — always string-reverse (scalar reverse), stryke extension.
1117    Rev(Box<Expr>),
1118    /// `JoinExpr` variant.
1119    JoinExpr {
1120        separator: Box<Expr>,
1121        list: Box<Expr>,
1122    },
1123    /// `SplitExpr` variant.
1124    SplitExpr {
1125        pattern: Box<Expr>,
1126        string: Box<Expr>,
1127        limit: Option<Box<Expr>>,
1128    },
1129    /// `each { BLOCK } @list` — execute BLOCK for each element
1130    /// with `$_` aliased; void context (returns count in scalar context).
1131    ForEachExpr {
1132        block: Block,
1133        list: Box<Expr>,
1134    },
1135
1136    // Parallel extensions
1137    /// `PMapExpr` variant.
1138    PMapExpr {
1139        block: Block,
1140        list: Box<Expr>,
1141        /// `pmap { } @list, progress => EXPR` — when truthy, print a progress bar on stderr.
1142        progress: Option<Box<Expr>>,
1143        /// `pflat_map { }` — flatten each block result like [`ExprKind::MapExpr`] (arrays expand);
1144        /// parallel output is stitched in **input order** (unlike plain `pmap`, which is unordered).
1145        flat_outputs: bool,
1146        /// `pmap_on $cluster { } @list` — fan out over SSH (`stryke --remote-worker`); `None` = local rayon.
1147        #[serde(default, skip_serializing_if = "Option::is_none")]
1148        on_cluster: Option<Box<Expr>>,
1149        /// `pmaps` / `pflat_maps` — streaming variant: returns a lazy iterator that processes
1150        /// chunks in parallel via rayon instead of eagerly collecting all results.
1151        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1152        stream: bool,
1153    },
1154    /// `pmap_chunked N { BLOCK } @list [, progress => EXPR]` — parallel map in batches of N.
1155    PMapChunkedExpr {
1156        chunk_size: Box<Expr>,
1157        block: Block,
1158        list: Box<Expr>,
1159        progress: Option<Box<Expr>>,
1160    },
1161    /// `PGrepExpr` variant.
1162    PGrepExpr {
1163        block: Block,
1164        list: Box<Expr>,
1165        /// `pgrep { } @list, progress => EXPR` — stderr progress bar when truthy.
1166        progress: Option<Box<Expr>>,
1167        /// `pgreps` — streaming variant: returns a lazy iterator.
1168        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1169        stream: bool,
1170    },
1171    /// `pfor { BLOCK } @list [, progress => EXPR]` — stderr progress bar when truthy.
1172    PForExpr {
1173        block: Block,
1174        list: Box<Expr>,
1175        progress: Option<Box<Expr>>,
1176    },
1177    /// `par { BLOCK } INPUT` — generic parallel-chunk wrapper. Splits INPUT
1178    /// (string → UTF-8-aligned byte chunks; array/list → element-chunks)
1179    /// into N pieces (N = available rayon threads), evaluates BLOCK per
1180    /// chunk in parallel with `$_` bound to the chunk, then concatenates
1181    /// results. Lets any whole-input op (`letters`, `chars`, `uc`, `freq`,
1182    /// regex `//g`, etc.) parallelize without needing a `pX` variant.
1183    ParExpr {
1184        block: Block,
1185        list: Box<Expr>,
1186    },
1187    /// `par_reduce { extract } [ { merge } ] INPUT` — chunk-extract-merge.
1188    /// Same chunker as `par {}`, but each chunk's result is reduced
1189    /// pairwise across chunks instead of concatenated.
1190    ///
1191    /// - One block: auto-merger picks based on result type (number → `+`,
1192    ///   `hash<num>` → key-wise `+`, array → concat, string → concat).
1193    /// - Two blocks: explicit pairwise reducer with `$a`/`$b`.
1194    ParReduceExpr {
1195        extract_block: Block,
1196        reduce_block: Option<Block>,
1197        list: Box<Expr>,
1198    },
1199    /// Distributed counterpart of [`ExprKind::ParReduceExpr`]. Same chunk-block
1200    /// semantics (stages operate on `@_`) but chunks ship to a `RemoteCluster`
1201    /// of SSH workers via the existing `cluster::run_cluster` dispatcher.
1202    /// Built by `~d> on $cluster SOURCE stage1 stage2 ...`.
1203    DistReduceExpr {
1204        cluster: Box<Expr>,
1205        extract_block: Block,
1206        list: Box<Expr>,
1207    },
1208    /// `par_lines PATH, fn { ... } [, progress => EXPR]` — optional stderr progress (per line).
1209    ParLinesExpr {
1210        path: Box<Expr>,
1211        callback: Box<Expr>,
1212        progress: Option<Box<Expr>>,
1213    },
1214    /// `par_walk PATH, fn { ... } [, progress => EXPR]` — parallel recursive directory walk; `$_` is each path.
1215    ParWalkExpr {
1216        path: Box<Expr>,
1217        callback: Box<Expr>,
1218        progress: Option<Box<Expr>>,
1219    },
1220    /// `pwatch GLOB, fn { ... }` — notify-based watcher (evaluated by interpreter).
1221    PwatchExpr {
1222        path: Box<Expr>,
1223        callback: Box<Expr>,
1224    },
1225    /// `psort { } @list [, progress => EXPR]` — stderr progress when truthy (start/end phases).
1226    PSortExpr {
1227        cmp: Option<Block>,
1228        list: Box<Expr>,
1229        progress: Option<Box<Expr>>,
1230    },
1231    /// `reduce { $a + $b } @list` — sequential left fold over the list.
1232    /// `$a` is the accumulator; `$b` is the next list element.
1233    ReduceExpr {
1234        block: Block,
1235        list: Box<Expr>,
1236    },
1237    /// `preduce { $a + $b } @list` — parallel fold/reduce using rayon.
1238    /// $a and $b are set to the accumulator and current element.
1239    PReduceExpr {
1240        block: Block,
1241        list: Box<Expr>,
1242        /// `preduce { } @list, progress => EXPR` — stderr progress bar when truthy.
1243        progress: Option<Box<Expr>>,
1244    },
1245    /// `preduce_init EXPR, { $a / $b } @list` — parallel fold with explicit identity.
1246    /// Each chunk starts from a clone of `EXPR`; partials are merged (hash maps add counts per key;
1247    /// other types use the same block with `$a` / `$b` as partial accumulators). `$a` is the
1248    /// accumulator, `$b` is the next list element; `@_` is `($a, $b)` for `my ($acc, $item) = @_`.
1249    PReduceInitExpr {
1250        init: Box<Expr>,
1251        block: Block,
1252        list: Box<Expr>,
1253        progress: Option<Box<Expr>>,
1254    },
1255    /// `pmap_reduce { map } { reduce } @list` — fused parallel map + tree reduce (no full mapped array).
1256    PMapReduceExpr {
1257        map_block: Block,
1258        reduce_block: Block,
1259        list: Box<Expr>,
1260        progress: Option<Box<Expr>>,
1261    },
1262    /// `pcache { BLOCK } @list [, progress => EXPR]` — stderr progress bar when truthy.
1263    PcacheExpr {
1264        block: Block,
1265        list: Box<Expr>,
1266        progress: Option<Box<Expr>>,
1267    },
1268    /// `pselect($rx1, $rx2, ...)` — optional `timeout => SECS` for bounded wait.
1269    PselectExpr {
1270        receivers: Vec<Expr>,
1271        timeout: Option<Box<Expr>>,
1272    },
1273    /// `fan [COUNT] { BLOCK }` — execute BLOCK COUNT times in parallel (default COUNT = rayon pool size).
1274    /// `fan_cap [COUNT] { BLOCK }` — same, but return value is a **list** of each block's return value (index order).
1275    /// `$_` is set to the iteration index (0..COUNT-1).
1276    /// Optional `, progress => EXPR` — stderr progress bar (like `pmap`).
1277    FanExpr {
1278        count: Option<Box<Expr>>,
1279        block: Block,
1280        progress: Option<Box<Expr>>,
1281        capture: bool,
1282    },
1283
1284    /// `async { BLOCK }` — run BLOCK on a worker thread; returns a task handle.
1285    AsyncBlock {
1286        body: Block,
1287    },
1288    /// `spawn { BLOCK }` — same as [`ExprKind::AsyncBlock`] (Rust `thread::spawn`–style naming); join with `await`.
1289    SpawnBlock {
1290        body: Block,
1291    },
1292    /// `trace { BLOCK }` — print `mysync` scalar mutations to stderr (for parallel debugging).
1293    Trace {
1294        body: Block,
1295    },
1296    /// `timer { BLOCK }` — run BLOCK and return elapsed wall time in milliseconds (float).
1297    Timer {
1298        body: Block,
1299    },
1300    /// `bench { BLOCK } N` — run BLOCK `N` times (warmup + min/mean/p99 wall time, ms).
1301    Bench {
1302        body: Block,
1303        times: Box<Expr>,
1304    },
1305    /// `spinner "msg" { BLOCK }` — animated spinner on stderr while block runs.
1306    Spinner {
1307        message: Box<Expr>,
1308        body: Block,
1309    },
1310    /// `await EXPR` — join an async task, or return EXPR unchanged.
1311    Await(Box<Expr>),
1312    /// Read entire file as UTF-8 (`slurp $path`).
1313    Slurp(Box<Expr>),
1314    /// `swallow PATTERN` — expand a zsh-style glob and return a hash
1315    /// `{ canonicalized_abspath => raw_bytes }`. Per-file body never decodes,
1316    /// so binary files round-trip cleanly. Hard-fails on non-regular matches
1317    /// the same way `slurp` does; opt out with the `(N)` null-glob qualifier.
1318    Swallow(Box<Expr>),
1319    /// `burp HASH` — inverse of `swallow`. Take a hash `{ path => bytes }`,
1320    /// write each entry to disk (creates parent directories automatically),
1321    /// and return the number of files written. Hard-fails on the first I/O
1322    /// error. Accepts plain hashes and hash refs; values may be bytes or any
1323    /// scalar that stringifies (matches `spew`/`spurt` conventions).
1324    Burp(Box<Expr>),
1325    /// `god EXPR` — omniscient runtime introspection. Returns a structured
1326    /// multi-line dump showing the type tag, heap pointer, Arc strong/weak
1327    /// counts, byte hex previews, generator/pipeline state, and closure
1328    /// captures. Cycle-safe via per-pointer recursion tracking. Sibling to
1329    /// `pp` (human-friendly) and `ddump` (deep structure).
1330    God(Box<Expr>),
1331    /// `ingest PATTERN` — streaming variant of `swallow`: returns a lazy
1332    /// iterator yielding `[canonicalized_abspath, raw_bytes]` per file. Only
1333    /// one file's bytes are resident at a time. Path list and stat/canonicalize
1334    /// are eager (full zsh qualifier support); file reads are lazy. Hard-fails
1335    /// on non-regular matches up-front, matching `slurp`/`swallow` policy.
1336    Ingest(Box<Expr>),
1337    /// Run shell command and return structured output (`capture "cmd"`).
1338    Capture(Box<Expr>),
1339    /// `` `cmd` `` / `qx{cmd}` — run via `sh -c`, return **stdout as a string** (Perl); updates `$?`.
1340    Qx(Box<Expr>),
1341    /// Blocking HTTP GET (`fetch_url $url`).
1342    FetchUrl(Box<Expr>),
1343
1344    /// `pchannel()` — unbounded; `pchannel(N)` — bounded capacity N.
1345    Pchannel {
1346        capacity: Option<Box<Expr>>,
1347    },
1348
1349    // Array/Hash operations
1350    /// `Push` variant.
1351    Push {
1352        array: Box<Expr>,
1353        values: Vec<Expr>,
1354    },
1355    /// `Pop` variant.
1356    Pop(Box<Expr>),
1357    /// `Shift` variant.
1358    Shift(Box<Expr>),
1359    /// `Unshift` variant.
1360    Unshift {
1361        array: Box<Expr>,
1362        values: Vec<Expr>,
1363    },
1364    /// `Splice` variant.
1365    Splice {
1366        array: Box<Expr>,
1367        offset: Option<Box<Expr>>,
1368        length: Option<Box<Expr>>,
1369        replacement: Vec<Expr>,
1370    },
1371    /// `Delete` variant.
1372    Delete(Box<Expr>),
1373    /// `Exists` variant.
1374    Exists(Box<Expr>),
1375    /// `Keys` variant.
1376    Keys(Box<Expr>),
1377    /// `Values` variant.
1378    Values(Box<Expr>),
1379    /// `Each` variant.
1380    Each(Box<Expr>),
1381
1382    // String operations
1383    /// `Chomp` variant.
1384    Chomp(Box<Expr>),
1385    /// `Chop` variant.
1386    Chop(Box<Expr>),
1387    /// `Length` variant.
1388    Length(Box<Expr>),
1389    /// `Substr` variant.
1390    Substr {
1391        string: Box<Expr>,
1392        offset: Box<Expr>,
1393        length: Option<Box<Expr>>,
1394        replacement: Option<Box<Expr>>,
1395    },
1396    /// `Index` variant.
1397    Index {
1398        string: Box<Expr>,
1399        substr: Box<Expr>,
1400        position: Option<Box<Expr>>,
1401    },
1402    /// `Rindex` variant.
1403    Rindex {
1404        string: Box<Expr>,
1405        substr: Box<Expr>,
1406        position: Option<Box<Expr>>,
1407    },
1408    /// `Sprintf` variant.
1409    Sprintf {
1410        format: Box<Expr>,
1411        args: Vec<Expr>,
1412    },
1413
1414    // Numeric
1415    /// `Abs` variant.
1416    Abs(Box<Expr>),
1417    /// `Int` variant.
1418    Int(Box<Expr>),
1419    /// `Sqrt` variant.
1420    Sqrt(Box<Expr>),
1421    /// `Sin` variant.
1422    Sin(Box<Expr>),
1423    /// `Cos` variant.
1424    Cos(Box<Expr>),
1425    /// `Atan2` variant.
1426    Atan2 {
1427        y: Box<Expr>,
1428        x: Box<Expr>,
1429    },
1430    /// `Exp` variant.
1431    Exp(Box<Expr>),
1432    /// `Log` variant.
1433    Log(Box<Expr>),
1434    /// `rand` with optional upper bound (none = Perl default 1.0).
1435    Rand(Option<Box<Expr>>),
1436    /// `srand` with optional seed (none = time-based).
1437    Srand(Option<Box<Expr>>),
1438    /// `Hex` variant.
1439    Hex(Box<Expr>),
1440    /// `Oct` variant.
1441    Oct(Box<Expr>),
1442
1443    // Case
1444    /// `Lc` variant.
1445    Lc(Box<Expr>),
1446    /// `Uc` variant.
1447    Uc(Box<Expr>),
1448    /// `Lcfirst` variant.
1449    Lcfirst(Box<Expr>),
1450    /// `Ucfirst` variant.
1451    Ucfirst(Box<Expr>),
1452
1453    /// Unicode case fold (Perl `fc`).
1454    Fc(Box<Expr>),
1455    /// Regex-escape a string (Perl `quotemeta`, aliased `qm`). Lowers to
1456    /// `Op::CallBuiltin(BuiltinId::Quotemeta, 1)` for JIT lowering.
1457    Quotemeta(Box<Expr>),
1458    /// DES-style `crypt` (see libc `crypt(3)` on Unix; empty on other targets).
1459    Crypt {
1460        plaintext: Box<Expr>,
1461        salt: Box<Expr>,
1462    },
1463    /// `pos` — optional scalar lvalue target (`None` = `$_`).
1464    Pos(Option<Box<Expr>>),
1465    /// `study` — hint for repeated matching; returns byte length of the string.
1466    Study(Box<Expr>),
1467
1468    // Type
1469    /// `Defined` variant.
1470    Defined(Box<Expr>),
1471    /// `Ref` variant.
1472    Ref(Box<Expr>),
1473    /// `ScalarContext` variant.
1474    ScalarContext(Box<Expr>),
1475
1476    // Char
1477    /// `Chr` variant.
1478    Chr(Box<Expr>),
1479    /// `Ord` variant.
1480    Ord(Box<Expr>),
1481
1482    // I/O
1483    /// `open my $fh` — only valid as [`ExprKind::Open::handle`]; declares `$fh` and binds the handle.
1484    OpenMyHandle {
1485        name: String,
1486    },
1487    /// `Open` variant.
1488    Open {
1489        handle: Box<Expr>,
1490        mode: Box<Expr>,
1491        file: Option<Box<Expr>>,
1492    },
1493    /// `Close` variant.
1494    Close(Box<Expr>),
1495    /// `ReadLine` variant.
1496    ReadLine(Option<String>),
1497    /// `Eof` variant.
1498    Eof(Option<Box<Expr>>),
1499    /// `Opendir` variant.
1500    Opendir {
1501        handle: Box<Expr>,
1502        path: Box<Expr>,
1503    },
1504    /// `Readdir` variant.
1505    Readdir(Box<Expr>),
1506    /// `Closedir` variant.
1507    Closedir(Box<Expr>),
1508    /// `Rewinddir` variant.
1509    Rewinddir(Box<Expr>),
1510    /// `Telldir` variant.
1511    Telldir(Box<Expr>),
1512    /// `Seekdir` variant.
1513    Seekdir {
1514        handle: Box<Expr>,
1515        position: Box<Expr>,
1516    },
1517
1518    // File tests
1519    /// `FileTest` variant.
1520    FileTest {
1521        op: char,
1522        expr: Box<Expr>,
1523    },
1524
1525    // System
1526    /// `System` variant.
1527    System(Vec<Expr>),
1528    /// `Exec` variant.
1529    Exec(Vec<Expr>),
1530    /// `Eval` variant.
1531    Eval(Box<Expr>),
1532    /// `Do` variant.
1533    Do(Box<Expr>),
1534    /// `Require` variant.
1535    Require(Box<Expr>),
1536    /// `Exit` variant.
1537    Exit(Option<Box<Expr>>),
1538    /// `Chdir` variant.
1539    Chdir(Box<Expr>),
1540    /// `Mkdir` variant.
1541    Mkdir {
1542        path: Box<Expr>,
1543        mode: Option<Box<Expr>>,
1544    },
1545    /// `Unlink` variant.
1546    Unlink(Vec<Expr>),
1547    /// `Rename` variant.
1548    Rename {
1549        old: Box<Expr>,
1550        new: Box<Expr>,
1551    },
1552    /// `chmod MODE, @files` — first expr is mode, rest are paths.
1553    Chmod(Vec<Expr>),
1554    /// `chown UID, GID, @files` — first two are uid/gid, rest are paths.
1555    Chown(Vec<Expr>),
1556    /// `Stat` variant.
1557    Stat(Box<Expr>),
1558    /// `Lstat` variant.
1559    Lstat(Box<Expr>),
1560    /// `Link` variant.
1561    Link {
1562        old: Box<Expr>,
1563        new: Box<Expr>,
1564    },
1565    /// `Symlink` variant.
1566    Symlink {
1567        old: Box<Expr>,
1568        new: Box<Expr>,
1569    },
1570    /// `Readlink` variant.
1571    Readlink(Box<Expr>),
1572    /// `files` / `files DIR` — list file names in a directory (default: `.`).
1573    Files(Vec<Expr>),
1574    /// `filesf` / `filesf DIR` / `f` — list only regular file names in a directory (default: `.`).
1575    Filesf(Vec<Expr>),
1576    /// `fr DIR` — list only regular file names recursively (default: `.`).
1577    FilesfRecursive(Vec<Expr>),
1578    /// `dirs` / `dirs DIR` / `d` — list subdirectory names in a directory (default: `.`).
1579    Dirs(Vec<Expr>),
1580    /// `dr DIR` — list subdirectory paths recursively (default: `.`).
1581    DirsRecursive(Vec<Expr>),
1582    /// `sym_links` / `sym_links DIR` — list symlink names in a directory (default: `.`).
1583    SymLinks(Vec<Expr>),
1584    /// `sockets` / `sockets DIR` — list Unix socket names in a directory (default: `.`).
1585    Sockets(Vec<Expr>),
1586    /// `pipes` / `pipes DIR` — list named-pipe (FIFO) names in a directory (default: `.`).
1587    Pipes(Vec<Expr>),
1588    /// `block_devices` / `block_devices DIR` — list block device names in a directory (default: `.`).
1589    BlockDevices(Vec<Expr>),
1590    /// `char_devices` / `char_devices DIR` — list character device names in a directory (default: `.`).
1591    CharDevices(Vec<Expr>),
1592    /// `exe` / `exe DIR` — list executable file names in a directory (default: `.`).
1593    Executables(Vec<Expr>),
1594    /// `Glob` variant.
1595    Glob(Vec<Expr>),
1596    /// Parallel recursive glob (rayon); same patterns as `glob`, different walk strategy.
1597    /// Optional `, progress => EXPR` — stderr progress bar (one tick per pattern).
1598    GlobPar {
1599        args: Vec<Expr>,
1600        progress: Option<Box<Expr>>,
1601    },
1602    /// `par_sed PATTERN, REPLACEMENT, FILES... [, progress => EXPR]` — parallel in-place regex replace per file (`g` semantics).
1603    ParSed {
1604        args: Vec<Expr>,
1605        progress: Option<Box<Expr>>,
1606    },
1607
1608    // Bless
1609    /// `Bless` variant.
1610    Bless {
1611        ref_expr: Box<Expr>,
1612        class: Option<Box<Expr>>,
1613    },
1614
1615    // Caller
1616    /// `Caller` variant.
1617    Caller(Option<Box<Expr>>),
1618
1619    // Wantarray
1620    /// `Wantarray` variant.
1621    Wantarray,
1622
1623    // List / Context
1624    /// `List` variant.
1625    List(Vec<Expr>),
1626
1627    // Postfix if/unless/while/until/for
1628    /// `PostfixIf` variant.
1629    PostfixIf {
1630        expr: Box<Expr>,
1631        condition: Box<Expr>,
1632    },
1633    /// `PostfixUnless` variant.
1634    PostfixUnless {
1635        expr: Box<Expr>,
1636        condition: Box<Expr>,
1637    },
1638    /// `PostfixWhile` variant.
1639    PostfixWhile {
1640        expr: Box<Expr>,
1641        condition: Box<Expr>,
1642    },
1643    /// `PostfixUntil` variant.
1644    PostfixUntil {
1645        expr: Box<Expr>,
1646        condition: Box<Expr>,
1647    },
1648    /// `PostfixForeach` variant.
1649    PostfixForeach {
1650        expr: Box<Expr>,
1651        list: Box<Expr>,
1652    },
1653
1654    /// `retry { BLOCK } times => N [, backoff => linear|exponential|none]` — re-run block until success or attempts exhausted.
1655    RetryBlock {
1656        body: Block,
1657        times: Box<Expr>,
1658        backoff: RetryBackoff,
1659    },
1660    /// `rate_limit(MAX, WINDOW) { BLOCK }` — sliding window: at most MAX runs per WINDOW (e.g. `"1s"`).
1661    /// `slot` is assigned at parse time for per-site state in the interpreter.
1662    RateLimitBlock {
1663        slot: u32,
1664        max: Box<Expr>,
1665        window: Box<Expr>,
1666        body: Block,
1667    },
1668    /// `every(INTERVAL) { BLOCK }` — repeat BLOCK forever with sleep (INTERVAL like `"5s"` or seconds).
1669    EveryBlock {
1670        interval: Box<Expr>,
1671        body: Block,
1672    },
1673    /// `gen { ... yield ... }` — lazy generator; call `->next` for each value.
1674    GenBlock {
1675        body: Block,
1676    },
1677    /// `yield EXPR` — only valid inside `gen { }` (and propagates through control flow).
1678    Yield(Box<Expr>),
1679
1680    /// `match (EXPR) { PATTERN => EXPR, ... }` — first matching arm; bindings scoped to the arm body.
1681    AlgebraicMatch {
1682        subject: Box<Expr>,
1683        arms: Vec<MatchArm>,
1684    },
1685}
1686/// `StringPart` — see variants.
1687#[derive(Debug, Clone, Serialize, Deserialize)]
1688pub enum StringPart {
1689    /// `Literal` variant.
1690    Literal(String),
1691    /// `ScalarVar` variant.
1692    ScalarVar(String),
1693    /// `ArrayVar` variant.
1694    ArrayVar(String),
1695    /// `Expr` variant.
1696    Expr(Expr),
1697}
1698/// `DerefKind` — see variants.
1699#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1700pub enum DerefKind {
1701    /// `Array` variant.
1702    Array,
1703    /// `Hash` variant.
1704    Hash,
1705    /// `Call` variant.
1706    Call,
1707}
1708/// `BinOp` — see variants.
1709#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1710pub enum BinOp {
1711    /// `Add` variant.
1712    Add,
1713    /// `Sub` variant.
1714    Sub,
1715    /// `Mul` variant.
1716    Mul,
1717    /// `Div` variant.
1718    Div,
1719    /// `Mod` variant.
1720    Mod,
1721    /// `Pow` variant.
1722    Pow,
1723    /// `Concat` variant.
1724    Concat,
1725    /// `NumEq` variant.
1726    NumEq,
1727    /// `NumNe` variant.
1728    NumNe,
1729    /// `NumLt` variant.
1730    NumLt,
1731    /// `NumGt` variant.
1732    NumGt,
1733    /// `NumLe` variant.
1734    NumLe,
1735    /// `NumGe` variant.
1736    NumGe,
1737    /// `Spaceship` variant.
1738    Spaceship,
1739    /// `StrEq` variant.
1740    StrEq,
1741    /// `StrNe` variant.
1742    StrNe,
1743    /// `StrLt` variant.
1744    StrLt,
1745    /// `StrGt` variant.
1746    StrGt,
1747    /// `StrLe` variant.
1748    StrLe,
1749    /// `StrGe` variant.
1750    StrGe,
1751    /// `StrCmp` variant.
1752    StrCmp,
1753    /// `LogAnd` variant.
1754    LogAnd,
1755    /// `LogOr` variant.
1756    LogOr,
1757    /// `DefinedOr` variant.
1758    DefinedOr,
1759    /// `BitAnd` variant.
1760    BitAnd,
1761    /// `BitOr` variant.
1762    BitOr,
1763    /// `BitXor` variant.
1764    BitXor,
1765    /// `ShiftLeft` variant.
1766    ShiftLeft,
1767    /// `ShiftRight` variant.
1768    ShiftRight,
1769    /// `LogAndWord` variant.
1770    LogAndWord,
1771    /// `LogOrWord` variant.
1772    LogOrWord,
1773    /// `BindMatch` variant.
1774    BindMatch,
1775    /// `BindNotMatch` variant.
1776    BindNotMatch,
1777}
1778/// `UnaryOp` — see variants.
1779#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1780pub enum UnaryOp {
1781    /// `Negate` variant.
1782    Negate,
1783    /// `LogNot` variant.
1784    LogNot,
1785    /// `BitNot` variant.
1786    BitNot,
1787    /// `LogNotWord` variant.
1788    LogNotWord,
1789    /// `PreIncrement` variant.
1790    PreIncrement,
1791    /// `PreDecrement` variant.
1792    PreDecrement,
1793    /// `Ref` variant.
1794    Ref,
1795}
1796/// `PostfixOp` — see variants.
1797#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1798pub enum PostfixOp {
1799    /// `Increment` variant.
1800    Increment,
1801    /// `Decrement` variant.
1802    Decrement,
1803}
1804
1805#[cfg(test)]
1806mod tests {
1807    use super::*;
1808
1809    #[test]
1810    fn binop_deref_kind_distinct() {
1811        assert_ne!(BinOp::Add, BinOp::Sub);
1812        assert_eq!(DerefKind::Call, DerefKind::Call);
1813    }
1814
1815    #[test]
1816    fn sigil_variants_exhaustive_in_tests() {
1817        let all = [Sigil::Scalar, Sigil::Array, Sigil::Hash];
1818        assert_eq!(all.len(), 3);
1819    }
1820
1821    #[test]
1822    fn program_empty_roundtrip_clone() {
1823        let p = Program { statements: vec![] };
1824        assert!(p.clone().statements.is_empty());
1825    }
1826
1827    #[test]
1828    fn program_serializes_to_json() {
1829        let p = crate::parse("1+2;").expect("parse");
1830        let s = serde_json::to_string(&p).expect("json");
1831        assert!(s.contains("\"statements\""));
1832        assert!(s.contains("BinOp"));
1833    }
1834
1835    // ─── GrepBuiltinKeyword ───────────────────────────────────────────
1836
1837    #[test]
1838    fn grep_keyword_as_str_matrix() {
1839        assert_eq!(GrepBuiltinKeyword::Grep.as_str(), "grep");
1840        assert_eq!(GrepBuiltinKeyword::Greps.as_str(), "greps");
1841        assert_eq!(GrepBuiltinKeyword::Filter.as_str(), "filter");
1842        assert_eq!(GrepBuiltinKeyword::FindAll.as_str(), "find_all");
1843    }
1844
1845    #[test]
1846    fn grep_keyword_is_stream_only_false_for_grep() {
1847        // `grep` is the collecting (non-streaming) variant; everything else streams.
1848        assert!(!GrepBuiltinKeyword::Grep.is_stream());
1849        assert!(GrepBuiltinKeyword::Greps.is_stream());
1850        assert!(GrepBuiltinKeyword::Filter.is_stream());
1851        assert!(GrepBuiltinKeyword::FindAll.is_stream());
1852    }
1853
1854    // ─── PerlTypeName byte encoding ───────────────────────────────────
1855
1856    #[test]
1857    fn perl_type_name_byte_roundtrip() {
1858        // 0..7 → simple types → back to same byte.
1859        for b in 0..=7u8 {
1860            let t = PerlTypeName::from_byte(b).unwrap_or_else(|| panic!("byte {b} unknown"));
1861            assert_eq!(t.as_byte(), Some(b), "round-trip failed for byte {b}");
1862        }
1863    }
1864
1865    #[test]
1866    fn perl_type_name_unknown_bytes_return_none() {
1867        assert!(PerlTypeName::from_byte(8).is_none());
1868        assert!(PerlTypeName::from_byte(255).is_none());
1869    }
1870
1871    #[test]
1872    fn perl_type_name_struct_and_enum_have_no_byte_encoding() {
1873        // Named types require name-pool lookup, not byte encoding.
1874        assert_eq!(PerlTypeName::Struct("Point".into()).as_byte(), None);
1875        assert_eq!(PerlTypeName::Enum("Color".into()).as_byte(), None);
1876    }
1877
1878    #[test]
1879    fn perl_type_name_simple_byte_assignments_are_stable() {
1880        // Pin the byte ordering so VM bytecode doesn't shift accidentally.
1881        assert_eq!(PerlTypeName::Int.as_byte(), Some(0));
1882        assert_eq!(PerlTypeName::Str.as_byte(), Some(1));
1883        assert_eq!(PerlTypeName::Float.as_byte(), Some(2));
1884        assert_eq!(PerlTypeName::Bool.as_byte(), Some(3));
1885        assert_eq!(PerlTypeName::Array.as_byte(), Some(4));
1886        assert_eq!(PerlTypeName::Hash.as_byte(), Some(5));
1887        assert_eq!(PerlTypeName::Ref.as_byte(), Some(6));
1888        assert_eq!(PerlTypeName::Any.as_byte(), Some(7));
1889    }
1890
1891    #[test]
1892    fn perl_type_name_display_name_simple_types() {
1893        assert_eq!(PerlTypeName::Int.display_name(), "Int");
1894        assert_eq!(PerlTypeName::Str.display_name(), "Str");
1895        assert_eq!(PerlTypeName::Float.display_name(), "Float");
1896        assert_eq!(PerlTypeName::Bool.display_name(), "Bool");
1897        assert_eq!(PerlTypeName::Array.display_name(), "Array");
1898        assert_eq!(PerlTypeName::Hash.display_name(), "Hash");
1899        assert_eq!(PerlTypeName::Ref.display_name(), "Ref");
1900        assert_eq!(PerlTypeName::Any.display_name(), "Any");
1901    }
1902
1903    #[test]
1904    fn perl_type_name_display_name_named_types() {
1905        assert_eq!(PerlTypeName::Struct("Point".into()).display_name(), "Point");
1906        assert_eq!(PerlTypeName::Enum("Color".into()).display_name(), "Color");
1907    }
1908
1909    // ─── PerlTypeName::check_value runtime type-check ─────────────────
1910
1911    #[test]
1912    fn perl_type_int_accepts_integer_like() {
1913        let v = crate::value::StrykeValue::integer(42);
1914        assert!(PerlTypeName::Int.check_value(&v).is_ok());
1915    }
1916
1917    #[test]
1918    fn perl_type_int_rejects_string() {
1919        let v = crate::value::StrykeValue::string("hi".into());
1920        let err = PerlTypeName::Int.check_value(&v);
1921        assert!(err.is_err());
1922        assert!(err.unwrap_err().contains("Int"));
1923    }
1924
1925    #[test]
1926    fn perl_type_str_accepts_string() {
1927        let v = crate::value::StrykeValue::string("hi".into());
1928        assert!(PerlTypeName::Str.check_value(&v).is_ok());
1929    }
1930
1931    #[test]
1932    fn perl_type_float_accepts_both_int_and_float() {
1933        // Float is permissive — accepts integer-like too (numeric promotion).
1934        assert!(PerlTypeName::Float
1935            .check_value(&crate::value::StrykeValue::integer(7))
1936            .is_ok());
1937        assert!(PerlTypeName::Float
1938            .check_value(&crate::value::StrykeValue::float(3.14))
1939            .is_ok());
1940    }
1941
1942    #[test]
1943    fn perl_type_bool_accepts_anything() {
1944        // Bool's check_value returns Ok(()) for everything (perl truthiness).
1945        assert!(PerlTypeName::Bool
1946            .check_value(&crate::value::StrykeValue::integer(0))
1947            .is_ok());
1948        assert!(PerlTypeName::Bool
1949            .check_value(&crate::value::StrykeValue::string("".into()))
1950            .is_ok());
1951        assert!(PerlTypeName::Bool
1952            .check_value(&crate::value::StrykeValue::UNDEF)
1953            .is_ok());
1954    }
1955
1956    // ─── Statement::new constructor ───────────────────────────────────
1957
1958    #[test]
1959    fn statement_new_preserves_line_and_kind() {
1960        let kind = StmtKind::Expression(Expr {
1961            kind: ExprKind::Integer(42),
1962            line: 7,
1963        });
1964        let s = Statement::new(kind, 7);
1965        assert_eq!(s.line, 7);
1966        // Round-trip the kind via debug formatting since pattern-match would
1967        // require StmtKind to be PartialEq.
1968        assert!(format!("{:?}", s.kind).contains("Expression"));
1969    }
1970}