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 [`StrykeValue`](crate::value::StrykeValue), `Str` only string, `Float` allows int or float.
526 pub fn check_value(&self, v: &crate::value::StrykeValue) -> 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 /// True when declared with parens: `my ($x) = @a` vs `my $x = @a`.
649 /// In list context, a scalar gets the first element; in scalar context, it gets the count.
650 #[serde(default)]
651 pub list_context: bool,
652}
653
654#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
655pub enum Sigil {
656 Scalar,
657 Array,
658 Hash,
659 /// `local *FH` — filehandle slot alias (limited typeglob).
660 Typeglob,
661}
662
663pub type Block = Vec<Statement>;
664
665/// Comparator for `sort` — `{ $a <=> $b }`, or a code ref / expression (Perl `sort $cmp LIST`).
666#[derive(Debug, Clone, Serialize, Deserialize)]
667pub enum SortComparator {
668 Block(Block),
669 Code(Box<Expr>),
670}
671
672// ── Algebraic `match` expression (stryke extension) ──
673
674/// One arm of [`ExprKind::AlgebraicMatch`]: `PATTERN [if EXPR] => EXPR`.
675#[derive(Debug, Clone, Serialize, Deserialize)]
676pub struct MatchArm {
677 pub pattern: MatchPattern,
678 /// Optional guard (`if EXPR`) evaluated after pattern match; `$_` is the match subject.
679 #[serde(skip_serializing_if = "Option::is_none")]
680 pub guard: Option<Box<Expr>>,
681 pub body: Expr,
682}
683
684/// `retry { } backoff => exponential` — sleep policy between attempts (after failure).
685#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
686pub enum RetryBackoff {
687 /// No delay between attempts.
688 None,
689 /// Delay grows linearly: `base_ms * attempt` (attempt starts at 1).
690 Linear,
691 /// Delay doubles each failure: `base_ms * 2^(attempt-1)` (capped).
692 Exponential,
693}
694
695/// Pattern for algebraic `match` (distinct from the `=~` / regex [`ExprKind::Match`]).
696#[derive(Debug, Clone, Serialize, Deserialize)]
697pub enum MatchPattern {
698 /// `_` — matches anything.
699 Any,
700 /// `/regex/` — subject stringified; on success the arm body sets `$_` to the subject and
701 /// populates match variables (`$1`…, `$&`, `${^MATCH}`, `@-`/`@+`, `%+`, …) like `=~`.
702 Regex { pattern: String, flags: String },
703 /// Arbitrary expression compared for equality / smart-match against the subject.
704 Value(Box<Expr>),
705 /// `[1, 2, *]` — prefix elements match; optional `*` matches any tail (must be last).
706 Array(Vec<MatchArrayElem>),
707 /// `{ name => $n, ... }` — required keys; `$n` binds the value for the arm body.
708 Hash(Vec<MatchHashPair>),
709 /// `Some($x)` — matches array-like values with **at least two** elements where index `1` is
710 /// Perl-truthy (stryke: `$gen->next` yields `[value, more]` with `more` truthy while iterating).
711 OptionSome(String),
712}
713
714#[derive(Debug, Clone, Serialize, Deserialize)]
715pub enum MatchArrayElem {
716 Expr(Expr),
717 /// `$name` at the top of a pattern element — bind this position to a new lexical `$name`.
718 /// Use `[($x)]` if you need smartmatch against the current value of `$x` instead.
719 CaptureScalar(String),
720 /// Rest-of-array wildcard (only valid as the last element).
721 Rest,
722 /// `@name` — bind remaining elements as a new array to `@name` (only valid as the last element).
723 RestBind(String),
724}
725
726#[derive(Debug, Clone, Serialize, Deserialize)]
727pub enum MatchHashPair {
728 /// `key => _` — key must exist.
729 KeyOnly { key: Expr },
730 /// `key => $name` — key must exist; value is bound to `$name` in the arm.
731 Capture { key: Expr, name: String },
732}
733
734#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
735pub enum MagicConstKind {
736 /// Current source path (`$0`-style script name or `-e`).
737 File,
738 /// Line number of this token (1-based, same as lexer).
739 Line,
740 /// Reference to currently executing subroutine (for anonymous recursion).
741 Sub,
742}
743
744#[derive(Debug, Clone, Serialize, Deserialize)]
745pub struct Expr {
746 pub kind: ExprKind,
747 pub line: usize,
748}
749
750#[derive(Debug, Clone, Serialize, Deserialize)]
751pub enum ExprKind {
752 // Literals
753 Integer(i64),
754 Float(f64),
755 String(String),
756 /// Unquoted identifier used as an expression term (`if (FOO)`), distinct from quoted `'FOO'` / `"FOO"`.
757 /// Resolved at runtime: nullary subroutine if defined, otherwise stringifies like Perl barewords.
758 Bareword(String),
759 Regex(String, String),
760 QW(Vec<String>),
761 Undef,
762 /// `__FILE__` / `__LINE__` (Perl compile-time literals).
763 MagicConst(MagicConstKind),
764
765 // Interpolated string (mix of literal and variable parts)
766 InterpolatedString(Vec<StringPart>),
767
768 // Variables
769 ScalarVar(String),
770 ArrayVar(String),
771 HashVar(String),
772 ArrayElement {
773 array: String,
774 index: Box<Expr>,
775 },
776 HashElement {
777 hash: String,
778 key: Box<Expr>,
779 },
780 ArraySlice {
781 array: String,
782 indices: Vec<Expr>,
783 },
784 HashSlice {
785 hash: String,
786 keys: Vec<Expr>,
787 },
788 /// `%h{KEYS}` — Perl 5.20+ key-value slice: returns a flat list of
789 /// (key, value, key, value, ...) pairs instead of just values. (BUG-008)
790 HashKvSlice {
791 hash: String,
792 keys: Vec<Expr>,
793 },
794 /// `@$container{keys}` — hash slice when the hash is reached via a scalar ref (Perl `@$href{k1,k2}`).
795 HashSliceDeref {
796 container: Box<Expr>,
797 keys: Vec<Expr>,
798 },
799 /// `(LIST)[i,...]` / `(sort ...)[0]` — subscript after a non-arrow container (not `$a[i]` / `$r->[i]`).
800 AnonymousListSlice {
801 source: Box<Expr>,
802 indices: Vec<Expr>,
803 },
804
805 // References
806 ScalarRef(Box<Expr>),
807 ArrayRef(Vec<Expr>),
808 HashRef(Vec<(Expr, Expr)>),
809 CodeRef {
810 params: Vec<SubSigParam>,
811 body: Block,
812 },
813 /// Unary `&name` — invoke subroutine `name` (Perl `&foo` / `&Foo::bar`).
814 SubroutineRef(String),
815 /// `\&name` — coderef to an existing named subroutine (Perl `\&foo`).
816 SubroutineCodeRef(String),
817 /// `\&{ EXPR }` — coderef to a subroutine whose name is given by `EXPR` (string or expression).
818 DynamicSubCodeRef(Box<Expr>),
819 Deref {
820 expr: Box<Expr>,
821 kind: Sigil,
822 },
823 ArrowDeref {
824 expr: Box<Expr>,
825 index: Box<Expr>,
826 kind: DerefKind,
827 },
828
829 // Operators
830 BinOp {
831 left: Box<Expr>,
832 op: BinOp,
833 right: Box<Expr>,
834 },
835 UnaryOp {
836 op: UnaryOp,
837 expr: Box<Expr>,
838 },
839 PostfixOp {
840 expr: Box<Expr>,
841 op: PostfixOp,
842 },
843 Assign {
844 target: Box<Expr>,
845 value: Box<Expr>,
846 },
847 CompoundAssign {
848 target: Box<Expr>,
849 op: BinOp,
850 value: Box<Expr>,
851 },
852 Ternary {
853 condition: Box<Expr>,
854 then_expr: Box<Expr>,
855 else_expr: Box<Expr>,
856 },
857
858 // Repetition operator `EXPR x N`.
859 //
860 // Perl distinguishes scalar string repetition (`"ab" x 3` → `"ababab"`) from
861 // list repetition (`(0) x 3` → `(0,0,0)`, `qw(a b) x 2` → `(a,b,a,b)`). The
862 // discriminator at parse time is the LHS shape: a top-level paren-list (or
863 // `qw(...)`) immediately before `x` is list-repeat; everything else is
864 // scalar-repeat. The parser sets `list_repeat=true` only in that case;
865 // `f(args) x N` (function-call parens, not list parens) stays scalar.
866 Repeat {
867 expr: Box<Expr>,
868 count: Box<Expr>,
869 list_repeat: bool,
870 },
871
872 // Range: `1..10` / `1...10` — in scalar context, `...` is the exclusive flip-flop (Perl `sed`-style).
873 // With step: `1..100:2` (1,3,5,...,99) or `100..1:-1` (100,99,...,1).
874 Range {
875 from: Box<Expr>,
876 to: Box<Expr>,
877 #[serde(default)]
878 exclusive: bool,
879 #[serde(default)]
880 step: Option<Box<Expr>>,
881 },
882
883 /// Slice subscript range with optional endpoints — Python-style `[start:stop:step]`.
884 /// Only emitted by the parser inside `@arr[...]` / `@h{...}` (and arrow-deref forms).
885 /// Open-ended forms: `[::-1]` (reverse), `[:N]`, `[N:]`, `[::M]`, `[N::M]`.
886 /// Compiler dispatches to typed integer-strict (array) or stringify-all (hash) ops.
887 SliceRange {
888 #[serde(default)]
889 from: Option<Box<Expr>>,
890 #[serde(default)]
891 to: Option<Box<Expr>>,
892 #[serde(default)]
893 step: Option<Box<Expr>>,
894 },
895
896 /// `my $x = EXPR` (or `our` / `state` / `local`) used as an *expression* —
897 /// e.g. inside `if (my $line = readline)` / `while (my $x = next())`.
898 /// Evaluation: declare each var in the current scope, evaluate the initializer
899 /// (or default to `undef`), then return the assigned value(s).
900 /// Distinct from `StmtKind::My` which only appears at statement level.
901 MyExpr {
902 keyword: String, // "my" / "our" / "state" / "local"
903 decls: Vec<VarDecl>,
904 },
905
906 // Function call
907 FuncCall {
908 name: String,
909 args: Vec<Expr>,
910 },
911
912 // Method call: $obj->method(args) or $obj->SUPER::method(args)
913 MethodCall {
914 object: Box<Expr>,
915 method: String,
916 args: Vec<Expr>,
917 /// When true, dispatch starts after the caller package in the linearized MRO.
918 #[serde(default)]
919 super_call: bool,
920 },
921 /// Call through a coderef or invokable scalar: `$cr->(...)` is [`MethodCall`]; this is
922 /// `$coderef(...)` or `&$coderef(...)` (the latter sets `ampersand`).
923 IndirectCall {
924 target: Box<Expr>,
925 args: Vec<Expr>,
926 #[serde(default)]
927 ampersand: bool,
928 /// True for unary `&$cr` with no `(...)` — Perl passes the caller's `@_` to the invoked sub.
929 #[serde(default)]
930 pass_caller_arglist: bool,
931 },
932 /// Limited typeglob: `*FOO` → handle name `FOO` for `open` / I/O.
933 Typeglob(String),
934 /// `*{ EXPR }` — typeglob slot by dynamic name (e.g. `*{$pkg . '::import'}`).
935 TypeglobExpr(Box<Expr>),
936
937 // Special forms
938 Print {
939 handle: Option<String>,
940 args: Vec<Expr>,
941 },
942 Say {
943 handle: Option<String>,
944 args: Vec<Expr>,
945 },
946 Printf {
947 handle: Option<String>,
948 args: Vec<Expr>,
949 },
950 Die(Vec<Expr>),
951 Warn(Vec<Expr>),
952
953 // Regex operations
954 Match {
955 expr: Box<Expr>,
956 pattern: String,
957 flags: String,
958 /// When true, `/g` uses Perl scalar semantics (one match per eval, updates `pos`).
959 scalar_g: bool,
960 #[serde(default = "default_delim")]
961 delim: char,
962 },
963 Substitution {
964 expr: Box<Expr>,
965 pattern: String,
966 replacement: String,
967 flags: String,
968 #[serde(default = "default_delim")]
969 delim: char,
970 },
971 Transliterate {
972 expr: Box<Expr>,
973 from: String,
974 to: String,
975 flags: String,
976 #[serde(default = "default_delim")]
977 delim: char,
978 },
979
980 // List operations
981 MapExpr {
982 block: Block,
983 list: Box<Expr>,
984 /// `flat_map { }` — peel one ARRAY ref from each iteration (stryke extension).
985 flatten_array_refs: bool,
986 /// `maps` / `flat_maps` — lazy iterator output (stryke); `map` / `flat_map` use `false`.
987 #[serde(default)]
988 stream: bool,
989 },
990 /// `map EXPR, LIST` — EXPR is evaluated in list context with `$_` set to each element.
991 MapExprComma {
992 expr: Box<Expr>,
993 list: Box<Expr>,
994 flatten_array_refs: bool,
995 #[serde(default)]
996 stream: bool,
997 },
998 GrepExpr {
999 block: Block,
1000 list: Box<Expr>,
1001 #[serde(default)]
1002 keyword: GrepBuiltinKeyword,
1003 },
1004 /// `grep EXPR, LIST` — EXPR is evaluated with `$_` set to each element (Perl list vs scalar context).
1005 GrepExprComma {
1006 expr: Box<Expr>,
1007 list: Box<Expr>,
1008 #[serde(default)]
1009 keyword: GrepBuiltinKeyword,
1010 },
1011 /// `sort BLOCK LIST`, `sort SUB LIST`, or `sort $coderef LIST` (Perl uses `$a`/`$b` in the comparator).
1012 SortExpr {
1013 cmp: Option<SortComparator>,
1014 list: Box<Expr>,
1015 },
1016 ReverseExpr(Box<Expr>),
1017 /// `rev EXPR` — always string-reverse (scalar reverse), stryke extension.
1018 Rev(Box<Expr>),
1019 JoinExpr {
1020 separator: Box<Expr>,
1021 list: Box<Expr>,
1022 },
1023 SplitExpr {
1024 pattern: Box<Expr>,
1025 string: Box<Expr>,
1026 limit: Option<Box<Expr>>,
1027 },
1028 /// `each { BLOCK } @list` — execute BLOCK for each element
1029 /// with `$_` aliased; void context (returns count in scalar context).
1030 ForEachExpr {
1031 block: Block,
1032 list: Box<Expr>,
1033 },
1034
1035 // Parallel extensions
1036 PMapExpr {
1037 block: Block,
1038 list: Box<Expr>,
1039 /// `pmap { } @list, progress => EXPR` — when truthy, print a progress bar on stderr.
1040 progress: Option<Box<Expr>>,
1041 /// `pflat_map { }` — flatten each block result like [`ExprKind::MapExpr`] (arrays expand);
1042 /// parallel output is stitched in **input order** (unlike plain `pmap`, which is unordered).
1043 flat_outputs: bool,
1044 /// `pmap_on $cluster { } @list` — fan out over SSH (`stryke --remote-worker`); `None` = local rayon.
1045 #[serde(default, skip_serializing_if = "Option::is_none")]
1046 on_cluster: Option<Box<Expr>>,
1047 /// `pmaps` / `pflat_maps` — streaming variant: returns a lazy iterator that processes
1048 /// chunks in parallel via rayon instead of eagerly collecting all results.
1049 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1050 stream: bool,
1051 },
1052 /// `pmap_chunked N { BLOCK } @list [, progress => EXPR]` — parallel map in batches of N.
1053 PMapChunkedExpr {
1054 chunk_size: Box<Expr>,
1055 block: Block,
1056 list: Box<Expr>,
1057 progress: Option<Box<Expr>>,
1058 },
1059 PGrepExpr {
1060 block: Block,
1061 list: Box<Expr>,
1062 /// `pgrep { } @list, progress => EXPR` — stderr progress bar when truthy.
1063 progress: Option<Box<Expr>>,
1064 /// `pgreps` — streaming variant: returns a lazy iterator.
1065 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1066 stream: bool,
1067 },
1068 /// `pfor { BLOCK } @list [, progress => EXPR]` — stderr progress bar when truthy.
1069 PForExpr {
1070 block: Block,
1071 list: Box<Expr>,
1072 progress: Option<Box<Expr>>,
1073 },
1074 /// `par { BLOCK } INPUT` — generic parallel-chunk wrapper. Splits INPUT
1075 /// (string → UTF-8-aligned byte chunks; array/list → element-chunks)
1076 /// into N pieces (N = available rayon threads), evaluates BLOCK per
1077 /// chunk in parallel with `$_` bound to the chunk, then concatenates
1078 /// results. Lets any whole-input op (`letters`, `chars`, `uc`, `freq`,
1079 /// regex `//g`, etc.) parallelize without needing a `pX` variant.
1080 ParExpr {
1081 block: Block,
1082 list: Box<Expr>,
1083 },
1084 /// `par_reduce { extract } [ { merge } ] INPUT` — chunk-extract-merge.
1085 /// Same chunker as `par {}`, but each chunk's result is reduced
1086 /// pairwise across chunks instead of concatenated.
1087 ///
1088 /// - One block: auto-merger picks based on result type (number → `+`,
1089 /// `hash<num>` → key-wise `+`, array → concat, string → concat).
1090 /// - Two blocks: explicit pairwise reducer with `$a`/`$b`.
1091 ParReduceExpr {
1092 extract_block: Block,
1093 reduce_block: Option<Block>,
1094 list: Box<Expr>,
1095 },
1096 /// Distributed counterpart of [`ExprKind::ParReduceExpr`]. Same chunk-block
1097 /// semantics (stages operate on `@_`) but chunks ship to a `RemoteCluster`
1098 /// of SSH workers via the existing `cluster::run_cluster` dispatcher.
1099 /// Built by `~d> on $cluster SOURCE stage1 stage2 ...`.
1100 DistReduceExpr {
1101 cluster: Box<Expr>,
1102 extract_block: Block,
1103 list: Box<Expr>,
1104 },
1105 /// `par_lines PATH, fn { ... } [, progress => EXPR]` — optional stderr progress (per line).
1106 ParLinesExpr {
1107 path: Box<Expr>,
1108 callback: Box<Expr>,
1109 progress: Option<Box<Expr>>,
1110 },
1111 /// `par_walk PATH, fn { ... } [, progress => EXPR]` — parallel recursive directory walk; `$_` is each path.
1112 ParWalkExpr {
1113 path: Box<Expr>,
1114 callback: Box<Expr>,
1115 progress: Option<Box<Expr>>,
1116 },
1117 /// `pwatch GLOB, fn { ... }` — notify-based watcher (evaluated by interpreter).
1118 PwatchExpr {
1119 path: Box<Expr>,
1120 callback: Box<Expr>,
1121 },
1122 /// `psort { } @list [, progress => EXPR]` — stderr progress when truthy (start/end phases).
1123 PSortExpr {
1124 cmp: Option<Block>,
1125 list: Box<Expr>,
1126 progress: Option<Box<Expr>>,
1127 },
1128 /// `reduce { $a + $b } @list` — sequential left fold over the list.
1129 /// `$a` is the accumulator; `$b` is the next list element.
1130 ReduceExpr {
1131 block: Block,
1132 list: Box<Expr>,
1133 },
1134 /// `preduce { $a + $b } @list` — parallel fold/reduce using rayon.
1135 /// $a and $b are set to the accumulator and current element.
1136 PReduceExpr {
1137 block: Block,
1138 list: Box<Expr>,
1139 /// `preduce { } @list, progress => EXPR` — stderr progress bar when truthy.
1140 progress: Option<Box<Expr>>,
1141 },
1142 /// `preduce_init EXPR, { $a / $b } @list` — parallel fold with explicit identity.
1143 /// Each chunk starts from a clone of `EXPR`; partials are merged (hash maps add counts per key;
1144 /// other types use the same block with `$a` / `$b` as partial accumulators). `$a` is the
1145 /// accumulator, `$b` is the next list element; `@_` is `($a, $b)` for `my ($acc, $item) = @_`.
1146 PReduceInitExpr {
1147 init: Box<Expr>,
1148 block: Block,
1149 list: Box<Expr>,
1150 progress: Option<Box<Expr>>,
1151 },
1152 /// `pmap_reduce { map } { reduce } @list` — fused parallel map + tree reduce (no full mapped array).
1153 PMapReduceExpr {
1154 map_block: Block,
1155 reduce_block: Block,
1156 list: Box<Expr>,
1157 progress: Option<Box<Expr>>,
1158 },
1159 /// `pcache { BLOCK } @list [, progress => EXPR]` — stderr progress bar when truthy.
1160 PcacheExpr {
1161 block: Block,
1162 list: Box<Expr>,
1163 progress: Option<Box<Expr>>,
1164 },
1165 /// `pselect($rx1, $rx2, ...)` — optional `timeout => SECS` for bounded wait.
1166 PselectExpr {
1167 receivers: Vec<Expr>,
1168 timeout: Option<Box<Expr>>,
1169 },
1170 /// `fan [COUNT] { BLOCK }` — execute BLOCK COUNT times in parallel (default COUNT = rayon pool size).
1171 /// `fan_cap [COUNT] { BLOCK }` — same, but return value is a **list** of each block's return value (index order).
1172 /// `$_` is set to the iteration index (0..COUNT-1).
1173 /// Optional `, progress => EXPR` — stderr progress bar (like `pmap`).
1174 FanExpr {
1175 count: Option<Box<Expr>>,
1176 block: Block,
1177 progress: Option<Box<Expr>>,
1178 capture: bool,
1179 },
1180
1181 /// `async { BLOCK }` — run BLOCK on a worker thread; returns a task handle.
1182 AsyncBlock {
1183 body: Block,
1184 },
1185 /// `spawn { BLOCK }` — same as [`ExprKind::AsyncBlock`] (Rust `thread::spawn`–style naming); join with `await`.
1186 SpawnBlock {
1187 body: Block,
1188 },
1189 /// `trace { BLOCK }` — print `mysync` scalar mutations to stderr (for parallel debugging).
1190 Trace {
1191 body: Block,
1192 },
1193 /// `timer { BLOCK }` — run BLOCK and return elapsed wall time in milliseconds (float).
1194 Timer {
1195 body: Block,
1196 },
1197 /// `bench { BLOCK } N` — run BLOCK `N` times (warmup + min/mean/p99 wall time, ms).
1198 Bench {
1199 body: Block,
1200 times: Box<Expr>,
1201 },
1202 /// `spinner "msg" { BLOCK }` — animated spinner on stderr while block runs.
1203 Spinner {
1204 message: Box<Expr>,
1205 body: Block,
1206 },
1207 /// `await EXPR` — join an async task, or return EXPR unchanged.
1208 Await(Box<Expr>),
1209 /// Read entire file as UTF-8 (`slurp $path`).
1210 Slurp(Box<Expr>),
1211 /// Run shell command and return structured output (`capture "cmd"`).
1212 Capture(Box<Expr>),
1213 /// `` `cmd` `` / `qx{cmd}` — run via `sh -c`, return **stdout as a string** (Perl); updates `$?`.
1214 Qx(Box<Expr>),
1215 /// Blocking HTTP GET (`fetch_url $url`).
1216 FetchUrl(Box<Expr>),
1217
1218 /// `pchannel()` — unbounded; `pchannel(N)` — bounded capacity N.
1219 Pchannel {
1220 capacity: Option<Box<Expr>>,
1221 },
1222
1223 // Array/Hash operations
1224 Push {
1225 array: Box<Expr>,
1226 values: Vec<Expr>,
1227 },
1228 Pop(Box<Expr>),
1229 Shift(Box<Expr>),
1230 Unshift {
1231 array: Box<Expr>,
1232 values: Vec<Expr>,
1233 },
1234 Splice {
1235 array: Box<Expr>,
1236 offset: Option<Box<Expr>>,
1237 length: Option<Box<Expr>>,
1238 replacement: Vec<Expr>,
1239 },
1240 Delete(Box<Expr>),
1241 Exists(Box<Expr>),
1242 Keys(Box<Expr>),
1243 Values(Box<Expr>),
1244 Each(Box<Expr>),
1245
1246 // String operations
1247 Chomp(Box<Expr>),
1248 Chop(Box<Expr>),
1249 Length(Box<Expr>),
1250 Substr {
1251 string: Box<Expr>,
1252 offset: Box<Expr>,
1253 length: Option<Box<Expr>>,
1254 replacement: Option<Box<Expr>>,
1255 },
1256 Index {
1257 string: Box<Expr>,
1258 substr: Box<Expr>,
1259 position: Option<Box<Expr>>,
1260 },
1261 Rindex {
1262 string: Box<Expr>,
1263 substr: Box<Expr>,
1264 position: Option<Box<Expr>>,
1265 },
1266 Sprintf {
1267 format: Box<Expr>,
1268 args: Vec<Expr>,
1269 },
1270
1271 // Numeric
1272 Abs(Box<Expr>),
1273 Int(Box<Expr>),
1274 Sqrt(Box<Expr>),
1275 Sin(Box<Expr>),
1276 Cos(Box<Expr>),
1277 Atan2 {
1278 y: Box<Expr>,
1279 x: Box<Expr>,
1280 },
1281 Exp(Box<Expr>),
1282 Log(Box<Expr>),
1283 /// `rand` with optional upper bound (none = Perl default 1.0).
1284 Rand(Option<Box<Expr>>),
1285 /// `srand` with optional seed (none = time-based).
1286 Srand(Option<Box<Expr>>),
1287 Hex(Box<Expr>),
1288 Oct(Box<Expr>),
1289
1290 // Case
1291 Lc(Box<Expr>),
1292 Uc(Box<Expr>),
1293 Lcfirst(Box<Expr>),
1294 Ucfirst(Box<Expr>),
1295
1296 /// Unicode case fold (Perl `fc`).
1297 Fc(Box<Expr>),
1298 /// DES-style `crypt` (see libc `crypt(3)` on Unix; empty on other targets).
1299 Crypt {
1300 plaintext: Box<Expr>,
1301 salt: Box<Expr>,
1302 },
1303 /// `pos` — optional scalar lvalue target (`None` = `$_`).
1304 Pos(Option<Box<Expr>>),
1305 /// `study` — hint for repeated matching; returns byte length of the string.
1306 Study(Box<Expr>),
1307
1308 // Type
1309 Defined(Box<Expr>),
1310 Ref(Box<Expr>),
1311 ScalarContext(Box<Expr>),
1312
1313 // Char
1314 Chr(Box<Expr>),
1315 Ord(Box<Expr>),
1316
1317 // I/O
1318 /// `open my $fh` — only valid as [`ExprKind::Open::handle`]; declares `$fh` and binds the handle.
1319 OpenMyHandle {
1320 name: String,
1321 },
1322 Open {
1323 handle: Box<Expr>,
1324 mode: Box<Expr>,
1325 file: Option<Box<Expr>>,
1326 },
1327 Close(Box<Expr>),
1328 ReadLine(Option<String>),
1329 Eof(Option<Box<Expr>>),
1330
1331 Opendir {
1332 handle: Box<Expr>,
1333 path: Box<Expr>,
1334 },
1335 Readdir(Box<Expr>),
1336 Closedir(Box<Expr>),
1337 Rewinddir(Box<Expr>),
1338 Telldir(Box<Expr>),
1339 Seekdir {
1340 handle: Box<Expr>,
1341 position: Box<Expr>,
1342 },
1343
1344 // File tests
1345 FileTest {
1346 op: char,
1347 expr: Box<Expr>,
1348 },
1349
1350 // System
1351 System(Vec<Expr>),
1352 Exec(Vec<Expr>),
1353 Eval(Box<Expr>),
1354 Do(Box<Expr>),
1355 Require(Box<Expr>),
1356 Exit(Option<Box<Expr>>),
1357 Chdir(Box<Expr>),
1358 Mkdir {
1359 path: Box<Expr>,
1360 mode: Option<Box<Expr>>,
1361 },
1362 Unlink(Vec<Expr>),
1363 Rename {
1364 old: Box<Expr>,
1365 new: Box<Expr>,
1366 },
1367 /// `chmod MODE, @files` — first expr is mode, rest are paths.
1368 Chmod(Vec<Expr>),
1369 /// `chown UID, GID, @files` — first two are uid/gid, rest are paths.
1370 Chown(Vec<Expr>),
1371
1372 Stat(Box<Expr>),
1373 Lstat(Box<Expr>),
1374 Link {
1375 old: Box<Expr>,
1376 new: Box<Expr>,
1377 },
1378 Symlink {
1379 old: Box<Expr>,
1380 new: Box<Expr>,
1381 },
1382 Readlink(Box<Expr>),
1383 /// `files` / `files DIR` — list file names in a directory (default: `.`).
1384 Files(Vec<Expr>),
1385 /// `filesf` / `filesf DIR` / `f` — list only regular file names in a directory (default: `.`).
1386 Filesf(Vec<Expr>),
1387 /// `fr DIR` — list only regular file names recursively (default: `.`).
1388 FilesfRecursive(Vec<Expr>),
1389 /// `dirs` / `dirs DIR` / `d` — list subdirectory names in a directory (default: `.`).
1390 Dirs(Vec<Expr>),
1391 /// `dr DIR` — list subdirectory paths recursively (default: `.`).
1392 DirsRecursive(Vec<Expr>),
1393 /// `sym_links` / `sym_links DIR` — list symlink names in a directory (default: `.`).
1394 SymLinks(Vec<Expr>),
1395 /// `sockets` / `sockets DIR` — list Unix socket names in a directory (default: `.`).
1396 Sockets(Vec<Expr>),
1397 /// `pipes` / `pipes DIR` — list named-pipe (FIFO) names in a directory (default: `.`).
1398 Pipes(Vec<Expr>),
1399 /// `block_devices` / `block_devices DIR` — list block device names in a directory (default: `.`).
1400 BlockDevices(Vec<Expr>),
1401 /// `char_devices` / `char_devices DIR` — list character device names in a directory (default: `.`).
1402 CharDevices(Vec<Expr>),
1403 /// `exe` / `exe DIR` — list executable file names in a directory (default: `.`).
1404 Executables(Vec<Expr>),
1405 Glob(Vec<Expr>),
1406 /// Parallel recursive glob (rayon); same patterns as `glob`, different walk strategy.
1407 /// Optional `, progress => EXPR` — stderr progress bar (one tick per pattern).
1408 GlobPar {
1409 args: Vec<Expr>,
1410 progress: Option<Box<Expr>>,
1411 },
1412 /// `par_sed PATTERN, REPLACEMENT, FILES... [, progress => EXPR]` — parallel in-place regex replace per file (`g` semantics).
1413 ParSed {
1414 args: Vec<Expr>,
1415 progress: Option<Box<Expr>>,
1416 },
1417
1418 // Bless
1419 Bless {
1420 ref_expr: Box<Expr>,
1421 class: Option<Box<Expr>>,
1422 },
1423
1424 // Caller
1425 Caller(Option<Box<Expr>>),
1426
1427 // Wantarray
1428 Wantarray,
1429
1430 // List / Context
1431 List(Vec<Expr>),
1432
1433 // Postfix if/unless/while/until/for
1434 PostfixIf {
1435 expr: Box<Expr>,
1436 condition: Box<Expr>,
1437 },
1438 PostfixUnless {
1439 expr: Box<Expr>,
1440 condition: Box<Expr>,
1441 },
1442 PostfixWhile {
1443 expr: Box<Expr>,
1444 condition: Box<Expr>,
1445 },
1446 PostfixUntil {
1447 expr: Box<Expr>,
1448 condition: Box<Expr>,
1449 },
1450 PostfixForeach {
1451 expr: Box<Expr>,
1452 list: Box<Expr>,
1453 },
1454
1455 /// `retry { BLOCK } times => N [, backoff => linear|exponential|none]` — re-run block until success or attempts exhausted.
1456 RetryBlock {
1457 body: Block,
1458 times: Box<Expr>,
1459 backoff: RetryBackoff,
1460 },
1461 /// `rate_limit(MAX, WINDOW) { BLOCK }` — sliding window: at most MAX runs per WINDOW (e.g. `"1s"`).
1462 /// `slot` is assigned at parse time for per-site state in the interpreter.
1463 RateLimitBlock {
1464 slot: u32,
1465 max: Box<Expr>,
1466 window: Box<Expr>,
1467 body: Block,
1468 },
1469 /// `every(INTERVAL) { BLOCK }` — repeat BLOCK forever with sleep (INTERVAL like `"5s"` or seconds).
1470 EveryBlock {
1471 interval: Box<Expr>,
1472 body: Block,
1473 },
1474 /// `gen { ... yield ... }` — lazy generator; call `->next` for each value.
1475 GenBlock {
1476 body: Block,
1477 },
1478 /// `yield EXPR` — only valid inside `gen { }` (and propagates through control flow).
1479 Yield(Box<Expr>),
1480
1481 /// `match (EXPR) { PATTERN => EXPR, ... }` — first matching arm; bindings scoped to the arm body.
1482 AlgebraicMatch {
1483 subject: Box<Expr>,
1484 arms: Vec<MatchArm>,
1485 },
1486}
1487
1488#[derive(Debug, Clone, Serialize, Deserialize)]
1489pub enum StringPart {
1490 Literal(String),
1491 ScalarVar(String),
1492 ArrayVar(String),
1493 Expr(Expr),
1494}
1495
1496#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1497pub enum DerefKind {
1498 Array,
1499 Hash,
1500 Call,
1501}
1502
1503#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1504pub enum BinOp {
1505 Add,
1506 Sub,
1507 Mul,
1508 Div,
1509 Mod,
1510 Pow,
1511 Concat,
1512 NumEq,
1513 NumNe,
1514 NumLt,
1515 NumGt,
1516 NumLe,
1517 NumGe,
1518 Spaceship,
1519 StrEq,
1520 StrNe,
1521 StrLt,
1522 StrGt,
1523 StrLe,
1524 StrGe,
1525 StrCmp,
1526 LogAnd,
1527 LogOr,
1528 DefinedOr,
1529 BitAnd,
1530 BitOr,
1531 BitXor,
1532 ShiftLeft,
1533 ShiftRight,
1534 LogAndWord,
1535 LogOrWord,
1536 BindMatch,
1537 BindNotMatch,
1538}
1539
1540#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1541pub enum UnaryOp {
1542 Negate,
1543 LogNot,
1544 BitNot,
1545 LogNotWord,
1546 PreIncrement,
1547 PreDecrement,
1548 Ref,
1549}
1550
1551#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1552pub enum PostfixOp {
1553 Increment,
1554 Decrement,
1555}
1556
1557#[cfg(test)]
1558mod tests {
1559 use super::*;
1560
1561 #[test]
1562 fn binop_deref_kind_distinct() {
1563 assert_ne!(BinOp::Add, BinOp::Sub);
1564 assert_eq!(DerefKind::Call, DerefKind::Call);
1565 }
1566
1567 #[test]
1568 fn sigil_variants_exhaustive_in_tests() {
1569 let all = [Sigil::Scalar, Sigil::Array, Sigil::Hash];
1570 assert_eq!(all.len(), 3);
1571 }
1572
1573 #[test]
1574 fn program_empty_roundtrip_clone() {
1575 let p = Program { statements: vec![] };
1576 assert!(p.clone().statements.is_empty());
1577 }
1578
1579 #[test]
1580 fn program_serializes_to_json() {
1581 let p = crate::parse("1+2;").expect("parse");
1582 let s = serde_json::to_string(&p).expect("json");
1583 assert!(s.contains("\"statements\""));
1584 assert!(s.contains("BinOp"));
1585 }
1586}