revset: add unary negate (or set complement) operator '~y'

Because a unary negation node '~y' is more primitive than the corresponding
difference node 'x~y', '~y' is easier to deal with while rewriting the tree.
That's the main reason to add RevsetExpression::NotIn node.

As we have a NotIn node, it makes sense to add an operator for that. This
patch reuses '~' token, which I feel intuitive since the other set operators
looks like bitwise ops. Another option is '!'.

The unary '~' operator has the highest precedence among the set operators,
but they are lower than the ranges. This might be counter intuitive, but
useful because a prefix range ':x' can be negated without parens.

Maybe we can remove the redundant infix operator 'x ~ y', but it isn't
decided yet.
This commit is contained in:
Yuya Nishihara 2022-11-17 21:58:51 +09:00
parent 2e725270e4
commit 48d10d648c
5 changed files with 53 additions and 2 deletions

View file

@ -57,6 +57,7 @@ only symbols.
* `x & y`: Revisions that are in both `x` and `y`.
* `x | y`: Revisions that are in either `x` or `y` (or both).
* `x ~ y`: Revisions that are in `x` but not in `y`.
* `~x`: Revisions that are not in `x`.
* `x-`: Parents of `x`.
* `x+`: Children of `x`.
* `:x`: Ancestors of `x`, including the commits in `x` itself.

View file

@ -37,6 +37,7 @@ range_ops = _{ dag_range_op | range_op }
range_pre_ops = _{ dag_range_pre_op | range_pre_op }
range_post_ops = _{ dag_range_post_op | range_post_op }
negate_op = { "~" }
union_op = { "|" }
intersection_op = { "&" }
difference_op = { "~" }
@ -68,7 +69,8 @@ range_expression = _{
}
expression = {
whitespace* ~ range_expression ~ whitespace* ~ (infix_op ~ whitespace* ~ range_expression ~ whitespace*)*
whitespace* ~ (negate_op ~ whitespace*)* ~ range_expression ~ whitespace*
~ (infix_op ~ whitespace* ~ (negate_op ~ whitespace*)* ~ range_expression ~ whitespace*)*
}
program = _{ SOI ~ expression ~ EOI }

View file

