Skip to content

Clearer errors for keyword field names and Enum! variant fields - #277

Merged
soareschen merged 1 commit into
mainfrom
shape-macro-improvements
Oct 6, 2026
Merged

soareschen merged 1 commit into
mainfrom
shape-macro-improvements

Conversation

@soareschen

Copy link
Copy Markdown
Collaborator

This PR improves the error messages of the Struct! and Enum! shape macros (#276) for three
mistakes a user is likely to make. A keyword used as a field name now gets a message that says so
and suggests the raw form. A field inside an Enum! variant now goes through exactly the checks
and messages a Struct! field does. A variant whose fields contradict its brackets says which form
the brackets require. No input that compiled before changes its meaning. Every case below was
already rejected, and only the message changes.

Background: what the shape macros do

Struct! and Enum! write the type-level shape of a struct or enum as the body of its
declaration. A shape is the Fields type that #[derive(HasFields)] generates: a list of Field
entries, one per field or variant, each tagged by its name. Generic code names a shape in bounds
such as T: FromFields<Fields = …>, and the macros let that code write the shape in the form a
Rust programmer already reads:

Struct! { name: String, age: u8 }      // Product![Field<Symbol!("name"), String>, Field<Symbol!("age"), u8>]
Enum! { Circle(f64), Rect { w: f64 } } // Sum![Field<Symbol!("Circle"), f64>, Field<Symbol!("Rect"), Struct! { w: f64 }>]

Each macro reads its body much as rustc reads a declaration, then rejects the parts a shape has
no use for (attributes, visibility, _, duplicate names, discriminants) with its own message. This
PR closes three gaps in that parsing.

What changes for the user

A keyword field name says what is wrong. A keyword is not an identifier, so Struct! read
type: u8 as a positional entry, and type then failed to parse as a type. The message listed
every token a type may start with and never mentioned the name. A keyword that has no raw form,
such as self, failed with an unrelated "expected ,".

Input Before After
Struct! { type: u8 } expected one of: `for`, parentheses, `fn`, … `type` is a keyword: write the field name as `r#type`
Struct! { self: u8 } expected `,` `self` cannot be a field name

self, Self, super, and crate get the second message, since r#self and the others are not
valid Rust.

An Enum! variant's fields follow the Struct! rules, with the same messages. Enum! read a
variant's fields with syn's own struct-body parsers, which skip the literal and keyword checks
that Struct! runs. The same mistake therefore got a different message inside a variant:

Input Before After
Enum! { V(1) } expected one of: `for`, parentheses, `fn`, … expected a type: a type-level shape lists field types, not values
Enum! { V { a: 1 } } expected one of: `for`, parentheses, `fn`, … expected a type: a type-level shape lists field types, not values
Enum! { V { type: u8 } } expected identifier, found keyword `type` `type` is a keyword: write the field name as `r#type`

A variant whose fields contradict its brackets says which form the brackets require. Inside
Enum!, a variant's brackets keep their Rust meaning: braces hold named fields and parentheses hold
bare types. This differs from the brackets around the whole Struct! or Enum! invocation, which a
procedural macro cannot see and which therefore do not matter. A mismatch used to surface as a
missing punctuation token:

Input Before After
Enum! { V { u8 } } expected `:` a variant in braces holds named fields: write each entry as `name: Type`
Enum! { V(a: u8) } expected `,` a variant in parentheses holds bare types: write named fields in braces, as in `Variant { name: Type }`

How it works

The changes are in
functions/shape/parse_fields.rs,
with a small change to parse_variant in
types/shape/enum_type.rs.

Keyword detection sits in the entry classifier. peek_named_entry decides whether an entry is
name: Type or a bare type before parsing it. Its peek(Ident) test, which syn defines to
exclude keywords, stays as it was. When that test fails, the classifier now also peeks with
Ident::peek_any, and a keyword followed by a single : (not ::) is reported at the keyword's
span. The existing checks are unchanged. A raw identifier still counts as
a name, _ still reaches its own rejection, and a path type such as crate::Foo or Self::Assoc
is still a positional field, because its : begins a ::.

Enum! variants now share the Struct! loop. The loop that reads a Struct! body is factored
into a private parse_entries, behind two entry points:

  • parse_shape_fields reads a Struct! body. The first entry decides the form, and a later entry of
    the other form is reported as mixed, exactly as before.
  • parse_variant_fields reads the inside of a variant's braces or parentheses. The delimiter fixes
    the form, and an entry of the other form is reported against that delimiter.

Every entry in both paths goes through peek_named_entry and then validate_shape_fields, so a
field rule and its message cannot differ between a struct body and a variant. An empty variant list
keeps its delimiter's form (V() parses as empty positional fields, V {} as empty named fields),
as syn's parsers did before. All three spellings, V, V(), and V {}, still encode to the same
Nil payload.

A keyword written as a shape's field name, as in `Struct! { type: u8 }`,
was read as a positional entry and failed in syn's type parser with a
message listing every token a type may start with. It now reports
"`type` is a keyword: write the field name as `r#type`", or that the
name cannot be a field for `self`, `Self`, `super`, and `crate`.

`Enum!` variant fields now go through `parse_variant_fields`, the loop
that reads a `Struct!` body with the form fixed by the variant's
delimiter. A literal or a keyword name inside a variant gets the same
message as in a `Struct!` body, and an entry that contradicts the
delimiter, such as `V { u8 }` or `V(a: u8)`, says which form the
brackets require. Empty `V()` and `V {}` keep their delimiter's form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@soareschen
soareschen merged commit bb8d764 into main Oct 6, 2026
@soareschen
soareschen deleted the shape-macro-improvements branch October 6, 2026 14:29
@soareschen soareschen added the AI label Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant