Skip to content

Make Zero/One subtypes of Real - #54

Merged
lkdvos merged 2 commits into
mainfrom
isreal
Sep 21, 2026
Merged

lkdvos merged 2 commits into
mainfrom
isreal

Conversation

@lkdvos

@lkdvos lkdvos commented Sep 10, 2026 •

Copy link
Copy Markdown
Member

This PR changes the Zero/One implementations, which is triggered because the new Julia 1.13's mul! calls isreal on the α/β coefficients.
This aims to fix that, and meanwhile also adds a bunch of other functions that I (and my robot friend) could come up with in an attempt to avoid this in the future.

I made a judgement call and changed these to subtype Real instead of Number, which means (I think?) that this is now a breaking change, but I'm open to suggestions here.

Below is some comments by Claude:


Paid for in this PR:

  • 26 ambiguities against Base's Real-vs-Complex methods → disambiguation block mirroring the existing ::Number bodies.
  • hash regresses to a MethodError and isfinite stops working → both defined.
  • <(x::T, y::T) where {T <: Real} is a Base no-op error and promote(Zero(), Zero()) is a fixed point → same-type isless/</<= and sign need explicit methods.
  • min/max defined to preserve the singletons instead of promoting to Bool, so α === One() fast paths keep firing.
  • Dropped the two convert(::Type{T}, ...) where {T <: Number} methods. Once Zero <: Real they supersede convert(::Type{T}, x::Number) for real argument types, taking invalidations from 6 to 201. Base's convert(::Type{T}, x::Number) = T(x)::T routes through the constructors instead — equivalent, and 0 invalidations.

Also fixes a pre-existing bug: isequal(Zero(), 0) was true while the hashes differed, so Dict(0 => :a)[Zero()] threw a KeyError.

Verification

check result
Full suite, 1.12 pass, incl. enzyme, mooncake, chainrules
test/onezero.jl on 1.10 / 1.12 / 1.13 pass
Aqua.test_all on 1.10 / 1.12 / 1.13 11/11, 0 ambiguities
Invalidations, 1.10 & 1.12 0 (6 on main, 201 with a naive supertype swap)
KrylovKit linsolve/eigsolve, real + complex pass

New tests cover the 5-arg mul! cases directly (mul!(C, A, B, One(), Zero()) and mul!(C, A, B, One(), One()), real and complex), plus axpy!/rmul!/lmul!, so whatever 1.13 calls on the coefficients is pinned.

Breaking — a supertype change alters downstream dispatch for anything with separate ::Real and ::Number methods. Version bumped to 0.7.0; worth a heads-up to KrylovKit and TensorKit before tagging.

🤖 Generated with Claude Code

@lkdvos
lkdvos requested a review from Jutho September 10, 2026 17:56
@lkdvos

lkdvos commented Sep 10, 2026

Copy link
Copy Markdown
Member Author

Errors are Enzyme + Julia 1.13 - related and not related to the changes here.

@kshyatt

kshyatt commented Sep 15, 2026

Copy link
Copy Markdown
Member

Hope springs eternal that Enzyme fixed this somehow in the last 5 days

@codecov

codecov Bot commented Sep 15, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 57.14286% with 21 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/onezero.jl 57.14% 21 Missing ⚠️
Files with missing lines Coverage Δ
src/onezero.jl 67.02% <57.14%> (-5.53%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@lkdvos

lkdvos commented Sep 15, 2026

Copy link
Copy Markdown
Member Author

@Jutho do you still want to have a look here or is this ok? The main annoyance is it being breaking, do you think that is fair/warranted?

@leburgel

Copy link
Copy Markdown
Member

Any news on this?

Comment thread src/onezero.jl
Comment on lines +115 to +120
Base.Bool(::One) = true
Base.Bool(::Zero) = false
Base.Complex(::One) = Complex(true)
Base.Complex(::Zero) = Complex(false)
Base.Complex{T}(::One) where {T <: Real} = Complex{T}(one(T))
Base.Complex{T}(::Zero) where {T <: Real} = Complex{T}(zero(T))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Aside from Complex, which is abstract, aren't Bool and Complex{T} not covered by the (T:Type{<:Number}) constructors on line 90 - 91?

@lkdvos lkdvos Sep 21, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

These are all here to fix dispatch, in this case because Base defines Complex{T}(x::Real) where {T <: Real}, which would be ambiguous with (T::Type{<:Number})(::One).

(This is true for all methods under this header)

@Jutho

Jutho commented Sep 21, 2026

Copy link
Copy Markdown
Member

I am fine with this, making them <:Real would have been the sensible choice from the start. I guess all the new method definitions are optimised to be both necessary and creating no/the least possible amount of invalidations?

@lkdvos

lkdvos commented Sep 21, 2026

Copy link
Copy Markdown
Member Author

The invalidations workflow actually is checking that this is not creating any new invalidations, and indeed the methods are meant to have everything working as much as possible without ambiguities.

lkdvos and others added 2 commits September 21, 2026 11:48
Julia 1.13's `mul!` calls `isreal` on the `α`/`β` coefficients, which forced
`Base.isreal` methods on `Zero`/`One` in the previous commit. That gap was not
isolated: because the singletons were `<: Number` but not `<: Real`, they
inherited almost none of Base's numeric fallbacks, and `real`, `imag`, `abs`,
`abs2`, `sign`, `signbit`, `isinteger`, `isinf`, `isless`/`<`/`<=` (hence `min`,
`max`, `cmp`, `sort`, `clamp`) and `isapprox` all threw `MethodError`.

Change the supertype instead of chasing these one release at a time. `isreal`,
`real`, `imag`, `abs`, `abs2`, `signbit`, `angle`, `sqrt`, `isnan`, mixed-type
ordering, `isapprox` and `Complex(One(), Zero())` now come from `Real`, so the
two `isreal` methods are dropped again.

This has to be paid for in one batch:

- 26 method ambiguities appear against Base's Real-vs-Complex methods; add a
  disambiguation block whose bodies mirror the existing `::Number` methods.
- `hash` regresses to a `MethodError` and `isfinite` stops working; define both.
- `<(x::T, y::T) where {T <: Real}` is a Base no-op error and
  `promote(Zero(), Zero())` is a fixed point, so same-type `isless`/`<`/`<=`
  and `sign` need explicit methods.
- `min`/`max` are defined to preserve the singletons rather than promote to
  `Bool`, keeping `α === One()` fast paths alive.
- Drop the two `convert(::Type{T}, ...) where {T <: Number}` methods. Once
  `Zero <: Real` they supersede `convert(::Type{T}, x::Number)` for real
  argument types and take invalidations from 6 to 201. Base's own
  `convert(::Type{T}, x::Number) = T(x)::T` routes through the constructors
  instead, which is equivalent and leaves 0 invalidations.

Also fixes a pre-existing bug: `isequal(Zero(), 0)` was true while the hashes
differed, so `Dict(0 => :a)[Zero()]` threw a `KeyError`.

Tested on 1.10, 1.12 and 1.13, including the 5-arg `mul!` cases that motivated
this. Breaking, since downstream dispatch on `::Real` vs `::Number` changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lkdvos
lkdvos enabled auto-merge (squash) September 21, 2026 15:48
@lkdvos
lkdvos merged commit b798587 into main Sep 21, 2026
9 of 10 checks passed
@lkdvos
lkdvos deleted the isreal branch September 21, 2026 16:27
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.

4 participants