Skip to main content

guppy/platform/
platform_eval.rs

1// Copyright (c) The cargo-guppy Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::platform::{Platform, PlatformSpec};
5use std::ops::{BitAnd, BitOr};
6use target_spec::TargetSpec;
7
8/// The status of a dependency or feature, which is possibly platform-dependent.
9///
10/// This is a sub-status of [`EnabledStatus`](crate::graph::EnabledStatus).
11#[derive(Copy, Clone, Debug)]
12pub enum PlatformStatus<'g> {
13    /// This dependency or feature is never enabled on any platforms.
14    Never,
15    /// This dependency or feature is always enabled on all platforms.
16    Always,
17    /// The status is platform-dependent.
18    PlatformDependent {
19        /// An evaluator to run queries against.
20        eval: PlatformEval<'g>,
21    },
22}
23
24assert_covariant!(PlatformStatus);
25
26impl<'g> PlatformStatus<'g> {
27    pub(crate) fn new(specs: &'g PlatformStatusImpl) -> Self {
28        match specs {
29            PlatformStatusImpl::Always => PlatformStatus::Always,
30            PlatformStatusImpl::Specs(specs) => {
31                if specs.is_empty() {
32                    PlatformStatus::Never
33                } else {
34                    PlatformStatus::PlatformDependent {
35                        eval: PlatformEval { specs },
36                    }
37                }
38            }
39        }
40    }
41
42    /// Returns true if this dependency is always enabled on all platforms.
43    pub fn is_always(&self) -> bool {
44        match self {
45            PlatformStatus::Always => true,
46            PlatformStatus::PlatformDependent { .. } | PlatformStatus::Never => false,
47        }
48    }
49
50    /// Returns true if this dependency is never enabled on any platform.
51    pub fn is_never(&self) -> bool {
52        match self {
53            PlatformStatus::Never => true,
54            PlatformStatus::PlatformDependent { .. } | PlatformStatus::Always => false,
55        }
56    }
57
58    /// Returns true if this dependency is possibly enabled on any platform.
59    pub fn is_present(&self) -> bool {
60        !self.is_never()
61    }
62
63    /// Evaluates whether this dependency is enabled on the given platform spec.
64    ///
65    /// Returns `Unknown` if the result was unknown, which may happen if
66    /// evaluating against [`PlatformSpec::Platforms`] and the target features
67    /// of one of its platforms are unknown.
68    pub fn enabled_on(&self, platform_spec: &PlatformSpec) -> EnabledTernary {
69        match platform_spec {
70            PlatformSpec::Any => match self {
71                PlatformStatus::Always | PlatformStatus::PlatformDependent { .. } => {
72                    EnabledTernary::Enabled
73                }
74                PlatformStatus::Never => EnabledTernary::Disabled,
75            },
76            PlatformSpec::Always => match self {
77                PlatformStatus::Always => EnabledTernary::Enabled,
78                PlatformStatus::Never | PlatformStatus::PlatformDependent { .. } => {
79                    EnabledTernary::Disabled
80                }
81            },
82            PlatformSpec::Platforms(platforms) => EnabledTernary::or_all(
83                platforms
84                    .iter()
85                    .map(|platform| self.enabled_on_platform(platform)),
86            ),
87        }
88    }
89
90    fn enabled_on_platform(&self, platform: &Platform) -> EnabledTernary {
91        match self {
92            PlatformStatus::Always => EnabledTernary::Enabled,
93            PlatformStatus::Never => EnabledTernary::Disabled,
94            PlatformStatus::PlatformDependent { eval } => eval.eval(platform),
95        }
96    }
97}
98
99/// Whether a dependency or feature is enabled on a specific platform.
100///
101/// This is a ternary or [three-valued logic](https://en.wikipedia.org/wiki/Three-valued_logic)
102/// because the result may be unknown in some situations.
103///
104/// Returned by the methods on `EnabledStatus`, `PlatformStatus`, and `PlatformEval`.
105//
106// Variant order is important: the derived `Ord` (`Disabled < Unknown <
107// Enabled`) is the K3 lattice order, and the `BitAnd`/`BitOr` impls below are
108// defined as `min`/`max` over it. `PlatformSpec`'s documented ordering of
109// query results also relies on it.
110#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
111pub enum EnabledTernary {
112    /// The dependency is disabled on this platform.
113    Disabled,
114    /// The status of this dependency is unknown on this platform.
115    ///
116    /// This may happen if evaluation involves unknown target features. Notably,
117    /// this will not be returned when evaluating only against
118    /// [`Platform::build_target()`], since the target features for the build
119    /// target platform are determined at compile time.
120    Unknown,
121    /// The dependency is enabled on this platform.
122    Enabled,
123}
124
125impl EnabledTernary {
126    fn new(x: Option<bool>) -> Self {
127        match x {
128            Some(false) => EnabledTernary::Disabled,
129            None => EnabledTernary::Unknown,
130            Some(true) => EnabledTernary::Enabled,
131        }
132    }
133
134    /// The K3 OR of all items in the iterator.
135    fn or_all(iter: impl IntoIterator<Item = EnabledTernary>) -> Self {
136        let mut res = EnabledTernary::Disabled;
137        for item in iter {
138            // Short-circuit evaluation if possible.
139            if item == EnabledTernary::Enabled {
140                return EnabledTernary::Enabled;
141            }
142            res = res | item;
143        }
144        res
145    }
146
147    /// Returns true if the status is known (either enabled or disabled).
148    pub fn is_known(self) -> bool {
149        match self {
150            EnabledTernary::Disabled | EnabledTernary::Enabled => true,
151            EnabledTernary::Unknown => false,
152        }
153    }
154}
155
156/// AND operation in Kleene K3 logic.
157impl BitAnd for EnabledTernary {
158    type Output = Self;
159
160    fn bitand(self, rhs: Self) -> Self::Output {
161        self.min(rhs)
162    }
163}
164
165/// OR operation in Kleene K3 logic.
166impl BitOr for EnabledTernary {
167    type Output = Self;
168
169    fn bitor(self, rhs: Self) -> Self {
170        self.max(rhs)
171    }
172}
173
174/// An evaluator for platform-specific dependencies.
175///
176/// This represents a collection of platform specifications, of the sort `cfg(unix)`.
177#[derive(Copy, Clone, Debug)]
178pub struct PlatformEval<'g> {
179    specs: &'g [TargetSpec],
180}
181
182assert_covariant!(PlatformEval);
183
184impl<'g> PlatformEval<'g> {
185    /// Runs this evaluator against the given platform.
186    pub fn eval(&self, platform: &Platform) -> EnabledTernary {
187        EnabledTernary::or_all(
188            self.specs
189                .iter()
190                .map(|spec| EnabledTernary::new(spec.eval(platform))),
191        )
192    }
193
194    /// Returns the [`TargetSpec`] instances backing this evaluator.
195    ///
196    /// The result of [`PlatformEval::eval`] against a platform is a logical OR
197    /// of the results of evaluating the platform against each target spec.
198    pub fn target_specs(&self) -> &'g [TargetSpec] {
199        self.specs
200    }
201}
202
203#[derive(Clone, Debug)]
204pub(crate) enum PlatformStatusImpl {
205    Always,
206    // Empty vector means never.
207    Specs(Vec<TargetSpec>),
208}
209
210impl PlatformStatusImpl {
211    /// Returns true if this is an empty predicate (i.e. will never match).
212    pub(crate) fn is_never(&self) -> bool {
213        match self {
214            PlatformStatusImpl::Always => false,
215            PlatformStatusImpl::Specs(specs) => specs.is_empty(),
216        }
217    }
218
219    pub(crate) fn extend(&mut self, other: &PlatformStatusImpl) {
220        // &mut *self is a reborrow to allow *self to work below.
221        match (&mut *self, other) {
222            (PlatformStatusImpl::Always, _) => {
223                // Always stays the same since it means all specs are included.
224            }
225            (PlatformStatusImpl::Specs(_), PlatformStatusImpl::Always) => {
226                // Mark self as Always.
227                *self = PlatformStatusImpl::Always;
228            }
229            (PlatformStatusImpl::Specs(specs), PlatformStatusImpl::Specs(other)) => {
230                specs.extend_from_slice(other.as_slice());
231            }
232        }
233    }
234
235    pub(crate) fn add_spec(&mut self, spec: Option<&TargetSpec>) {
236        // &mut *self is a reborrow to allow *self to work below.
237        match (&mut *self, spec) {
238            (PlatformStatusImpl::Always, _) => {
239                // Always stays the same since it means all specs are included.
240            }
241            (PlatformStatusImpl::Specs(_), None) => {
242                // Mark self as Always.
243                *self = PlatformStatusImpl::Always;
244            }
245            (PlatformStatusImpl::Specs(specs), Some(spec)) => {
246                specs.push(spec.clone());
247            }
248        }
249    }
250}
251
252impl Default for PlatformStatusImpl {
253    fn default() -> Self {
254        // Empty vector means never.
255        PlatformStatusImpl::Specs(vec![])
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262    use crate::platform::TargetFeatures;
263    use std::sync::Arc;
264
265    fn platform(triple: &'static str, target_features: TargetFeatures) -> Arc<Platform> {
266        Arc::new(Platform::new(triple, target_features).expect("triple is known"))
267    }
268
269    fn specs(specs: &[&'static str]) -> PlatformStatusImpl {
270        PlatformStatusImpl::Specs(
271            specs
272                .iter()
273                .map(|spec| TargetSpec::new(*spec).expect("spec is valid"))
274                .collect(),
275        )
276    }
277
278    fn linux() -> Arc<Platform> {
279        platform("x86_64-unknown-linux-gnu", TargetFeatures::Unknown)
280    }
281
282    fn windows() -> Arc<Platform> {
283        platform("x86_64-pc-windows-msvc", TargetFeatures::Unknown)
284    }
285
286    fn windows_sse() -> Arc<Platform> {
287        platform(
288            "x86_64-pc-windows-msvc",
289            TargetFeatures::features(["sse"].iter().copied()),
290        )
291    }
292
293    #[test]
294    fn platforms_is_a_union() {
295        let status_impl = specs(&["cfg(windows)"]);
296        let status = PlatformStatus::new(&status_impl);
297
298        assert_eq!(
299            status.enabled_on(&PlatformSpec::Platforms(vec![linux(), windows()])),
300            EnabledTernary::Enabled,
301            "enabled on the union of linux and windows",
302        );
303        assert_eq!(
304            status.enabled_on(&PlatformSpec::Platforms(vec![linux()])),
305            EnabledTernary::Disabled,
306            "disabled on the union of just linux",
307        );
308        assert_eq!(
309            status.enabled_on(&PlatformSpec::Platforms(vec![windows()])),
310            EnabledTernary::Enabled,
311            "enabled on the union of just windows",
312        );
313    }
314
315    #[test]
316    fn platforms_empty_is_disabled() {
317        let status_impl = specs(&["cfg(windows)"]);
318        let status = PlatformStatus::new(&status_impl);
319        assert_eq!(
320            status.enabled_on(&PlatformSpec::Platforms(vec![])),
321            EnabledTernary::Disabled,
322            "the empty union is vacuously disabled",
323        );
324
325        // Always is "enabled on every platform", but there are no platforms
326        // here, so the union is still disabled.
327        let always = PlatformStatus::new(&PlatformStatusImpl::Always);
328        assert_eq!(
329            always.enabled_on(&PlatformSpec::Platforms(vec![])),
330            EnabledTernary::Disabled,
331            "an always-enabled status is disabled on the empty union",
332        );
333        assert_eq!(
334            always.enabled_on(&PlatformSpec::Platforms(vec![linux()])),
335            EnabledTernary::Enabled,
336            "an always-enabled status is enabled on a non-empty union",
337        );
338
339        let never_impl = specs(&[]);
340        let never = PlatformStatus::new(&never_impl);
341        assert_eq!(
342            never.enabled_on(&PlatformSpec::Platforms(vec![linux(), windows()])),
343            EnabledTernary::Disabled,
344            "a never-enabled status stays disabled",
345        );
346    }
347
348    #[test]
349    fn platforms_unknown_follows_k3_or() {
350        let status_impl = specs(&["cfg(all(windows, target_feature = \"sse\"))"]);
351        let status = PlatformStatus::new(&status_impl);
352
353        assert_eq!(
354            status.enabled_on(&PlatformSpec::from(windows())),
355            EnabledTernary::Unknown,
356            "unknown target features make the status unknown",
357        );
358        assert_eq!(
359            status.enabled_on(&PlatformSpec::Platforms(vec![linux(), windows()])),
360            EnabledTernary::Unknown,
361            "disabled | unknown is unknown",
362        );
363        assert_eq!(
364            status.enabled_on(&PlatformSpec::Platforms(vec![windows(), windows_sse()])),
365            EnabledTernary::Enabled,
366            "unknown | enabled is enabled",
367        );
368    }
369
370    #[test]
371    fn k3_truth_tables() {
372        use EnabledTernary::*;
373
374        // This is a direct expression of the K3 truth tables, independent of
375        // the derived Ord that BitAnd/BitOr use.
376        let and_table = [
377            (Disabled, Disabled, Disabled),
378            (Disabled, Unknown, Disabled),
379            (Disabled, Enabled, Disabled),
380            (Unknown, Disabled, Disabled),
381            (Unknown, Unknown, Unknown),
382            (Unknown, Enabled, Unknown),
383            (Enabled, Disabled, Disabled),
384            (Enabled, Unknown, Unknown),
385            (Enabled, Enabled, Enabled),
386        ];
387        for (a, b, expected) in and_table {
388            assert_eq!(a & b, expected, "{a:?} & {b:?}");
389        }
390
391        let or_table = [
392            (Disabled, Disabled, Disabled),
393            (Disabled, Unknown, Unknown),
394            (Disabled, Enabled, Enabled),
395            (Unknown, Disabled, Unknown),
396            (Unknown, Unknown, Unknown),
397            (Unknown, Enabled, Enabled),
398            (Enabled, Disabled, Enabled),
399            (Enabled, Unknown, Enabled),
400            (Enabled, Enabled, Enabled),
401        ];
402        for (a, b, expected) in or_table {
403            assert_eq!(a | b, expected, "{a:?} | {b:?}");
404        }
405
406        assert_eq!(EnabledTernary::or_all([]), Disabled, "empty OR is Disabled");
407        assert_eq!(
408            EnabledTernary::or_all([Disabled, Unknown, Disabled]),
409            Unknown,
410            "OR of Disabled and Unknown is Unknown",
411        );
412        assert_eq!(
413            EnabledTernary::or_all([Unknown, Enabled, Disabled]),
414            Enabled,
415            "OR with any Enabled is Enabled",
416        );
417    }
418}
419
420#[cfg(all(test, feature = "proptest1"))]
421mod proptests {
422    use super::*;
423    use crate::platform::TargetFeatures;
424    use proptest::{
425        collection::{SizeRange, vec},
426        prelude::*,
427        sample::select,
428    };
429    use std::sync::Arc;
430
431    /// Target specs chosen so that, across the generated platforms, evaluation
432    /// produces every `EnabledTernary` value.
433    fn target_spec_strategy() -> impl Strategy<Value = TargetSpec> {
434        static SPECS: &[&str] = &[
435            "cfg(windows)",
436            "cfg(unix)",
437            "cfg(not(windows))",
438            "cfg(target_feature = \"sse\")",
439            "cfg(all(unix, target_feature = \"sse\"))",
440            "cfg(any(target_os = \"macos\", target_feature = \"avx\"))",
441            "x86_64-unknown-linux-gnu",
442        ];
443        select(SPECS).prop_map(|spec| TargetSpec::new(spec).expect("spec is valid"))
444    }
445
446    fn status_impl_strategy() -> impl Strategy<Value = PlatformStatusImpl> {
447        prop_oneof![
448            1 => Just(PlatformStatusImpl::Always),
449            // An empty vector is never.
450            4 => vec(target_spec_strategy(), 0..4).prop_map(PlatformStatusImpl::Specs),
451        ]
452    }
453
454    fn platforms_strategy(size: impl Into<SizeRange>) -> impl Strategy<Value = Vec<Arc<Platform>>> {
455        vec(
456            Platform::strategy(any::<TargetFeatures>()).prop_map(Arc::new),
457            size,
458        )
459    }
460
461    fn enabled_on(
462        status_impl: &PlatformStatusImpl,
463        platforms: Vec<Arc<Platform>>,
464    ) -> EnabledTernary {
465        PlatformStatus::new(status_impl).enabled_on(&PlatformSpec::Platforms(platforms))
466    }
467
468    proptest! {
469        #[test]
470        fn platforms_is_the_k3_or_of_its_elements(
471            status_impl in status_impl_strategy(),
472            platforms in platforms_strategy(0..4),
473        ) {
474            let expected = EnabledTernary::or_all(
475                platforms
476                    .iter()
477                    .map(|platform| enabled_on(&status_impl, vec![platform.clone()])),
478            );
479            prop_assert_eq!(enabled_on(&status_impl, platforms), expected);
480        }
481
482        #[test]
483        fn platforms_append_is_or(
484            status_impl in status_impl_strategy(),
485            a in platforms_strategy(0..3),
486            b in platforms_strategy(0..3),
487        ) {
488            let combined: Vec<_> = a.iter().chain(&b).cloned().collect();
489            let expected = enabled_on(&status_impl, a) | enabled_on(&status_impl, b);
490            prop_assert_eq!(enabled_on(&status_impl, combined), expected);
491        }
492
493        #[test]
494        fn platforms_order_and_duplicates_do_not_matter(
495            status_impl in status_impl_strategy(),
496            (platforms, shuffled) in platforms_strategy(0..4).prop_flat_map(|platforms| {
497                (Just(platforms.clone()), Just(platforms).prop_shuffle())
498            }),
499        ) {
500            let expected = enabled_on(&status_impl, platforms.clone());
501            prop_assert_eq!(enabled_on(&status_impl, shuffled), expected, "order");
502            let doubled: Vec<_> = platforms.iter().chain(&platforms).cloned().collect();
503            prop_assert_eq!(enabled_on(&status_impl, doubled), expected, "duplicates");
504        }
505
506        #[test]
507        fn single_platform_matches_direct_eval(
508            status_impl in status_impl_strategy(),
509            platform in Platform::strategy(any::<TargetFeatures>()),
510        ) {
511            let status = PlatformStatus::new(&status_impl);
512            let expected = match status {
513                PlatformStatus::Always => EnabledTernary::Enabled,
514                PlatformStatus::Never => EnabledTernary::Disabled,
515                PlatformStatus::PlatformDependent { eval } => eval.eval(&platform),
516            };
517            prop_assert_eq!(status.enabled_on(&PlatformSpec::from(platform)), expected);
518        }
519
520        #[test]
521        fn specs_are_ordered(
522            status_impl in status_impl_strategy(),
523            platforms in platforms_strategy(1..4),
524        ) {
525            let status = PlatformStatus::new(&status_impl);
526            let empty = status.enabled_on(&PlatformSpec::Platforms(vec![]));
527            let always = status.enabled_on(&PlatformSpec::Always);
528            let some = status.enabled_on(&PlatformSpec::Platforms(platforms));
529            let any = status.enabled_on(&PlatformSpec::Any);
530            prop_assert!(empty <= always, "Platforms([]) <= Always: {:?} <= {:?}", empty, always);
531            prop_assert!(always <= some, "Always <= Platforms(non-empty): {:?} <= {:?}", always, some);
532            prop_assert!(some <= any, "Platforms(non-empty) <= Any: {:?} <= {:?}", some, any);
533        }
534    }
535}