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}
504
505#[derive(Debug, Clone, Copy, Eq, PartialEq)]
506pub enum LogicalOp {
507 And,
508 Or,
509}
510
511#[derive(Debug, Clone, PartialEq)]
512pub enum Expr {
513 Literal(Value),
514 Var(String),
515 Env(String),
518 KeyPath {
519 base: String,
520 keys: Vec<String>,
521 },
522 List(Vec<Expr>),
523 Map(Vec<(String, Expr)>),
524 Call {
525 name: String,
526 args: Vec<Expr>,
527 },
528 Compare {
529 op: CompareOp,
530 left: Box<Expr>,
531 right: Box<Expr>,
532 },
533 Not(Box<Expr>),
534 Logical {
535 op: LogicalOp,
536 left: Box<Expr>,
537 right: Box<Expr>,
538 },
539}
540
541#[derive(Debug, Clone, PartialEq)]
542pub struct Step {
543 pub guard: Option<GuardExpr>,
544 pub kind: StepKind,
545 pub scope_enter: usize,
546 pub scope_exit: usize,
547}
548
549#[derive(Debug, Clone, Eq, PartialEq)]
550pub enum WorkspaceTarget {
551 Snapshot,
552 Local,
553}
554
555fn platform_matches(target: PlatformGuard) -> bool {
556 #[allow(clippy::disallowed_macros)]
557 match target {
558 PlatformGuard::Unix => cfg!(unix),
559 PlatformGuard::Windows => cfg!(windows),
560 PlatformGuard::Macos => cfg!(target_os = "macos"),
561 PlatformGuard::Linux => cfg!(target_os = "linux"),
562 }
563}
564
565pub trait EnvLookup {
566 fn get_env(&self, key: &str) -> Option<&str>;
567}
568
569impl EnvLookup for HashMap<String, String> {
570 fn get_env(&self, key: &str) -> Option<&str> {
571 self.get(key).map(|s| s.as_str())
572 }
573}
574
575impl EnvLookup for Arc<HashMap<String, String>> {
576 fn get_env(&self, key: &str) -> Option<&str> {
577 (**self).get_env(key)
578 }
579}
580
581pub fn guard_allows(guard: &Guard, env: &impl EnvLookup) -> bool {
582 match guard {
583 Guard::Platform { target } => platform_matches(*target),
584 Guard::EnvExists { key } => env.get_env(key).map(|v| !v.is_empty()).unwrap_or(false),
585 Guard::EnvEquals { key, value } => env
586 .get_env(key)
587 .map(|v| v == value.as_str())
588 .unwrap_or(false),
589 Guard::StaticBool { value } => value.parse::<bool>().unwrap_or(false),
590 }
591}
592
593pub fn guard_expr_allows(expr: &GuardExpr, env: &impl EnvLookup) -> bool {
594 match expr {
595 GuardExpr::Predicate(guard) => guard_allows(guard, env),
596 GuardExpr::All(children) => children.iter().all(|g| guard_expr_allows(g, env)),
597 GuardExpr::Or(children) => children.iter().any(|g| guard_expr_allows(g, env)),
598 GuardExpr::Not(child) => !guard_expr_allows(child, env),
599 }
600}
601
602pub fn guard_option_allows(expr: Option<&GuardExpr>, env: &impl EnvLookup) -> bool {
603 match expr {
604 Some(e) => guard_expr_allows(e, env),
605 None => true,
606 }
607}
608
609use std::fmt;
610
611impl fmt::Display for PlatformGuard {
612 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
613 match self {
614 PlatformGuard::Unix => write!(f, "unix"),
615 PlatformGuard::Windows => write!(f, "windows"),
616 PlatformGuard::Macos => write!(f, "macos"),
617 PlatformGuard::Linux => write!(f, "linux"),
618 }
619 }
620}
621
622impl fmt::Display for Guard {
623 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
624 match self {
625 Guard::Platform { target } => write!(f, "{}", target),
626 Guard::EnvExists { key } => write!(f, "env:{}", key),
627 Guard::EnvEquals { key, value } => write!(f, "eq(env:{}, {})", key, value),
628 Guard::StaticBool { value } => write!(f, "bool:{}", value),
629 }
630 }
631}
632
633impl fmt::Display for WorkspaceTarget {
634 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
635 match self {
636 WorkspaceTarget::Snapshot => write!(f, "SNAPSHOT"),
637 WorkspaceTarget::Local => write!(f, "LOCAL"),
638 }
639 }
640}
641
642impl fmt::Display for Value {
643 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
644 match self {
645 Value::String(s) => write!(f, "\"{}\"", s),
646 Value::Int(i) => write!(f, "{}", i),
647 Value::Float(v) => write!(f, "{}", v),
648 Value::Pipe(n) => write!(f, "pipe:{}", n),
649 Value::Duration(d) => write!(f, "{}", crate::command::format_duration(d)),
650 Value::Path(p) => write!(f, "{}", p.display()),
651 Value::List(items) => {
652 write!(f, "[")?;
653 for (i, item) in items.iter().enumerate() {
654 if i > 0 {
655 write!(f, ", ")?;
656 }
657 write!(f, "{}", item)?;
658 }
659 write!(f, "]")
660 }
661 Value::Map(map) => {
662 write!(f, "{{")?;
663 for (i, (k, v)) in map.iter().enumerate() {
664 if i > 0 {
665 write!(f, ", ")?;
666 }
667 write!(f, "{}: {}", k, v)?;
668 }
669 write!(f, "}}")
670 }
671 Value::Bool(b) => write!(f, "{}", b),
672 Value::TaskHandle(id) => write!(f, "task#{}", id),
673 }
674 }
675}
676
677impl fmt::Display for Expr {
678 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
679 match self {
680 Expr::Literal(v) => write!(f, "{}", v),
681 Expr::Var(name) => write!(f, "${}", name),
682 Expr::Env(key) => write!(f, "env:{}", key),
683 Expr::KeyPath { base, keys } => {
684 write!(f, "${}", base)?;
685 for key in keys {
686 write!(f, ".{}", key)?;
687 }
688 Ok(())
689 }
690 Expr::Call { name, args } => {
691 write!(f, "{}(", name)?;
692 for (i, arg) in args.iter().enumerate() {
693 if i > 0 {
694 write!(f, ", ")?;
695 }
696 write!(f, "{}", arg)?;
697 }
698 write!(f, ")")
699 }
700 Expr::List(items) => {
701 write!(f, "[")?;
702 for (i, item) in items.iter().enumerate() {
703 if i > 0 {
704 write!(f, ", ")?;
705 }
706 write!(f, "{}", item)?;
707 }
708 write!(f, "]")
709 }
710 Expr::Map(entries) => {
711 write!(f, "{{")?;
712 for (i, (key, val)) in entries.iter().enumerate() {
713 if i > 0 {
714 write!(f, ", ")?;
715 }
716 write!(f, "\"{}\": {}", key, val)?;
717 }
718 write!(f, "}}")
719 }
720 Expr::Compare { op, left, right } => {
721 write!(f, "{} {} {}", left, op, right)
722 }
723 Expr::Not(inner) => {
724 match inner.as_ref() {
727 Expr::Compare { .. } => write!(f, "!({})", inner),
728 _ => write!(f, "!{}", inner),
729 }
730 }
731 Expr::Logical { op, left, right } => {
732 write!(f, "({} {} {})", left, op, right)
733 }
734 }
735 }
736}
737
738impl fmt::Display for CompareOp {
739 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
740 match self {
741 CompareOp::Eq => write!(f, "=="),
742 CompareOp::Ne => write!(f, "!="),
743 }
744 }
745}
746
747impl fmt::Display for LogicalOp {
748 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
749 match self {
750 LogicalOp::And => write!(f, "&&"),
751 LogicalOp::Or => write!(f, "||"),
752 }
753 }
754}
755
756enum GuardDisplayContext {
757 Root,
758 InAnyArg,
759 InNot,
760 InAll,
761}
762
763impl GuardExpr {
764 fn fmt_with_ctx(&self, f: &mut fmt::Formatter<'_>, ctx: GuardDisplayContext) -> fmt::Result {
765 match self {
766 GuardExpr::Predicate(guard) => write!(f, "{}", guard),
767 GuardExpr::All(children) => {
768 let wrap = matches!(
769 ctx,
770 GuardDisplayContext::InAnyArg | GuardDisplayContext::InNot
771 ) && children.len() > 1;
772 if wrap {
773 write!(f, "(")?;
774 }
775 for (i, child) in children.iter().enumerate() {
776 if i > 0 {
777 write!(f, ", ")?;
778 }
779 child.fmt_with_ctx(f, GuardDisplayContext::InAll)?;
780 }
781 if wrap {
782 write!(f, ")")?;
783 }
784 Ok(())
785 }
786 GuardExpr::Or(children) => {
787 write!(f, "any(")?;
788 for (i, child) in children.iter().enumerate() {
789 if i > 0 {
790 write!(f, ", ")?;
791 }
792 child.fmt_with_ctx(f, GuardDisplayContext::InAnyArg)?;
793 }
794 write!(f, ")")
795 }
796 GuardExpr::Not(child) => {
797 write!(f, "not(")?;
798 child.fmt_with_ctx(f, GuardDisplayContext::InNot)?;
799 write!(f, ")")
800 }
801 }
802 }
803}
804
805impl fmt::Display for GuardExpr {
806 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
807 self.fmt_with_ctx(f, GuardDisplayContext::Root)
808 }
809}
810
811impl fmt::Display for Step {
812 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
813 if let Some(expr) = &self.guard {
814 write!(f, "[{}] ", expr)?;
815 }
816 write!(f, "{}", self.kind)
817 }
818}