@ -367,6 +367,7 @@ pub enum RevsetExpression {
predicate: RevsetFilterPredicate,
},
Present(Rc<RevsetExpression>),
NotIn(Rc<RevsetExpression>),
Union(Rc<RevsetExpression>, Rc<RevsetExpression>),
Intersection(Rc<RevsetExpression>, Rc<RevsetExpression>),
Difference(Rc<RevsetExpression>, Rc<RevsetExpression>),
@ -487,6 +488,11 @@ impl RevsetExpression {
})
}
/// Commits that are not in `self`, i.e. the complement of `self`.
pub fn negated(self: &Rc<RevsetExpression>) -> Rc<RevsetExpression> {
Rc::new(RevsetExpression::NotIn(self.clone()))
}
/// Commits that are in `self` or in `other` (or both).
pub fn union(
self: &Rc<RevsetExpression>,
@ -686,6 +692,7 @@ fn parse_expression_rule(
.op(Op::infix(Rule::union_op, Assoc::Left))
.op(Op::infix(Rule::intersection_op, Assoc::Left)
| Op::infix(Rule::difference_op, Assoc::Left))
.op(Op::prefix(Rule::negate_op))
// Ranges can't be nested without parentheses. Associativity doesn't matter.
.op(Op::infix(Rule::dag_range_op, Assoc::Left) | Op::infix(Rule::range_op, Assoc::Left))
.op(Op::prefix(Rule::dag_range_pre_op) | Op::prefix(Rule::range_pre_op))
@ -696,6 +703,7 @@ fn parse_expression_rule(
PRATT
.map_primary(|primary| parse_primary_rule(primary, state))
.map_prefix(|op, rhs| match op.as_rule() {
Rule::negate_op => Ok(rhs?.negated()),
Rule::dag_range_pre_op | Rule::range_pre_op => Ok(rhs?.ancestors()),
r => panic!("unexpected prefix operator rule {r:?}"),
})
@ -1103,6 +1111,9 @@ fn transform_expression_bottom_up(
RevsetExpression::Present(candidates) => {
transform_rec(candidates, f).map(RevsetExpression::Present)
}
RevsetExpression::NotIn(complement) => {
transform_rec(complement, f).map(RevsetExpression::NotIn)
}
RevsetExpression::Union(expression1, expression2) => {
transform_rec_pair((expression1, expression2), f).map(
|(expression1, expression2)| RevsetExpression::Union(expression1, expression2),
@ -1801,6 +1812,11 @@ pub fn evaluate_expression<'repo>(
Err(RevsetError::NoSuchRevision(_)) => Ok(Box::new(EagerRevset::empty())),
r @ Err(RevsetError::AmbiguousIdPrefix(_) | RevsetError::StoreError(_)) => r,
},
RevsetExpression::NotIn(complement) => {
let set1 = RevsetExpression::All.evaluate(repo, workspace_ctx)?;
let set2 = complement.evaluate(repo, workspace_ctx)?;
Ok(Box::new(DifferenceRevset { set1, set2 }))
}
RevsetExpression::Union(expression1, expression2) => {
let set1 = expression1.evaluate(repo, workspace_ctx)?;
let set2 = expression2.evaluate(repo, workspace_ctx)?;
@ -1947,6 +1963,10 @@ mod tests {
heads: wc_symbol.clone()
})
);
assert_eq!(
foo_symbol.negated(),
Rc::new(RevsetExpression::NotIn(foo_symbol.clone()))
);
assert_eq!(
foo_symbol.union(&wc_symbol),
Rc::new(RevsetExpression::Union(
@ -2013,6 +2033,12 @@ mod tests {
Ok(wc_symbol.range(&RevsetExpression::visible_heads()))
);
assert_eq!(parse("foo..bar"), Ok(foo_symbol.range(&bar_symbol)));
// Parse the "negate" operator
assert_eq!(parse("~ foo"), Ok(foo_symbol.negated()));
assert_eq!(
parse("~ ~~ foo"),
Ok(foo_symbol.negated().negated().negated())
);
// Parse the "intersection" operator
assert_eq!(parse("foo & bar"), Ok(foo_symbol.intersection(&bar_symbol)));
// Parse the "union" operator
@ -2057,6 +2083,11 @@ mod tests {
Ok(foo_symbol.children().children().children())
);
// Set operator associativity/precedence
assert_eq!(parse("~x|y").unwrap(), parse("(~x)|y").unwrap());
assert_eq!(parse("x&~y").unwrap(), parse("x&(~y)").unwrap());
assert_eq!(parse("x~~y").unwrap(), parse("x~(~y)").unwrap());
assert_eq!(parse("x~~~y").unwrap(), parse("x~(~(~y))").unwrap());
assert_eq!(parse("~x:y").unwrap(), parse("~(x:y)").unwrap());
assert_eq!(parse("x|y|z").unwrap(), parse("(x|y)|z").unwrap());
assert_eq!(parse("x&y|z").unwrap(), parse("(x&y)|z").unwrap());
assert_eq!(parse("x|y&z").unwrap(), parse("x|(y&z)").unwrap());
@ -2342,6 +2373,10 @@ mod tests {
Rc::new(RevsetExpression::Present(RevsetExpression::branches()))
);
assert_eq!(
optimize(parse("~branches() & all()").unwrap()),
RevsetExpression::branches().negated()
);
assert_eq!(
optimize(parse("(branches() & all()) | (all() & tags())").unwrap()),
RevsetExpression::branches().union(&RevsetExpression::tags())

View file

@ -1734,6 +1734,12 @@ fn test_evaluate_expression_difference(use_git: bool) {
let commit4 = graph_builder.commit_with_parents(&[&commit3]);
let commit5 = graph_builder.commit_with_parents(&[&commit2]);
// Difference from all
assert_eq!(
resolve_commit_ids(mut_repo.as_repo_ref(), &format!("~:{}", commit5.id().hex())),
vec![commit4.id().clone(), commit3.id().clone()]
);
// Difference between ancestors
assert_eq!(
resolve_commit_ids(
@ -1749,6 +1755,13 @@ fn test_evaluate_expression_difference(use_git: bool) {
),
vec![commit5.id().clone()]
);
assert_eq!(
resolve_commit_ids(
mut_repo.as_repo_ref(),
&format!("~:{} & :{}", commit4.id().hex(), commit5.id().hex())
),
vec![commit5.id().clone()]
);
assert_eq!(
resolve_commit_ids(
mut_repo.as_repo_ref(),

View file

@ -29,7 +29,7 @@ fn test_syntax_error() {
1 | x &
| ^---
|
= expected dag_range_pre_op, range_pre_op, or primary
= expected dag_range_pre_op, range_pre_op, negate_op, or primary
"###);
}