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