Skip to content

Always try to evaluate default field values and lint if it is too generic - #163235

Open
estebank wants to merge 3 commits into
rust-lang:mainfrom
estebank:default-field-values-lint
Open

estebank wants to merge 3 commits into
rust-lang:mainfrom
estebank:default-field-values-lint

Conversation

@estebank

@estebank estebank commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

View all comments

When trying to evaluate constants, if they reference const generics they will not be evaluated. When encountering this in default field values, emit a warn-by-default lint so that API designers are not caught of guard by this behavior.

warning: field `multiline_field` has a default value that is only checked when a value of `Z` is constructed
  --> $DIR/field-references-param-accurate-span.rs:8:15
   |
LL |   struct Z<const X: usize> {
LL |       multiline_field:
LL |           ()
LL |               = {
   |  _______________^
LL | |                 f::<X>();
   | |                 -------- this can't be const-evaluated until use
LL | |                 panic!();
LL | |             },
   | |_____________^ unevaluated default value
   |
   = note: `#[warn(unevaluated_default_field_value)]` on by default
help: if this behavior is acceptable, allow the lint and preferably write a test relying on the default value
   |
LL + #[allow(unevaluated_default_field_value)]
LL | struct Z<const X: usize> {
   |

Try spans better for TooGeneric errors.

Support Span context in lints.

Fixes #146496.
Part of #132162.
Alternative to #163182.

CC @fmease @BoxyUwU

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Sep 23, 2026
@rust-log-analyzer

This comment was marked as outdated.

@estebank
estebank force-pushed the default-field-values-lint branch from d223a25 to 0e5fba6 Compare September 24, 2026 04:32
@estebank
estebank marked this pull request as ready for review September 24, 2026 04:33
@rustbot

rustbot commented Sep 24, 2026

Copy link
Copy Markdown
Collaborator

Some changes occurred to the CTFE machinery

cc @RalfJung, @oli-obk, @lcnr

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Sep 24, 2026
@rustbot

This comment was marked as outdated.

@estebank estebank left a comment •

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 don't know if this is the way we should go, but I feel like having this lint gets us most of the behavior we'd want. People can't get into a bad condition without warning, and it can be as unobtrusive as adding the allow on the crate root for those who really don't care. I just wouldn't want to have someone writing Struct<const T: u8> { field: u8 = const_fn() } and then changing that to Struct<const T: u8> { field: u8 = const_fn() + T } and then get a silent change in behavior.

View changes since this review

.last()
.map(|f| f.span)
.unwrap_or(ecx.tcx.span);
ErrorHandled::TooGeneric(span)

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.

This change here...

Comment on lines +1 to +14
warning: field `multiline_field` has a default value that is only checked when a value of `Z` is constructed
--> $DIR/field-references-param-accurate-span.rs:8:15
|
LL | struct Z<const X: usize> {
LL | // Ensure that proper context is shown in lint.
LL | multiline_field:
LL | ()
LL | = {
| _______________^
LL | | f::<X>();
| | -------- this can't be const-evaluated until use
LL | | panic!();
LL | | },
| |_____________^ unevaluated default value

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.

...is so that we can get this, where we don't just point at the whole default field value, but also to the exact place in the const that stopped it from being eagerly computed.

Comment on lines +999 to +1000
if let Some(def_id) = field.value {
if let Err(ErrorHandled::TooGeneric(span)) = tcx.const_eval_poly(def_id)

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.

Need feedback on whether just doing this is reasonable.

@fmease fmease Sep 24, 2026 •

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.

Note that strictly speaking this is equally "unprincipled" in the sense that relying on when const_eval_poly returns TooGeneric "exposes" the implementation quirks of const eval.

On main, it's particularly "egregious" because it decides whether a given program is valid or not (pre-monomorphization). Under your PR it's at least a lint that can be silenced but still it means that changes to const eval that are meant to be purely internal / mere refactorings might lead to the lint getting emitted in fewer or more cases [edit: please see also #163235 (comment)] (AFAIU but I'm a layperson when it comes to const eval's internals).

Moreover, I don't know if Rust's (pre-monormorphization) semantics already depends on when const eval returns TooGeneric or not for code that may reference generic parameters (I'm specific here since TooGeneric can also be returned on certain kinds of normalization failures IIRC).

@fmease fmease Sep 24, 2026 •

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.

To give another example (apart from the one I gave in the GH issue).

This absolutely minor change makes const_eval_poly silently bail out with TooGeneric instead of evaluating & diverging with a const panic:

  #![feature(default_field_values)]
  
  struct X<T> {
      x: () = {
-         let _: T;
+         let _x: T;
          panic!()
      },
      y: T,
  }

That's exactly what I mean by the word "unprincipled". Under your PR, changes like this still determine whether to lint or not. That's … not great IMHO.

@fmease fmease Sep 24, 2026 •

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.

On main, it's particularly "egregious" because it decides whether a given program is valid or not (pre-monomorphization). Under your PR it's at least a lint that can be silenced but still it means that changes to const eval that are meant to be purely internal / mere refactorings might lead to the lint getting emitted in fewer or more cases (AFAIU but I'm a layperson when it comes to const eval's internals).

I'm still waking up, so I'm realizing now that under your PR it of course continues to be the case that Rust's (pre-monorphization) semantics (specifically what program to accept or to reject) would depend on the whether const_eval_poly returns TooGeneric! It's just that in one case we now emit a lint (which is irrelevant when talking core semantics).

@fmease fmease Sep 24, 2026 •

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.

All that to say,

Fixes #146496.

sadly your PR does in fact not address this issue. Looking at the example I gave in that issue, uncommenting that innocuous-seeming line upstream still breaks downstream!

Moreover, the lint message doesn't make that clear since it's obviously only targeted towards explaining why the default isn't evaluated now to address the first paragraph(s) of your comment #163182 (comment). But it completely sweeps under the table the SemVer implications.

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.

When I read the first paragraph(s) of your comment #163182 (comment) I thought you meant "let's take fmease's approach from PR #163182 but also emit a lint" (which would indeed affect all structs with type or const params that have field defaults, so that might be a non-starter).

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.

We can follow your approach with a less targeted lint. We just need some feedback. The problem with your approach is that the lint will be much more noisy. My biggest concern is that addint a type param to a struct all of a sudden causes the semantics to change. That is a pretty big foot gun.

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 pushed the behavior from your draft, + an updated lint. The lint gets quite noisy, bordering on unusable, and we of course lose some opportunities to emit errors, which I am concerned about. I wonder if we could silence the lint if there was at least one construction of the struct with default values... 🤔

@fmease fmease self-assigned this Sep 24, 2026
@fmease fmease added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Sep 24, 2026
@estebank

Copy link
Copy Markdown
Contributor Author

Got concerned that not evaluating the const would cause arbitrary expressions through, but that is not the case:

warning: field `f` has a default value that is only checked when a value of `S` is constructed
 --> x.rs:4:12
  |
3 | struct S<T> {
4 |     f: T = { foo::<T>() },
  |            ^^^^^^^^^^^^^^ this can't be const-evaluated until use
  |
  = help: structs with type and const parameters only evaluate their default field values during construction, not eagerly when declared
  = note: `#[warn(unevaluated_default_field_value)]` on by default
help: if this behavior is acceptable, allow the lint and preferably write a test relying on the default value
  |
3 + #[expect(unevaluated_default_field_value)]
4 | struct S<T> {
  |

error[E0015]: cannot call non-const function `foo::<T>` in constants
 --> x.rs:4:14
  |
4 |     f: T = { foo::<T>() },
  |              ^^^^^^^^^^
  |
note: function `foo` is not const
 --> x.rs:6:1
  |
6 | fn foo<T>()->T { panic!()}
  | ^^^^^^^^^^^^^^
  = note: calls in constants are limited to constant functions, tuple structs and tuple variants

@rust-bors

This comment has been minimized.

…eric

When trying to evaluate constants, if they reference const generics they will not be evaluated. When encountering this in default field values, emit a warn-by-default lint so that API designers are not caught of guard by this behavior.

```
warning: field `multiline_field` has a default value that is only checked when a value of `Z` is constructed
  --> $DIR/field-references-param-accurate-span.rs:8:15
   |
LL |   struct Z<const X: usize> {
LL |       multiline_field:
LL |           ()
LL |               = {
   |  _______________^
LL | |                 f::<X>();
   | |                 -------- this can't be const-evaluated until use
LL | |                 panic!();
LL | |             },
   | |_____________^ unevaluated default value
   |
   = note: `#[warn(unevaluated_default_field_value)]` on by default
help: if this behavior is acceptable, allow the lint and preferably write a test relying on the default value
   |
LL + #[allow(unevaluated_default_field_value)]
LL | struct Z<const X: usize> {
   |
```

Try spans better for `TooGeneric` errors.

Support `Span` context in lints.
@estebank
estebank force-pushed the default-field-values-lint branch from f9acb05 to c6ed74c Compare October 4, 2026 14:27
@rustbot

rustbot commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator

This PR was rebased onto a different main commit. Here's a range-diff highlighting what actually changed.

Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers.

@rust-bors

rust-bors Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

☔ The latest upstream changes (presumably #163754) made this pull request unmergeable. Please resolve the merge conflicts by rebasing.

@fmease fmease unassigned mejrs Oct 4, 2026

@fmease fmease left a comment •

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.

Thanks! I've got a couple of comments that need addressing, then I'll approve the PR after a re-review.

Could you update the PR title+description & squash away the outdated approach?

View changes since this review

/// evaluated eagerly.
pub UNEVALUATED_DEFAULT_FIELD_VALUE,
Warn,
r#"detects incompatible uses of `#[sanitize(realtime = "nonblocking")]` on async functions"#,

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.

Suggested change
r#"detects incompatible uses of `#[sanitize(realtime = "nonblocking")]` on async functions"#,
r#"detects incompatible uses of `#[sanitize(realtime = "nonblocking")]` on async functions"#,
@feature_gate = default_field_values;

let variants = adt_def.variants();
let packed = adt_def.repr().packed();
let own_params_require_monomorphization =
LazyCell::new(|| tcx.generics_of(item).own_requires_monomorphization());

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.

Could you remove the LazyCell and perf it?

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.

The changes in this file are no longer necessary under the new approach, right? Could you drop them again?

pub ban: u8 = panic!("asdf"),
// ^ If we run `const_eval_poly` without restricting const params, this would be
// evaluation panicked: asdf
// FIXME: This whould WARN!

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.

Why doesn't it?

//~^ ERROR attempt to compute `130_u8 + 130_u8`, which would overflow
}

pub struct Baz<const C: u8> {

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 we deem it to spammy later on we can consider linting the type instead...

struct Z<const X: usize> {
post_mono: usize = X / 0,
post_mono: usize = X / 0, //~ WARN
//~^ ERROR attempt to divide `1_usize` by zero

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.

Wait, this shouldn't get eval'ed post mono either.

The behavior should mirror our behavior for GCI:

//@ build-pass
#![feature(generic_const_items)]
const Z<const X: usize>: usize = X / 0;

You probably need to hunt down all other places in the compiler that evaluate field defaults and add the same own_requires_monomorphization checks there to achieve that.

}

pub const fn f<const N: usize>() {
let _ = [0u8; N]; // <-- comment out this line to break downstream!

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 feel like these comments are out of context & outdated. They'd just confuse future readers.

}

pub const fn f<const N: usize>() {
let _ = [0u8; N]; // <-- comment out this line to break downstream!

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.

similarly

Suggested change
let _ = [0u8; N]; // <-- comment out this line to break downstream!
let _ = [0u8; N];

()
= { //~ WARN default value
f::<X>();
panic!(); //~ ERROR: explicit panic

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.

This should only diverge post-mono since it's instantiated in main. I guess that's not the case yet (CC my other comment) but once it is, it should warrant a comment.

@fmease fmease added the F-default_field_values `#![feature(default_field_values)]` label Oct 4, 2026
constructed"
)]
#[help(
"structs with type and const parameters only evaluate their default field values during \

@fmease fmease Oct 4, 2026 •

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.

structs

and enums; well, struct-style enum variants

View changes since the review

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

F-default_field_values `#![feature(default_field_values)]` S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The way default_field_values deals with in-scope generic parameters is slightly unprincipled

5 participants