jj_lib/
revset.rs

1// Copyright 2021 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![allow(missing_docs)]
16
17use std::any::Any;
18use std::collections::hash_map;
19use std::collections::HashMap;
20use std::convert::Infallible;
21use std::fmt;
22use std::ops::Range;
23use std::rc::Rc;
24use std::sync::Arc;
25
26use itertools::Itertools as _;
27use once_cell::sync::Lazy;
28use thiserror::Error;
29
30use crate::backend::BackendError;
31use crate::backend::ChangeId;
32use crate::backend::CommitId;
33use crate::commit::Commit;
34use crate::dsl_util;
35use crate::dsl_util::collect_similar;
36use crate::dsl_util::AliasExpandError as _;
37use crate::fileset;
38use crate::fileset::FilesetDiagnostics;
39use crate::fileset::FilesetExpression;
40use crate::graph::GraphNode;
41use crate::hex_util::to_forward_hex;
42use crate::id_prefix::IdPrefixContext;
43use crate::id_prefix::IdPrefixIndex;
44use crate::object_id::HexPrefix;
45use crate::object_id::PrefixResolution;
46use crate::op_store::RemoteRefState;
47use crate::op_walk;
48use crate::ref_name::RemoteRefSymbol;
49use crate::ref_name::RemoteRefSymbolBuf;
50use crate::ref_name::WorkspaceName;
51use crate::ref_name::WorkspaceNameBuf;
52use crate::repo::ReadonlyRepo;
53use crate::repo::Repo;
54use crate::repo::RepoLoaderError;
55use crate::repo_path::RepoPathUiConverter;
56use crate::revset_parser;
57pub use crate::revset_parser::expect_literal;
58pub use crate::revset_parser::parse_program;
59pub use crate::revset_parser::parse_symbol;
60pub use crate::revset_parser::BinaryOp;
61pub use crate::revset_parser::ExpressionKind;
62pub use crate::revset_parser::ExpressionNode;
63pub use crate::revset_parser::FunctionCallNode;
64pub use crate::revset_parser::RevsetAliasesMap;
65pub use crate::revset_parser::RevsetDiagnostics;
66pub use crate::revset_parser::RevsetParseError;
67pub use crate::revset_parser::RevsetParseErrorKind;
68pub use crate::revset_parser::UnaryOp;
69use crate::store::Store;
70use crate::str_util::StringPattern;
71use crate::time_util::DatePattern;
72use crate::time_util::DatePatternContext;
73
74/// Error occurred during symbol resolution.
75#[derive(Debug, Error)]
76pub enum RevsetResolutionError {
77    #[error("Revision `{name}` doesn't exist")]
78    NoSuchRevision {
79        name: String,
80        candidates: Vec<String>,
81    },
82    #[error("Workspace `{}` doesn't have a working-copy commit", name.as_symbol())]
83    WorkspaceMissingWorkingCopy { name: WorkspaceNameBuf },
84    #[error("An empty string is not a valid revision")]
85    EmptyString,
86    #[error("Commit ID prefix `{0}` is ambiguous")]
87    AmbiguousCommitIdPrefix(String),
88    #[error("Change ID prefix `{0}` is ambiguous")]
89    AmbiguousChangeIdPrefix(String),
90    #[error("Unexpected error from store")]
91    StoreError(#[source] BackendError),
92    #[error(transparent)]
93    Other(#[from] Box<dyn std::error::Error + Send + Sync>),
94}
95
96/// Error occurred during revset evaluation.
97#[derive(Debug, Error)]
98pub enum RevsetEvaluationError {
99    #[error("Unexpected error from store")]
100    StoreError(#[from] BackendError),
101    #[error(transparent)]
102    Other(Box<dyn std::error::Error + Send + Sync>),
103}
104
105impl RevsetEvaluationError {
106    pub fn expect_backend_error(self) -> BackendError {
107        match self {
108            Self::StoreError(err) => err,
109            Self::Other(err) => panic!("Unexpected revset error: {err}"),
110        }
111    }
112}
113
114// assumes index has less than u64::MAX entries.
115pub const GENERATION_RANGE_FULL: Range<u64> = 0..u64::MAX;
116pub const GENERATION_RANGE_EMPTY: Range<u64> = 0..0;
117
118/// Global flag applied to the entire expression.
119///
120/// The core revset engine doesn't use this value. It's up to caller to
121/// interpret it to change the evaluation behavior.
122#[derive(Clone, Copy, Debug, Eq, PartialEq)]
123pub enum RevsetModifier {
124    /// Expression can be evaluated to multiple revisions even if a single
125    /// revision is expected by default.
126    All,
127}
128
129/// Symbol or function to be resolved to `CommitId`s.
130#[derive(Clone, Debug)]
131pub enum RevsetCommitRef {
132    WorkingCopy(WorkspaceNameBuf),
133    WorkingCopies,
134    Symbol(String),
135    RemoteSymbol(RemoteRefSymbolBuf),
136    Bookmarks(StringPattern),
137    RemoteBookmarks {
138        bookmark_pattern: StringPattern,
139        remote_pattern: StringPattern,
140        remote_ref_state: Option<RemoteRefState>,
141    },
142    Tags(StringPattern),
143    GitRefs,
144    GitHead,
145}
146
147/// A custom revset filter expression, defined by an extension.
148pub trait RevsetFilterExtension: std::fmt::Debug + Any {
149    fn as_any(&self) -> &dyn Any;
150
151    /// Returns true iff this filter matches the specified commit.
152    fn matches_commit(&self, commit: &Commit) -> bool;
153}
154
155#[derive(Clone, Debug)]
156pub enum RevsetFilterPredicate {
157    /// Commits with number of parents in the range.
158    ParentCount(Range<u32>),
159    /// Commits with description matching the pattern.
160    Description(StringPattern),
161    /// Commits with first line of the description matching the pattern.
162    Subject(StringPattern),
163    /// Commits with author name matching the pattern.
164    AuthorName(StringPattern),
165    /// Commits with author email matching the pattern.
166    AuthorEmail(StringPattern),
167    /// Commits with author dates matching the given date pattern.
168    AuthorDate(DatePattern),
169    /// Commits with committer name matching the pattern.
170    CommitterName(StringPattern),
171    /// Commits with committer email matching the pattern.
172    CommitterEmail(StringPattern),
173    /// Commits with committer dates matching the given date pattern.
174    CommitterDate(DatePattern),
175    /// Commits modifying the paths specified by the fileset.
176    File(FilesetExpression),
177    /// Commits containing diffs matching the `text` pattern within the `files`.
178    DiffContains {
179        text: StringPattern,
180        files: FilesetExpression,
181    },
182    /// Commits with conflicts
183    HasConflict,
184    /// Custom predicates provided by extensions
185    Extension(Rc<dyn RevsetFilterExtension>),
186}
187
188mod private {
189    /// Defines [`RevsetExpression`] variants depending on resolution state.
190    pub trait ExpressionState {
191        type CommitRef: Clone;
192        type Operation: Clone;
193    }
194
195    // Not constructible because these state types just define associated types.
196    #[derive(Debug)]
197    pub enum UserExpressionState {}
198    #[derive(Debug)]
199    pub enum ResolvedExpressionState {}
200}
201
202use private::ExpressionState;
203use private::ResolvedExpressionState;
204use private::UserExpressionState;
205
206impl ExpressionState for UserExpressionState {
207    type CommitRef = RevsetCommitRef;
208    type Operation = String;
209}
210
211impl ExpressionState for ResolvedExpressionState {
212    type CommitRef = Infallible;
213    type Operation = Infallible;
214}
215
216/// [`RevsetExpression`] that may contain unresolved commit refs.
217pub type UserRevsetExpression = RevsetExpression<UserExpressionState>;
218/// [`RevsetExpression`] that never contains unresolved commit refs.
219pub type ResolvedRevsetExpression = RevsetExpression<ResolvedExpressionState>;
220
221/// Tree of revset expressions describing DAG operations.
222///
223/// Use [`UserRevsetExpression`] or [`ResolvedRevsetExpression`] to construct
224/// expression of that state.
225#[derive(Clone, Debug)]
226pub enum RevsetExpression<St: ExpressionState> {
227    None,
228    All,
229    VisibleHeads,
230    Root,
231    Commits(Vec<CommitId>),
232    CommitRef(St::CommitRef),
233    Ancestors {
234        heads: Rc<Self>,
235        generation: Range<u64>,
236    },
237    Descendants {
238        roots: Rc<Self>,
239        generation: Range<u64>,
240    },
241    // Commits that are ancestors of "heads" but not ancestors of "roots"
242    Range {
243        roots: Rc<Self>,
244        heads: Rc<Self>,
245        generation: Range<u64>,
246    },
247    // Commits that are descendants of "roots" and ancestors of "heads"
248    DagRange {
249        roots: Rc<Self>,
250        heads: Rc<Self>,
251        // TODO: maybe add generation_from_roots/heads?
252    },
253    // Commits reachable from "sources" within "domain"
254    Reachable {
255        sources: Rc<Self>,
256        domain: Rc<Self>,
257    },
258    Heads(Rc<Self>),
259    Roots(Rc<Self>),
260    ForkPoint(Rc<Self>),
261    Latest {
262        candidates: Rc<Self>,
263        count: usize,
264    },
265    Filter(RevsetFilterPredicate),
266    /// Marker for subtree that should be intersected as filter.
267    AsFilter(Rc<Self>),
268    /// Resolves symbols and visibility at the specified operation.
269    AtOperation {
270        operation: St::Operation,
271        candidates: Rc<Self>,
272    },
273    /// Resolves visibility within the specified repo state.
274    WithinVisibility {
275        candidates: Rc<Self>,
276        /// Copy of `repo.view().heads()` at the operation.
277        visible_heads: Vec<CommitId>,
278    },
279    Coalesce(Rc<Self>, Rc<Self>),
280    Present(Rc<Self>),
281    NotIn(Rc<Self>),
282    Union(Rc<Self>, Rc<Self>),
283    Intersection(Rc<Self>, Rc<Self>),
284    Difference(Rc<Self>, Rc<Self>),
285}
286
287// Leaf expression that never contains unresolved commit refs, which can be
288// either user or resolved expression
289impl<St: ExpressionState> RevsetExpression<St> {
290    pub fn none() -> Rc<Self> {
291        Rc::new(Self::None)
292    }
293
294    pub fn all() -> Rc<Self> {
295        Rc::new(Self::All)
296    }
297
298    pub fn visible_heads() -> Rc<Self> {
299        Rc::new(Self::VisibleHeads)
300    }
301
302    pub fn root() -> Rc<Self> {
303        Rc::new(Self::Root)
304    }
305
306    pub fn commit(commit_id: CommitId) -> Rc<Self> {
307        Self::commits(vec![commit_id])
308    }
309
310    pub fn commits(commit_ids: Vec<CommitId>) -> Rc<Self> {
311        Rc::new(Self::Commits(commit_ids))
312    }
313
314    pub fn filter(predicate: RevsetFilterPredicate) -> Rc<Self> {
315        Rc::new(Self::Filter(predicate))
316    }
317
318    /// Find any empty commits.
319    pub fn is_empty() -> Rc<Self> {
320        Self::filter(RevsetFilterPredicate::File(FilesetExpression::all())).negated()
321    }
322}
323
324// Leaf expression that represents unresolved commit refs
325impl<St: ExpressionState<CommitRef = RevsetCommitRef>> RevsetExpression<St> {
326    pub fn working_copy(name: WorkspaceNameBuf) -> Rc<Self> {
327        Rc::new(Self::CommitRef(RevsetCommitRef::WorkingCopy(name)))
328    }
329
330    pub fn working_copies() -> Rc<Self> {
331        Rc::new(Self::CommitRef(RevsetCommitRef::WorkingCopies))
332    }
333
334    pub fn symbol(value: String) -> Rc<Self> {
335        Rc::new(Self::CommitRef(RevsetCommitRef::Symbol(value)))
336    }
337
338    pub fn remote_symbol(value: RemoteRefSymbolBuf) -> Rc<Self> {
339        let commit_ref = RevsetCommitRef::RemoteSymbol(value);
340        Rc::new(Self::CommitRef(commit_ref))
341    }
342
343    pub fn bookmarks(pattern: StringPattern) -> Rc<Self> {
344        Rc::new(Self::CommitRef(RevsetCommitRef::Bookmarks(pattern)))
345    }
346
347    pub fn remote_bookmarks(
348        bookmark_pattern: StringPattern,
349        remote_pattern: StringPattern,
350        remote_ref_state: Option<RemoteRefState>,
351    ) -> Rc<Self> {
352        Rc::new(Self::CommitRef(RevsetCommitRef::RemoteBookmarks {
353            bookmark_pattern,
354            remote_pattern,
355            remote_ref_state,
356        }))
357    }
358
359    pub fn tags(pattern: StringPattern) -> Rc<Self> {
360        Rc::new(Self::CommitRef(RevsetCommitRef::Tags(pattern)))
361    }
362
363    pub fn git_refs() -> Rc<Self> {
364        Rc::new(Self::CommitRef(RevsetCommitRef::GitRefs))
365    }
366
367    pub fn git_head() -> Rc<Self> {
368        Rc::new(Self::CommitRef(RevsetCommitRef::GitHead))
369    }
370}
371
372// Compound expression
373impl<St: ExpressionState> RevsetExpression<St> {
374    pub fn latest(self: &Rc<Self>, count: usize) -> Rc<Self> {
375        Rc::new(Self::Latest {
376            candidates: self.clone(),
377            count,
378        })
379    }
380
381    /// Commits in `self` that don't have descendants in `self`.
382    pub fn heads(self: &Rc<Self>) -> Rc<Self> {
383        Rc::new(Self::Heads(self.clone()))
384    }
385
386    /// Commits in `self` that don't have ancestors in `self`.
387    pub fn roots(self: &Rc<Self>) -> Rc<Self> {
388        Rc::new(Self::Roots(self.clone()))
389    }
390
391    /// Parents of `self`.
392    pub fn parents(self: &Rc<Self>) -> Rc<Self> {
393        self.ancestors_at(1)
394    }
395
396    /// Ancestors of `self`, including `self`.
397    pub fn ancestors(self: &Rc<Self>) -> Rc<Self> {
398        self.ancestors_range(GENERATION_RANGE_FULL)
399    }
400
401    /// Ancestors of `self` at an offset of `generation` behind `self`.
402    /// The `generation` offset is zero-based starting from `self`.
403    pub fn ancestors_at(self: &Rc<Self>, generation: u64) -> Rc<Self> {
404        self.ancestors_range(generation..(generation + 1))
405    }
406
407    /// Ancestors of `self` in the given range.
408    pub fn ancestors_range(self: &Rc<Self>, generation_range: Range<u64>) -> Rc<Self> {
409        Rc::new(Self::Ancestors {
410            heads: self.clone(),
411            generation: generation_range,
412        })
413    }
414
415    /// Children of `self`.
416    pub fn children(self: &Rc<Self>) -> Rc<Self> {
417        self.descendants_at(1)
418    }
419
420    /// Descendants of `self`, including `self`.
421    pub fn descendants(self: &Rc<Self>) -> Rc<Self> {
422        self.descendants_range(GENERATION_RANGE_FULL)
423    }
424
425    /// Descendants of `self` at an offset of `generation` ahead of `self`.
426    /// The `generation` offset is zero-based starting from `self`.
427    pub fn descendants_at(self: &Rc<Self>, generation: u64) -> Rc<Self> {
428        self.descendants_range(generation..(generation + 1))
429    }
430
431    /// Descendants of `self` in the given range.
432    pub fn descendants_range(self: &Rc<Self>, generation_range: Range<u64>) -> Rc<Self> {
433        Rc::new(Self::Descendants {
434            roots: self.clone(),
435            generation: generation_range,
436        })
437    }
438
439    /// Fork point (best common ancestors) of `self`.
440    pub fn fork_point(self: &Rc<Self>) -> Rc<Self> {
441        Rc::new(Self::ForkPoint(self.clone()))
442    }
443
444    /// Filter all commits by `predicate` in `self`.
445    pub fn filtered(self: &Rc<Self>, predicate: RevsetFilterPredicate) -> Rc<Self> {
446        self.intersection(&Self::filter(predicate))
447    }
448
449    /// Commits that are descendants of `self` and ancestors of `heads`, both
450    /// inclusive.
451    pub fn dag_range_to(self: &Rc<Self>, heads: &Rc<Self>) -> Rc<Self> {
452        Rc::new(Self::DagRange {
453            roots: self.clone(),
454            heads: heads.clone(),
455        })
456    }
457
458    /// Connects any ancestors and descendants in the set by adding the commits
459    /// between them.
460    pub fn connected(self: &Rc<Self>) -> Rc<Self> {
461        self.dag_range_to(self)
462    }
463
464    /// All commits within `domain` reachable from this set of commits, by
465    /// traversing either parent or child edges.
466    pub fn reachable(self: &Rc<Self>, domain: &Rc<Self>) -> Rc<Self> {
467        Rc::new(Self::Reachable {
468            sources: self.clone(),
469            domain: domain.clone(),
470        })
471    }
472
473    /// Commits reachable from `heads` but not from `self`.
474    pub fn range(self: &Rc<Self>, heads: &Rc<Self>) -> Rc<Self> {
475        Rc::new(Self::Range {
476            roots: self.clone(),
477            heads: heads.clone(),
478            generation: GENERATION_RANGE_FULL,
479        })
480    }
481
482    /// Suppresses name resolution error within `self`.
483    pub fn present(self: &Rc<Self>) -> Rc<Self> {
484        Rc::new(Self::Present(self.clone()))
485    }
486
487    /// Commits that are not in `self`, i.e. the complement of `self`.
488    pub fn negated(self: &Rc<Self>) -> Rc<Self> {
489        Rc::new(Self::NotIn(self.clone()))
490    }
491
492    /// Commits that are in `self` or in `other` (or both).
493    pub fn union(self: &Rc<Self>, other: &Rc<Self>) -> Rc<Self> {
494        Rc::new(Self::Union(self.clone(), other.clone()))
495    }
496
497    /// Commits that are in any of the `expressions`.
498    pub fn union_all(expressions: &[Rc<Self>]) -> Rc<Self> {
499        match expressions {
500            [] => Self::none(),
501            [expression] => expression.clone(),
502            _ => {
503                // Build balanced tree to minimize the recursion depth.
504                let (left, right) = expressions.split_at(expressions.len() / 2);
505                Self::union(&Self::union_all(left), &Self::union_all(right))
506            }
507        }
508    }
509
510    /// Commits that are in `self` and in `other`.
511    pub fn intersection(self: &Rc<Self>, other: &Rc<Self>) -> Rc<Self> {
512        Rc::new(Self::Intersection(self.clone(), other.clone()))
513    }
514
515    /// Commits that are in `self` but not in `other`.
516    pub fn minus(self: &Rc<Self>, other: &Rc<Self>) -> Rc<Self> {
517        Rc::new(Self::Difference(self.clone(), other.clone()))
518    }
519
520    /// Commits that are in the first expression in `expressions` that is not
521    /// `none()`.
522    pub fn coalesce(expressions: &[Rc<Self>]) -> Rc<Self> {
523        match expressions {
524            [] => Self::none(),
525            [expression] => expression.clone(),
526            _ => {
527                // Build balanced tree to minimize the recursion depth.
528                let (left, right) = expressions.split_at(expressions.len() / 2);
529                Rc::new(Self::Coalesce(Self::coalesce(left), Self::coalesce(right)))
530            }
531        }
532    }
533}
534
535impl<St: ExpressionState<CommitRef = RevsetCommitRef>> RevsetExpression<St> {
536    /// Returns symbol string if this expression is of that type.
537    pub fn as_symbol(&self) -> Option<&str> {
538        match self {
539            RevsetExpression::CommitRef(RevsetCommitRef::Symbol(name)) => Some(name),
540            _ => None,
541        }
542    }
543}
544
545impl UserRevsetExpression {
546    /// Resolve a user-provided expression. Symbols will be resolved using the
547    /// provided `SymbolResolver`.
548    pub fn resolve_user_expression(
549        &self,
550        repo: &dyn Repo,
551        symbol_resolver: &dyn SymbolResolver,
552    ) -> Result<Rc<ResolvedRevsetExpression>, RevsetResolutionError> {
553        resolve_symbols(repo, self, symbol_resolver)
554    }
555}
556
557impl ResolvedRevsetExpression {
558    /// Optimizes and evaluates this expression.
559    pub fn evaluate<'index>(
560        self: Rc<Self>,
561        repo: &'index dyn Repo,
562    ) -> Result<Box<dyn Revset + 'index>, RevsetEvaluationError> {
563        optimize(self).evaluate_unoptimized(repo)
564    }
565
566    /// Evaluates this expression without optimizing it.
567    ///
568    /// Use this function if `self` is already optimized, or to debug
569    /// optimization pass.
570    pub fn evaluate_unoptimized<'index>(
571        &self,
572        repo: &'index dyn Repo,
573    ) -> Result<Box<dyn Revset + 'index>, RevsetEvaluationError> {
574        let expr = self.to_backend_expression(repo);
575        repo.index().evaluate_revset(&expr, repo.store())
576    }
577
578    /// Transforms this expression to the form which the `Index` backend will
579    /// process.
580    pub fn to_backend_expression(&self, repo: &dyn Repo) -> ResolvedExpression {
581        resolve_visibility(repo, self)
582    }
583}
584
585#[derive(Clone, Debug)]
586pub enum ResolvedPredicateExpression {
587    /// Pure filter predicate.
588    Filter(RevsetFilterPredicate),
589    /// Set expression to be evaluated as filter. This is typically a subtree
590    /// node of `Union` with a pure filter predicate.
591    Set(Box<ResolvedExpression>),
592    NotIn(Box<ResolvedPredicateExpression>),
593    Union(
594        Box<ResolvedPredicateExpression>,
595        Box<ResolvedPredicateExpression>,
596    ),
597}
598
599/// Describes evaluation plan of revset expression.
600///
601/// Unlike `RevsetExpression`, this doesn't contain unresolved symbols or `View`
602/// properties.
603///
604/// Use `RevsetExpression` API to build a query programmatically.
605// TODO: rename to BackendExpression?
606#[derive(Clone, Debug)]
607pub enum ResolvedExpression {
608    Commits(Vec<CommitId>),
609    Ancestors {
610        heads: Box<Self>,
611        generation: Range<u64>,
612    },
613    /// Commits that are ancestors of `heads` but not ancestors of `roots`.
614    Range {
615        roots: Box<Self>,
616        heads: Box<Self>,
617        generation: Range<u64>,
618    },
619    /// Commits that are descendants of `roots` and ancestors of `heads`.
620    DagRange {
621        roots: Box<Self>,
622        heads: Box<Self>,
623        generation_from_roots: Range<u64>,
624    },
625    /// Commits reachable from `sources` within `domain`.
626    Reachable {
627        sources: Box<Self>,
628        domain: Box<Self>,
629    },
630    Heads(Box<Self>),
631    Roots(Box<Self>),
632    ForkPoint(Box<Self>),
633    Latest {
634        candidates: Box<Self>,
635        count: usize,
636    },
637    Coalesce(Box<Self>, Box<Self>),
638    Union(Box<Self>, Box<Self>),
639    /// Intersects `candidates` with `predicate` by filtering.
640    FilterWithin {
641        candidates: Box<Self>,
642        predicate: ResolvedPredicateExpression,
643    },
644    /// Intersects expressions by merging.
645    Intersection(Box<Self>, Box<Self>),
646    Difference(Box<Self>, Box<Self>),
647}
648
649pub type RevsetFunction = fn(
650    &mut RevsetDiagnostics,
651    &FunctionCallNode,
652    &LoweringContext,
653) -> Result<Rc<UserRevsetExpression>, RevsetParseError>;
654
655static BUILTIN_FUNCTION_MAP: Lazy<HashMap<&'static str, RevsetFunction>> = Lazy::new(|| {
656    // Not using maplit::hashmap!{} or custom declarative macro here because
657    // code completion inside macro is quite restricted.
658    let mut map: HashMap<&'static str, RevsetFunction> = HashMap::new();
659    map.insert("parents", |diagnostics, function, context| {
660        let [arg] = function.expect_exact_arguments()?;
661        let expression = lower_expression(diagnostics, arg, context)?;
662        Ok(expression.parents())
663    });
664    map.insert("children", |diagnostics, function, context| {
665        let [arg] = function.expect_exact_arguments()?;
666        let expression = lower_expression(diagnostics, arg, context)?;
667        Ok(expression.children())
668    });
669    map.insert("ancestors", |diagnostics, function, context| {
670        let ([heads_arg], [depth_opt_arg]) = function.expect_arguments()?;
671        let heads = lower_expression(diagnostics, heads_arg, context)?;
672        let generation = if let Some(depth_arg) = depth_opt_arg {
673            let depth = expect_literal(diagnostics, "integer", depth_arg)?;
674            0..depth
675        } else {
676            GENERATION_RANGE_FULL
677        };
678        Ok(heads.ancestors_range(generation))
679    });
680    map.insert("descendants", |diagnostics, function, context| {
681        let ([roots_arg], [depth_opt_arg]) = function.expect_arguments()?;
682        let roots = lower_expression(diagnostics, roots_arg, context)?;
683        let generation = if let Some(depth_arg) = depth_opt_arg {
684            let depth = expect_literal(diagnostics, "integer", depth_arg)?;
685            0..depth
686        } else {
687            GENERATION_RANGE_FULL
688        };
689        Ok(roots.descendants_range(generation))
690    });
691    map.insert("connected", |diagnostics, function, context| {
692        let [arg] = function.expect_exact_arguments()?;
693        let candidates = lower_expression(diagnostics, arg, context)?;
694        Ok(candidates.connected())
695    });
696    map.insert("reachable", |diagnostics, function, context| {
697        let [source_arg, domain_arg] = function.expect_exact_arguments()?;
698        let sources = lower_expression(diagnostics, source_arg, context)?;
699        let domain = lower_expression(diagnostics, domain_arg, context)?;
700        Ok(sources.reachable(&domain))
701    });
702    map.insert("none", |_diagnostics, function, _context| {
703        function.expect_no_arguments()?;
704        Ok(RevsetExpression::none())
705    });
706    map.insert("all", |_diagnostics, function, _context| {
707        function.expect_no_arguments()?;
708        Ok(RevsetExpression::all())
709    });
710    map.insert("working_copies", |_diagnostics, function, _context| {
711        function.expect_no_arguments()?;
712        Ok(RevsetExpression::working_copies())
713    });
714    map.insert("heads", |diagnostics, function, context| {
715        let [arg] = function.expect_exact_arguments()?;
716        let candidates = lower_expression(diagnostics, arg, context)?;
717        Ok(candidates.heads())
718    });
719    map.insert("roots", |diagnostics, function, context| {
720        let [arg] = function.expect_exact_arguments()?;
721        let candidates = lower_expression(diagnostics, arg, context)?;
722        Ok(candidates.roots())
723    });
724    map.insert("visible_heads", |_diagnostics, function, _context| {
725        function.expect_no_arguments()?;
726        Ok(RevsetExpression::visible_heads())
727    });
728    map.insert("root", |_diagnostics, function, _context| {
729        function.expect_no_arguments()?;
730        Ok(RevsetExpression::root())
731    });
732    map.insert("bookmarks", |diagnostics, function, _context| {
733        let ([], [opt_arg]) = function.expect_arguments()?;
734        let pattern = if let Some(arg) = opt_arg {
735            expect_string_pattern(diagnostics, arg)?
736        } else {
737            StringPattern::everything()
738        };
739        Ok(RevsetExpression::bookmarks(pattern))
740    });
741    map.insert("remote_bookmarks", |diagnostics, function, _context| {
742        parse_remote_bookmarks_arguments(diagnostics, function, None)
743    });
744    map.insert(
745        "tracked_remote_bookmarks",
746        |diagnostics, function, _context| {
747            parse_remote_bookmarks_arguments(diagnostics, function, Some(RemoteRefState::Tracked))
748        },
749    );
750    map.insert(
751        "untracked_remote_bookmarks",
752        |diagnostics, function, _context| {
753            parse_remote_bookmarks_arguments(diagnostics, function, Some(RemoteRefState::New))
754        },
755    );
756    map.insert("tags", |diagnostics, function, _context| {
757        let ([], [opt_arg]) = function.expect_arguments()?;
758        let pattern = if let Some(arg) = opt_arg {
759            expect_string_pattern(diagnostics, arg)?
760        } else {
761            StringPattern::everything()
762        };
763        Ok(RevsetExpression::tags(pattern))
764    });
765    map.insert("git_refs", |_diagnostics, function, _context| {
766        function.expect_no_arguments()?;
767        Ok(RevsetExpression::git_refs())
768    });
769    map.insert("git_head", |_diagnostics, function, _context| {
770        function.expect_no_arguments()?;
771        Ok(RevsetExpression::git_head())
772    });
773    map.insert("latest", |diagnostics, function, context| {
774        let ([candidates_arg], [count_opt_arg]) = function.expect_arguments()?;
775        let candidates = lower_expression(diagnostics, candidates_arg, context)?;
776        let count = if let Some(count_arg) = count_opt_arg {
777            expect_literal(diagnostics, "integer", count_arg)?
778        } else {
779            1
780        };
781        Ok(candidates.latest(count))
782    });
783    map.insert("fork_point", |diagnostics, function, context| {
784        let [expression_arg] = function.expect_exact_arguments()?;
785        let expression = lower_expression(diagnostics, expression_arg, context)?;
786        Ok(RevsetExpression::fork_point(&expression))
787    });
788    map.insert("merges", |_diagnostics, function, _context| {
789        function.expect_no_arguments()?;
790        Ok(RevsetExpression::filter(
791            RevsetFilterPredicate::ParentCount(2..u32::MAX),
792        ))
793    });
794    map.insert("description", |diagnostics, function, _context| {
795        let [arg] = function.expect_exact_arguments()?;
796        let pattern = expect_string_pattern(diagnostics, arg)?;
797        Ok(RevsetExpression::filter(
798            RevsetFilterPredicate::Description(pattern),
799        ))
800    });
801    map.insert("subject", |diagnostics, function, _context| {
802        let [arg] = function.expect_exact_arguments()?;
803        let pattern = expect_string_pattern(diagnostics, arg)?;
804        let predicate = RevsetFilterPredicate::Subject(pattern);
805        Ok(RevsetExpression::filter(predicate))
806    });
807    map.insert("author", |diagnostics, function, _context| {
808        let [arg] = function.expect_exact_arguments()?;
809        let pattern = expect_string_pattern(diagnostics, arg)?;
810        let name_predicate = RevsetFilterPredicate::AuthorName(pattern.clone());
811        let email_predicate = RevsetFilterPredicate::AuthorEmail(pattern);
812        Ok(RevsetExpression::filter(name_predicate)
813            .union(&RevsetExpression::filter(email_predicate)))
814    });
815    map.insert("author_name", |diagnostics, function, _context| {
816        let [arg] = function.expect_exact_arguments()?;
817        let pattern = expect_string_pattern(diagnostics, arg)?;
818        let predicate = RevsetFilterPredicate::AuthorName(pattern);
819        Ok(RevsetExpression::filter(predicate))
820    });
821    map.insert("author_email", |diagnostics, function, _context| {
822        let [arg] = function.expect_exact_arguments()?;
823        let pattern = expect_string_pattern(diagnostics, arg)?;
824        let predicate = RevsetFilterPredicate::AuthorEmail(pattern);
825        Ok(RevsetExpression::filter(predicate))
826    });
827    map.insert("author_date", |diagnostics, function, context| {
828        let [arg] = function.expect_exact_arguments()?;
829        let pattern = expect_date_pattern(diagnostics, arg, context.date_pattern_context())?;
830        Ok(RevsetExpression::filter(RevsetFilterPredicate::AuthorDate(
831            pattern,
832        )))
833    });
834    map.insert("mine", |_diagnostics, function, context| {
835        function.expect_no_arguments()?;
836        // Email address domains are inherently case‐insensitive, and the local‐parts
837        // are generally (although not universally) treated as case‐insensitive too, so
838        // we use a case‐insensitive match here.
839        let predicate =
840            RevsetFilterPredicate::AuthorEmail(StringPattern::exact_i(context.user_email));
841        Ok(RevsetExpression::filter(predicate))
842    });
843    map.insert("committer", |diagnostics, function, _context| {
844        let [arg] = function.expect_exact_arguments()?;
845        let pattern = expect_string_pattern(diagnostics, arg)?;
846        let name_predicate = RevsetFilterPredicate::CommitterName(pattern.clone());
847        let email_predicate = RevsetFilterPredicate::CommitterEmail(pattern);
848        Ok(RevsetExpression::filter(name_predicate)
849            .union(&RevsetExpression::filter(email_predicate)))
850    });
851    map.insert("committer_name", |diagnostics, function, _context| {
852        let [arg] = function.expect_exact_arguments()?;
853        let pattern = expect_string_pattern(diagnostics, arg)?;
854        let predicate = RevsetFilterPredicate::CommitterName(pattern);
855        Ok(RevsetExpression::filter(predicate))
856    });
857    map.insert("committer_email", |diagnostics, function, _context| {
858        let [arg] = function.expect_exact_arguments()?;
859        let pattern = expect_string_pattern(diagnostics, arg)?;
860        let predicate = RevsetFilterPredicate::CommitterEmail(pattern);
861        Ok(RevsetExpression::filter(predicate))
862    });
863    map.insert("committer_date", |diagnostics, function, context| {
864        let [arg] = function.expect_exact_arguments()?;
865        let pattern = expect_date_pattern(diagnostics, arg, context.date_pattern_context())?;
866        Ok(RevsetExpression::filter(
867            RevsetFilterPredicate::CommitterDate(pattern),
868        ))
869    });
870    map.insert("empty", |_diagnostics, function, _context| {
871        function.expect_no_arguments()?;
872        Ok(RevsetExpression::is_empty())
873    });
874    map.insert("files", |diagnostics, function, context| {
875        let ctx = context.workspace.as_ref().ok_or_else(|| {
876            RevsetParseError::with_span(
877                RevsetParseErrorKind::FsPathWithoutWorkspace,
878                function.args_span, // TODO: better to use name_span?
879            )
880        })?;
881        let [arg] = function.expect_exact_arguments()?;
882        let expr = expect_fileset_expression(diagnostics, arg, ctx.path_converter)?;
883        Ok(RevsetExpression::filter(RevsetFilterPredicate::File(expr)))
884    });
885    map.insert("diff_contains", |diagnostics, function, context| {
886        let ([text_arg], [files_opt_arg]) = function.expect_arguments()?;
887        let text = expect_string_pattern(diagnostics, text_arg)?;
888        let files = if let Some(files_arg) = files_opt_arg {
889            let ctx = context.workspace.as_ref().ok_or_else(|| {
890                RevsetParseError::with_span(
891                    RevsetParseErrorKind::FsPathWithoutWorkspace,
892                    files_arg.span,
893                )
894            })?;
895            expect_fileset_expression(diagnostics, files_arg, ctx.path_converter)?
896        } else {
897            // TODO: defaults to CLI path arguments?
898            // https://github.com/jj-vcs/jj/issues/2933#issuecomment-1925870731
899            FilesetExpression::all()
900        };
901        Ok(RevsetExpression::filter(
902            RevsetFilterPredicate::DiffContains { text, files },
903        ))
904    });
905    map.insert("conflicts", |_diagnostics, function, _context| {
906        function.expect_no_arguments()?;
907        Ok(RevsetExpression::filter(RevsetFilterPredicate::HasConflict))
908    });
909    map.insert("present", |diagnostics, function, context| {
910        let [arg] = function.expect_exact_arguments()?;
911        let expression = lower_expression(diagnostics, arg, context)?;
912        Ok(expression.present())
913    });
914    map.insert("at_operation", |diagnostics, function, context| {
915        let [op_arg, cand_arg] = function.expect_exact_arguments()?;
916        // TODO: Parse "opset" here if we add proper language support.
917        let operation =
918            revset_parser::expect_expression_with(diagnostics, op_arg, |_diagnostics, node| {
919                Ok(node.span.as_str().to_owned())
920            })?;
921        let candidates = lower_expression(diagnostics, cand_arg, context)?;
922        Ok(Rc::new(RevsetExpression::AtOperation {
923            operation,
924            candidates,
925        }))
926    });
927    map.insert("coalesce", |diagnostics, function, context| {
928        let ([], args) = function.expect_some_arguments()?;
929        let expressions: Vec<_> = args
930            .iter()
931            .map(|arg| lower_expression(diagnostics, arg, context))
932            .try_collect()?;
933        Ok(RevsetExpression::coalesce(&expressions))
934    });
935    map
936});
937
938/// Parses the given `node` as a fileset expression.
939pub fn expect_fileset_expression(
940    diagnostics: &mut RevsetDiagnostics,
941    node: &ExpressionNode,
942    path_converter: &RepoPathUiConverter,
943) -> Result<FilesetExpression, RevsetParseError> {
944    // Alias handling is a bit tricky. The outermost expression `alias` is
945    // substituted, but inner expressions `x & alias` aren't. If this seemed
946    // weird, we can either transform AST or turn off revset aliases completely.
947    revset_parser::expect_expression_with(diagnostics, node, |diagnostics, node| {
948        let mut inner_diagnostics = FilesetDiagnostics::new();
949        let expression = fileset::parse(&mut inner_diagnostics, node.span.as_str(), path_converter)
950            .map_err(|err| {
951                RevsetParseError::expression("In fileset expression", node.span).with_source(err)
952            })?;
953        diagnostics.extend_with(inner_diagnostics, |diag| {
954            RevsetParseError::expression("In fileset expression", node.span).with_source(diag)
955        });
956        Ok(expression)
957    })
958}
959
960pub fn expect_string_pattern(
961    diagnostics: &mut RevsetDiagnostics,
962    node: &ExpressionNode,
963) -> Result<StringPattern, RevsetParseError> {
964    revset_parser::expect_pattern_with(
965        diagnostics,
966        "string pattern",
967        node,
968        |_diagnostics, value, kind| match kind {
969            Some(kind) => StringPattern::from_str_kind(value, kind),
970            None => Ok(StringPattern::Substring(value.to_owned())),
971        },
972    )
973}
974
975pub fn expect_date_pattern(
976    diagnostics: &mut RevsetDiagnostics,
977    node: &ExpressionNode,
978    context: &DatePatternContext,
979) -> Result<DatePattern, RevsetParseError> {
980    revset_parser::expect_pattern_with(
981        diagnostics,
982        "date pattern",
983        node,
984        |_diagnostics, value, kind| -> Result<_, Box<dyn std::error::Error + Send + Sync>> {
985            match kind {
986                None => Err("Date pattern must specify 'after' or 'before'".into()),
987                Some(kind) => Ok(context.parse_relative(value, kind)?),
988            }
989        },
990    )
991}
992
993fn parse_remote_bookmarks_arguments(
994    diagnostics: &mut RevsetDiagnostics,
995    function: &FunctionCallNode,
996    remote_ref_state: Option<RemoteRefState>,
997) -> Result<Rc<UserRevsetExpression>, RevsetParseError> {
998    let ([], [bookmark_opt_arg, remote_opt_arg]) =
999        function.expect_named_arguments(&["", "remote"])?;
1000    let bookmark_pattern = if let Some(bookmark_arg) = bookmark_opt_arg {
1001        expect_string_pattern(diagnostics, bookmark_arg)?
1002    } else {
1003        StringPattern::everything()
1004    };
1005    let remote_pattern = if let Some(remote_arg) = remote_opt_arg {
1006        expect_string_pattern(diagnostics, remote_arg)?
1007    } else {
1008        StringPattern::everything()
1009    };
1010    Ok(RevsetExpression::remote_bookmarks(
1011        bookmark_pattern,
1012        remote_pattern,
1013        remote_ref_state,
1014    ))
1015}
1016
1017/// Resolves function call by using the given function map.
1018fn lower_function_call(
1019    diagnostics: &mut RevsetDiagnostics,
1020    function: &FunctionCallNode,
1021    context: &LoweringContext,
1022) -> Result<Rc<UserRevsetExpression>, RevsetParseError> {
1023    let function_map = &context.extensions.function_map;
1024    if let Some(func) = function_map.get(function.name) {
1025        func(diagnostics, function, context)
1026    } else {
1027        Err(RevsetParseError::with_span(
1028            RevsetParseErrorKind::NoSuchFunction {
1029                name: function.name.to_owned(),
1030                candidates: collect_similar(function.name, function_map.keys()),
1031            },
1032            function.name_span,
1033        ))
1034    }
1035}
1036
1037/// Transforms the given AST `node` into expression that describes DAG
1038/// operation. Function calls will be resolved at this stage.
1039pub fn lower_expression(
1040    diagnostics: &mut RevsetDiagnostics,
1041    node: &ExpressionNode,
1042    context: &LoweringContext,
1043) -> Result<Rc<UserRevsetExpression>, RevsetParseError> {
1044    match &node.kind {
1045        ExpressionKind::Identifier(name) => Ok(RevsetExpression::symbol((*name).to_owned())),
1046        ExpressionKind::String(name) => Ok(RevsetExpression::symbol(name.to_owned())),
1047        ExpressionKind::StringPattern { .. } => Err(RevsetParseError::with_span(
1048            RevsetParseErrorKind::NotInfixOperator {
1049                op: ":".to_owned(),
1050                similar_op: "::".to_owned(),
1051                description: "DAG range".to_owned(),
1052            },
1053            node.span,
1054        )),
1055        ExpressionKind::RemoteSymbol(symbol) => Ok(RevsetExpression::remote_symbol(symbol.clone())),
1056        ExpressionKind::AtWorkspace(name) => Ok(RevsetExpression::working_copy(name.into())),
1057        ExpressionKind::AtCurrentWorkspace => {
1058            let ctx = context.workspace.as_ref().ok_or_else(|| {
1059                RevsetParseError::with_span(
1060                    RevsetParseErrorKind::WorkingCopyWithoutWorkspace,
1061                    node.span,
1062                )
1063            })?;
1064            Ok(RevsetExpression::working_copy(
1065                ctx.workspace_name.to_owned(),
1066            ))
1067        }
1068        ExpressionKind::DagRangeAll => Ok(RevsetExpression::all()),
1069        ExpressionKind::RangeAll => {
1070            Ok(RevsetExpression::root().range(&RevsetExpression::visible_heads()))
1071        }
1072        ExpressionKind::Unary(op, arg_node) => {
1073            let arg = lower_expression(diagnostics, arg_node, context)?;
1074            match op {
1075                UnaryOp::Negate => Ok(arg.negated()),
1076                UnaryOp::DagRangePre => Ok(arg.ancestors()),
1077                UnaryOp::DagRangePost => Ok(arg.descendants()),
1078                UnaryOp::RangePre => Ok(RevsetExpression::root().range(&arg)),
1079                UnaryOp::RangePost => Ok(arg.range(&RevsetExpression::visible_heads())),
1080                UnaryOp::Parents => Ok(arg.parents()),
1081                UnaryOp::Children => Ok(arg.children()),
1082            }
1083        }
1084        ExpressionKind::Binary(op, lhs_node, rhs_node) => {
1085            let lhs = lower_expression(diagnostics, lhs_node, context)?;
1086            let rhs = lower_expression(diagnostics, rhs_node, context)?;
1087            match op {
1088                BinaryOp::Intersection => Ok(lhs.intersection(&rhs)),
1089                BinaryOp::Difference => Ok(lhs.minus(&rhs)),
1090                BinaryOp::DagRange => Ok(lhs.dag_range_to(&rhs)),
1091                BinaryOp::Range => Ok(lhs.range(&rhs)),
1092            }
1093        }
1094        ExpressionKind::UnionAll(nodes) => {
1095            let expressions: Vec<_> = nodes
1096                .iter()
1097                .map(|node| lower_expression(diagnostics, node, context))
1098                .try_collect()?;
1099            Ok(RevsetExpression::union_all(&expressions))
1100        }
1101        ExpressionKind::FunctionCall(function) => {
1102            lower_function_call(diagnostics, function, context)
1103        }
1104        ExpressionKind::Modifier(modifier) => {
1105            let name = modifier.name;
1106            Err(RevsetParseError::expression(
1107                format!("Modifier `{name}:` is not allowed in sub expression"),
1108                modifier.name_span,
1109            ))
1110        }
1111        ExpressionKind::AliasExpanded(id, subst) => {
1112            let mut inner_diagnostics = RevsetDiagnostics::new();
1113            let expression = lower_expression(&mut inner_diagnostics, subst, context)
1114                .map_err(|e| e.within_alias_expansion(*id, node.span))?;
1115            diagnostics.extend_with(inner_diagnostics, |diag| {
1116                diag.within_alias_expansion(*id, node.span)
1117            });
1118            Ok(expression)
1119        }
1120    }
1121}
1122
1123pub fn parse(
1124    diagnostics: &mut RevsetDiagnostics,
1125    revset_str: &str,
1126    context: &RevsetParseContext,
1127) -> Result<Rc<UserRevsetExpression>, RevsetParseError> {
1128    let node = parse_program(revset_str)?;
1129    let node =
1130        dsl_util::expand_aliases_with_locals(node, context.aliases_map, &context.local_variables)?;
1131    lower_expression(diagnostics, &node, &context.to_lowering_context())
1132        .map_err(|err| err.extend_function_candidates(context.aliases_map.function_names()))
1133}
1134
1135pub fn parse_with_modifier(
1136    diagnostics: &mut RevsetDiagnostics,
1137    revset_str: &str,
1138    context: &RevsetParseContext,
1139) -> Result<(Rc<UserRevsetExpression>, Option<RevsetModifier>), RevsetParseError> {
1140    let node = parse_program(revset_str)?;
1141    let node =
1142        dsl_util::expand_aliases_with_locals(node, context.aliases_map, &context.local_variables)?;
1143    revset_parser::expect_program_with(
1144        diagnostics,
1145        &node,
1146        |diagnostics, node| lower_expression(diagnostics, node, &context.to_lowering_context()),
1147        |_diagnostics, name, span| match name {
1148            "all" => Ok(RevsetModifier::All),
1149            _ => Err(RevsetParseError::with_span(
1150                RevsetParseErrorKind::NoSuchModifier(name.to_owned()),
1151                span,
1152            )),
1153        },
1154    )
1155    .map_err(|err| err.extend_function_candidates(context.aliases_map.function_names()))
1156}
1157
1158/// `Some` for rewritten expression, or `None` to reuse the original expression.
1159type TransformedExpression<St> = Option<Rc<RevsetExpression<St>>>;
1160
1161/// Walks `expression` tree and applies `f` recursively from leaf nodes.
1162fn transform_expression_bottom_up<St: ExpressionState>(
1163    expression: &Rc<RevsetExpression<St>>,
1164    mut f: impl FnMut(&Rc<RevsetExpression<St>>) -> TransformedExpression<St>,
1165) -> TransformedExpression<St> {
1166    try_transform_expression::<St, Infallible>(
1167        expression,
1168        |_| Ok(None),
1169        |expression| Ok(f(expression)),
1170    )
1171    .unwrap()
1172}
1173
1174/// Walks `expression` tree and applies transformation recursively.
1175///
1176/// `pre` is the callback to rewrite subtree including children. It is
1177/// invoked before visiting the child nodes. If returned `Some`, children
1178/// won't be visited.
1179///
1180/// `post` is the callback to rewrite from leaf nodes. If returned `None`,
1181/// the original expression node will be reused.
1182///
1183/// If no nodes rewritten, this function returns `None`.
1184/// `std::iter::successors()` could be used if the transformation needs to be
1185/// applied repeatedly until converged.
1186fn try_transform_expression<St: ExpressionState, E>(
1187    expression: &Rc<RevsetExpression<St>>,
1188    mut pre: impl FnMut(&Rc<RevsetExpression<St>>) -> Result<TransformedExpression<St>, E>,
1189    mut post: impl FnMut(&Rc<RevsetExpression<St>>) -> Result<TransformedExpression<St>, E>,
1190) -> Result<TransformedExpression<St>, E> {
1191    fn transform_child_rec<St: ExpressionState, E>(
1192        expression: &Rc<RevsetExpression<St>>,
1193        pre: &mut impl FnMut(&Rc<RevsetExpression<St>>) -> Result<TransformedExpression<St>, E>,
1194        post: &mut impl FnMut(&Rc<RevsetExpression<St>>) -> Result<TransformedExpression<St>, E>,
1195    ) -> Result<TransformedExpression<St>, E> {
1196        Ok(match expression.as_ref() {
1197            RevsetExpression::None => None,
1198            RevsetExpression::All => None,
1199            RevsetExpression::VisibleHeads => None,
1200            RevsetExpression::Root => None,
1201            RevsetExpression::Commits(_) => None,
1202            RevsetExpression::CommitRef(_) => None,
1203            RevsetExpression::Ancestors { heads, generation } => transform_rec(heads, pre, post)?
1204                .map(|heads| RevsetExpression::Ancestors {
1205                    heads,
1206                    generation: generation.clone(),
1207                }),
1208            RevsetExpression::Descendants { roots, generation } => transform_rec(roots, pre, post)?
1209                .map(|roots| RevsetExpression::Descendants {
1210                    roots,
1211                    generation: generation.clone(),
1212                }),
1213            RevsetExpression::Range {
1214                roots,
1215                heads,
1216                generation,
1217            } => transform_rec_pair((roots, heads), pre, post)?.map(|(roots, heads)| {
1218                RevsetExpression::Range {
1219                    roots,
1220                    heads,
1221                    generation: generation.clone(),
1222                }
1223            }),
1224            RevsetExpression::DagRange { roots, heads } => {
1225                transform_rec_pair((roots, heads), pre, post)?
1226                    .map(|(roots, heads)| RevsetExpression::DagRange { roots, heads })
1227            }
1228            RevsetExpression::Reachable { sources, domain } => {
1229                transform_rec_pair((sources, domain), pre, post)?
1230                    .map(|(sources, domain)| RevsetExpression::Reachable { sources, domain })
1231            }
1232            RevsetExpression::Heads(candidates) => {
1233                transform_rec(candidates, pre, post)?.map(RevsetExpression::Heads)
1234            }
1235            RevsetExpression::Roots(candidates) => {
1236                transform_rec(candidates, pre, post)?.map(RevsetExpression::Roots)
1237            }
1238            RevsetExpression::ForkPoint(expression) => {
1239                transform_rec(expression, pre, post)?.map(RevsetExpression::ForkPoint)
1240            }
1241            RevsetExpression::Latest { candidates, count } => transform_rec(candidates, pre, post)?
1242                .map(|candidates| RevsetExpression::Latest {
1243                    candidates,
1244                    count: *count,
1245                }),
1246            RevsetExpression::Filter(_) => None,
1247            RevsetExpression::AsFilter(candidates) => {
1248                transform_rec(candidates, pre, post)?.map(RevsetExpression::AsFilter)
1249            }
1250            RevsetExpression::AtOperation {
1251                operation,
1252                candidates,
1253            } => transform_rec(candidates, pre, post)?.map(|candidates| {
1254                RevsetExpression::AtOperation {
1255                    operation: operation.clone(),
1256                    candidates,
1257                }
1258            }),
1259            RevsetExpression::WithinVisibility {
1260                candidates,
1261                visible_heads,
1262            } => transform_rec(candidates, pre, post)?.map(|candidates| {
1263                RevsetExpression::WithinVisibility {
1264                    candidates,
1265                    visible_heads: visible_heads.clone(),
1266                }
1267            }),
1268            RevsetExpression::Coalesce(expression1, expression2) => transform_rec_pair(
1269                (expression1, expression2),
1270                pre,
1271                post,
1272            )?
1273            .map(|(expression1, expression2)| RevsetExpression::Coalesce(expression1, expression2)),
1274            RevsetExpression::Present(candidates) => {
1275                transform_rec(candidates, pre, post)?.map(RevsetExpression::Present)
1276            }
1277            RevsetExpression::NotIn(complement) => {
1278                transform_rec(complement, pre, post)?.map(RevsetExpression::NotIn)
1279            }
1280            RevsetExpression::Union(expression1, expression2) => {
1281                transform_rec_pair((expression1, expression2), pre, post)?.map(
1282                    |(expression1, expression2)| RevsetExpression::Union(expression1, expression2),
1283                )
1284            }
1285            RevsetExpression::Intersection(expression1, expression2) => {
1286                transform_rec_pair((expression1, expression2), pre, post)?.map(
1287                    |(expression1, expression2)| {
1288                        RevsetExpression::Intersection(expression1, expression2)
1289                    },
1290                )
1291            }
1292            RevsetExpression::Difference(expression1, expression2) => {
1293                transform_rec_pair((expression1, expression2), pre, post)?.map(
1294                    |(expression1, expression2)| {
1295                        RevsetExpression::Difference(expression1, expression2)
1296                    },
1297                )
1298            }
1299        }
1300        .map(Rc::new))
1301    }
1302
1303    #[expect(clippy::type_complexity)]
1304    fn transform_rec_pair<St: ExpressionState, E>(
1305        (expression1, expression2): (&Rc<RevsetExpression<St>>, &Rc<RevsetExpression<St>>),
1306        pre: &mut impl FnMut(&Rc<RevsetExpression<St>>) -> Result<TransformedExpression<St>, E>,
1307        post: &mut impl FnMut(&Rc<RevsetExpression<St>>) -> Result<TransformedExpression<St>, E>,
1308    ) -> Result<Option<(Rc<RevsetExpression<St>>, Rc<RevsetExpression<St>>)>, E> {
1309        match (
1310            transform_rec(expression1, pre, post)?,
1311            transform_rec(expression2, pre, post)?,
1312        ) {
1313            (Some(new_expression1), Some(new_expression2)) => {
1314                Ok(Some((new_expression1, new_expression2)))
1315            }
1316            (Some(new_expression1), None) => Ok(Some((new_expression1, expression2.clone()))),
1317            (None, Some(new_expression2)) => Ok(Some((expression1.clone(), new_expression2))),
1318            (None, None) => Ok(None),
1319        }
1320    }
1321
1322    fn transform_rec<St: ExpressionState, E>(
1323        expression: &Rc<RevsetExpression<St>>,
1324        pre: &mut impl FnMut(&Rc<RevsetExpression<St>>) -> Result<TransformedExpression<St>, E>,
1325        post: &mut impl FnMut(&Rc<RevsetExpression<St>>) -> Result<TransformedExpression<St>, E>,
1326    ) -> Result<TransformedExpression<St>, E> {
1327        if let Some(new_expression) = pre(expression)? {
1328            return Ok(Some(new_expression));
1329        }
1330        if let Some(new_expression) = transform_child_rec(expression, pre, post)? {
1331            // must propagate new expression tree
1332            Ok(Some(post(&new_expression)?.unwrap_or(new_expression)))
1333        } else {
1334            post(expression)
1335        }
1336    }
1337
1338    transform_rec(expression, &mut pre, &mut post)
1339}
1340
1341/// Visitor-like interface to transform [`RevsetExpression`] state recursively.
1342///
1343/// This is similar to [`try_transform_expression()`], but is supposed to
1344/// transform the resolution state from `InSt` to `OutSt`.
1345trait ExpressionStateFolder<InSt: ExpressionState, OutSt: ExpressionState> {
1346    type Error;
1347
1348    /// Transforms the `expression`. By default, inner items are transformed
1349    /// recursively.
1350    fn fold_expression(
1351        &mut self,
1352        expression: &RevsetExpression<InSt>,
1353    ) -> Result<Rc<RevsetExpression<OutSt>>, Self::Error> {
1354        fold_child_expression_state(self, expression)
1355    }
1356
1357    /// Transforms commit ref such as symbol.
1358    fn fold_commit_ref(
1359        &mut self,
1360        commit_ref: &InSt::CommitRef,
1361    ) -> Result<Rc<RevsetExpression<OutSt>>, Self::Error>;
1362
1363    /// Transforms `at_operation(operation, candidates)` expression.
1364    fn fold_at_operation(
1365        &mut self,
1366        operation: &InSt::Operation,
1367        candidates: &RevsetExpression<InSt>,
1368    ) -> Result<Rc<RevsetExpression<OutSt>>, Self::Error>;
1369}
1370
1371/// Transforms inner items of the `expression` by using the `folder`.
1372fn fold_child_expression_state<InSt, OutSt, F>(
1373    folder: &mut F,
1374    expression: &RevsetExpression<InSt>,
1375) -> Result<Rc<RevsetExpression<OutSt>>, F::Error>
1376where
1377    InSt: ExpressionState,
1378    OutSt: ExpressionState,
1379    F: ExpressionStateFolder<InSt, OutSt> + ?Sized,
1380{
1381    let expression: Rc<_> = match expression {
1382        RevsetExpression::None => RevsetExpression::None.into(),
1383        RevsetExpression::All => RevsetExpression::All.into(),
1384        RevsetExpression::VisibleHeads => RevsetExpression::VisibleHeads.into(),
1385        RevsetExpression::Root => RevsetExpression::Root.into(),
1386        RevsetExpression::Commits(ids) => RevsetExpression::Commits(ids.clone()).into(),
1387        RevsetExpression::CommitRef(commit_ref) => folder.fold_commit_ref(commit_ref)?,
1388        RevsetExpression::Ancestors { heads, generation } => {
1389            let heads = folder.fold_expression(heads)?;
1390            let generation = generation.clone();
1391            RevsetExpression::Ancestors { heads, generation }.into()
1392        }
1393        RevsetExpression::Descendants { roots, generation } => {
1394            let roots = folder.fold_expression(roots)?;
1395            let generation = generation.clone();
1396            RevsetExpression::Descendants { roots, generation }.into()
1397        }
1398        RevsetExpression::Range {
1399            roots,
1400            heads,
1401            generation,
1402        } => {
1403            let roots = folder.fold_expression(roots)?;
1404            let heads = folder.fold_expression(heads)?;
1405            let generation = generation.clone();
1406            RevsetExpression::Range {
1407                roots,
1408                heads,
1409                generation,
1410            }
1411            .into()
1412        }
1413        RevsetExpression::DagRange { roots, heads } => {
1414            let roots = folder.fold_expression(roots)?;
1415            let heads = folder.fold_expression(heads)?;
1416            RevsetExpression::DagRange { roots, heads }.into()
1417        }
1418        RevsetExpression::Reachable { sources, domain } => {
1419            let sources = folder.fold_expression(sources)?;
1420            let domain = folder.fold_expression(domain)?;
1421            RevsetExpression::Reachable { sources, domain }.into()
1422        }
1423        RevsetExpression::Heads(heads) => {
1424            let heads = folder.fold_expression(heads)?;
1425            RevsetExpression::Heads(heads).into()
1426        }
1427        RevsetExpression::Roots(roots) => {
1428            let roots = folder.fold_expression(roots)?;
1429            RevsetExpression::Roots(roots).into()
1430        }
1431        RevsetExpression::ForkPoint(expression) => {
1432            let expression = folder.fold_expression(expression)?;
1433            RevsetExpression::ForkPoint(expression).into()
1434        }
1435        RevsetExpression::Latest { candidates, count } => {
1436            let candidates = folder.fold_expression(candidates)?;
1437            let count = *count;
1438            RevsetExpression::Latest { candidates, count }.into()
1439        }
1440        RevsetExpression::Filter(predicate) => RevsetExpression::Filter(predicate.clone()).into(),
1441        RevsetExpression::AsFilter(candidates) => {
1442            let candidates = folder.fold_expression(candidates)?;
1443            RevsetExpression::AsFilter(candidates).into()
1444        }
1445        RevsetExpression::AtOperation {
1446            operation,
1447            candidates,
1448        } => folder.fold_at_operation(operation, candidates)?,
1449        RevsetExpression::WithinVisibility {
1450            candidates,
1451            visible_heads,
1452        } => {
1453            let candidates = folder.fold_expression(candidates)?;
1454            let visible_heads = visible_heads.clone();
1455            RevsetExpression::WithinVisibility {
1456                candidates,
1457                visible_heads,
1458            }
1459            .into()
1460        }
1461        RevsetExpression::Coalesce(expression1, expression2) => {
1462            let expression1 = folder.fold_expression(expression1)?;
1463            let expression2 = folder.fold_expression(expression2)?;
1464            RevsetExpression::Coalesce(expression1, expression2).into()
1465        }
1466        RevsetExpression::Present(candidates) => {
1467            let candidates = folder.fold_expression(candidates)?;
1468            RevsetExpression::Present(candidates).into()
1469        }
1470        RevsetExpression::NotIn(complement) => {
1471            let complement = folder.fold_expression(complement)?;
1472            RevsetExpression::NotIn(complement).into()
1473        }
1474        RevsetExpression::Union(expression1, expression2) => {
1475            let expression1 = folder.fold_expression(expression1)?;
1476            let expression2 = folder.fold_expression(expression2)?;
1477            RevsetExpression::Union(expression1, expression2).into()
1478        }
1479        RevsetExpression::Intersection(expression1, expression2) => {
1480            let expression1 = folder.fold_expression(expression1)?;
1481            let expression2 = folder.fold_expression(expression2)?;
1482            RevsetExpression::Intersection(expression1, expression2).into()
1483        }
1484        RevsetExpression::Difference(expression1, expression2) => {
1485            let expression1 = folder.fold_expression(expression1)?;
1486            let expression2 = folder.fold_expression(expression2)?;
1487            RevsetExpression::Difference(expression1, expression2).into()
1488        }
1489    };
1490    Ok(expression)
1491}
1492
1493/// Transforms filter expressions, by applying the following rules.
1494///
1495/// a. Moves as many sets to left of filter intersection as possible, to
1496///    minimize the filter inputs.
1497/// b. TODO: Rewrites set operations to and/or/not of predicates, to
1498///    help further optimization (e.g. combine `file(_)` matchers.)
1499/// c. Wraps union of filter and set (e.g. `author(_) | heads()`), to
1500///    ensure inner filter wouldn't need to evaluate all the input sets.
1501fn internalize_filter<St: ExpressionState>(
1502    expression: &Rc<RevsetExpression<St>>,
1503) -> TransformedExpression<St> {
1504    fn is_filter<St: ExpressionState>(expression: &RevsetExpression<St>) -> bool {
1505        matches!(
1506            expression,
1507            RevsetExpression::Filter(_) | RevsetExpression::AsFilter(_)
1508        )
1509    }
1510
1511    fn is_filter_tree<St: ExpressionState>(expression: &RevsetExpression<St>) -> bool {
1512        is_filter(expression) || as_filter_intersection(expression).is_some()
1513    }
1514
1515    // Extracts 'c & f' from intersect_down()-ed node.
1516    #[expect(clippy::type_complexity)]
1517    fn as_filter_intersection<St: ExpressionState>(
1518        expression: &RevsetExpression<St>,
1519    ) -> Option<(&Rc<RevsetExpression<St>>, &Rc<RevsetExpression<St>>)> {
1520        if let RevsetExpression::Intersection(expression1, expression2) = expression {
1521            is_filter(expression2).then_some((expression1, expression2))
1522        } else {
1523            None
1524        }
1525    }
1526
1527    // Since both sides must have already been intersect_down()-ed, we don't need to
1528    // apply the whole bottom-up pass to new intersection node. Instead, just push
1529    // new 'c & (d & g)' down-left to '(c & d) & g' while either side is
1530    // an intersection of filter node.
1531    fn intersect_down<St: ExpressionState>(
1532        expression1: &Rc<RevsetExpression<St>>,
1533        expression2: &Rc<RevsetExpression<St>>,
1534    ) -> TransformedExpression<St> {
1535        let recurse = |e1, e2| intersect_down(e1, e2).unwrap_or_else(|| e1.intersection(e2));
1536        match (expression1.as_ref(), expression2.as_ref()) {
1537            // Don't reorder 'f1 & f2'
1538            (_, e2) if is_filter(e2) => None,
1539            // f1 & e2 -> e2 & f1
1540            (e1, _) if is_filter(e1) => Some(expression2.intersection(expression1)),
1541            (e1, e2) => match (as_filter_intersection(e1), as_filter_intersection(e2)) {
1542                // e1 & (c2 & f2) -> (e1 & c2) & f2
1543                // (c1 & f1) & (c2 & f2) -> ((c1 & f1) & c2) & f2 -> ((c1 & c2) & f1) & f2
1544                (_, Some((c2, f2))) => Some(recurse(expression1, c2).intersection(f2)),
1545                // (c1 & f1) & e2 -> (c1 & e2) & f1
1546                // ((c1 & f1) & g1) & e2 -> ((c1 & f1) & e2) & g1 -> ((c1 & e2) & f1) & g1
1547                (Some((c1, f1)), _) => Some(recurse(c1, expression2).intersection(f1)),
1548                (None, None) => None,
1549            },
1550        }
1551    }
1552
1553    // Bottom-up pass pulls up-right filter node from leaf '(c & f) & e' ->
1554    // '(c & e) & f', so that an intersection of filter node can be found as
1555    // a direct child of another intersection node. However, the rewritten
1556    // intersection node 'c & e' can also be a rewrite target if 'e' contains
1557    // a filter node. That's why intersect_down() is also recursive.
1558    transform_expression_bottom_up(expression, |expression| match expression.as_ref() {
1559        RevsetExpression::Present(e) => {
1560            is_filter_tree(e).then(|| Rc::new(RevsetExpression::AsFilter(expression.clone())))
1561        }
1562        RevsetExpression::NotIn(e) => {
1563            is_filter_tree(e).then(|| Rc::new(RevsetExpression::AsFilter(expression.clone())))
1564        }
1565        RevsetExpression::Union(e1, e2) => (is_filter_tree(e1) || is_filter_tree(e2))
1566            .then(|| Rc::new(RevsetExpression::AsFilter(expression.clone()))),
1567        RevsetExpression::Intersection(expression1, expression2) => {
1568            intersect_down(expression1, expression2)
1569        }
1570        // Difference(e1, e2) should have been unfolded to Intersection(e1, NotIn(e2)).
1571        _ => None,
1572    })
1573}
1574
1575/// Eliminates redundant nodes like `x & all()`, `~~x`.
1576///
1577/// This does not rewrite 'x & none()' to 'none()' because 'x' may be an invalid
1578/// symbol.
1579fn fold_redundant_expression<St: ExpressionState>(
1580    expression: &Rc<RevsetExpression<St>>,
1581) -> TransformedExpression<St> {
1582    transform_expression_bottom_up(expression, |expression| match expression.as_ref() {
1583        RevsetExpression::NotIn(outer) => match outer.as_ref() {
1584            RevsetExpression::NotIn(inner) => Some(inner.clone()),
1585            _ => None,
1586        },
1587        RevsetExpression::Intersection(expression1, expression2) => {
1588            match (expression1.as_ref(), expression2.as_ref()) {
1589                (_, RevsetExpression::All) => Some(expression1.clone()),
1590                (RevsetExpression::All, _) => Some(expression2.clone()),
1591                _ => None,
1592            }
1593        }
1594        _ => None,
1595    })
1596}
1597
1598fn to_difference_range<St: ExpressionState>(
1599    expression: &Rc<RevsetExpression<St>>,
1600    complement: &Rc<RevsetExpression<St>>,
1601) -> TransformedExpression<St> {
1602    match (expression.as_ref(), complement.as_ref()) {
1603        // ::heads & ~(::roots) -> roots..heads
1604        (
1605            RevsetExpression::Ancestors { heads, generation },
1606            RevsetExpression::Ancestors {
1607                heads: roots,
1608                generation: GENERATION_RANGE_FULL,
1609            },
1610        ) => Some(Rc::new(RevsetExpression::Range {
1611            roots: roots.clone(),
1612            heads: heads.clone(),
1613            generation: generation.clone(),
1614        })),
1615        // ::heads & ~(::roots-) -> ::heads & ~ancestors(roots, 1..) -> roots-..heads
1616        (
1617            RevsetExpression::Ancestors { heads, generation },
1618            RevsetExpression::Ancestors {
1619                heads: roots,
1620                generation:
1621                    Range {
1622                        start: roots_start,
1623                        end: u64::MAX,
1624                    },
1625            },
1626        ) => Some(Rc::new(RevsetExpression::Range {
1627            roots: roots.ancestors_at(*roots_start),
1628            heads: heads.clone(),
1629            generation: generation.clone(),
1630        })),
1631        _ => None,
1632    }
1633}
1634
1635/// Transforms negative intersection to difference. Redundant intersections like
1636/// `all() & e` should have been removed.
1637fn fold_difference<St: ExpressionState>(
1638    expression: &Rc<RevsetExpression<St>>,
1639) -> TransformedExpression<St> {
1640    fn to_difference<St: ExpressionState>(
1641        expression: &Rc<RevsetExpression<St>>,
1642        complement: &Rc<RevsetExpression<St>>,
1643    ) -> Rc<RevsetExpression<St>> {
1644        to_difference_range(expression, complement).unwrap_or_else(|| expression.minus(complement))
1645    }
1646
1647    transform_expression_bottom_up(expression, |expression| match expression.as_ref() {
1648        RevsetExpression::Intersection(expression1, expression2) => {
1649            match (expression1.as_ref(), expression2.as_ref()) {
1650                // For '~x & f', don't move filter node 'f' left
1651                (_, RevsetExpression::Filter(_) | RevsetExpression::AsFilter(_)) => None,
1652                (_, RevsetExpression::NotIn(complement)) => {
1653                    Some(to_difference(expression1, complement))
1654                }
1655                (RevsetExpression::NotIn(complement), _) => {
1656                    Some(to_difference(expression2, complement))
1657                }
1658                _ => None,
1659            }
1660        }
1661        _ => None,
1662    })
1663}
1664
1665/// Transforms remaining negated ancestors `~(::h)` to range `h..`.
1666///
1667/// Since this rule inserts redundant `visible_heads()`, negative intersections
1668/// should have been transformed.
1669fn fold_not_in_ancestors<St: ExpressionState>(
1670    expression: &Rc<RevsetExpression<St>>,
1671) -> TransformedExpression<St> {
1672    transform_expression_bottom_up(expression, |expression| match expression.as_ref() {
1673        RevsetExpression::NotIn(complement)
1674            if matches!(complement.as_ref(), RevsetExpression::Ancestors { .. }) =>
1675        {
1676            // ~(::heads) -> heads..
1677            // ~(::heads-) -> ~ancestors(heads, 1..) -> heads-..
1678            to_difference_range(&RevsetExpression::visible_heads().ancestors(), complement)
1679        }
1680        _ => None,
1681    })
1682}
1683
1684/// Transforms binary difference to more primitive negative intersection.
1685///
1686/// For example, `all() ~ e` will become `all() & ~e`, which can be simplified
1687/// further by `fold_redundant_expression()`.
1688fn unfold_difference<St: ExpressionState>(
1689    expression: &Rc<RevsetExpression<St>>,
1690) -> TransformedExpression<St> {
1691    transform_expression_bottom_up(expression, |expression| match expression.as_ref() {
1692        // roots..heads -> ::heads & ~(::roots)
1693        RevsetExpression::Range {
1694            roots,
1695            heads,
1696            generation,
1697        } => {
1698            let heads_ancestors = Rc::new(RevsetExpression::Ancestors {
1699                heads: heads.clone(),
1700                generation: generation.clone(),
1701            });
1702            Some(heads_ancestors.intersection(&roots.ancestors().negated()))
1703        }
1704        RevsetExpression::Difference(expression1, expression2) => {
1705            Some(expression1.intersection(&expression2.negated()))
1706        }
1707        _ => None,
1708    })
1709}
1710
1711/// Transforms nested `ancestors()`/`parents()`/`descendants()`/`children()`
1712/// like `h---`/`r+++`.
1713fn fold_generation<St: ExpressionState>(
1714    expression: &Rc<RevsetExpression<St>>,
1715) -> TransformedExpression<St> {
1716    fn add_generation(generation1: &Range<u64>, generation2: &Range<u64>) -> Range<u64> {
1717        // For any (g1, g2) in (generation1, generation2), g1 + g2.
1718        if generation1.is_empty() || generation2.is_empty() {
1719            GENERATION_RANGE_EMPTY
1720        } else {
1721            let start = u64::saturating_add(generation1.start, generation2.start);
1722            let end = u64::saturating_add(generation1.end, generation2.end - 1);
1723            start..end
1724        }
1725    }
1726
1727    transform_expression_bottom_up(expression, |expression| match expression.as_ref() {
1728        RevsetExpression::Ancestors {
1729            heads,
1730            generation: generation1,
1731        } => {
1732            match heads.as_ref() {
1733                // (h-)- -> ancestors(ancestors(h, 1), 1) -> ancestors(h, 2)
1734                // ::(h-) -> ancestors(ancestors(h, 1), ..) -> ancestors(h, 1..)
1735                // (::h)- -> ancestors(ancestors(h, ..), 1) -> ancestors(h, 1..)
1736                RevsetExpression::Ancestors {
1737                    heads,
1738                    generation: generation2,
1739                } => Some(Rc::new(RevsetExpression::Ancestors {
1740                    heads: heads.clone(),
1741                    generation: add_generation(generation1, generation2),
1742                })),
1743                _ => None,
1744            }
1745        }
1746        RevsetExpression::Descendants {
1747            roots,
1748            generation: generation1,
1749        } => {
1750            match roots.as_ref() {
1751                // (r+)+ -> descendants(descendants(r, 1), 1) -> descendants(r, 2)
1752                // (r+):: -> descendants(descendants(r, 1), ..) -> descendants(r, 1..)
1753                // (r::)+ -> descendants(descendants(r, ..), 1) -> descendants(r, 1..)
1754                RevsetExpression::Descendants {
1755                    roots,
1756                    generation: generation2,
1757                } => Some(Rc::new(RevsetExpression::Descendants {
1758                    roots: roots.clone(),
1759                    generation: add_generation(generation1, generation2),
1760                })),
1761                _ => None,
1762            }
1763        }
1764        // Range should have been unfolded to intersection of Ancestors.
1765        _ => None,
1766    })
1767}
1768
1769/// Rewrites the given `expression` tree to reduce evaluation cost. Returns new
1770/// tree.
1771pub fn optimize<St: ExpressionState>(
1772    expression: Rc<RevsetExpression<St>>,
1773) -> Rc<RevsetExpression<St>> {
1774    let expression = unfold_difference(&expression).unwrap_or(expression);
1775    let expression = fold_redundant_expression(&expression).unwrap_or(expression);
1776    let expression = fold_generation(&expression).unwrap_or(expression);
1777    let expression = internalize_filter(&expression).unwrap_or(expression);
1778    let expression = fold_difference(&expression).unwrap_or(expression);
1779    fold_not_in_ancestors(&expression).unwrap_or(expression)
1780}
1781
1782// TODO: find better place to host this function (or add compile-time revset
1783// parsing and resolution like
1784// `revset!("{unwanted}..{wanted}").evaluate(repo)`?)
1785pub fn walk_revs<'index>(
1786    repo: &'index dyn Repo,
1787    wanted: &[CommitId],
1788    unwanted: &[CommitId],
1789) -> Result<Box<dyn Revset + 'index>, RevsetEvaluationError> {
1790    RevsetExpression::commits(unwanted.to_vec())
1791        .range(&RevsetExpression::commits(wanted.to_vec()))
1792        .evaluate(repo)
1793}
1794
1795fn reload_repo_at_operation(
1796    repo: &dyn Repo,
1797    op_str: &str,
1798) -> Result<Arc<ReadonlyRepo>, RevsetResolutionError> {
1799    // TODO: Maybe we should ensure that the resolved operation is an ancestor
1800    // of the current operation. If it weren't, there might be commits unknown
1801    // to the outer repo.
1802    let base_repo = repo.base_repo();
1803    let operation = op_walk::resolve_op_with_repo(base_repo, op_str)
1804        .map_err(|err| RevsetResolutionError::Other(err.into()))?;
1805    base_repo.reload_at(&operation).map_err(|err| match err {
1806        RepoLoaderError::Backend(err) => RevsetResolutionError::StoreError(err),
1807        RepoLoaderError::IndexRead(_)
1808        | RepoLoaderError::OpHeadResolution(_)
1809        | RepoLoaderError::OpHeadsStoreError(_)
1810        | RepoLoaderError::OpStore(_) => RevsetResolutionError::Other(err.into()),
1811    })
1812}
1813
1814fn resolve_remote_bookmark(repo: &dyn Repo, symbol: RemoteRefSymbol<'_>) -> Option<Vec<CommitId>> {
1815    let target = &repo.view().get_remote_bookmark(symbol).target;
1816    target
1817        .is_present()
1818        .then(|| target.added_ids().cloned().collect())
1819}
1820
1821fn all_formatted_bookmark_symbols(
1822    repo: &dyn Repo,
1823    include_synced_remotes: bool,
1824) -> impl Iterator<Item = String> + use<'_> {
1825    let view = repo.view();
1826    view.bookmarks().flat_map(move |(name, bookmark_target)| {
1827        let local_target = bookmark_target.local_target;
1828        let local_symbol = local_target
1829            .is_present()
1830            .then(|| format_symbol(name.as_str()));
1831        let remote_symbols = bookmark_target
1832            .remote_refs
1833            .into_iter()
1834            .filter(move |&(_, remote_ref)| {
1835                include_synced_remotes
1836                    || !remote_ref.is_tracked()
1837                    || remote_ref.target != *local_target
1838            })
1839            .map(move |(remote, _)| format_remote_symbol(name.as_str(), remote.as_str()));
1840        local_symbol.into_iter().chain(remote_symbols)
1841    })
1842}
1843
1844fn make_no_such_symbol_error(repo: &dyn Repo, name: String) -> RevsetResolutionError {
1845    // TODO: include tags?
1846    let bookmark_names = all_formatted_bookmark_symbols(repo, name.contains('@'));
1847    let candidates = collect_similar(&name, bookmark_names);
1848    RevsetResolutionError::NoSuchRevision { name, candidates }
1849}
1850
1851pub trait SymbolResolver {
1852    /// Looks up `symbol` in the given `repo`.
1853    fn resolve_symbol(
1854        &self,
1855        repo: &dyn Repo,
1856        symbol: &str,
1857    ) -> Result<Vec<CommitId>, RevsetResolutionError>;
1858}
1859
1860/// Fails on any attempt to resolve a symbol.
1861pub struct FailingSymbolResolver;
1862
1863impl SymbolResolver for FailingSymbolResolver {
1864    fn resolve_symbol(
1865        &self,
1866        _repo: &dyn Repo,
1867        symbol: &str,
1868    ) -> Result<Vec<CommitId>, RevsetResolutionError> {
1869        Err(RevsetResolutionError::NoSuchRevision {
1870            name: format!(
1871                "Won't resolve symbol {symbol:?}. When creating revsets programmatically, avoid \
1872                 using RevsetExpression::symbol(); use RevsetExpression::commits() instead."
1873            ),
1874            candidates: Default::default(),
1875        })
1876    }
1877}
1878
1879/// A symbol resolver for a specific namespace of labels.
1880///
1881/// Returns None if it cannot handle the symbol.
1882pub trait PartialSymbolResolver {
1883    fn resolve_symbol(
1884        &self,
1885        repo: &dyn Repo,
1886        symbol: &str,
1887    ) -> Result<Option<Vec<CommitId>>, RevsetResolutionError>;
1888}
1889
1890struct TagResolver;
1891
1892impl PartialSymbolResolver for TagResolver {
1893    fn resolve_symbol(
1894        &self,
1895        repo: &dyn Repo,
1896        symbol: &str,
1897    ) -> Result<Option<Vec<CommitId>>, RevsetResolutionError> {
1898        let target = repo.view().get_tag(symbol.as_ref());
1899        Ok(target
1900            .is_present()
1901            .then(|| target.added_ids().cloned().collect()))
1902    }
1903}
1904
1905struct BookmarkResolver;
1906
1907impl PartialSymbolResolver for BookmarkResolver {
1908    fn resolve_symbol(
1909        &self,
1910        repo: &dyn Repo,
1911        symbol: &str,
1912    ) -> Result<Option<Vec<CommitId>>, RevsetResolutionError> {
1913        let target = repo.view().get_local_bookmark(symbol.as_ref());
1914        Ok(target
1915            .is_present()
1916            .then(|| target.added_ids().cloned().collect()))
1917    }
1918}
1919
1920struct GitRefResolver;
1921
1922impl PartialSymbolResolver for GitRefResolver {
1923    fn resolve_symbol(
1924        &self,
1925        repo: &dyn Repo,
1926        symbol: &str,
1927    ) -> Result<Option<Vec<CommitId>>, RevsetResolutionError> {
1928        let view = repo.view();
1929        for git_ref_prefix in &["", "refs/"] {
1930            let target = view.get_git_ref([git_ref_prefix, symbol].concat().as_ref());
1931            if target.is_present() {
1932                return Ok(Some(target.added_ids().cloned().collect()));
1933            }
1934        }
1935
1936        Ok(None)
1937    }
1938}
1939
1940const DEFAULT_RESOLVERS: &[&'static dyn PartialSymbolResolver] =
1941    &[&TagResolver, &BookmarkResolver, &GitRefResolver];
1942
1943struct CommitPrefixResolver<'a> {
1944    context_repo: &'a dyn Repo,
1945    context: Option<&'a IdPrefixContext>,
1946}
1947
1948impl PartialSymbolResolver for CommitPrefixResolver<'_> {
1949    fn resolve_symbol(
1950        &self,
1951        repo: &dyn Repo,
1952        symbol: &str,
1953    ) -> Result<Option<Vec<CommitId>>, RevsetResolutionError> {
1954        if let Some(prefix) = HexPrefix::new(symbol) {
1955            let index = self
1956                .context
1957                .map(|ctx| ctx.populate(self.context_repo))
1958                .transpose()
1959                .map_err(|err| RevsetResolutionError::Other(err.into()))?
1960                .unwrap_or(IdPrefixIndex::empty());
1961            match index.resolve_commit_prefix(repo, &prefix) {
1962                PrefixResolution::AmbiguousMatch => Err(
1963                    RevsetResolutionError::AmbiguousCommitIdPrefix(symbol.to_owned()),
1964                ),
1965                PrefixResolution::SingleMatch(id) => Ok(Some(vec![id])),
1966                PrefixResolution::NoMatch => Ok(None),
1967            }
1968        } else {
1969            Ok(None)
1970        }
1971    }
1972}
1973
1974struct ChangePrefixResolver<'a> {
1975    context_repo: &'a dyn Repo,
1976    context: Option<&'a IdPrefixContext>,
1977}
1978
1979impl PartialSymbolResolver for ChangePrefixResolver<'_> {
1980    fn resolve_symbol(
1981        &self,
1982        repo: &dyn Repo,
1983        symbol: &str,
1984    ) -> Result<Option<Vec<CommitId>>, RevsetResolutionError> {
1985        if let Some(prefix) = to_forward_hex(symbol).as_deref().and_then(HexPrefix::new) {
1986            let index = self
1987                .context
1988                .map(|ctx| ctx.populate(self.context_repo))
1989                .transpose()
1990                .map_err(|err| RevsetResolutionError::Other(err.into()))?
1991                .unwrap_or(IdPrefixIndex::empty());
1992            match index.resolve_change_prefix(repo, &prefix) {
1993                PrefixResolution::AmbiguousMatch => Err(
1994                    RevsetResolutionError::AmbiguousChangeIdPrefix(symbol.to_owned()),
1995                ),
1996                PrefixResolution::SingleMatch(ids) => Ok(Some(ids)),
1997                PrefixResolution::NoMatch => Ok(None),
1998            }
1999        } else {
2000            Ok(None)
2001        }
2002    }
2003}
2004
2005/// An extension of the DefaultSymbolResolver.
2006///
2007/// Each PartialSymbolResolver will be invoked in order, its result used if one
2008/// is provided. Native resolvers are always invoked first. In the future, we
2009/// may provide a way for extensions to override native resolvers like tags and
2010/// bookmarks.
2011pub trait SymbolResolverExtension {
2012    /// PartialSymbolResolvers can initialize some global data by using the
2013    /// `context_repo`, but the `context_repo` may point to a different
2014    /// operation from the `repo` passed into `resolve_symbol()`. For
2015    /// resolution, the latter `repo` should be used.
2016    fn new_resolvers<'a>(
2017        &self,
2018        context_repo: &'a dyn Repo,
2019    ) -> Vec<Box<dyn PartialSymbolResolver + 'a>>;
2020}
2021
2022/// Resolves bookmarks, remote bookmarks, tags, git refs, and full and
2023/// abbreviated commit and change ids.
2024pub struct DefaultSymbolResolver<'a> {
2025    commit_id_resolver: CommitPrefixResolver<'a>,
2026    change_id_resolver: ChangePrefixResolver<'a>,
2027    extensions: Vec<Box<dyn PartialSymbolResolver + 'a>>,
2028}
2029
2030impl<'a> DefaultSymbolResolver<'a> {
2031    /// Creates new symbol resolver that will first disambiguate short ID
2032    /// prefixes within the given `context_repo` if configured.
2033    pub fn new(
2034        context_repo: &'a dyn Repo,
2035        extensions: &[impl AsRef<dyn SymbolResolverExtension>],
2036    ) -> Self {
2037        DefaultSymbolResolver {
2038            commit_id_resolver: CommitPrefixResolver {
2039                context_repo,
2040                context: None,
2041            },
2042            change_id_resolver: ChangePrefixResolver {
2043                context_repo,
2044                context: None,
2045            },
2046            extensions: extensions
2047                .iter()
2048                .flat_map(|ext| ext.as_ref().new_resolvers(context_repo))
2049                .collect(),
2050        }
2051    }
2052
2053    pub fn with_id_prefix_context(mut self, id_prefix_context: &'a IdPrefixContext) -> Self {
2054        self.commit_id_resolver.context = Some(id_prefix_context);
2055        self.change_id_resolver.context = Some(id_prefix_context);
2056        self
2057    }
2058
2059    fn partial_resolvers(&self) -> impl Iterator<Item = &(dyn PartialSymbolResolver + 'a)> {
2060        let prefix_resolvers: [&dyn PartialSymbolResolver; 2] =
2061            [&self.commit_id_resolver, &self.change_id_resolver];
2062        itertools::chain!(
2063            DEFAULT_RESOLVERS.iter().copied(),
2064            prefix_resolvers,
2065            self.extensions.iter().map(|e| e.as_ref())
2066        )
2067    }
2068}
2069
2070impl SymbolResolver for DefaultSymbolResolver<'_> {
2071    fn resolve_symbol(
2072        &self,
2073        repo: &dyn Repo,
2074        symbol: &str,
2075    ) -> Result<Vec<CommitId>, RevsetResolutionError> {
2076        if symbol.is_empty() {
2077            return Err(RevsetResolutionError::EmptyString);
2078        }
2079
2080        for partial_resolver in self.partial_resolvers() {
2081            if let Some(ids) = partial_resolver.resolve_symbol(repo, symbol)? {
2082                return Ok(ids);
2083            }
2084        }
2085
2086        Err(make_no_such_symbol_error(repo, format_symbol(symbol)))
2087    }
2088}
2089
2090fn resolve_commit_ref(
2091    repo: &dyn Repo,
2092    commit_ref: &RevsetCommitRef,
2093    symbol_resolver: &dyn SymbolResolver,
2094) -> Result<Vec<CommitId>, RevsetResolutionError> {
2095    match commit_ref {
2096        RevsetCommitRef::Symbol(symbol) => symbol_resolver.resolve_symbol(repo, symbol),
2097        RevsetCommitRef::RemoteSymbol(symbol) => resolve_remote_bookmark(repo, symbol.as_ref())
2098            .ok_or_else(|| make_no_such_symbol_error(repo, symbol.to_string())),
2099        RevsetCommitRef::WorkingCopy(name) => {
2100            if let Some(commit_id) = repo.view().get_wc_commit_id(name) {
2101                Ok(vec![commit_id.clone()])
2102            } else {
2103                Err(RevsetResolutionError::WorkspaceMissingWorkingCopy { name: name.clone() })
2104            }
2105        }
2106        RevsetCommitRef::WorkingCopies => {
2107            let wc_commits = repo.view().wc_commit_ids().values().cloned().collect_vec();
2108            Ok(wc_commits)
2109        }
2110        RevsetCommitRef::Bookmarks(pattern) => {
2111            let commit_ids = repo
2112                .view()
2113                .local_bookmarks_matching(pattern)
2114                .flat_map(|(_, target)| target.added_ids())
2115                .cloned()
2116                .collect();
2117            Ok(commit_ids)
2118        }
2119        RevsetCommitRef::RemoteBookmarks {
2120            bookmark_pattern,
2121            remote_pattern,
2122            remote_ref_state,
2123        } => {
2124            // TODO: should we allow to select @git bookmarks explicitly?
2125            let commit_ids = repo
2126                .view()
2127                .remote_bookmarks_matching(bookmark_pattern, remote_pattern)
2128                .filter(|(_, remote_ref)| {
2129                    remote_ref_state.is_none_or(|state| remote_ref.state == state)
2130                })
2131                .filter(|&(symbol, _)| !crate::git::is_special_git_remote(symbol.remote))
2132                .flat_map(|(_, remote_ref)| remote_ref.target.added_ids())
2133                .cloned()
2134                .collect();
2135            Ok(commit_ids)
2136        }
2137        RevsetCommitRef::Tags(pattern) => {
2138            let commit_ids = repo
2139                .view()
2140                .tags_matching(pattern)
2141                .flat_map(|(_, target)| target.added_ids())
2142                .cloned()
2143                .collect();
2144            Ok(commit_ids)
2145        }
2146        RevsetCommitRef::GitRefs => {
2147            let mut commit_ids = vec![];
2148            for ref_target in repo.view().git_refs().values() {
2149                commit_ids.extend(ref_target.added_ids().cloned());
2150            }
2151            Ok(commit_ids)
2152        }
2153        RevsetCommitRef::GitHead => Ok(repo.view().git_head().added_ids().cloned().collect()),
2154    }
2155}
2156
2157/// Resolves symbols and commit refs recursively.
2158struct ExpressionSymbolResolver<'a> {
2159    base_repo: &'a dyn Repo,
2160    repo_stack: Vec<Arc<ReadonlyRepo>>,
2161    symbol_resolver: &'a dyn SymbolResolver,
2162}
2163
2164impl<'a> ExpressionSymbolResolver<'a> {
2165    fn new(base_repo: &'a dyn Repo, symbol_resolver: &'a dyn SymbolResolver) -> Self {
2166        ExpressionSymbolResolver {
2167            base_repo,
2168            repo_stack: vec![],
2169            symbol_resolver,
2170        }
2171    }
2172
2173    fn repo(&self) -> &dyn Repo {
2174        self.repo_stack
2175            .last()
2176            .map_or(self.base_repo, |repo| repo.as_ref())
2177    }
2178}
2179
2180impl ExpressionStateFolder<UserExpressionState, ResolvedExpressionState>
2181    for ExpressionSymbolResolver<'_>
2182{
2183    type Error = RevsetResolutionError;
2184
2185    fn fold_expression(
2186        &mut self,
2187        expression: &UserRevsetExpression,
2188    ) -> Result<Rc<ResolvedRevsetExpression>, Self::Error> {
2189        match expression {
2190            // 'present(x)' opens new symbol resolution scope to map error to 'none()'
2191            RevsetExpression::Present(candidates) => {
2192                self.fold_expression(candidates).or_else(|err| match err {
2193                    RevsetResolutionError::NoSuchRevision { .. }
2194                    | RevsetResolutionError::WorkspaceMissingWorkingCopy { .. } => {
2195                        Ok(RevsetExpression::none())
2196                    }
2197                    RevsetResolutionError::EmptyString
2198                    | RevsetResolutionError::AmbiguousCommitIdPrefix(_)
2199                    | RevsetResolutionError::AmbiguousChangeIdPrefix(_)
2200                    | RevsetResolutionError::StoreError(_)
2201                    | RevsetResolutionError::Other(_) => Err(err),
2202                })
2203            }
2204            _ => fold_child_expression_state(self, expression),
2205        }
2206    }
2207
2208    fn fold_commit_ref(
2209        &mut self,
2210        commit_ref: &RevsetCommitRef,
2211    ) -> Result<Rc<ResolvedRevsetExpression>, Self::Error> {
2212        let commit_ids = resolve_commit_ref(self.repo(), commit_ref, self.symbol_resolver)?;
2213        Ok(RevsetExpression::commits(commit_ids))
2214    }
2215
2216    fn fold_at_operation(
2217        &mut self,
2218        operation: &String,
2219        candidates: &UserRevsetExpression,
2220    ) -> Result<Rc<ResolvedRevsetExpression>, Self::Error> {
2221        let repo = reload_repo_at_operation(self.repo(), operation)?;
2222        self.repo_stack.push(repo);
2223        let candidates = self.fold_expression(candidates)?;
2224        let visible_heads = self.repo().view().heads().iter().cloned().collect();
2225        self.repo_stack.pop();
2226        Ok(Rc::new(RevsetExpression::WithinVisibility {
2227            candidates,
2228            visible_heads,
2229        }))
2230    }
2231}
2232
2233fn resolve_symbols(
2234    repo: &dyn Repo,
2235    expression: &UserRevsetExpression,
2236    symbol_resolver: &dyn SymbolResolver,
2237) -> Result<Rc<ResolvedRevsetExpression>, RevsetResolutionError> {
2238    let mut resolver = ExpressionSymbolResolver::new(repo, symbol_resolver);
2239    resolver.fold_expression(expression)
2240}
2241
2242/// Inserts implicit `all()` and `visible_heads()` nodes to the `expression`.
2243///
2244/// Symbols and commit refs in the `expression` should have been resolved.
2245///
2246/// This is a separate step because a symbol-resolved `expression` could be
2247/// transformed further to e.g. combine OR-ed `Commits(_)`, or to collect
2248/// commit ids to make `all()` include hidden-but-specified commits. The
2249/// return type `ResolvedExpression` is stricter than `RevsetExpression`,
2250/// and isn't designed for such transformation.
2251fn resolve_visibility(
2252    repo: &dyn Repo,
2253    expression: &ResolvedRevsetExpression,
2254) -> ResolvedExpression {
2255    let context = VisibilityResolutionContext {
2256        visible_heads: &repo.view().heads().iter().cloned().collect_vec(),
2257        root: repo.store().root_commit_id(),
2258    };
2259    context.resolve(expression)
2260}
2261
2262#[derive(Clone, Debug)]
2263struct VisibilityResolutionContext<'a> {
2264    visible_heads: &'a [CommitId],
2265    root: &'a CommitId,
2266}
2267
2268impl VisibilityResolutionContext<'_> {
2269    /// Resolves expression tree as set.
2270    fn resolve(&self, expression: &ResolvedRevsetExpression) -> ResolvedExpression {
2271        match expression {
2272            RevsetExpression::None => ResolvedExpression::Commits(vec![]),
2273            RevsetExpression::All => self.resolve_all(),
2274            RevsetExpression::VisibleHeads => self.resolve_visible_heads(),
2275            RevsetExpression::Root => self.resolve_root(),
2276            RevsetExpression::Commits(commit_ids) => {
2277                ResolvedExpression::Commits(commit_ids.clone())
2278            }
2279            RevsetExpression::CommitRef(commit_ref) => match *commit_ref {},
2280            RevsetExpression::Ancestors { heads, generation } => ResolvedExpression::Ancestors {
2281                heads: self.resolve(heads).into(),
2282                generation: generation.clone(),
2283            },
2284            RevsetExpression::Descendants { roots, generation } => ResolvedExpression::DagRange {
2285                roots: self.resolve(roots).into(),
2286                heads: self.resolve_visible_heads().into(),
2287                generation_from_roots: generation.clone(),
2288            },
2289            RevsetExpression::Range {
2290                roots,
2291                heads,
2292                generation,
2293            } => ResolvedExpression::Range {
2294                roots: self.resolve(roots).into(),
2295                heads: self.resolve(heads).into(),
2296                generation: generation.clone(),
2297            },
2298            RevsetExpression::DagRange { roots, heads } => ResolvedExpression::DagRange {
2299                roots: self.resolve(roots).into(),
2300                heads: self.resolve(heads).into(),
2301                generation_from_roots: GENERATION_RANGE_FULL,
2302            },
2303            RevsetExpression::Reachable { sources, domain } => ResolvedExpression::Reachable {
2304                sources: self.resolve(sources).into(),
2305                domain: self.resolve(domain).into(),
2306            },
2307            RevsetExpression::Heads(candidates) => {
2308                ResolvedExpression::Heads(self.resolve(candidates).into())
2309            }
2310            RevsetExpression::Roots(candidates) => {
2311                ResolvedExpression::Roots(self.resolve(candidates).into())
2312            }
2313            RevsetExpression::ForkPoint(expression) => {
2314                ResolvedExpression::ForkPoint(self.resolve(expression).into())
2315            }
2316            RevsetExpression::Latest { candidates, count } => ResolvedExpression::Latest {
2317                candidates: self.resolve(candidates).into(),
2318                count: *count,
2319            },
2320            RevsetExpression::Filter(_) | RevsetExpression::AsFilter(_) => {
2321                // Top-level filter without intersection: e.g. "~author(_)" is represented as
2322                // `AsFilter(NotIn(Filter(Author(_))))`.
2323                ResolvedExpression::FilterWithin {
2324                    candidates: self.resolve_all().into(),
2325                    predicate: self.resolve_predicate(expression),
2326                }
2327            }
2328            RevsetExpression::AtOperation { operation, .. } => match *operation {},
2329            RevsetExpression::WithinVisibility {
2330                candidates,
2331                visible_heads,
2332            } => {
2333                let context = VisibilityResolutionContext {
2334                    visible_heads,
2335                    root: self.root,
2336                };
2337                context.resolve(candidates)
2338            }
2339            RevsetExpression::Coalesce(expression1, expression2) => ResolvedExpression::Coalesce(
2340                self.resolve(expression1).into(),
2341                self.resolve(expression2).into(),
2342            ),
2343            // present(x) is noop if x doesn't contain any commit refs.
2344            RevsetExpression::Present(candidates) => self.resolve(candidates),
2345            RevsetExpression::NotIn(complement) => ResolvedExpression::Difference(
2346                self.resolve_all().into(),
2347                self.resolve(complement).into(),
2348            ),
2349            RevsetExpression::Union(expression1, expression2) => ResolvedExpression::Union(
2350                self.resolve(expression1).into(),
2351                self.resolve(expression2).into(),
2352            ),
2353            RevsetExpression::Intersection(expression1, expression2) => {
2354                match expression2.as_ref() {
2355                    RevsetExpression::Filter(_) | RevsetExpression::AsFilter(_) => {
2356                        ResolvedExpression::FilterWithin {
2357                            candidates: self.resolve(expression1).into(),
2358                            predicate: self.resolve_predicate(expression2),
2359                        }
2360                    }
2361                    _ => ResolvedExpression::Intersection(
2362                        self.resolve(expression1).into(),
2363                        self.resolve(expression2).into(),
2364                    ),
2365                }
2366            }
2367            RevsetExpression::Difference(expression1, expression2) => {
2368                ResolvedExpression::Difference(
2369                    self.resolve(expression1).into(),
2370                    self.resolve(expression2).into(),
2371                )
2372            }
2373        }
2374    }
2375
2376    fn resolve_all(&self) -> ResolvedExpression {
2377        // Since `all()` does not include hidden commits, some of the logical
2378        // transformation rules may subtly change the evaluated set. For example,
2379        // `all() & x` is not `x` if `x` is hidden. This wouldn't matter in practice,
2380        // but if it does, the heads set could be extended to include the commits
2381        // (and `remote_bookmarks()`) specified in the revset expression. Alternatively,
2382        // some optimization rules could be removed, but that means `author(_) & x`
2383        // would have to test `::visible_heads() & x`.
2384        ResolvedExpression::Ancestors {
2385            heads: self.resolve_visible_heads().into(),
2386            generation: GENERATION_RANGE_FULL,
2387        }
2388    }
2389
2390    fn resolve_visible_heads(&self) -> ResolvedExpression {
2391        ResolvedExpression::Commits(self.visible_heads.to_owned())
2392    }
2393
2394    fn resolve_root(&self) -> ResolvedExpression {
2395        ResolvedExpression::Commits(vec![self.root.to_owned()])
2396    }
2397
2398    /// Resolves expression tree as filter predicate.
2399    ///
2400    /// For filter expression, this never inserts a hidden `all()` since a
2401    /// filter predicate doesn't need to produce revisions to walk.
2402    fn resolve_predicate(
2403        &self,
2404        expression: &ResolvedRevsetExpression,
2405    ) -> ResolvedPredicateExpression {
2406        match expression {
2407            RevsetExpression::None
2408            | RevsetExpression::All
2409            | RevsetExpression::VisibleHeads
2410            | RevsetExpression::Root
2411            | RevsetExpression::Commits(_)
2412            | RevsetExpression::CommitRef(_)
2413            | RevsetExpression::Ancestors { .. }
2414            | RevsetExpression::Descendants { .. }
2415            | RevsetExpression::Range { .. }
2416            | RevsetExpression::DagRange { .. }
2417            | RevsetExpression::Reachable { .. }
2418            | RevsetExpression::Heads(_)
2419            | RevsetExpression::Roots(_)
2420            | RevsetExpression::ForkPoint(_)
2421            | RevsetExpression::Latest { .. } => {
2422                ResolvedPredicateExpression::Set(self.resolve(expression).into())
2423            }
2424            RevsetExpression::Filter(predicate) => {
2425                ResolvedPredicateExpression::Filter(predicate.clone())
2426            }
2427            RevsetExpression::AsFilter(candidates) => self.resolve_predicate(candidates),
2428            RevsetExpression::AtOperation { operation, .. } => match *operation {},
2429            // Filters should be intersected with all() within the at-op repo.
2430            RevsetExpression::WithinVisibility { .. } => {
2431                ResolvedPredicateExpression::Set(self.resolve(expression).into())
2432            }
2433            RevsetExpression::Coalesce(_, _) => {
2434                ResolvedPredicateExpression::Set(self.resolve(expression).into())
2435            }
2436            // present(x) is noop if x doesn't contain any commit refs.
2437            RevsetExpression::Present(candidates) => self.resolve_predicate(candidates),
2438            RevsetExpression::NotIn(complement) => {
2439                ResolvedPredicateExpression::NotIn(self.resolve_predicate(complement).into())
2440            }
2441            RevsetExpression::Union(expression1, expression2) => {
2442                let predicate1 = self.resolve_predicate(expression1);
2443                let predicate2 = self.resolve_predicate(expression2);
2444                ResolvedPredicateExpression::Union(predicate1.into(), predicate2.into())
2445            }
2446            // Intersection of filters should have been substituted by optimize().
2447            // If it weren't, just fall back to the set evaluation path.
2448            RevsetExpression::Intersection(..) | RevsetExpression::Difference(..) => {
2449                ResolvedPredicateExpression::Set(self.resolve(expression).into())
2450            }
2451        }
2452    }
2453}
2454
2455pub trait Revset: fmt::Debug {
2456    /// Iterate in topological order with children before parents.
2457    fn iter<'a>(&self) -> Box<dyn Iterator<Item = Result<CommitId, RevsetEvaluationError>> + 'a>
2458    where
2459        Self: 'a;
2460
2461    /// Iterates commit/change id pairs in topological order.
2462    fn commit_change_ids<'a>(
2463        &self,
2464    ) -> Box<dyn Iterator<Item = Result<(CommitId, ChangeId), RevsetEvaluationError>> + 'a>
2465    where
2466        Self: 'a;
2467
2468    fn iter_graph<'a>(
2469        &self,
2470    ) -> Box<dyn Iterator<Item = Result<GraphNode<CommitId>, RevsetEvaluationError>> + 'a>
2471    where
2472        Self: 'a;
2473
2474    /// Returns true if iterator will emit no commit nor error.
2475    fn is_empty(&self) -> bool;
2476
2477    /// Inclusive lower bound and, optionally, inclusive upper bound of how many
2478    /// commits are in the revset. The implementation can use its discretion as
2479    /// to how much effort should be put into the estimation, and how accurate
2480    /// the resulting estimate should be.
2481    fn count_estimate(&self) -> Result<(usize, Option<usize>), RevsetEvaluationError>;
2482
2483    /// Returns a closure that checks if a commit is contained within the
2484    /// revset.
2485    ///
2486    /// The implementation may construct and maintain any necessary internal
2487    /// context to optimize the performance of the check.
2488    fn containing_fn<'a>(&self) -> Box<RevsetContainingFn<'a>>
2489    where
2490        Self: 'a;
2491}
2492
2493/// Function that checks if a commit is contained within the revset.
2494pub type RevsetContainingFn<'a> = dyn Fn(&CommitId) -> Result<bool, RevsetEvaluationError> + 'a;
2495
2496pub trait RevsetIteratorExt<I> {
2497    fn commits(self, store: &Arc<Store>) -> RevsetCommitIterator<I>;
2498}
2499
2500impl<I: Iterator<Item = Result<CommitId, RevsetEvaluationError>>> RevsetIteratorExt<I> for I {
2501    fn commits(self, store: &Arc<Store>) -> RevsetCommitIterator<I> {
2502        RevsetCommitIterator {
2503            iter: self,
2504            store: store.clone(),
2505        }
2506    }
2507}
2508
2509pub struct RevsetCommitIterator<I> {
2510    store: Arc<Store>,
2511    iter: I,
2512}
2513
2514impl<I: Iterator<Item = Result<CommitId, RevsetEvaluationError>>> Iterator
2515    for RevsetCommitIterator<I>
2516{
2517    type Item = Result<Commit, RevsetEvaluationError>;
2518
2519    fn next(&mut self) -> Option<Self::Item> {
2520        self.iter.next().map(|commit_id| {
2521            let commit_id = commit_id?;
2522            self.store
2523                .get_commit(&commit_id)
2524                .map_err(RevsetEvaluationError::StoreError)
2525        })
2526    }
2527}
2528
2529/// A set of extensions for revset evaluation.
2530pub struct RevsetExtensions {
2531    symbol_resolvers: Vec<Box<dyn SymbolResolverExtension>>,
2532    function_map: HashMap<&'static str, RevsetFunction>,
2533}
2534
2535impl Default for RevsetExtensions {
2536    fn default() -> Self {
2537        Self::new()
2538    }
2539}
2540
2541impl RevsetExtensions {
2542    pub fn new() -> Self {
2543        Self {
2544            symbol_resolvers: vec![],
2545            function_map: BUILTIN_FUNCTION_MAP.clone(),
2546        }
2547    }
2548
2549    pub fn symbol_resolvers(&self) -> &[impl AsRef<dyn SymbolResolverExtension> + use<>] {
2550        &self.symbol_resolvers
2551    }
2552
2553    pub fn add_symbol_resolver(&mut self, symbol_resolver: Box<dyn SymbolResolverExtension>) {
2554        self.symbol_resolvers.push(symbol_resolver);
2555    }
2556
2557    pub fn add_custom_function(&mut self, name: &'static str, func: RevsetFunction) {
2558        match self.function_map.entry(name) {
2559            hash_map::Entry::Occupied(_) => {
2560                panic!("Conflict registering revset function '{name}'")
2561            }
2562            hash_map::Entry::Vacant(v) => v.insert(func),
2563        };
2564    }
2565}
2566
2567/// Information needed to parse revset expression.
2568#[derive(Clone)]
2569pub struct RevsetParseContext<'a> {
2570    pub aliases_map: &'a RevsetAliasesMap,
2571    pub local_variables: HashMap<&'a str, ExpressionNode<'a>>,
2572    pub user_email: &'a str,
2573    pub date_pattern_context: DatePatternContext,
2574    pub extensions: &'a RevsetExtensions,
2575    pub workspace: Option<RevsetWorkspaceContext<'a>>,
2576}
2577
2578impl<'a> RevsetParseContext<'a> {
2579    fn to_lowering_context(&self) -> LoweringContext<'a> {
2580        let RevsetParseContext {
2581            aliases_map: _,
2582            local_variables: _,
2583            user_email,
2584            date_pattern_context,
2585            extensions,
2586            workspace,
2587        } = *self;
2588        LoweringContext {
2589            user_email,
2590            date_pattern_context,
2591            extensions,
2592            workspace,
2593        }
2594    }
2595}
2596
2597/// Information needed to transform revset AST into `UserRevsetExpression`.
2598#[derive(Clone)]
2599pub struct LoweringContext<'a> {
2600    user_email: &'a str,
2601    date_pattern_context: DatePatternContext,
2602    extensions: &'a RevsetExtensions,
2603    workspace: Option<RevsetWorkspaceContext<'a>>,
2604}
2605
2606impl<'a> LoweringContext<'a> {
2607    pub fn user_email(&self) -> &'a str {
2608        self.user_email
2609    }
2610
2611    pub fn date_pattern_context(&self) -> &DatePatternContext {
2612        &self.date_pattern_context
2613    }
2614
2615    pub fn symbol_resolvers(&self) -> &'a [impl AsRef<dyn SymbolResolverExtension> + use<>] {
2616        self.extensions.symbol_resolvers()
2617    }
2618}
2619
2620/// Workspace information needed to parse revset expression.
2621#[derive(Clone, Copy, Debug)]
2622pub struct RevsetWorkspaceContext<'a> {
2623    pub path_converter: &'a RepoPathUiConverter,
2624    pub workspace_name: &'a WorkspaceName,
2625}
2626
2627/// Formats a string as symbol by quoting and escaping it if necessary.
2628///
2629/// Note that symbols may be substituted to user aliases. Use
2630/// [`format_string()`] to ensure that the provided string is resolved as a
2631/// tag/bookmark name, commit/change ID prefix, etc.
2632pub fn format_symbol(literal: &str) -> String {
2633    if revset_parser::is_identifier(literal) {
2634        literal.to_string()
2635    } else {
2636        format_string(literal)
2637    }
2638}
2639
2640/// Formats a string by quoting and escaping it.
2641pub fn format_string(literal: &str) -> String {
2642    format!(r#""{}""#, dsl_util::escape_string(literal))
2643}
2644
2645/// Formats a `name@remote` symbol, applies quoting and escaping if necessary.
2646pub fn format_remote_symbol(name: &str, remote: &str) -> String {
2647    let name = format_symbol(name);
2648    let remote = format_symbol(remote);
2649    format!("{name}@{remote}")
2650}
2651
2652#[cfg(test)]
2653mod tests {
2654    use std::path::PathBuf;
2655
2656    use assert_matches::assert_matches;
2657
2658    use super::*;
2659
2660    fn parse(revset_str: &str) -> Result<Rc<UserRevsetExpression>, RevsetParseError> {
2661        parse_with_aliases(revset_str, [] as [(&str, &str); 0])
2662    }
2663
2664    fn parse_with_workspace(
2665        revset_str: &str,
2666        workspace_name: &WorkspaceName,
2667    ) -> Result<Rc<UserRevsetExpression>, RevsetParseError> {
2668        parse_with_aliases_and_workspace(revset_str, [] as [(&str, &str); 0], workspace_name)
2669    }
2670
2671    fn parse_with_aliases(
2672        revset_str: &str,
2673        aliases: impl IntoIterator<Item = (impl AsRef<str>, impl Into<String>)>,
2674    ) -> Result<Rc<UserRevsetExpression>, RevsetParseError> {
2675        let mut aliases_map = RevsetAliasesMap::new();
2676        for (decl, defn) in aliases {
2677            aliases_map.insert(decl, defn).unwrap();
2678        }
2679        let context = RevsetParseContext {
2680            aliases_map: &aliases_map,
2681            local_variables: HashMap::new(),
2682            user_email: "test.user@example.com",
2683            date_pattern_context: chrono::Utc::now().fixed_offset().into(),
2684            extensions: &RevsetExtensions::default(),
2685            workspace: None,
2686        };
2687        super::parse(&mut RevsetDiagnostics::new(), revset_str, &context)
2688    }
2689
2690    fn parse_with_aliases_and_workspace(
2691        revset_str: &str,
2692        aliases: impl IntoIterator<Item = (impl AsRef<str>, impl Into<String>)>,
2693        workspace_name: &WorkspaceName,
2694    ) -> Result<Rc<UserRevsetExpression>, RevsetParseError> {
2695        // Set up pseudo context to resolve `workspace_name@` and `file(path)`
2696        let path_converter = RepoPathUiConverter::Fs {
2697            cwd: PathBuf::from("/"),
2698            base: PathBuf::from("/"),
2699        };
2700        let workspace_ctx = RevsetWorkspaceContext {
2701            path_converter: &path_converter,
2702            workspace_name,
2703        };
2704        let mut aliases_map = RevsetAliasesMap::new();
2705        for (decl, defn) in aliases {
2706            aliases_map.insert(decl, defn).unwrap();
2707        }
2708        let context = RevsetParseContext {
2709            aliases_map: &aliases_map,
2710            local_variables: HashMap::new(),
2711            user_email: "test.user@example.com",
2712            date_pattern_context: chrono::Utc::now().fixed_offset().into(),
2713            extensions: &RevsetExtensions::default(),
2714            workspace: Some(workspace_ctx),
2715        };
2716        super::parse(&mut RevsetDiagnostics::new(), revset_str, &context)
2717    }
2718
2719    fn parse_with_modifier(
2720        revset_str: &str,
2721    ) -> Result<(Rc<UserRevsetExpression>, Option<RevsetModifier>), RevsetParseError> {
2722        parse_with_aliases_and_modifier(revset_str, [] as [(&str, &str); 0])
2723    }
2724
2725    fn parse_with_aliases_and_modifier(
2726        revset_str: &str,
2727        aliases: impl IntoIterator<Item = (impl AsRef<str>, impl Into<String>)>,
2728    ) -> Result<(Rc<UserRevsetExpression>, Option<RevsetModifier>), RevsetParseError> {
2729        let mut aliases_map = RevsetAliasesMap::new();
2730        for (decl, defn) in aliases {
2731            aliases_map.insert(decl, defn).unwrap();
2732        }
2733        let context = RevsetParseContext {
2734            aliases_map: &aliases_map,
2735            local_variables: HashMap::new(),
2736            user_email: "test.user@example.com",
2737            date_pattern_context: chrono::Utc::now().fixed_offset().into(),
2738            extensions: &RevsetExtensions::default(),
2739            workspace: None,
2740        };
2741        super::parse_with_modifier(&mut RevsetDiagnostics::new(), revset_str, &context)
2742    }
2743
2744    fn insta_settings() -> insta::Settings {
2745        let mut settings = insta::Settings::clone_current();
2746        // Collapse short "Thing(_,)" repeatedly to save vertical space and make
2747        // the output more readable.
2748        for _ in 0..4 {
2749            settings.add_filter(
2750                r"(?x)
2751                \b([A-Z]\w*)\(\n
2752                    \s*(.{1,60}),\n
2753                \s*\)",
2754                "$1($2)",
2755            );
2756        }
2757        settings
2758    }
2759
2760    #[test]
2761    #[expect(clippy::redundant_clone)] // allow symbol.clone()
2762    fn test_revset_expression_building() {
2763        let settings = insta_settings();
2764        let _guard = settings.bind_to_scope();
2765        let current_wc = UserRevsetExpression::working_copy(WorkspaceName::DEFAULT.to_owned());
2766        let foo_symbol = UserRevsetExpression::symbol("foo".to_string());
2767        let bar_symbol = UserRevsetExpression::symbol("bar".to_string());
2768        let baz_symbol = UserRevsetExpression::symbol("baz".to_string());
2769
2770        insta::assert_debug_snapshot!(
2771            current_wc,
2772            @r#"CommitRef(WorkingCopy(WorkspaceNameBuf("default")))"#);
2773        insta::assert_debug_snapshot!(
2774            current_wc.heads(),
2775            @r#"Heads(CommitRef(WorkingCopy(WorkspaceNameBuf("default"))))"#);
2776        insta::assert_debug_snapshot!(
2777            current_wc.roots(),
2778            @r#"Roots(CommitRef(WorkingCopy(WorkspaceNameBuf("default"))))"#);
2779        insta::assert_debug_snapshot!(
2780            current_wc.parents(), @r#"
2781        Ancestors {
2782            heads: CommitRef(WorkingCopy(WorkspaceNameBuf("default"))),
2783            generation: 1..2,
2784        }
2785        "#);
2786        insta::assert_debug_snapshot!(
2787            current_wc.ancestors(), @r#"
2788        Ancestors {
2789            heads: CommitRef(WorkingCopy(WorkspaceNameBuf("default"))),
2790            generation: 0..18446744073709551615,
2791        }
2792        "#);
2793        insta::assert_debug_snapshot!(
2794            foo_symbol.children(), @r#"
2795        Descendants {
2796            roots: CommitRef(Symbol("foo")),
2797            generation: 1..2,
2798        }
2799        "#);
2800        insta::assert_debug_snapshot!(
2801            foo_symbol.descendants(), @r#"
2802        Descendants {
2803            roots: CommitRef(Symbol("foo")),
2804            generation: 0..18446744073709551615,
2805        }
2806        "#);
2807        insta::assert_debug_snapshot!(
2808            foo_symbol.dag_range_to(&current_wc), @r#"
2809        DagRange {
2810            roots: CommitRef(Symbol("foo")),
2811            heads: CommitRef(WorkingCopy(WorkspaceNameBuf("default"))),
2812        }
2813        "#);
2814        insta::assert_debug_snapshot!(
2815            foo_symbol.connected(), @r#"
2816        DagRange {
2817            roots: CommitRef(Symbol("foo")),
2818            heads: CommitRef(Symbol("foo")),
2819        }
2820        "#);
2821        insta::assert_debug_snapshot!(
2822            foo_symbol.range(&current_wc), @r#"
2823        Range {
2824            roots: CommitRef(Symbol("foo")),
2825            heads: CommitRef(WorkingCopy(WorkspaceNameBuf("default"))),
2826            generation: 0..18446744073709551615,
2827        }
2828        "#);
2829        insta::assert_debug_snapshot!(
2830            foo_symbol.negated(),
2831            @r#"NotIn(CommitRef(Symbol("foo")))"#);
2832        insta::assert_debug_snapshot!(
2833            foo_symbol.union(&current_wc), @r#"
2834        Union(
2835            CommitRef(Symbol("foo")),
2836            CommitRef(WorkingCopy(WorkspaceNameBuf("default"))),
2837        )
2838        "#);
2839        insta::assert_debug_snapshot!(
2840            UserRevsetExpression::union_all(&[]),
2841            @"None");
2842        insta::assert_debug_snapshot!(
2843            RevsetExpression::union_all(&[current_wc.clone()]),
2844            @r#"CommitRef(WorkingCopy(WorkspaceNameBuf("default")))"#);
2845        insta::assert_debug_snapshot!(
2846            RevsetExpression::union_all(&[current_wc.clone(), foo_symbol.clone()]),
2847            @r#"
2848        Union(
2849            CommitRef(WorkingCopy(WorkspaceNameBuf("default"))),
2850            CommitRef(Symbol("foo")),
2851        )
2852        "#);
2853        insta::assert_debug_snapshot!(
2854            RevsetExpression::union_all(&[
2855                current_wc.clone(),
2856                foo_symbol.clone(),
2857                bar_symbol.clone(),
2858            ]),
2859            @r#"
2860        Union(
2861            CommitRef(WorkingCopy(WorkspaceNameBuf("default"))),
2862            Union(
2863                CommitRef(Symbol("foo")),
2864                CommitRef(Symbol("bar")),
2865            ),
2866        )
2867        "#);
2868        insta::assert_debug_snapshot!(
2869            RevsetExpression::union_all(&[
2870                current_wc.clone(),
2871                foo_symbol.clone(),
2872                bar_symbol.clone(),
2873                baz_symbol.clone(),
2874            ]),
2875            @r#"
2876        Union(
2877            Union(
2878                CommitRef(WorkingCopy(WorkspaceNameBuf("default"))),
2879                CommitRef(Symbol("foo")),
2880            ),
2881            Union(
2882                CommitRef(Symbol("bar")),
2883                CommitRef(Symbol("baz")),
2884            ),
2885        )
2886        "#);
2887        insta::assert_debug_snapshot!(
2888            foo_symbol.intersection(&current_wc), @r#"
2889        Intersection(
2890            CommitRef(Symbol("foo")),
2891            CommitRef(WorkingCopy(WorkspaceNameBuf("default"))),
2892        )
2893        "#);
2894        insta::assert_debug_snapshot!(
2895            foo_symbol.minus(&current_wc), @r#"
2896        Difference(
2897            CommitRef(Symbol("foo")),
2898            CommitRef(WorkingCopy(WorkspaceNameBuf("default"))),
2899        )
2900        "#);
2901        insta::assert_debug_snapshot!(
2902            UserRevsetExpression::coalesce(&[]),
2903            @"None");
2904        insta::assert_debug_snapshot!(
2905            RevsetExpression::coalesce(&[current_wc.clone()]),
2906            @r#"CommitRef(WorkingCopy(WorkspaceNameBuf("default")))"#);
2907        insta::assert_debug_snapshot!(
2908            RevsetExpression::coalesce(&[current_wc.clone(), foo_symbol.clone()]),
2909            @r#"
2910        Coalesce(
2911            CommitRef(WorkingCopy(WorkspaceNameBuf("default"))),
2912            CommitRef(Symbol("foo")),
2913        )
2914        "#);
2915        insta::assert_debug_snapshot!(
2916            RevsetExpression::coalesce(&[
2917                current_wc.clone(),
2918                foo_symbol.clone(),
2919                bar_symbol.clone(),
2920            ]),
2921            @r#"
2922        Coalesce(
2923            CommitRef(WorkingCopy(WorkspaceNameBuf("default"))),
2924            Coalesce(
2925                CommitRef(Symbol("foo")),
2926                CommitRef(Symbol("bar")),
2927            ),
2928        )
2929        "#);
2930    }
2931
2932    #[test]
2933    fn test_parse_revset() {
2934        let settings = insta_settings();
2935        let _guard = settings.bind_to_scope();
2936        let main_workspace_name = WorkspaceNameBuf::from("main");
2937        let other_workspace_name = WorkspaceNameBuf::from("other");
2938
2939        // Parse "@" (the current working copy)
2940        insta::assert_debug_snapshot!(
2941            parse("@").unwrap_err().kind(),
2942            @"WorkingCopyWithoutWorkspace");
2943        insta::assert_debug_snapshot!(
2944            parse("main@").unwrap(),
2945            @r#"CommitRef(WorkingCopy(WorkspaceNameBuf("main")))"#);
2946        insta::assert_debug_snapshot!(
2947            parse_with_workspace("@", &main_workspace_name).unwrap(),
2948            @r#"CommitRef(WorkingCopy(WorkspaceNameBuf("main")))"#);
2949        insta::assert_debug_snapshot!(
2950            parse_with_workspace("main@", &other_workspace_name).unwrap(),
2951            @r#"CommitRef(WorkingCopy(WorkspaceNameBuf("main")))"#);
2952        // "@" in function argument must be quoted
2953        insta::assert_debug_snapshot!(
2954            parse("author_name(foo@)").unwrap_err().kind(),
2955            @r#"Expression("Expected expression of string pattern")"#);
2956        insta::assert_debug_snapshot!(
2957            parse(r#"author_name("foo@")"#).unwrap(),
2958            @r#"Filter(AuthorName(Substring("foo@")))"#);
2959        // Parse a single symbol
2960        insta::assert_debug_snapshot!(
2961            parse("foo").unwrap(),
2962            @r#"CommitRef(Symbol("foo"))"#);
2963        // Default arguments for *bookmarks() are all ""
2964        insta::assert_debug_snapshot!(
2965            parse("bookmarks()").unwrap(),
2966            @r#"CommitRef(Bookmarks(Substring("")))"#);
2967        // Default argument for tags() is ""
2968        insta::assert_debug_snapshot!(
2969            parse("tags()").unwrap(),
2970            @r#"CommitRef(Tags(Substring("")))"#);
2971        insta::assert_debug_snapshot!(parse("remote_bookmarks()").unwrap(), @r#"
2972        CommitRef(
2973            RemoteBookmarks {
2974                bookmark_pattern: Substring(""),
2975                remote_pattern: Substring(""),
2976                remote_ref_state: None,
2977            },
2978        )
2979        "#);
2980        insta::assert_debug_snapshot!(parse("tracked_remote_bookmarks()").unwrap(), @r#"
2981        CommitRef(
2982            RemoteBookmarks {
2983                bookmark_pattern: Substring(""),
2984                remote_pattern: Substring(""),
2985                remote_ref_state: Some(Tracked),
2986            },
2987        )
2988        "#);
2989        insta::assert_debug_snapshot!(parse("untracked_remote_bookmarks()").unwrap(), @r#"
2990        CommitRef(
2991            RemoteBookmarks {
2992                bookmark_pattern: Substring(""),
2993                remote_pattern: Substring(""),
2994                remote_ref_state: Some(New),
2995            },
2996        )
2997        "#);
2998        // Parse a quoted symbol
2999        insta::assert_debug_snapshot!(
3000            parse("'foo'").unwrap(),
3001            @r#"CommitRef(Symbol("foo"))"#);
3002        // Parse the "parents" operator
3003        insta::assert_debug_snapshot!(parse("foo-").unwrap(), @r#"
3004        Ancestors {
3005            heads: CommitRef(Symbol("foo")),
3006            generation: 1..2,
3007        }
3008        "#);
3009        // Parse the "children" operator
3010        insta::assert_debug_snapshot!(parse("foo+").unwrap(), @r#"
3011        Descendants {
3012            roots: CommitRef(Symbol("foo")),
3013            generation: 1..2,
3014        }
3015        "#);
3016        // Parse the "ancestors" operator
3017        insta::assert_debug_snapshot!(parse("::foo").unwrap(), @r#"
3018        Ancestors {
3019            heads: CommitRef(Symbol("foo")),
3020            generation: 0..18446744073709551615,
3021        }
3022        "#);
3023        // Parse the "descendants" operator
3024        insta::assert_debug_snapshot!(parse("foo::").unwrap(), @r#"
3025        Descendants {
3026            roots: CommitRef(Symbol("foo")),
3027            generation: 0..18446744073709551615,
3028        }
3029        "#);
3030        // Parse the "dag range" operator
3031        insta::assert_debug_snapshot!(parse("foo::bar").unwrap(), @r#"
3032        DagRange {
3033            roots: CommitRef(Symbol("foo")),
3034            heads: CommitRef(Symbol("bar")),
3035        }
3036        "#);
3037        // Parse the nullary "dag range" operator
3038        insta::assert_debug_snapshot!(parse("::").unwrap(), @"All");
3039        // Parse the "range" prefix operator
3040        insta::assert_debug_snapshot!(parse("..foo").unwrap(), @r#"
3041        Range {
3042            roots: Root,
3043            heads: CommitRef(Symbol("foo")),
3044            generation: 0..18446744073709551615,
3045        }
3046        "#);
3047        insta::assert_debug_snapshot!(parse("foo..").unwrap(), @r#"
3048        Range {
3049            roots: CommitRef(Symbol("foo")),
3050            heads: VisibleHeads,
3051            generation: 0..18446744073709551615,
3052        }
3053        "#);
3054        insta::assert_debug_snapshot!(parse("foo..bar").unwrap(), @r#"
3055        Range {
3056            roots: CommitRef(Symbol("foo")),
3057            heads: CommitRef(Symbol("bar")),
3058            generation: 0..18446744073709551615,
3059        }
3060        "#);
3061        // Parse the nullary "range" operator
3062        insta::assert_debug_snapshot!(parse("..").unwrap(), @r"
3063        Range {
3064            roots: Root,
3065            heads: VisibleHeads,
3066            generation: 0..18446744073709551615,
3067        }
3068        ");
3069        // Parse the "negate" operator
3070        insta::assert_debug_snapshot!(
3071            parse("~ foo").unwrap(),
3072            @r#"NotIn(CommitRef(Symbol("foo")))"#);
3073        // Parse the "intersection" operator
3074        insta::assert_debug_snapshot!(parse("foo & bar").unwrap(), @r#"
3075        Intersection(
3076            CommitRef(Symbol("foo")),
3077            CommitRef(Symbol("bar")),
3078        )
3079        "#);
3080        // Parse the "union" operator
3081        insta::assert_debug_snapshot!(parse("foo | bar").unwrap(), @r#"
3082        Union(
3083            CommitRef(Symbol("foo")),
3084            CommitRef(Symbol("bar")),
3085        )
3086        "#);
3087        // Parse the "difference" operator
3088        insta::assert_debug_snapshot!(parse("foo ~ bar").unwrap(), @r#"
3089        Difference(
3090            CommitRef(Symbol("foo")),
3091            CommitRef(Symbol("bar")),
3092        )
3093        "#);
3094    }
3095
3096    #[test]
3097    fn test_parse_revset_with_modifier() {
3098        let settings = insta_settings();
3099        let _guard = settings.bind_to_scope();
3100
3101        insta::assert_debug_snapshot!(
3102            parse_with_modifier("all:foo").unwrap(), @r#"
3103        (
3104            CommitRef(Symbol("foo")),
3105            Some(All),
3106        )
3107        "#);
3108
3109        // Top-level string pattern can't be parsed, which is an error anyway
3110        insta::assert_debug_snapshot!(
3111            parse_with_modifier(r#"exact:"foo""#).unwrap_err().kind(),
3112            @r#"NoSuchModifier("exact")"#);
3113    }
3114
3115    #[test]
3116    fn test_parse_string_pattern() {
3117        let settings = insta_settings();
3118        let _guard = settings.bind_to_scope();
3119
3120        insta::assert_debug_snapshot!(
3121            parse(r#"bookmarks("foo")"#).unwrap(),
3122            @r#"CommitRef(Bookmarks(Substring("foo")))"#);
3123        insta::assert_debug_snapshot!(
3124            parse(r#"bookmarks(exact:"foo")"#).unwrap(),
3125            @r#"CommitRef(Bookmarks(Exact("foo")))"#);
3126        insta::assert_debug_snapshot!(
3127            parse(r#"bookmarks(substring:"foo")"#).unwrap(),
3128            @r#"CommitRef(Bookmarks(Substring("foo")))"#);
3129        insta::assert_debug_snapshot!(
3130            parse(r#"bookmarks(bad:"foo")"#).unwrap_err().kind(),
3131            @r#"Expression("Invalid string pattern")"#);
3132        insta::assert_debug_snapshot!(
3133            parse(r#"bookmarks(exact::"foo")"#).unwrap_err().kind(),
3134            @r#"Expression("Expected expression of string pattern")"#);
3135        insta::assert_debug_snapshot!(
3136            parse(r#"bookmarks(exact:"foo"+)"#).unwrap_err().kind(),
3137            @r#"Expression("Expected expression of string pattern")"#);
3138
3139        insta::assert_debug_snapshot!(
3140            parse(r#"tags("foo")"#).unwrap(),
3141            @r#"CommitRef(Tags(Substring("foo")))"#);
3142        insta::assert_debug_snapshot!(
3143            parse(r#"tags(exact:"foo")"#).unwrap(),
3144            @r#"CommitRef(Tags(Exact("foo")))"#);
3145        insta::assert_debug_snapshot!(
3146            parse(r#"tags(substring:"foo")"#).unwrap(),
3147            @r#"CommitRef(Tags(Substring("foo")))"#);
3148        insta::assert_debug_snapshot!(
3149            parse(r#"tags(bad:"foo")"#).unwrap_err().kind(),
3150            @r#"Expression("Invalid string pattern")"#);
3151        insta::assert_debug_snapshot!(
3152            parse(r#"tags(exact::"foo")"#).unwrap_err().kind(),
3153            @r#"Expression("Expected expression of string pattern")"#);
3154        insta::assert_debug_snapshot!(
3155            parse(r#"tags(exact:"foo"+)"#).unwrap_err().kind(),
3156            @r#"Expression("Expected expression of string pattern")"#);
3157
3158        // String pattern isn't allowed at top level.
3159        assert_matches!(
3160            parse(r#"(exact:"foo")"#).unwrap_err().kind(),
3161            RevsetParseErrorKind::NotInfixOperator { .. }
3162        );
3163    }
3164
3165    #[test]
3166    fn test_parse_revset_function() {
3167        let settings = insta_settings();
3168        let _guard = settings.bind_to_scope();
3169
3170        insta::assert_debug_snapshot!(
3171            parse("parents(foo)").unwrap(), @r#"
3172        Ancestors {
3173            heads: CommitRef(Symbol("foo")),
3174            generation: 1..2,
3175        }
3176        "#);
3177        insta::assert_debug_snapshot!(
3178            parse("parents(\"foo\")").unwrap(), @r#"
3179        Ancestors {
3180            heads: CommitRef(Symbol("foo")),
3181            generation: 1..2,
3182        }
3183        "#);
3184        insta::assert_debug_snapshot!(
3185            parse("ancestors(parents(foo))").unwrap(), @r#"
3186        Ancestors {
3187            heads: Ancestors {
3188                heads: CommitRef(Symbol("foo")),
3189                generation: 1..2,
3190            },
3191            generation: 0..18446744073709551615,
3192        }
3193        "#);
3194        insta::assert_debug_snapshot!(
3195            parse("parents(foo,foo)").unwrap_err().kind(), @r#"
3196        InvalidFunctionArguments {
3197            name: "parents",
3198            message: "Expected 1 arguments",
3199        }
3200        "#);
3201        insta::assert_debug_snapshot!(
3202            parse("root()").unwrap(),
3203            @"Root");
3204        assert!(parse("root(a)").is_err());
3205        insta::assert_debug_snapshot!(
3206            parse(r#"description("")"#).unwrap(),
3207            @r#"Filter(Description(Substring("")))"#);
3208        insta::assert_debug_snapshot!(
3209            parse("description(foo)").unwrap(),
3210            @r#"Filter(Description(Substring("foo")))"#);
3211        insta::assert_debug_snapshot!(
3212            parse("description(visible_heads())").unwrap_err().kind(),
3213            @r#"Expression("Expected expression of string pattern")"#);
3214        insta::assert_debug_snapshot!(
3215            parse("description(\"(foo)\")").unwrap(),
3216            @r#"Filter(Description(Substring("(foo)")))"#);
3217        assert!(parse("mine(foo)").is_err());
3218        insta::assert_debug_snapshot!(
3219            parse_with_workspace("empty()", WorkspaceName::DEFAULT).unwrap(),
3220            @"NotIn(Filter(File(All)))");
3221        assert!(parse_with_workspace("empty(foo)", WorkspaceName::DEFAULT).is_err());
3222        assert!(parse_with_workspace("file()", WorkspaceName::DEFAULT).is_err());
3223        insta::assert_debug_snapshot!(
3224            parse_with_workspace("files(foo)", WorkspaceName::DEFAULT).unwrap(),
3225            @r#"Filter(File(Pattern(PrefixPath("foo"))))"#);
3226        insta::assert_debug_snapshot!(
3227            parse_with_workspace("files(all())", WorkspaceName::DEFAULT).unwrap(),
3228            @"Filter(File(All))");
3229        insta::assert_debug_snapshot!(
3230            parse_with_workspace(r#"files(file:"foo")"#, WorkspaceName::DEFAULT).unwrap(),
3231            @r#"Filter(File(Pattern(FilePath("foo"))))"#);
3232        insta::assert_debug_snapshot!(
3233            parse_with_workspace("files(foo|bar&baz)", WorkspaceName::DEFAULT).unwrap(), @r#"
3234        Filter(
3235            File(
3236                UnionAll(
3237                    [
3238                        Pattern(PrefixPath("foo")),
3239                        Intersection(
3240                            Pattern(PrefixPath("bar")),
3241                            Pattern(PrefixPath("baz")),
3242                        ),
3243                    ],
3244                ),
3245            ),
3246        )
3247        "#);
3248    }
3249
3250    #[test]
3251    fn test_parse_revset_author_committer_functions() {
3252        let settings = insta_settings();
3253        let _guard = settings.bind_to_scope();
3254
3255        insta::assert_debug_snapshot!(
3256            parse("author(foo)").unwrap(), @r#"
3257        Union(
3258            Filter(AuthorName(Substring("foo"))),
3259            Filter(AuthorEmail(Substring("foo"))),
3260        )
3261        "#);
3262        insta::assert_debug_snapshot!(
3263            parse("author_name(foo)").unwrap(),
3264            @r#"Filter(AuthorName(Substring("foo")))"#);
3265        insta::assert_debug_snapshot!(
3266            parse("author_email(foo)").unwrap(),
3267            @r#"Filter(AuthorEmail(Substring("foo")))"#);
3268
3269        insta::assert_debug_snapshot!(
3270            parse("committer(foo)").unwrap(), @r#"
3271        Union(
3272            Filter(CommitterName(Substring("foo"))),
3273            Filter(CommitterEmail(Substring("foo"))),
3274        )
3275        "#);
3276        insta::assert_debug_snapshot!(
3277            parse("committer_name(foo)").unwrap(),
3278            @r#"Filter(CommitterName(Substring("foo")))"#);
3279        insta::assert_debug_snapshot!(
3280            parse("committer_email(foo)").unwrap(),
3281            @r#"Filter(CommitterEmail(Substring("foo")))"#);
3282
3283        insta::assert_debug_snapshot!(
3284            parse("mine()").unwrap(),
3285            @r#"Filter(AuthorEmail(ExactI("test.user@example.com")))"#);
3286    }
3287
3288    #[test]
3289    fn test_parse_revset_keyword_arguments() {
3290        let settings = insta_settings();
3291        let _guard = settings.bind_to_scope();
3292
3293        insta::assert_debug_snapshot!(
3294            parse("remote_bookmarks(remote=foo)").unwrap(), @r#"
3295        CommitRef(
3296            RemoteBookmarks {
3297                bookmark_pattern: Substring(""),
3298                remote_pattern: Substring("foo"),
3299                remote_ref_state: None,
3300            },
3301        )
3302        "#);
3303        insta::assert_debug_snapshot!(
3304            parse("remote_bookmarks(foo, remote=bar)").unwrap(), @r#"
3305        CommitRef(
3306            RemoteBookmarks {
3307                bookmark_pattern: Substring("foo"),
3308                remote_pattern: Substring("bar"),
3309                remote_ref_state: None,
3310            },
3311        )
3312        "#);
3313        insta::assert_debug_snapshot!(
3314            parse("tracked_remote_bookmarks(foo, remote=bar)").unwrap(), @r#"
3315        CommitRef(
3316            RemoteBookmarks {
3317                bookmark_pattern: Substring("foo"),
3318                remote_pattern: Substring("bar"),
3319                remote_ref_state: Some(Tracked),
3320            },
3321        )
3322        "#);
3323        insta::assert_debug_snapshot!(
3324            parse("untracked_remote_bookmarks(foo, remote=bar)").unwrap(), @r#"
3325        CommitRef(
3326            RemoteBookmarks {
3327                bookmark_pattern: Substring("foo"),
3328                remote_pattern: Substring("bar"),
3329                remote_ref_state: Some(New),
3330            },
3331        )
3332        "#);
3333        insta::assert_debug_snapshot!(
3334            parse(r#"remote_bookmarks(remote=foo, bar)"#).unwrap_err().kind(),
3335            @r#"
3336        InvalidFunctionArguments {
3337            name: "remote_bookmarks",
3338            message: "Positional argument follows keyword argument",
3339        }
3340        "#);
3341        insta::assert_debug_snapshot!(
3342            parse(r#"remote_bookmarks("", foo, remote=bar)"#).unwrap_err().kind(),
3343            @r#"
3344        InvalidFunctionArguments {
3345            name: "remote_bookmarks",
3346            message: "Got multiple values for keyword \"remote\"",
3347        }
3348        "#);
3349        insta::assert_debug_snapshot!(
3350            parse(r#"remote_bookmarks(remote=bar, remote=bar)"#).unwrap_err().kind(),
3351            @r#"
3352        InvalidFunctionArguments {
3353            name: "remote_bookmarks",
3354            message: "Got multiple values for keyword \"remote\"",
3355        }
3356        "#);
3357        insta::assert_debug_snapshot!(
3358            parse(r#"remote_bookmarks(unknown=bar)"#).unwrap_err().kind(),
3359            @r#"
3360        InvalidFunctionArguments {
3361            name: "remote_bookmarks",
3362            message: "Unexpected keyword argument \"unknown\"",
3363        }
3364        "#);
3365    }
3366
3367    #[test]
3368    fn test_expand_symbol_alias() {
3369        let settings = insta_settings();
3370        let _guard = settings.bind_to_scope();
3371
3372        insta::assert_debug_snapshot!(
3373            parse_with_aliases("AB|c", [("AB", "a|b")]).unwrap(), @r#"
3374        Union(
3375            Union(
3376                CommitRef(Symbol("a")),
3377                CommitRef(Symbol("b")),
3378            ),
3379            CommitRef(Symbol("c")),
3380        )
3381        "#);
3382
3383        // Alias can be substituted to string literal.
3384        insta::assert_debug_snapshot!(
3385            parse_with_aliases_and_workspace("files(A)", [("A", "a")], WorkspaceName::DEFAULT)
3386                .unwrap(),
3387            @r#"Filter(File(Pattern(PrefixPath("a"))))"#);
3388
3389        // Alias can be substituted to string pattern.
3390        insta::assert_debug_snapshot!(
3391            parse_with_aliases("author_name(A)", [("A", "a")]).unwrap(),
3392            @r#"Filter(AuthorName(Substring("a")))"#);
3393        // However, parentheses are required because top-level x:y is parsed as
3394        // program modifier.
3395        insta::assert_debug_snapshot!(
3396            parse_with_aliases("author_name(A)", [("A", "(exact:a)")]).unwrap(),
3397            @r#"Filter(AuthorName(Exact("a")))"#);
3398
3399        // Sub-expression alias cannot be substituted to modifier expression.
3400        insta::assert_debug_snapshot!(
3401            parse_with_aliases_and_modifier("A-", [("A", "all:a")]).unwrap_err().kind(),
3402            @r#"InAliasExpansion("A")"#);
3403    }
3404
3405    #[test]
3406    fn test_expand_function_alias() {
3407        let settings = insta_settings();
3408        let _guard = settings.bind_to_scope();
3409
3410        // Pass string literal as parameter.
3411        insta::assert_debug_snapshot!(
3412            parse_with_aliases("F(a)", [("F(x)", "author_name(x)|committer_name(x)")]).unwrap(),
3413            @r#"
3414        Union(
3415            Filter(AuthorName(Substring("a"))),
3416            Filter(CommitterName(Substring("a"))),
3417        )
3418        "#);
3419    }
3420
3421    #[test]
3422    fn test_optimize_subtree() {
3423        let settings = insta_settings();
3424        let _guard = settings.bind_to_scope();
3425
3426        // Check that transform_expression_bottom_up() never rewrites enum variant
3427        // (e.g. Range -> DagRange) nor reorders arguments unintentionally.
3428
3429        insta::assert_debug_snapshot!(
3430            optimize(parse("parents(bookmarks() & all())").unwrap()), @r#"
3431        Ancestors {
3432            heads: CommitRef(Bookmarks(Substring(""))),
3433            generation: 1..2,
3434        }
3435        "#);
3436        insta::assert_debug_snapshot!(
3437            optimize(parse("children(bookmarks() & all())").unwrap()), @r#"
3438        Descendants {
3439            roots: CommitRef(Bookmarks(Substring(""))),
3440            generation: 1..2,
3441        }
3442        "#);
3443        insta::assert_debug_snapshot!(
3444            optimize(parse("ancestors(bookmarks() & all())").unwrap()), @r#"
3445        Ancestors {
3446            heads: CommitRef(Bookmarks(Substring(""))),
3447            generation: 0..18446744073709551615,
3448        }
3449        "#);
3450        insta::assert_debug_snapshot!(
3451            optimize(parse("descendants(bookmarks() & all())").unwrap()), @r#"
3452        Descendants {
3453            roots: CommitRef(Bookmarks(Substring(""))),
3454            generation: 0..18446744073709551615,
3455        }
3456        "#);
3457
3458        insta::assert_debug_snapshot!(
3459            optimize(parse("(bookmarks() & all())..(all() & tags())").unwrap()), @r#"
3460        Range {
3461            roots: CommitRef(Bookmarks(Substring(""))),
3462            heads: CommitRef(Tags(Substring(""))),
3463            generation: 0..18446744073709551615,
3464        }
3465        "#);
3466        insta::assert_debug_snapshot!(
3467            optimize(parse("(bookmarks() & all())::(all() & tags())").unwrap()), @r#"
3468        DagRange {
3469            roots: CommitRef(Bookmarks(Substring(""))),
3470            heads: CommitRef(Tags(Substring(""))),
3471        }
3472        "#);
3473
3474        insta::assert_debug_snapshot!(
3475            optimize(parse("heads(bookmarks() & all())").unwrap()),
3476            @r#"Heads(CommitRef(Bookmarks(Substring(""))))"#);
3477        insta::assert_debug_snapshot!(
3478            optimize(parse("roots(bookmarks() & all())").unwrap()),
3479            @r#"Roots(CommitRef(Bookmarks(Substring(""))))"#);
3480
3481        insta::assert_debug_snapshot!(
3482            optimize(parse("latest(bookmarks() & all(), 2)").unwrap()), @r#"
3483        Latest {
3484            candidates: CommitRef(Bookmarks(Substring(""))),
3485            count: 2,
3486        }
3487        "#);
3488
3489        insta::assert_debug_snapshot!(
3490            optimize(parse("present(foo ~ bar)").unwrap()), @r#"
3491        Present(
3492            Difference(
3493                CommitRef(Symbol("foo")),
3494                CommitRef(Symbol("bar")),
3495            ),
3496        )
3497        "#);
3498        insta::assert_debug_snapshot!(
3499            optimize(parse("present(bookmarks() & all())").unwrap()),
3500            @r#"Present(CommitRef(Bookmarks(Substring(""))))"#);
3501
3502        insta::assert_debug_snapshot!(
3503            optimize(parse("at_operation(@-, bookmarks() & all())").unwrap()), @r#"
3504        AtOperation {
3505            operation: "@-",
3506            candidates: CommitRef(Bookmarks(Substring(""))),
3507        }
3508        "#);
3509        insta::assert_debug_snapshot!(
3510            optimize(Rc::new(RevsetExpression::WithinVisibility {
3511                candidates: parse("bookmarks() & all()").unwrap(),
3512                visible_heads: vec![CommitId::from_hex("012345")],
3513            })), @r#"
3514        WithinVisibility {
3515            candidates: CommitRef(Bookmarks(Substring(""))),
3516            visible_heads: [
3517                CommitId("012345"),
3518            ],
3519        }
3520        "#);
3521
3522        insta::assert_debug_snapshot!(
3523            optimize(parse("~bookmarks() & all()").unwrap()),
3524            @r#"NotIn(CommitRef(Bookmarks(Substring(""))))"#);
3525        insta::assert_debug_snapshot!(
3526            optimize(parse("(bookmarks() & all()) | (all() & tags())").unwrap()), @r#"
3527        Union(
3528            CommitRef(Bookmarks(Substring(""))),
3529            CommitRef(Tags(Substring(""))),
3530        )
3531        "#);
3532        insta::assert_debug_snapshot!(
3533            optimize(parse("(bookmarks() & all()) & (all() & tags())").unwrap()), @r#"
3534        Intersection(
3535            CommitRef(Bookmarks(Substring(""))),
3536            CommitRef(Tags(Substring(""))),
3537        )
3538        "#);
3539        insta::assert_debug_snapshot!(
3540            optimize(parse("(bookmarks() & all()) ~ (all() & tags())").unwrap()), @r#"
3541        Difference(
3542            CommitRef(Bookmarks(Substring(""))),
3543            CommitRef(Tags(Substring(""))),
3544        )
3545        "#);
3546    }
3547
3548    #[test]
3549    fn test_optimize_unchanged_subtree() {
3550        fn unwrap_union(
3551            expression: &UserRevsetExpression,
3552        ) -> (&Rc<UserRevsetExpression>, &Rc<UserRevsetExpression>) {
3553            match expression {
3554                RevsetExpression::Union(left, right) => (left, right),
3555                _ => panic!("unexpected expression: {expression:?}"),
3556            }
3557        }
3558
3559        // transform_expression_bottom_up() should not recreate tree unnecessarily.
3560        let parsed = parse("foo-").unwrap();
3561        let optimized = optimize(parsed.clone());
3562        assert!(Rc::ptr_eq(&parsed, &optimized));
3563
3564        let parsed = parse("bookmarks() | tags()").unwrap();
3565        let optimized = optimize(parsed.clone());
3566        assert!(Rc::ptr_eq(&parsed, &optimized));
3567
3568        let parsed = parse("bookmarks() & tags()").unwrap();
3569        let optimized = optimize(parsed.clone());
3570        assert!(Rc::ptr_eq(&parsed, &optimized));
3571
3572        // Only left subtree should be rewritten.
3573        let parsed = parse("(bookmarks() & all()) | tags()").unwrap();
3574        let optimized = optimize(parsed.clone());
3575        assert_matches!(
3576            unwrap_union(&optimized).0.as_ref(),
3577            RevsetExpression::CommitRef(RevsetCommitRef::Bookmarks(_))
3578        );
3579        assert!(Rc::ptr_eq(
3580            unwrap_union(&parsed).1,
3581            unwrap_union(&optimized).1
3582        ));
3583
3584        // Only right subtree should be rewritten.
3585        let parsed = parse("bookmarks() | (all() & tags())").unwrap();
3586        let optimized = optimize(parsed.clone());
3587        assert!(Rc::ptr_eq(
3588            unwrap_union(&parsed).0,
3589            unwrap_union(&optimized).0
3590        ));
3591        assert_matches!(
3592            unwrap_union(&optimized).1.as_ref(),
3593            RevsetExpression::CommitRef(RevsetCommitRef::Tags(_))
3594        );
3595    }
3596
3597    #[test]
3598    fn test_optimize_difference() {
3599        let settings = insta_settings();
3600        let _guard = settings.bind_to_scope();
3601
3602        insta::assert_debug_snapshot!(optimize(parse("foo & ~bar").unwrap()), @r#"
3603        Difference(
3604            CommitRef(Symbol("foo")),
3605            CommitRef(Symbol("bar")),
3606        )
3607        "#);
3608        insta::assert_debug_snapshot!(optimize(parse("~foo & bar").unwrap()), @r#"
3609        Difference(
3610            CommitRef(Symbol("bar")),
3611            CommitRef(Symbol("foo")),
3612        )
3613        "#);
3614        insta::assert_debug_snapshot!(optimize(parse("~foo & bar & ~baz").unwrap()), @r#"
3615        Difference(
3616            Difference(
3617                CommitRef(Symbol("bar")),
3618                CommitRef(Symbol("foo")),
3619            ),
3620            CommitRef(Symbol("baz")),
3621        )
3622        "#);
3623        insta::assert_debug_snapshot!(optimize(parse("(all() & ~foo) & bar").unwrap()), @r#"
3624        Difference(
3625            CommitRef(Symbol("bar")),
3626            CommitRef(Symbol("foo")),
3627        )
3628        "#);
3629
3630        // Binary difference operation should go through the same optimization passes.
3631        insta::assert_debug_snapshot!(
3632            optimize(parse("all() ~ foo").unwrap()),
3633            @r#"NotIn(CommitRef(Symbol("foo")))"#);
3634        insta::assert_debug_snapshot!(optimize(parse("foo ~ bar").unwrap()), @r#"
3635        Difference(
3636            CommitRef(Symbol("foo")),
3637            CommitRef(Symbol("bar")),
3638        )
3639        "#);
3640        insta::assert_debug_snapshot!(optimize(parse("(all() ~ foo) & bar").unwrap()), @r#"
3641        Difference(
3642            CommitRef(Symbol("bar")),
3643            CommitRef(Symbol("foo")),
3644        )
3645        "#);
3646
3647        // Range expression.
3648        insta::assert_debug_snapshot!(optimize(parse("::foo & ~::bar").unwrap()), @r#"
3649        Range {
3650            roots: CommitRef(Symbol("bar")),
3651            heads: CommitRef(Symbol("foo")),
3652            generation: 0..18446744073709551615,
3653        }
3654        "#);
3655        insta::assert_debug_snapshot!(optimize(parse("~::foo & ::bar").unwrap()), @r#"
3656        Range {
3657            roots: CommitRef(Symbol("foo")),
3658            heads: CommitRef(Symbol("bar")),
3659            generation: 0..18446744073709551615,
3660        }
3661        "#);
3662        insta::assert_debug_snapshot!(optimize(parse("foo..").unwrap()), @r#"
3663        Range {
3664            roots: CommitRef(Symbol("foo")),
3665            heads: VisibleHeads,
3666            generation: 0..18446744073709551615,
3667        }
3668        "#);
3669        insta::assert_debug_snapshot!(optimize(parse("foo..bar").unwrap()), @r#"
3670        Range {
3671            roots: CommitRef(Symbol("foo")),
3672            heads: CommitRef(Symbol("bar")),
3673            generation: 0..18446744073709551615,
3674        }
3675        "#);
3676
3677        // Double/triple negates.
3678        insta::assert_debug_snapshot!(optimize(parse("foo & ~~bar").unwrap()), @r#"
3679        Intersection(
3680            CommitRef(Symbol("foo")),
3681            CommitRef(Symbol("bar")),
3682        )
3683        "#);
3684        insta::assert_debug_snapshot!(optimize(parse("foo & ~~~bar").unwrap()), @r#"
3685        Difference(
3686            CommitRef(Symbol("foo")),
3687            CommitRef(Symbol("bar")),
3688        )
3689        "#);
3690        insta::assert_debug_snapshot!(optimize(parse("~(all() & ~foo) & bar").unwrap()), @r#"
3691        Intersection(
3692            CommitRef(Symbol("foo")),
3693            CommitRef(Symbol("bar")),
3694        )
3695        "#);
3696
3697        // Should be better than '(all() & ~foo) & (all() & ~bar)'.
3698        insta::assert_debug_snapshot!(optimize(parse("~foo & ~bar").unwrap()), @r#"
3699        Difference(
3700            NotIn(CommitRef(Symbol("foo"))),
3701            CommitRef(Symbol("bar")),
3702        )
3703        "#);
3704    }
3705
3706    #[test]
3707    fn test_optimize_not_in_ancestors() {
3708        let settings = insta_settings();
3709        let _guard = settings.bind_to_scope();
3710
3711        // '~(::foo)' is equivalent to 'foo..'.
3712        insta::assert_debug_snapshot!(optimize(parse("~(::foo)").unwrap()), @r#"
3713        Range {
3714            roots: CommitRef(Symbol("foo")),
3715            heads: VisibleHeads,
3716            generation: 0..18446744073709551615,
3717        }
3718        "#);
3719
3720        // '~(::foo-)' is equivalent to 'foo-..'.
3721        insta::assert_debug_snapshot!(optimize(parse("~(::foo-)").unwrap()), @r#"
3722        Range {
3723            roots: Ancestors {
3724                heads: CommitRef(Symbol("foo")),
3725                generation: 1..2,
3726            },
3727            heads: VisibleHeads,
3728            generation: 0..18446744073709551615,
3729        }
3730        "#);
3731        insta::assert_debug_snapshot!(optimize(parse("~(::foo--)").unwrap()), @r#"
3732        Range {
3733            roots: Ancestors {
3734                heads: CommitRef(Symbol("foo")),
3735                generation: 2..3,
3736            },
3737            heads: VisibleHeads,
3738            generation: 0..18446744073709551615,
3739        }
3740        "#);
3741
3742        // Bounded ancestors shouldn't be substituted.
3743        insta::assert_debug_snapshot!(optimize(parse("~ancestors(foo, 1)").unwrap()), @r#"
3744        NotIn(
3745            Ancestors {
3746                heads: CommitRef(Symbol("foo")),
3747                generation: 0..1,
3748            },
3749        )
3750        "#);
3751        insta::assert_debug_snapshot!(optimize(parse("~ancestors(foo-, 1)").unwrap()), @r#"
3752        NotIn(
3753            Ancestors {
3754                heads: CommitRef(Symbol("foo")),
3755                generation: 1..2,
3756            },
3757        )
3758        "#);
3759    }
3760
3761    #[test]
3762    fn test_optimize_filter_difference() {
3763        let settings = insta_settings();
3764        let _guard = settings.bind_to_scope();
3765
3766        // '~empty()' -> '~~file(*)' -> 'file(*)'
3767        insta::assert_debug_snapshot!(optimize(parse("~empty()").unwrap()), @"Filter(File(All))");
3768
3769        // '& baz' can be moved into the filter node, and form a difference node.
3770        insta::assert_debug_snapshot!(
3771            optimize(parse("(author_name(foo) & ~bar) & baz").unwrap()), @r#"
3772        Intersection(
3773            Difference(
3774                CommitRef(Symbol("baz")),
3775                CommitRef(Symbol("bar")),
3776            ),
3777            Filter(AuthorName(Substring("foo"))),
3778        )
3779        "#);
3780
3781        // '~set & filter()' shouldn't be substituted.
3782        insta::assert_debug_snapshot!(
3783            optimize(parse("~foo & author_name(bar)").unwrap()), @r#"
3784        Intersection(
3785            NotIn(CommitRef(Symbol("foo"))),
3786            Filter(AuthorName(Substring("bar"))),
3787        )
3788        "#);
3789        insta::assert_debug_snapshot!(
3790            optimize(parse("~foo & (author_name(bar) | baz)").unwrap()), @r#"
3791        Intersection(
3792            NotIn(CommitRef(Symbol("foo"))),
3793            AsFilter(
3794                Union(
3795                    Filter(AuthorName(Substring("bar"))),
3796                    CommitRef(Symbol("baz")),
3797                ),
3798            ),
3799        )
3800        "#);
3801
3802        // Filter should be moved right of the intersection.
3803        insta::assert_debug_snapshot!(
3804            optimize(parse("author_name(foo) ~ bar").unwrap()), @r#"
3805        Intersection(
3806            NotIn(CommitRef(Symbol("bar"))),
3807            Filter(AuthorName(Substring("foo"))),
3808        )
3809        "#);
3810    }
3811
3812    #[test]
3813    fn test_optimize_filter_intersection() {
3814        let settings = insta_settings();
3815        let _guard = settings.bind_to_scope();
3816
3817        insta::assert_debug_snapshot!(
3818            optimize(parse("author_name(foo)").unwrap()),
3819            @r#"Filter(AuthorName(Substring("foo")))"#);
3820
3821        insta::assert_debug_snapshot!(optimize(parse("foo & description(bar)").unwrap()), @r#"
3822        Intersection(
3823            CommitRef(Symbol("foo")),
3824            Filter(Description(Substring("bar"))),
3825        )
3826        "#);
3827        insta::assert_debug_snapshot!(optimize(parse("author_name(foo) & bar").unwrap()), @r#"
3828        Intersection(
3829            CommitRef(Symbol("bar")),
3830            Filter(AuthorName(Substring("foo"))),
3831        )
3832        "#);
3833        insta::assert_debug_snapshot!(
3834            optimize(parse("author_name(foo) & committer_name(bar)").unwrap()), @r#"
3835        Intersection(
3836            Filter(AuthorName(Substring("foo"))),
3837            Filter(CommitterName(Substring("bar"))),
3838        )
3839        "#);
3840
3841        insta::assert_debug_snapshot!(
3842            optimize(parse("foo & description(bar) & author_name(baz)").unwrap()), @r#"
3843        Intersection(
3844            Intersection(
3845                CommitRef(Symbol("foo")),
3846                Filter(Description(Substring("bar"))),
3847            ),
3848            Filter(AuthorName(Substring("baz"))),
3849        )
3850        "#);
3851        insta::assert_debug_snapshot!(
3852            optimize(parse("committer_name(foo) & bar & author_name(baz)").unwrap()), @r#"
3853        Intersection(
3854            Intersection(
3855                CommitRef(Symbol("bar")),
3856                Filter(CommitterName(Substring("foo"))),
3857            ),
3858            Filter(AuthorName(Substring("baz"))),
3859        )
3860        "#);
3861        insta::assert_debug_snapshot!(
3862            optimize(parse_with_workspace(
3863                "committer_name(foo) & files(bar) & baz",
3864                WorkspaceName::DEFAULT).unwrap(),
3865            ), @r#"
3866        Intersection(
3867            Intersection(
3868                CommitRef(Symbol("baz")),
3869                Filter(CommitterName(Substring("foo"))),
3870            ),
3871            Filter(File(Pattern(PrefixPath("bar")))),
3872        )
3873        "#);
3874        insta::assert_debug_snapshot!(
3875            optimize(parse_with_workspace(
3876                "committer_name(foo) & files(bar) & author_name(baz)",
3877                WorkspaceName::DEFAULT).unwrap(),
3878            ), @r#"
3879        Intersection(
3880            Intersection(
3881                Filter(CommitterName(Substring("foo"))),
3882                Filter(File(Pattern(PrefixPath("bar")))),
3883            ),
3884            Filter(AuthorName(Substring("baz"))),
3885        )
3886        "#);
3887        insta::assert_debug_snapshot!(
3888            optimize(parse_with_workspace(
3889                "foo & files(bar) & baz",
3890                WorkspaceName::DEFAULT).unwrap(),
3891            ), @r#"
3892        Intersection(
3893            Intersection(
3894                CommitRef(Symbol("foo")),
3895                CommitRef(Symbol("baz")),
3896            ),
3897            Filter(File(Pattern(PrefixPath("bar")))),
3898        )
3899        "#);
3900
3901        insta::assert_debug_snapshot!(
3902            optimize(parse("foo & description(bar) & author_name(baz) & qux").unwrap()), @r#"
3903        Intersection(
3904            Intersection(
3905                Intersection(
3906                    CommitRef(Symbol("foo")),
3907                    CommitRef(Symbol("qux")),
3908                ),
3909                Filter(Description(Substring("bar"))),
3910            ),
3911            Filter(AuthorName(Substring("baz"))),
3912        )
3913        "#);
3914        insta::assert_debug_snapshot!(
3915            optimize(parse("foo & description(bar) & parents(author_name(baz)) & qux").unwrap()),
3916            @r#"
3917        Intersection(
3918            Intersection(
3919                Intersection(
3920                    CommitRef(Symbol("foo")),
3921                    Ancestors {
3922                        heads: Filter(AuthorName(Substring("baz"))),
3923                        generation: 1..2,
3924                    },
3925                ),
3926                CommitRef(Symbol("qux")),
3927            ),
3928            Filter(Description(Substring("bar"))),
3929        )
3930        "#);
3931        insta::assert_debug_snapshot!(
3932            optimize(parse("foo & description(bar) & parents(author_name(baz) & qux)").unwrap()),
3933            @r#"
3934        Intersection(
3935            Intersection(
3936                CommitRef(Symbol("foo")),
3937                Ancestors {
3938                    heads: Intersection(
3939                        CommitRef(Symbol("qux")),
3940                        Filter(AuthorName(Substring("baz"))),
3941                    ),
3942                    generation: 1..2,
3943                },
3944            ),
3945            Filter(Description(Substring("bar"))),
3946        )
3947        "#);
3948
3949        // Symbols have to be pushed down to the innermost filter node.
3950        insta::assert_debug_snapshot!(
3951            optimize(parse("(a & author_name(A)) & (b & author_name(B)) & (c & author_name(C))").unwrap()),
3952            @r#"
3953        Intersection(
3954            Intersection(
3955                Intersection(
3956                    Intersection(
3957                        Intersection(
3958                            CommitRef(Symbol("a")),
3959                            CommitRef(Symbol("b")),
3960                        ),
3961                        CommitRef(Symbol("c")),
3962                    ),
3963                    Filter(AuthorName(Substring("A"))),
3964                ),
3965                Filter(AuthorName(Substring("B"))),
3966            ),
3967            Filter(AuthorName(Substring("C"))),
3968        )
3969        "#);
3970        insta::assert_debug_snapshot!(
3971            optimize(parse("(a & author_name(A)) & ((b & author_name(B)) & (c & author_name(C))) & d").unwrap()),
3972            @r#"
3973        Intersection(
3974            Intersection(
3975                Intersection(
3976                    Intersection(
3977                        Intersection(
3978                            CommitRef(Symbol("a")),
3979                            Intersection(
3980                                CommitRef(Symbol("b")),
3981                                CommitRef(Symbol("c")),
3982                            ),
3983                        ),
3984                        CommitRef(Symbol("d")),
3985                    ),
3986                    Filter(AuthorName(Substring("A"))),
3987                ),
3988                Filter(AuthorName(Substring("B"))),
3989            ),
3990            Filter(AuthorName(Substring("C"))),
3991        )
3992        "#);
3993
3994        // 'all()' moves in to 'filter()' first, so 'A & filter()' can be found.
3995        insta::assert_debug_snapshot!(
3996            optimize(parse("foo & (all() & description(bar)) & (author_name(baz) & all())").unwrap()),
3997            @r#"
3998        Intersection(
3999            Intersection(
4000                CommitRef(Symbol("foo")),
4001                Filter(Description(Substring("bar"))),
4002            ),
4003            Filter(AuthorName(Substring("baz"))),
4004        )
4005        "#);
4006
4007        // Filter node shouldn't move across at_operation() boundary.
4008        insta::assert_debug_snapshot!(
4009            optimize(parse("author_name(foo) & bar & at_operation(@-, committer_name(baz))").unwrap()),
4010            @r#"
4011        Intersection(
4012            Intersection(
4013                CommitRef(Symbol("bar")),
4014                AtOperation {
4015                    operation: "@-",
4016                    candidates: Filter(CommitterName(Substring("baz"))),
4017                },
4018            ),
4019            Filter(AuthorName(Substring("foo"))),
4020        )
4021        "#);
4022    }
4023
4024    #[test]
4025    fn test_optimize_filter_subtree() {
4026        let settings = insta_settings();
4027        let _guard = settings.bind_to_scope();
4028
4029        insta::assert_debug_snapshot!(
4030            optimize(parse("(author_name(foo) | bar) & baz").unwrap()), @r#"
4031        Intersection(
4032            CommitRef(Symbol("baz")),
4033            AsFilter(
4034                Union(
4035                    Filter(AuthorName(Substring("foo"))),
4036                    CommitRef(Symbol("bar")),
4037                ),
4038            ),
4039        )
4040        "#);
4041
4042        insta::assert_debug_snapshot!(
4043            optimize(parse("(foo | committer_name(bar)) & description(baz) & qux").unwrap()), @r#"
4044        Intersection(
4045            Intersection(
4046                CommitRef(Symbol("qux")),
4047                AsFilter(
4048                    Union(
4049                        CommitRef(Symbol("foo")),
4050                        Filter(CommitterName(Substring("bar"))),
4051                    ),
4052                ),
4053            ),
4054            Filter(Description(Substring("baz"))),
4055        )
4056        "#);
4057
4058        insta::assert_debug_snapshot!(
4059            optimize(parse("(~present(author_name(foo) & bar) | baz) & qux").unwrap()), @r#"
4060        Intersection(
4061            CommitRef(Symbol("qux")),
4062            AsFilter(
4063                Union(
4064                    AsFilter(
4065                        NotIn(
4066                            AsFilter(
4067                                Present(
4068                                    Intersection(
4069                                        CommitRef(Symbol("bar")),
4070                                        Filter(AuthorName(Substring("foo"))),
4071                                    ),
4072                                ),
4073                            ),
4074                        ),
4075                    ),
4076                    CommitRef(Symbol("baz")),
4077                ),
4078            ),
4079        )
4080        "#);
4081
4082        // Symbols have to be pushed down to the innermost filter node.
4083        insta::assert_debug_snapshot!(
4084            optimize(parse(
4085                "(a & (author_name(A) | 0)) & (b & (author_name(B) | 1)) & (c & (author_name(C) | 2))").unwrap()),
4086            @r#"
4087        Intersection(
4088            Intersection(
4089                Intersection(
4090                    Intersection(
4091                        Intersection(
4092                            CommitRef(Symbol("a")),
4093                            CommitRef(Symbol("b")),
4094                        ),
4095                        CommitRef(Symbol("c")),
4096                    ),
4097                    AsFilter(
4098                        Union(
4099                            Filter(AuthorName(Substring("A"))),
4100                            CommitRef(Symbol("0")),
4101                        ),
4102                    ),
4103                ),
4104                AsFilter(
4105                    Union(
4106                        Filter(AuthorName(Substring("B"))),
4107                        CommitRef(Symbol("1")),
4108                    ),
4109                ),
4110            ),
4111            AsFilter(
4112                Union(
4113                    Filter(AuthorName(Substring("C"))),
4114                    CommitRef(Symbol("2")),
4115                ),
4116            ),
4117        )
4118        "#);
4119    }
4120
4121    #[test]
4122    fn test_optimize_ancestors() {
4123        let settings = insta_settings();
4124        let _guard = settings.bind_to_scope();
4125
4126        // Typical scenario: fold nested parents()
4127        insta::assert_debug_snapshot!(optimize(parse("foo--").unwrap()), @r#"
4128        Ancestors {
4129            heads: CommitRef(Symbol("foo")),
4130            generation: 2..3,
4131        }
4132        "#);
4133        insta::assert_debug_snapshot!(optimize(parse("::(foo---)").unwrap()), @r#"
4134        Ancestors {
4135            heads: CommitRef(Symbol("foo")),
4136            generation: 3..18446744073709551615,
4137        }
4138        "#);
4139        insta::assert_debug_snapshot!(optimize(parse("(::foo)---").unwrap()), @r#"
4140        Ancestors {
4141            heads: CommitRef(Symbol("foo")),
4142            generation: 3..18446744073709551615,
4143        }
4144        "#);
4145
4146        // 'foo-+' is not 'foo'.
4147        insta::assert_debug_snapshot!(optimize(parse("foo---+").unwrap()), @r#"
4148        Descendants {
4149            roots: Ancestors {
4150                heads: CommitRef(Symbol("foo")),
4151                generation: 3..4,
4152            },
4153            generation: 1..2,
4154        }
4155        "#);
4156
4157        // For 'roots..heads', heads can be folded.
4158        insta::assert_debug_snapshot!(optimize(parse("foo..(bar--)").unwrap()), @r#"
4159        Range {
4160            roots: CommitRef(Symbol("foo")),
4161            heads: CommitRef(Symbol("bar")),
4162            generation: 2..18446744073709551615,
4163        }
4164        "#);
4165        // roots can also be folded, and the range expression is reconstructed.
4166        insta::assert_debug_snapshot!(optimize(parse("(foo--)..(bar---)").unwrap()), @r#"
4167        Range {
4168            roots: Ancestors {
4169                heads: CommitRef(Symbol("foo")),
4170                generation: 2..3,
4171            },
4172            heads: CommitRef(Symbol("bar")),
4173            generation: 3..18446744073709551615,
4174        }
4175        "#);
4176        // Bounded ancestors shouldn't be substituted to range.
4177        insta::assert_debug_snapshot!(
4178            optimize(parse("~ancestors(foo, 2) & ::bar").unwrap()), @r#"
4179        Difference(
4180            Ancestors {
4181                heads: CommitRef(Symbol("bar")),
4182                generation: 0..18446744073709551615,
4183            },
4184            Ancestors {
4185                heads: CommitRef(Symbol("foo")),
4186                generation: 0..2,
4187            },
4188        )
4189        "#);
4190
4191        // If inner range is bounded by roots, it cannot be merged.
4192        // e.g. '..(foo..foo)' is equivalent to '..none()', not to '..foo'
4193        insta::assert_debug_snapshot!(optimize(parse("(foo..bar)--").unwrap()), @r#"
4194        Ancestors {
4195            heads: Range {
4196                roots: CommitRef(Symbol("foo")),
4197                heads: CommitRef(Symbol("bar")),
4198                generation: 0..18446744073709551615,
4199            },
4200            generation: 2..3,
4201        }
4202        "#);
4203        insta::assert_debug_snapshot!(optimize(parse("foo..(bar..baz)").unwrap()), @r#"
4204        Range {
4205            roots: CommitRef(Symbol("foo")),
4206            heads: Range {
4207                roots: CommitRef(Symbol("bar")),
4208                heads: CommitRef(Symbol("baz")),
4209                generation: 0..18446744073709551615,
4210            },
4211            generation: 0..18446744073709551615,
4212        }
4213        "#);
4214
4215        // Ancestors of empty generation range should be empty.
4216        insta::assert_debug_snapshot!(
4217            optimize(parse("ancestors(ancestors(foo), 0)").unwrap()), @r#"
4218        Ancestors {
4219            heads: CommitRef(Symbol("foo")),
4220            generation: 0..0,
4221        }
4222        "#
4223        );
4224        insta::assert_debug_snapshot!(
4225            optimize(parse("ancestors(ancestors(foo, 0))").unwrap()), @r#"
4226        Ancestors {
4227            heads: CommitRef(Symbol("foo")),
4228            generation: 0..0,
4229        }
4230        "#
4231        );
4232    }
4233
4234    #[test]
4235    fn test_optimize_descendants() {
4236        let settings = insta_settings();
4237        let _guard = settings.bind_to_scope();
4238
4239        // Typical scenario: fold nested children()
4240        insta::assert_debug_snapshot!(optimize(parse("foo++").unwrap()), @r#"
4241        Descendants {
4242            roots: CommitRef(Symbol("foo")),
4243            generation: 2..3,
4244        }
4245        "#);
4246        insta::assert_debug_snapshot!(optimize(parse("(foo+++)::").unwrap()), @r#"
4247        Descendants {
4248            roots: CommitRef(Symbol("foo")),
4249            generation: 3..18446744073709551615,
4250        }
4251        "#);
4252        insta::assert_debug_snapshot!(optimize(parse("(foo::)+++").unwrap()), @r#"
4253        Descendants {
4254            roots: CommitRef(Symbol("foo")),
4255            generation: 3..18446744073709551615,
4256        }
4257        "#);
4258
4259        // 'foo+-' is not 'foo'.
4260        insta::assert_debug_snapshot!(optimize(parse("foo+++-").unwrap()), @r#"
4261        Ancestors {
4262            heads: Descendants {
4263                roots: CommitRef(Symbol("foo")),
4264                generation: 3..4,
4265            },
4266            generation: 1..2,
4267        }
4268        "#);
4269
4270        // TODO: Inner Descendants can be folded into DagRange. Perhaps, we can rewrite
4271        // 'x::y' to 'x:: & ::y' first, so the common substitution rule can handle both
4272        // 'x+::y' and 'x+ & ::y'.
4273        insta::assert_debug_snapshot!(optimize(parse("(foo++)::bar").unwrap()), @r#"
4274        DagRange {
4275            roots: Descendants {
4276                roots: CommitRef(Symbol("foo")),
4277                generation: 2..3,
4278            },
4279            heads: CommitRef(Symbol("bar")),
4280        }
4281        "#);
4282    }
4283
4284    #[test]
4285    fn test_escape_string_literal() {
4286        // Valid identifiers don't need quoting
4287        assert_eq!(format_symbol("foo"), "foo");
4288        assert_eq!(format_symbol("foo.bar"), "foo.bar");
4289
4290        // Invalid identifiers need quoting
4291        assert_eq!(format_symbol("foo@bar"), r#""foo@bar""#);
4292        assert_eq!(format_symbol("foo bar"), r#""foo bar""#);
4293        assert_eq!(format_symbol(" foo "), r#"" foo ""#);
4294        assert_eq!(format_symbol("(foo)"), r#""(foo)""#);
4295        assert_eq!(format_symbol("all:foo"), r#""all:foo""#);
4296
4297        // Some characters also need escaping
4298        assert_eq!(format_symbol("foo\"bar"), r#""foo\"bar""#);
4299        assert_eq!(format_symbol("foo\\bar"), r#""foo\\bar""#);
4300        assert_eq!(format_symbol("foo\\\"bar"), r#""foo\\\"bar""#);
4301        assert_eq!(format_symbol("foo\nbar"), r#""foo\nbar""#);
4302
4303        // Some characters don't technically need escaping, but we escape them for
4304        // clarity
4305        assert_eq!(format_symbol("foo\"bar"), r#""foo\"bar""#);
4306        assert_eq!(format_symbol("foo\\bar"), r#""foo\\bar""#);
4307        assert_eq!(format_symbol("foo\\\"bar"), r#""foo\\\"bar""#);
4308        assert_eq!(format_symbol("foo \x01 bar"), r#""foo \x01 bar""#);
4309    }
4310
4311    #[test]
4312    fn test_escape_remote_symbol() {
4313        assert_eq!(format_remote_symbol("foo", "bar"), "foo@bar");
4314        assert_eq!(
4315            format_remote_symbol(" foo ", "bar:baz"),
4316            r#"" foo "@"bar:baz""#
4317        );
4318    }
4319}