Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ where
.map(|pred| goal.with(cx, pred)),
);

let normalized = match inherent.kind {
let normalized: I::Term = match inherent.kind {
ty::AliasTermKind::InherentTy { def_id } => {
cx.type_of(def_id.into()).instantiate(cx, inherent_args).skip_norm_wip().into()
}
Expand All @@ -74,6 +74,7 @@ where
}
kind => panic!("expected inherent alias, found {kind:?}"),
};

self.instantiate_normalizes_to_term(goal, normalized);
self.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
}
Expand Down
14 changes: 14 additions & 0 deletions compiler/rustc_next_trait_solver/src/solve/normalizes_to/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,25 @@ where
/// We know `term` to always be a fully unconstrained inference variable, so
/// `eq` should never fail here. However, in case `term` contains aliases, we
/// emit nested `AliasRelate` goals to structurally normalize the alias.
///
/// Additionally, when `term` is a const, this registers a `ConstArgHasType`
/// goal to ensure that the const value's type matches the type of the
/// alias it was normalized from, preventing ICEs from type mismatches.
Copy link
Copy Markdown
Contributor

@khyperia khyperia Jun 5, 2026

Choose a reason for hiding this comment

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

sorry for butting in kinda late - some optional feedback here:

The way that I would expect this to work is that things are wfchecked at definition site before they're attempted to be evaluated (and so would be tainted, and this usage would never even get to this point of attempting to normalize it). You write in the PR description:

The existing ConstArgHasType check in wfcheck::check_type_const does catch this at the definition site, but due to query evaluation ordering, the normalization path can reach MIR validation before that check has run.

I presume there is some cursed arcane reason why we cannot easily fix the "due to query evaluation ordering" part of that sentence, and why this approach of slapping on ConstArgHasType obligations to the normalization result is needed.

Would you mind writing that cursed arcane reason into a comment here? I find comments of the style "hey reader, you're not insane, this is indeed wonky and not what you would expect, because [...]" to be extremely helpful ❤️

View changes since the review

pub fn instantiate_normalizes_to_term(
&mut self,
goal: Goal<I, NormalizesTo<I>>,
term: I::Term,
) {
if let Some(ct) = term.as_const() {
let cx = self.cx();
let alias = goal.predicate.alias;
let expected_ty =
cx.type_of(alias.def_id()).instantiate(cx, alias.args).skip_norm_wip();
self.add_goal(
GoalSource::Misc,
goal.with(cx, ty::ClauseKind::ConstArgHasType(ct, expected_ty)),
);
}
self.eq(goal.param_env, goal.predicate.term, term)
.expect("expected goal term to be fully unconstrained");
}
Expand Down
15 changes: 14 additions & 1 deletion compiler/rustc_trait_selection/src/traits/normalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ impl<'a, 'b, 'tcx> AssocTypeNormalizer<'a, 'b, 'tcx> {
}),
);
self.depth += 1;
let res = if free.kind.is_type() {
let res: ty::Term<'tcx> = if free.kind.is_type() {
infcx
.tcx
.type_of(free.def_id())
Expand All @@ -356,6 +356,19 @@ impl<'a, 'b, 'tcx> AssocTypeNormalizer<'a, 'b, 'tcx> {
.fold_with(self)
.into()
};
// When normalizing a free const alias, register a `ConstArgHasType`
// obligation to ensure the const value's type matches the declared type.
if let Some(ct) = res.as_const() {
let expected_ty =
infcx.tcx.type_of(free.def_id()).instantiate(infcx.tcx, free.args).skip_norm_wip();
self.obligations.push(Obligation::with_depth(
infcx.tcx,
self.cause.clone(),
self.depth,
self.param_env,
ty::ClauseKind::ConstArgHasType(ct, expected_ty),
));
}
self.depth -= 1;
res
}
Expand Down
49 changes: 48 additions & 1 deletion compiler/rustc_trait_selection/src/traits/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::ops::ControlFlow;
use rustc_data_structures::sso::SsoHashSet;
use rustc_data_structures::stack::ensure_sufficient_stack;
use rustc_errors::ErrorGuaranteed;
use rustc_hir::def_id::DefId;
use rustc_hir::lang_items::LangItem;
use rustc_infer::infer::DefineOpaqueTypes;
use rustc_infer::infer::resolve::OpportunisticRegionResolver;
Expand Down Expand Up @@ -487,6 +488,30 @@ fn normalize_to_error<'a, 'tcx>(
Normalized { value: new_value, obligations }
}

