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

Implement #[must_not_suspend] #88865

Merged
merged 9 commits into from
Sep 22, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions compiler/rustc_feature/src/active.rs
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,10 @@ declare_features! (
/// Allows `let...else` statements.
(active, let_else, "1.56.0", Some(87335), None),

/// Allows `#[must_not_suspend]`.
(active, must_not_suspend, "1.56.0", Some(83310), None),

guswynn marked this conversation as resolved.
Show resolved Hide resolved

// -------------------------------------------------------------------------
// feature-group-end: actual feature gates
// -------------------------------------------------------------------------
Expand Down
4 changes: 4 additions & 0 deletions compiler/rustc_feature/src/builtin_attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,10 @@ pub const BUILTIN_ATTRIBUTES: &[BuiltinAttribute] = &[
ungated!(forbid, Normal, template!(List: r#"lint1, lint2, ..., /*opt*/ reason = "...""#)),
ungated!(deny, Normal, template!(List: r#"lint1, lint2, ..., /*opt*/ reason = "...""#)),
ungated!(must_use, Normal, template!(Word, NameValueStr: "reason")),
gated!(
must_not_suspend, Normal, template!(Word, NameValueStr: "reason"), must_not_suspend,
experimental!(must_not_suspend)
),
// FIXME(#14407)
ungated!(
deprecated, Normal,
Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_lint/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ fn register_builtins(store: &mut LintStore, no_interleave_lints: bool) {
UNUSED_LABELS,
UNUSED_PARENS,
UNUSED_BRACES,
MUST_NOT_SUSPEND,
REDUNDANT_SEMICOLONS
);

Expand Down
31 changes: 31 additions & 0 deletions compiler/rustc_lint_defs/src/builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,36 @@ declare_lint! {
"imports that are never used"
}

declare_lint! {
/// The `must_not_suspend` lint detects values that are marked with the `#[must_not_suspend]`
/// attribute being held across yield points. A "yield" point is usually a `.await` in an async
/// function.
///
/// This attribute can be used to mark values that are semantically incorrect across yields
/// (like certain types of timers), values that have async alternatives, and values that
/// regularly cause problems with the `Send`-ness of async fn's returned futures (like
/// `MutexGuard`'s)
///
/// ### Example
///
/// ```rust
/// #![feature(must_not_suspend)]
///
/// #[must_not_suspend]
/// struct SyncThing {}
///
/// async fn yield_now() {}
///
/// pub async fn uhoh() {
/// let guard = SyncThing {};
/// yield_now().await;
/// }
/// ```
pub MUST_NOT_SUSPEND,
Warn,
"Use of a `#[must_not_suspend]` value across a yield point",
guswynn marked this conversation as resolved.
Show resolved Hide resolved
}

declare_lint! {
/// The `unused_extern_crates` lint guards against `extern crate` items
/// that are never used.
Expand Down Expand Up @@ -2993,6 +3023,7 @@ declare_lint_pass! {
CENUM_IMPL_DROP_CAST,
CONST_EVALUATABLE_UNCHECKED,
INEFFECTIVE_UNSTABLE_TRAIT_IMPL,
MUST_NOT_SUSPEND,
UNINHABITED_STATIC,
FUNCTION_ITEM_REFERENCES,
USELESS_DEPRECATED,
Expand Down
16 changes: 16 additions & 0 deletions compiler/rustc_passes/src/check_attr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ impl CheckAttrVisitor<'tcx> {
sym::default_method_body_is_const => {
self.check_default_method_body_is_const(attr, span, target)
}
sym::must_not_suspend => self.check_must_not_suspend(&attr, span, target),
sym::rustc_const_unstable
| sym::rustc_const_stable
| sym::unstable
Expand Down Expand Up @@ -1014,6 +1015,21 @@ impl CheckAttrVisitor<'tcx> {
is_valid
}

/// Checks if `#[must_not_suspend]` is applied to a function. Returns `true` if valid.
fn check_must_not_suspend(&self, attr: &Attribute, span: &Span, target: Target) -> bool {
match target {
Target::Struct | Target::Enum | Target::Union | Target::Trait => true,
_ => {
self.tcx
.sess
.struct_span_err(attr.span, "`must_not_suspend` attribute should be applied to a struct, enum, or trait")
.span_label(*span, "is not a struct, enum, or trait")
.emit();
false
}
}
}

/// Checks if `#[cold]` is applied to a non-function. Returns `true` if valid.
fn check_cold(&self, hir_id: HirId, attr: &Attribute, span: &Span, target: Target) {
match target {
Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_span/src/symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,7 @@ symbols! {
mul,
mul_assign,
mul_with_overflow,
must_not_suspend,
must_use,
mut_ptr,
mut_slice_ptr,
Expand Down
229 changes: 224 additions & 5 deletions compiler/rustc_typeck/src/check/generator_interior.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@

use super::FnCtxt;
use rustc_data_structures::fx::{FxHashSet, FxIndexSet};
use rustc_errors::pluralize;
use rustc_hir as hir;
use rustc_hir::def::{CtorKind, DefKind, Res};
use rustc_hir::def_id::DefId;
use rustc_hir::hir_id::HirIdSet;
use rustc_hir::intravisit::{self, NestedVisitorMap, Visitor};
use rustc_hir::{Arm, Expr, ExprKind, Guard, HirId, Pat, PatKind};
use rustc_middle::middle::region::{self, YieldData};
use rustc_middle::ty::{self, Ty};
use rustc_middle::ty::{self, Ty, TyCtxt};
use rustc_span::symbol::sym;
use rustc_span::Span;
use smallvec::SmallVec;
use tracing::debug;

struct InteriorVisitor<'a, 'tcx> {
fcx: &'a FnCtxt<'a, 'tcx>,
Expand All @@ -36,6 +39,7 @@ impl<'a, 'tcx> InteriorVisitor<'a, 'tcx> {
fn record(
&mut self,
ty: Ty<'tcx>,
hir_id: HirId,
scope: Option<region::Scope>,
expr: Option<&'tcx Expr<'tcx>>,
source_span: Span,
Expand Down Expand Up @@ -117,6 +121,19 @@ impl<'a, 'tcx> InteriorVisitor<'a, 'tcx> {
} else {
// Insert the type into the ordered set.
let scope_span = scope.map(|s| s.span(self.fcx.tcx, self.region_scope_tree));

check_must_not_suspend_ty(
self.fcx,
ty,
hir_id,
expr,
source_span,
yield_data.span,
"",
"",
1,
);

self.types.insert(ty::GeneratorInteriorTypeCause {
span: source_span,
ty: &ty,
Expand Down Expand Up @@ -290,7 +307,7 @@ impl<'a, 'tcx> Visitor<'tcx> for InteriorVisitor<'a, 'tcx> {
if let PatKind::Binding(..) = pat.kind {
let scope = self.region_scope_tree.var_scope(pat.hir_id.local_id);
let ty = self.fcx.typeck_results.borrow().pat_ty(pat);
self.record(ty, Some(scope), None, pat.span, false);
self.record(ty, pat.hir_id, Some(scope), None, pat.span, false);
guswynn marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down Expand Up @@ -342,7 +359,14 @@ impl<'a, 'tcx> Visitor<'tcx> for InteriorVisitor<'a, 'tcx> {
// If there are adjustments, then record the final type --
// this is the actual value that is being produced.
if let Some(adjusted_ty) = self.fcx.typeck_results.borrow().expr_ty_adjusted_opt(expr) {
self.record(adjusted_ty, scope, Some(expr), expr.span, guard_borrowing_from_pattern);
self.record(
adjusted_ty,
expr.hir_id,
scope,
Some(expr),
expr.span,
guard_borrowing_from_pattern,
);
}

// Also record the unadjusted type (which is the only type if
Expand Down Expand Up @@ -380,9 +404,23 @@ impl<'a, 'tcx> Visitor<'tcx> for InteriorVisitor<'a, 'tcx> {
tcx.mk_region(ty::RegionKind::ReErased),
ty::TypeAndMut { ty, mutbl: hir::Mutability::Not },
);
self.record(ref_ty, scope, Some(expr), expr.span, guard_borrowing_from_pattern);
self.record(
ref_ty,
expr.hir_id,
scope,
Some(expr),
expr.span,
guard_borrowing_from_pattern,
);
}
self.record(ty, scope, Some(expr), expr.span, guard_borrowing_from_pattern);
self.record(
ty,
expr.hir_id,
scope,
Some(expr),
expr.span,
guard_borrowing_from_pattern,
);
} else {
self.fcx.tcx.sess.delay_span_bug(expr.span, "no type for node");
}
Expand All @@ -409,3 +447,184 @@ impl<'a, 'tcx> Visitor<'tcx> for ArmPatCollector<'a> {
}
}
}

// Returns whether it emitted a diagnostic or not
// Note that this fn and the proceding one are based on the code
// for creating must_use diagnostics
pub fn check_must_not_suspend_ty<'tcx>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The complexity of this function seems ~okay to me, but I'm still wondering if the (discussed and dismissed in the RFC) marker trait (Suspend?) approach would work better. The attribute could impl !Suspend for structs, add it as a supertrait for traits and then the one caller to check_must_not_suspend_ty could make the InferCtxt add an obligation for Suspend to exist.

This would require some extra logic to make that obligation failure a lint instead of an error, so if we don't already have something like this, we should keep doing what we're doing here. Maybe just add the above paragraph as a comment for why we are implementing it this way?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the choice basically "we decided against this in the RFC"?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it was never discussed in the RFC except for an honorable mention here

fcx: &FnCtxt<'_, 'tcx>,
ty: Ty<'tcx>,
hir_id: HirId,
expr: Option<&'tcx Expr<'tcx>>,
source_span: Span,
yield_span: Span,
descr_pre: &str,
descr_post: &str,
plural_len: usize,
) -> bool {
if ty.is_unit()
// || fcx.tcx.is_ty_uninhabited_from(fcx.tcx.parent_module(hir_id).to_def_id(), ty, fcx.param_env)
guswynn marked this conversation as resolved.
Show resolved Hide resolved
// FIXME: should this check is_ty_uninhabited_from
{
return true;
guswynn marked this conversation as resolved.
Show resolved Hide resolved
}

let plural_suffix = pluralize!(plural_len);

match *ty.kind() {
ty::Adt(..) if ty.is_box() => {
let boxed_ty = ty.boxed_ty();
let descr_pre = &format!("{}boxed ", descr_pre);
check_must_not_suspend_ty(
guswynn marked this conversation as resolved.
Show resolved Hide resolved
fcx,
boxed_ty,
hir_id,
expr,
source_span,
yield_span,
descr_pre,
descr_post,
plural_len,
guswynn marked this conversation as resolved.
Show resolved Hide resolved
)
}
ty::Adt(def, _) => check_must_not_suspend_def(
fcx.tcx,
def.did,
hir_id,
source_span,
yield_span,
descr_pre,
descr_post,
),
ty::Opaque(def, _) => {
guswynn marked this conversation as resolved.
Show resolved Hide resolved
let mut has_emitted = false;
for &(predicate, _) in fcx.tcx.explicit_item_bounds(def) {
// We only look at the `DefId`, so it is safe to skip the binder here.
if let ty::PredicateKind::Trait(ref poly_trait_predicate) =
predicate.kind().skip_binder()
{
let def_id = poly_trait_predicate.trait_ref.def_id;
let descr_pre = &format!("{}implementer{} of ", descr_pre, plural_suffix,);
if check_must_not_suspend_def(
fcx.tcx,
def_id,
hir_id,
source_span,
yield_span,
descr_pre,
descr_post,
) {
has_emitted = true;
break;
}
}
}
has_emitted
}
ty::Dynamic(binder, _) => {
let mut has_emitted = false;
for predicate in binder.iter() {
if let ty::ExistentialPredicate::Trait(ref trait_ref) = predicate.skip_binder() {
let def_id = trait_ref.def_id;
let descr_post = &format!(" trait object{}{}", plural_suffix, descr_post,);
if check_must_not_suspend_def(
fcx.tcx,
def_id,
hir_id,
source_span,
yield_span,
descr_pre,
descr_post,
) {
has_emitted = true;
break;
}
}
}
has_emitted
}
ty::Tuple(ref tys) => {
let mut has_emitted = false;
let spans = if let Some(hir::ExprKind::Tup(comps)) = expr.map(|e| &e.kind) {
debug_assert_eq!(comps.len(), tys.len());
comps.iter().map(|e| e.span).collect()
} else {
vec![]
};
for (i, ty) in tys.iter().map(|k| k.expect_ty()).enumerate() {
let descr_post = &format!(" in tuple element {}", i);
let span = *spans.get(i).unwrap_or(&source_span);
if check_must_not_suspend_ty(
fcx, ty, hir_id, expr, span, yield_span, descr_pre, descr_post, plural_len,
) {
has_emitted = true;
}
}
has_emitted
}
ty::Array(ty, len) => {
let descr_pre = &format!("{}array{} of ", descr_pre, plural_suffix,);
check_must_not_suspend_ty(
fcx,
ty,
hir_id,
expr,
source_span,
yield_span,
descr_pre,
descr_post,
len.try_eval_usize(fcx.tcx, fcx.param_env).unwrap_or(0) as usize + 1,
)
}
_ => false,
}
}

fn check_must_not_suspend_def(
tcx: TyCtxt<'_>,
def_id: DefId,
hir_id: HirId,
source_span: Span,
yield_span: Span,
descr_pre_path: &str,
descr_post_path: &str,
) -> bool {
for attr in tcx.get_attrs(def_id).iter() {
if attr.has_name(sym::must_not_suspend) {
tcx.struct_span_lint_hir(
rustc_session::lint::builtin::MUST_NOT_SUSPEND,
hir_id,
source_span,
|lint| {
let msg = format!(
"{}`{}`{} held across a yield point, but should not be",
descr_pre_path,
tcx.def_path_str(def_id),
descr_post_path
);
let mut err = lint.build(&msg);

// Add optional reason note
if let Some(note) = attr.value_str() {
err.note(&note.as_str());
}

// add span pointing to the offending yield/await)
guswynn marked this conversation as resolved.
Show resolved Hide resolved
err.span_label(yield_span, "The value is held across this yield point");
guswynn marked this conversation as resolved.
Show resolved Hide resolved

// Add some quick suggestions on what to do
err.span_help(
source_span,
"`drop` this value before the yield point, or use a block (`{ ... }`) \"
guswynn marked this conversation as resolved.
Show resolved Hide resolved
to shrink its scope",
);

err.emit();
},
);

return true;
}
}
false
}
Loading