Skip to content

Commit

Permalink
Auto merge of rust-lang#117726 - eggyal:automatically_make_trivial_ty…
Browse files Browse the repository at this point in the history
…pes_noop-traversable, r=<try>

Automatically make trivial types noop-traversable

This is a second reincarnation of rust-lang#108214 (or at least, what that PR ultimately became), the previous reincarnation in rust-lang#117620 having been closed accidentally.  It may be easier to review by commit, albeit there are quite a few of them.

## Terminology

I refer to:
* folds and visits as "traversals"
* the types that are folded or visited as "traversables"
* the folders and visitors as "traversers"
* traversables on which traversers can act as "interesting" (there are currently only five: binders, types, consts, regions and predicates) and all others as "trivial" (traversals of which are no-ops)
* the `TypeFoldable` and `TypeVisitable` traits (and only these traits) as the "traversable traits"

## Content

This PR:

* introduces a macro, `rustc_type_ir::noop_if_trivially_traversable` that uses [auto-deref specialisation](http://lukaskalbertodt.github.io/2019/12/05/generalized-autoref-based-specialization.html) to:
    * perform a no-op traversal on values of type `T` if the interner implements `rustc_type_ir::TriviallyTraverses<T>` (thereby obviating all need for most trivial types to implement the traversable traits—and indeed such implementations are removed) or
    * delegate to the relevant traversable trait implementation on `T` otherwise;
* introduces an auto-trait, `rustc_middle::ty::TriviallyTraversable`, that is then "unimplemented" on the interesting types (and thereby remains implemented only on, but on all, trivial types);
* implements `rustc_type_ir::TriviallyTraverses<T>` on the `TyCtxt<'tcx>` interner for all `T: rustc_middle::ty::TriviallyTraversable` (thus ensuring that, for that interner at least, trivial types do not require implementations of the traversable traits);
* updates the traversable traits' derive macros to:
    * skip fields that reference neither any generic type parameters nor, if present, the `'tcx` lifetime parameter
    * use the above specialisation macro when traversing fields
    * if the traversable is not parameterised by a `'tcx` lifetime, generate implementations that are generic over the interner;
* replaces those derive macros' distinct helper attributes (introduced in rust-lang#108040) with a unified `#[skip_traversal]` helper attribute that requires a justifying reason to be provided (this does mean that the different types of traversal can no longer be independently skipped, but nowhere is that currently required or ever expected to be);
    * the derive macros by default refuse to be implemented on trivial types as specialisation usually negates any need—but this can be overridden with a `#[skip_traversal(impl_despite_trivial_because = "<reason>")]` attribute;
* uses those derive macros in place of remaining usages of the `TrivialTypeTraversal` macros and some explicit implementations; and
* a few other minor relevant refactorings.

## Commentary

One limitation of auto-deref specialisation is that it cannot work for unconstrained generic types.  Therefore, absent real specialisation, implementations of the traversable traits must constrain their generics:
* by default, the derive macros constrain every field type `T` that references one or more generic type parameters to implementors of the relevant traversable trait—however this can be modified on a field (or variant) basis with a `#[skip_traversal(because_trivial)]` attribute so that instead the interner is constrained to implement `TriviallyTraverses<T>` (and the field is not traversed)
    * the constraints are applied to the field types rather than the generic type parameters to achieve a "[perfect derive](https://smallcultfollowing.com/babysteps/blog/2022/04/12/implied-bounds-and-perfect-derive/)" that not only avoids having to propagate `#[skip_traversal]` annotations into wrapping types but also works with associated types and other esoteric situations—however this could result in trait solver cycles if interesting generic field types are one day involved in recursive type definitions;
    * for exceptionally rare cases where traversal of an item/variant/field should be a no-op despite it being (potentially, if generic) interesting, it can be annotated with the explicit and deliberately cumbersome `#[skip_traversal(despite_potential_miscompilation_because = "<reason>")]` (which produces a no-op without adding any constraint).
* (the few remaining) explicit implementations of the traversable traits over generic types are similarly constrained
    * indeed, for generic tuples *all element types* must implement the trait—however, since (most) trivial types no longer do, this unfortunately means that the implementations are not applicable to tuples with trivial elements and hence some such tuples have been replaced with more concrete newtypes that have appropriate constraints: c5e9447 is a particularly hefty example of this (where tuples containing `Span` are replaced with `Spanned<T>` instead).

r? `@lcnr`
cc `@oli-obk`

`@rustbot` label A-query-system T-compiler WG-trait-system-refactor
  • Loading branch information
bors committed Nov 9, 2023
2 parents 492e57c + 37bab7a commit 793a8bf
Show file tree
Hide file tree
Showing 163 changed files with 1,881 additions and 1,231 deletions.
6 changes: 6 additions & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3918,6 +3918,7 @@ dependencies = [
"rustc_serialize",
"rustc_span",
"rustc_target",
"rustc_type_ir",
"smallvec",
"tracing",
]
Expand Down Expand Up @@ -4032,6 +4033,7 @@ dependencies = [
"rustc_serialize",
"rustc_span",
"rustc_target",
"rustc_type_ir",
"smallvec",
"tracing",
]
Expand Down Expand Up @@ -4531,6 +4533,7 @@ dependencies = [
"rustc_index",
"rustc_macros",
"rustc_serialize",
"rustc_type_ir",
"scoped-tls",
"sha1",
"sha2",
Expand Down Expand Up @@ -4570,6 +4573,7 @@ dependencies = [
"rustc_macros",
"rustc_serialize",
"rustc_span",
"rustc_type_ir",
"serde_json",
"tracing",
]
Expand Down Expand Up @@ -4601,6 +4605,7 @@ dependencies = [
"rustc_span",
"rustc_target",
"rustc_transmute",
"rustc_type_ir",
"smallvec",
"tracing",
]
Expand Down Expand Up @@ -4630,6 +4635,7 @@ dependencies = [
"rustc_middle",
"rustc_span",
"rustc_target",
"rustc_type_ir",
"tracing",
]

Expand Down
19 changes: 10 additions & 9 deletions compiler/rustc_borrowck/src/diagnostics/conflict_errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ use rustc_middle::hir::nested_filter::OnlyBodies;
use rustc_middle::mir::tcx::PlaceTy;
use rustc_middle::mir::{
self, AggregateKind, BindingForm, BorrowKind, CallSource, ClearCrossCrate, ConstraintCategory,
FakeReadCause, LocalDecl, LocalInfo, LocalKind, Location, MutBorrowKind, Operand, Place,
PlaceRef, ProjectionElem, Rvalue, Statement, StatementKind, Terminator, TerminatorKind,
VarBindingForm,
FakeReadCause, FakeReadCauseAndPlace, LocalDecl, LocalInfo, LocalKind, Location, MutBorrowKind,
Operand, Place, PlaceRef, ProjectionElem, Rvalue, Statement, StatementKind, Terminator,
TerminatorKind, VarBindingForm,
};
use rustc_middle::ty::{self, suggest_constraining_type_params, PredicateKind, Ty};
use rustc_middle::util::CallKind;
Expand Down Expand Up @@ -671,8 +671,8 @@ impl<'cx, 'tcx> MirBorrowckCtxt<'cx, 'tcx> {
let tcx = self.infcx.tcx;

// Find out if the predicates show that the type is a Fn or FnMut
let find_fn_kind_from_did = |(pred, _): (ty::Clause<'tcx>, _)| {
if let ty::ClauseKind::Trait(pred) = pred.kind().skip_binder()
let find_fn_kind_from_did = |pred: ty::Spanned<ty::Clause<'tcx>>| {
if let ty::ClauseKind::Trait(pred) = pred.node.kind().skip_binder()
&& pred.self_ty() == ty
{
if Some(pred.def_id()) == tcx.lang_items().fn_trait() {
Expand All @@ -698,7 +698,7 @@ impl<'cx, 'tcx> MirBorrowckCtxt<'cx, 'tcx> {
ty::Alias(ty::Opaque, ty::AliasTy { def_id, args, .. }) => tcx
.explicit_item_bounds(def_id)
.iter_instantiated_copied(tcx, args)
.find_map(|(clause, span)| find_fn_kind_from_did((clause, span))),
.find_map(find_fn_kind_from_did),
ty::Closure(_, args) => match args.as_closure().kind() {
ty::ClosureKind::Fn => Some(hir::Mutability::Not),
ty::ClosureKind::FnMut => Some(hir::Mutability::Mut),
Expand Down Expand Up @@ -3010,9 +3010,10 @@ impl<'cx, 'tcx> MirBorrowckCtxt<'cx, 'tcx> {
impl<'tcx> Visitor<'tcx> for FakeReadCauseFinder<'tcx> {
fn visit_statement(&mut self, statement: &Statement<'tcx>, _: Location) {
match statement {
Statement { kind: StatementKind::FakeRead(box (cause, place)), .. }
if *place == self.place =>
{
Statement {
kind: StatementKind::FakeRead(box FakeReadCauseAndPlace(cause, place)),
..
} if *place == self.place => {
self.cause = Some(*cause);
}
_ => (),
Expand Down
10 changes: 7 additions & 3 deletions compiler/rustc_borrowck/src/diagnostics/explain_borrow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ use rustc_hir::intravisit::Visitor;
use rustc_index::IndexSlice;
use rustc_infer::infer::NllRegionVariableOrigin;
use rustc_middle::mir::{
Body, CallSource, CastKind, ConstraintCategory, FakeReadCause, Local, LocalInfo, Location,
Operand, Place, Rvalue, Statement, StatementKind, TerminatorKind,
Body, CallSource, CastKind, ConstraintCategory, FakeReadCause, FakeReadCauseAndPlace, Local,
LocalInfo, Location, Operand, Place, Rvalue, Statement, StatementKind, TerminatorKind,
};
use rustc_middle::ty::adjustment::PointerCoercion;
use rustc_middle::ty::{self, RegionVid, TyCtxt};
Expand Down Expand Up @@ -477,7 +477,11 @@ impl<'cx, 'tcx> MirBorrowckCtxt<'cx, 'tcx> {
let block = &self.body.basic_blocks[location.block];

let kind = if let Some(&Statement {
kind: StatementKind::FakeRead(box (FakeReadCause::ForLet(_), place)),
kind:
StatementKind::FakeRead(box FakeReadCauseAndPlace(
FakeReadCause::ForLet(_),
place,
)),
..
}) = block.statements.get(location.statement_index)
{
Expand Down
8 changes: 4 additions & 4 deletions compiler/rustc_borrowck/src/diagnostics/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ use rustc_index::IndexSlice;
use rustc_infer::infer::LateBoundRegionConversionTime;
use rustc_middle::mir::tcx::PlaceTy;
use rustc_middle::mir::{
AggregateKind, CallSource, ConstOperand, FakeReadCause, Local, LocalInfo, LocalKind, Location,
Operand, Place, PlaceRef, ProjectionElem, Rvalue, Statement, StatementKind, Terminator,
TerminatorKind,
AggregateKind, CallSource, ConstOperand, FakeReadCause, FakeReadCauseAndPlace, Local,
LocalInfo, LocalKind, Location, Operand, Place, PlaceRef, ProjectionElem, Rvalue, Statement,
StatementKind, Terminator, TerminatorKind,
};
use rustc_middle::ty::print::Print;
use rustc_middle::ty::{self, Instance, Ty, TyCtxt};
Expand Down Expand Up @@ -797,7 +797,7 @@ impl<'cx, 'tcx> MirBorrowckCtxt<'cx, 'tcx> {

// StatementKind::FakeRead only contains a def_id if they are introduced as a result
// of pattern matching within a closure.
if let StatementKind::FakeRead(box (cause, place)) = stmt.kind {
if let StatementKind::FakeRead(box FakeReadCauseAndPlace(cause, place)) = stmt.kind {
match cause {
FakeReadCause::ForMatchedPlace(Some(closure_def_id))
| FakeReadCause::ForLet(Some(closure_def_id)) => {
Expand Down
6 changes: 3 additions & 3 deletions compiler/rustc_borrowck/src/diagnostics/move_errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ impl<'a, 'tcx> MirBorrowckCtxt<'a, 'tcx> {
// opt_match_place is None for let [mut] x = ... statements,
// whether or not the right-hand side is a place expression
if let LocalInfo::User(BindingForm::Var(VarBindingForm {
opt_match_place: Some((opt_match_place, match_span)),
opt_match_place: Some(opt_match_place),
binding_mode: _,
opt_ty_info: _,
pat_span: _,
Expand All @@ -143,8 +143,8 @@ impl<'a, 'tcx> MirBorrowckCtxt<'a, 'tcx> {
original_path,
*move_from,
local,
opt_match_place,
match_span,
opt_match_place.node,
opt_match_place.span,
stmt_source_info.span,
);
return;
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_borrowck/src/invalidation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ impl<'cx, 'tcx> Visitor<'tcx> for InvalidationGenerator<'cx, 'tcx> {

self.mutate_place(location, *lhs, Shallow(None));
}
StatementKind::FakeRead(box (_, _)) => {
StatementKind::FakeRead(_) => {
// Only relevant for initialized/liveness/safety checks.
}
StatementKind::Intrinsic(box NonDivergingIntrinsic::Assume(op)) => {
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_borrowck/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -629,7 +629,7 @@ impl<'cx, 'tcx, R> rustc_mir_dataflow::ResultsVisitor<'cx, 'tcx, R> for MirBorro

self.mutate_place(location, (*lhs, span), Shallow(None), flow_state);
}
StatementKind::FakeRead(box (_, place)) => {
StatementKind::FakeRead(box FakeReadCauseAndPlace(_, place)) => {
// Read for match doesn't access any memory and is used to
// assert that a place is safe and live. So we don't have to
// do any checks here.
Expand Down
8 changes: 4 additions & 4 deletions compiler/rustc_borrowck/src/type_check/canonical.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,10 @@ impl<'a, 'tcx> TypeChecker<'a, 'tcx> {
instantiated_predicates: ty::InstantiatedPredicates<'tcx>,
locations: Locations,
) {
for (predicate, span) in instantiated_predicates {
debug!(?predicate);
let category = ConstraintCategory::Predicate(span);
let predicate = self.normalize_with_category(predicate, locations, category);
for predicate in instantiated_predicates {
debug!(?predicate.node);
let category = ConstraintCategory::Predicate(predicate.span);
let predicate = self.normalize_with_category(predicate.node, locations, category);
self.prove_predicate(predicate, locations, category);
}
}
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_borrowck/src/type_check/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1297,7 +1297,7 @@ impl<'a, 'tcx> TypeChecker<'a, 'tcx> {
);
}
}
StatementKind::AscribeUserType(box (place, projection), variance) => {
StatementKind::AscribeUserType(box AscribeUserType(place, projection), variance) => {
let place_ty = place.ty(body, tcx).ty;
if let Err(terr) = self.relate_type_and_user_type(
place_ty,
Expand Down
10 changes: 5 additions & 5 deletions compiler/rustc_codegen_llvm/src/debuginfo/create_scope_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,13 @@ fn make_mir_scope<'ll, 'tcx>(
let file_metadata = file_metadata(cx, &loc.file);

let parent_dbg_scope = match scope_data.inlined {
Some((callee, _)) => {
Some(callee) => {
// FIXME(eddyb) this would be `self.monomorphize(&callee)`
// if this is moved to `rustc_codegen_ssa::mir::debuginfo`.
let callee = cx.tcx.instantiate_and_normalize_erasing_regions(
instance.args,
ty::ParamEnv::reveal_all(),
ty::EarlyBinder::bind(callee),
ty::EarlyBinder::bind(callee.node),
);
debug_context.inlined_function_scopes.entry(callee).or_insert_with(|| {
let callee_fn_abi = cx.fn_abi_of_instance(callee, ty::List::empty());
Expand All @@ -116,11 +116,11 @@ fn make_mir_scope<'ll, 'tcx>(
)
};

let inlined_at = scope_data.inlined.map(|(_, callsite_span)| {
let inlined_at = scope_data.inlined.map(|callee| {
// FIXME(eddyb) this doesn't account for the macro-related
// `Span` fixups that `rustc_codegen_ssa::mir::debuginfo` does.
let callsite_scope = parent_scope.adjust_dbg_scope_for_span(cx, callsite_span);
cx.dbg_loc(callsite_scope, parent_scope.inlined_at, callsite_span)
let callsite_scope = parent_scope.adjust_dbg_scope_for_span(cx, callee.span);
cx.dbg_loc(callsite_scope, parent_scope.inlined_at, callee.span)
});

debug_context.scopes[scope] = DebugScope {
Expand Down
6 changes: 3 additions & 3 deletions compiler/rustc_const_eval/src/interpret/eval_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1165,9 +1165,9 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
// If the stacktrace passes through MIR-inlined source scopes, add them.
let mir::SourceInfo { mut span, scope } = *frame.body.source_info(loc);
let mut scope_data = &frame.body.source_scopes[scope];
while let Some((instance, call_span)) = scope_data.inlined {
frames.push(FrameInfo { span, instance });
span = call_span;
while let Some(instance) = scope_data.inlined {
frames.push(FrameInfo { span, instance: instance.node });
span = instance.span;
scope_data = &frame.body.source_scopes[scope_data.parent_scope.unwrap()];
}
span
Expand Down
4 changes: 4 additions & 0 deletions compiler/rustc_data_structures/src/intern.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ mod private {
#[rustc_pass_by_value]
pub struct Interned<'a, T>(pub &'a T, pub private::PrivateZst);

pub trait Internable<'a, I>: Sized {
fn intern(self, interner: I) -> Interned<'a, Self>;
}

impl<'a, T> Interned<'a, T> {
/// Create a new `Interned` value. The value referred to *must* be interned
/// and thus be unique, and it *must* remain unique in the future. This
Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_hir/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ rustc_macros = { path = "../rustc_macros" }
rustc_serialize = { path = "../rustc_serialize" }
rustc_span = { path = "../rustc_span" }
rustc_target = { path = "../rustc_target" }
rustc_type_ir = { path = "../rustc_type_ir" }
smallvec = { version = "1.8.1", features = ["union", "may_dangle"] }
tracing = "0.1"
# tidy-alphabetical-end
5 changes: 4 additions & 1 deletion compiler/rustc_hir/src/hir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3256,7 +3256,10 @@ impl<'hir> Item<'hir> {
}

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
#[derive(Encodable, Decodable, HashStable_Generic)]
#[derive(Encodable, Decodable, HashStable_Generic, TypeFoldable, TypeVisitable)]
#[skip_traversal(
but_impl_despite_trivial_because = "`Unsafety` impls `Relate`, which is a subtrait of `TypeFoldable`."
)]
pub enum Unsafety {
Unsafe,
Normal,
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_hir_analysis/src/astconv/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1057,7 +1057,7 @@ impl<'o, 'tcx> dyn AstConv<'tcx> + 'o {
tcx,
predicates
.iter()
.filter_map(|(p, _)| Some(p.as_trait_clause()?.map_bound(|t| t.trait_ref))),
.filter_map(|p| Some(p.node.as_trait_clause()?.map_bound(|t| t.trait_ref))),
assoc_name,
)
},
Expand Down
8 changes: 4 additions & 4 deletions compiler/rustc_hir_analysis/src/astconv/object_safety.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,15 @@ impl<'o, 'tcx> dyn AstConv<'tcx> + 'o {

let mut trait_bounds = vec![];
let mut projection_bounds = vec![];
for (pred, span) in bounds.clauses() {
let bound_pred = pred.kind();
for pred in bounds.clauses() {
let bound_pred = pred.node.kind();
match bound_pred.skip_binder() {
ty::ClauseKind::Trait(trait_pred) => {
assert_eq!(trait_pred.polarity, ty::ImplPolarity::Positive);
trait_bounds.push((bound_pred.rebind(trait_pred.trait_ref), span));
trait_bounds.push((bound_pred.rebind(trait_pred.trait_ref), pred.span));
}
ty::ClauseKind::Projection(proj) => {
projection_bounds.push((bound_pred.rebind(proj), span));
projection_bounds.push((bound_pred.rebind(proj), pred.span));
}
ty::ClauseKind::TypeOutlives(_) => {
// Do nothing, we deal with regions separately
Expand Down
24 changes: 13 additions & 11 deletions compiler/rustc_hir_analysis/src/bounds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use rustc_span::Span;
/// include the self type (e.g., `trait_bounds`) but in others we do not
#[derive(Default, PartialEq, Eq, Clone, Debug)]
pub struct Bounds<'tcx> {
pub clauses: Vec<(ty::Clause<'tcx>, Span)>,
pub clauses: Vec<ty::Spanned<ty::Clause<'tcx>>>,
}

impl<'tcx> Bounds<'tcx> {
Expand All @@ -33,8 +33,10 @@ impl<'tcx> Bounds<'tcx> {
region: ty::PolyTypeOutlivesPredicate<'tcx>,
span: Span,
) {
self.clauses
.push((region.map_bound(|p| ty::ClauseKind::TypeOutlives(p)).to_predicate(tcx), span));
self.clauses.push(ty::Spanned {
node: region.map_bound(|p| ty::ClauseKind::TypeOutlives(p)).to_predicate(tcx),
span,
});
}

pub fn push_trait_bound(
Expand Down Expand Up @@ -72,14 +74,14 @@ impl<'tcx> Bounds<'tcx> {
span: Span,
polarity: ty::ImplPolarity,
) {
self.clauses.push((
trait_ref
self.clauses.push(ty::Spanned {
node: trait_ref
.map_bound(|trait_ref| {
ty::ClauseKind::Trait(ty::TraitPredicate { trait_ref, polarity })
})
.to_predicate(tcx),
span,
));
});
}

pub fn push_projection_bound(
Expand All @@ -88,20 +90,20 @@ impl<'tcx> Bounds<'tcx> {
projection: ty::PolyProjectionPredicate<'tcx>,
span: Span,
) {
self.clauses.push((
projection.map_bound(|proj| ty::ClauseKind::Projection(proj)).to_predicate(tcx),
self.clauses.push(ty::Spanned {
node: projection.map_bound(|proj| ty::ClauseKind::Projection(proj)).to_predicate(tcx),
span,
));
});
}

pub fn push_sized(&mut self, tcx: TyCtxt<'tcx>, ty: Ty<'tcx>, span: Span) {
let sized_def_id = tcx.require_lang_item(LangItem::Sized, Some(span));
let trait_ref = ty::TraitRef::new(tcx, sized_def_id, [ty]);
// Preferable to put this obligation first, since we report better errors for sized ambiguity.
self.clauses.insert(0, (trait_ref.to_predicate(tcx), span));
self.clauses.insert(0, ty::Spanned { node: trait_ref.to_predicate(tcx), span });
}

pub fn clauses(&self) -> impl Iterator<Item = (ty::Clause<'tcx>, Span)> + '_ {
pub fn clauses(&self) -> impl Iterator<Item = ty::Spanned<ty::Clause<'tcx>>> + '_ {
self.clauses.iter().cloned()
}
}
Loading

0 comments on commit 793a8bf

Please sign in to comment.