/// When normalizing a const alias, register a `ConstArgHasType` obligation
/// to ensure the const value's type matches the declared type.
fn push_const_arg_has_type_obligation<'tcx>(
tcx: TyCtxt<'tcx>,
obligations: &mut PredicateObligations<'tcx>,
cause: &ObligationCause<'tcx>,
depth: usize,
param_env: ty::ParamEnv<'tcx>,
term: Term<'tcx>,
def_id: DefId,
args: ty::GenericArgsRef<'tcx>,
) {
if let Some(ct) = term.as_const() {
let expected_ty = tcx.type_of(def_id).instantiate(tcx, args).skip_norm_wip();
obligations.push(Obligation::with_depth(
tcx,
cause.clone(),
depth,
param_env,
ty::ClauseKind::ConstArgHasType(ct, expected_ty),
));
}
}

/// Confirm and normalize the given inherent projection.
// FIXME(mgca): While this supports constants, it is only used for types by default right now
#[instrument(level = "debug", skip(selcx, param_env, cause, obligations))]
Expand Down Expand Up @@ -554,6 +579,17 @@ pub fn normalize_inherent_projection<'a, 'b, 'tcx>(
tcx.const_of_item(alias_term.def_id()).instantiate(tcx, args).skip_norm_wip().into()
};

push_const_arg_has_type_obligation(
tcx,
obligations,
&cause,
depth + 1,
param_env,
term,
alias_term.def_id(),
args,
);

let mut term = selcx.infcx.resolve_vars_if_possible(term);
if term.has_aliases() {
term =
Expand Down Expand Up @@ -2049,7 +2085,18 @@ fn confirm_impl_candidate<'cx, 'tcx>(
Progress { term: err, obligations: nested }
} else {
assoc_term_own_obligations(selcx, obligation, &mut nested);
Progress { term: term.instantiate(tcx, args).skip_norm_wip(), obligations: nested }
let instantiated_term: Term<'tcx> = term.instantiate(tcx, args).skip_norm_wip();
push_const_arg_has_type_obligation(
tcx,
&mut nested,
&obligation.cause,
obligation.recursion_depth + 1,
obligation.param_env,
instantiated_term,
assoc_term.item.def_id,
args,
);
Progress { term: instantiated_term, obligations: nested }
};
Ok(Projected::Progress(progress))
}
Expand Down
41 changes: 39 additions & 2 deletions compiler/rustc_traits/src/normalize_projection_ty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use rustc_infer::infer::TyCtxtInferExt;
use rustc_infer::infer::canonical::{Canonical, QueryResponse};
use rustc_infer::traits::PredicateObligations;
use rustc_middle::query::Providers;
use rustc_middle::ty::{ParamEnvAnd, TyCtxt};
use rustc_middle::ty::{self, ParamEnvAnd, TyCtxt};
use rustc_trait_selection::error_reporting::InferCtxtErrorExt;
use rustc_trait_selection::infer::InferCtxtBuilderExt;
use rustc_trait_selection::traits::query::normalize::NormalizationResult;
Expand All @@ -19,6 +19,25 @@ pub(crate) fn provide(p: &mut Providers) {
};
}

