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