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