From a31eed44e9b4adf65629f675cbb73130e959f75b Mon Sep 17 00:00:00 2001 From: Urgau Date: Sun, 8 Mar 2026 11:41:14 +0100 Subject: [PATCH 1/4] Add `#[rustc_panics_when_zero]` attribute --- .../src/attributes/rustc_internal.rs | 15 +++++++++++++++ compiler/rustc_attr_parsing/src/context.rs | 1 + compiler/rustc_feature/src/builtin_attrs.rs | 4 ++++ compiler/rustc_hir/src/attrs/data_structures.rs | 3 +++ .../rustc_hir/src/attrs/encode_cross_crate.rs | 1 + compiler/rustc_passes/src/check_attr.rs | 1 + compiler/rustc_span/src/symbol.rs | 1 + 7 files changed, 26 insertions(+) diff --git a/compiler/rustc_attr_parsing/src/attributes/rustc_internal.rs b/compiler/rustc_attr_parsing/src/attributes/rustc_internal.rs index 3f4049366f405..3e3767584639f 100644 --- a/compiler/rustc_attr_parsing/src/attributes/rustc_internal.rs +++ b/compiler/rustc_attr_parsing/src/attributes/rustc_internal.rs @@ -6,6 +6,7 @@ use rustc_hir::attrs::{ BorrowckGraphvizFormatKind, CguFields, CguKind, DivergingBlockBehavior, DivergingFallbackBehavior, RustcCleanAttribute, RustcCleanQueries, RustcMirKind, }; +use rustc_hir::target::GenericParamKind; use rustc_span::Symbol; use super::prelude::*; @@ -81,6 +82,20 @@ impl NoArgsAttributeParser for RustcNeverReturnsNullPtrParser { ]); const CREATE: fn(Span) -> AttributeKind = |_| AttributeKind::RustcNeverReturnsNullPtr; } + +pub(crate) struct RustcPanicsWhenZeroParser; + +impl NoArgsAttributeParser for RustcPanicsWhenZeroParser { + const PATH: &[Symbol] = &[sym::rustc_panics_when_zero]; + const ON_DUPLICATE: OnDuplicate = OnDuplicate::Error; + const ALLOWED_TARGETS: AllowedTargets = AllowedTargets::AllowList(&[ + Allow(Target::GenericParam { kind: GenericParamKind::Const, has_default: true }), + Allow(Target::GenericParam { kind: GenericParamKind::Const, has_default: false }), + ]); + + const CREATE: fn(Span) -> AttributeKind = |_| AttributeKind::RustcPanicsWhenZero; +} + pub(crate) struct RustcNoImplicitAutorefsParser; impl NoArgsAttributeParser for RustcNoImplicitAutorefsParser { diff --git a/compiler/rustc_attr_parsing/src/context.rs b/compiler/rustc_attr_parsing/src/context.rs index becdaee0f3d3a..29dc97a87c957 100644 --- a/compiler/rustc_attr_parsing/src/context.rs +++ b/compiler/rustc_attr_parsing/src/context.rs @@ -322,6 +322,7 @@ attribute_parsers!( Single>, Single>, Single>, + Single>, Single>, Single>, Single>, diff --git a/compiler/rustc_feature/src/builtin_attrs.rs b/compiler/rustc_feature/src/builtin_attrs.rs index 053caee258f7b..b66fcf5e009d3 100644 --- a/compiler/rustc_feature/src/builtin_attrs.rs +++ b/compiler/rustc_feature/src/builtin_attrs.rs @@ -627,6 +627,10 @@ pub static BUILTIN_ATTRIBUTES: &[BuiltinAttribute] = &[ rustc_coherence_is_core, "`#![rustc_coherence_is_core]` allows inherent methods on builtin types, only intended to be used in `core`" ), + rustc_attr!( + rustc_panics_when_zero, + "`#[rustc_panics_when_zero]` is used to mark a const generic argument which makes the function panic when it is zero" + ), rustc_attr!( rustc_coinductive, "`#[rustc_coinductive]` changes a trait to be coinductive, allowing cycles in the trait solver" diff --git a/compiler/rustc_hir/src/attrs/data_structures.rs b/compiler/rustc_hir/src/attrs/data_structures.rs index 05f398058ecaa..ea4c4d1fc3f47 100644 --- a/compiler/rustc_hir/src/attrs/data_structures.rs +++ b/compiler/rustc_hir/src/attrs/data_structures.rs @@ -1533,6 +1533,9 @@ pub enum AttributeKind { /// Represents `#[rustc_offload_kernel]` RustcOffloadKernel, + /// Represents `#[rustc_panics_when_zero]` (used for linting). + RustcPanicsWhenZero, + /// Represents `#[rustc_paren_sugar]`. RustcParenSugar(Span), diff --git a/compiler/rustc_hir/src/attrs/encode_cross_crate.rs b/compiler/rustc_hir/src/attrs/encode_cross_crate.rs index ad4d0728888bf..a187acfc1fcca 100644 --- a/compiler/rustc_hir/src/attrs/encode_cross_crate.rs +++ b/compiler/rustc_hir/src/attrs/encode_cross_crate.rs @@ -170,6 +170,7 @@ impl AttributeKind { RustcObjcClass { .. } => No, RustcObjcSelector { .. } => No, RustcOffloadKernel => Yes, + RustcPanicsWhenZero => Yes, RustcParenSugar(..) => No, RustcPassByValue(..) => Yes, RustcPassIndirectlyInNonRusticAbis(..) => No, diff --git a/compiler/rustc_passes/src/check_attr.rs b/compiler/rustc_passes/src/check_attr.rs index 10a805a159b06..0f066f1319f2f 100644 --- a/compiler/rustc_passes/src/check_attr.rs +++ b/compiler/rustc_passes/src/check_attr.rs @@ -357,6 +357,7 @@ impl<'tcx> CheckAttrVisitor<'tcx> { | AttributeKind::RustcObjcClass { .. } | AttributeKind::RustcObjcSelector { .. } | AttributeKind::RustcOffloadKernel + | AttributeKind::RustcPanicsWhenZero | AttributeKind::RustcParenSugar(..) | AttributeKind::RustcPassByValue (..) | AttributeKind::RustcPassIndirectlyInNonRusticAbis(..) diff --git a/compiler/rustc_span/src/symbol.rs b/compiler/rustc_span/src/symbol.rs index 981bfed363dcc..f561793c577e8 100644 --- a/compiler/rustc_span/src/symbol.rs +++ b/compiler/rustc_span/src/symbol.rs @@ -1777,6 +1777,7 @@ symbols! { rustc_objc_selector, rustc_offload_kernel, rustc_on_unimplemented, + rustc_panics_when_zero, rustc_paren_sugar, rustc_partition_codegened, rustc_partition_reused, From 7c512ea30292371fbf3c7c63cc37f87f28c0d3a3 Mon Sep 17 00:00:00 2001 From: Urgau Date: Sun, 8 Mar 2026 11:42:58 +0100 Subject: [PATCH 2/4] Apply `#[rustc_panics_when_zero]` to the relevant core functions --- library/core/src/iter/traits/iterator.rs | 16 +++++-------- library/core/src/slice/mod.rs | 24 +++++++++++++------ .../tests/iter/adapters/map_windows.rs | 1 + 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/library/core/src/iter/traits/iterator.rs b/library/core/src/iter/traits/iterator.rs index c6a06c133a156..043e76daba4f4 100644 --- a/library/core/src/iter/traits/iterator.rs +++ b/library/core/src/iter/traits/iterator.rs @@ -1653,14 +1653,7 @@ pub const trait Iterator { /// /// # Panics /// - /// Panics if `N` is zero. This check will most probably get changed to a - /// compile time error before this method gets stabilized. - /// - /// ```should_panic - /// #![feature(iter_map_windows)] - /// - /// let iter = std::iter::repeat(0).map_windows(|&[]| ()); - /// ``` + /// Panics if `N` is zero. /// /// # Examples /// @@ -1770,7 +1763,10 @@ pub const trait Iterator { /// ``` #[inline] #[unstable(feature = "iter_map_windows", issue = "87155")] - fn map_windows(self, f: F) -> MapWindows + fn map_windows( + self, + f: F, + ) -> MapWindows where Self: Sized, F: FnMut(&[Self::Item; N]) -> R, @@ -3632,7 +3628,7 @@ pub const trait Iterator { /// ``` #[track_caller] #[unstable(feature = "iter_array_chunks", issue = "100450")] - fn array_chunks(self) -> ArrayChunks + fn array_chunks<#[rustc_panics_when_zero] const N: usize>(self) -> ArrayChunks where Self: Sized, { diff --git a/library/core/src/slice/mod.rs b/library/core/src/slice/mod.rs index a838ba009b484..7d9f6a051ded8 100644 --- a/library/core/src/slice/mod.rs +++ b/library/core/src/slice/mod.rs @@ -1336,7 +1336,9 @@ impl [T] { #[inline] #[must_use] #[track_caller] - pub const unsafe fn as_chunks_unchecked(&self) -> &[[T; N]] { + pub const unsafe fn as_chunks_unchecked<#[rustc_panics_when_zero] const N: usize>( + &self, + ) -> &[[T; N]] { assert_unsafe_precondition!( check_language_ub, "slice::as_chunks_unchecked requires `N != 0` and the slice to split exactly into `N`-element chunks", @@ -1394,7 +1396,7 @@ impl [T] { #[inline] #[track_caller] #[must_use] - pub const fn as_chunks(&self) -> (&[[T; N]], &[T]) { + pub const fn as_chunks<#[rustc_panics_when_zero] const N: usize>(&self) -> (&[[T; N]], &[T]) { assert!(N != 0, "chunk size must be non-zero"); let len_rounded_down = self.len() / N * N; // SAFETY: The rounded-down value is always the same or smaller than the @@ -1441,7 +1443,7 @@ impl [T] { #[inline] #[track_caller] #[must_use] - pub const fn as_rchunks(&self) -> (&[T], &[[T; N]]) { + pub const fn as_rchunks<#[rustc_panics_when_zero] const N: usize>(&self) -> (&[T], &[[T; N]]) { assert!(N != 0, "chunk size must be non-zero"); let len = self.len() / N; let (remainder, multiple_of_n) = self.split_at(self.len() - len * N); @@ -1496,7 +1498,9 @@ impl [T] { #[inline] #[must_use] #[track_caller] - pub const unsafe fn as_chunks_unchecked_mut(&mut self) -> &mut [[T; N]] { + pub const unsafe fn as_chunks_unchecked_mut<#[rustc_panics_when_zero] const N: usize>( + &mut self, + ) -> &mut [[T; N]] { assert_unsafe_precondition!( check_language_ub, "slice::as_chunks_unchecked requires `N != 0` and the slice to split exactly into `N`-element chunks", @@ -1550,7 +1554,9 @@ impl [T] { #[inline] #[track_caller] #[must_use] - pub const fn as_chunks_mut(&mut self) -> (&mut [[T; N]], &mut [T]) { + pub const fn as_chunks_mut<#[rustc_panics_when_zero] const N: usize>( + &mut self, + ) -> (&mut [[T; N]], &mut [T]) { assert!(N != 0, "chunk size must be non-zero"); let len_rounded_down = self.len() / N * N; // SAFETY: The rounded-down value is always the same or smaller than the @@ -1603,7 +1609,9 @@ impl [T] { #[inline] #[track_caller] #[must_use] - pub const fn as_rchunks_mut(&mut self) -> (&mut [T], &mut [[T; N]]) { + pub const fn as_rchunks_mut<#[rustc_panics_when_zero] const N: usize>( + &mut self, + ) -> (&mut [T], &mut [[T; N]]) { assert!(N != 0, "chunk size must be non-zero"); let len = self.len() / N; let (remainder, multiple_of_n) = self.split_at_mut(self.len() - len * N); @@ -1644,7 +1652,9 @@ impl [T] { #[rustc_const_unstable(feature = "const_slice_make_iter", issue = "137737")] #[inline] #[track_caller] - pub const fn array_windows(&self) -> ArrayWindows<'_, T, N> { + pub const fn array_windows<#[rustc_panics_when_zero] const N: usize>( + &self, + ) -> ArrayWindows<'_, T, N> { assert!(N != 0, "window size must be non-zero"); ArrayWindows::new(self) } diff --git a/library/coretests/tests/iter/adapters/map_windows.rs b/library/coretests/tests/iter/adapters/map_windows.rs index 994ec087e5e8b..25b2d8f7fc31a 100644 --- a/library/coretests/tests/iter/adapters/map_windows.rs +++ b/library/coretests/tests/iter/adapters/map_windows.rs @@ -172,6 +172,7 @@ fn test_case_from_pr_82413_comment() { #[test] #[should_panic = "array in `Iterator::map_windows` must contain more than 0 elements"] +#[allow(unconditional_panic)] fn check_zero_window() { let _ = std::iter::repeat(0).map_windows(|_: &[_; 0]| ()); } From 4b51b1db3214e941a8dbc6411bf8f248101ed2e1 Mon Sep 17 00:00:00 2001 From: Urgau Date: Sun, 29 Mar 2026 18:58:29 +0200 Subject: [PATCH 3/4] Encode const params attributes in `rmeta` --- compiler/rustc_metadata/src/rmeta/encoder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/rustc_metadata/src/rmeta/encoder.rs b/compiler/rustc_metadata/src/rmeta/encoder.rs index ece9dc52c292c..69b86ed4cefe7 100644 --- a/compiler/rustc_metadata/src/rmeta/encoder.rs +++ b/compiler/rustc_metadata/src/rmeta/encoder.rs @@ -943,6 +943,7 @@ fn should_encode_attrs(def_kind: DefKind) -> bool { | DefKind::AssocConst { .. } | DefKind::Macro(_) | DefKind::Field + | DefKind::ConstParam | DefKind::Impl { .. } => true, // Encoding attrs for `Use` items allows `#[doc(hidden)]` on re-exports // to be read cross-crate, which is needed for diagnostic path selection @@ -955,7 +956,6 @@ fn should_encode_attrs(def_kind: DefKind) -> bool { DefKind::Closure => true, DefKind::SyntheticCoroutineBody => false, DefKind::TyParam - | DefKind::ConstParam | DefKind::Ctor(..) | DefKind::ExternCrate | DefKind::ForeignMod From e3bb247bd4d423f506489cfc0f8a2852c969a382 Mon Sep 17 00:00:00 2001 From: Urgau Date: Sun, 8 Mar 2026 12:57:23 +0100 Subject: [PATCH 4/4] Lint against iterator functions that panics when `N` is zero --- compiler/rustc_mir_transform/src/errors.rs | 8 +++ .../src/known_panics_lint.rs | 43 +++++++++++++-- tests/ui/lint/const-n-is-zero.rs | 55 +++++++++++++++++++ tests/ui/lint/const-n-is-zero.stderr | 52 ++++++++++++++++++ 4 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 tests/ui/lint/const-n-is-zero.rs create mode 100644 tests/ui/lint/const-n-is-zero.stderr diff --git a/compiler/rustc_mir_transform/src/errors.rs b/compiler/rustc_mir_transform/src/errors.rs index 77dea3ae1003b..21121b74401ad 100644 --- a/compiler/rustc_mir_transform/src/errors.rs +++ b/compiler/rustc_mir_transform/src/errors.rs @@ -159,6 +159,14 @@ impl AssertLintKind { } } +#[derive(Diagnostic)] +#[diag("this operation will panic at runtime")] +pub(crate) struct ConstNIsZero { + #[label("const parameter `{$const_param_name}` is zero")] + pub const_param_span: Span, + pub const_param_name: Symbol, +} + #[derive(Diagnostic)] #[diag("call to inline assembly that may unwind")] pub(crate) struct AsmUnwindCall { diff --git a/compiler/rustc_mir_transform/src/known_panics_lint.rs b/compiler/rustc_mir_transform/src/known_panics_lint.rs index 8beb00c5deffa..beeceaa25fb25 100644 --- a/compiler/rustc_mir_transform/src/known_panics_lint.rs +++ b/compiler/rustc_mir_transform/src/known_panics_lint.rs @@ -10,19 +10,23 @@ use rustc_const_eval::interpret::{ ImmTy, InterpCx, InterpResult, Projectable, Scalar, format_interp_error, interp_ok, }; use rustc_data_structures::fx::FxHashSet; -use rustc_hir::HirId; use rustc_hir::def::DefKind; +use rustc_hir::{HirId, find_attr}; use rustc_index::IndexVec; use rustc_index::bit_set::DenseBitSet; use rustc_middle::bug; use rustc_middle::mir::visit::{MutatingUseContext, NonMutatingUseContext, PlaceContext, Visitor}; use rustc_middle::mir::*; use rustc_middle::ty::layout::{LayoutError, LayoutOf, LayoutOfHelpers, TyAndLayout}; -use rustc_middle::ty::{self, ConstInt, ScalarInt, Ty, TyCtxt, TypeVisitableExt, Unnormalized}; +use rustc_middle::ty::{ + self, ConstInt, GenericArgKind, GenericParamDefKind, ScalarInt, Ty, TyCtxt, TypeVisitableExt, + Unnormalized, +}; +use rustc_session::lint::builtin::UNCONDITIONAL_PANIC; use rustc_span::Span; use tracing::{debug, instrument, trace}; -use crate::errors::{AssertLint, AssertLintKind}; +use crate::errors::{AssertLint, AssertLintKind, ConstNIsZero}; pub(super) struct KnownPanicsLint; @@ -768,6 +772,38 @@ impl<'tcx> Visitor<'tcx> for ConstPropagator<'_, 'tcx> { } // We failed to evaluate the discriminant, fallback to visiting all successors. } + TerminatorKind::Call { func, args: _, .. } => { + if let Some((def_id, generic_args)) = func.const_fn_def() { + for (index, arg) in generic_args.iter().enumerate() { + if let GenericArgKind::Const(ct) = arg.kind() { + let generics = self.tcx.generics_of(def_id); + let param_def = generics.param_at(index, self.tcx); + + if let GenericParamDefKind::Const { .. } = param_def.kind + && find_attr!(self.tcx, param_def.def_id, RustcPanicsWhenZero) + && let Some(0) = ct.try_to_target_usize(self.tcx) + { + // We managed to figure-out that the value of a + // `#[rustc_panics_when_zero]` const-generic parameter is zero. + // + // Let's report it as an unconditional panic. + let source_info = self.body.source_info(location); + if let Some(lint_root) = self.lint_root(*source_info) { + self.tcx.emit_node_span_lint( + UNCONDITIONAL_PANIC, + lint_root, + source_info.span, + ConstNIsZero { + const_param_span: source_info.span, + const_param_name: param_def.name, + }, + ); + } + } + } + } + } + } // None of these have Operands to const-propagate. TerminatorKind::Goto { .. } | TerminatorKind::UnwindResume @@ -780,7 +816,6 @@ impl<'tcx> Visitor<'tcx> for ConstPropagator<'_, 'tcx> { | TerminatorKind::CoroutineDrop | TerminatorKind::FalseEdge { .. } | TerminatorKind::FalseUnwind { .. } - | TerminatorKind::Call { .. } | TerminatorKind::InlineAsm { .. } => {} } diff --git a/tests/ui/lint/const-n-is-zero.rs b/tests/ui/lint/const-n-is-zero.rs new file mode 100644 index 0000000000000..3ebce6f547954 --- /dev/null +++ b/tests/ui/lint/const-n-is-zero.rs @@ -0,0 +1,55 @@ +// Test that core functions annotated with `#[rustc_panics_when_zero]` lint when `N` is zero + +//@ build-fail + +#![feature(iter_map_windows)] +#![feature(iter_array_chunks)] + +const ZERO: usize = 0; +const ONE: usize = 1; + +fn main() { + let s = [1, 2, 3, 4]; + + let _ = s.array_windows::<0>(); + //~^ ERROR this operation will panic at runtime + //~| NOTE `#[deny(unconditional_panic)]` on by default + //~| NOTE const parameter `N` is zero + + let _ = s.as_chunks::<{ 0 }>(); + //~^ ERROR this operation will panic at runtime + //~| NOTE const parameter `N` is zero + // + let _ = s.as_rchunks::<{ 1 - 1 }>(); + //~^ ERROR this operation will panic at runtime + //~| NOTE const parameter `N` is zero + + let mut m = [1, 2, 3, 4]; + + let _ = m.as_chunks_mut::(); + //~^ ERROR this operation will panic at runtime + //~| NOTE const parameter `N` is zero + + let _ = m.as_rchunks_mut::<{ if ZERO == 0 { 0 } else { 1 } }>(); + //~^ ERROR this operation will panic at runtime + //~| NOTE const parameter `N` is zero + + let _ = s.array_windows().any(|[]| true); + //~^ ERROR this operation will panic at runtime + //~| NOTE const parameter `N` is zero + + let _ = s.iter().map_windows(|[]| true); + //~^ ERROR this operation will panic at runtime + //~| NOTE const parameter `N` is zero + + let _ = s.iter().array_chunks().any(|[]| true); + //~^ ERROR this operation will panic at runtime + //~| NOTE const parameter `N` is zero + + // Shouldn't lint + let _ = s.array_windows::<2>(); + let _ = s.as_chunks::<1>(); + let _ = m.as_chunks_mut::(); + let _ = m.as_rchunks::<{ 1 + 1 }>(); + let _ = m.as_rchunks_mut::<{ if ZERO == 1 { 0 } else { 5 } }>(); +} diff --git a/tests/ui/lint/const-n-is-zero.stderr b/tests/ui/lint/const-n-is-zero.stderr new file mode 100644 index 0000000000000..d27a680e14b6a --- /dev/null +++ b/tests/ui/lint/const-n-is-zero.stderr @@ -0,0 +1,52 @@ +error: this operation will panic at runtime + --> $DIR/const-n-is-zero.rs:14:13 + | +LL | let _ = s.array_windows::<0>(); + | ^^^^^^^^^^^^^^^^^^^^^^ const parameter `N` is zero + | + = note: `#[deny(unconditional_panic)]` on by default + +error: this operation will panic at runtime + --> $DIR/const-n-is-zero.rs:19:13 + | +LL | let _ = s.as_chunks::<{ 0 }>(); + | ^^^^^^^^^^^^^^^^^^^^^^ const parameter `N` is zero + +error: this operation will panic at runtime + --> $DIR/const-n-is-zero.rs:23:13 + | +LL | let _ = s.as_rchunks::<{ 1 - 1 }>(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ const parameter `N` is zero + +error: this operation will panic at runtime + --> $DIR/const-n-is-zero.rs:29:13 + | +LL | let _ = m.as_chunks_mut::(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^ const parameter `N` is zero + +error: this operation will panic at runtime + --> $DIR/const-n-is-zero.rs:33:13 + | +LL | let _ = m.as_rchunks_mut::<{ if ZERO == 0 { 0 } else { 1 } }>(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ const parameter `N` is zero + +error: this operation will panic at runtime + --> $DIR/const-n-is-zero.rs:37:13 + | +LL | let _ = s.array_windows().any(|[]| true); + | ^^^^^^^^^^^^^^^^^ const parameter `N` is zero + +error: this operation will panic at runtime + --> $DIR/const-n-is-zero.rs:41:13 + | +LL | let _ = s.iter().map_windows(|[]| true); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ const parameter `N` is zero + +error: this operation will panic at runtime + --> $DIR/const-n-is-zero.rs:45:13 + | +LL | let _ = s.iter().array_chunks().any(|[]| true); + | ^^^^^^^^^^^^^^^^^^^^^^^ const parameter `N` is zero + +error: aborting due to 8 previous errors +