git-ents.gitmain
⌘K
foforge
ast.rs235 lines · 7.4 KB · rusthistorycomment on this file
1//! The `CommitQuery` syntax tree (`query.grammar`).
2
3use ents_model::Status;
4
5use crate::pattern::{Footprint, RefPattern};
6use 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)]
12pub 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
24impl 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
58impl 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)]
72pub enum SetOp {
73 /// `|` — union.
74 Union,
75 /// `&` — intersection.
76 Intersect,
77 /// `-` — difference.
78 Difference,
79}
80
81impl 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)]
111pub 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
137impl 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
208impl 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
229impl 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}