From d54bd0607b32b962542ac5bbf6ba69717fd7a2f6 Mon Sep 17 00:00:00 2001 From: Sean Olson Date: Fri, 12 Jan 2024 16:14:15 -0800 Subject: [PATCH] Reimplement variance in terms of new traits. --- src/encode.rs | 10 +- src/lib.rs | 8 +- src/rule.rs | 54 +-- src/token/mod.rs | 479 +++++++++++++-------- src/token/variance/invariant/mod.rs | 134 ++++++ src/token/variance/{ => invariant}/text.rs | 117 ++--- src/token/variance/mod.rs | 371 +++++++--------- 7 files changed, 670 insertions(+), 503 deletions(-) create mode 100644 src/token/variance/invariant/mod.rs rename src/token/variance/{ => invariant}/text.rs (59%) diff --git a/src/encode.rs b/src/encode.rs index 0763dae..7d18d42 100644 --- a/src/encode.rs +++ b/src/encode.rs @@ -8,7 +8,7 @@ use std::borrow::{Borrow, Cow}; use std::fmt::Display; use thiserror::Error; -use crate::token::Token; +use crate::token::{Bound, Token}; /// A regular expression that never matches. /// @@ -215,7 +215,7 @@ fn encode<'t, A, T>( }, (position, Repetition(repetition)) => { let encoding = { - let (lower, upper) = repetition.bounds(); + let cardinality = repetition.cardinality(); let mut pattern = String::new(); pattern.push_str("(?:"); encode( @@ -224,11 +224,11 @@ fn encode<'t, A, T>( &mut pattern, repetition.tokens().iter(), ); - pattern.push_str(&if let Some(upper) = upper { - format!("){{{},{}}}", lower, upper) + pattern.push_str(&if let Bound::Bounded(upper) = cardinality().upper() { + format!("){{{},{}}}", cardinality.lower(), upper) } else { - format!("){{{},}}", lower) + format!("){{{},}}", cardinality.lower()) }); pattern }; diff --git a/src/lib.rs b/src/lib.rs index f876f1d..fff4e60 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,7 +74,7 @@ use thiserror::Error; use crate::encode::CompileError; use crate::rule::{Checked, RuleError}; -use crate::token::{InvariantText, ParseError, Token, TokenTree, Tokenized}; +use crate::token::{ParseError, Text, Token, TokenTree, Tokenized}; #[cfg(feature = "walk")] use crate::walk::WalkError; @@ -217,8 +217,8 @@ impl Variance { } } -impl From>> for Variance { - fn from(variance: token::Variance>) -> Self { +impl From>> for Variance { + fn from(variance: token::Variance>) -> Self { match variance { token::Variance::Invariant(text) => { Variance::Invariant(PathBuf::from(text.to_string().into_owned())) @@ -817,7 +817,7 @@ impl<'t> Program<'t> for Any<'t> { } fn variance(&self) -> Variance { - self.tree.as_ref().variance::().into() + self.tree.as_ref().variance::().into() } fn is_exhaustive(&self) -> bool { diff --git a/src/rule.rs b/src/rule.rs index 57c47d2..e45dda4 100644 --- a/src/rule.rs +++ b/src/rule.rs @@ -24,7 +24,7 @@ use std::slice; use thiserror::Error; use crate::diagnostics::{CompositeSpan, CorrelatedSpan, SpanExt as _}; -use crate::token::{self, InvariantSize, Token, TokenKind, TokenTree, Tokenized}; +use crate::token::{self, Cardinality, Size, Token, TokenKind, TokenTree, Tokenized}; use crate::{Any, BuildError, Glob, Pattern}; /// Maximum invariant size. @@ -35,7 +35,7 @@ use crate::{Any, BuildError, Glob, Pattern}; /// /// This limit is independent of the back end encoding. This code does not rely on errors in the /// encoder by design, such as size limitations. -const MAX_INVARIANT_SIZE: InvariantSize = InvariantSize::new(0x10000); +const MAX_INVARIANT_SIZE: Size = Size::new(0x10000); trait IteratorExt: Iterator + Sized { fn adjacent(self) -> Adjacent @@ -363,7 +363,7 @@ impl<'t> TryFrom<&'t str> for Checked> { pub fn check(tokenized: Tokenized) -> Result, RuleError> { boundary(&tokenized)?; - bounds(&tokenized)?; + //cardinality(&tokenized)?; // TODO: See `cardinality` below. group(&tokenized)?; size(&tokenized)?; Ok(Checked { inner: tokenized }) @@ -518,7 +518,7 @@ fn group<'t>(tokenized: &Tokenized<'t>) -> Result<(), RuleError<'t>> { let tokens = repetition.tokens(); if let Some(terminals) = tokens.terminals() { check_group(terminals, outer).map_err(diagnose)?; - check_group_repetition(terminals, outer, repetition.bounds()) + check_group_repetition(terminals, outer, repetition.cardinality()) .map_err(diagnose)?; } recurse(expression, tokens.iter(), outer)?; @@ -656,10 +656,10 @@ fn group<'t>(tokenized: &Tokenized<'t>) -> Result<(), RuleError<'t>> { fn check_group_repetition<'t>( terminals: Terminals<&Token>, outer: Outer<'t, 't>, - bounds: (usize, Option), + cardinality: Cardinality, ) -> Result<(), CorrelatedError> { let Outer { left, .. } = outer; - let (lower, _) = bounds; + let lower = cardinality.lower(); match terminals.map(|token| (token, token.kind())) { // The repetition is preceded by a termination; disallow rooted sub-globs with a zero // lower bound. @@ -723,24 +723,28 @@ fn group<'t>(tokenized: &Tokenized<'t>) -> Result<(), RuleError<'t>> { recurse(tokenized.expression(), tokenized.tokens(), Outer::default()) } -fn bounds<'t>(tokenized: &Tokenized<'t>) -> Result<(), RuleError<'t>> { - if let Some((_, token)) = tokenized.walk().find(|(_, token)| match token.kind() { - TokenKind::Repetition(ref repetition) => { - let (lower, upper) = repetition.bounds(); - upper.map_or(false, |upper| upper < lower || upper == 0) - }, - _ => false, - }) { - Err(RuleError::new( - tokenized.expression().clone(), - RuleErrorKind::IncompatibleBounds, - CompositeSpan::spanned("here", *token.annotation()), - )) - } - else { - Ok(()) - } -} +// TODO: `Cardinality` masks this error by ordering its inputs. Either glob expressions allow +// closed repetition bounds to be in any order, or this rule must more directly check the +// relevant fields. This is a great illustration of why differentiating trees (syntactic vs. +// semantic) can be useful! +//fn cardinality<'t>(tokenized: &Tokenized<'t>) -> Result<(), RuleError<'t>> { +// if let Some((_, token)) = tokenized.walk().find(|(_, token)| match token.kind() { +// TokenKind::Repetition(ref repetition) => { +// let (lower, upper) = repetition.bounds(); +// upper.map_or(false, |upper| upper < lower || upper == 0) +// }, +// _ => false, +// }) { +// Err(RuleError::new( +// tokenized.expression().clone(), +// RuleErrorKind::IncompatibleBounds, +// CompositeSpan::spanned("here", *token.annotation()), +// )) +// } +// else { +// Ok(()) +// } +//} fn size<'t>(tokenized: &Tokenized<'t>) -> Result<(), RuleError<'t>> { if let Some((_, token)) = tokenized @@ -750,7 +754,7 @@ fn size<'t>(tokenized: &Tokenized<'t>) -> Result<(), RuleError<'t>> { // revisiting the same tokens to recompute their local variance. .find(|(_, token)| { token - .variance::() + .variance::() .as_invariance() .map_or(false, |size| *size >= MAX_INVARIANT_SIZE) }) diff --git a/src/token/mod.rs b/src/token/mod.rs index dfae2eb..452dbf7 100644 --- a/src/token/mod.rs +++ b/src/token/mod.rs @@ -6,21 +6,18 @@ use std::borrow::Cow; use std::cmp; use std::collections::VecDeque; use std::mem; -use std::path::{PathBuf, MAIN_SEPARATOR}; +use std::path::{PathBuf, MAIN_SEPARATOR_STR}; use std::slice; use std::str; use crate::diagnostics::{Span, Spanned}; -use crate::token::variance::{ - CompositeBreadth, CompositeDepth, ConjunctiveVariance, DisjunctiveVariance, IntoInvariantText, - Invariant, UnitBreadth, UnitDepth, UnitVariance, -}; +use crate::token::variance::invariant::{IntoNominalText, IntoStructuralText}; +use crate::token::variance::{VarianceFold, VarianceTerm}; use crate::{StrExt as _, PATHS_ARE_CASE_INSENSITIVE}; pub use crate::token::parse::{parse, ParseError, ROOT_SEPARATOR_EXPRESSION}; -pub use crate::token::variance::{ - invariant_text_prefix, is_exhaustive, Bound, InvariantSize, InvariantText, Variance, -}; +pub use crate::token::variance::invariant::{Breadth, Depth, Invariant, Size, Text}; +pub use crate::token::variance::{invariant_text_prefix, Bound, Cardinality, Variance}; // TODO: Expression and glob trees differ only in their annotation data. This supports the // distinction, but is inflexible and greatly limits any intermediate representation of a @@ -75,39 +72,50 @@ pub enum When { Never, } +// TODO: Consider the name `Composition` or something similar. #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum Junction { +pub enum Composition { Conjunctive(T), Disjunctive(T), } -impl Junction { +impl Composition { pub fn into_inner(self) -> T { match self { - Junction::Conjunctive(inner) | Junction::Disjunctive(inner) => inner, + Composition::Conjunctive(inner) | Composition::Disjunctive(inner) => inner, + } + } + + pub fn map(self, f: F) -> Composition + where + F: FnOnce(T) -> U, + { + match self { + Composition::Conjunctive(inner) => Composition::Conjunctive(f(inner)), + Composition::Disjunctive(inner) => Composition::Disjunctive(f(inner)), } } pub fn conjunctive(self) -> Option { match self { - Junction::Conjunctive(inner) => Some(inner), + Composition::Conjunctive(inner) => Some(inner), _ => None, } } pub fn disjunctive(self) -> Option { match self { - Junction::Disjunctive(inner) => Some(inner), + Composition::Disjunctive(inner) => Some(inner), _ => None, } } } -impl AsRef for Junction { +impl AsRef for Composition { fn as_ref(&self) -> &T { match self { - Junction::Conjunctive(ref inner) => inner, - Junction::Disjunctive(ref inner) => inner, + Composition::Conjunctive(ref inner) => inner, + Composition::Disjunctive(ref inner) => inner, } } } @@ -137,18 +145,6 @@ impl<'t, A> Tokenized<'t, A> { pub fn expression(&self) -> &Cow<'t, str> { &self.expression } - - pub fn variance(&self) -> Variance - where - T: Invariant, - for<'i> &'i Token<'t, A>: UnitVariance, - { - self.tokens().iter().conjunctive_variance() - } - - pub fn walk(&self) -> Walk<'_, 't, A> { - Walk::from(&self.tokens) - } } impl<'t, A> Tokenized<'t, A> @@ -223,7 +219,7 @@ pub trait Fold<'t, A> { None } - fn accumulate( + fn fold( &mut self, branch: &BranchKind<'t, A>, accumulator: Self::Term, @@ -237,6 +233,22 @@ pub trait Fold<'t, A> { fn term(&mut self, leaf: &LeafKind<'t>) -> Self::Term; } +pub trait FoldBatch<'t, A> { + type Term; + + fn initialize(&mut self, _branch: &BranchKind<'t, A>) -> Option { + None + } + + fn fold(&mut self, branch: &BranchKind<'t, A>, terms: Vec) -> Self::Term; + + fn finalize(&mut self, _branch: &BranchKind<'t, A>, accumulator: Self::Term) -> Self::Term { + accumulator + } + + fn term(&mut self, leaf: &LeafKind<'t>) -> Self::Term; +} + pub trait FoldMap<'t, A> { type Annotation; @@ -286,6 +298,13 @@ impl<'t, A> Token<'t, A> { todo!() } + pub fn fold_batch(&self, f: F) -> Option + where + F: FoldBatch<'t, A>, + { + todo!() + } + pub fn fold_map(self, f: F) -> Token<'t, F::Annotation> where F: FoldMap<'t, A>, @@ -297,12 +316,12 @@ impl<'t, A> Token<'t, A> { Walk::from(self) } - pub fn tokens(&self) -> Option> { + pub fn tokens(&self) -> Option> { self.as_branch().map(BranchKind::tokens) } pub fn conjunction(&self) -> &[Self] { - if let Some(Junction::Conjunctive(tokens)) = self.tokens() { + if let Some(Composition::Conjunctive(tokens)) = self.tokens() { tokens } else { @@ -310,6 +329,11 @@ impl<'t, A> Token<'t, A> { } } + pub fn composition(&self) -> Composition<()> { + self.tokens() + .map_or(Composition::Conjuntive(()), |tokens| tokens.map(|_| ())) + } + pub fn topology(&self) -> &TokenTopology<'t, A> { &self.topology } @@ -496,9 +520,9 @@ impl<'t, A> BranchKind<'t, A> { } } - pub fn tokens(&self) -> Junction<&[Token<'t, A>]> { + pub fn tokens(&self) -> Composition<&[Token<'t, A>]> { use BranchKind::{Alternation, Concatenation, Repetition}; - use Junction::{Conjunctive, Disjunctive}; + use Composition::{Conjunctive, Disjunctive}; match self { Alternation(alternative) => Disjunctive(alternative.tokens()), @@ -617,36 +641,29 @@ impl<'t, A> From>> for Alternation<'t, A> { } } -//impl<'i, 't, A> UnitBreadth for &'i Alternation<'t, A> { -// fn unit_breadth(self) -> Bound { -// self.branches() -// .iter() -// .map(|tokens| tokens.iter().composite_breadth()) -// .composite_breadth() -// } -//} -// -//impl<'i, 't, A> UnitDepth for &'i Alternation<'t, A> { -// fn unit_depth(self) -> Bound { -// self.branches() -// .iter() -// .map(|tokens| tokens.iter().composite_depth()) -// .composite_depth() -// } -//} -// -//impl<'i, 't, A, T> UnitVariance for &'i Alternation<'t, A> -//where -// T: Invariance, -// &'i Token<'t, A>: UnitVariance, -//{ -// fn unit_variance(self) -> Variance { -// self.branches() -// .iter() -// .map(|tokens| tokens.iter().conjunctive_variance()) -// .disjunctive_variance() -// } -//} +impl<'t, A> VarianceFold for Alternation<'t, A> { + fn fold(&self, terms: Vec>) -> Variance { + terms.into_iter().reduce(variance::disjunction) + } +} + +impl<'t, A> VarianceFold for Alternation<'t, A> { + fn fold(&self, terms: Vec>) -> Variance { + terms.into_iter().reduce(variance::disjunction) + } +} + +impl<'t, A> VarianceFold for Alternation<'t, A> { + fn fold(&self, terms: Vec>) -> Variance { + terms.into_iter().reduce(variance::disjunction) + } +} + +impl<'t, A> VarianceFold> for Alternation<'t, A> { + fn fold(&self, terms: Vec>>) -> Variance> { + terms.into_iter().reduce(variance::disjunction) + } +} #[derive(Clone, Copy, Debug)] pub enum Archetype { @@ -655,11 +672,40 @@ pub enum Archetype { // should elegantly handle Unicode arguments that cannot be represented this way. For // example, what happens if a user specifies a range between two grapheme clusters that // each require more than one 32-bit code point? + // + // It may be reasonable to restrict ranges to ASCII, though that isn't strictly + // necessary. Either way, support for predefined classes will be important. For example, + // it isn't yet possible to select classes like `{greek}` or `{flag}`. Such support may + // relieve the limitations of a code point range. Range(char, char), } -impl Archetype { - fn variance(&self) -> Variance { +impl From for Archetype { + fn from(literal: char) -> Self { + Archetype::Character(literal) + } +} + +impl From<(char, char)> for Archetype { + fn from(range: (char, char)) -> Self { + Archetype::Range(range.0, range.1) + } +} + +impl VarianceTerm for Archetype { + fn term(&self) -> Variance { + // TODO: Examine the archetype instead of blindly assuming a constant size. This becomes + // especially important if character classes gain support for named classes, as these + // may contain graphemes that cannot be encoded as a single code point. See related + // comments on `Archetype::Range`. + // This is pessimistic and assumes that the code point will require four bytes when encoded + // as UTF-8. This is possible, but most commonly only one or two bytes will be required. + Variance::Invariant(4.into()) + } +} + +impl VarianceTerm> for Archetype { + fn term(&self) -> Variance> { match self { Archetype::Character(x) => { if PATHS_ARE_CASE_INSENSITIVE { @@ -678,34 +724,7 @@ impl Archetype { } }, } - } -} - -impl From for Archetype { - fn from(literal: char) -> Self { - Archetype::Character(literal) - } -} - -impl From<(char, char)> for Archetype { - fn from(range: (char, char)) -> Self { - Archetype::Range(range.0, range.1) - } -} - -impl<'i, 't> UnitVariance> for &'i Archetype { - fn unit_variance(self) -> Variance> { - self.variance() - .map_invariance(|invariance| invariance.to_string().into_nominal_text()) - } -} - -impl<'i> UnitVariance for &'i Archetype { - fn unit_variance(self) -> Variance { - // This is pessimistic and assumes that the code point will require four bytes when encoded - // as UTF-8. This is technically possible, but most commonly only one or two bytes will be - // required. - self.variance().map_invariance(|_| 4.into()) + .map_invariance(|invariance| invariance.to_string().into_nominal_text()) } } @@ -723,27 +742,45 @@ impl Class { pub fn is_negated(&self) -> bool { self.is_negated } + + fn fold(&self, f: F) -> Variance + where + Archetype: VarianceTerm, + F: FnMut(Variance) -> Variance, + { + self.archetypes().iter().map(Archetype::term).reduce(f) + } } -impl<'i> UnitBreadth for &'i Class {} +impl VarianceTerm for Class { + fn term(&self) -> Variance { + Variance::identity() + } +} -impl<'i> UnitDepth for &'i Class {} +impl VarianceTerm for Class { + fn term(&self) -> Variance { + Variance::identity() + } +} -impl<'i, T> UnitVariance for &'i Class -where - &'i Archetype: UnitVariance, - T: Invariant, -{ - fn unit_variance(self) -> Variance { +impl VarianceTerm for Class { + fn term(&self) -> Variance { + self.fold(variance::disjunction) + } +} + +impl<'t> VarianceTerm> for Class { + fn term(&self) -> Variance> { if self.is_negated { // It is not feasible to encode a character class that matches all UTF-8 text and // therefore nothing when negated, and so a character class must be variant if it is - // negated. + // negated and is furthermore assumed to be bounded. Variance::Variant(Bound::Bounded) } else { // TODO: This ignores casing groups, such as in the pattern `[aA]`. - self.archetypes().iter().disjunctive_variance() + self.fold(variance::disjunction) } } } @@ -767,7 +804,31 @@ impl<'t, A> From>> for Concatenation<'t, A> { } } -#[derive(Clone, Copy, Debug)] +impl<'t, A> VarianceFold for Concatenation<'t, A> { + fn fold(&self, terms: Vec>) -> Variance { + terms.into_iter().reduce(variance::disjunction) + } +} + +impl<'t, A> VarianceFold for Concatenation<'t, A> { + fn fold(&self, terms: Vec>) -> Variance { + terms.into_iter().reduce(variance::union) + } +} + +impl<'t, A> VarianceFold for Concatenation<'t, A> { + fn fold(&self, terms: Vec>) -> Variance { + terms.into_iter().reduce(variance::conjunction) + } +} + +impl<'t, A> VarianceFold> for Concatenation<'t, A> { + fn fold(&self, terms: Vec>>) -> Variance> { + terms.into_iter().reduce(variance::conjunction) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Evaluation { Eager, Lazy, @@ -786,7 +847,7 @@ impl<'t> Literal<'t> { fn variance(&self) -> Variance<&Cow<'t, str>> { if self.has_variant_casing() { - Variance::Variant(Bound::Bounded) + Variance::Variant(Bound::Bounded(())) } else { Variance::Invariant(&self.text) @@ -804,21 +865,29 @@ impl<'t> Literal<'t> { } } -impl<'i, 't> UnitBreadth for &'i Literal<'t> {} +impl<'t> VarianceTerm for Literal<'t> { + fn term(&self) -> Variance { + Variance::identity() + } +} -impl<'i, 't> UnitDepth for &'i Literal<'t> {} +impl<'t> VarianceTerm for Literal<'t> { + fn term(&self) -> Variance { + Variance::identity() + } +} -impl<'i, 't> UnitVariance> for &'i Literal<'t> { - fn unit_variance(self) -> Variance> { +impl<'t> VarianceTerm for Literal<'t> { + fn term(&self) -> Variance { self.variance() - .map_invariance(|invariance| invariance.clone().into_nominal_text()) + .map_invariance(|invariance| invariance.as_bytes().len().into()) } } -impl<'i, 't> UnitVariance for &'i Literal<'t> { - fn unit_variance(self) -> Variance { +impl<'t> VarianceTerm> for Literal<'t> { + fn term(&self) -> Variance> { self.variance() - .map_invariance(|invariance| invariance.as_bytes().len().into()) + .map_invariance(|invariance| invariance.clone().into_nominal_text()) } } @@ -852,73 +921,87 @@ impl<'t, A> Repetition<'t, A> { &self.tokens } - pub fn bounds(&self) -> (usize, Option) { - (self.lower, self.upper) + pub fn cardinality(&self) -> Cardinality { + Cardinality::from(( + self.lower, + match self.upper { + Some(upper) => Bound::Bounded(upper), + _ => Bound::Unbounded, + }, + )) } - pub fn is_converged(&self) -> bool { - self.upper.map_or(false, |upper| self.lower == upper) + fn repeat_or_unbounded(&self, accumulator: Variance) -> Variance + where + T: Invariant, + { + // NOTE: This completely discards any bounds on the cardinality of the repetition when + // variant. For example, no distinction is made between `0,4` (finite) and `0,` + // (infinite). + self.repeat_or_else(accumulator, |accumulator| { + variance::conjunction(accumulator, Variance::unbounded()) + }) + } + + fn repeat_or_else(&self, accumulator: Variance, f: F) -> Variance + where + T: Invariant, + F: FnOnce(Variance) -> Variance, + { + if let Some(&n) = self.cardinality().convergence() { + // Repeating invariance can cause overflows, very large allocations, and very + // inefficient comparisons (e.g., comparing very large strings). This is detected by + // both `encode::compile` and `rule::check` (in distinct but similar ways). Querying + // token trees for their invariance must be done with care (after using these + // functions) to avoid expanding pathological invariant expressions like + // ``. + accumulator.map_invariance(|text| text * n) + } + else { + f(accumulator) + } } } -impl<'i, 't, A> UnitBreadth for &'i Repetition<'t, A> { - fn unit_breadth(self) -> Bound { - self.tokens().iter().composite_breadth() +impl<'t, A> VarianceFold for Repetition<'t, A> { + fn fold(&self, terms: Vec>) -> Variance { + terms.into_iter().reduce(variance::disjunction) } } -impl<'i, 't, A> UnitDepth for &'i Repetition<'t, A> { - fn unit_depth(self) -> Bound { - let (_, upper) = self.bounds(); - if upper.is_none() && self.walk().any(|(_, token)| token.is_component_boundary()) { - Bound::Unbounded - } - else { - Bound::Bounded - } +impl<'t, A> VarianceFold for Repetition<'t, A> { + fn fold(&self, terms: Vec>) -> Variance { + terms.into_iter().reduce(variance::union) + } + + fn finalize(&self, accumulator: Variance) -> Variance { + self.repeat_or_else(accumulator, |accumulator| match accumulator { + Variance::Variant(ref depth) if depth.as_ref() == &0 => accumulator, + _ => variance::union( + accumulator, + Variance::Variant(self.cardinality().upper().map_bounded(|_| ())), + ), + }) } } -impl<'i, 't, A, T> UnitVariance for &'i Repetition<'t, A> -where - T: Invariant, - &'i Token<'t, A>: UnitVariance, -{ - fn unit_variance(self) -> Variance { - use Bound::Unbounded; - use TokenKind::Separator; - use Variance::Variant; +impl<'t, A> VarianceFold for Repetition<'t, A> { + fn fold(&self, terms: Vec>) -> Variance { + terms.into_iter().reduce(variance::conjunction) + } - let variance = self - .tokens() - .iter() - // Coalesce tokens with open variance with separators. This isn't destructive and - // doesn't affect invariance, because this only happens in the presence of open - // variance, which means that the repetition is variant (and has no invariant size or - // text). - .coalesce(|left, right| { - match ( - (left.kind(), left.unit_variance()), - (right.kind(), right.unit_variance()), - ) { - ((Separator(_), _), (_, Variant(Unbounded))) => Ok(right), - ((_, Variant(Unbounded)), (Separator(_), _)) => Ok(left), - _ => Err((left, right)), - } - }) - .conjunctive_variance(); - match self.upper { - // Repeating invariance can cause overflows, very large allocations, and very - // inefficient comparisons (e.g., comparing very large strings). This is detected by - // both `encode::compile` and `rule::check` (in distinct but similar ways). Querying - // token trees for their invariance must be done with care (after using these - // functions) to avoid expanding pathological invariant expressions like - // ``. - Some(_) if self.is_converged() => { - variance.map_invariance(|invariance| invariance * self.lower) - }, - _ => variance + Variant(Unbounded), - } + fn finalize(&self, accumulator: Variance) -> Variance { + self.repeat_or_unbounded(accumulator) + } +} + +impl<'t, A> VarianceFold> for Repetition<'t, A> { + fn fold(&self, terms: Vec>>) -> Variance> { + terms.into_iter().reduce(variance::conjunction) + } + + fn finalize(&self, accumulator: Variance>) -> Variance> { + self.repeat_or_unbounded(accumulator) } } @@ -926,24 +1009,30 @@ where pub struct Separator; impl Separator { - pub fn invariant_text() -> String { - MAIN_SEPARATOR.to_string() - } + const INVARIANT_TEXT: &str = MAIN_SEPARATOR_STR; } -impl<'i> UnitBreadth for &'i Separator {} +impl VarianceTerm for Separator { + fn term(&self) -> Variance { + Variance::identity() + } +} -impl<'i> UnitDepth for &'i Separator {} +impl VarianceTerm for Separator { + fn term(&self) -> Variance { + Variance::Invariant(1.into()) + } +} -impl<'i, 't> UnitVariance> for &'i Separator { - fn unit_variance(self) -> Variance> { - Variance::Invariant(Separator::invariant_text().into_structural_text()) +impl VarianceTerm for Separator { + fn term(&self) -> Variance { + Variance::Invariant(Separator::INVARIANT_TEXT.as_bytes().len().into()) } } -impl<'i> UnitVariance for &'i Separator { - fn unit_variance(self) -> Variance { - Variance::Invariant(Separator::invariant_text().as_bytes().len().into()) +impl<'t> VarianceTerm> for Separator { + fn term(&self) -> Variance> { + Variance::Invariant(Separator::INVARIANT_TEXT.into_structural_text()) } } @@ -963,24 +1052,30 @@ impl Wildcard { } } -impl<'i> UnitBreadth for &'i Wildcard { - fn unit_breadth(self) -> Bound { +impl VarianceTerm for Wildcard { + fn term(&self) -> Variance { match self { - Wildcard::One => Bound::Bounded, - _ => Bound::Unbounded, + Wildcard::One => Variance::identity(), + _ => Variance::unbounded(), } } } -impl<'i> UnitDepth for &'i Wildcard { - fn unit_depth(self) -> Bound { +impl VarianceTerm for Wildcard { + fn term(&self) -> Variance { match self { - Wildcard::Tree { .. } => Bound::Unbounded, - _ => Bound::Bounded, + Wildcard::Tree { .. } => Variance::unbounded(), + _ => Variance::identity(), } } } +impl VarianceTerm> for Wildcard { + fn term(&self) -> Variance> { + Variance::unbounded() + } +} + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum Position { Conjunctive { depth: usize }, @@ -1091,7 +1186,7 @@ where fn next(&mut self) -> Option { if let Some((position, token)) = self.buffer.pop_front() { match token.tokens() { - Some(Junction::Conjunctive(tokens)) => self + Some(Composition::Conjunctive(tokens)) => self .buffer .extend(tokens.iter().map(|token| (position.converge(), token))), // TODO: Previously, this pushed a divergent position **for each token in an @@ -1099,7 +1194,7 @@ where // within an alternation, but these are (almost?) always themselves // concatenations now. The tokens within such a concatenation will be // conjunctive. Some code may break due to bad assumptions here. - Some(Junction::Disjunctive(tokens)) => self.buffer.extend( + Some(Composition::Disjunctive(tokens)) => self.buffer.extend( tokens .iter() .enumerate() @@ -1180,9 +1275,11 @@ impl<'i, 't, A> Component<'i, 't, A> { pub fn variance(&self) -> Variance where T: Invariant, - &'i Token<'t, A>: UnitVariance, { - self.0.iter().copied().conjunctive_variance() + // TODO: It seems that components are _always_ conjunctive (or, put another way, derived + // from the conjunction of a token). This should likely apply `variance::conjunction` + // to the variance of the tokens in the component. + todo!() } pub fn depth(&self) -> Bound { diff --git a/src/token/variance/invariant/mod.rs b/src/token/variance/invariant/mod.rs new file mode 100644 index 0000000..03f05ff --- /dev/null +++ b/src/token/variance/invariant/mod.rs @@ -0,0 +1,134 @@ +mod text; + +use std::ops::{Add, Mul}; + +pub use crate::token::variance::invariant::text::{IntoNominalText, IntoStructuralText, Text}; + +// TODO: Require an associated type for the bounds of the invariant. This can be forwarded to +// `Bound` and allow an invariant to specify domain-specific bounds in the variant case. Note +// that this requires a bound on `Invariant` in `Variance` or else `Variance` would need to +// introduce an additional type parameter and the spelling would become +// `Variance`, which is a bit unwieldy. +pub trait Invariant: + Add + Eq + Mul + PartialEq + Sized +{ + fn identity() -> Self; + + fn once() -> Self { + Self::identity() + } +} + +// NOTE: Breadth is probably the least "interesting" invariant to query. The variance w.r.t. +// breadth is critical in determining the exhaustiveness of a glob (amongst other useful +// things), but the bounds of both invariant and variant breadth are not particularly useful. +// Moreover, the "most correct" metric for such bounds is likely Unicode width, which +// introduces some additional dependencies and some strange edge cases (though these are very +// unlikely to occur in real paths). For now, no bounds are supported w.r.t. breadth. Revisit +// this as needed (and perhaps decompose and relocate this comment as documentation, internal +// or otherwise). +macro_rules! impl_invariant_unit { + ($name:ident $(,)?) => { + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct $name; + + impl $name { + pub const fn new() -> Self { + $name + } + } + + impl Add<$name> for $name { + type Output = Self; + + fn add(self, _: $name) -> Self::Output { + $name + } + } + + impl Invariant for $name { + fn identity() -> Self { + $name + } + } + + impl Mul for $name { + type Output = Self; + + fn mul(self, _: usize) -> Self::Output { + $name + } + } + }; +} +impl_invariant_unit!(Breadth); + +macro_rules! impl_invariant_usize { + ($name:ident $(,)?) => { + impl_invariant_usize!($name, identity => 0, once => 0); + }; + ($name:ident, identity => $empty:expr, once => $once:expr $(,)?) => { + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct $name(usize); + + impl $name { + pub const fn new(n: usize) -> Self { + $name(n) + } + } + + impl Add<$name> for $name { + type Output = Self; + + fn add(self, rhs: $name) -> Self::Output { + $name( + self.0 + .checked_add(rhs.0) + .expect("overflow determining invariant"), + ) + } + } + + impl AsRef for $name { + fn as_ref(&self) -> &usize { + &self.0 + } + } + + impl From for $name { + fn from(n: usize) -> $name { + $name(n) + } + } + + impl From<$name> for usize { + fn from(size: $name) -> usize { + size.0 + } + } + + impl Invariant for $name { + fn identity() -> Self { + $name($empty) + } + + fn once() -> Self { + $name($once) + } + } + + impl Mul for $name { + type Output = Self; + + fn mul(self, rhs: usize) -> Self::Output { + $name( + self.0 + .checked_mul(rhs) + .expect("overflow determining invariant"), + ) + } + } + }; +} +impl_invariant_usize!(Depth, identity => 0, once => 1); +impl_invariant_usize!(Size); diff --git a/src/token/variance/text.rs b/src/token/variance/invariant/text.rs similarity index 59% rename from src/token/variance/text.rs rename to src/token/variance/invariant/text.rs index 9edb93d..c6d23bf 100644 --- a/src/token/variance/text.rs +++ b/src/token/variance/invariant/text.rs @@ -3,32 +3,38 @@ use std::collections::VecDeque; use std::ops::{Add, Mul}; use crate::encode; -use crate::token::variance::Invariant; +use crate::token::variance::invariant::Invariant; use crate::PATHS_ARE_CASE_INSENSITIVE; -pub trait IntoInvariantText<'t> { - fn into_nominal_text(self) -> InvariantText<'t>; - - fn into_structural_text(self) -> InvariantText<'t>; +pub trait IntoNominalText<'t> { + fn into_nominal_text(self) -> Text<'t>; } -impl<'t> IntoInvariantText<'t> for Cow<'t, str> { - fn into_nominal_text(self) -> InvariantText<'t> { - InvariantFragment::Nominal(self).into() +impl<'t> IntoNominalText<'t> for Cow<'t, str> { + fn into_nominal_text(self) -> Text<'t> { + Fragment::Nominal(self).into() } +} - fn into_structural_text(self) -> InvariantText<'t> { - InvariantFragment::Structural(self).into() +impl IntoNominalText<'static> for String { + fn into_nominal_text(self) -> Text<'static> { + Fragment::Nominal(self.into()).into() } } -impl IntoInvariantText<'static> for String { - fn into_nominal_text(self) -> InvariantText<'static> { - InvariantFragment::Nominal(self.into()).into() +pub trait IntoStructuralText<'t> { + fn into_structural_text(self) -> Text<'t>; +} + +impl<'t> IntoStructuralText<'t> for Cow<'t, str> { + fn into_structural_text(self) -> Text<'t> { + Fragment::Structural(self).into() } +} - fn into_structural_text(self) -> InvariantText<'static> { - InvariantFragment::Structural(self.into()).into() +impl IntoStructuralText<'static> for String { + fn into_structural_text(self) -> Text<'static> { + Fragment::Structural(self.into()).into() } } @@ -36,24 +42,21 @@ impl IntoInvariantText<'static> for String { // fragments that are equivalent to an aggregated fragment. This works, but relies on // constructing `InvariantText` by consistently appending fragments. #[derive(Clone, Debug, Eq, PartialEq)] -pub struct InvariantText<'t> { - fragments: VecDeque>, +pub struct Text<'t> { + fragments: VecDeque>, } -impl<'t> InvariantText<'t> { +impl<'t> Text<'t> { pub fn new() -> Self { - InvariantText { + Text { fragments: VecDeque::new(), } } - pub fn into_owned(self) -> InvariantText<'static> { - let InvariantText { fragments } = self; - InvariantText { - fragments: fragments - .into_iter() - .map(InvariantFragment::into_owned) - .collect(), + pub fn into_owned(self) -> Text<'static> { + let Text { fragments } = self; + Text { + fragments: fragments.into_iter().map(Fragment::into_owned).collect(), } } @@ -70,12 +73,12 @@ impl<'t> InvariantText<'t> { self } else { - let InvariantText { fragments } = self; + let Text { fragments } = self; let n = (n - 1) .checked_mul(fragments.len()) .expect("overflow determining invariant text"); let first = fragments.clone(); - InvariantText { + Text { fragments: first .into_iter() .chain(fragments.into_iter().cycle().take(n)) @@ -85,58 +88,58 @@ impl<'t> InvariantText<'t> { } } -impl<'t> Add for InvariantText<'t> { +impl<'t> Add for Text<'t> { type Output = Self; fn add(self, other: Self) -> Self::Output { - let InvariantText { + let Text { fragments: mut left, } = self; - let InvariantText { + let Text { fragments: mut right, } = other; let end = left.pop_back(); let start = right.pop_front(); - let InvariantText { fragments: middle } = match (end, start) { + let Text { fragments: middle } = match (end, start) { (Some(left), Some(right)) => left + right, (Some(middle), None) | (None, Some(middle)) => middle.into(), - (None, None) => InvariantText::new(), + (None, None) => Text::new(), }; - InvariantText { + Text { fragments: left.into_iter().chain(middle).chain(right).collect(), } } } -impl<'t> Add> for InvariantText<'t> { +impl<'t> Add> for Text<'t> { type Output = Self; - fn add(self, fragment: InvariantFragment<'t>) -> Self::Output { + fn add(self, fragment: Fragment<'t>) -> Self::Output { self + Self::from(fragment) } } -impl<'t> Default for InvariantText<'t> { +impl<'t> Default for Text<'t> { fn default() -> Self { Self::new() } } -impl<'t> From> for InvariantText<'t> { - fn from(fragment: InvariantFragment<'t>) -> Self { - InvariantText { +impl<'t> From> for Text<'t> { + fn from(fragment: Fragment<'t>) -> Self { + Text { fragments: [fragment].into_iter().collect(), } } } -impl<'t> Invariant for InvariantText<'t> { - fn empty() -> Self { - InvariantText::new() +impl<'t> Invariant for Text<'t> { + fn identity() -> Self { + Text::new() } } -impl<'t> Mul for InvariantText<'t> { +impl<'t> Mul for Text<'t> { type Output = Self; fn mul(self, n: usize) -> Self::Output { @@ -145,14 +148,14 @@ impl<'t> Mul for InvariantText<'t> { } #[derive(Clone, Debug, Eq)] -enum InvariantFragment<'t> { +enum Fragment<'t> { Nominal(Cow<'t, str>), Structural(Cow<'t, str>), } -impl<'t> InvariantFragment<'t> { - pub fn into_owned(self) -> InvariantFragment<'static> { - use InvariantFragment::{Nominal, Structural}; +impl<'t> Fragment<'t> { + pub fn into_owned(self) -> Fragment<'static> { + use Fragment::{Nominal, Structural}; match self { Nominal(text) => Nominal(text.into_owned().into()), @@ -162,34 +165,34 @@ impl<'t> InvariantFragment<'t> { pub fn as_string(&self) -> &Cow<'t, str> { match self { - InvariantFragment::Nominal(ref text) | InvariantFragment::Structural(ref text) => text, + Fragment::Nominal(ref text) | Fragment::Structural(ref text) => text, } } } -impl<'t> Add for InvariantFragment<'t> { - type Output = InvariantText<'t>; +impl<'t> Add for Fragment<'t> { + type Output = Text<'t>; fn add(self, other: Self) -> Self::Output { - use InvariantFragment::{Nominal, Structural}; + use Fragment::{Nominal, Structural}; match (self, other) { - (Nominal(left), Nominal(right)) => InvariantText { + (Nominal(left), Nominal(right)) => Text { fragments: [Nominal(left + right)].into_iter().collect(), }, - (Structural(left), Structural(right)) => InvariantText { + (Structural(left), Structural(right)) => Text { fragments: [Structural(left + right)].into_iter().collect(), }, - (left, right) => InvariantText { + (left, right) => Text { fragments: [left, right].into_iter().collect(), }, } } } -impl<'t> PartialEq for InvariantFragment<'t> { +impl<'t> PartialEq for Fragment<'t> { fn eq(&self, other: &Self) -> bool { - use InvariantFragment::{Nominal, Structural}; + use Fragment::{Nominal, Structural}; match (self, other) { (Nominal(ref left), Nominal(ref right)) => { diff --git a/src/token/variance/mod.rs b/src/token/variance/mod.rs index 70f126a..25e3b22 100644 --- a/src/token/variance/mod.rs +++ b/src/token/variance/mod.rs @@ -1,210 +1,23 @@ -mod text; +pub mod invariant; use itertools::Itertools as _; use std::borrow::Cow; use std::cmp::Ordering; -use std::ops::{Add, Mul}; +use std::ops::Add; +use crate::token::variance::invariant::{Invariant, Text}; use crate::token::{self, Separator, Token}; -pub use crate::token::variance::text::{IntoInvariantText, InvariantText}; - -pub trait Invariant: - Add + Eq + Mul + PartialEq + Sized -{ - fn empty() -> Self; -} - pub trait VarianceTerm { fn term(&self) -> Variance; } +// TODO: Implement the tree fold traits with some variance type parameterized over invariant. pub trait VarianceFold { - fn fold(&self) -> Variance; -} - -// TODO: Replace with `VarianceTerm`. -pub trait UnitVariance { - fn unit_variance(self) -> Variance; -} - -impl UnitVariance for Variance { - fn unit_variance(self) -> Variance { - self - } -} - -pub trait ConjunctiveVariance: Iterator + Sized -where - Self::Item: UnitVariance, - T: Invariant, -{ - fn conjunctive_variance(self) -> Variance { - self.map(UnitVariance::unit_variance) - .reduce(Add::add) - .unwrap_or_else(|| Variance::Invariant(T::empty())) - } -} - -impl ConjunctiveVariance for I -where - I: Iterator, - I::Item: UnitVariance, - T: Invariant, -{ -} - -pub trait DisjunctiveVariance: Iterator + Sized -where - Self::Item: UnitVariance, - T: Invariant, -{ - fn disjunctive_variance(self) -> Variance { - // TODO: This implementation is incomplete. Unbounded variance (and unbounded depth) are - // "infectious" when disjunctive. If any unit variance is variant and unbounded - // (open), then the disjunctive variance should be the same. - // There are three distinct possibilities for disjunctive variance. - // - // - The iterator is empty and there are no unit variances to - // consider. The disjunctive variance is the empty invariant. - // - The iterator is non-empty and all unit variances are equal. The - // disjunctive variance is the same as any of the like unit - // variances. - // - The iterator is non-empty and the unit variances are **not** all - // equal. The disjunctive variance is variant and bounded (closed). - let mut variances = self.map(UnitVariance::unit_variance).fuse(); - let first = variances - .next() - .unwrap_or_else(|| Variance::Invariant(T::empty())); - if variances.all(|variance| first == variance) { - first - } - else { - Variance::Variant(Bound::Bounded) - } - } -} - -impl DisjunctiveVariance for I -where - I: Iterator, - I::Item: UnitVariance, - T: Invariant, -{ -} - -pub trait UnitDepth: Sized { - fn unit_depth(self) -> Bound { - Bound::Bounded - } -} - -impl UnitDepth for Bound { - fn unit_depth(self) -> Bound { - self - } -} - -pub trait CompositeDepth: Iterator + Sized { - fn composite_depth(self) -> Bound; -} - -impl CompositeDepth for I -where - I: Iterator, - I::Item: UnitDepth, -{ - fn composite_depth(self) -> Bound { - if self - .map(UnitDepth::unit_depth) - .any(|depth| depth.is_unbounded()) - { - Bound::Unbounded - } - else { - Bound::Bounded - } - } -} - -pub trait UnitBreadth: Sized { - fn unit_breadth(self) -> Bound { - Bound::Bounded - } -} - -impl UnitBreadth for Bound { - fn unit_breadth(self) -> Bound { - self - } -} - -pub trait CompositeBreadth: Iterator + Sized { - fn composite_breadth(self) -> Bound; -} - -impl CompositeBreadth for I -where - I: Iterator, - I::Item: UnitBreadth, -{ - fn composite_breadth(self) -> Bound { - if self - .map(UnitBreadth::unit_breadth) - .any(|breadth| breadth.is_unbounded()) - { - Bound::Unbounded - } - else { - Bound::Bounded - } - } -} - -#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] -pub struct InvariantSize(usize); - -impl InvariantSize { - pub const fn new(n: usize) -> Self { - InvariantSize(n) - } -} - -impl Add for InvariantSize { - type Output = Self; + fn fold(&self, terms: Vec>) -> Variance; - fn add(self, other: Self) -> Self::Output { - InvariantSize(self.0 + other.0) - } -} - -impl From for usize { - fn from(size: InvariantSize) -> Self { - size.0 - } -} - -impl From for InvariantSize { - fn from(n: usize) -> Self { - InvariantSize(n) - } -} - -impl Invariant for InvariantSize { - fn empty() -> Self { - InvariantSize(0) - } -} - -impl Mul for InvariantSize { - type Output = Self; - - fn mul(self, n: usize) -> Self::Output { - InvariantSize( - self.0 - .checked_mul(n) - .expect("overflow determining invariant size"), - ) + fn finalize(&self, accumulator: Variance) -> Variance { + accumulator } } @@ -269,20 +82,91 @@ where } } +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct Cardinality { + lower: T, + upper: Bound, +} + +impl Cardinality { + fn new_unchecked(lower: T, upper: Bound) -> Self { + Cardinality { lower, upper } + } + + pub fn lower(&self) -> &T { + &self.lower + } + + pub fn upper(&self) -> Bound<&T> { + self.upper.as_ref() + } + + pub fn convergence(&self) -> Option<&T> + where + T: Eq, + { + match self { + Cardinality { + upper: Bound::Bounded(ref upper), + .. + } if &self.lower == upper => Some(&self.lower), + _ => None, + } + } + + pub fn is_unbounded(&self) -> bool { + matches!(self.upper, Unbounded) + } +} + +impl From<(T, Bound)> for Cardinality +where + Bound: Ord, + T: Ord, +{ + fn from((n, bound): (T, Bound)) -> Self { + let (lower, upper) = match bound { + Bound::Bounded(m) => match Ord::cmp(&n, &m) { + Ordering::Greater => (m, Bound::Bounded(n)), + _ => (n, Bound::Bounded(m)), + }, + _ => (n, bound), + }; + Cardinality { lower, upper } + } +} + #[derive(Clone, Debug, Eq)] pub enum Variance { Invariant(T), - // In this context, _boundedness_ refers to whether or not a variant token or expression is - // _constrained_ or _unconstrained_. For example, the expression `**` is unconstrained and - // matches _any and all_, while the expression `a*z` is constrained and matches _some_. Note - // that both expressions match an infinite number of components, but the constrained expression - // does *not* match any component. Boundedness does **not** consider length, only whether or - // not some part of an expression is constrained to a known set of matches. As such, both the - // expressions `?` and `*` are variant with open bounds. - Variant(Bound), + // When variant, the bound describes the constraints of that variance. For example, the + // expression `**` is unconstrained and matches _any and all_ text, breadths, and depths. On + // the other hand, the expression `a*z` is constrained and matches _some_ text and _some_ + // breadths (and is invariant w.r.t. depth). Regarding text, note that bounds do **not** + // consider the length at all (breadth). Only constraints that limit an expression to a known + // set of matches are considered, so both `?` and `*` are unbounded w.r.t. text. + Variant(Bound<()>), } impl Variance { + pub const fn unbounded() -> Self { + Variance::Variant(Bound::Unbounded) + } + + pub fn identity() -> Self + where + T: Invariant, + { + Variance::Invariant(T::identity()) + } + + pub fn once() -> Self + where + T: Invariant, + { + Variance::Invariant(T::once()) + } + pub fn map_invariance(self, mut f: impl FnMut(T) -> U) -> Variance { match self { Variance::Invariant(invariant) => Variance::Invariant(f(invariant)), @@ -297,9 +181,9 @@ impl Variance { } } - pub fn boundedness(&self) -> Bound { + pub fn bound(&self) -> Bound<()> { match self { - Variance::Variant(ref boundedness) => *boundedness, + Variance::Variant(ref bound) => *bound, _ => Bound::Bounded, } } @@ -369,9 +253,9 @@ where &token::components(tokens) .map(|component| { component - .variance::() + .variance::() .as_invariance() - .map(InvariantText::to_string) + .map(Text::to_string) .map(Cow::into_owned) }) .take_while(Option::is_some) @@ -401,7 +285,7 @@ where return n; }, _ => { - if token.variance::().is_invariant() { + if token.variance::().is_invariant() { continue; } return match separator { @@ -414,26 +298,71 @@ where m + 1 } +// TODO: Replace and remove this. /// Returns `true` if the token tree is exhaustive. /// /// A glob expression and its token tree are exhaustive if the terminal component has unbounded /// depth and unbounded variance. -pub fn is_exhaustive<'i, 't, A, I>(tokens: I) -> bool +//pub fn is_exhaustive<'i, 't, A, I>(tokens: I) -> bool +//where +// 't: 'i, +// A: 't, +// I: IntoIterator>, +//{ +// let component = token::components(tokens).last(); +// matches!( +// component.map(|component| { +// ( +// component.depth(), +// component.variance::().bound(), +// ) +// }), +// Some((Bound::Unbounded, Bound::Unbounded)), +// ) +//} + +pub fn conjunction(lhs: Variance, rhs: Variance) -> Variance where - 't: 'i, - A: 't, - I: IntoIterator>, + T: Invariant, { - let component = token::components(tokens).last(); - matches!( - component.map(|component| { - ( - component.depth(), - component.variance::().boundedness(), - ) - }), - Some((Bound::Unbounded, Bound::Unbounded)), - ) + use Bound::{Bounded, Unbounded}; + use Variance::{Invariant, Variant}; + + match (lhs, rhs) { + (Invariant(lhs), Invariant(rhs)) => Invariant(lhs + rhs), + (Variant(Unbounded), Variant(Unbounded)) => Variant(Unbounded), + (Invariant(_) | Variant(_), Variant(_)) | (Variant(_), Invariant(_)) => { + Variant(Bounded(())) + }, + } +} + +pub fn disjunction(lhs: Variance, rhs: Variance) -> Variance +where + T: Invariant, +{ + use Bound::{Bounded, Unbounded}; + use Variance::Variant; + + match (&lhs, &rhs) { + (Variant(Unbounded), _) | (_, Variant(Unbounded)) => Variant(Unbounded), + _ if lhs == rhs => lhs, + _ => Variant(Bounded(())), + } +} + +pub fn union(lhs: Variance, rhs: Variance) -> Variance +where + T: Invariant, +{ + use Bound::{Bounded, Unbounded}; + use Variance::{Invariant, Variant}; + + match (lhs, rhs) { + (Invariant(lhs), Invariant(rhs)) => Invariant(lhs + rhs), + (Variant(Unbounded), _) | (_, Variant(Unbounded)) => Variant(Unbounded), + _ => Variant(Bounded(())), + } } #[cfg(test)]