Skip to content

fix: dispatch on whether a comparator is implemented - #2840

Merged
zachdaniel merged 2 commits into
ash-project:mainfrom
matt-beanland:fix/comp-dispatch-on-implementation
Aug 4, 2026
Merged

fix: dispatch on whether a comparator is implemented#2840
zachdaniel merged 2 commits into
ash-project:mainfrom
matt-beanland:fix/comp-dispatch-on-implementation

Conversation

@matt-beanland

Copy link
Copy Markdown
Contributor

Contributor checklist

Leave anything that you believe does not apply unchecked.

  • I accept the AI Policy, or AI was not used in the creation of this PR.
  • Bug fixes include regression tests
  • Chores
  • Documentation changes
  • Features include unit/acceptance tests
  • Refactoring
  • Update dependencies

Summary

Fixes #2839

`Comp` cannot dispatch a protocol on two operand types directly, so it encodes
the pair into a synthetic module name and dispatches on that. Nothing today
pins which implementation a given pair actually reaches, so a change to how
that name is resolved could route every pair to the generic fallback without a
single test noticing.

Pin both outcomes with comparisons the fallback would answer differently:
Erlang term order is case-sensitive, orders a struct before a binary, and
orders `Decimal` by its fields rather than by its value.

Passes against unmodified code.
`Module.safe_concat/1` succeeds when the atom exists, which says nothing about
whether the pair has a comparator behind it. Anything that ever names
`Comparable.Type.<L>.To.<R>` creates that atom — a dynamic `Module.concat/1`, a
typespec, a doc reference — so the same comparison answers from the fallback or
raises `Protocol.UndefinedError` depending on ambient state the caller can
neither see nor control:

    Comp.compare(Ash.CiString.new("a"), Decimal.new(1))
    #=> :lt

    _ = Comparable.Type.Ash.CiString.To.Decimal
    Comp.compare(Ash.CiString.new("a"), Decimal.new(1))
    #=> ** (Protocol.UndefinedError)

Decide on the implementation instead, which is the condition the rescue was
reaching for. Pairs that do have a comparator still reach it, pinned by the
preceding commit.

This makes the answer consistent, not meaningful: the fallback still orders
`Ash.CiString` against `Decimal` by map size rather than by anything about the
values. `Comparable.compare/1` returns `:lt | :eq | :gt` or raises, so there is
no way to say a pair has no ordering.
@zachdaniel
zachdaniel merged commit 764d1d6 into ash-project:main Aug 4, 2026
51 checks passed
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.

Comp dispatches when comparator module atom exists regardless of whether comparator implementation exists

2 participants