Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
7 changes: 7 additions & 0 deletions compiler/rustc_lint/messages.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ lint_builtin_allow_internal_unsafe =
lint_builtin_anonymous_params = anonymous parameters are deprecated and will be removed in the next edition
.suggestion = try naming the parameter or explicitly ignoring it

lint_builtin_black_box_zst_call = `black_box` on zero-sized callable `{$ty}` has no effect on call opacity
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think the terminology "zero-sized callable" appears anywhere else. Should we be more specific and say "function item" or "closure" here?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

There is another footgun potential here. For the specific case of closures, using black_box is probably wrong even if the closure's hidden type is has non-zero size.

After all, such use of black_box will apply the black box effect to the values of captured variables, but like here, it has no effect on call opacity.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think if I used "function item" in the message it would look confusing if a user triggered it with black_box(()) as my implementation checks layout.is_zst(). If you like I can update the message to "zero-sized type" to be more readable and accurate.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

And for the footgun I agree but this PR is scoped to ZSTs so I think to address it in a followup PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If it's not limited to callables, then yeah this should be reworded.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've updated the error message to zero-sized type making it more readable and accurate. Thanks!

.label = zero-sized callable passed here

lint_builtin_black_box_zst_help = coerce to a function pointer and call `black_box` on that pointer instead
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It'd be cool if we were actually able to suggest a code fix here! But, I don't think that's necessary for this PR, just a nice future improvement.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Like? any example or idea?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

guess the error message already says to coerce the object to a pointer, but we could also show a suggested code snippet. I think that would also let rust-analyzer make the change automatically. Given an error message like:

error: `black_box` on zero-sized callable `fn(u32, u32) -> u32 {add}` has no effect on call opacity
  --> $DIR/lint-black-box-zst-call.rs:10:18
   |
LL |     let add_bb = black_box(add);
   |                  ^^^^^^^^^^---^
   |                            |
   |                            zero-sized callable passed here
   |
   = note: zero-sized callable values have no runtime representation, so the call still targets the original function directly
   = help: coerce to a function pointer and call `black_box` on that pointer instead

The suggestion would be something like:

    let add_ptr: fn(u32, u32) -> u32 = add;
    let add_bb = black_box(add_ptr);

We have some other errors and warnings that make suggestions like this, so I'd try to look at what they do and see if you can reuse that infrastructure.


lint_builtin_black_box_zst_note = zero-sized callable values have no runtime representation, so the call still targets the original function directly

lint_builtin_clashing_extern_diff_name = `{$this}` redeclares `{$orig}` with a different signature
.previous_decl_label = `{$orig}` previously declared here
.mismatch_label = this signature doesn't match the previous declaration
Expand Down
94 changes: 84 additions & 10 deletions compiler/rustc_lint/src/builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,17 @@ use rustc_trait_selection::traits::{self};

use crate::errors::BuiltinEllipsisInclusiveRangePatterns;
use crate::lints::{
BuiltinAnonymousParams, BuiltinConstNoMangle, BuiltinDerefNullptr, BuiltinDoubleNegations,
BuiltinDoubleNegationsAddParens, BuiltinEllipsisInclusiveRangePatternsLint,
BuiltinExplicitOutlives, BuiltinExplicitOutlivesSuggestion, BuiltinFeatureIssueNote,
BuiltinIncompleteFeatures, BuiltinIncompleteFeaturesHelp, BuiltinInternalFeatures,
BuiltinKeywordIdents, BuiltinMissingCopyImpl, BuiltinMissingDebugImpl, BuiltinMissingDoc,
BuiltinMutablesTransmutes, BuiltinNoMangleGeneric, BuiltinNonShorthandFieldPatterns,
BuiltinSpecialModuleNameUsed, BuiltinTrivialBounds, BuiltinTypeAliasBounds,
BuiltinUngatedAsyncFnTrackCaller, BuiltinUnpermittedTypeInit, BuiltinUnpermittedTypeInitSub,
BuiltinUnreachablePub, BuiltinUnsafe, BuiltinUnstableFeatures, BuiltinUnusedDocComment,
BuiltinUnusedDocCommentSub, BuiltinWhileTrue, InvalidAsmLabel,
BuiltinAnonymousParams, BuiltinBlackBoxZstCall, BuiltinConstNoMangle, BuiltinDerefNullptr,
BuiltinDoubleNegations, BuiltinDoubleNegationsAddParens,
BuiltinEllipsisInclusiveRangePatternsLint, BuiltinExplicitOutlives,
BuiltinExplicitOutlivesSuggestion, BuiltinFeatureIssueNote, BuiltinIncompleteFeatures,
BuiltinIncompleteFeaturesHelp, BuiltinInternalFeatures, BuiltinKeywordIdents,
BuiltinMissingCopyImpl, BuiltinMissingDebugImpl, BuiltinMissingDoc, BuiltinMutablesTransmutes,
BuiltinNoMangleGeneric, BuiltinNonShorthandFieldPatterns, BuiltinSpecialModuleNameUsed,
BuiltinTrivialBounds, BuiltinTypeAliasBounds, BuiltinUngatedAsyncFnTrackCaller,
BuiltinUnpermittedTypeInit, BuiltinUnpermittedTypeInitSub, BuiltinUnreachablePub,
BuiltinUnsafe, BuiltinUnstableFeatures, BuiltinUnusedDocComment, BuiltinUnusedDocCommentSub,
BuiltinWhileTrue, InvalidAsmLabel,
};
use crate::{
EarlyContext, EarlyLintPass, LateContext, LateLintPass, Level, LintContext,
Expand Down Expand Up @@ -3110,6 +3111,79 @@ impl<'tcx> LateLintPass<'tcx> for AsmLabels {
}
}

