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_REMOTE, KEYWORD_RETURN, KEYWORD_TYPE, 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)]
51pub enum Command {
52 InheritEnv,
53 Workdir,
54 Workspace,
55 Env,
56 Echo,
57 Run,
58 Copy,
59 WithIo,
60 CopyGit,
61 HashSha256,
62 Symlink,
63 Mkdir,
64 Install,
65 Ls,
66 Cwd,
67 Read,
68 ReadLine,
69 Prompt,
70 Write,
71 Append,
72 Remove,
73 Expand,
74 AssertEq,
75 AssertContains,
76 Exit,
77 Async,
78 Timeout,
79 Sleep,
80 ListAppend,
81}
82
83pub const COMMANDS: &[Command] = &[
84 Command::InheritEnv,
85 Command::Workdir,
86 Command::Workspace,
87 Command::Env,
88 Command::Echo,
89 Command::Run,
90 Command::Copy,
91 Command::WithIo,
92 Command::CopyGit,
93 Command::HashSha256,
94 Command::Symlink,
95 Command::Mkdir,
96 Command::Install,
97 Command::Ls,
98 Command::Cwd,
99 Command::Read,
100 Command::ReadLine,
101 Command::Prompt,
102 Command::Write,
103 Command::Append,
104 Command::Remove,
105 Command::Expand,
106 Command::AssertEq,
107 Command::AssertContains,
108 Command::Exit,
109 Command::Timeout,
110 Command::Sleep,
111 Command::ListAppend,
112];
113
114pub const COMMAND_CATEGORIES: &[&str] =
119 &["INHERIT", "WITH", "COPY", "HASH", "READ", "ASSERT", "LIST"];
120
121impl Command {
122 pub const fn as_str(self) -> &'static str {
123 match self {
124 Command::InheritEnv => "INHERIT_ENV",
125 Command::Workdir => "WORKDIR",
126 Command::Workspace => "WORKSPACE",
127 Command::Env => "ENV",
128 Command::Echo => "ECHO",
129 Command::Run => "RUN",
130 Command::Copy => "COPY",
131 Command::WithIo => "WITH_IO",
132 Command::CopyGit => "COPY_GIT",
133 Command::HashSha256 => "HASH_SHA256",
134 Command::Symlink => "SYMLINK",
135 Command::Mkdir => "MKDIR",
136 Command::Install => "INSTALL",
137 Command::Ls => "LS",
138 Command::Cwd => "CWD",
139 Command::Read => "READ",
140 Command::ReadLine => "READ_LINE",
141 Command::Prompt => "PROMPT",
142 Command::Write => "WRITE",
143 Command::Append => "APPEND",
144 Command::Remove => "REMOVE",
145 Command::Expand => "EXPAND",
146 Command::AssertEq => "ASSERT_EQ",
147 Command::AssertContains => "ASSERT_CONTAINS",
148 Command::Exit => "EXIT",
149 Command::Async => "ASYNC",
150 Command::Timeout => "TIMEOUT",
151 Command::Sleep => "SLEEP",
152 Command::ListAppend => "LIST_APPEND",
153 }
154 }
155
156 pub const fn syntax(self) -> &'static str {
157 match self {
158 Command::InheritEnv => "INHERIT_ENV [KEY1, KEY2, ...]",
159 Command::Workdir => "WORKDIR <path>",
160 Command::Workspace => "WORKSPACE SNAPSHOT|LOCAL|CACHE|SYSTEM [--local]",
161 Command::Env => "ENV KEY=value",
162 Command::Echo => "ECHO <message>",
163 Command::Run => "RUN <command...> | RUN [\"exe\", \"arg\", ...]",
164 Command::Copy => "COPY [--from-workspace SNAPSHOT|LOCAL|CACHE|SYSTEM] <from> <to>",
165 Command::CopyGit => "COPY_GIT [--include-dirty] <rev> <src> <dst>",
166 Command::WithIo => "WITH_IO [bindings] [command | { block }]",
167 Command::HashSha256 => "HASH_SHA256 <path>",
168 Command::Symlink => {
169 "SYMLINK [--from-workspace SNAPSHOT|LOCAL|CACHE|SYSTEM] <from> <to>"
170 }
171 Command::Mkdir => "MKDIR <path>",
172 Command::Install => "INSTALL <source> <dest>",
173 Command::Ls => "LS [<path>]",
174 Command::Cwd => "CWD",
175 Command::Read => "READ [<path>]",
176 Command::ReadLine => "READ_LINE $var",
177 Command::Prompt => "PROMPT $var <message> [<default>]",
178 Command::Write => "WRITE <path> [<contents>]",
179 Command::Append => "APPEND <path> [<contents>]",
180 Command::Remove => "REMOVE [--recursive] <path>",
181 Command::Expand => "EXPAND [<path>] [<KEY=val> ...]",
182 Command::AssertEq => "ASSERT_EQ [--hash <sha256>] <actual> <expected>",
183 Command::AssertContains => "ASSERT_CONTAINS <haystack> <needle>",
184 Command::Exit => "EXIT <code>",
185 Command::Async => "ASYNC <command...> | ASYNC { <commands> }",
186 Command::Timeout => {
187 "TIMEOUT <duration> <command...> | TIMEOUT <duration> { <commands> }"
188 }
189 Command::Sleep => "SLEEP <duration>",
190 Command::ListAppend => "LIST_APPEND $list <item>",
191 }
192 }
193
194 pub const fn expects_inner_command(self) -> bool {
195 matches!(self, Command::WithIo | Command::Async | Command::Timeout)
196 }
197
198 pub fn parse(s: &str) -> Option<Self> {
199 match s {
200 "INHERIT_ENV" => Some(Command::InheritEnv),
201 "WORKDIR" => Some(Command::Workdir),
202 "WORKSPACE" => Some(Command::Workspace),
203 "ENV" => Some(Command::Env),
204 "ECHO" => Some(Command::Echo),
205 "RUN" => Some(Command::Run),
206 "COPY" => Some(Command::Copy),
207 "WITH_IO" => Some(Command::WithIo),
208 "COPY_GIT" => Some(Command::CopyGit),
209 "HASH_SHA256" => Some(Command::HashSha256),
210 "SYMLINK" => Some(Command::Symlink),
211 "MKDIR" => Some(Command::Mkdir),
212 "INSTALL" => Some(Command::Install),
213 "LS" => Some(Command::Ls),
214 "CWD" => Some(Command::Cwd),
215 "READ" => Some(Command::Read),
216 "READ_LINE" => Some(Command::ReadLine),
217 "PROMPT" => Some(Command::Prompt),
218 "WRITE" => Some(Command::Write),
219 "APPEND" => Some(Command::Append),
220 "REMOVE" => Some(Command::Remove),
221 "EXPAND" => Some(Command::Expand),
222 "ASSERT_EQ" => Some(Command::AssertEq),
223 "ASSERT_CONTAINS" => Some(Command::AssertContains),
224 "EXIT" => Some(Command::Exit),
225 "ASYNC" => Some(Command::Async),
226 "TIMEOUT" => Some(Command::Timeout),
227 "SLEEP" => Some(Command::Sleep),
228 "LIST_APPEND" => Some(Command::ListAppend),
229 _ => None,
230 }
231 }
232
233 pub(crate) fn is_statement_keyword(s: &str) -> bool {
238 Command::parse(s).is_some() || STRUCTURAL_KEYWORDS.contains(&s)
239 }
240}
241
242pub const STRUCTURAL_KEYWORDS: &[&str] = &[
248 KEYWORD_LET,
249 KEYWORD_FOR,
250 KEYWORD_IF,
251 KEYWORD_ELSE,
252 KEYWORD_ASYNC,
253 KEYWORD_AWAIT,
254 KEYWORD_CANCEL,
255 KEYWORD_FUNC,
256 KEYWORD_RETURN,
257 KEYWORD_WHILE,
258 KEYWORD_BREAK,
259 KEYWORD_CONTINUE,
260 KEYWORD_REMOTE,
261 KEYWORD_IMPORT,
262 KEYWORD_EXPORT,
263 KEYWORD_TYPE,
264];
265
266pub const CLAUSE_KEYWORDS: &[&str] = &["IN"];
271
272#[derive(Debug, Clone, Eq, PartialEq)]
285pub enum Guard {
286 Attr {
287 ns: Ns,
288 key: Option<String>,
289 val: Option<String>,
290 },
291}
292
293#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
297pub enum Ns {
298 Os,
299 Arch,
300 Bool,
301 Env,
302}
303
304impl fmt::Display for Ns {
305 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
306 match self {
307 Ns::Os => write!(f, "os"),
308 Ns::Arch => write!(f, "arch"),
309 Ns::Bool => write!(f, "bool"),
310 Ns::Env => write!(f, "env"),
311 }
312 }
313}
314
315pub const ARCH_VALUES: &[&str] = &[
319 "aarch64",
320 "arm",
321 "arm64ec",
322 "avr",
323 "bpf",
324 "csky",
325 "hexagon",
326 "loongarch32",
327 "loongarch64",
328 "mips",
329 "mips32r6",
330 "mips64",
331 "mips64r6",
332 "msp430",
333 "nvptx64",
334 "powerpc",
335 "powerpc64",
336 "riscv32",
337 "riscv64",
338 "s390x",
339 "sparc",
340 "sparc64",
341 "wasm32",
342 "wasm64",
343 "x86",
344 "x86_64",
345 "xtensa",
346];
347
348#[derive(Debug, Clone, Eq, PartialEq)]
349pub enum GuardExpr {
350 Predicate(Guard),
351 All(Vec<GuardExpr>),
352 Or(Vec<GuardExpr>),
353 Not(Box<GuardExpr>),
354}
355
356impl GuardExpr {
357 pub fn all(exprs: Vec<GuardExpr>) -> GuardExpr {
358 let mut flat = Vec::new();
359 for expr in exprs {
360 match expr {
361 GuardExpr::All(children) => flat.extend(children),
362 other => flat.push(other),
363 }
364 }
365 match flat.len() {
366 0 => panic!("GuardExpr::all requires at least one expression"),
367 1 => flat.into_iter().next().unwrap(),
368 _ => GuardExpr::All(flat),
369 }
370 }
371
372 pub fn or(exprs: Vec<GuardExpr>) -> GuardExpr {
373 let mut flat = Vec::new();
374 for expr in exprs {
375 match expr {
376 GuardExpr::Or(children) => flat.extend(children),
377 other => flat.push(other),
378 }
379 }
380 match flat.len() {
381 0 => panic!("GuardExpr::or requires at least one expression"),
382 1 => flat.into_iter().next().unwrap(),
383 _ => GuardExpr::Or(flat),
384 }
385 }
386
387 pub fn invert(expr: GuardExpr) -> GuardExpr {
388 match expr {
389 GuardExpr::Not(inner) => *inner,
390 other => GuardExpr::Not(Box::new(other)),
391 }
392 }
393}
394
395impl std::ops::Not for GuardExpr {
396 type Output = GuardExpr;
397
398 fn not(self) -> GuardExpr {
399 match self {
400 GuardExpr::Not(inner) => *inner,
401 other => GuardExpr::Not(Box::new(other)),
402 }
403 }
404}
405
406impl From<Guard> for GuardExpr {
407 fn from(guard: Guard) -> Self {
408 GuardExpr::Predicate(guard)
409 }
410}
411
412#[derive(Debug, Clone, PartialEq)]
414pub enum Arg {
415 String(String, bool),
419 Expr(Expr),
421 Parts(Vec<ArgPart>),
425}
426
427#[derive(Debug, Clone, PartialEq)]
429pub enum ArgPart {
430 Text(String, bool),
433 Expr(Expr),
435}
436
437impl Arg {
438 pub fn as_str(&self) -> &str {
439 match self {
440 Arg::String(s, _) => s,
441 Arg::Expr(_) | Arg::Parts(_) => "",
444 }
445 }
446
447 pub fn render(&self) -> String {
451 match self {
452 Arg::String(s, _) => s.clone(),
453 Arg::Expr(e) => e.to_string(),
454 Arg::Parts(parts) => parts.iter().map(ArgPart::render).collect(),
455 }
456 }
457
458 pub fn is_quoted(&self) -> bool {
459 matches!(self, Arg::String(_, true))
460 }
461}
462
463impl ArgPart {
464 pub fn render(&self) -> String {
465 match self {
466 ArgPart::Text(s, _) => s.clone(),
467 ArgPart::Expr(e) => e.to_string(),
468 }
469 }
470}
471
472impl From<String> for Arg {
473 fn from(s: String) -> Self {
474 Arg::String(s, false)
475 }
476}
477
478impl From<&str> for Arg {
479 fn from(s: &str) -> Self {
480 Arg::String(s.to_string(), false)
481 }
482}
483
484impl std::fmt::Display for Arg {
485 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
486 match self {
487 Arg::String(s, _) => write!(f, "{}", s),
488 Arg::Expr(e) => write!(f, "{}", e),
489 Arg::Parts(parts) => {
490 for part in parts {
491 match part {
492 ArgPart::Text(s, _) => write!(f, "{}", s)?,
493 ArgPart::Expr(e) => write!(f, "{}", e)?,
494 }
495 }
496 Ok(())
497 }
498 }
499 }
500}
501
502impl AsRef<str> for Arg {
503 fn as_ref(&self) -> &str {
504 self.as_str()
505 }
506}
507
508impl PartialEq<str> for Arg {
509 fn eq(&self, other: &str) -> bool {
510 self.as_str() == other
511 }
512}
513
514impl PartialEq<&str> for Arg {
515 fn eq(&self, other: &&str) -> bool {
516 self.as_str() == *other
517 }
518}
519
520#[derive(Debug, Clone, Copy, Eq, PartialEq)]
521pub enum IoStream {
522 Stdin,
523 Stdout,
524 Stderr,
525}
526
527#[derive(Debug, Clone, Eq, PartialEq)]
528pub struct IoBinding {
529 pub stream: IoStream,
530 pub pipe: Option<PipeTarget>,
531}
532
533#[derive(Debug, Clone, Eq, PartialEq)]
536pub enum PipeTarget {
537 Var(String),
538}
539
540pub use crate::value::{
544 OxDockType, SemaphoreState, TypeDescriptor, Value, ValuePayload, clone_boxed, clone_copy,
545 clone_shared, drop_boxed, drop_noop, drop_shared, eq_boxed, eq_inline, eq_shared, fmt_boxed,
546 fmt_inline, fmt_shared, load_inline, startup_descriptors, store_inline, type_anchor,
547 unshare_boxed, unshare_inline, unshare_shared,
548};
549
550#[derive(Debug, Clone, Copy, Eq, PartialEq)]
551pub enum CompareOp {
552 Eq,
553 Ne,
554 Lt,
555 Le,
556 Gt,
557 Ge,
558}
559
560#[derive(Debug, Clone, Copy, Eq, PartialEq)]
561pub enum ArithOp {
562 Add,
563 Sub,
564 Mul,
565 Div,
566}
567
568#[derive(Debug, Clone, PartialEq)]
577pub enum MathOp {
578 PushConst(Value),
579 LoadVar(String),
580 LoadEnv(String),
581 LoadKeyPath { base: String, keys: Vec<String> },
582 Call { name: String, arity: usize },
583 Inspect(String),
584 Neg,
585 Add,
586 Sub,
587 Mul,
588 Div,
589 Lt,
590 Le,
591 Gt,
592 Ge,
593 Eq,
594 Ne,
595}
596
597#[derive(Debug, Clone, Copy, Eq, PartialEq)]
598pub enum LogicalOp {
599 And,
600 Or,
601}
602
603#[derive(Debug, Clone, PartialEq)]
604pub enum Expr {
605 Literal(Value),
606 Var(String),
607 Env(String),
610 KeyPath {
611 base: String,
612 keys: Vec<String>,
613 },
614 List(Vec<Expr>),
615 Map(Vec<(String, Expr)>),
616 Block(Vec<Step>),
621 Call {
622 name: String,
623 args: Vec<Expr>,
624 },
625 Inspect(String),
630 Compare {
631 op: CompareOp,
632 left: Box<Expr>,
633 right: Box<Expr>,
634 },
635 Arithmetic {
636 op: ArithOp,
637 left: Box<Expr>,
638 right: Box<Expr>,
639 },
640 CompiledMath(Vec<MathOp>),
643 FreshPipe,
649 UnsignedIntBoundary(u64),
653 Not(Box<Expr>),
654 Logical {
655 op: LogicalOp,
656 left: Box<Expr>,
657 right: Box<Expr>,
658 },
659}
660
661#[derive(Debug, Clone, PartialEq)]
662pub struct Step {
663 pub guard: Option<GuardExpr>,
664 pub kind: StepKind,
665 pub scope_enter: usize,
666 pub scope_exit: usize,
667}
668
669impl Expr {
670 pub fn walk(&self, f: &mut impl FnMut(&Expr)) {
677 f(self);
678 match self {
679 Expr::Literal(_)
680 | Expr::Var(_)
681 | Expr::Env(_)
682 | Expr::KeyPath { .. }
683 | Expr::Inspect(_)
684 | Expr::FreshPipe
685 | Expr::UnsignedIntBoundary(_) => {}
686 Expr::Call { args, .. } => {
687 for arg in args {
688 arg.walk(f);
689 }
690 }
691 Expr::List(items) => {
692 for item in items {
693 item.walk(f);
694 }
695 }
696 Expr::Map(entries) => {
697 for (_, value) in entries {
698 value.walk(f);
699 }
700 }
701 Expr::Block(steps) => {
702 for step in steps {
703 step.kind.walk_step_exprs(f);
704 }
705 }
706 Expr::Compare { left, right, .. }
707 | Expr::Arithmetic { left, right, .. }
708 | Expr::Logical { left, right, .. } => {
709 left.walk(f);
710 right.walk(f);
711 }
712 Expr::CompiledMath(_) => {}
713 Expr::Not(inner) => inner.walk(f),
714 }
715 }
716}
717
718#[derive(Debug, Clone, Eq, PartialEq)]
719pub enum WorkspaceTarget {
720 Snapshot,
721 Local,
722 Cache { local: bool },
723 System,
724}
725
726pub trait EnvLookup {
727 fn get_env(&self, key: &str) -> Option<&str>;
728}
729
730impl EnvLookup for HashMap<String, String> {
731 fn get_env(&self, key: &str) -> Option<&str> {
732 self.get(key).map(|s| s.as_str())
733 }
734}
735
736impl EnvLookup for Arc<HashMap<String, String>> {
737 fn get_env(&self, key: &str) -> Option<&str> {
738 (**self).get_env(key)
739 }
740}
741
742pub fn guard_allows(guard: &Guard, env: &impl EnvLookup) -> bool {
743 let Guard::Attr { ns, key, val } = guard;
744 match ns {
745 Ns::Os => match val.as_deref() {
746 Some("macos") => platform_is_macos(),
747 Some("linux") => platform_is_linux(),
748 Some("windows") => platform_is_windows(),
749 _ => false,
750 },
751 Ns::Arch => match val.as_deref() {
752 Some(want) => ARCH_TARGET == want,
753 None => false,
754 },
755 Ns::Bool => val
756 .as_deref()
757 .map(|v| v.parse::<bool>().unwrap_or(false))
758 .unwrap_or(false),
759 Ns::Env => match key.as_deref() {
760 Some(var) => match val {
761 None => env.get_env(var).map(|v| !v.is_empty()).unwrap_or(false),
762 Some(want) => env
763 .get_env(var)
764 .map(|v| v == want.as_str())
765 .unwrap_or(false),
766 },
767 None => false,
769 },
770 }
771}
772
773#[allow(clippy::disallowed_macros)]
776fn platform_is_windows() -> bool {
777 cfg!(windows)
778}
779
780#[allow(clippy::disallowed_macros)]
782fn platform_is_macos() -> bool {
783 cfg!(target_os = "macos")
784}
785
786#[allow(clippy::disallowed_macros)]
788fn platform_is_linux() -> bool {
789 cfg!(target_os = "linux")
790}
791
792const ARCH_TARGET: &str = std::env::consts::ARCH;
795
796pub fn guard_expr_allows(expr: &GuardExpr, env: &impl EnvLookup) -> bool {
797 match expr {
798 GuardExpr::Predicate(guard) => guard_allows(guard, env),
799 GuardExpr::All(children) => children.iter().all(|g| guard_expr_allows(g, env)),
800 GuardExpr::Or(children) => children.iter().any(|g| guard_expr_allows(g, env)),
801 GuardExpr::Not(child) => !guard_expr_allows(child, env),
802 }
803}
804
805pub fn guard_option_allows(expr: Option<&GuardExpr>, env: &impl EnvLookup) -> bool {
806 match expr {
807 Some(e) => guard_expr_allows(e, env),
808 None => true,
809 }
810}
811
812impl Guard {
813 pub fn is_disjoint_from(&self, other: &Self) -> bool {
822 match (self, other) {
823 (
824 Guard::Attr {
825 ns: n1,
826 key: k1,
827 val: Some(v1),
828 },
829 Guard::Attr {
830 ns: n2,
831 key: k2,
832 val: Some(v2),
833 },
834 ) => n1 == n2 && k1 == k2 && v1 != v2,
835 _ => false,
836 }
837 }
838}
839
840impl GuardExpr {
841 pub fn is_disjoint_from(&self, other: &Self) -> bool {
847 match (self, other) {
848 (x, GuardExpr::Not(inner)) | (GuardExpr::Not(inner), x) => {
856 x == inner.as_ref() || unset_excludes(x, inner.as_ref())
857 }
858 (GuardExpr::Predicate(p1), GuardExpr::Predicate(p2)) => p1.is_disjoint_from(p2),
860 (GuardExpr::Or(arms), other) | (other, GuardExpr::Or(arms)) => {
862 arms.iter().all(|arm| arm.is_disjoint_from(other))
863 }
864 (GuardExpr::All(arms), other) | (other, GuardExpr::All(arms)) => {
866 arms.iter().any(|arm| arm.is_disjoint_from(other))
867 }
868 }
869 }
870}
871
872fn conjuncts(expr: &GuardExpr) -> Vec<&GuardExpr> {
876 match expr {
877 GuardExpr::All(items) => items.iter().collect(),
878 single => vec![single],
879 }
880}
881
882fn closed_domain_values(ns: Ns, key: Option<&str>) -> Option<&'static [&'static str]> {
889 match (ns, key) {
890 (Ns::Bool, None) => Some(&["true", "false"]),
891 (Ns::Os, None) => Some(&["macos", "linux", "windows"]),
892 (Ns::Arch, None) => Some(ARCH_VALUES),
893 _ => None,
894 }
895}
896
897#[derive(Clone)]
900enum DnfLit {
901 Pos(Guard),
902 Neg(Guard),
903}
904
905impl DnfLit {
906 fn as_expr(&self) -> GuardExpr {
907 match self {
908 DnfLit::Pos(guard) => GuardExpr::Predicate(guard.clone()),
909 DnfLit::Neg(guard) => GuardExpr::Not(Box::new(GuardExpr::Predicate(guard.clone()))),
910 }
911 }
912}
913
914const MAX_DNF_DISJUNCTS: usize = 64;
919
920fn dnf_push(expr: &GuardExpr, polarity: bool, out: &mut Vec<Vec<DnfLit>>) -> bool {
925 match (expr, polarity) {
926 (GuardExpr::Predicate(guard), true) => {
927 for disjunct in out.iter_mut() {
928 disjunct.push(DnfLit::Pos(guard.clone()));
929 }
930 true
931 }
932 (GuardExpr::Predicate(guard), false) => {
933 for disjunct in out.iter_mut() {
934 disjunct.push(DnfLit::Neg(guard.clone()));
935 }
936 true
937 }
938 (GuardExpr::All(children), true) | (GuardExpr::Or(children), false) => {
939 for child in children {
941 if !dnf_push(child, polarity, out) {
942 return false;
943 }
944 }
945 true
946 }
947 (GuardExpr::Or(children), true) | (GuardExpr::All(children), false) => {
948 let mut forked = Vec::new();
950 for disjunct in out.drain(..) {
951 for child in children {
952 let mut branch = vec![disjunct.clone()];
953 if !dnf_push(child, polarity, &mut branch) {
954 return false;
955 }
956 forked.extend(branch);
957 if forked.len() > MAX_DNF_DISJUNCTS {
958 return false;
959 }
960 }
961 }
962 *out = forked;
963 true
964 }
965 (GuardExpr::Not(inner), _) => dnf_push(inner, !polarity, out),
966 }
967}
968
969fn disjunct_satisfiable(lits: &[DnfLit]) -> bool {
973 for lit in lits {
977 match lit {
978 DnfLit::Pos(Guard::Attr {
979 ns: Ns::Bool, val, ..
980 }) if val.as_deref() != Some("true") => return false,
981 DnfLit::Neg(Guard::Attr {
982 ns: Ns::Bool,
983 key: None,
984 val: Some(value),
985 }) if value == "true" => return false,
986 _ => {}
987 }
988 }
989 for (index, first) in lits.iter().enumerate() {
992 for second in &lits[index + 1..] {
993 if first.as_expr().is_disjoint_from(&second.as_expr()) {
994 return false;
995 }
996 }
997 }
998 for lit in lits {
1002 let (ns, key) = match lit {
1003 DnfLit::Neg(Guard::Attr {
1004 ns,
1005 key,
1006 val: Some(_),
1007 }) => (*ns, key.as_deref()),
1008 _ => continue,
1009 };
1010 let Some(domain) = closed_domain_values(ns, key) else {
1011 continue;
1012 };
1013 let exhausted = domain.iter().all(|value| {
1014 lits.iter().any(|other| match other {
1015 DnfLit::Neg(Guard::Attr {
1016 ns: other_ns,
1017 key: other_key,
1018 val: Some(other_val),
1019 }) => *other_ns == ns && other_key.as_deref() == key && other_val == value,
1020 _ => false,
1021 })
1022 });
1023 if exhausted {
1024 return false;
1025 }
1026 }
1027 true
1028}
1029
1030fn conjunction_dead(atoms: &[&GuardExpr]) -> bool {
1037 if atoms.is_empty() {
1038 return false;
1039 }
1040 let mut disjuncts = vec![Vec::new()];
1041 for atom in atoms {
1042 if !dnf_push(atom, true, &mut disjuncts) {
1043 return false;
1044 }
1045 }
1046 !disjuncts
1047 .iter()
1048 .any(|disjunct| disjunct_satisfiable(disjunct))
1049}
1050
1051pub fn guard_satisfiable(expr: &GuardExpr) -> bool {
1053 !conjunction_dead(&conjuncts(expr))
1054}
1055
1056pub fn guard_valid(expr: &GuardExpr) -> bool {
1059 match expr {
1060 GuardExpr::Predicate(Guard::Attr {
1061 ns: Ns::Bool,
1062 val: Some(value),
1063 ..
1064 }) => value.as_str() == "true",
1065 GuardExpr::All(items) => items.iter().all(guard_valid),
1066 GuardExpr::Not(inner) => !guard_satisfiable(inner),
1067 _ => false,
1068 }
1069}
1070
1071pub fn guards_overlap(a: Option<&GuardExpr>, b: Option<&GuardExpr>) -> bool {
1074 match (a, b) {
1075 (None, None) => true,
1076 (Some(x), None) | (None, Some(x)) => guard_satisfiable(x),
1077 (Some(x), Some(y)) => {
1078 let mut atoms = conjuncts(x);
1079 atoms.extend(conjuncts(y));
1080 !conjunction_dead(&atoms)
1081 }
1082 }
1083}
1084
1085fn unset_excludes(x: &GuardExpr, inner: &GuardExpr) -> bool {
1088 match (x, inner) {
1089 (
1090 GuardExpr::Predicate(Guard::Attr {
1091 ns: n2, key: k2, ..
1092 }),
1093 GuardExpr::Predicate(Guard::Attr {
1094 ns: n1,
1095 key: k1,
1096 val: None,
1097 }),
1098 ) => n1 == n2 && k1 == k2,
1099 _ => false,
1100 }
1101}
1102
1103impl fmt::Display for Guard {
1104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1110 match self {
1111 Guard::Attr {
1112 ns: Ns::Env,
1113 key: Some(var),
1114 val: None,
1115 } => write!(f, "env:{var}"),
1116 Guard::Attr {
1117 ns: Ns::Env,
1118 key: Some(var),
1119 val: Some(want),
1120 } => write!(f, "eq(env:{var}, {want})"),
1121 Guard::Attr {
1122 ns,
1123 key: _,
1124 val: Some(value),
1125 } => write!(f, "{ns}:{value}"),
1126 Guard::Attr { ns, key, val: None } => match key {
1127 Some(subject) => write!(f, "{ns}:{subject}"),
1128 None => write!(f, "{ns}"),
1129 },
1130 }
1131 }
1132}
1133
1134impl fmt::Display for WorkspaceTarget {
1135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1136 match self {
1137 WorkspaceTarget::Snapshot => write!(f, "SNAPSHOT"),
1138 WorkspaceTarget::Local => write!(f, "LOCAL"),
1139 WorkspaceTarget::Cache { local: false } => write!(f, "CACHE"),
1140 WorkspaceTarget::Cache { local: true } => write!(f, "CACHE --local"),
1141 WorkspaceTarget::System => write!(f, "SYSTEM"),
1142 }
1143 }
1144}
1145
1146impl fmt::Display for Expr {
1147 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1148 match self {
1149 Expr::Literal(v) => write!(f, "{}", v),
1150 Expr::Var(name) => write!(f, "${}", name),
1151 Expr::Env(key) => write!(f, "env:{}", key),
1152 Expr::KeyPath { base, keys } => {
1153 write!(f, "${}", base)?;
1154 for key in keys {
1155 write!(f, ".{}", key)?;
1156 }
1157 Ok(())
1158 }
1159 Expr::Call { name, args } => {
1160 write!(f, "{}(", name)?;
1161 for (i, arg) in args.iter().enumerate() {
1162 if i > 0 {
1163 write!(f, ", ")?;
1164 }
1165 write!(f, "{}", arg)?;
1166 }
1167 write!(f, ")")
1168 }
1169 Expr::Inspect(var) => write!(f, "INSPECT(${})", var),
1170 Expr::List(items) => {
1171 write!(f, "[")?;
1172 for (i, item) in items.iter().enumerate() {
1173 if i > 0 {
1174 write!(f, ", ")?;
1175 }
1176 write!(f, "{}", item)?;
1177 }
1178 write!(f, "]")
1179 }
1180 Expr::Map(entries) => {
1181 write!(f, "{{")?;
1182 for (i, (key, val)) in entries.iter().enumerate() {
1183 if i > 0 {
1184 write!(f, ", ")?;
1185 }
1186 write!(f, "\"{}\": {}", key, val)?;
1187 }
1188 write!(f, "}}")
1189 }
1190 Expr::Block(steps) => {
1191 write!(f, "{{ ")?;
1192 for (i, step) in steps.iter().enumerate() {
1193 if i > 0 {
1194 write!(f, "; ")?;
1195 }
1196 write!(f, "{}", step.kind)?;
1197 }
1198 write!(f, " }}")
1199 }
1200 Expr::Compare { op, left, right } => {
1201 write!(f, "{} {} {}", left, op, right)
1202 }
1203 Expr::Arithmetic { op, left, right } => {
1204 write!(f, "({} {} {})", left, op, right)
1205 }
1206 Expr::CompiledMath(ops) => {
1207 write!(f, "{}", format_compiled_math(ops))
1208 }
1209 Expr::FreshPipe => write!(f, "<fresh pipe>"),
1213 Expr::UnsignedIntBoundary(n) => write!(f, "{}", n),
1214 Expr::Not(inner) => {
1215 match inner.as_ref() {
1218 Expr::Compare { .. } | Expr::Arithmetic { .. } | Expr::CompiledMath(_) => {
1219 write!(f, "!({})", inner)
1220 }
1221 _ => write!(f, "!{}", inner),
1222 }
1223 }
1224 Expr::Logical { op, left, right } => {
1225 write!(f, "({} {} {})", left, op, right)
1226 }
1227 }
1228 }
1229}
1230
1231impl fmt::Display for CompareOp {
1232 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1233 match self {
1234 CompareOp::Eq => write!(f, "=="),
1235 CompareOp::Ne => write!(f, "!="),
1236 CompareOp::Lt => write!(f, "<"),
1237 CompareOp::Le => write!(f, "<="),
1238 CompareOp::Gt => write!(f, ">"),
1239 CompareOp::Ge => write!(f, ">="),
1240 }
1241 }
1242}
1243
1244impl fmt::Display for ArithOp {
1245 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1246 match self {
1247 ArithOp::Add => write!(f, "+"),
1248 ArithOp::Sub => write!(f, "-"),
1249 ArithOp::Mul => write!(f, "*"),
1250 ArithOp::Div => write!(f, "/"),
1251 }
1252 }
1253}
1254
1255fn format_compiled_math(ops: &[MathOp]) -> String {
1260 let mut stack: Vec<String> = Vec::new();
1261 for op in ops {
1262 match op {
1263 MathOp::PushConst(v) => stack.push(format!("{}", v)),
1264 MathOp::LoadVar(name) => stack.push(format!("${}", name)),
1265 MathOp::LoadEnv(key) => stack.push(format!("env:{}", key)),
1266 MathOp::LoadKeyPath { base, keys } => {
1267 let mut s = format!("${}", base);
1268 for key in keys {
1269 s.push('.');
1270 s.push_str(key);
1271 }
1272 stack.push(s);
1273 }
1274 MathOp::Call { name, arity } => {
1275 let mut args = Vec::new();
1276 for _ in 0..*arity {
1277 args.push(stack.pop().unwrap_or_else(|| "<underflow>".to_string()));
1278 }
1279 args.reverse();
1280 stack.push(format!("{}({})", name, args.join(", ")));
1281 }
1282 MathOp::Inspect(name) => stack.push(format!("INSPECT(${})", name)),
1283 MathOp::Neg => {
1284 let inner = stack.pop().unwrap_or_else(|| "<underflow>".to_string());
1285 stack.push(format!("(-{})", inner));
1286 }
1287 MathOp::Add => push_bin(&mut stack, "+"),
1288 MathOp::Sub => push_bin(&mut stack, "-"),
1289 MathOp::Mul => push_bin(&mut stack, "*"),
1290 MathOp::Div => push_bin(&mut stack, "/"),
1291 MathOp::Lt => push_bin(&mut stack, "<"),
1292 MathOp::Le => push_bin(&mut stack, "<="),
1293 MathOp::Gt => push_bin(&mut stack, ">"),
1294 MathOp::Ge => push_bin(&mut stack, ">="),
1295 MathOp::Eq => push_bin(&mut stack, "=="),
1296 MathOp::Ne => push_bin(&mut stack, "!="),
1297 }
1298 }
1299 if stack.len() == 1 {
1300 let mut items = stack;
1301 items.pop().unwrap_or_else(|| "<empty>".to_string())
1302 } else {
1303 stack.join(" ")
1304 }
1305}
1306
1307fn push_bin(stack: &mut Vec<String>, op: &str) {
1308 let right = stack.pop().unwrap_or_else(|| "<underflow>".to_string());
1309 let left = stack.pop().unwrap_or_else(|| "<underflow>".to_string());
1310 stack.push(format!("({} {} {})", left, op, right));
1311}
1312
1313impl fmt::Display for LogicalOp {
1314 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1315 match self {
1316 LogicalOp::And => write!(f, "&&"),
1317 LogicalOp::Or => write!(f, "||"),
1318 }
1319 }
1320}
1321
1322enum GuardDisplayContext {
1323 Root,
1324 InAnyArg,
1325 InNot,
1326 InAll,
1327}
1328
1329impl GuardExpr {
1330 fn fmt_with_ctx(&self, f: &mut fmt::Formatter<'_>, ctx: GuardDisplayContext) -> fmt::Result {
1331 match self {
1332 GuardExpr::Predicate(guard) => write!(f, "{}", guard),
1333 GuardExpr::All(children) => {
1334 let wrap = matches!(
1335 ctx,
1336 GuardDisplayContext::InAnyArg | GuardDisplayContext::InNot
1337 ) && children.len() > 1;
1338 if wrap {
1339 write!(f, "(")?;
1340 }
1341 for (i, child) in children.iter().enumerate() {
1342 if i > 0 {
1343 write!(f, ", ")?;
1344 }
1345 child.fmt_with_ctx(f, GuardDisplayContext::InAll)?;
1346 }
1347 if wrap {
1348 write!(f, ")")?;
1349 }
1350 Ok(())
1351 }
1352 GuardExpr::Or(children) => {
1353 write!(f, "any(")?;
1354 for (i, child) in children.iter().enumerate() {
1355 if i > 0 {
1356 write!(f, ", ")?;
1357 }
1358 child.fmt_with_ctx(f, GuardDisplayContext::InAnyArg)?;
1359 }
1360 write!(f, ")")
1361 }
1362 GuardExpr::Not(child) => {
1363 write!(f, "not(")?;
1364 child.fmt_with_ctx(f, GuardDisplayContext::InNot)?;
1365 write!(f, ")")
1366 }
1367 }
1368 }
1369}
1370
1371impl fmt::Display for GuardExpr {
1372 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1373 self.fmt_with_ctx(f, GuardDisplayContext::Root)
1374 }
1375}
1376
1377impl fmt::Display for Step {
1378 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1379 if let Some(expr) = &self.guard {
1380 write!(f, "[{}] ", expr)?;
1381 }
1382 write!(f, "{}", self.kind)
1383 }
1384}
1385
1386#[cfg(test)]
1387mod tests {
1388 use super::*;
1389 #[test]
1402 fn statement_keywords_match_pest_grammar() {
1403 use pest_meta::{ast::Expr, parser};
1404 use std::collections::{HashMap, HashSet};
1405
1406 let pest_src = include_str!("dsl.pest");
1407 let pairs = parser::parse(parser::Rule::grammar_rules, pest_src)
1408 .expect("dsl.pest must parse as a pest grammar");
1409 let rules = parser::consume_rules(pairs).expect("dsl.pest rules must consume");
1410 let by_name: HashMap<&str, &Expr> = rules
1411 .iter()
1412 .map(|rule| (rule.name.as_str(), &rule.expr))
1413 .collect();
1414
1415 let mut all_literals = HashSet::new();
1416 for rule in &rules {
1417 for node in rule.expr.iter_top_down() {
1418 match node {
1419 Expr::Str(literal) | Expr::Insens(literal) => {
1420 all_literals.insert(literal.clone());
1421 }
1422 _ => {}
1423 }
1424 }
1425 }
1426 for kw in STRUCTURAL_KEYWORDS {
1427 assert!(
1428 all_literals.contains(*kw),
1429 "keyword {kw} in STRUCTURAL_KEYWORDS missing from dsl.pest"
1430 );
1431 }
1432
1433 let mut seen = HashSet::new();
1434 let mut stack = vec!["element".to_string(), "block_element".to_string()];
1435 let mut stmt_literals = HashSet::new();
1436 while let Some(name) = stack.pop() {
1437 if !seen.insert(name.clone()) {
1438 continue;
1439 }
1440 let Some(expr) = by_name.get(name.as_str()) else {
1441 continue;
1442 };
1443 for node in expr.iter_top_down() {
1444 match node {
1445 Expr::Str(literal) | Expr::Insens(literal) => {
1446 stmt_literals.insert(literal.clone());
1447 }
1448 Expr::Ident(dependency) => stack.push(dependency.clone()),
1449 _ => {}
1450 }
1451 }
1452 }
1453 assert!(
1454 seen.contains("element") && seen.contains("block_element"),
1455 "grammar must define element and block_element instruction rules"
1456 );
1457 for kw in stmt_literals {
1458 if kw.len() > 1 && kw.chars().all(|c| c.is_ascii_uppercase()) {
1459 assert!(
1460 crate::Command::is_statement_keyword(&kw)
1461 || CLAUSE_KEYWORDS.contains(&kw.as_str()),
1462 "uppercase literal \"{kw}\" reachable from dsl.pest instruction rules is not a registered statement or clause keyword"
1463 );
1464 }
1465 }
1466 }
1467
1468 #[test]
1472 fn command_names_group_by_category() {
1473 for command in COMMANDS {
1474 let name = command.as_str();
1475 let Some((prefix, _)) = name.split_once('_') else {
1476 continue;
1477 };
1478 assert!(
1479 COMMAND_CATEGORIES.contains(&prefix),
1480 "command {name} introduces unregistered category {prefix}; add it to COMMAND_CATEGORIES"
1481 );
1482 }
1483 }
1484}
1485
1486#[cfg(test)]
1487mod guard_analysis_tests {
1488 use super::*;
1489
1490 fn attr(ns: Ns, key: Option<&str>, val: Option<&str>) -> GuardExpr {
1491 GuardExpr::Predicate(Guard::Attr {
1492 ns,
1493 key: key.map(str::to_string),
1494 val: val.map(str::to_string),
1495 })
1496 }
1497
1498 fn os(value: &str) -> GuardExpr {
1499 attr(Ns::Os, None, Some(value))
1500 }
1501
1502 fn arch(value: &str) -> GuardExpr {
1503 attr(Ns::Arch, None, Some(value))
1504 }
1505
1506 fn unix() -> GuardExpr {
1509 GuardExpr::Or(vec![os("macos"), os("linux")])
1510 }
1511
1512 fn windows() -> GuardExpr {
1513 os("windows")
1514 }
1515
1516 fn env_eq(var: &str, value: &str) -> GuardExpr {
1517 attr(Ns::Env, Some(var), Some(value))
1518 }
1519
1520 fn not(expr: GuardExpr) -> GuardExpr {
1521 GuardExpr::Not(Box::new(expr))
1522 }
1523
1524 #[test]
1528 fn partition_negations_are_jointly_dead() {
1529 let os_exhausted = GuardExpr::All(vec![not(os("macos")), not(os("linux")), not(windows())]);
1530 assert!(
1531 !guard_satisfiable(&os_exhausted),
1532 "no host is outside `macos`/`linux`/`windows`"
1533 );
1534 let os_partial = GuardExpr::All(vec![not(os("macos")), not(os("linux"))]);
1536 assert!(guard_satisfiable(&os_partial), "`windows` is still open");
1537 let bool_exhausted = GuardExpr::All(vec![
1539 not(attr(Ns::Bool, None, Some("true"))),
1540 not(attr(Ns::Bool, None, Some("false"))),
1541 ]);
1542 assert!(!guard_satisfiable(&bool_exhausted));
1543 let env_open = GuardExpr::All(vec![
1545 not(env_eq("MODE", "dev")),
1546 not(env_eq("MODE", "prod")),
1547 ]);
1548 assert!(guard_satisfiable(&env_open), "MODE may hold a third value");
1549 assert!(guard_satisfiable(¬(unix())));
1551 }
1552
1553 #[test]
1556 fn unix_alias_is_os_disjunction() {
1557 assert!(!guards_overlap(Some(&unix()), Some(&windows())));
1558 assert!(guards_overlap(Some(&unix()), Some(&os("macos"))));
1559 let covered = GuardExpr::All(vec![GuardExpr::Not(Box::new(unix())), not(windows())]);
1562 assert!(!guard_satisfiable(&covered));
1563 }
1564
1565 #[test]
1569 fn arch_guards_decide_by_value() {
1570 assert!(!guards_overlap(
1571 Some(&arch("x86_64")),
1572 Some(&arch("aarch64"))
1573 ));
1574 assert!(guards_overlap(Some(&arch("x86_64")), Some(&arch("x86_64"))));
1575 assert!(guards_overlap(Some(&arch("aarch64")), Some(&os("macos"))));
1576 let exhausted = GuardExpr::All(ARCH_VALUES.iter().map(|value| not(arch(value))).collect());
1577 assert!(!guard_satisfiable(&exhausted));
1578 let host = attr(Ns::Arch, None, Some(ARCH_TARGET));
1580 assert!(guard_expr_allows(&host, &HashMap::new()));
1581 }
1582
1583 #[test]
1587 fn platform_overlap_matrix_is_exhaustive() {
1588 let cases = [
1589 ("macos", "macos", true),
1590 ("macos", "linux", false),
1591 ("macos", "windows", false),
1592 ("linux", "linux", true),
1593 ("linux", "windows", false),
1594 ("windows", "windows", true),
1595 ];
1596 for (v1, v2, expected) in cases {
1597 let left = os(v1);
1598 let right = os(v2);
1599 assert_eq!(
1600 guards_overlap(Some(&left), Some(&right)),
1601 expected,
1602 "os:{v1} vs os:{v2}"
1603 );
1604 }
1605 }
1606
1607 #[test]
1610 fn disjoint_pairs_never_cofire_here() {
1611 let macos = os("macos");
1612 let holds = |expr: &GuardExpr| guard_expr_allows(expr, &HashMap::new());
1613 assert!(
1614 !(holds(&macos) && holds(&windows())),
1615 "os:macos and os:windows co-fired"
1616 );
1617 }
1618
1619 #[test]
1620 fn overlap_follows_namespace_semantics() {
1621 assert!(!guards_overlap(Some(&windows()), Some(&unix())));
1623 assert!(!guards_overlap(Some(&os("macos")), Some(&os("linux"))));
1624 assert!(guards_overlap(None, Some(&windows())));
1626 assert!(guards_overlap(None, None));
1627 let dead = GuardExpr::All(vec![os("macos"), windows()]);
1629 assert!(!guard_satisfiable(&dead));
1630 assert!(!guards_overlap(Some(&dead), Some(&os("macos"))));
1631 assert!(guard_valid(&GuardExpr::Predicate(Guard::Attr {
1632 ns: Ns::Bool,
1633 key: None,
1634 val: Some("true".to_string()),
1635 })));
1636 assert!(!guard_valid(&unix()));
1637 }
1638
1639 #[test]
1640 fn env_value_conflicts_decide_without_enumeration() {
1641 let fast = env_eq("MODE", "fast");
1642 let slow = env_eq("MODE", "slow");
1643 assert!(
1644 !guards_overlap(Some(&fast), Some(&slow)),
1645 "same key distinct values are disjoint"
1646 );
1647 let also_fast = env_eq("MODE", "fast");
1648 assert!(guards_overlap(Some(&fast), Some(&also_fast)));
1649 let bare = attr(Ns::Env, Some("MODE"), None);
1651 assert!(guards_overlap(Some(&bare), Some(&fast)));
1652 }
1653}
1654
1655#[cfg(test)]
1656mod guard_algebra_tests {
1657 use super::*;
1658
1659 fn os(value: &str) -> GuardExpr {
1660 GuardExpr::Predicate(Guard::Attr {
1661 ns: Ns::Os,
1662 key: None,
1663 val: Some(value.to_string()),
1664 })
1665 }
1666
1667 #[test]
1668 fn or_distributes_over_disjointness() {
1669 fn env_eq(var: &str, value: &str) -> GuardExpr {
1670 GuardExpr::Predicate(Guard::Attr {
1671 ns: Ns::Env,
1672 key: Some(var.to_string()),
1673 val: Some(value.to_string()),
1674 })
1675 }
1676 let mixed = GuardExpr::Or(vec![env_eq("MODE", "dev"), env_eq("MODE", "prod")]);
1679 assert!(!mixed.is_disjoint_from(&env_eq("MODE", "dev")));
1680 assert!(mixed.is_disjoint_from(&env_eq("MODE", "test")));
1682 let cross = GuardExpr::Or(vec![env_eq("MODE", "dev")]);
1684 let platform = os("windows");
1685 assert!(!cross.is_disjoint_from(&platform));
1686 }
1687
1688 #[test]
1689 fn all_distributes_over_disjointness() {
1690 let stacked = GuardExpr::All(vec![
1691 os("macos"),
1692 GuardExpr::Or(vec![os("macos"), os("linux")]),
1693 os("windows"),
1694 ]);
1695 assert!(stacked.is_disjoint_from(&os("macos")));
1696 let live = GuardExpr::All(vec![
1699 os("macos"),
1700 GuardExpr::Predicate(Guard::Attr {
1701 ns: Ns::Env,
1702 key: Some("MODE".to_string()),
1703 val: Some("dev".to_string()),
1704 }),
1705 ]);
1706 assert!(!live.is_disjoint_from(&os("macos")));
1707 }
1708
1709 #[test]
1710 fn negation_is_structural_not_syntactic() {
1711 let not_win = GuardExpr::Not(Box::new(os("windows")));
1712 assert!(not_win.is_disjoint_from(&os("windows")));
1713 assert!(os("windows").is_disjoint_from(¬_win));
1714 assert!(!not_win.is_disjoint_from(&os("macos")));
1715 }
1716}