1use std::collections::{HashMap, HashSet};
2use std::fmt;
3use std::sync::Arc;
4
5pub use crate::commands::{AssertTarget, StepKind};
6use crate::constants::{
7 KEYWORD_ASYNC, KEYWORD_AWAIT, KEYWORD_BREAK, KEYWORD_CANCEL, KEYWORD_CONTINUE, KEYWORD_ELSE,
8 KEYWORD_EXPORT, KEYWORD_FOR, KEYWORD_FUNC, KEYWORD_IF, KEYWORD_IMPORT, KEYWORD_LET,
9 KEYWORD_RETURN, KEYWORD_WHILE,
10};
11
12#[derive(Debug, Clone, Default)]
16pub struct ModuleFuncs {
17 pub functions: HashSet<String>,
19}
20
21#[derive(Debug, Clone, Default)]
26pub struct ModuleTable {
27 pub modules: HashMap<String, Option<ModuleFuncs>>,
28}
29
30impl ModuleTable {
31 pub fn reserved_base_names(&self) -> HashSet<String> {
35 let mut out = HashSet::new();
36 for funcs in self.modules.values().flatten() {
37 out.extend(funcs.functions.iter().cloned());
38 }
39 out
40 }
41}
42
43#[derive(Copy, Clone, Debug, Eq, PartialEq)]
44pub enum Command {
45 InheritEnv,
46 Workdir,
47 Workspace,
48 Env,
49 Echo,
50 Run,
51 Copy,
52 WithIo,
53 CopyGit,
54 HashSha256,
55 Symlink,
56 Mkdir,
57 Ls,
58 Cwd,
59 Read,
60 ReadLine,
61 Write,
62 Append,
63 Expand,
64 AssertEq,
65 AssertContains,
66 Exit,
67 Async,
68 Timeout,
69 Sleep,
70}
71
72pub const COMMANDS: &[Command] = &[
73 Command::InheritEnv,
74 Command::Workdir,
75 Command::Workspace,
76 Command::Env,
77 Command::Echo,
78 Command::Run,
79 Command::Copy,
80 Command::WithIo,
81 Command::CopyGit,
82 Command::HashSha256,
83 Command::Symlink,
84 Command::Mkdir,
85 Command::Ls,
86 Command::Cwd,
87 Command::Read,
88 Command::ReadLine,
89 Command::Write,
90 Command::Append,
91 Command::Expand,
92 Command::AssertEq,
93 Command::AssertContains,
94 Command::Exit,
95 Command::Timeout,
96 Command::Sleep,
97];
98
99impl Command {
100 pub const fn as_str(self) -> &'static str {
101 match self {
102 Command::InheritEnv => "INHERIT_ENV",
103 Command::Workdir => "WORKDIR",
104 Command::Workspace => "WORKSPACE",
105 Command::Env => "ENV",
106 Command::Echo => "ECHO",
107 Command::Run => "RUN",
108 Command::Copy => "COPY",
109 Command::WithIo => "WITH_IO",
110 Command::CopyGit => "COPY_GIT",
111 Command::HashSha256 => "HASH_SHA256",
112 Command::Symlink => "SYMLINK",
113 Command::Mkdir => "MKDIR",
114 Command::Ls => "LS",
115 Command::Cwd => "CWD",
116 Command::Read => "READ",
117 Command::ReadLine => "READ_LINE",
118 Command::Write => "WRITE",
119 Command::Append => "APPEND",
120 Command::Expand => "EXPAND",
121 Command::AssertEq => "ASSERT_EQ",
122 Command::AssertContains => "ASSERT_CONTAINS",
123 Command::Exit => "EXIT",
124 Command::Async => "ASYNC",
125 Command::Timeout => "TIMEOUT",
126 Command::Sleep => "SLEEP",
127 }
128 }
129
130 pub const fn syntax(self) -> &'static str {
131 match self {
132 Command::InheritEnv => "INHERIT_ENV [KEY1, KEY2, ...]",
133 Command::Workdir => "WORKDIR <path>",
134 Command::Workspace => "WORKSPACE SNAPSHOT|LOCAL",
135 Command::Env => "ENV KEY=value",
136 Command::Echo => "ECHO <message>",
137 Command::Run => "RUN <command...> | RUN [\"exe\", \"arg\", ...]",
138 Command::Copy => "COPY [--from-current-workspace] <from> <to>",
139 Command::CopyGit => "COPY_GIT [--include-dirty] <rev> <src> <dst>",
140 Command::WithIo => "WITH_IO [bindings] [command | { block }]",
141 Command::HashSha256 => "HASH_SHA256 <path>",
142 Command::Symlink => "SYMLINK <from> <to>",
143 Command::Mkdir => "MKDIR <path>",
144 Command::Ls => "LS [<path>]",
145 Command::Cwd => "CWD",
146 Command::Read => "READ [<path>]",
147 Command::ReadLine => "READ_LINE $var",
148 Command::Write => "WRITE <path> [<contents>]",
149 Command::Append => "APPEND <path> [<contents>]",
150 Command::Expand => "EXPAND [<path>] [<KEY=val> ...]",
151 Command::AssertEq => "ASSERT_EQ [--hash <sha256>] <actual> <expected>",
152 Command::AssertContains => "ASSERT_CONTAINS <haystack> <needle>",
153 Command::Exit => "EXIT <code>",
154 Command::Async => "ASYNC <command...> | ASYNC { <commands> }",
155 Command::Timeout => {
156 "TIMEOUT <duration> <command...> | TIMEOUT <duration> { <commands> }"
157 }
158 Command::Sleep => "SLEEP <duration>",
159 }
160 }
161
162 pub const fn expects_inner_command(self) -> bool {
163 matches!(self, Command::WithIo | Command::Async | Command::Timeout)
164 }
165
166 pub fn parse(s: &str) -> Option<Self> {
167 match s {
168 "INHERIT_ENV" => Some(Command::InheritEnv),
169 "WORKDIR" => Some(Command::Workdir),
170 "WORKSPACE" => Some(Command::Workspace),
171 "ENV" => Some(Command::Env),
172 "ECHO" => Some(Command::Echo),
173 "RUN" => Some(Command::Run),
174 "COPY" => Some(Command::Copy),
175 "WITH_IO" => Some(Command::WithIo),
176 "COPY_GIT" => Some(Command::CopyGit),
177 "HASH_SHA256" => Some(Command::HashSha256),
178 "SYMLINK" => Some(Command::Symlink),
179 "MKDIR" => Some(Command::Mkdir),
180 "LS" => Some(Command::Ls),
181 "CWD" => Some(Command::Cwd),
182 "READ" => Some(Command::Read),
183 "READ_LINE" => Some(Command::ReadLine),
184 "WRITE" => Some(Command::Write),
185 "APPEND" => Some(Command::Append),
186 "EXPAND" => Some(Command::Expand),
187 "ASSERT_EQ" => Some(Command::AssertEq),
188 "ASSERT_CONTAINS" => Some(Command::AssertContains),
189 "EXIT" => Some(Command::Exit),
190 "ASYNC" => Some(Command::Async),
191 "TIMEOUT" => Some(Command::Timeout),
192 "SLEEP" => Some(Command::Sleep),
193 _ => None,
194 }
195 }
196
197 pub(crate) fn is_statement_keyword(s: &str) -> bool {
202 Command::parse(s).is_some() || STRUCTURAL_KEYWORDS.contains(&s)
203 }
204}
205
206pub const STRUCTURAL_KEYWORDS: &[&str] = &[
212 KEYWORD_LET,
213 KEYWORD_FOR,
214 KEYWORD_IF,
215 KEYWORD_ELSE,
216 KEYWORD_ASYNC,
217 KEYWORD_AWAIT,
218 KEYWORD_CANCEL,
219 KEYWORD_FUNC,
220 KEYWORD_RETURN,
221 KEYWORD_WHILE,
222 KEYWORD_BREAK,
223 KEYWORD_CONTINUE,
224 KEYWORD_IMPORT,
225 KEYWORD_EXPORT,
226];
227
228pub const CLAUSE_KEYWORDS: &[&str] = &["IN"];
233
234#[derive(Copy, Clone, Debug, Eq, PartialEq)]
235pub enum PlatformGuard {
236 Unix,
237 Windows,
238 Macos,
239 Linux,
240}
241
242#[derive(Debug, Clone, Eq, PartialEq)]
243pub enum Guard {
244 Platform { target: PlatformGuard },
245 EnvExists { key: String },
246 EnvEquals { key: String, value: String },
247 StaticBool { value: String },
248}
249
250#[derive(Debug, Clone, Eq, PartialEq)]
251pub enum GuardExpr {
252 Predicate(Guard),
253 All(Vec<GuardExpr>),
254 Or(Vec<GuardExpr>),
255 Not(Box<GuardExpr>),
256}
257
258impl GuardExpr {
259 pub fn all(exprs: Vec<GuardExpr>) -> GuardExpr {
260 let mut flat = Vec::new();
261 for expr in exprs {
262 match expr {
263 GuardExpr::All(children) => flat.extend(children),
264 other => flat.push(other),
265 }
266 }
267 match flat.len() {
268 0 => panic!("GuardExpr::all requires at least one expression"),
269 1 => flat.into_iter().next().unwrap(),
270 _ => GuardExpr::All(flat),
271 }
272 }
273
274 pub fn or(exprs: Vec<GuardExpr>) -> GuardExpr {
275 let mut flat = Vec::new();
276 for expr in exprs {
277 match expr {
278 GuardExpr::Or(children) => flat.extend(children),
279 other => flat.push(other),
280 }
281 }
282 match flat.len() {
283 0 => panic!("GuardExpr::or requires at least one expression"),
284 1 => flat.into_iter().next().unwrap(),
285 _ => GuardExpr::Or(flat),
286 }
287 }
288
289 pub fn invert(expr: GuardExpr) -> GuardExpr {
290 match expr {
291 GuardExpr::Not(inner) => *inner,
292 other => GuardExpr::Not(Box::new(other)),
293 }
294 }
295}
296
297impl std::ops::Not for GuardExpr {
298 type Output = GuardExpr;
299
300 fn not(self) -> GuardExpr {
301 match self {
302 GuardExpr::Not(inner) => *inner,
303 other => GuardExpr::Not(Box::new(other)),
304 }
305 }
306}
307
308impl From<Guard> for GuardExpr {
309 fn from(guard: Guard) -> Self {
310 GuardExpr::Predicate(guard)
311 }
312}
313
314#[derive(Debug, Clone, PartialEq)]
316pub enum Arg {
317 String(String, bool),
321 Expr(Expr),
323 Parts(Vec<ArgPart>),
327}
328
329#[derive(Debug, Clone, PartialEq)]
331pub enum ArgPart {
332 Text(String, bool),
335 Expr(Expr),
337}
338
339impl Arg {
340 pub fn as_str(&self) -> &str {
341 match self {
342 Arg::String(s, _) => s,
343 Arg::Expr(_) | Arg::Parts(_) => "",
346 }
347 }
348
349 pub fn render(&self) -> String {
353 match self {
354 Arg::String(s, _) => s.clone(),
355 Arg::Expr(e) => e.to_string(),
356 Arg::Parts(parts) => parts.iter().map(ArgPart::render).collect(),
357 }
358 }
359
360 pub fn is_quoted(&self) -> bool {
361 matches!(self, Arg::String(_, true))
362 }
363}
364
365impl ArgPart {
366 pub fn render(&self) -> String {
367 match self {
368 ArgPart::Text(s, _) => s.clone(),
369 ArgPart::Expr(e) => e.to_string(),
370 }
371 }
372}
373
374impl From<String> for Arg {
375 fn from(s: String) -> Self {
376 Arg::String(s, false)
377 }
378}
379
380impl From<&str> for Arg {
381 fn from(s: &str) -> Self {
382 Arg::String(s.to_string(), false)
383 }
384}
385
386impl std::fmt::Display for Arg {
387 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
388 match self {
389 Arg::String(s, _) => write!(f, "{}", s),
390 Arg::Expr(e) => write!(f, "{}", e),
391 Arg::Parts(parts) => {
392 for part in parts {
393 match part {
394 ArgPart::Text(s, _) => write!(f, "{}", s)?,
395 ArgPart::Expr(e) => write!(f, "{}", e)?,
396 }
397 }
398 Ok(())
399 }
400 }
401 }
402}
403
404impl AsRef<str> for Arg {
405 fn as_ref(&self) -> &str {
406 self.as_str()
407 }
408}
409
410impl PartialEq<str> for Arg {
411 fn eq(&self, other: &str) -> bool {
412 self.as_str() == other
413 }
414}
415
416impl PartialEq<&str> for Arg {
417 fn eq(&self, other: &&str) -> bool {
418 self.as_str() == *other
419 }
420}
421
422#[derive(Debug, Clone, Copy, Eq, PartialEq)]
423pub enum IoStream {
424 Stdin,
425 Stdout,
426 Stderr,
427}
428
429#[derive(Debug, Clone, Eq, PartialEq)]
430pub struct IoBinding {
431 pub stream: IoStream,
432 pub pipe: Option<PipeTarget>,
433}
434
435#[derive(Debug, Clone, Eq, PartialEq)]
439pub enum PipeTarget {
440 Name(String),
441 Var(String),
442}
443
444pub use crate::value::{
448 OxDockType, TypeDescriptor, Value, ValuePayload, clone_boxed, clone_copy, clone_shared,
449 drop_boxed, drop_noop, drop_shared, eq_boxed, eq_inline, eq_shared, fmt_boxed, fmt_inline,
450 fmt_shared, load_inline, startup_descriptors, store_inline, type_anchor, unshare_boxed,
451 unshare_inline, unshare_shared,
452};
453
454#[derive(Debug, Clone, Copy, Eq, PartialEq)]
455pub enum CompareOp {
456 Eq,
457 Ne,
458 Lt,
459 Le,
460 Gt,
461 Ge,
462}
463
464#[derive(Debug, Clone, Copy, Eq, PartialEq)]
465pub enum ArithOp {
466 Add,
467 Sub,
468 Mul,
469 Div,
470}
471
472#[derive(Debug, Clone, PartialEq)]
481pub enum MathOp {
482 PushConst(Value),
483 LoadVar(String),
484 LoadEnv(String),
485 LoadKeyPath { base: String, keys: Vec<String> },
486 Call { name: String, arity: usize },
487 Inspect(String),
488 Neg,
489 Add,
490 Sub,
491 Mul,
492 Div,
493 Lt,
494 Le,
495 Gt,
496 Ge,
497 Eq,
498 Ne,
499}
500
501#[derive(Debug, Clone, Copy, Eq, PartialEq)]
502pub enum LogicalOp {
503 And,
504 Or,
505}
506
507#[derive(Debug, Clone, PartialEq)]
508pub enum Expr {
509 Literal(Value),
510 Var(String),
511 Env(String),
514 KeyPath {
515 base: String,
516 keys: Vec<String>,
517 },
518 List(Vec<Expr>),
519 Map(Vec<(String, Expr)>),
520 Call {
521 name: String,
522 args: Vec<Expr>,
523 },
524 Inspect(String),
529 Compare {
530 op: CompareOp,
531 left: Box<Expr>,
532 right: Box<Expr>,
533 },
534 Arithmetic {
535 op: ArithOp,
536 left: Box<Expr>,
537 right: Box<Expr>,
538 },
539 CompiledMath(Vec<MathOp>),
542 UnsignedIntBoundary(u64),
546 Not(Box<Expr>),
547 Logical {
548 op: LogicalOp,
549 left: Box<Expr>,
550 right: Box<Expr>,
551 },
552}
553
554#[derive(Debug, Clone, PartialEq)]
555pub struct Step {
556 pub guard: Option<GuardExpr>,
557 pub kind: StepKind,
558 pub scope_enter: usize,
559 pub scope_exit: usize,
560}
561
562#[derive(Debug, Clone, Eq, PartialEq)]
563pub enum WorkspaceTarget {
564 Snapshot,
565 Local,
566}
567
568fn platform_matches(target: PlatformGuard) -> bool {
569 #[allow(clippy::disallowed_macros)]
570 match target {
571 PlatformGuard::Unix => cfg!(unix),
572 PlatformGuard::Windows => cfg!(windows),
573 PlatformGuard::Macos => cfg!(target_os = "macos"),
574 PlatformGuard::Linux => cfg!(target_os = "linux"),
575 }
576}
577
578pub trait EnvLookup {
579 fn get_env(&self, key: &str) -> Option<&str>;
580}
581
582impl EnvLookup for HashMap<String, String> {
583 fn get_env(&self, key: &str) -> Option<&str> {
584 self.get(key).map(|s| s.as_str())
585 }
586}
587
588impl EnvLookup for Arc<HashMap<String, String>> {
589 fn get_env(&self, key: &str) -> Option<&str> {
590 (**self).get_env(key)
591 }
592}
593
594pub fn guard_allows(guard: &Guard, env: &impl EnvLookup) -> bool {
595 match guard {
596 Guard::Platform { target } => platform_matches(*target),
597 Guard::EnvExists { key } => env.get_env(key).map(|v| !v.is_empty()).unwrap_or(false),
598 Guard::EnvEquals { key, value } => env
599 .get_env(key)
600 .map(|v| v == value.as_str())
601 .unwrap_or(false),
602 Guard::StaticBool { value } => value.parse::<bool>().unwrap_or(false),
603 }
604}
605
606pub fn guard_expr_allows(expr: &GuardExpr, env: &impl EnvLookup) -> bool {
607 match expr {
608 GuardExpr::Predicate(guard) => guard_allows(guard, env),
609 GuardExpr::All(children) => children.iter().all(|g| guard_expr_allows(g, env)),
610 GuardExpr::Or(children) => children.iter().any(|g| guard_expr_allows(g, env)),
611 GuardExpr::Not(child) => !guard_expr_allows(child, env),
612 }
613}
614
615pub fn guard_option_allows(expr: Option<&GuardExpr>, env: &impl EnvLookup) -> bool {
616 match expr {
617 Some(e) => guard_expr_allows(e, env),
618 None => true,
619 }
620}
621
622impl fmt::Display for PlatformGuard {
623 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
624 match self {
625 PlatformGuard::Unix => write!(f, "unix"),
626 PlatformGuard::Windows => write!(f, "windows"),
627 PlatformGuard::Macos => write!(f, "macos"),
628 PlatformGuard::Linux => write!(f, "linux"),
629 }
630 }
631}
632
633impl fmt::Display for Guard {
634 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
635 match self {
636 Guard::Platform { target } => write!(f, "{}", target),
637 Guard::EnvExists { key } => write!(f, "env:{}", key),
638 Guard::EnvEquals { key, value } => write!(f, "eq(env:{}, {})", key, value),
639 Guard::StaticBool { value } => write!(f, "bool:{}", value),
640 }
641 }
642}
643
644impl fmt::Display for WorkspaceTarget {
645 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
646 match self {
647 WorkspaceTarget::Snapshot => write!(f, "SNAPSHOT"),
648 WorkspaceTarget::Local => write!(f, "LOCAL"),
649 }
650 }
651}
652
653impl fmt::Display for Expr {
654 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
655 match self {
656 Expr::Literal(v) => write!(f, "{}", v),
657 Expr::Var(name) => write!(f, "${}", name),
658 Expr::Env(key) => write!(f, "env:{}", key),
659 Expr::KeyPath { base, keys } => {
660 write!(f, "${}", base)?;
661 for key in keys {
662 write!(f, ".{}", key)?;
663 }
664 Ok(())
665 }
666 Expr::Call { name, args } => {
667 write!(f, "{}(", name)?;
668 for (i, arg) in args.iter().enumerate() {
669 if i > 0 {
670 write!(f, ", ")?;
671 }
672 write!(f, "{}", arg)?;
673 }
674 write!(f, ")")
675 }
676 Expr::Inspect(var) => write!(f, "INSPECT(${})", var),
677 Expr::List(items) => {
678 write!(f, "[")?;
679 for (i, item) in items.iter().enumerate() {
680 if i > 0 {
681 write!(f, ", ")?;
682 }
683 write!(f, "{}", item)?;
684 }
685 write!(f, "]")
686 }
687 Expr::Map(entries) => {
688 write!(f, "{{")?;
689 for (i, (key, val)) in entries.iter().enumerate() {
690 if i > 0 {
691 write!(f, ", ")?;
692 }
693 write!(f, "\"{}\": {}", key, val)?;
694 }
695 write!(f, "}}")
696 }
697 Expr::Compare { op, left, right } => {
698 write!(f, "{} {} {}", left, op, right)
699 }
700 Expr::Arithmetic { op, left, right } => {
701 write!(f, "({} {} {})", left, op, right)
702 }
703 Expr::CompiledMath(ops) => {
704 write!(f, "{}", format_compiled_math(ops))
705 }
706 Expr::UnsignedIntBoundary(n) => write!(f, "{}", n),
707 Expr::Not(inner) => {
708 match inner.as_ref() {
711 Expr::Compare { .. } | Expr::Arithmetic { .. } | Expr::CompiledMath(_) => {
712 write!(f, "!({})", inner)
713 }
714 _ => write!(f, "!{}", inner),
715 }
716 }
717 Expr::Logical { op, left, right } => {
718 write!(f, "({} {} {})", left, op, right)
719 }
720 }
721 }
722}
723
724impl fmt::Display for CompareOp {
725 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
726 match self {
727 CompareOp::Eq => write!(f, "=="),
728 CompareOp::Ne => write!(f, "!="),
729 CompareOp::Lt => write!(f, "<"),
730 CompareOp::Le => write!(f, "<="),
731 CompareOp::Gt => write!(f, ">"),
732 CompareOp::Ge => write!(f, ">="),
733 }
734 }
735}
736
737impl fmt::Display for ArithOp {
738 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
739 match self {
740 ArithOp::Add => write!(f, "+"),
741 ArithOp::Sub => write!(f, "-"),
742 ArithOp::Mul => write!(f, "*"),
743 ArithOp::Div => write!(f, "/"),
744 }
745 }
746}
747
748fn format_compiled_math(ops: &[MathOp]) -> String {
753 let mut stack: Vec<String> = Vec::new();
754 for op in ops {
755 match op {
756 MathOp::PushConst(v) => stack.push(format!("{}", v)),
757 MathOp::LoadVar(name) => stack.push(format!("${}", name)),
758 MathOp::LoadEnv(key) => stack.push(format!("env:{}", key)),
759 MathOp::LoadKeyPath { base, keys } => {
760 let mut s = format!("${}", base);
761 for key in keys {
762 s.push('.');
763 s.push_str(key);
764 }
765 stack.push(s);
766 }
767 MathOp::Call { name, arity } => {
768 let mut args = Vec::new();
769 for _ in 0..*arity {
770 args.push(stack.pop().unwrap_or_else(|| "<underflow>".to_string()));
771 }
772 args.reverse();
773 stack.push(format!("{}({})", name, args.join(", ")));
774 }
775 MathOp::Inspect(name) => stack.push(format!("INSPECT(${})", name)),
776 MathOp::Neg => {
777 let inner = stack.pop().unwrap_or_else(|| "<underflow>".to_string());
778 stack.push(format!("(-{})", inner));
779 }
780 MathOp::Add => push_bin(&mut stack, "+"),
781 MathOp::Sub => push_bin(&mut stack, "-"),
782 MathOp::Mul => push_bin(&mut stack, "*"),
783 MathOp::Div => push_bin(&mut stack, "/"),
784 MathOp::Lt => push_bin(&mut stack, "<"),
785 MathOp::Le => push_bin(&mut stack, "<="),
786 MathOp::Gt => push_bin(&mut stack, ">"),
787 MathOp::Ge => push_bin(&mut stack, ">="),
788 MathOp::Eq => push_bin(&mut stack, "=="),
789 MathOp::Ne => push_bin(&mut stack, "!="),
790 }
791 }
792 if stack.len() == 1 {
793 let mut items = stack;
794 items.pop().unwrap_or_else(|| "<empty>".to_string())
795 } else {
796 stack.join(" ")
797 }
798}
799
800fn push_bin(stack: &mut Vec<String>, op: &str) {
801 let right = stack.pop().unwrap_or_else(|| "<underflow>".to_string());
802 let left = stack.pop().unwrap_or_else(|| "<underflow>".to_string());
803 stack.push(format!("({} {} {})", left, op, right));
804}
805
806impl fmt::Display for LogicalOp {
807 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
808 match self {
809 LogicalOp::And => write!(f, "&&"),
810 LogicalOp::Or => write!(f, "||"),
811 }
812 }
813}
814
815enum GuardDisplayContext {
816 Root,
817 InAnyArg,
818 InNot,
819 InAll,
820}
821
822impl GuardExpr {
823 fn fmt_with_ctx(&self, f: &mut fmt::Formatter<'_>, ctx: GuardDisplayContext) -> fmt::Result {
824 match self {
825 GuardExpr::Predicate(guard) => write!(f, "{}", guard),
826 GuardExpr::All(children) => {
827 let wrap = matches!(
828 ctx,
829 GuardDisplayContext::InAnyArg | GuardDisplayContext::InNot
830 ) && children.len() > 1;
831 if wrap {
832 write!(f, "(")?;
833 }
834 for (i, child) in children.iter().enumerate() {
835 if i > 0 {
836 write!(f, ", ")?;
837 }
838 child.fmt_with_ctx(f, GuardDisplayContext::InAll)?;
839 }
840 if wrap {
841 write!(f, ")")?;
842 }
843 Ok(())
844 }
845 GuardExpr::Or(children) => {
846 write!(f, "any(")?;
847 for (i, child) in children.iter().enumerate() {
848 if i > 0 {
849 write!(f, ", ")?;
850 }
851 child.fmt_with_ctx(f, GuardDisplayContext::InAnyArg)?;
852 }
853 write!(f, ")")
854 }
855 GuardExpr::Not(child) => {
856 write!(f, "not(")?;
857 child.fmt_with_ctx(f, GuardDisplayContext::InNot)?;
858 write!(f, ")")
859 }
860 }
861 }
862}
863
864impl fmt::Display for GuardExpr {
865 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
866 self.fmt_with_ctx(f, GuardDisplayContext::Root)
867 }
868}
869
870impl fmt::Display for Step {
871 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
872 if let Some(expr) = &self.guard {
873 write!(f, "[{}] ", expr)?;
874 }
875 write!(f, "{}", self.kind)
876 }
877}
878
879#[cfg(test)]
880mod tests {
881 use super::*;
882 #[test]
895 fn statement_keywords_match_pest_grammar() {
896 use pest_meta::{ast::Expr, parser};
897 use std::collections::{HashMap, HashSet};
898
899 let pest_src = include_str!("dsl.pest");
900 let pairs = parser::parse(parser::Rule::grammar_rules, pest_src)
901 .expect("dsl.pest must parse as a pest grammar");
902 let rules = parser::consume_rules(pairs).expect("dsl.pest rules must consume");
903 let by_name: HashMap<&str, &Expr> = rules
904 .iter()
905 .map(|rule| (rule.name.as_str(), &rule.expr))
906 .collect();
907
908 let mut all_literals = HashSet::new();
909 for rule in &rules {
910 for node in rule.expr.iter_top_down() {
911 match node {
912 Expr::Str(literal) | Expr::Insens(literal) => {
913 all_literals.insert(literal.clone());
914 }
915 _ => {}
916 }
917 }
918 }
919 for kw in STRUCTURAL_KEYWORDS {
920 assert!(
921 all_literals.contains(*kw),
922 "keyword {kw} in STRUCTURAL_KEYWORDS missing from dsl.pest"
923 );
924 }
925
926 let mut seen = HashSet::new();
927 let mut stack = vec!["element".to_string(), "block_element".to_string()];
928 let mut stmt_literals = HashSet::new();
929 while let Some(name) = stack.pop() {
930 if !seen.insert(name.clone()) {
931 continue;
932 }
933 let Some(expr) = by_name.get(name.as_str()) else {
934 continue;
935 };
936 for node in expr.iter_top_down() {
937 match node {
938 Expr::Str(literal) | Expr::Insens(literal) => {
939 stmt_literals.insert(literal.clone());
940 }
941 Expr::Ident(dependency) => stack.push(dependency.clone()),
942 _ => {}
943 }
944 }
945 }
946 assert!(
947 seen.contains("element") && seen.contains("block_element"),
948 "grammar must define element and block_element instruction rules"
949 );
950 for kw in stmt_literals {
951 if kw.len() > 1 && kw.chars().all(|c| c.is_ascii_uppercase()) {
952 assert!(
953 crate::Command::is_statement_keyword(&kw)
954 || CLAUSE_KEYWORDS.contains(&kw.as_str()),
955 "uppercase literal \"{kw}\" reachable from dsl.pest instruction rules is not a registered statement or clause keyword"
956 );
957 }
958 }
959 }
960}