Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

or-patterns: Push `PatKind/PatternKind::Or` at top level to HIR & HAIR #64508

Merged
merged 22 commits into from Sep 25, 2019

Conversation

@Centril
Copy link
Member

commented Sep 16, 2019

Following up on work in #64111, #63693, and #61708, in this PR:

  • We change hair::Arm.patterns: Vec<Pattern<'_>> into hir::Arm.pattern: Pattern<'_>.

    • fn hair::Arm::top_pats_hack is introduced as a temporary crutch in MIR building to avoid more changes.
  • We change hir::Arm.pats: HirVec<P<Pat>> into hir::Arm.pat: P<Pat>.

    • The hacks in rustc::hir::lowering are removed since the representation hack is no longer necessary.

    • In some places, fn hir::Arm::top_pats_hack is introduced to leave some things as future work.

    • Misc changes: HIR pretty printing is adjusted to behave uniformly wrt. top/inner levels, rvalue promotion is adjusted, regionck, and dead_code is also.

    • Type checking is adjusted to uniformly handle or-patterns at top/inner levels.
      To make things compile, p_0 | ... | p_n is redefined as a "reference pattern" in fn is_non_ref_pat for now. This is done so that reference types are not eagerly stripped from the expected: Ty<'tcx>.

    • Liveness is adjusted wrt. the unused_variables and unused_assignments lints to handle top/inner levels uniformly and the handling of fn parameters, let locals, and match arms are unified in this respect. This is not tested for now as exhaustiveness checks are reachable and will ICE.

    • In check_match, checking @ and by-move bindings is adjusted. However, exhaustiveness checking is not adjusted the moment and is handled by @dlrobertson in #63688.

    • AST borrowck (construct.rs) is not adjusted as AST borrowck will be removed soon.

r? @matthewjasper
cc @dlrobertson @varkor @oli-obk

src/librustc/middle/dead.rs Outdated Show resolved Hide resolved
src/librustc/middle/liveness.rs Show resolved Hide resolved
src/librustc/middle/liveness.rs Show resolved Hide resolved
@@ -142,7 +142,7 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
// Step 2. Create the otherwise and prebinding blocks.

// create binding start block for link them by false edges
let candidate_count = arms.iter().map(|c| c.patterns.len()).sum::<usize>();
let candidate_count = arms.iter().map(|c| c.top_pats_hack().len()).sum::<usize>();

This comment has been minimized.

Copy link
@Centril

Centril Sep 16, 2019

Author Member

@dlrobertson You might want to link up with this (and the other diffs in this file) in #63688 or leave it as a follow up.

@@ -97,11 +97,10 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
self.check_pat_struct(pat, qpath, fields, *etc, expected, def_bm, discrim_span)
}
PatKind::Or(pats) => {
let expected_ty = self.structurally_resolved_type(pat.span, expected);

This comment has been minimized.

Copy link
@Centril

Centril Sep 16, 2019

Author Member

@dlrobertson Was there a specific reason for .structurally_resolved_type here?

for p in &arm.pats {
self.constrain_bindings_in_pat(p);
}
self.constrain_bindings_in_pat(&arm.pat);

This comment has been minimized.

Copy link
@Centril

Centril Sep 16, 2019

Author Member

Not sure what appropriate tests for diffs in this module would be.

}