declare_lint! {
/// The `black_box_zst_calls` lint detects calls to `core::hint::black_box`
/// where the argument is a zero-sized callable (e.g. a function item or
/// a capture-less closure). These values have no runtime representation,
Comment thread
Darksonn marked this conversation as resolved.
Outdated
/// so the black boxing does not make subsequent calls opaque to the
/// optimizer.
///
/// ### Example
///
/// ```rust,compile_fail
/// #![deny(black_box_zst_calls)]
/// use std::hint::black_box;
///
/// fn add(a: u32, b: u32) -> u32 {
/// a + b
/// }
///
/// fn main() {
/// let add_bb = black_box(add);
/// let _ = add_bb(1, 2);
/// }
/// ```
///
/// {{produces}}
///
/// ### Explanation
///
/// Function items and capture-less closures are zero-sized. Passing them
/// to `black_box` does not force the optimizer to treat the subsequent
/// call as opaque. Coerce the callable to a function pointer and black_box
/// that pointer instead.
pub BLACK_BOX_ZST_CALLS,
Warn,
"calling `black_box` on zero-sized callables has no effect on opacity"
}

declare_lint_pass!(BlackBoxZstCalls => [BLACK_BOX_ZST_CALLS]);

impl<'tcx> LateLintPass<'tcx> for BlackBoxZstCalls {
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &hir::Expr<'tcx>) {
let hir::ExprKind::Call(callee, args) = expr.kind else { return };
if args.len() != 1 {
return;
}

let hir::ExprKind::Path(ref qpath) = callee.kind else { return };
let Some(def_id) = cx.qpath_res(qpath, callee.hir_id).opt_def_id() else { return };

if !cx.tcx.is_diagnostic_item(sym::black_box, def_id) {
return;
}

let arg = &args[0];
let arg_ty = cx.typeck_results().expr_ty_adjusted(arg);

if !is_callable_zst(cx, arg_ty) {
return;
}

let ty_name = with_no_trimmed_paths!(arg_ty.to_string());
cx.emit_span_lint(
BLACK_BOX_ZST_CALLS,
expr.span,
BuiltinBlackBoxZstCall { arg_span: arg.span, ty: ty_name },
);
}
}

fn is_callable_zst<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool {
matches!(ty.kind(), ty::FnDef(..) | ty::Closure(..))
&& cx.tcx.layout_of(cx.typing_env().as_query_input(ty)).is_ok_and(|layout| layout.is_zst())
}

declare_lint! {
/// The `special_module_name` lint detects module
/// declarations for files that have a special meaning.
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 @@ -201,6 +201,7 @@ late_lint_methods!(
InvalidFromUtf8: InvalidFromUtf8,
VariantSizeDifferences: VariantSizeDifferences,
PathStatements: PathStatements,
BlackBoxZstCalls: BlackBoxZstCalls,
LetUnderscore: LetUnderscore,
InvalidReferenceCasting: InvalidReferenceCasting,
ImplicitAutorefs: ImplicitAutorefs,
Expand Down
10 changes: 10 additions & 0 deletions compiler/rustc_lint/src/lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,16 @@ pub(crate) struct BuiltinNonShorthandFieldPatterns {
pub prefix: &'static str,
}

#[derive(LintDiagnostic)]
#[diag(lint_builtin_black_box_zst_call)]
#[note(lint_builtin_black_box_zst_note)]
#[help(lint_builtin_black_box_zst_help)]
pub(crate) struct BuiltinBlackBoxZstCall {
#[label]
pub arg_span: Span,
pub ty: String,
}

#[derive(LintDiagnostic)]
pub(crate) enum BuiltinUnsafe {
#[diag(lint_builtin_allow_internal_unsafe)]
Expand Down
1 change: 1 addition & 0 deletions library/core/src/hint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,7 @@ pub fn spin_loop() {
#[inline]
#[stable(feature = "bench_black_box", since = "1.66.0")]
#[rustc_const_stable(feature = "const_black_box", since = "1.86.0")]
#[rustc_diagnostic_item = "black_box"]
pub const fn black_box<T>(dummy: T) -> T {
crate::intrinsics::black_box(dummy)
}
Expand Down
13 changes: 13 additions & 0 deletions tests/ui/lint/lint-black-box-zst-call.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#![deny(black_box_zst_calls)]

use std::hint::black_box;

fn add(a: u32, b: u32) -> u32 {
a + b
}

fn main() {
let add_bb = black_box(add);
//~^ ERROR `black_box` on zero-sized callable
let _ = add_bb(1, 2);
}
18 changes: 18 additions & 0 deletions tests/ui/lint/lint-black-box-zst-call.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
error: `black_box` on zero-sized callable `fn(u32, u32) -> u32 {add}` has no effect on call opacity
--> $DIR/lint-black-box-zst-call.rs:10:18
|
LL | let add_bb = black_box(add);
| ^^^^^^^^^^---^
| |
| zero-sized callable passed here
|
= note: zero-sized callable values have no runtime representation, so the call still targets the original function directly
= help: coerce to a function pointer and call `black_box` on that pointer instead
note: the lint level is defined here
--> $DIR/lint-black-box-zst-call.rs:1:9
|
LL | #![deny(black_box_zst_calls)]
| ^^^^^^^^^^^^^^^^^^^

error: aborting due to 1 previous error

Loading