fix: stabilize field order in Ash.Expr.determine_map_type/1#2679
Merged
zachdaniel merged 1 commit intoApr 17, 2026
Merged
Conversation
Iterating a literal map expr (e.g. `expr(%{a: id, b: name})`) produces
a `{Ash.Type.Map, fields: [...]}` whose field order depends on Erlang
map iteration, which for small flatmaps is driven by internal atom
term ordering and varies between BEAM loads. Downstream consumers
that render ordered output (e.g. ash_typescript codegen) see flaky,
shuffled results across compiles.
Sort the resulting fields by atom name before returning so the output
is stable across compiles. The contract is only "deterministic", not
"alphabetical forever" — consumers should not rely on the specific
order, only that it doesn't change.
Torkan
approved these changes
Apr 17, 2026
zachdaniel
approved these changes
Apr 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A literal map expression inside an
:auto-typed calculation — e.g.currently produces a
{Ash.Type.Map, fields: [...]}whose field order is non-deterministic across compiles.Ash.Expr.determine_map_type/1receives the map and iterates it withEnum.reduce_while. For small Erlang flatmaps that dispatches to:maps.to_list/1, which walks by internal atom term order — i.e. atom-table index order — which varies between BEAM loads (warm_build/devvs clean_build/test). The resultingfields:keyword list is stable within a single compile but can reshuffle between compiles.Impact
Any consumer that renders ordered output from
calc.constraints[:fields]sees flaky results. Concrete case: ash-project/ash_typescript#67 — generated TypeScript interfaces whose field order changes between cold and warm builds, polluting snapshot diffs.Fix
One line: sort the accumulated fields by
Atom.to_string(key)before returning.Atom.to_string/1is deterministic across BEAM loads (atom names are part of the atom's identity), unlike term ordering. Replaces theEnum.reverse/1that was there purely to counter the reduce-into-head pattern — sorting is end-to-end deterministic and doesn't need the reverse.Contract
The guarantee is deterministic, not alphabetical. Consumers must not depend on the specific ordering — only on stability across compiles. Today nothing downstream can rely on a specific order (there isn't one), so the behavior change is observable only as "the flaky thing stopped being flaky."
Test plan
mix test test/type/auto_type_test.exs— 67 tests, 0 failures:ordered_cardcalc with source keysz, a, mexpects[:a, :m, :z]outputmix format --check-formattedclean