Skip to content

Support Default Field Values, backwards compatibly#1955

Open
DJMcNab wants to merge 1 commit intodtolnay:masterfrom
DJMcNab:with-default-fields
Open

Support Default Field Values, backwards compatibly#1955
DJMcNab wants to merge 1 commit intodtolnay:masterfrom
DJMcNab:with-default-fields

Conversation

@DJMcNab
Copy link

@DJMcNab DJMcNab commented Jan 5, 2026

This PR incorporates work from @estebank in #1851. See also rust-lang/rust#132162 (comment) for context; this is the PR for "solution 2", to unblock the language feature.

Fixes #1774

#[derive(Default)]
struct Pet {
    name: Option<String>,
    age: i128 = 42,
    //        ^^^^
}

By running cargo-semver-checks --all-features (v0.45.0), I have confirmed that this is not a breaking change.

This PR has two related changes:

  1. It exposes FieldWithDefault, which is a version of Field, with the addition of the optional default field. This is then used to create a parallel hierarchy of all Syn types, with the same naming pattern. That is, it also creates FieldsNamedWithDefault, FieldsWithDefault, VariantsWithDefault, DataStructWithDefault, etc.
  2. The parsing for this is shared with Field, to allow for as much shared code as possible. There is one subtle change here; the parsing for Field (and therefore FieldsNamed, Fields, DeriveInput, etc.) will now ignore any default value provided (i.e. not give an error, but also not reflect it in the AST). This enables a silent upgrade of most derive macros to support their same codegen as if the default field values didn't exist, which will be extremely valuable in the ecosystem.

This is explicitly not #1911; instead, that issue would likely be updated to mean "revert this PR, and land #1851 as part of v3.x".

The new structs in this PR is not behind a feature flag. This is because the current infrastructure in syn-internal-codegen only supports "any" style feature flags, whereas this would need to be handled as "all" style feature flags.
I'm not going to make the changes to the codegen infrastructure myself. I would move these types behind feature flags if:

  • You say that you want this code to be in its own modules (and therefore under two feature flags); OR
  • You provide the patches to the infrastructure which means it supports the "all" style flags.

The silent ignoring of the default values would definitely be reasonable to object to. I think it's the least bad option on an ecosystem level, but I'm happy to change it (either some form of logging, or just throwing an error).

The parallel hierarchy is not complete. That is, we do not support structs with default field values inside blocks (i.e. inside functions/block expressions/etc.). That's because it requires changing so much more code. (But obviously if they exist, they'll be silently ignored).

A reasonable alternative would be to implement #1870 for those nested fields.

This is exposed as a version of the structures with a
`WithDefault` suffix, as the simpler solution has
been ruled out.

Co-Authored-By: =?UTF-8?q?Esteban=20K=C3=BCber?= <esteban@kuber.com.ar>
@nik-rev
Copy link

nik-rev commented Jan 14, 2026

Suggestion: Rather than having unique names for these v2 types, they could exist in a separate module and retain their name.

The primary motivation behind this is that it will be easier for people to migrate to supporting default field values, with a simple change:

-use syn::Variants;
+use syn::with_default::Variants;

And they won't have to do much else besides fixing the unhandled fields.

Once syn v3 comes, they can switch back:

-use syn::with_default::Variants;
+use syn::Variants;

Imagine if instead the type names were different. You'd first have to rename every type, then you'd have to rename them back

Rename table

current suggested
FieldWithDefault with_default::Field
FieldsNamedWithDefault with_default::FieldsNamed
FieldsWithDefault with_default::Fields
VariantsWithDefault with_default::Variants
DataStructWithDefaul with_default::DataStruct
DeriveInput with_default::DeriveInput

@DJMcNab
Copy link
Author

DJMcNab commented Jan 14, 2026

It's plausible, and it does have appealing properties. I don't particularly want to spend much more time working on this without David's input, however.

@estebank
Copy link

Gentle ping @dtolnay, in case you can quickly comment on the above open question even if you don't have the time to perform a full review.

Copy link
Owner

@dtolnay dtolnay left a comment

Choose a reason for hiding this comment

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

the parsing for Field (and therefore FieldsNamed, Fields, DeriveInput, etc.) will now ignore any default value provided (i.e. not give an error, but also not reflect it in the AST).