/// If `normalized_term` is a const, returns a `ConstArgHasType` obligation
/// to verify that the const value's type matches the alias's declared type.
/// Returns `None` if the term is a type rather than a const.
fn const_arg_has_type_obligation<'tcx>(
tcx: TyCtxt<'tcx>,
param_env: ty::ParamEnv<'tcx>,
normalized_term: ty::Term<'tcx>,
goal: ty::AliasTerm<'tcx>,
) -> Option<traits::PredicateObligation<'tcx>> {
let ct = normalized_term.as_const()?;
let expected_ty = tcx.type_of(goal.def_id()).instantiate(tcx, goal.args).skip_norm_wip();
Some(traits::Obligation::new(
tcx,
ObligationCause::dummy(),
param_env,
ty::ClauseKind::ConstArgHasType(ct, expected_ty),
))
}

fn normalize_canonicalized_projection<'tcx>(
tcx: TyCtxt<'tcx>,
goal: CanonicalAliasGoal<'tcx>,
Expand All @@ -40,6 +59,12 @@ fn normalize_canonicalized_projection<'tcx>(
0,
&mut obligations,
);
obligations.extend(const_arg_has_type_obligation(
tcx,
param_env,
normalized_term,
goal.into(),
));
ocx.register_obligations(obligations);
// #112047: With projections and opaques, we are able to create opaques that
// are recursive (given some generic parameters of the opaque's type variables).
Expand Down Expand Up @@ -86,11 +111,17 @@ fn normalize_canonicalized_free_alias<'tcx>(
},
);
ocx.register_obligations(obligations);
let normalized_term = if goal.kind.is_type() {
let normalized_term: ty::Term<'tcx> = if goal.kind.is_type() {
tcx.type_of(goal.def_id()).instantiate(tcx, goal.args).skip_norm_wip().into()
} else {
tcx.const_of_item(goal.def_id()).instantiate(tcx, goal.args).skip_norm_wip().into()
};
ocx.register_obligations(const_arg_has_type_obligation(
tcx,
param_env,
normalized_term,
goal.into(),
));
Ok(NormalizationResult { normalized_term })
},
)
Expand All @@ -116,6 +147,12 @@ fn normalize_canonicalized_inherent_projection<'tcx>(
0,
&mut obligations,
);
obligations.extend(const_arg_has_type_obligation(
tcx,
param_env,
normalized_term,
goal.into(),
));
ocx.register_obligations(obligations);

Ok(NormalizationResult { normalized_term })
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//@ compile-flags: -Zvalidate-mir -Znext-solver

#![feature(min_generic_const_args)]
#![expect(incomplete_features)]

type const X: usize = const { N };
//~^ ERROR type annotations needed

type const N: usize = "this isn't a usize";
//~^ ERROR the constant `"this isn't a usize"` is not of type `usize`

fn main() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
error[E0284]: type annotations needed: cannot normalize `X::{constant#0}`
--> $DIR/type-const-free-anon-const-mismatch.rs:6:1
|
LL | type const X: usize = const { N };
| ^^^^^^^^^^^^^^^^^^^ cannot normalize `X::{constant#0}`

error: the constant `"this isn't a usize"` is not of type `usize`
--> $DIR/type-const-free-anon-const-mismatch.rs:9:1
|
LL | type const N: usize = "this isn't a usize";
| ^^^^^^^^^^^^^^^^^^^ expected `usize`, found `&'static str`

error: aborting due to 2 previous errors

For more information about this error, try `rustc --explain E0284`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
error: the constant `"this isn't a usize"` is not of type `usize`
--> $DIR/type-const-free-value-type-mismatch.rs:9:1
|
LL | type const N: usize = "this isn't a usize";
| ^^^^^^^^^^^^^^^^^^^ expected `usize`, found `&'static str`

error[E0308]: mismatched types
--> $DIR/type-const-free-value-type-mismatch.rs:12:11
|
LL | fn f() -> [u8; const { N }] {}
| - ^^^^^^^^^^^^^^^^^ expected `[u8; const { N }]`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression

error: aborting due to 2 previous errors

