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