I feel that this is misconceived. This is going to result in attributes silently erasing default values written in the source code. For example the following program would print 1 0 instead of 1 1. Discovering occurrences of this in a large codebase is going to be difficult, and making sense of why their default value is not working is going to be difficult for users. I would expect instead an error explaining that code is being parsed by a macro that does not support default field values, with guidance what change to make to the macro.

#[derive(Default)]
struct Outer {
    x: u8 = 1,
}

#[tracing::instrument]
fn main() {
    #[derive(Default)]
    struct Inner {
        x: u8 = 1,
    }

    println!("{}", Outer::default().x);
    println!("{}", Inner::default().x);
}

@dtolnay
Copy link
Owner

dtolnay commented Jan 26, 2026

Regarding #1955 (comment), taking a step back, it is not evident to me that unblocking rust-lang/rust#132162 requires these proposed data structures (regardless of name) being added into syn v2. If macros are going to be required to make code changes to become compatible with default field values, and then reverse those changes when updating to syn v3, then the transitory implementation might as well be served by a different crate.

What is the subset of this PR that necessarily has to go into syn?

@DJMcNab
Copy link
Author

DJMcNab commented Jan 27, 2026

I would expect instead an error explaining that code is being parsed by a macro that does not support default field values, with guidance what change to make to the macro.

As noted in the PR description, I agree that this is the worst part of this PR, and I'm happy to change this back. In a prior commit, I did have an error for this case:

syn/src/data.rs

Lines 609 to 615 in 50da7cf

return Err(Error::new(
// TODO: Better span here?
eq.span,
"This parse does not support defining default values for struct/enum fields.\n\
You should ask its developers to port it to `DataWithDefault` if it's a macro."
.to_string(),
));

Regarding #1955 (comment), taking a step back, it is not evident to me that unblocking rust-lang/rust#132162 requires these proposed data structures (regardless of name) being added into syn v2. If macros are going to be required to make code changes to become compatible with default field values, and then reverse those changes when updating to syn v3, then the transitory implementation might as well be served by a different crate.

I absolutely considered this possibility, but the reason it hasn't been otherwise publicly discussed is as such:

  • Proposed by anyone other than yourself, before you've even engaged with the topic, it has the appearance of proposing a "hostile" fork, which has side-effects beyond the technical implications.
  • The maintenance burden of such a crate (i.e. quickly forward-porting new syn changes) isn't something I'm placed to take on, and it's not something I can volunteer someone else to do (see below). That's especially the case since there's no notice before a syn release (such a PR to make the release).
  • It's a supply-chain management nightmare, in that asking people to depend on a DJMcNab/syn_but_with_default_field_values has security implications across a wider range of the ecosystem than I'm comfortable with being responsible for. And for the above reasons, I couldn't ask for it to be a crate under (say) rust-lang, at the very least until this became publicly discussable.
  • I don't believe that there's a viable path to this being smaller than a complete fork of syn (see below)

What is the subset of this PR that necessarily has to go into syn?

In theory, the changes in the parsing modules could be used, i.e. having generics down the stack for any place which Field is used. But from a practical perspective, I don't think that's meaningfully viable; it would require massive changes, and would also add a whole pile more public API to syn, which is otherwise (I presume) intentionally private. Additionally, the changes would need to be even more invasive than they are in this PR, to allow the parsing logic for (say) expressions to be parsed with a nested default field value.
As such, I think the only viable form such a crate could take is to be a fork with #1851 applied. However, I struggle to believe that's the right approach, as it is, from a "downsides to the ecosystem" perspective, largely equivalent to you releasing syn v3.0 with the same contents, as any serious macro would be expected to port to it.

Hopefully I'm missing something which makes this more tractable...
I suppose one option would be for you to maintain this parallel version in this same repository (although I wonder if at that point, we should just call it syn v3.x? ).

@estebank
Copy link

What is the subset of this PR that necessarily has to go into syn?

At the very least, we'd need syn to ignore but successfully parse default field values. That would enable using them for the pure-Rust benefits of the feature, while still gating the "leverage the syntax for proc-macros that want to specify a default without an attribute".

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Parse default values in fields

4 participants