use std::collections::HashMap;
use std::sync::Arc;
pub use crate::commands::{AssertTarget, StepKind};
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Command {
InheritEnv,
Workdir,
Workspace,
Env,
Echo,
Run,
Copy,
WithIo,
CopyGit,
HashSha256,
Symlink,
Mkdir,
Ls,
Cwd,
Read,
ReadLine,
Write,
Append,
Expand,
AssertEq,
AssertContains,
Exit,
Async,
Timeout,
Sleep,
}
pub const COMMANDS: &[Command] = &[
Command::InheritEnv,
Command::Workdir,
Command::Workspace,
Command::Env,
Command::Echo,
Command::Run,
Command::Copy,
Command::WithIo,
Command::CopyGit,
Command::HashSha256,
Command::Symlink,
Command::Mkdir,
Command::Ls,
Command::Cwd,
Command::Read,
Command::ReadLine,
Command::Write,
Command::Append,
Command::Expand,
Command::AssertEq,
Command::AssertContains,
Command::Exit,
Command::Timeout,
Command::Sleep,
];
impl Command {
pub const fn as_str(self) -> &'static str {
match self {
Command::InheritEnv => "INHERIT_ENV",
Command::Workdir => "WORKDIR",
Command::Workspace => "WORKSPACE",
Command::Env => "ENV",
Command::Echo => "ECHO",
Command::Run => "RUN",
Command::Copy => "COPY",
Command::WithIo => "WITH_IO",
Command::CopyGit => "COPY_GIT",
Command::HashSha256 => "HASH_SHA256",
Command::Symlink => "SYMLINK",
Command::Mkdir => "MKDIR",
Command::Ls => "LS",
Command::Cwd => "CWD",
Command::Read => "READ",
Command::ReadLine => "READ_LINE",
Command::Write => "WRITE",
Command::Append => "APPEND",
Command::Expand => "EXPAND",
Command::AssertEq => "ASSERT_EQ",
Command::AssertContains => "ASSERT_CONTAINS",
Command::Exit => "EXIT",
Command::Async => "ASYNC",
Command::Timeout => "TIMEOUT",
Command::Sleep => "SLEEP",
}
}
pub const fn syntax(self) -> &'static str {
match self {
Command::InheritEnv => "INHERIT_ENV [KEY1, KEY2, ...]",
Command::Workdir => "WORKDIR <path>",
Command::Workspace => "WORKSPACE SNAPSHOT|LOCAL",
Command::Env => "ENV KEY=value",
Command::Echo => "ECHO <message>",
Command::Run => "RUN <command...> | RUN [\"exe\", \"arg\", ...]",
Command::Copy => "COPY [--from-current-workspace] <from> <to>",
Command::CopyGit => "COPY_GIT [--include-dirty] <rev> <src> <dst>",
Command::WithIo => "WITH_IO [bindings] [command | { block }]",
Command::HashSha256 => "HASH_SHA256 <path>",
Command::Symlink => "SYMLINK <from> <to>",
Command::Mkdir => "MKDIR <path>",
Command::Ls => "LS [<path>]",
Command::Cwd => "CWD",
Command::Read => "READ [<path>]",
Command::ReadLine => "READ_LINE $var",
Command::Write => "WRITE <path> [<contents>]",
Command::Append => "APPEND <path> [<contents>]",
Command::Expand => "EXPAND [<path>] [<KEY=val> ...]",
Command::AssertEq => "ASSERT_EQ [--hash <sha256>] <actual> <expected>",
Command::AssertContains => "ASSERT_CONTAINS <haystack> <needle>",
Command::Exit => "EXIT <code>",
Command::Async => "ASYNC <command...> | ASYNC { <commands> }",
Command::Timeout => {
"TIMEOUT <duration> <command...> | TIMEOUT <duration> { <commands> }"
}
Command::Sleep => "SLEEP <duration>",
}
}
pub const fn expects_inner_command(self) -> bool {
matches!(self, Command::WithIo | Command::Async | Command::Timeout)
}
pub fn parse(s: &str) -> Option<Self> {
match s {
"INHERIT_ENV" => Some(Command::InheritEnv),
"WORKDIR" => Some(Command::Workdir),
"WORKSPACE" => Some(Command::Workspace),
"ENV" => Some(Command::Env),
"ECHO" => Some(Command::Echo),
"RUN" => Some(Command::Run),
"COPY" => Some(Command::Copy),
"WITH_IO" => Some(Command::WithIo),
"COPY_GIT" => Some(Command::CopyGit),
"HASH_SHA256" => Some(Command::HashSha256),
"SYMLINK" => Some(Command::Symlink),
"MKDIR" => Some(Command::Mkdir),
"LS" => Some(Command::Ls),
"CWD" => Some(Command::Cwd),
"READ" => Some(Command::Read),
"READ_LINE" => Some(Command::ReadLine),
"WRITE" => Some(Command::Write),
"APPEND" => Some(Command::Append),
"EXPAND" => Some(Command::Expand),
"ASSERT_EQ" => Some(Command::AssertEq),
"ASSERT_CONTAINS" => Some(Command::AssertContains),
"EXIT" => Some(Command::Exit),
"ASYNC" => Some(Command::Async),
"TIMEOUT" => Some(Command::Timeout),
"SLEEP" => Some(Command::Sleep),
_ => None,
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum PlatformGuard {
Unix,
Windows,
Macos,
Linux,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Guard {
Platform { target: PlatformGuard },
EnvExists { key: String },
EnvEquals { key: String, value: String },
StaticBool { value: String },
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum GuardExpr {
Predicate(Guard),
All(Vec<GuardExpr>),
Or(Vec<GuardExpr>),
Not(Box<GuardExpr>),
}
impl GuardExpr {
pub fn all(exprs: Vec<GuardExpr>) -> GuardExpr {
let mut flat = Vec::new();
for expr in exprs {
match expr {
GuardExpr::All(children) => flat.extend(children),
other => flat.push(other),
}
}
match flat.len() {
0 => panic!("GuardExpr::all requires at least one expression"),
1 => flat.into_iter().next().unwrap(),
_ => GuardExpr::All(flat),
}
}
pub fn or(exprs: Vec<GuardExpr>) -> GuardExpr {
let mut flat = Vec::new();
for expr in exprs {
match expr {
GuardExpr::Or(children) => flat.extend(children),
other => flat.push(other),
}
}
match flat.len() {
0 => panic!("GuardExpr::or requires at least one expression"),
1 => flat.into_iter().next().unwrap(),
_ => GuardExpr::Or(flat),
}
}
pub fn invert(expr: GuardExpr) -> GuardExpr {
match expr {
GuardExpr::Not(inner) => *inner,
other => GuardExpr::Not(Box::new(other)),
}
}
}
impl std::ops::Not for GuardExpr {
type Output = GuardExpr;
fn not(self) -> GuardExpr {
match self {
GuardExpr::Not(inner) => *inner,
other => GuardExpr::Not(Box::new(other)),
}
}
}
impl From<Guard> for GuardExpr {
fn from(guard: Guard) -> Self {
GuardExpr::Predicate(guard)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Arg {
String(String, bool),
Expr(Expr),
Parts(Vec<ArgPart>),
}
#[derive(Debug, Clone, PartialEq)]
pub enum ArgPart {
Text(String, bool),
Expr(Expr),
}
impl Arg {
pub fn as_str(&self) -> &str {
match self {
Arg::String(s, _) => s,
Arg::Expr(_) | Arg::Parts(_) => "",
}
}
pub fn render(&self) -> String {
match self {
Arg::String(s, _) => s.clone(),
Arg::Expr(e) => e.to_string(),
Arg::Parts(parts) => parts.iter().map(ArgPart::render).collect(),
}
}
pub fn is_quoted(&self) -> bool {
matches!(self, Arg::String(_, true))
}
}
impl ArgPart {
pub fn render(&self) -> String {
match self {
ArgPart::Text(s, _) => s.clone(),
ArgPart::Expr(e) => e.to_string(),
}
}
}
impl From<String> for Arg {
fn from(s: String) -> Self {
Arg::String(s, false)
}
}
impl From<&str> for Arg {
fn from(s: &str) -> Self {
Arg::String(s.to_string(), false)
}
}
impl std::fmt::Display for Arg {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Arg::String(s, _) => write!(f, "{}", s),
Arg::Expr(e) => write!(f, "{}", e),
Arg::Parts(parts) => {
for part in parts {
match part {
ArgPart::Text(s, _) => write!(f, "{}", s)?,
ArgPart::Expr(e) => write!(f, "{}", e)?,
}
}
Ok(())
}
}
}
}
impl AsRef<str> for Arg {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl PartialEq<str> for Arg {
fn eq(&self, other: &str) -> bool {
self.as_str() == other
}
}
impl PartialEq<&str> for Arg {
fn eq(&self, other: &&str) -> bool {
self.as_str() == *other
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum IoStream {
Stdin,
Stdout,
Stderr,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct IoBinding {
pub stream: IoStream,
pub pipe: Option<PipeTarget>,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum PipeTarget {
Name(String),
Var(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TypeKind {
String,
Int,
Float,
Bool,
Pipe,
List,
Map,
Handle,
Duration,
Path,
}
impl TypeKind {
pub const CANONICAL: &[TypeKind] = &[
TypeKind::String,
TypeKind::Int,
TypeKind::Float,
TypeKind::Bool,
TypeKind::Pipe,
TypeKind::List,
TypeKind::Map,
TypeKind::Handle,
TypeKind::Duration,
TypeKind::Path,
];
pub fn label(&self) -> &'static str {
match self {
TypeKind::String => "STRING",
TypeKind::Int => "INT",
TypeKind::Float => "FLOAT",
TypeKind::Bool => "BOOL",
TypeKind::Pipe => "PIPE",
TypeKind::List => "LIST",
TypeKind::Map => "MAP",
TypeKind::Handle => "HANDLE",
TypeKind::Duration => "DURATION",
TypeKind::Path => "PATH",
}
}
pub fn doc(&self) -> Option<(String, &'static str)> {
let body = match self {
TypeKind::String => {
"Arbitrary text. Quotes keep exact bytes, lone `$var` evaluates, `{{ ... }}` interpolates."
}
TypeKind::Int => "64-bit signed integer, e.g. an exit code.",
TypeKind::Float => "64-bit float, e.g. a ratio.",
TypeKind::Bool => "Boolean `true` or `false`.",
TypeKind::Pipe => {
"Named script pipe. Validity is checked against the pipe registry at coercion time."
}
TypeKind::List => "Ordered list of values.",
TypeKind::Map => "String-keyed map of values.",
TypeKind::Handle => "Background ASYNC task handle for AWAIT/CANCEL.",
TypeKind::Duration => {
"Positive time span: `500ms`, `10s`, `2m`, `1h`; bare number means seconds."
}
TypeKind::Path => "Workspace path, resolved against cwd and guarded against escape.",
};
Some((format!("Value type: {}", self.label()), body))
}
pub fn anchor(&self) -> String {
format!("value-type-{}", self.label().to_lowercase())
}
}
impl std::str::FromStr for TypeKind {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(kind) = Self::CANONICAL.iter().find(|k| k.label() == s) {
return Ok(*kind);
}
let inventory = Self::CANONICAL
.iter()
.map(|k| k.label())
.collect::<Vec<_>>()
.join(", ");
anyhow::bail!("unknown type `{s}`; expected one of {inventory}")
}
}
impl std::fmt::Display for TypeKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.label())
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
String(String),
Int(i64),
Float(f64),
List(Vec<Value>),
Map(std::collections::BTreeMap<String, Value>),
Bool(bool),
Pipe(String), Duration(std::time::Duration),
#[allow(clippy::disallowed_types)]
Path(std::path::PathBuf),
TaskHandle(u64),
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum CompareOp {
Eq,
Ne,
Lt,
Le,
Gt,
Ge,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum ArithOp {
Add,
Sub,
Mul,
Div,
}
#[derive(Debug, Clone, PartialEq)]
pub enum MathOp {
PushConst(Value),
LoadVar(String),
LoadEnv(String),
LoadKeyPath { base: String, keys: Vec<String> },
Call { name: String, arity: usize },
Inspect(String),
Neg,
Add,
Sub,
Mul,
Div,
Lt,
Le,
Gt,
Ge,
Eq,
Ne,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum LogicalOp {
And,
Or,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Expr {
Literal(Value),
Var(String),
Env(String),
KeyPath {
base: String,
keys: Vec<String>,
},
List(Vec<Expr>),
Map(Vec<(String, Expr)>),
Call {
name: String,
args: Vec<Expr>,
},
Compare {
op: CompareOp,
left: Box<Expr>,
right: Box<Expr>,
},
Arithmetic {
op: ArithOp,
left: Box<Expr>,
right: Box<Expr>,
},
CompiledMath(Vec<MathOp>),
UnsignedIntBoundary(u64),
Not(Box<Expr>),
Logical {
op: LogicalOp,
left: Box<Expr>,
right: Box<Expr>,
},
}
#[derive(Debug, Clone, PartialEq)]
pub struct Step {
pub guard: Option<GuardExpr>,
pub kind: StepKind,
pub scope_enter: usize,
pub scope_exit: usize,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum WorkspaceTarget {
Snapshot,
Local,
}
fn platform_matches(target: PlatformGuard) -> bool {
#[allow(clippy::disallowed_macros)]
match target {
PlatformGuard::Unix => cfg!(unix),
PlatformGuard::Windows => cfg!(windows),
PlatformGuard::Macos => cfg!(target_os = "macos"),
PlatformGuard::Linux => cfg!(target_os = "linux"),
}
}
pub trait EnvLookup {
fn get_env(&self, key: &str) -> Option<&str>;
}
impl EnvLookup for HashMap<String, String> {
fn get_env(&self, key: &str) -> Option<&str> {
self.get(key).map(|s| s.as_str())
}
}
impl EnvLookup for Arc<HashMap<String, String>> {
fn get_env(&self, key: &str) -> Option<&str> {
(**self).get_env(key)
}
}
pub fn guard_allows(guard: &Guard, env: &impl EnvLookup) -> bool {
match guard {
Guard::Platform { target } => platform_matches(*target),
Guard::EnvExists { key } => env.get_env(key).map(|v| !v.is_empty()).unwrap_or(false),
Guard::EnvEquals { key, value } => env
.get_env(key)
.map(|v| v == value.as_str())
.unwrap_or(false),
Guard::StaticBool { value } => value.parse::<bool>().unwrap_or(false),
}
}
pub fn guard_expr_allows(expr: &GuardExpr, env: &impl EnvLookup) -> bool {
match expr {
GuardExpr::Predicate(guard) => guard_allows(guard, env),
GuardExpr::All(children) => children.iter().all(|g| guard_expr_allows(g, env)),
GuardExpr::Or(children) => children.iter().any(|g| guard_expr_allows(g, env)),
GuardExpr::Not(child) => !guard_expr_allows(child, env),
}
}
pub fn guard_option_allows(expr: Option<&GuardExpr>, env: &impl EnvLookup) -> bool {
match expr {
Some(e) => guard_expr_allows(e, env),
None => true,
}
}
use std::fmt;
impl fmt::Display for PlatformGuard {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PlatformGuard::Unix => write!(f, "unix"),
PlatformGuard::Windows => write!(f, "windows"),
PlatformGuard::Macos => write!(f, "macos"),
PlatformGuard::Linux => write!(f, "linux"),
}
}
}
impl fmt::Display for Guard {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Guard::Platform { target } => write!(f, "{}", target),
Guard::EnvExists { key } => write!(f, "env:{}", key),
Guard::EnvEquals { key, value } => write!(f, "eq(env:{}, {})", key, value),
Guard::StaticBool { value } => write!(f, "bool:{}", value),
}
}
}
impl fmt::Display for WorkspaceTarget {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
WorkspaceTarget::Snapshot => write!(f, "SNAPSHOT"),
WorkspaceTarget::Local => write!(f, "LOCAL"),
}
}
}
impl fmt::Display for Value {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Value::String(s) => write!(f, "\"{}\"", s),
Value::Int(i) => write!(f, "{}", i),
Value::Float(v) => write!(f, "{}", v),
Value::Pipe(n) => write!(f, "pipe:{}", n),
Value::Duration(d) => write!(f, "{}", crate::command::format_duration(d)),
Value::Path(p) => write!(f, "{}", p.display()),
Value::List(items) => {
write!(f, "[")?;
for (i, item) in items.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{}", item)?;
}
write!(f, "]")
}
Value::Map(map) => {
write!(f, "{{")?;
for (i, (k, v)) in map.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{}: {}", k, v)?;
}
write!(f, "}}")
}
Value::Bool(b) => write!(f, "{}", b),
Value::TaskHandle(id) => write!(f, "task#{}", id),
}
}
}
impl fmt::Display for Expr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Expr::Literal(v) => write!(f, "{}", v),
Expr::Var(name) => write!(f, "${}", name),
Expr::Env(key) => write!(f, "env:{}", key),
Expr::KeyPath { base, keys } => {
write!(f, "${}", base)?;
for key in keys {
write!(f, ".{}", key)?;
}
Ok(())
}
Expr::Call { name, args } => {
write!(f, "{}(", name)?;
for (i, arg) in args.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{}", arg)?;
}
write!(f, ")")
}
Expr::List(items) => {
write!(f, "[")?;
for (i, item) in items.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{}", item)?;
}
write!(f, "]")
}
Expr::Map(entries) => {
write!(f, "{{")?;
for (i, (key, val)) in entries.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "\"{}\": {}", key, val)?;
}
write!(f, "}}")
}
Expr::Compare { op, left, right } => {
write!(f, "{} {} {}", left, op, right)
}
Expr::Arithmetic { op, left, right } => {
write!(f, "({} {} {})", left, op, right)
}
Expr::CompiledMath(ops) => {
write!(f, "{}", format_compiled_math(ops))
}
Expr::UnsignedIntBoundary(n) => write!(f, "{}", n),
Expr::Not(inner) => {
match inner.as_ref() {
Expr::Compare { .. } | Expr::Arithmetic { .. } | Expr::CompiledMath(_) => {
write!(f, "!({})", inner)
}
_ => write!(f, "!{}", inner),
}
}
Expr::Logical { op, left, right } => {
write!(f, "({} {} {})", left, op, right)
}
}
}
}
impl fmt::Display for CompareOp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CompareOp::Eq => write!(f, "=="),
CompareOp::Ne => write!(f, "!="),
CompareOp::Lt => write!(f, "<"),
CompareOp::Le => write!(f, "<="),
CompareOp::Gt => write!(f, ">"),
CompareOp::Ge => write!(f, ">="),
}
}
}
impl fmt::Display for ArithOp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ArithOp::Add => write!(f, "+"),
ArithOp::Sub => write!(f, "-"),
ArithOp::Mul => write!(f, "*"),
ArithOp::Div => write!(f, "/"),
}
}
}
fn format_compiled_math(ops: &[MathOp]) -> String {
let mut stack: Vec<String> = Vec::new();
for op in ops {
match op {
MathOp::PushConst(v) => stack.push(format!("{}", v)),
MathOp::LoadVar(name) => stack.push(format!("${}", name)),
MathOp::LoadEnv(key) => stack.push(format!("env:{}", key)),
MathOp::LoadKeyPath { base, keys } => {
let mut s = format!("${}", base);
for key in keys {
s.push('.');
s.push_str(key);
}
stack.push(s);
}
MathOp::Call { name, arity } => {
let mut args = Vec::new();
for _ in 0..*arity {
args.push(stack.pop().unwrap_or_else(|| "<underflow>".to_string()));
}
args.reverse();
stack.push(format!("{}({})", name, args.join(", ")));
}
MathOp::Inspect(name) => stack.push(format!("INSPECT(${})", name)),
MathOp::Neg => {
let inner = stack.pop().unwrap_or_else(|| "<underflow>".to_string());
stack.push(format!("(-{})", inner));
}
MathOp::Add => push_bin(&mut stack, "+"),
MathOp::Sub => push_bin(&mut stack, "-"),
MathOp::Mul => push_bin(&mut stack, "*"),
MathOp::Div => push_bin(&mut stack, "/"),
MathOp::Lt => push_bin(&mut stack, "<"),
MathOp::Le => push_bin(&mut stack, "<="),
MathOp::Gt => push_bin(&mut stack, ">"),
MathOp::Ge => push_bin(&mut stack, ">="),
MathOp::Eq => push_bin(&mut stack, "=="),
MathOp::Ne => push_bin(&mut stack, "!="),
}
}
if stack.len() == 1 {
let mut items = stack;
items.pop().unwrap_or_else(|| "<empty>".to_string())
} else {
stack.join(" ")
}
}
fn push_bin(stack: &mut Vec<String>, op: &str) {
let right = stack.pop().unwrap_or_else(|| "<underflow>".to_string());
let left = stack.pop().unwrap_or_else(|| "<underflow>".to_string());
stack.push(format!("({} {} {})", left, op, right));
}
impl fmt::Display for LogicalOp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LogicalOp::And => write!(f, "&&"),
LogicalOp::Or => write!(f, "||"),
}
}
}
enum GuardDisplayContext {
Root,
InAnyArg,
InNot,
InAll,
}
impl GuardExpr {
fn fmt_with_ctx(&self, f: &mut fmt::Formatter<'_>, ctx: GuardDisplayContext) -> fmt::Result {
match self {
GuardExpr::Predicate(guard) => write!(f, "{}", guard),
GuardExpr::All(children) => {
let wrap = matches!(
ctx,
GuardDisplayContext::InAnyArg | GuardDisplayContext::InNot
) && children.len() > 1;
if wrap {
write!(f, "(")?;
}
for (i, child) in children.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
child.fmt_with_ctx(f, GuardDisplayContext::InAll)?;
}
if wrap {
write!(f, ")")?;
}
Ok(())
}
GuardExpr::Or(children) => {
write!(f, "any(")?;
for (i, child) in children.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
child.fmt_with_ctx(f, GuardDisplayContext::InAnyArg)?;
}
write!(f, ")")
}
GuardExpr::Not(child) => {
write!(f, "not(")?;
child.fmt_with_ctx(f, GuardDisplayContext::InNot)?;
write!(f, ")")
}
}
}
}
impl fmt::Display for GuardExpr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.fmt_with_ctx(f, GuardDisplayContext::Root)
}
}
impl fmt::Display for Step {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(expr) = &self.guard {
write!(f, "[{}] ", expr)?;
}
write!(f, "{}", self.kind)
}
}