crates/kernel/ents-query/src/ast.rs
| 1 | //! The `CommitQuery` syntax tree (`query.grammar`). |
| 2 | |
| 3 | use ents_model::Status; |
| 4 | |
| 5 | use crate::pattern::{Footprint, RefPattern}; |
| 6 | use crate::rev::RevExpr; |
| 7 | |
| 8 | /// The status argument of a `results()` atom: one of the closed |
| 9 | /// taxonomy's three values, or `any` for any recorded status |
| 10 | /// (`query.results`, `model.result-taxonomy`). |
| 11 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 12 | pub enum StatusFilter { |
| 13 | /// Only `pass` results. |
| 14 | Pass, |
| 15 | /// Only `fail` results. |
| 16 | Fail, |
| 17 | /// Only `error` results. |
| 18 | Error, |
| 19 | /// Any recorded status — the work-set subtraction side |
| 20 | /// (`query.workset`). |
| 21 | Any, |
| 22 | } |
| 23 | |
| 24 | impl StatusFilter { |
| 25 | /// Whether a recorded status satisfies this filter. |
| 26 | /// |
| 27 | /// # Examples |
| 28 | /// |
| 29 | /// ``` |
| 30 | /// use ents_model::Status; |
| 31 | /// use ents_query::StatusFilter; |
| 32 | /// |
| 33 | /// assert!(StatusFilter::Any.admits(Status::Fail)); |
| 34 | /// assert!(StatusFilter::Pass.admits(Status::Pass)); |
| 35 | /// assert!(!StatusFilter::Pass.admits(Status::Error)); |
| 36 | /// ``` |
| 37 | #[must_use] |
| 38 | pub fn admits(&self, status: Status) -> bool { |
| 39 | match self { |
| 40 | Self::Any => true, |
| 41 | Self::Pass => status == Status::Pass, |
| 42 | Self::Fail => status == Status::Fail, |
| 43 | Self::Error => status == Status::Error, |
| 44 | } |
| 45 | } |
| 46 | |
| 47 | pub(crate) fn parse(text: &str) -> Option<Self> { |
| 48 | match text { |
| 49 | "pass" => Some(Self::Pass), |
| 50 | "fail" => Some(Self::Fail), |
| 51 | "error" => Some(Self::Error), |
| 52 | "any" => Some(Self::Any), |
| 53 | _ => None, |
| 54 | } |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | impl std::fmt::Display for StatusFilter { |
| 59 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 60 | f.write_str(match self { |
| 61 | Self::Pass => "pass", |
| 62 | Self::Fail => "fail", |
| 63 | Self::Error => "error", |
| 64 | Self::Any => "any", |
| 65 | }) |
| 66 | } |
| 67 | } |
| 68 | |
| 69 | /// A binary set operator (`query.set-ops`). All three share one |
| 70 | /// precedence level and associate left (`query.grammar`). |
| 71 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 72 | pub enum SetOp { |
| 73 | /// `|` — union. |
| 74 | Union, |
| 75 | /// `&` — intersection. |
| 76 | Intersect, |
| 77 | /// `-` — difference. |
| 78 | Difference, |
| 79 | } |
| 80 | |
| 81 | impl std::fmt::Display for SetOp { |
| 82 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 83 | f.write_str(match self { |
| 84 | Self::Union => "|", |
| 85 | Self::Intersect => "&", |
| 86 | Self::Difference => "-", |
| 87 | }) |
| 88 | } |
| 89 | } |
| 90 | |
| 91 | /// A parsed `CommitQuery`: three atoms closed under union, |
| 92 | /// intersection, and difference (`query.grammar`), denoting a set of |
| 93 | /// commits as a pure function of ref state. |
| 94 | /// |
| 95 | /// The enum is deliberately exhaustive and public: `query.no-extensions` |
| 96 | /// freezes the atom set (no content, time, or external-event terms), and |
| 97 | /// `query.recursion` depends on downstream-of-an-effect being visible in |
| 98 | /// this structure — see [`Query::results_dependencies`]. |
| 99 | /// |
| 100 | /// # Examples |
| 101 | /// |
| 102 | /// ``` |
| 103 | /// use ents_query::{Query, SetOp}; |
| 104 | /// |
| 105 | /// // The staged-pipeline idiom. |
| 106 | /// let query: Query = "rev(refs/heads/main) & results(unit, pass)".parse().expect("valid"); |
| 107 | /// let Query::Op { op: SetOp::Intersect, .. } = query else { panic!("an intersection") }; |
| 108 | /// ``` |
| 109 | // @relation(query.grammar, query.no-extensions, scope=file) |
| 110 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 111 | pub enum Query { |
| 112 | /// `rev(expr)` — an ordinary Git revspec or ref glob over refs |
| 113 | /// outside `refs/meta/*` (`query.rev`). |
| 114 | Rev(RevExpr), |
| 115 | /// `results(effect, status)` — commits carrying a recorded result |
| 116 | /// (`query.results`). |
| 117 | Results { |
| 118 | /// The effect whose results namespace is scanned. |
| 119 | effect: String, |
| 120 | /// Which recorded statuses count. |
| 121 | status: StatusFilter, |
| 122 | }, |
| 123 | /// `meta(glob)` — tip commits of matching author-written meta-refs |
| 124 | /// (`query.meta`). |
| 125 | Meta(RefPattern), |
| 126 | /// A binary set operation (`query.set-ops`). |
| 127 | Op { |
| 128 | /// The operator. |
| 129 | op: SetOp, |
| 130 | /// Left operand. |
| 131 | lhs: Box<Query>, |
| 132 | /// Right operand. |
| 133 | rhs: Box<Query>, |
| 134 | }, |
| 135 | } |
| 136 | |
| 137 | impl Query { |
| 138 | /// The refname patterns this query depends on, by static analysis |
| 139 | /// of the syntax tree alone (`query.footprint`): a `rev` term |
| 140 | /// contributes its own ref patterns, `results(effect, _)` |
| 141 | /// contributes the effect's results namespace, `meta(glob)` |
| 142 | /// contributes the glob itself. |
| 143 | // @relation(query.footprint, scope=function) |
| 144 | #[must_use] |
| 145 | pub fn footprint(&self) -> Footprint { |
| 146 | let mut patterns = Vec::new(); |
| 147 | self.collect_patterns(&mut patterns); |
| 148 | Footprint::from_patterns(patterns) |
| 149 | } |
| 150 | |
| 151 | fn collect_patterns(&self, out: &mut Vec<RefPattern>) { |
| 152 | match self { |
| 153 | Self::Rev(expr) => out.extend(expr.patterns()), |
| 154 | Self::Results { effect, .. } => { |
| 155 | if let Ok(pattern) = RefPattern::new(format!("refs/meta/results/{effect}/*")) { |
| 156 | out.push(pattern); |
| 157 | } |
| 158 | } |
| 159 | Self::Meta(glob) => out.push(glob.clone()), |
| 160 | Self::Op { lhs, rhs, .. } => { |
| 161 | lhs.collect_patterns(out); |
| 162 | rhs.collect_patterns(out); |
| 163 | } |
| 164 | } |
| 165 | } |
| 166 | |
| 167 | /// The effects whose results this query reacts to, in syntactic |
| 168 | /// order — whether a query is downstream of an effect is determined |
| 169 | /// by inspecting whether `results(...)` appears in its text, never |
| 170 | /// by runtime behavior (`query.recursion`). |
| 171 | /// |
| 172 | /// # Examples |
| 173 | /// |
| 174 | /// ``` |
| 175 | /// use ents_query::Query; |
| 176 | /// |
| 177 | /// let query: Query = "rev(main) & results(unit, pass) | results(integ, any)" |
| 178 | /// .parse().expect("valid"); |
| 179 | /// assert_eq!(query.results_dependencies(), ["unit", "integ"]); |
| 180 | /// |
| 181 | /// let plain: Query = "rev(main)".parse().expect("valid"); |
| 182 | /// assert!(plain.results_dependencies().is_empty()); |
| 183 | /// ``` |
| 184 | // @relation(query.recursion, scope=function) |
| 185 | #[must_use] |
| 186 | pub fn results_dependencies(&self) -> Vec<&str> { |
| 187 | let mut out = Vec::new(); |
| 188 | self.collect_dependencies(&mut out); |
| 189 | out |
| 190 | } |
| 191 | |
| 192 | fn collect_dependencies<'a>(&'a self, out: &mut Vec<&'a str>) { |
| 193 | match self { |
| 194 | Self::Rev(_) | Self::Meta(_) => {} |
| 195 | Self::Results { effect, .. } => { |
| 196 | if !out.contains(&effect.as_str()) { |
| 197 | out.push(effect); |
| 198 | } |
| 199 | } |
| 200 | Self::Op { lhs, rhs, .. } => { |
| 201 | lhs.collect_dependencies(out); |
| 202 | rhs.collect_dependencies(out); |
| 203 | } |
| 204 | } |
| 205 | } |
| 206 | } |
| 207 | |
| 208 | impl std::fmt::Display for Query { |
| 209 | /// Canonical text: atoms as written, operators space-separated, a |
| 210 | /// parenthesized right operand wherever left-associativity would |
| 211 | /// otherwise regroup it. `parse(display(q)) == q` for every query. |
| 212 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 213 | match self { |
| 214 | Self::Rev(expr) => write!(f, "rev({})", expr.raw()), |
| 215 | Self::Results { effect, status } => write!(f, "results({effect}, {status})"), |
| 216 | Self::Meta(glob) => write!(f, "meta({glob})"), |
| 217 | Self::Op { op, lhs, rhs } => { |
| 218 | write!(f, "{lhs} {op} ")?; |
| 219 | if matches!(**rhs, Self::Op { .. }) { |
| 220 | write!(f, "({rhs})") |
| 221 | } else { |
| 222 | write!(f, "{rhs}") |
| 223 | } |
| 224 | } |
| 225 | } |
| 226 | } |
| 227 | } |
| 228 | |
| 229 | impl std::str::FromStr for Query { |
| 230 | type Err = crate::error::ParseError; |
| 231 | |
| 232 | fn from_str(s: &str) -> Result<Self, Self::Err> { |
| 233 | crate::parse::parse(s) |
| 234 | } |
| 235 | } |