Skip to content

Switch from HashSet to BTreeSet for reproducibility - #257

Open
sietseringers wants to merge 3 commits into
greyblake:masterfrom
sietseringers:reproducibility
Open

Switch from HashSet to BTreeSet for reproducibility#257
sietseringers wants to merge 3 commits into
greyblake:masterfrom
sietseringers:reproducibility

Conversation

@sietseringers

@sietseringers sietseringers commented Aug 24, 2026

Copy link
Copy Markdown

This MR makes macro expansion reproducible, to facilitate reproducible builds.

Previous behaviour

#[nutype(...)] collected derive traits into a HashSet, whose iteration order is random. The result of this was that the order of #[derive(...)] entries and of the generated impl blocks in the emitted code differed every time the macro ran. For downstream crates, this means that they cannot have reproducible builds.

Fix

Traits now travel through a BTreeSet instead of a HashSet, and the five *DeriveTrait enums (any, decimal, float, integer, string) derive PartialOrd, Ord.

The derived Ord ranks variants by declaration order, so the emitted order is now the order the variants are written in.

The HashSets that remain (common/validate.rs, float/validate.rs) are used only for contains and the boolean result of insert. They are never iterated, so they cannot leak ordering into the output or into which error gets reported first.

Testing

nutype_macros/src/expansion_tests.rs expands the same input 100 times and asserts every result is byte-identical, with one test per code path: string, integer, float, any, decimal, and conditional (cfg_attr) derives. The conditional case has its own test because process_conditional_derives builds its sets separately from the unconditional path.

string/generate/traits/mod.rs also gained a unit test pinning the split of traits into transparent vs. irregular groups to declaration order, using an input deliberately built in neither declaration nor alphabetical order.

Note

Making this testable required one production change: parse_meta now calls syn::parse2 instead of syn::parse(ts.into()). syn::parse is defined as parse2 preceded by a proc_macro2 -> proc_macro -> proc_macro2 round-trip, which panics outside a real proc-macro invocation. Consequentially, expand_nutype could not be called from a unit test. The change drops the round-trip; the parsing call underneath is the same one.

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.

1 participant