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