For more information about this error, try `rustc --explain E0308`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
error: the constant `"this isn't a usize"` is not of type `usize`
--> $DIR/type-const-free-value-type-mismatch.rs:9:1
|
LL | type const N: usize = "this isn't a usize";
| ^^^^^^^^^^^^^^^^^^^ expected `usize`, found `&'static str`

error[E0284]: type annotations needed: cannot normalize `f::{constant#0}`
--> $DIR/type-const-free-value-type-mismatch.rs:12:11
|
LL | fn f() -> [u8; const { N }] {}
| ^^^^^^^^^^^^^^^^^ cannot normalize `f::{constant#0}`

error[E0308]: mismatched types
--> $DIR/type-const-free-value-type-mismatch.rs:12:11
|
LL | fn f() -> [u8; const { N }] {}
| - ^^^^^^^^^^^^^^^^^ expected `[u8; _]`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression

error: aborting due to 3 previous errors

Some errors have detailed explanations: E0284, E0308.
For more information about an error, try `rustc --explain E0284`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#![feature(min_generic_const_args)]
#![expect(incomplete_features)]

//@ revisions: current next
//@ ignore-compare-mode-next-solver (explicit revisions)
//@[next] compile-flags: -Znext-solver
//@ compile-flags: -Zvalidate-mir

type const N: usize = "this isn't a usize";
//~^ ERROR the constant `"this isn't a usize"` is not of type `usize`

fn f() -> [u8; const { N }] {}
//[current]~^ ERROR mismatched types [E0308]
//[next]~^^ ERROR type annotations needed
//[next]~| ERROR mismatched types [E0308]

fn main() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
error: the constant `"this isn't a usize"` is not of type `usize`
--> $DIR/type-const-inherent-value-type-mismatch.rs:14:5
|
LL | type const N: usize = "this isn't a usize";
| ^^^^^^^^^^^^^^^^^^^ expected `usize`, found `&'static str`

error[E0308]: mismatched types
--> $DIR/type-const-inherent-value-type-mismatch.rs:18:11
|
LL | fn f() -> [u8; const { Struct::N }] {}
| - ^^^^^^^^^^^^^^^^^^^^^^^^^ expected `[u8; const { Struct::N }]`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression

error: aborting due to 2 previous errors

For more information about this error, try `rustc --explain E0308`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
error[E0284]: type annotations needed: cannot normalize `f::{constant#0}`
--> $DIR/type-const-inherent-value-type-mismatch.rs:18:11
|
LL | fn f() -> [u8; const { Struct::N }] {}
| ^^^^^^^^^^^^^^^^^^^^^^^^^ cannot normalize `f::{constant#0}`

error: the constant `"this isn't a usize"` is not of type `usize`
--> $DIR/type-const-inherent-value-type-mismatch.rs:14:5
|
LL | type const N: usize = "this isn't a usize";
| ^^^^^^^^^^^^^^^^^^^ expected `usize`, found `&'static str`

error[E0308]: mismatched types
--> $DIR/type-const-inherent-value-type-mismatch.rs:18:11
|
LL | fn f() -> [u8; const { Struct::N }] {}
| - ^^^^^^^^^^^^^^^^^^^^^^^^^ expected `[u8; _]`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression

error: aborting due to 3 previous errors

Some errors have detailed explanations: E0284, E0308.
For more information about an error, try `rustc --explain E0284`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Regression test for https://github.com/rust-lang/rust/issues/152962

//@ revisions: current next
//@ ignore-compare-mode-next-solver (explicit revisions)
//@[next] compile-flags: -Znext-solver
//@ compile-flags: -Zvalidate-mir

#![feature(min_generic_const_args)]
#![expect(incomplete_features)]

struct Struct;

impl Struct {
type const N: usize = "this isn't a usize";
//~^ ERROR the constant `"this isn't a usize"` is not of type `usize`
}

fn f() -> [u8; const { Struct::N }] {}
//~^ ERROR mismatched types [E0308]
//[next]~| ERROR type annotations needed

fn main() {}
Loading
Loading