let module = self.tcx.hir().get_module_parent(scrut.hir_id);
MatchCheckCtxt::create_and_enter(self.tcx, self.param_env, module, |ref mut cx| {
let mut have_errors = false;

let inlined_arms : Vec<(Vec<_>, _)> = arms.iter().map(|arm| (
arm.pats.iter().map(|pat| {
arm.top_pats_hack().iter().map(|pat| {

This comment has been minimized.

Copy link
@Centril

Centril Sep 16, 2019

Author Member

@dlrobertson You might want to extract the exhaustiveness checking changes out from #63688 to a dedicated PR and link up with the change here. That might allow us to parallelize adding tests for exhaustiveness / liveness stuff.

Pacify `tidy`. It's also more correct in this context.
/// `match foo() { Some(a) => (), None => () }`.
///
/// When encountering an or-pattern `p_0 | ... | p_n` only `p_0` will be visited.
pub fn each_binding_or_first<F>(&self, c: &mut F)

This comment has been minimized.

Copy link
@varkor

varkor Sep 16, 2019

Member
Suggested change
pub fn each_binding_or_first<F>(&self, c: &mut F)
pub fn each_binding_or_first<F>(&self, f: &mut F)

This comment has been minimized.

Copy link
@varkor

varkor Sep 16, 2019

Member

Is calling each_binding_or_first recursively going to have the right behaviour for nested Or patterns? Shouldn't it pick the first pattern only at the top level? (In which case, you can use each_binding instead and just check the top level at the call site.)

This comment has been minimized.

Copy link
@Centril

Centril Sep 16, 2019

Author Member

(Using c for callback as f is used for FieldPat; could switch to g for the callback instead.)

(We discussed this a bit privately on Discord; The existing commentary in define_bindings_in_pat states that we only consider the first pattern since any later patterns must have the same bindings (and the first pattern is considered to have the authoritative set of ids). This suggests that we should do the same in nested positions as there should be no intrinsic semantic difference between Some(a @ 1) | Some(a @ 2) and Some(a @ (1 | 2)). That said, I don't really know why we only consider the first pattern and if we just use .each_binding(...) uniformly for top/nested then the UI tests pass at least.)

This comment has been minimized.

Copy link
@Centril

Centril Sep 16, 2019

Author Member

Looking at blame, it seems the comment was left by @eddyb 5 years ago in b062128. Maybe they still know. =P

This comment has been minimized.

Copy link
@varkor

This comment has been minimized.

Copy link
@nikomatsakis

nikomatsakis Sep 18, 2019

Contributor

I don't even understand what the question is here. :)

This comment has been minimized.

Copy link
@Centril

Centril Sep 18, 2019

Author Member

@nikomatsakis So your comment to a call of .define_bindings_in_pat(...) in liveness.rs regarding ExprKind::Match(...) => states that:

                    // only consider the first pattern; any later patterns must have
                    // the same bindings, and we also consider the first pattern to be
                    // the "authoritative" set of ids
                    let arm_succ =
                        self.define_bindings_in_arm_pats(arm.pats.first().map(|p| &**p),
                                                         guard_succ);

Now, I don't exactly understand why this is, but my intuition says that if we want to extend this logic to nested or-patterns, e.g. Some(A(x) | B(x)) then we should do the same think there. That is, in any PatKind::Or(pats) we will just visit pats[0] and not pats[1..]. That's what the code in this PR does now.

This comment has been minimized.

Copy link
@nikomatsakis

nikomatsakis Sep 19, 2019

Contributor

Well, so here's the problem. Presently we use the id of the AST node for a pattern binding as the variable's id -- but when you have e.g. Foo(x) | Bar(x), you wind up with two id's for x! Clearly all the alternatives must have the same set of bindings (i.e., they must all define x), so yeah I dimly recall deciding that we would just use the id's from the first set of patterns to define the variables, and then check the remaining patterns against that first set.

I think another viable approach would be to use some kind of fresh identifiers for the variables (not the id from the pattern node) and then be able to "resolve" each pattern node to that. Or perhaps to have a table of "redirects" where for most bindings the redirect is the identity, but for | patterns they point to the bindings from the first set.

In any case I think you are correct that if we are moving | patterns further down, and we want to try and use the same strategy, then we only want to take the left-most branches (but check the other branches for consistency).

This comment has been minimized.

Copy link
@Centril

Centril Sep 19, 2019

Author Member

Thanks for the elaboration!

Clearly all the alternatives must have the same set of bindings (i.e., they must all define x), so yeah I dimly recall deciding that we would just use the id's from the first set of patterns to define the variables, and then check the remaining patterns against that first set.

(but check the other branches for consistency).

Yea, that's what we do; I recently tweaked this logic in resolve here:

fn resolve_params(&mut self, params: &[Param]) {
let mut bindings = smallvec![(PatBoundCtx::Product, Default::default())];
for Param { pat, ty, .. } in params {
self.resolve_pattern(pat, PatternSource::FnParam, &mut bindings);
self.visit_ty(ty);
debug!("(resolving function / closure) recorded parameter");
}
}
fn resolve_local(&mut self, local: &Local) {
// Resolve the type.
walk_list!(self, visit_ty, &local.ty);
// Resolve the initializer.
walk_list!(self, visit_expr, &local.init);
// Resolve the pattern.
self.resolve_pattern_top(&local.pat, PatternSource::Let);
}
/// build a map from pattern identifiers to binding-info's.
/// this is done hygienically. This could arise for a macro
/// that expands into an or-pattern where one 'x' was from the
/// user and one 'x' came from the macro.
fn binding_mode_map(&mut self, pat: &Pat) -> BindingMap {
let mut binding_map = FxHashMap::default();
pat.walk(&mut |pat| {
match pat.node {
PatKind::Ident(binding_mode, ident, ref sub_pat)
if sub_pat.is_some() || self.is_base_res_local(pat.id) =>
{
binding_map.insert(ident, BindingInfo { span: ident.span, binding_mode });
}
PatKind::Or(ref ps) => {
// Check the consistency of this or-pattern and
// then add all bindings to the larger map.
for bm in self.check_consistent_bindings(ps) {
binding_map.extend(bm);
}
return false;
}
_ => {}
}
true
});
binding_map
}
fn is_base_res_local(&self, nid: NodeId) -> bool {
match self.r.partial_res_map.get(&nid).map(|res| res.base_res()) {
Some(Res::Local(..)) => true,
_ => false,
}
}
/// Checks that all of the arms in an or-pattern have exactly the
/// same set of bindings, with the same binding modes for each.
fn check_consistent_bindings(&mut self, pats: &[P<Pat>]) -> Vec<BindingMap> {
let mut missing_vars = FxHashMap::default();
let mut inconsistent_vars = FxHashMap::default();
// 1) Compute the binding maps of all arms.
let maps = pats.iter()
.map(|pat| self.binding_mode_map(pat))
.collect::<Vec<_>>();
// 2) Record any missing bindings or binding mode inconsistencies.
for (map_outer, pat_outer) in pats.iter().enumerate().map(|(idx, pat)| (&maps[idx], pat)) {
// Check against all arms except for the same pattern which is always self-consistent.
let inners = pats.iter().enumerate()
.filter(|(_, pat)| pat.id != pat_outer.id)
.flat_map(|(idx, _)| maps[idx].iter())
.map(|(key, binding)| (key.name, map_outer.get(&key), binding));
for (name, info, &binding_inner) in inners {
match info {
None => { // The inner binding is missing in the outer.
let binding_error = missing_vars
.entry(name)
.or_insert_with(|| BindingError {
name,
origin: BTreeSet::new(),
target: BTreeSet::new(),
could_be_path: name.as_str().starts_with(char::is_uppercase),
});
binding_error.origin.insert(binding_inner.span);
binding_error.target.insert(pat_outer.span);
}
Some(binding_outer) => {
if binding_outer.binding_mode != binding_inner.binding_mode {
// The binding modes in the outer and inner bindings differ.
inconsistent_vars
.entry(name)
.or_insert((binding_inner.span, binding_outer.span));
}
}
}
}
}
// 3) Report all missing variables we found.
let mut missing_vars = missing_vars.iter_mut().collect::<Vec<_>>();
missing_vars.sort();
for (name, mut v) in missing_vars {
if inconsistent_vars.contains_key(name) {
v.could_be_path = false;
}
self.r.report_error(
*v.origin.iter().next().unwrap(),
ResolutionError::VariableNotBoundInPattern(v));
}
// 4) Report all inconsistencies in binding modes we found.
let mut inconsistent_vars = inconsistent_vars.iter().collect::<Vec<_>>();
inconsistent_vars.sort();
for (name, v) in inconsistent_vars {
self.r.report_error(v.0, ResolutionError::VariableBoundWithDifferentMode(*name, v.1));
}
// 5) Finally bubble up all the binding maps.
maps
}
/// Check the consistency of the outermost or-patterns.
fn check_consistent_bindings_top(&mut self, pat: &Pat) {
pat.walk(&mut |pat| match pat.node {
PatKind::Or(ref ps) => {
self.check_consistent_bindings(ps);
false
},
_ => true,
})
}
fn resolve_arm(&mut self, arm: &Arm) {
self.with_rib(ValueNS, NormalRibKind, |this| {
this.resolve_pattern_top(&arm.pat, PatternSource::Match);
walk_list!(this, visit_expr, &arm.guard);
this.visit_expr(&arm.body);
});
}
/// Arising from `source`, resolve a top level pattern.
fn resolve_pattern_top(&mut self, pat: &Pat, pat_src: PatternSource) {
let mut bindings = smallvec![(PatBoundCtx::Product, Default::default())];
self.resolve_pattern(pat, pat_src, &mut bindings);
}
fn resolve_pattern(
&mut self,
pat: &Pat,
pat_src: PatternSource,
bindings: &mut SmallVec<[(PatBoundCtx, FxHashSet<Ident>); 1]>,
) {
self.resolve_pattern_inner(pat, pat_src, bindings);
// This has to happen *after* we determine which pat_idents are variants:
self.check_consistent_bindings_top(pat);
visit::walk_pat(self, pat);
}
/// Resolve bindings in a pattern. This is a helper to `resolve_pattern`.
///
/// ### `bindings`
///
/// A stack of sets of bindings accumulated.
///
/// In each set, `PatBoundCtx::Product` denotes that a found binding in it should
/// be interpreted as re-binding an already bound binding. This results in an error.
/// Meanwhile, `PatBound::Or` denotes that a found binding in the set should result
/// in reusing this binding rather than creating a fresh one.
///
/// When called at the top level, the stack must have a single element
/// with `PatBound::Product`. Otherwise, pushing to the stack happens as
/// or-patterns (`p_0 | ... | p_n`) are encountered and the context needs
/// to be switched to `PatBoundCtx::Or` and then `PatBoundCtx::Product` for each `p_i`.
/// When each `p_i` has been dealt with, the top set is merged with its parent.
/// When a whole or-pattern has been dealt with, the thing happens.
///
/// See the implementation and `fresh_binding` for more details.
fn resolve_pattern_inner(
&mut self,
pat: &Pat,
pat_src: PatternSource,
bindings: &mut SmallVec<[(PatBoundCtx, FxHashSet<Ident>); 1]>,
) {
// Visit all direct subpatterns of this pattern.
pat.walk(&mut |pat| {
debug!("resolve_pattern pat={:?} node={:?}", pat, pat.node);
match pat.node {
PatKind::Ident(bmode, ident, ref sub) => {
// First try to resolve the identifier as some existing entity,
// then fall back to a fresh binding.
let has_sub = sub.is_some();
let res = self.try_resolve_as_non_binding(pat_src, pat, bmode, ident, has_sub)
.unwrap_or_else(|| self.fresh_binding(ident, pat.id, pat_src, bindings));
self.r.record_partial_res(pat.id, PartialRes::new(res));
}
PatKind::TupleStruct(ref path, ..) => {
self.smart_resolve_path(pat.id, None, path, PathSource::TupleStruct);
}
PatKind::Path(ref qself, ref path) => {
self.smart_resolve_path(pat.id, qself.as_ref(), path, PathSource::Pat);
}
PatKind::Struct(ref path, ..) => {
self.smart_resolve_path(pat.id, None, path, PathSource::Struct);
}
PatKind::Or(ref ps) => {
// Add a new set of bindings to the stack. `Or` here records that when a
// binding already exists in this set, it should not result in an error because
// `V1(a) | V2(a)` must be allowed and are checked for consistency later.
bindings.push((PatBoundCtx::Or, Default::default()));
for p in ps {
// Now we need to switch back to a product context so that each
// part of the or-pattern internally rejects already bound names.
// For example, `V1(a) | V2(a, a)` and `V1(a, a) | V2(a)` are bad.
bindings.push((PatBoundCtx::Product, Default::default()));
self.resolve_pattern_inner(p, pat_src, bindings);
// Move up the non-overlapping bindings to the or-pattern.
// Existing bindings just get "merged".
let collected = bindings.pop().unwrap().1;
bindings.last_mut().unwrap().1.extend(collected);
}
// This or-pattern itself can itself be part of a product,
// e.g. `(V1(a) | V2(a), a)` or `(a, V1(a) | V2(a))`.
// Both cases bind `a` again in a product pattern and must be rejected.
let collected = bindings.pop().unwrap().1;
bindings.last_mut().unwrap().1.extend(collected);
// Prevent visiting `ps` as we've already done so above.
return false;
}
_ => {}
}
true
});
}
fn fresh_binding(
&mut self,
ident: Ident,
pat_id: NodeId,
pat_src: PatternSource,
bindings: &mut SmallVec<[(PatBoundCtx, FxHashSet<Ident>); 1]>,
) -> Res {
// Add the binding to the local ribs, if it doesn't already exist in the bindings map.
// (We must not add it if it's in the bindings map because that breaks the assumptions
// later passes make about or-patterns.)
let ident = ident.modern_and_legacy();
// Walk outwards the stack of products / or-patterns and
// find out if the identifier has been bound in any of these.
let mut already_bound_and = false;
let mut already_bound_or = false;
for (is_sum, set) in bindings.iter_mut().rev() {
match (is_sum, set.get(&ident).cloned()) {
// Already bound in a product pattern, e.g. `(a, a)` which is not allowed.
(PatBoundCtx::Product, Some(..)) => already_bound_and = true,
// Already bound in an or-pattern, e.g. `V1(a) | V2(a)`.
// This is *required* for consistency which is checked later.
(PatBoundCtx::Or, Some(..)) => already_bound_or = true,
// Not already bound here.
_ => {}
}
}
if already_bound_and {
// Overlap in a product pattern somewhere; report an error.
use ResolutionError::*;
let error = match pat_src {
// `fn f(a: u8, a: u8)`:
PatternSource::FnParam => IdentifierBoundMoreThanOnceInParameterList,
// `Variant(a, a)`:
_ => IdentifierBoundMoreThanOnceInSamePattern,
};
self.r.report_error(ident.span, error(&ident.as_str()));
}
// Record as bound if it's valid:
let ident_valid = ident.name != kw::Invalid;
if ident_valid {
bindings.last_mut().unwrap().1.insert(ident);
}
if already_bound_or {
// `Variant1(a) | Variant2(a)`, ok
// Reuse definition from the first `a`.
self.innermost_rib_bindings(ValueNS)[&ident]
} else {
let res = Res::Local(pat_id);
if ident_valid {
// A completely fresh binding add to the set if it's valid.
self.innermost_rib_bindings(ValueNS).insert(ident, res);
}
res
}
}
fn innermost_rib_bindings(&mut self, ns: Namespace) -> &mut IdentMap<Res> {
&mut self.ribs[ns].last_mut().unwrap().bindings
}
fn try_resolve_as_non_binding(
&mut self,
pat_src: PatternSource,
pat: &Pat,
bm: BindingMode,
ident: Ident,
has_sub: bool,
) -> Option<Res> {
let binding = self.resolve_ident_in_lexical_scope(ident, ValueNS, None, pat.span)?.item()?;
let res = binding.res();
// An immutable (no `mut`) by-value (no `ref`) binding pattern without
// a sub pattern (no `@ $pat`) is syntactically ambiguous as it could
// also be interpreted as a path to e.g. a constant, variant, etc.
let is_syntactic_ambiguity = !has_sub && bm == BindingMode::ByValue(Mutability::Immutable);
match res {
Res::Def(DefKind::Ctor(_, CtorKind::Const), _) |
Res::Def(DefKind::Const, _) if is_syntactic_ambiguity => {
// Disambiguate in favor of a unit struct/variant or constant pattern.
self.r.record_use(ident, ValueNS, binding, false);
Some(res)
}
Res::Def(DefKind::Ctor(..), _)
| Res::Def(DefKind::Const, _)
| Res::Def(DefKind::Static, _) => {
// This is unambiguously a fresh binding, either syntactically
// (e.g., `IDENT @ PAT` or `ref IDENT`) or because `IDENT` resolves
// to something unusable as a pattern (e.g., constructor function),
// but we still conservatively report an error, see
// issues/33118#issuecomment-233962221 for one reason why.
self.r.report_error(
ident.span,
ResolutionError::BindingShadowsSomethingUnacceptable(
pat_src.descr(),
ident.name,
binding,
),
);
None
}
Res::Def(DefKind::Fn, _) | Res::Err => {
// These entities are explicitly allowed to be shadowed by fresh bindings.
None
}
res => {
span_bug!(ident.span, "unexpected resolution for an \
identifier in pattern: {:?}", res);
}
}
}

I think another viable approach would be to use some kind of fresh identifiers for the variables (not the id from the pattern node) and then be able to "resolve" each pattern node to that. Or perhaps to have a table of "redirects" where for most bindings the redirect is the identity, but for | patterns they point to the bindings from the first set.

Sounds interesting (but should be done in a follow-up if so). cc @petrochenkov may be interested.

Copy link
Member

left a comment

Looks good overall. I want to check whether https://github.com/rust-lang/rust/pull/64508/files#r324706033 is intentional or not, though.

// }
// ```
//
// We should consider whether we should do something special in nested or-patterns.

This comment has been minimized.

Copy link
@varkor

varkor Sep 16, 2019

Member

Does this mean things are broken with the current set of changes?

This comment has been minimized.

Copy link
@Centril

Centril Sep 16, 2019

Author Member

I'm not sure. It might possibly mean that there's a language design choice. At least, if there is, then consistency (and compiler simplicity) compels us to use the behavior encoded in this PR as opposed to diverging between the top / nested positions.

If I don't do this change then things break in the libproc_macro crate wrt. exhaustiveness checking.

I've mostly left the FIXME for now so I can come back to it once we have the exhaustiveness checking and MIR building in place and the or-patterns feature can be used for real. Evaluating the FIXME is probably easier at that stage. IOW, I think this can be filed under "polish".

/// `match foo() { Some(a) => (), None => () }`.
///
/// When encountering an or-pattern `p_0 | ... | p_n` only `p_0` will be visited.
pub fn each_binding_or_first<F>(&self, c: &mut F)

This comment has been minimized.

Copy link
@varkor

varkor Sep 16, 2019

Member

Is calling each_binding_or_first recursively going to have the right behaviour for nested Or patterns? Shouldn't it pick the first pattern only at the top level? (In which case, you can use each_binding instead and just check the top level at the call site.)

src/librustc/middle/liveness.rs Show resolved Hide resolved
Centril added 2 commits Sep 16, 2019
Also tweak walkers on `Pat`.
@Centril

This comment has been minimized.

Copy link
Member Author

commented Sep 21, 2019

@matthewjasper @varkor Tweaked dead.rs + some walkers. Should be ready for final review now.

@matthewjasper

This comment has been minimized.

Copy link
Contributor

commented Sep 22, 2019

@bors r+

@bors

This comment has been minimized.

Copy link
Contributor

commented Sep 22, 2019

📌 Commit 0918dc4 has been approved by matthewjasper

Centril added a commit to Centril/rust that referenced this pull request Sep 22, 2019
or-patterns: Push `PatKind/PatternKind::Or` at top level to HIR & HAIR

Following up on work in rust-lang#64111, rust-lang#63693, and rust-lang#61708, in this PR:

- We change `hair::Arm.patterns: Vec<Pattern<'_>>` into `hir::Arm.pattern: Pattern<'_>`.

   - `fn hair::Arm::top_pats_hack` is introduced as a temporary crutch in MIR building to avoid more changes.

- We change `hir::Arm.pats: HirVec<P<Pat>>` into `hir::Arm.pat: P<Pat>`.

   - The hacks in `rustc::hir::lowering` are removed since the representation hack is no longer necessary.

   - In some places, `fn hir::Arm::top_pats_hack` is introduced to leave some things as future work.

   - Misc changes: HIR pretty printing is adjusted to behave uniformly wrt. top/inner levels, rvalue promotion is adjusted, regionck, and dead_code is also.

   - Type checking is adjusted to uniformly handle or-patterns at top/inner levels.
      To make things compile, `p_0 | ... | p_n` is redefined as a "reference pattern" in [`fn is_non_ref_pat`](https://doc.rust-lang.org/nightly/nightly-rustc/rustc_typeck/check/struct.FnCtxt.html#method.is_non_ref_pat) for now. This is done so that reference types are not eagerly stripped from the `expected: Ty<'tcx>`.

    - Liveness is adjusted wrt. the `unused_variables` and `unused_assignments` lints to handle top/inner levels uniformly and the handling of `fn` parameters, `let` locals, and `match` arms are unified in this respect. This is not tested for now as exhaustiveness checks are reachable and will ICE.

    - In `check_match`, checking `@` and by-move bindings is adjusted. However, exhaustiveness checking is not adjusted the moment and is handled by @dlrobertson in rust-lang#63688.

    - AST borrowck (`construct.rs`) is not adjusted as AST borrowck will be removed soon.

r? @matthewjasper
cc @dlrobertson @varkor @oli-obk
bors added a commit that referenced this pull request Sep 22, 2019
Rollup of 9 pull requests

Successful merges:

 - #64158 (panic=abort support in libtest)
 - #64294 (Fix `Stdio::piped` example code and lint)
 - #64508 (or-patterns: Push `PatKind/PatternKind::Or` at top level to HIR & HAIR)
 - #64546 (Bugfix/rfc 2451 rerebalance tests)
 - #64670 (Cleanup syntax::ext::build)
 - #64674 (Propagate `types.err` in locals further to avoid spurious knock-down errors)
 - #64676 (Parse assoc type bounds in generic params and provide custom diagnostic)
 - #64677 (remove outdated comment)
 - #64679 (Infer consts more consistently)

Failed merges:

r? @ghost
@bors

This comment has been minimized.

Copy link
Contributor

commented Sep 23, 2019

⌛️ Testing commit 0918dc4 with merge 6c3d391...

bors added a commit that referenced this pull request Sep 23, 2019
or-patterns: Push `PatKind/PatternKind::Or` at top level to HIR & HAIR

Following up on work in #64111, #63693, and #61708, in this PR:

- We change `hair::Arm.patterns: Vec<Pattern<'_>>` into `hir::Arm.pattern: Pattern<'_>`.

   - `fn hair::Arm::top_pats_hack` is introduced as a temporary crutch in MIR building to avoid more changes.

- We change `hir::Arm.pats: HirVec<P<Pat>>` into `hir::Arm.pat: P<Pat>`.

   - The hacks in `rustc::hir::lowering` are removed since the representation hack is no longer necessary.

   - In some places, `fn hir::Arm::top_pats_hack` is introduced to leave some things as future work.

   - Misc changes: HIR pretty printing is adjusted to behave uniformly wrt. top/inner levels, rvalue promotion is adjusted, regionck, and dead_code is also.

   - Type checking is adjusted to uniformly handle or-patterns at top/inner levels.
      To make things compile, `p_0 | ... | p_n` is redefined as a "reference pattern" in [`fn is_non_ref_pat`](https://doc.rust-lang.org/nightly/nightly-rustc/rustc_typeck/check/struct.FnCtxt.html#method.is_non_ref_pat) for now. This is done so that reference types are not eagerly stripped from the `expected: Ty<'tcx>`.

    - Liveness is adjusted wrt. the `unused_variables` and `unused_assignments` lints to handle top/inner levels uniformly and the handling of `fn` parameters, `let` locals, and `match` arms are unified in this respect. This is not tested for now as exhaustiveness checks are reachable and will ICE.

    - In `check_match`, checking `@` and by-move bindings is adjusted. However, exhaustiveness checking is not adjusted the moment and is handled by @dlrobertson in #63688.

    - AST borrowck (`construct.rs`) is not adjusted as AST borrowck will be removed soon.

r? @matthewjasper
cc @dlrobertson @varkor @oli-obk
@bors

This comment was marked as resolved.

Copy link
Contributor

commented Sep 23, 2019

💔 Test failed - checks-azure

@rust-highfive

This comment was marked as resolved.

Copy link
Collaborator

commented Sep 23, 2019

The job x86_64-gnu-tools of your PR failed (pretty log, raw log). Through arcane magic we have determined that the following fragments from the build log may contain information about the problem.

Click to expand the log.
2019-09-23T12:56:25.4047241Z 
2019-09-23T12:56:25.5598174Z error[E0609]: no field `pats` on type `&rustc::hir::Arm`
2019-09-23T12:56:25.5599115Z    --> src/tools/clippy/clippy_lints/src/utils/author.rs:357:82
2019-09-23T12:56:25.5599487Z     |
2019-09-23T12:56:25.5599904Z 357 |                     println!("    if {}[{}].pats.len() == {};", arms_pat, i, arm.pats.len());
2019-09-23T12:56:25.5600451Z 
2019-09-23T12:56:25.5661503Z error[E0609]: no field `pats` on type `&rustc::hir::Arm`
2019-09-23T12:56:25.5661945Z    --> src/tools/clippy/clippy_lints/src/utils/author.rs:358:41
2019-09-23T12:56:25.5662387Z     |
2019-09-23T12:56:25.5662387Z     |
2019-09-23T12:56:25.5662763Z 358 |                     for (j, pat) in arm.pats.iter().enumerate() {
2019-09-23T12:56:25.5663556Z 
2019-09-23T12:56:25.9278213Z error[E0609]: no field `pats` on type `&rustc::hir::Arm`
2019-09-23T12:56:25.9279597Z    --> src/tools/clippy/clippy_lints/src/utils/hir_utils.rs:127:40
2019-09-23T12:56:25.9280002Z     |
---
2019-09-23T12:56:26.0542751Z [RUSTC-TIMING] cargo_metadata test:false 24.201
2019-09-23T12:56:26.5990084Z error[E0609]: no field `pats` on type `&rustc::hir::Arm`
2019-09-23T12:56:26.5990636Z    --> src/tools/clippy/clippy_lints/src/cognitive_complexity.rs:115:61
2019-09-23T12:56:26.5990967Z     |
2019-09-23T12:56:26.5991375Z 115 |                 let arms_n: u64 = arms.iter().map(|arm| arm.pats.len() as u64).sum();
2019-09-23T12:56:26.5991928Z 
2019-09-23T12:56:26.6301265Z error[E0609]: no field `pats` on type `&rustc::hir::Arm`
2019-09-23T12:56:26.6301710Z    --> src/tools/clippy/clippy_lints/src/copies.rs:196:54
2019-09-23T12:56:26.6302016Z     |
---
2019-09-23T12:56:26.6392590Z 
2019-09-23T12:56:26.6409775Z error[E0609]: no field `pats` on type `&rustc::hir::Arm`
2019-09-23T12:56:26.6410176Z    --> src/tools/clippy/clippy_lints/src/copies.rs:217:49
2019-09-23T12:56:26.6410483Z     |
2019-09-23T12:56:26.6410868Z 217 |                         let lhs = snippet(cx, i.pats[0].span, "<pat1>");
2019-09-23T12:56:26.6411349Z 
2019-09-23T12:56:26.6429845Z error[E0609]: no field `pats` on type `&rustc::hir::Arm`
2019-09-23T12:56:26.6430283Z    --> src/tools/clippy/clippy_lints/src/copies.rs:218:49
2019-09-23T12:56:26.6430571Z     |
2019-09-23T12:56:26.6430571Z     |
2019-09-23T12:56:26.6430990Z 218 |                         let rhs = snippet(cx, j.pats[0].span, "<pat2>");
2019-09-23T12:56:26.6431475Z 
2019-09-23T12:56:26.6449595Z error[E0609]: no field `pats` on type `&rustc::hir::Arm`
2019-09-23T12:56:26.6450027Z    --> src/tools/clippy/clippy_lints/src/copies.rs:220:50
2019-09-23T12:56:26.6450335Z     |
---
2019-09-23T12:56:27.0756638Z 
2019-09-23T12:56:27.0783036Z error[E0609]: no field `pats` on type `rustc::hir::Arm`
2019-09-23T12:56:27.0783462Z   --> src/tools/clippy/clippy_lints/src/infallible_destructuring_match.rs:51:105
2019-09-23T12:56:27.0783798Z    |
2019-09-23T12:56:27.0784231Z 51 |             if let PatKind::TupleStruct(QPath::Resolved(None, ref variant_name), ref args, _) = arms[0].pats[0].node;
2019-09-23T12:56:27.0784896Z 
2019-09-23T12:56:27.3545921Z error[E0609]: no field `pats` on type `rustc::hir::Arm`
2019-09-23T12:56:27.3546567Z    --> src/tools/clippy/clippy_lints/src/loops.rs:520:44
2019-09-23T12:56:27.3546877Z     |
---
2019-09-23T12:56:27.3575045Z 
2019-09-23T12:56:27.3611497Z error[E0609]: no field `pats` on type `rustc::hir::Arm`
2019-09-23T12:56:27.3612335Z    --> src/tools/clippy/clippy_lints/src/loops.rs:544:80
2019-09-23T12:56:27.3612709Z     |
2019-09-23T12:56:27.3613116Z 544 | ...                   snippet_with_applicability(cx, arms[0].pats[0].span, "..", &mut applicability),
2019-09-23T12:56:27.3613714Z 
2019-09-23T12:56:27.3645864Z error[E0609]: no field `pats` on type `rustc::hir::Arm`
2019-09-23T12:56:27.3646327Z    --> src/tools/clippy/clippy_lints/src/loops.rs:557:32
2019-09-23T12:56:27.3646583Z     |
---
2019-09-23T12:56:27.6540303Z 
2019-09-23T12:56:27.6566193Z error[E0609]: no field `pats` on type `rustc::hir::Arm`
2019-09-23T12:56:27.6566563Z    --> src/tools/clippy/clippy_lints/src/matches.rs:370:77
2019-09-23T12:56:27.6566848Z     |
2019-09-23T12:56:27.6567241Z 370 |                     let exprs = if let PatKind::Lit(ref arm_bool) = arms[0].pats[0].node {
2019-09-23T12:56:27.6567884Z 
2019-09-23T12:56:27.6719476Z error[E0609]: no field `pats` on type `&rustc::hir::Arm`
2019-09-23T12:56:27.6719998Z    --> src/tools/clippy/clippy_lints/src/matches.rs:449:71
2019-09-23T12:56:27.6720312Z     |
---
2019-09-23T12:56:27.6884411Z 
2019-09-23T12:56:27.7031651Z error[E0609]: no field `pats` on type `&rustc::hir::Arm`
2019-09-23T12:56:27.7032086Z    --> src/tools/clippy/clippy_lints/src/matches.rs:591:50
2019-09-23T12:56:27.7032534Z     |
2019-09-23T12:56:27.7032912Z 591 |         suggs.extend(arms.iter().flat_map(|a| &a.pats).filter_map(|p| {
2019-09-23T12:56:27.7033364Z 
2019-09-23T12:56:27.7092447Z error[E0609]: no field `pats` on type `rustc::hir::Arm`
2019-09-23T12:56:27.7092854Z    --> src/tools/clippy/clippy_lints/src/matches.rs:609:20
2019-09-23T12:56:27.7093114Z     |
---
2019-09-23T12:56:27.7123287Z 
2019-09-23T12:56:27.7198997Z error[E0026]: struct `rustc::hir::Arm` does not have a field named `pats`
2019-09-23T12:56:27.7199449Z    --> src/tools/clippy/clippy_lints/src/matches.rs:669:21
2019-09-23T12:56:27.7199733Z     |
2019-09-23T12:56:27.7200091Z 669 |                 ref pats, guard: None, ..
2019-09-23T12:56:27.7200780Z     |                     |
2019-09-23T12:56:27.7201191Z     |                     struct `rustc::hir::Arm` does not have this field
2019-09-23T12:56:27.7201860Z     |                     help: a field with a similar name exists: `pat`
2019-09-23T12:56:27.7201967Z 
---
2019-09-23T12:56:27.7434854Z 
2019-09-23T12:56:28.4409517Z error[E0609]: no field `pats` on type `rustc::hir::Arm`
2019-09-23T12:56:28.4411641Z   --> src/tools/clippy/clippy_lints/src/ok_if_let.rs:45:89
2019-09-23T12:56:28.4411994Z    |
2019-09-23T12:56:28.4412487Z 45 |             if let PatKind::TupleStruct(QPath::Resolved(_, ref x), ref y, _)  = body[0].pats[0].node; //get operation
2019-09-23T12:56:28.4413293Z 
2019-09-23T12:56:28.6246549Z error[E0609]: no field `pats` on type `rustc::hir::Arm`
2019-09-23T12:56:28.6247101Z   --> src/tools/clippy/clippy_lints/src/redundant_pattern_matching.rs:60:16
2019-09-23T12:56:28.6247411Z    |
---
2019-09-23T12:56:30.9150478Z 
2019-09-23T12:56:31.0624160Z error[E0609]: no field `pats` on type `&rustc::hir::Arm`
2019-09-23T12:56:31.0624651Z    --> src/tools/clippy/clippy_lints/src/utils/author.rs:357:82
2019-09-23T12:56:31.0624921Z     |
2019-09-23T12:56:31.0625346Z 357 |                     println!("    if {}[{}].pats.len() == {};", arms_pat, i, arm.pats.len());
2019-09-23T12:56:31.0626020Z 
2019-09-23T12:56:31.0649278Z error[E0609]: no field `pats` on type `&rustc::hir::Arm`
2019-09-23T12:56:31.0649697Z    --> src/tools/clippy/clippy_lints/src/utils/author.rs:358:41
2019-09-23T12:56:31.0649997Z     |
2019-09-23T12:56:31.0649997Z     |
2019-09-23T12:56:31.0650753Z 358 |                     for (j, pat) in arm.pats.iter().enumerate() {
2019-09-23T12:56:31.0651223Z 
2019-09-23T12:56:31.4403986Z error[E0609]: no field `pats` on type `&rustc::hir::Arm`
2019-09-23T12:56:31.4405685Z    --> src/tools/clippy/clippy_lints/src/utils/hir_utils.rs:127:40
2019-09-23T12:56:31.4406349Z     |
---
2019-09-23T12:56:31.5431638Z 
2019-09-23T12:56:32.0925446Z error[E0609]: no field `pats` on type `&rustc::hir::Arm`
2019-09-23T12:56:32.0926022Z    --> src/tools/clippy/clippy_lints/src/cognitive_complexity.rs:115:61
2019-09-23T12:56:32.0926680Z     |
2019-09-23T12:56:32.0927065Z 115 |                 let arms_n: u64 = arms.iter().map(|arm| arm.pats.len() as u64).sum();
2019-09-23T12:56:32.0927568Z 
2019-09-23T12:56:32.1228271Z error[E0609]: no field `pats` on type `&rustc::hir::Arm`
2019-09-23T12:56:32.1228796Z    --> src/tools/clippy/clippy_lints/src/copies.rs:196:54
2019-09-23T12:56:32.1229135Z     |
---
2019-09-23T12:56:32.1314911Z 
2019-09-23T12:56:32.1342514Z error[E0609]: no field `pats` on type `&rustc::hir::Arm`
2019-09-23T12:56:32.1342991Z    --> src/tools/clippy/clippy_lints/src/copies.rs:217:49
2019-09-23T12:56:32.1343290Z     |
2019-09-23T12:56:32.1343679Z 217 |                         let lhs = snippet(cx, i.pats[0].span, "<pat1>");
2019-09-23T12:56:32.1344187Z 
2019-09-23T12:56:32.1363536Z error[E0609]: no field `pats` on type `&rustc::hir::Arm`
2019-09-23T12:56:32.1364258Z    --> src/tools/clippy/clippy_lints/src/copies.rs:218:49
2019-09-23T12:56:32.1364552Z     |
2019-09-23T12:56:32.1364552Z     |
2019-09-23T12:56:32.1364927Z 218 |                         let rhs = snippet(cx, j.pats[0].span, "<pat2>");
2019-09-23T12:56:32.1365388Z 
2019-09-23T12:56:32.1385467Z error[E0609]: no field `pats` on type `&rustc::hir::Arm`
2019-09-23T12:56:32.1385883Z    --> src/tools/clippy/clippy_lints/src/copies.rs:220:50
2019-09-23T12:56:32.1386157Z     |
---
2019-09-23T12:56:32.5922688Z 
2019-09-23T12:56:32.5939851Z error[E0609]: no field `pats` on type `rustc::hir::Arm`
2019-09-23T12:56:32.5940300Z   --> src/tools/clippy/clippy_lints/src/infallible_destructuring_match.rs:51:105
2019-09-23T12:56:32.5940654Z    |
2019-09-23T12:56:32.5941120Z 51 |             if let PatKind::TupleStruct(QPath::Resolved(None, ref variant_name), ref args, _) = arms[0].pats[0].node;
2019-09-23T12:56:32.5941844Z 
2019-09-23T12:56:32.8686480Z error[E0609]: no field `pats` on type `rustc::hir::Arm`
2019-09-23T12:56:32.8687027Z    --> src/tools/clippy/clippy_lints/src/loops.rs:520:44
2019-09-23T12:56:32.8687337Z     |
---
2019-09-23T12:56:32.8709042Z 
2019-09-23T12:56:32.8742583Z error[E0609]: no field `pats` on type `rustc::hir::Arm`
2019-09-23T12:56:32.8743071Z    --> src/tools/clippy/clippy_lints/src/loops.rs:544:80
2019-09-23T12:56:32.8743407Z     |
2019-09-23T12:56:32.8743871Z 544 | ...                   snippet_with_applicability(cx, arms[0].pats[0].span, "..", &mut applicability),
2019-09-23T12:56:32.8744549Z 
2019-09-23T12:56:32.8773741Z error[E0609]: no field `pats` on type `rustc::hir::Arm`
2019-09-23T12:56:32.8774362Z    --> src/tools/clippy/clippy_lints/src/loops.rs:557:32
2019-09-23T12:56:32.8774672Z     |
---
2019-09-23T12:56:33.1669332Z 
2019-09-23T12:56:33.1686181Z error[E0609]: no field `pats` on type `rustc::hir::Arm`
2019-09-23T12:56:33.1686617Z    --> src/tools/clippy/clippy_lints/src/matches.rs:370:77
2019-09-23T12:56:33.1686921Z     |
2019-09-23T12:56:33.1687329Z 370 |                     let exprs = if let PatKind::Lit(ref arm_bool) = arms[0].pats[0].node {
2019-09-23T12:56:33.1692841Z 
2019-09-23T12:56:33.1822869Z error[E0609]: no field `pats` on type `&rustc::hir::Arm`
2019-09-23T12:56:33.1823295Z    --> src/tools/clippy/clippy_lints/src/matches.rs:449:71
2019-09-23T12:56:33.1823572Z     |
---
2019-09-23T12:56:33.1952806Z 
2019-09-23T12:56:33.2096425Z error[E0609]: no field `pats` on type `&rustc::hir::Arm`
2019-09-23T12:56:33.2096827Z    --> src/tools/clippy/clippy_lints/src/matches.rs:591:50
2019-09-23T12:56:33.2097139Z     |
2019-09-23T12:56:33.2097541Z 591 |         suggs.extend(arms.iter().flat_map(|a| &a.pats).filter_map(|p| {
2019-09-23T12:56:33.2098281Z 
2019-09-23T12:56:33.2152199Z error[E0609]: no field `pats` on type `rustc::hir::Arm`
2019-09-23T12:56:33.2152640Z    --> src/tools/clippy/clippy_lints/src/matches.rs:609:20
2019-09-23T12:56:33.2152943Z     |
---
2019-09-23T12:56:33.2179208Z 
2019-09-23T12:56:33.2241610Z error[E0026]: struct `rustc::hir::Arm` does not have a field named `pats`
2019-09-23T12:56:33.2242044Z    --> src/tools/clippy/clippy_lints/src/matches.rs:669:21
2019-09-23T12:56:33.2242513Z     |
2019-09-23T12:56:33.2243114Z 669 |                 ref pats, guard: None, ..
2019-09-23T12:56:33.2243800Z     |                     |
2019-09-23T12:56:33.2244467Z     |                     struct `rustc::hir::Arm` does not have this field
2019-09-23T12:56:33.2244990Z     |                     help: a field with a similar name exists: `pat`
2019-09-23T12:56:33.2250516Z 
---
2019-09-23T12:56:33.2489580Z 
2019-09-23T12:56:33.9479739Z error[E0609]: no field `pats` on type `rustc::hir::Arm`
2019-09-23T12:56:33.9480222Z   --> src/tools/clippy/clippy_lints/src/ok_if_let.rs:45:89
2019-09-23T12:56:33.9480559Z    |
2019-09-23T12:56:33.9481417Z 45 |             if let PatKind::TupleStruct(QPath::Resolved(_, ref x), ref y, _)  = body[0].pats[0].node; //get operation
2019-09-23T12:56:33.9482444Z 
2019-09-23T12:56:34.1242305Z error[E0609]: no field `pats` on type `rustc::hir::Arm`
2019-09-23T12:56:34.1243077Z   --> src/tools/clippy/clippy_lints/src/redundant_pattern_matching.rs:60:16
2019-09-23T12:56:34.1243465Z    |
---
2019-09-23T13:43:03.2530367Z == clock drift check ==
2019-09-23T13:43:03.2543833Z   local time: Mon Sep 23 13:43:03 UTC 2019
2019-09-23T13:43:03.3254202Z   network time: Mon, 23 Sep 2019 13:43:03 GMT
2019-09-23T13:43:03.3260122Z == end clock drift check ==
2019-09-23T13:43:04.0187603Z ##[error]Bash exited with code '1'.
2019-09-23T13:43:04.0226579Z ##[section]Starting: Upload CPU usage statistics
2019-09-23T13:43:04.0233304Z ==============================================================================
2019-09-23T13:43:04.0233428Z Task         : Bash
2019-09-23T13:43:04.0233523Z Description  : Run a Bash script on macOS, Linux, or Windows

I'm a bot! I can only do what humans tell me to, so if this was not helpful or you have suggestions for improvements, please ping or otherwise contact @TimNN. (Feature Requests)

@Centril

This comment has been minimized.

Copy link
Member Author

commented Sep 23, 2019

Triage: Blocking on toolstate breakage-ban being lifted in a few days.

@Centril

This comment has been minimized.

Copy link
Member Author

commented Sep 25, 2019

@bors retry can break toolstate again.

@bors bors added S-waiting-on-bors and removed S-blocked labels Sep 25, 2019
Centril added a commit to Centril/rust that referenced this pull request Sep 25, 2019
or-patterns: Push `PatKind/PatternKind::Or` at top level to HIR & HAIR

Following up on work in rust-lang#64111, rust-lang#63693, and rust-lang#61708, in this PR:

- We change `hair::Arm.patterns: Vec<Pattern<'_>>` into `hir::Arm.pattern: Pattern<'_>`.

   - `fn hair::Arm::top_pats_hack` is introduced as a temporary crutch in MIR building to avoid more changes.

- We change `hir::Arm.pats: HirVec<P<Pat>>` into `hir::Arm.pat: P<Pat>`.

   - The hacks in `rustc::hir::lowering` are removed since the representation hack is no longer necessary.

   - In some places, `fn hir::Arm::top_pats_hack` is introduced to leave some things as future work.

   - Misc changes: HIR pretty printing is adjusted to behave uniformly wrt. top/inner levels, rvalue promotion is adjusted, regionck, and dead_code is also.

   - Type checking is adjusted to uniformly handle or-patterns at top/inner levels.
      To make things compile, `p_0 | ... | p_n` is redefined as a "reference pattern" in [`fn is_non_ref_pat`](https://doc.rust-lang.org/nightly/nightly-rustc/rustc_typeck/check/struct.FnCtxt.html#method.is_non_ref_pat) for now. This is done so that reference types are not eagerly stripped from the `expected: Ty<'tcx>`.

    - Liveness is adjusted wrt. the `unused_variables` and `unused_assignments` lints to handle top/inner levels uniformly and the handling of `fn` parameters, `let` locals, and `match` arms are unified in this respect. This is not tested for now as exhaustiveness checks are reachable and will ICE.

    - In `check_match`, checking `@` and by-move bindings is adjusted. However, exhaustiveness checking is not adjusted the moment and is handled by @dlrobertson in rust-lang#63688.

    - AST borrowck (`construct.rs`) is not adjusted as AST borrowck will be removed soon.

r? @matthewjasper
cc @dlrobertson @varkor @oli-obk
bors added a commit that referenced this pull request Sep 25, 2019
Rollup of 6 pull requests

Successful merges:

 - #62975 (Almost fully deprecate hir::map::Map.hir_to_node_id)
 - #64386 (use `sign` variable in abs and wrapping_abs methods)
 - #64508 (or-patterns: Push `PatKind/PatternKind::Or` at top level to HIR & HAIR)
 - #64738 (Add const-eval support for SIMD types, insert, and extract)
 - #64759 (Refactor mbe a tiny bit)
 - #64764 (Master is now 1.40 )

Failed merges:

r? @ghost
@bors bors merged commit 0918dc4 into rust-lang:master Sep 25, 2019
4 of 5 checks passed
4 of 5 checks passed
homu Test failed
Details
pr Build #20190921.21 succeeded
Details
pr (Linux mingw-check) Linux mingw-check succeeded
Details
pr (Linux x86_64-gnu-llvm-6.0) Linux x86_64-gnu-llvm-6.0 succeeded
Details
pr (LinuxTools) LinuxTools succeeded
Details
@Centril Centril deleted the Centril:or-pat-hir branch Sep 25, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
6 participants
You can’t perform that action at this time.