Skip to main content

nextest_filtering/
expression.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::{
5    errors::{FiltersetParseErrors, ParseSingleError},
6    parsing::{
7        DisplayParsedRegex, DisplayParsedString, ExprResult, GenericGlob, ParsedExpr, ParsedLeaf,
8        new_span, parse,
9    },
10};
11use guppy::{
12    PackageId,
13    graph::{BuildTargetId, PackageGraph, PackageMetadata, cargo::BuildPlatform},
14};
15use miette::SourceSpan;
16use nextest_metadata::{RustBinaryId, RustTestBinaryKind, TestCaseName};
17use recursion::{Collapsible, CollapsibleExt, MappableFrame, PartiallyApplied};
18use smol_str::SmolStr;
19use std::{collections::HashSet, fmt, sync::OnceLock};
20
21/// Matcher for name
22///
23/// Used both for package name and test name
24#[derive(Debug, Clone)]
25pub enum NameMatcher {
26    /// Exact value
27    Equal { value: String, implicit: bool },
28    /// Simple contains test
29    Contains { value: String, implicit: bool },
30    /// Test against a glob
31    Glob { glob: GenericGlob, implicit: bool },
32    /// Test against a regex
33    Regex(regex::Regex),
34}
35
36impl NameMatcher {
37    pub(crate) fn implicit_equal(value: String) -> Self {
38        Self::Equal {
39            value,
40            implicit: true,
41        }
42    }
43
44    pub(crate) fn implicit_contains(value: String) -> Self {
45        Self::Contains {
46            value,
47            implicit: true,
48        }
49    }
50}
51
52impl PartialEq for NameMatcher {
53    fn eq(&self, other: &Self) -> bool {
54        match (self, other) {
55            (
56                Self::Contains {
57                    value: s1,
58                    implicit: default1,
59                },
60                Self::Contains {
61                    value: s2,
62                    implicit: default2,
63                },
64            ) => s1 == s2 && default1 == default2,
65            (
66                Self::Equal {
67                    value: s1,
68                    implicit: default1,
69                },
70                Self::Equal {
71                    value: s2,
72                    implicit: default2,
73                },
74            ) => s1 == s2 && default1 == default2,
75            (Self::Regex(r1), Self::Regex(r2)) => r1.as_str() == r2.as_str(),
76            (Self::Glob { glob: g1, .. }, Self::Glob { glob: g2, .. }) => {
77                g1.regex().as_str() == g2.regex().as_str()
78            }
79            _ => false,
80        }
81    }
82}
83
84impl Eq for NameMatcher {}
85
86impl fmt::Display for NameMatcher {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        match self {
89            Self::Equal { value, implicit } => write!(
90                f,
91                "{}{}",
92                if *implicit { "" } else { "=" },
93                DisplayParsedString(value)
94            ),
95            Self::Contains { value, implicit } => write!(
96                f,
97                "{}{}",
98                if *implicit { "" } else { "~" },
99                DisplayParsedString(value)
100            ),
101            Self::Glob { glob, implicit } => write!(
102                f,
103                "{}{}",
104                if *implicit { "" } else { "#" },
105                DisplayParsedString(glob.as_str())
106            ),
107            Self::Regex(r) => write!(f, "/{}/", DisplayParsedRegex(r)),
108        }
109    }
110}
111
112/// A leaf node in a filterset expression tree.
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub enum FiltersetLeaf {
115    /// All tests in packages
116    Packages(HashSet<PackageId>),
117    /// All tests present in this kind of binary.
118    Kind(NameMatcher, SourceSpan),
119    /// The platform a test is built for.
120    Platform(BuildPlatform, SourceSpan),
121    /// All binaries matching a name
122    Binary(NameMatcher, SourceSpan),
123    /// All binary IDs matching a name
124    BinaryId(NameMatcher, SourceSpan),
125    /// All tests matching a name
126    Test(NameMatcher, SourceSpan),
127    /// All tests in the named test group.
128    Group(NameMatcher, SourceSpan),
129    /// The default set of tests to run.
130    Default,
131    /// All tests
132    All,
133    /// No tests
134    None,
135}
136
137impl FiltersetLeaf {
138    /// Returns true if this leaf can only be evaluated at runtime, i.e. it
139    /// requires test names to be available.
140    ///
141    /// Currently, this also returns true (conservatively) for the `Default`
142    /// leaf, which is used to represent the default set of tests to run.
143    pub fn is_runtime_only(&self) -> bool {
144        matches!(self, Self::Test(_, _) | Self::Group(_, _) | Self::Default)
145    }
146}
147
148/// Trait for looking up test group membership during filterset evaluation.
149///
150/// Implemented in `nextest-runner` and passed to
151/// [`CompiledExpr::matches_test_with_groups`] to resolve `group()`
152/// predicates.
153pub trait GroupLookup: fmt::Debug {
154    /// Returns true if the given test is a member of a group matching
155    /// the provided name matcher.
156    fn is_member_test(&self, test: &TestQuery<'_>, matcher: &NameMatcher) -> bool;
157}
158
159/// A query for a binary, passed into [`Filterset::matches_binary`].
160#[derive(Copy, Clone, Debug, Eq, PartialEq)]
161pub struct BinaryQuery<'a> {
162    /// The package ID.
163    pub package_id: &'a PackageId,
164
165    /// The binary ID.
166    pub binary_id: &'a RustBinaryId,
167
168    /// The name of the binary.
169    pub binary_name: &'a str,
170
171    /// The kind of binary this test is (lib, test etc).
172    pub kind: &'a RustTestBinaryKind,
173
174    /// The platform this test is built for.
175    pub platform: BuildPlatform,
176}
177
178/// A query for a specific test, passed into [`Filterset::matches_test`].
179#[derive(Copy, Clone, Debug, Eq, PartialEq)]
180pub struct TestQuery<'a> {
181    /// The binary query.
182    pub binary_query: BinaryQuery<'a>,
183
184    /// The name of the test.
185    pub test_name: &'a TestCaseName,
186}
187
188/// A filterset that has been parsed and compiled.
189///
190/// Used to filter tests to run.
191#[derive(Debug, Clone, PartialEq, Eq)]
192pub struct Filterset {
193    /// The raw expression passed in.
194    pub input: String,
195
196    /// The parsed-but-not-compiled expression.
197    pub parsed: ParsedExpr,
198
199    /// The compiled expression.
200    pub compiled: CompiledExpr,
201}
202
203#[derive(Debug, Clone, PartialEq, Eq)]
204pub enum CompiledExpr {
205    /// Accepts every test not in the given expression
206    Not(Box<CompiledExpr>),
207    /// Accepts every test in either given expression
208    Union(Box<CompiledExpr>, Box<CompiledExpr>),
209    /// Accepts every test in both given expressions
210    Intersection(Box<CompiledExpr>, Box<CompiledExpr>),
211    /// Accepts every test in a set
212    Set(FiltersetLeaf),
213}
214
215impl CompiledExpr {
216    /// Returns a value indicating all tests are accepted by this filterset.
217    pub const ALL: Self = CompiledExpr::Set(FiltersetLeaf::All);
218
219    /// Returns a value indicating if the given binary is accepted by this filterset.
220    ///
221    /// The value is:
222    /// * `Some(true)` if this binary is definitely accepted by this filterset.
223    /// * `Some(false)` if this binary is definitely not accepted.
224    /// * `None` if this binary might or might not be accepted.
225    pub fn matches_binary(&self, query: &BinaryQuery<'_>, cx: &EvalContext<'_>) -> Option<bool> {
226        use ExprFrame::*;
227        Wrapped(self).collapse_frames(|layer: ExprFrame<&FiltersetLeaf, Option<bool>>| {
228            match layer {
229                Set(set) => set.matches_binary(query, cx),
230                Not(a) => a.logic_not(),
231                // TODO: or_else/and_then?
232                Union(a, b) => a.logic_or(b),
233                Intersection(a, b) => a.logic_and(b),
234                Difference(a, b) => a.logic_and(b.logic_not()),
235                Parens(a) => a,
236            }
237        })
238    }
239
240    /// Returns true if the given test is accepted by this filterset.
241    ///
242    /// If a `group()` predicate is encountered, this method panics.
243    /// Only use this for expressions that are guaranteed group-free
244    /// (i.e. compiled with any [`FiltersetKind`] other than
245    /// [`FiltersetKind::Test`], or Test expressions where
246    /// [`has_group_matchers`](Self::has_group_matchers) returns false).
247    pub fn matches_test(&self, query: &TestQuery<'_>, cx: &EvalContext<'_>) -> bool {
248        self.matches_test_impl(query, cx, &NoGroups)
249    }
250
251    /// Returns true if the given test is accepted by this filterset,
252    /// resolving `group()` predicates via the provided lookup.
253    ///
254    /// Use this for [`FiltersetKind::Test`] expressions that contain
255    /// `group()` predicates.
256    pub fn matches_test_with_groups(
257        &self,
258        query: &TestQuery<'_>,
259        cx: &EvalContext<'_>,
260        groups: &dyn GroupLookup,
261    ) -> bool {
262        self.matches_test_impl(query, cx, &WithGroups(groups))
263    }
264
265    fn matches_test_impl(
266        &self,
267        query: &TestQuery<'_>,
268        cx: &EvalContext<'_>,
269        groups: &impl GroupResolver,
270    ) -> bool {
271        use ExprFrame::*;
272        Wrapped(self).collapse_frames(|layer: ExprFrame<&FiltersetLeaf, bool>| match layer {
273            Set(set) => set.matches_test_impl(query, cx, groups),
274            Not(a) => !a,
275            Union(a, b) => a || b,
276            Intersection(a, b) => a && b,
277            Difference(a, b) => a && !b,
278            Parens(a) => a,
279        })
280    }
281
282    /// Returns true if this expression contains any `group()` predicates.
283    pub fn has_group_matchers(&self) -> bool {
284        let mut found = false;
285        Wrapped(self).collapse_frames(|layer: ExprFrame<&FiltersetLeaf, ()>| {
286            if matches!(layer, ExprFrame::Set(FiltersetLeaf::Group(_, _))) {
287                found = true;
288            }
289        });
290        found
291    }
292}
293
294impl NameMatcher {
295    /// Returns true if the given input matches this name matcher.
296    pub fn is_match(&self, input: &str) -> bool {
297        match self {
298            Self::Equal { value, .. } => value == input,
299            Self::Contains { value, .. } => input.contains(value),
300            Self::Glob { glob, .. } => glob.is_match(input),
301            Self::Regex(reg) => reg.is_match(input),
302        }
303    }
304}
305
306impl FiltersetLeaf {
307    fn matches_test_impl(
308        &self,
309        query: &TestQuery<'_>,
310        cx: &EvalContext,
311        groups: &impl GroupResolver,
312    ) -> bool {
313        match self {
314            Self::All => true,
315            Self::None => false,
316            // The default filter is always group-free (group() is banned
317            // in DefaultFilter), so recurse with NoGroups to enforce that
318            // invariant.
319            Self::Default => cx.default_filter.matches_test_impl(query, cx, &NoGroups),
320            Self::Test(matcher, _) => matcher.is_match(query.test_name.as_str()),
321            Self::Binary(matcher, _) => matcher.is_match(query.binary_query.binary_name),
322            Self::BinaryId(matcher, _) => matcher.is_match(query.binary_query.binary_id.as_str()),
323            Self::Platform(platform, _) => query.binary_query.platform == *platform,
324            Self::Kind(matcher, _) => matcher.is_match(query.binary_query.kind.as_str()),
325            Self::Packages(packages) => packages.contains(query.binary_query.package_id),
326            Self::Group(matcher, _) => groups.resolve_group(query, matcher),
327        }
328    }
329
330    fn matches_binary(&self, query: &BinaryQuery<'_>, cx: &EvalContext) -> Option<bool> {
331        match self {
332            Self::All => Logic::top(),
333            Self::None => Logic::bottom(),
334            Self::Default => cx.default_filter.matches_binary(query, cx),
335            Self::Test(_, _) => None,
336            Self::Binary(matcher, _) => Some(matcher.is_match(query.binary_name)),
337            Self::BinaryId(matcher, _) => Some(matcher.is_match(query.binary_id.as_str())),
338            Self::Platform(platform, _) => Some(query.platform == *platform),
339            Self::Kind(matcher, _) => Some(matcher.is_match(query.kind.as_str())),
340            Self::Packages(packages) => Some(packages.contains(query.package_id)),
341            // Group membership cannot be determined at the binary level.
342            Self::Group(_, _) => None,
343        }
344    }
345}
346
347/// Known test group names for validating `group()` predicates.
348///
349/// Passed to [`Filterset::parse`] to control group name validation at
350/// compile time.
351#[derive(Debug)]
352pub enum KnownGroups {
353    /// A known set of valid group names. The `group()` predicate is
354    /// validated against these names during compilation.
355    ///
356    /// `custom_groups` contains only custom (non-`@global`) group names.
357    /// `@global` is always implicitly valid and does not need to be
358    /// included.
359    Known { custom_groups: HashSet<String> },
360
361    /// Group names are not available in this context. If a `group()`
362    /// predicate reaches validation, it indicates a bug: the predicate
363    /// should have been banned during compilation for this filterset kind.
364    Unavailable,
365}
366
367impl KnownGroups {
368    /// Returns true if the given matcher matches any known group name
369    /// (including `@global`).
370    ///
371    /// # Panics
372    ///
373    /// Panics if `self` is `Unavailable`, indicating a bug where `group()`
374    /// bypassed the ban check.
375    pub(crate) fn matches(&self, matcher: &NameMatcher) -> bool {
376        let custom_groups = match self {
377            KnownGroups::Known { custom_groups } => custom_groups,
378            KnownGroups::Unavailable => panic!(
379                "group() validation data is unavailable; \
380                 this is a nextest bug (group() should have been banned \
381                 during compilation for this filterset kind)"
382            ),
383        };
384
385        // Always check @global first.
386        if matcher.is_match(nextest_metadata::GLOBAL_TEST_GROUP) {
387            return true;
388        }
389
390        match matcher {
391            NameMatcher::Equal { value, .. } => custom_groups.contains(value.as_str()),
392            _ => {
393                // For anything more complex than equals, iterate over all
394                // known groups.
395                custom_groups.iter().any(|g| matcher.is_match(g))
396            }
397        }
398    }
399}
400
401/// Inputs to filterset parsing.
402#[derive(Debug)]
403pub struct ParseContext<'g> {
404    /// The package graph.
405    graph: &'g PackageGraph,
406
407    /// Cached data computed on first access.
408    cache: OnceLock<ParseContextCache<'g>>,
409}
410
411impl<'g> ParseContext<'g> {
412    /// Creates a new `ParseContext`.
413    #[inline]
414    pub fn new(graph: &'g PackageGraph) -> Self {
415        Self {
416            graph,
417            cache: OnceLock::new(),
418        }
419    }
420
421    /// Returns the package graph.
422    #[inline]
423    pub fn graph(&self) -> &'g PackageGraph {
424        self.graph
425    }
426
427    pub(crate) fn make_cache(&self) -> &ParseContextCache<'g> {
428        self.cache
429            .get_or_init(|| ParseContextCache::new(self.graph))
430    }
431}
432
433#[derive(Debug)]
434pub(crate) struct ParseContextCache<'g> {
435    pub(crate) workspace_packages: Vec<PackageMetadata<'g>>,
436    // Ordinarily we'd store RustBinaryId here, but that wouldn't allow looking
437    // up a string.
438    pub(crate) binary_ids: HashSet<SmolStr>,
439    pub(crate) binary_names: HashSet<&'g str>,
440}
441
442impl<'g> ParseContextCache<'g> {
443    fn new(graph: &'g PackageGraph) -> Self {
444        let workspace_packages: Vec<_> = graph
445            .resolve_workspace()
446            .packages(guppy::graph::DependencyDirection::Forward)
447            .collect();
448        let (binary_ids, binary_names) = workspace_packages
449            .iter()
450            .flat_map(|pkg| {
451                pkg.build_targets().filter_map(|bt| {
452                    let kind = compute_kind(&bt.id())?;
453                    let binary_id = RustBinaryId::from_parts(pkg.name(), &kind, bt.name());
454                    Some((SmolStr::new(binary_id.as_str()), bt.name()))
455                })
456            })
457            .unzip();
458
459        Self {
460            workspace_packages,
461            binary_ids,
462            binary_names,
463        }
464    }
465}
466
467fn compute_kind(id: &BuildTargetId<'_>) -> Option<RustTestBinaryKind> {
468    match id {
469        // Note this covers both libraries and proc macros, but we treat
470        // libraries the same as proc macros while constructing a `RustBinaryId`
471        // anyway.
472        BuildTargetId::Library => Some(RustTestBinaryKind::LIB),
473        BuildTargetId::Benchmark(_) => Some(RustTestBinaryKind::BENCH),
474        BuildTargetId::Example(_) => Some(RustTestBinaryKind::EXAMPLE),
475        BuildTargetId::BuildScript => {
476            // Build scripts don't have tests in them.
477            None
478        }
479        BuildTargetId::Binary(_) => Some(RustTestBinaryKind::BIN),
480        BuildTargetId::Test(_) => Some(RustTestBinaryKind::TEST),
481        _ => panic!("unknown build target id: {id:?}"),
482    }
483}
484
485/// The kind of filterset being parsed.
486#[derive(Copy, Clone, Debug, PartialEq, Eq)]
487pub enum FiltersetKind {
488    /// A test filterset.
489    Test,
490
491    /// A test archive filterset.
492    TestArchive,
493
494    /// An override filter in a config profile.
495    ///
496    /// Override filters cannot contain `group()` predicates because
497    /// group membership is determined by overrides, which would create
498    /// a circular dependency.
499    OverrideFilter,
500
501    /// A default-filter filterset.
502    ///
503    /// To prevent recursion, default-filter expressions cannot contain `default()` themselves.
504    /// (This is a limited kind of the infinite recursion checking we'll need to do in the future.)
505    DefaultFilter,
506}
507
508impl fmt::Display for FiltersetKind {
509    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
510        match self {
511            Self::Test => write!(f, "test"),
512            Self::OverrideFilter => write!(f, "override-filter"),
513            Self::TestArchive => write!(f, "archive-filter"),
514            Self::DefaultFilter => write!(f, "default-filter"),
515        }
516    }
517}
518
519/// Inputs to filterset evaluation functions.
520#[derive(Copy, Clone, Debug)]
521pub struct EvalContext<'a> {
522    /// The default set of tests to run.
523    pub default_filter: &'a CompiledExpr,
524}
525
526/// Internal trait for resolving `group()` predicates during expression
527/// evaluation. Not public; exposed only through the two `matches_test`
528/// variants on [`CompiledExpr`].
529trait GroupResolver {
530    fn resolve_group(&self, query: &TestQuery<'_>, matcher: &NameMatcher) -> bool;
531}
532
533/// Used by [`CompiledExpr::matches_test`]. Panics if a `group()` leaf
534/// is encountered, since the expression should have been compiled with
535/// a filterset kind that bans `group()`.
536struct NoGroups;
537
538impl GroupResolver for NoGroups {
539    fn resolve_group(&self, _query: &TestQuery<'_>, _matcher: &NameMatcher) -> bool {
540        panic!(
541            "group() predicate in expression where groups are not expected; \
542             this is a nextest bug (group() should be banned during compilation \
543             for this filterset kind)"
544        )
545    }
546}
547
548/// Used by [`CompiledExpr::matches_test_with_groups`]. Delegates to the
549/// provided [`GroupLookup`].
550struct WithGroups<'a>(&'a dyn GroupLookup);
551
552impl GroupResolver for WithGroups<'_> {
553    fn resolve_group(&self, query: &TestQuery<'_>, matcher: &NameMatcher) -> bool {
554        self.0.is_member_test(query, matcher)
555    }
556}
557
558impl Filterset {
559    /// Parse a filterset.
560    pub fn parse(
561        input: String,
562        cx: &ParseContext<'_>,
563        kind: FiltersetKind,
564        known_groups: &KnownGroups,
565    ) -> Result<Self, FiltersetParseErrors> {
566        let mut errors = Vec::new();
567        match parse(new_span(&input, &mut errors)) {
568            Ok(parsed_expr) => {
569                if !errors.is_empty() {
570                    return Err(FiltersetParseErrors::new(input.clone(), errors));
571                }
572
573                match parsed_expr {
574                    ExprResult::Valid(parsed) => {
575                        let compiled = crate::compile::compile(&parsed, cx, kind, known_groups)
576                            .map_err(|errors| FiltersetParseErrors::new(input.clone(), errors))?;
577                        Ok(Self {
578                            input,
579                            parsed,
580                            compiled,
581                        })
582                    }
583                    _ => {
584                        // should not happen
585                        // If an ParsedExpr::Error is produced, we should also have an error inside
586                        // errors and we should already have returned
587                        // IMPROVE this is an internal error => add log to suggest opening an bug ?
588                        Err(FiltersetParseErrors::new(
589                            input,
590                            vec![ParseSingleError::Unknown],
591                        ))
592                    }
593                }
594            }
595            Err(_) => {
596                // should not happen
597                // According to our parsing strategy we should never produce an Err(_)
598                // IMPROVE this is an internal error => add log to suggest opening an bug ?
599                Err(FiltersetParseErrors::new(
600                    input,
601                    vec![ParseSingleError::Unknown],
602                ))
603            }
604        }
605    }
606
607    /// Returns a value indicating if the given binary is accepted by this filterset.
608    ///
609    /// The value is:
610    /// * `Some(true)` if this binary is definitely accepted by this filterset.
611    /// * `Some(false)` if this binary is definitely not accepted.
612    /// * `None` if this binary might or might not be accepted.
613    pub fn matches_binary(&self, query: &BinaryQuery<'_>, cx: &EvalContext<'_>) -> Option<bool> {
614        self.compiled.matches_binary(query, cx)
615    }
616
617    /// Returns true if the given test is accepted by this filterset.
618    ///
619    /// Panics if a `group()` predicate is encountered. See
620    /// [`CompiledExpr::matches_test`] for details.
621    pub fn matches_test(&self, query: &TestQuery<'_>, cx: &EvalContext<'_>) -> bool {
622        self.compiled.matches_test(query, cx)
623    }
624
625    /// Returns true if the given test is accepted by this filterset,
626    /// resolving `group()` predicates via the provided lookup.
627    ///
628    /// See [`CompiledExpr::matches_test_with_groups`] for details.
629    pub fn matches_test_with_groups(
630        &self,
631        query: &TestQuery<'_>,
632        cx: &EvalContext<'_>,
633        groups: &dyn GroupLookup,
634    ) -> bool {
635        self.compiled.matches_test_with_groups(query, cx, groups)
636    }
637
638    /// Returns true if the given expression needs dependencies information to work
639    pub fn needs_deps(raw_expr: &str) -> bool {
640        // the expression needs dependencies expression if it uses deps(..) or rdeps(..)
641        raw_expr.contains("deps")
642    }
643}
644
645/// A propositional logic used to evaluate `Expression` instances.
646///
647/// An `Expression` consists of some predicates and the `any`, `all` and `not` operators. An
648/// implementation of `Logic` defines how the `any`, `all` and `not` operators should be evaluated.
649trait Logic {
650    /// The result of an `all` operation with no operands, akin to Boolean `true`.
651    fn top() -> Self;
652
653    /// The result of an `any` operation with no operands, akin to Boolean `false`.
654    fn bottom() -> Self;
655
656    /// `AND`, which corresponds to the `all` operator.
657    fn logic_and(self, other: Self) -> Self;
658
659    /// `OR`, which corresponds to the `any` operator.
660    fn logic_or(self, other: Self) -> Self;
661
662    /// `NOT`, which corresponds to the `not` operator.
663    fn logic_not(self) -> Self;
664}
665
666/// A boolean logic.
667impl Logic for bool {
668    #[inline]
669    fn top() -> Self {
670        true
671    }
672
673    #[inline]
674    fn bottom() -> Self {
675        false
676    }
677
678    #[inline]
679    fn logic_and(self, other: Self) -> Self {
680        self && other
681    }
682
683    #[inline]
684    fn logic_or(self, other: Self) -> Self {
685        self || other
686    }
687
688    #[inline]
689    fn logic_not(self) -> Self {
690        !self
691    }
692}
693
694/// A three-valued logic -- `None` stands for the value being unknown.
695///
696/// The truth tables for this logic are described on
697/// [Wikipedia](https://en.wikipedia.org/wiki/Three-valued_logic#Kleene_and_Priest_logics).
698impl Logic for Option<bool> {
699    #[inline]
700    fn top() -> Self {
701        Some(true)
702    }
703
704    #[inline]
705    fn bottom() -> Self {
706        Some(false)
707    }
708
709    #[inline]
710    fn logic_and(self, other: Self) -> Self {
711        match (self, other) {
712            // If either is false, the expression is false.
713            (Some(false), _) | (_, Some(false)) => Some(false),
714            // If both are true, the expression is true.
715            (Some(true), Some(true)) => Some(true),
716            // One or both are unknown -- the result is unknown.
717            _ => None,
718        }
719    }
720
721    #[inline]
722    fn logic_or(self, other: Self) -> Self {
723        match (self, other) {
724            // If either is true, the expression is true.
725            (Some(true), _) | (_, Some(true)) => Some(true),
726            // If both are false, the expression is false.
727            (Some(false), Some(false)) => Some(false),
728            // One or both are unknown -- the result is unknown.
729            _ => None,
730        }
731    }
732
733    #[inline]
734    fn logic_not(self) -> Self {
735        self.map(|v| !v)
736    }
737}
738
739pub(crate) enum ExprFrame<Set, A> {
740    Not(A),
741    Union(A, A),
742    Intersection(A, A),
743    Difference(A, A),
744    Parens(A),
745    Set(Set),
746}
747
748impl<Set> MappableFrame for ExprFrame<Set, PartiallyApplied> {
749    type Frame<Next> = ExprFrame<Set, Next>;
750
751    fn map_frame<A, B>(input: Self::Frame<A>, mut f: impl FnMut(A) -> B) -> Self::Frame<B> {
752        use ExprFrame::*;
753        match input {
754            Not(a) => Not(f(a)),
755            // Note: reverse the order because the recursion crate processes
756            // entries via a stack, as LIFO. Calling f(b) before f(a) means
757            // error messages for a show up before those for b.
758            Union(a, b) => {
759                let b = f(b);
760                let a = f(a);
761                Union(a, b)
762            }
763            Intersection(a, b) => {
764                let b = f(b);
765                let a = f(a);
766                Intersection(a, b)
767            }
768            Difference(a, b) => {
769                let b = f(b);
770                let a = f(a);
771                Difference(a, b)
772            }
773            Parens(a) => Parens(f(a)),
774            Set(f) => Set(f),
775        }
776    }
777}
778
779// Wrapped struct to prevent trait impl leakages.
780pub(crate) struct Wrapped<T>(pub(crate) T);
781
782impl<'a> Collapsible for Wrapped<&'a CompiledExpr> {
783    type FrameToken = ExprFrame<&'a FiltersetLeaf, PartiallyApplied>;
784
785    fn into_frame(self) -> <Self::FrameToken as MappableFrame>::Frame<Self> {
786        match self.0 {
787            CompiledExpr::Not(a) => ExprFrame::Not(Wrapped(a.as_ref())),
788            CompiledExpr::Union(a, b) => ExprFrame::Union(Wrapped(a.as_ref()), Wrapped(b.as_ref())),
789            CompiledExpr::Intersection(a, b) => {
790                ExprFrame::Intersection(Wrapped(a.as_ref()), Wrapped(b.as_ref()))
791            }
792            CompiledExpr::Set(f) => ExprFrame::Set(f),
793        }
794    }
795}
796
797impl<'a> Collapsible for Wrapped<&'a ParsedExpr> {
798    type FrameToken = ExprFrame<&'a ParsedLeaf, PartiallyApplied>;
799
800    fn into_frame(self) -> <Self::FrameToken as MappableFrame>::Frame<Self> {
801        match self.0 {
802            ParsedExpr::Not(_, a) => ExprFrame::Not(Wrapped(a.as_ref())),
803            ParsedExpr::Union(_, a, b) => {
804                ExprFrame::Union(Wrapped(a.as_ref()), Wrapped(b.as_ref()))
805            }
806            ParsedExpr::Intersection(_, a, b) => {
807                ExprFrame::Intersection(Wrapped(a.as_ref()), Wrapped(b.as_ref()))
808            }
809            ParsedExpr::Difference(_, a, b) => {
810                ExprFrame::Difference(Wrapped(a.as_ref()), Wrapped(b.as_ref()))
811            }
812            ParsedExpr::Parens(a) => ExprFrame::Parens(Wrapped(a.as_ref())),
813            ParsedExpr::Set(f) => ExprFrame::Set(f),
814        }
815    }
816}