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