1use std::collections::HashMap;
2use std::sync::Arc;
3
4pub use crate::commands::StepKind;
5
6#[derive(Copy, Clone, Debug, Eq, PartialEq)]
7pub enum Command {
8 InheritEnv,
9 Workdir,
10 Workspace,
11 Env,
12 Echo,
13 Run,
14 Copy,
15 WithIo,
16 CopyGit,
17 HashSha256,
18 Symlink,
19 Mkdir,
20 Ls,
21 Cwd,
22 Read,
23 ReadLine,
24 Write,
25 Append,
26 Expand,
27 AssertFile,
28 AssertDir,
29 AssertAbsent,
30 AssertStdout,
31 Exit,
32 Async,
33 Timeout,
34 Sleep,
35}
36
37pub const COMMANDS: &[Command] = &[
38 Command::InheritEnv,
39 Command::Workdir,
40 Command::Workspace,
41 Command::Env,
42 Command::Echo,
43 Command::Run,
44 Command::Copy,
45 Command::WithIo,
46 Command::CopyGit,
47 Command::HashSha256,
48 Command::Symlink,
49 Command::Mkdir,
50 Command::Ls,
51 Command::Cwd,
52 Command::Read,
53 Command::ReadLine,
54 Command::Write,
55 Command::Append,
56 Command::Expand,
57 Command::AssertFile,
58 Command::AssertDir,
59 Command::AssertAbsent,
60 Command::AssertStdout,
61 Command::Exit,
62 Command::Timeout,
63 Command::Sleep,
64];
65
66impl Command {
67 pub const fn as_str(self) -> &'static str {
68 match self {
69 Command::InheritEnv => "INHERIT_ENV",
70 Command::Workdir => "WORKDIR",
71 Command::Workspace => "WORKSPACE",
72 Command::Env => "ENV",
73 Command::Echo => "ECHO",
74 Command::Run => "RUN",
75 Command::Copy => "COPY",
76 Command::WithIo => "WITH_IO",
77 Command::CopyGit => "COPY_GIT",
78 Command::HashSha256 => "HASH_SHA256",
79 Command::Symlink => "SYMLINK",
80 Command::Mkdir => "MKDIR",
81 Command::Ls => "LS",
82 Command::Cwd => "CWD",
83 Command::Read => "READ",
84 Command::ReadLine => "READ_LINE",
85 Command::Write => "WRITE",
86 Command::Append => "APPEND",
87 Command::Expand => "EXPAND",
88 Command::AssertFile => "ASSERT_FILE",
89 Command::AssertDir => "ASSERT_DIR",
90 Command::AssertAbsent => "ASSERT_ABSENT",
91 Command::AssertStdout => "ASSERT_STDOUT",
92 Command::Exit => "EXIT",
93 Command::Async => "ASYNC",
94 Command::Timeout => "TIMEOUT",
95 Command::Sleep => "SLEEP",
96 }
97 }
98
99 pub const fn syntax(self) -> &'static str {
100 match self {
101 Command::InheritEnv => "INHERIT_ENV [KEY1, KEY2, ...]",
102 Command::Workdir => "WORKDIR <path>",
103 Command::Workspace => "WORKSPACE SNAPSHOT|LOCAL",
104 Command::Env => "ENV KEY=value",
105 Command::Echo => "ECHO <message>",
106 Command::Run => "RUN <command...> | RUN [\"exe\", \"arg\", ...]",
107 Command::Copy => "COPY [--from-current-workspace] <from> <to>",
108 Command::CopyGit => "COPY_GIT [--include-dirty] <rev> <src> <dst>",
109 Command::WithIo => "WITH_IO [bindings] [command | { block }]",
110 Command::HashSha256 => "HASH_SHA256 <path>",
111 Command::Symlink => "SYMLINK <from> <to>",
112 Command::Mkdir => "MKDIR <path>",
113 Command::Ls => "LS [<path>]",
114 Command::Cwd => "CWD",
115 Command::Read => "READ [<path>]",
116 Command::ReadLine => "READ_LINE $var",
117 Command::Write => "WRITE <path> [<contents>]",
118 Command::Append => "APPEND <path> [<contents>]",
119 Command::Expand => "EXPAND [<path>] [<KEY=val> ...]",
120 Command::AssertFile => "ASSERT_FILE [--hash <sha256>] <path> [<expected>]",
121 Command::AssertDir => "ASSERT_DIR <path>",
122 Command::AssertAbsent => "ASSERT_ABSENT <path>",
123 Command::AssertStdout => "ASSERT_STDOUT <substring>",
124 Command::Exit => "EXIT <code>",
125 Command::Async => "ASYNC <command...> | ASYNC { <commands> }",
126 Command::Timeout => {
127 "TIMEOUT <duration> <command...> | TIMEOUT <duration> { <commands> }"
128 }
129 Command::Sleep => "SLEEP <duration>",
130 }
131 }
132
133 pub const fn expects_inner_command(self) -> bool {
134 matches!(self, Command::WithIo | Command::Async | Command::Timeout)
135 }
136
137 pub fn parse(s: &str) -> Option<Self> {
138 match s {
139 "INHERIT_ENV" => Some(Command::InheritEnv),
140 "WORKDIR" => Some(Command::Workdir),
141 "WORKSPACE" => Some(Command::Workspace),
142 "ENV" => Some(Command::Env),
143 "ECHO" => Some(Command::Echo),
144 "RUN" => Some(Command::Run),
145 "COPY" => Some(Command::Copy),
146 "WITH_IO" => Some(Command::WithIo),
147 "COPY_GIT" => Some(Command::CopyGit),
148 "HASH_SHA256" => Some(Command::HashSha256),
149 "SYMLINK" => Some(Command::Symlink),
150 "MKDIR" => Some(Command::Mkdir),
151 "LS" => Some(Command::Ls),
152 "CWD" => Some(Command::Cwd),
153 "READ" => Some(Command::Read),
154 "READ_LINE" => Some(Command::ReadLine),
155 "WRITE" => Some(Command::Write),
156 "APPEND" => Some(Command::Append),
157 "EXPAND" => Some(Command::Expand),
158 "ASSERT_FILE" => Some(Command::AssertFile),
159 "ASSERT_DIR" => Some(Command::AssertDir),
160 "ASSERT_ABSENT" => Some(Command::AssertAbsent),
161 "ASSERT_STDOUT" => Some(Command::AssertStdout),
162 "EXIT" => Some(Command::Exit),
163 "ASYNC" => Some(Command::Async),
164 "TIMEOUT" => Some(Command::Timeout),
165 "SLEEP" => Some(Command::Sleep),
166 _ => None,
167 }
168 }
169}
170
171#[derive(Copy, Clone, Debug, Eq, PartialEq)]
172pub enum PlatformGuard {
173 Unix,
174 Windows,
175 Macos,
176 Linux,
177}
178
179#[derive(Debug, Clone, Eq, PartialEq)]
180pub enum Guard {
181 Platform { target: PlatformGuard },
182 EnvExists { key: String },
183 EnvEquals { key: String, value: String },
184 StaticBool { value: String },
185}
186
187#[derive(Debug, Clone, Eq, PartialEq)]
188pub enum GuardExpr {
189 Predicate(Guard),
190 All(Vec<GuardExpr>),
191 Or(Vec<GuardExpr>),
192 Not(Box<GuardExpr>),
193}
194
195impl GuardExpr {
196 pub fn all(exprs: Vec<GuardExpr>) -> GuardExpr {
197 let mut flat = Vec::new();
198 for expr in exprs {
199 match expr {
200 GuardExpr::All(children) => flat.extend(children),
201 other => flat.push(other),
202 }
203 }
204 match flat.len() {
205 0 => panic!("GuardExpr::all requires at least one expression"),
206 1 => flat.into_iter().next().unwrap(),
207 _ => GuardExpr::All(flat),
208 }
209 }
210
211 pub fn or(exprs: Vec<GuardExpr>) -> GuardExpr {
212 let mut flat = Vec::new();
213 for expr in exprs {
214 match expr {
215 GuardExpr::Or(children) => flat.extend(children),
216 other => flat.push(other),
217 }
218 }
219 match flat.len() {
220 0 => panic!("GuardExpr::or requires at least one expression"),
221 1 => flat.into_iter().next().unwrap(),
222 _ => GuardExpr::Or(flat),
223 }
224 }
225
226 pub fn invert(expr: GuardExpr) -> GuardExpr {
227 match expr {
228 GuardExpr::Not(inner) => *inner,
229 other => GuardExpr::Not(Box::new(other)),
230 }
231 }
232}
233
234impl std::ops::Not for GuardExpr {
235 type Output = GuardExpr;
236
237 fn not(self) -> GuardExpr {
238 match self {
239 GuardExpr::Not(inner) => *inner,
240 other => GuardExpr::Not(Box::new(other)),
241 }
242 }
243}
244
245impl From<Guard> for GuardExpr {
246 fn from(guard: Guard) -> Self {
247 GuardExpr::Predicate(guard)
248 }
249}
250
251#[derive(Debug, Clone, PartialEq)]
253pub enum Arg {
254 String(String, bool),
258 Expr(Expr),
260 Parts(Vec<ArgPart>),
264}
265
266#[derive(Debug, Clone, PartialEq)]
268pub enum ArgPart {
269 Text(String, bool),
272 Expr(Expr),
274}
275
276impl Arg {
277 pub fn as_str(&self) -> &str {
278 match self {
279 Arg::String(s, _) => s,
280 Arg::Expr(_) | Arg::Parts(_) => "",
283 }
284 }
285
286 pub fn render(&self) -> String {
290 match self {
291 Arg::String(s, _) => s.clone(),
292 Arg::Expr(e) => e.to_string(),
293 Arg::Parts(parts) => parts.iter().map(ArgPart::render).collect(),
294 }
295 }
296
297 pub fn is_quoted(&self) -> bool {
298 matches!(self, Arg::String(_, true))
299 }
300}
301
302impl ArgPart {
303 pub fn render(&self) -> String {
304 match self {
305 ArgPart::Text(s, _) => s.clone(),
306 ArgPart::Expr(e) => e.to_string(),
307 }
308 }
309}
310
311impl From<String> for Arg {
312 fn from(s: String) -> Self {
313 Arg::String(s, false)
314 }
315}
316
317impl From<&str> for Arg {
318 fn from(s: &str) -> Self {
319 Arg::String(s.to_string(), false)
320 }
321}
322
323impl std::fmt::Display for Arg {
324 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
325 match self {
326 Arg::String(s, _) => write!(f, "{}", s),
327 Arg::Expr(e) => write!(f, "{}", e),
328 Arg::Parts(parts) => {
329 for part in parts {
330 match part {
331 ArgPart::Text(s, _) => write!(f, "{}", s)?,
332 ArgPart::Expr(e) => write!(f, "{}", e)?,
333 }
334 }
335 Ok(())
336 }
337 }
338 }
339}
340
341impl AsRef<str> for Arg {
342 fn as_ref(&self) -> &str {
343 self.as_str()
344 }
345}
346
347impl PartialEq<str> for Arg {
348 fn eq(&self, other: &str) -> bool {
349 self.as_str() == other
350 }
351}
352
353impl PartialEq<&str> for Arg {
354 fn eq(&self, other: &&str) -> bool {
355 self.as_str() == *other
356 }
357}
358
359#[derive(Debug, Clone, Copy, Eq, PartialEq)]
360pub enum IoStream {
361 Stdin,
362 Stdout,
363 Stderr,
364}
365
366#[derive(Debug, Clone, Eq, PartialEq)]
367pub struct IoBinding {
368 pub stream: IoStream,
369 pub pipe: Option<PipeTarget>,
370}
371
372#[derive(Debug, Clone, Eq, PartialEq)]
376pub enum PipeTarget {
377 Name(String),
378 Var(String),
379}
380
381#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
382pub enum TypeKind {
383 String,
384 Int,
385 Float,
386 Bool,
387 Pipe,
388 List,
389 Map,
390 Handle,
391 Duration,
392 Path,
393}
394
395impl TypeKind {
396 pub const CANONICAL: &[TypeKind] = &[
397 TypeKind::String,
398 TypeKind::Int,
399 TypeKind::Float,
400 TypeKind::Bool,
401 TypeKind::Pipe,
402 TypeKind::List,
403 TypeKind::Map,
404 TypeKind::Handle,
405 TypeKind::Duration,
406 TypeKind::Path,
407 ];
408
409 pub fn label(&self) -> &'static str {
413 match self {
414 TypeKind::String => "STRING",
415 TypeKind::Int => "INT",
416 TypeKind::Float => "FLOAT",
417 TypeKind::Bool => "BOOL",
418 TypeKind::Pipe => "PIPE",
419 TypeKind::List => "LIST",
420 TypeKind::Map => "MAP",
421 TypeKind::Handle => "HANDLE",
422 TypeKind::Duration => "DURATION",
423 TypeKind::Path => "PATH",
424 }
425 }
426
427 pub fn doc(&self) -> Option<(String, &'static str)> {
430 let body = match self {
431 TypeKind::String => {
432 "Arbitrary text. Quotes keep exact bytes, lone `$var` evaluates, `{{ ... }}` interpolates."
433 }
434 TypeKind::Int => "64-bit signed integer, e.g. an exit code.",
435 TypeKind::Float => "64-bit float, e.g. a ratio.",
436 TypeKind::Bool => "Boolean `true` or `false`.",
437 TypeKind::Pipe => {
438 "Named script pipe. Validity is checked against the pipe registry at coercion time."
439 }
440 TypeKind::List => "Ordered list of values.",
441 TypeKind::Map => "String-keyed map of values.",
442 TypeKind::Handle => "Background ASYNC task handle for AWAIT/CANCEL.",
443 TypeKind::Duration => {
444 "Positive time span: `500ms`, `10s`, `2m`, `1h`; bare number means seconds."
445 }
446 TypeKind::Path => "Workspace path, resolved against cwd and guarded against escape.",
447 };
448 Some((format!("Value type: {}", self.label()), body))
449 }
450
451 pub fn anchor(&self) -> String {
455 format!("value-type-{}", self.label().to_lowercase())
456 }
457}
458
459impl std::str::FromStr for TypeKind {
460 type Err = anyhow::Error;
461 fn from_str(s: &str) -> Result<Self, Self::Err> {
462 if let Some(kind) = Self::CANONICAL.iter().find(|k| k.label() == s) {
463 return Ok(*kind);
464 }
465 let inventory = Self::CANONICAL
466 .iter()
467 .map(|k| k.label())
468 .collect::<Vec<_>>()
469 .join(", ");
470 anyhow::bail!("unknown type `{s}`; expected one of {inventory}")
471 }
472}
473
474impl std::fmt::Display for TypeKind {
475 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
476 write!(f, "{}", self.label())
477 }
478}
479
480#[derive(Debug, Clone, PartialEq)]
481pub enum Value {
482 String(String),
483 Int(i64),
484 Float(f64),
485 List(Vec<Value>),
486 Map(std::collections::BTreeMap<String, Value>),
487 Bool(bool),
488 Pipe(String), Duration(std::time::Duration),
490 #[allow(clippy::disallowed_types)]
493 Path(std::path::PathBuf),
494 TaskHandle(u64),
497}
498
499#[derive(Debug, Clone, Copy, Eq, PartialEq)]
500pub enum CompareOp {
501 Eq,
502 Ne,
503 Lt,
504 Le,
505 Gt,
506 Ge,
507}
508
509#[derive(Debug, Clone, Copy, Eq, PartialEq)]
510pub enum ArithOp {
511 Add,
512 Sub,
513 Mul,
514 Div,
515}
516
517#[derive(Debug, Clone, PartialEq)]
526pub enum MathOp {
527 PushConst(Value),
528 LoadVar(String),
529 LoadEnv(String),
530 LoadKeyPath { base: String, keys: Vec<String> },
531 Call { name: String, arity: usize },
532 Inspect(String),
533 Neg,
534 Add,
535 Sub,
536 Mul,
537 Div,
538 Lt,
539 Le,
540 Gt,
541 Ge,
542 Eq,
543 Ne,
544}
545
546#[derive(Debug, Clone, Copy, Eq, PartialEq)]
547pub enum LogicalOp {
548 And,
549 Or,
550}
551
552#[derive(Debug, Clone, PartialEq)]
553pub enum Expr {
554 Literal(Value),
555 Var(String),
556 Env(String),
559 KeyPath {
560 base: String,
561 keys: Vec<String>,
562 },
563 List(Vec<Expr>),
564 Map(Vec<(String, Expr)>),
565 Call {
566 name: String,
567 args: Vec<Expr>,
568 },
569 Compare {
570 op: CompareOp,
571 left: Box<Expr>,
572 right: Box<Expr>,
573 },
574 Arithmetic {
575 op: ArithOp,
576 left: Box<Expr>,
577 right: Box<Expr>,
578 },
579 CompiledMath(Vec<MathOp>),
582 UnsignedIntBoundary(u64),
586 Not(Box<Expr>),
587 Logical {
588 op: LogicalOp,
589 left: Box<Expr>,
590 right: Box<Expr>,
591 },
592}
593
594#[derive(Debug, Clone, PartialEq)]
595pub struct Step {
596 pub guard: Option<GuardExpr>,
597 pub kind: StepKind,
598 pub scope_enter: usize,
599 pub scope_exit: usize,
600}
601
602#[derive(Debug, Clone, Eq, PartialEq)]
603pub enum WorkspaceTarget {
604 Snapshot,
605 Local,
606}
607
608fn platform_matches(target: PlatformGuard) -> bool {
609 #[allow(clippy::disallowed_macros)]
610 match target {
611 PlatformGuard::Unix => cfg!(unix),
612 PlatformGuard::Windows => cfg!(windows),
613 PlatformGuard::Macos => cfg!(target_os = "macos"),
614 PlatformGuard::Linux => cfg!(target_os = "linux"),
615 }
616}
617
618pub trait EnvLookup {
619 fn get_env(&self, key: &str) -> Option<&str>;
620}
621
622impl EnvLookup for HashMap<String, String> {
623 fn get_env(&self, key: &str) -> Option<&str> {
624 self.get(key).map(|s| s.as_str())
625 }
626}
627
628impl EnvLookup for Arc<HashMap<String, String>> {
629 fn get_env(&self, key: &str) -> Option<&str> {
630 (**self).get_env(key)
631 }
632}
633
634pub fn guard_allows(guard: &Guard, env: &impl EnvLookup) -> bool {
635 match guard {
636 Guard::Platform { target } => platform_matches(*target),
637 Guard::EnvExists { key } => env.get_env(key).map(|v| !v.is_empty()).unwrap_or(false),
638 Guard::EnvEquals { key, value } => env
639 .get_env(key)
640 .map(|v| v == value.as_str())
641 .unwrap_or(false),
642 Guard::StaticBool { value } => value.parse::<bool>().unwrap_or(false),
643 }
644}
645
646pub fn guard_expr_allows(expr: &GuardExpr, env: &impl EnvLookup) -> bool {
647 match expr {
648 GuardExpr::Predicate(guard) => guard_allows(guard, env),
649 GuardExpr::All(children) => children.iter().all(|g| guard_expr_allows(g, env)),
650 GuardExpr::Or(children) => children.iter().any(|g| guard_expr_allows(g, env)),
651 GuardExpr::Not(child) => !guard_expr_allows(child, env),
652 }
653}
654
655pub fn guard_option_allows(expr: Option<&GuardExpr>, env: &impl EnvLookup) -> bool {
656 match expr {
657 Some(e) => guard_expr_allows(e, env),
658 None => true,
659 }
660}
661
662use std::fmt;
663
664impl fmt::Display for PlatformGuard {
665 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
666 match self {
667 PlatformGuard::Unix => write!(f, "unix"),
668 PlatformGuard::Windows => write!(f, "windows"),
669 PlatformGuard::Macos => write!(f, "macos"),
670 PlatformGuard::Linux => write!(f, "linux"),
671 }
672 }
673}
674
675impl fmt::Display for Guard {
676 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
677 match self {
678 Guard::Platform { target } => write!(f, "{}", target),
679 Guard::EnvExists { key } => write!(f, "env:{}", key),
680 Guard::EnvEquals { key, value } => write!(f, "eq(env:{}, {})", key, value),
681 Guard::StaticBool { value } => write!(f, "bool:{}", value),
682 }
683 }
684}
685
686impl fmt::Display for WorkspaceTarget {
687 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
688 match self {
689 WorkspaceTarget::Snapshot => write!(f, "SNAPSHOT"),
690 WorkspaceTarget::Local => write!(f, "LOCAL"),
691 }
692 }
693}
694
695impl fmt::Display for Value {
696 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
697 match self {
698 Value::String(s) => write!(f, "\"{}\"", s),
699 Value::Int(i) => write!(f, "{}", i),
700 Value::Float(v) => write!(f, "{}", v),
701 Value::Pipe(n) => write!(f, "pipe:{}", n),
702 Value::Duration(d) => write!(f, "{}", crate::command::format_duration(d)),
703 Value::Path(p) => write!(f, "{}", p.display()),
704 Value::List(items) => {
705 write!(f, "[")?;
706 for (i, item) in items.iter().enumerate() {
707 if i > 0 {
708 write!(f, ", ")?;
709 }
710 write!(f, "{}", item)?;
711 }
712 write!(f, "]")
713 }
714 Value::Map(map) => {
715 write!(f, "{{")?;
716 for (i, (k, v)) in map.iter().enumerate() {
717 if i > 0 {
718 write!(f, ", ")?;
719 }
720 write!(f, "{}: {}", k, v)?;
721 }
722 write!(f, "}}")
723 }
724 Value::Bool(b) => write!(f, "{}", b),
725 Value::TaskHandle(id) => write!(f, "task#{}", id),
726 }
727 }
728}
729
730impl fmt::Display for Expr {
731 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
732 match self {
733 Expr::Literal(v) => write!(f, "{}", v),
734 Expr::Var(name) => write!(f, "${}", name),
735 Expr::Env(key) => write!(f, "env:{}", key),
736 Expr::KeyPath { base, keys } => {
737 write!(f, "${}", base)?;
738 for key in keys {
739 write!(f, ".{}", key)?;
740 }
741 Ok(())
742 }
743 Expr::Call { name, args } => {
744 write!(f, "{}(", name)?;
745 for (i, arg) in args.iter().enumerate() {
746 if i > 0 {
747 write!(f, ", ")?;
748 }
749 write!(f, "{}", arg)?;
750 }
751 write!(f, ")")
752 }
753 Expr::List(items) => {
754 write!(f, "[")?;
755 for (i, item) in items.iter().enumerate() {
756 if i > 0 {
757 write!(f, ", ")?;
758 }
759 write!(f, "{}", item)?;
760 }
761 write!(f, "]")
762 }
763 Expr::Map(entries) => {
764 write!(f, "{{")?;
765 for (i, (key, val)) in entries.iter().enumerate() {
766 if i > 0 {
767 write!(f, ", ")?;
768 }
769 write!(f, "\"{}\": {}", key, val)?;
770 }
771 write!(f, "}}")
772 }
773 Expr::Compare { op, left, right } => {
774 write!(f, "{} {} {}", left, op, right)
775 }
776 Expr::Arithmetic { op, left, right } => {
777 write!(f, "({} {} {})", left, op, right)
778 }
779 Expr::CompiledMath(ops) => {
780 write!(f, "{}", format_compiled_math(ops))
781 }
782 Expr::UnsignedIntBoundary(n) => write!(f, "{}", n),
783 Expr::Not(inner) => {
784 match inner.as_ref() {
787 Expr::Compare { .. } | Expr::Arithmetic { .. } | Expr::CompiledMath(_) => {
788 write!(f, "!({})", inner)
789 }
790 _ => write!(f, "!{}", inner),
791 }
792 }
793 Expr::Logical { op, left, right } => {
794 write!(f, "({} {} {})", left, op, right)
795 }
796 }
797 }
798}
799
800impl fmt::Display for CompareOp {
801 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
802 match self {
803 CompareOp::Eq => write!(f, "=="),
804 CompareOp::Ne => write!(f, "!="),
805 CompareOp::Lt => write!(f, "<"),
806 CompareOp::Le => write!(f, "<="),
807 CompareOp::Gt => write!(f, ">"),
808 CompareOp::Ge => write!(f, ">="),
809 }
810 }
811}
812
813impl fmt::Display for ArithOp {
814 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
815 match self {
816 ArithOp::Add => write!(f, "+"),
817 ArithOp::Sub => write!(f, "-"),
818 ArithOp::Mul => write!(f, "*"),
819 ArithOp::Div => write!(f, "/"),
820 }
821 }
822}
823
824fn format_compiled_math(ops: &[MathOp]) -> String {
829 let mut stack: Vec<String> = Vec::new();
830 for op in ops {
831 match op {
832 MathOp::PushConst(v) => stack.push(format!("{}", v)),
833 MathOp::LoadVar(name) => stack.push(format!("${}", name)),
834 MathOp::LoadEnv(key) => stack.push(format!("env:{}", key)),
835 MathOp::LoadKeyPath { base, keys } => {
836 let mut s = format!("${}", base);
837 for key in keys {
838 s.push('.');
839 s.push_str(key);
840 }
841 stack.push(s);
842 }
843 MathOp::Call { name, arity } => {
844 let mut args = Vec::new();
845 for _ in 0..*arity {
846 args.push(stack.pop().unwrap_or_else(|| "<underflow>".to_string()));
847 }
848 args.reverse();
849 stack.push(format!("{}({})", name, args.join(", ")));
850 }
851 MathOp::Inspect(name) => stack.push(format!("INSPECT(${})", name)),
852 MathOp::Neg => {
853 let inner = stack.pop().unwrap_or_else(|| "<underflow>".to_string());
854 stack.push(format!("(-{})", inner));
855 }
856 MathOp::Add => push_bin(&mut stack, "+"),
857 MathOp::Sub => push_bin(&mut stack, "-"),
858 MathOp::Mul => push_bin(&mut stack, "*"),
859 MathOp::Div => push_bin(&mut stack, "/"),
860 MathOp::Lt => push_bin(&mut stack, "<"),
861 MathOp::Le => push_bin(&mut stack, "<="),
862 MathOp::Gt => push_bin(&mut stack, ">"),
863 MathOp::Ge => push_bin(&mut stack, ">="),
864 MathOp::Eq => push_bin(&mut stack, "=="),
865 MathOp::Ne => push_bin(&mut stack, "!="),
866 }
867 }
868 if stack.len() == 1 {
869 let mut items = stack;
870 items.pop().unwrap_or_else(|| "<empty>".to_string())
871 } else {
872 stack.join(" ")
873 }
874}
875
876fn push_bin(stack: &mut Vec<String>, op: &str) {
877 let right = stack.pop().unwrap_or_else(|| "<underflow>".to_string());
878 let left = stack.pop().unwrap_or_else(|| "<underflow>".to_string());
879 stack.push(format!("({} {} {})", left, op, right));
880}
881
882impl fmt::Display for LogicalOp {
883 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
884 match self {
885 LogicalOp::And => write!(f, "&&"),
886 LogicalOp::Or => write!(f, "||"),
887 }
888 }
889}
890
891enum GuardDisplayContext {
892 Root,
893 InAnyArg,
894 InNot,
895 InAll,
896}
897
898impl GuardExpr {
899 fn fmt_with_ctx(&self, f: &mut fmt::Formatter<'_>, ctx: GuardDisplayContext) -> fmt::Result {
900 match self {
901 GuardExpr::Predicate(guard) => write!(f, "{}", guard),
902 GuardExpr::All(children) => {
903 let wrap = matches!(
904 ctx,
905 GuardDisplayContext::InAnyArg | GuardDisplayContext::InNot
906 ) && children.len() > 1;
907 if wrap {
908 write!(f, "(")?;
909 }
910 for (i, child) in children.iter().enumerate() {
911 if i > 0 {
912 write!(f, ", ")?;
913 }
914 child.fmt_with_ctx(f, GuardDisplayContext::InAll)?;
915 }
916 if wrap {
917 write!(f, ")")?;
918 }
919 Ok(())
920 }
921 GuardExpr::Or(children) => {
922 write!(f, "any(")?;
923 for (i, child) in children.iter().enumerate() {
924 if i > 0 {
925 write!(f, ", ")?;
926 }
927 child.fmt_with_ctx(f, GuardDisplayContext::InAnyArg)?;
928 }
929 write!(f, ")")
930 }
931 GuardExpr::Not(child) => {
932 write!(f, "not(")?;
933 child.fmt_with_ctx(f, GuardDisplayContext::InNot)?;
934 write!(f, ")")
935 }
936 }
937 }
938}
939
940impl fmt::Display for GuardExpr {
941 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
942 self.fmt_with_ctx(f, GuardDisplayContext::Root)
943 }
944}
945
946impl fmt::Display for Step {
947 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
948 if let Some(expr) = &self.guard {
949 write!(f, "[{}] ", expr)?;
950 }
951 write!(f, "{}", self.kind)
952 }
953}