From 67b766a0e107c408a1015e8d185ac8f9f3d3adc8 Mon Sep 17 00:00:00 2001 From: Max Horn Date: Wed, 15 Jan 2025 01:18:07 +0100 Subject: [PATCH] Add ConformanceTests._implements trait ... to allow fine control over which things to test in the conformance tests and which not. --- src/ConformanceTests.jl | 57 ++++++++++++++++++++++++++++++ src/FreeAssociativeAlgebra.jl | 2 ++ src/MPoly.jl | 6 ++++ src/MatRing.jl | 4 +++ src/NCRings.jl | 2 ++ src/Poly.jl | 5 +++ src/algorithms/GenericFunctions.jl | 9 +++++ src/algorithms/LaurentPoly.jl | 4 +++ src/generic/LaurentMPoly.jl | 4 +++ 9 files changed, 93 insertions(+) diff --git a/src/ConformanceTests.jl b/src/ConformanceTests.jl index d47f9db4fe..7c8a276333 100644 --- a/src/ConformanceTests.jl +++ b/src/ConformanceTests.jl @@ -43,6 +43,63 @@ It is supposed to be implemented in the src file of the respective type. function generate_element end +############################################################################### +# +# `implements` trait +# +############################################################################### + +# Calling `_implements(T, f)` checks whether a "sensible" method for the unary +# function `f` is implemented for inputs of type `T`. The argument order is +# meant to be similar to e.g. `isa`, and thus indicates `T implements f`. +# +# For example, `_implements(MyRingElem, is_unit)` should return true if +# invoking `is_unit` on elements of type `MyRingElem` is supported. +# +# The generic fallback uses `hasmethod`. However, this may return `true` in +# cases where it shouldn't, as we often provide generic methods for that rely +# on other methods being implemented -- either for the same type, or for types +# derived from it. For example the `is_nilpotent(::PolyElem{T})` method needs +# `is_nilpotent(::T)` in order to work. +# +# To reflect this, additional `_implements` methods need to be provided. +# We currently do this for at least the following functions: +# - factor +# - is_irreducible +# - is_nilpotent +# - is_squarefree +# - is_unit +# - is_zero_divisor +# +_implements(::Type{T}, f::Any) where {T} = hasmethod(f, Tuple{T}) + +# Alternatively, the first argument can be a concrete object. By default we +# then redispatch to the type based version. But one may also choose to +# implement custom methods for this: certain operations will only work for +# *some* instances. E.g. for `Z/nZ` it may happen that for `n` a prime we can +# perform a certain operation, but not if `n` is composite. +# +# In that case the recommendation is that `_implements` invoked on the type +# returns `false`, but invoked on a concrete instance of a type, it may use +# specifics of the instance to also return `true` if appropriate. +function _implements(x::T, f::Any) where {T} + @assert !(x isa Type) # paranoia + return _implements(T, f) +end + +# helper for `_implements` which checks if `f` has a method explicitly for +# a concrete type `T` (i.e. not a generic method that can be specialized to `T` +# but really one that is implement for `T` and `T` only). +function _implements_directly(::Type{T}, f::Any) where {T} + isconcretetype(T) || return false # TODO: drop this? + meth = methods(f, Tuple{T}) + # TODO: deal with type parameters: if `T` is `FreeAssociativeAlgebraElem{ZZRingElem}` + # and `f` has a method for `FreeAssociativeAlgebraElem` then we should still consider + # this a match. + return any(m -> m.sig == Tuple{typeof(f), T}, meth) +end + + ############################################################################### # # The following function stubs' actual implementations are in the folder `ext/TestExt/`. diff --git a/src/FreeAssociativeAlgebra.jl b/src/FreeAssociativeAlgebra.jl index 49548f31bf..9ec98ae165 100644 --- a/src/FreeAssociativeAlgebra.jl +++ b/src/FreeAssociativeAlgebra.jl @@ -136,6 +136,8 @@ function is_unit(a::FreeAssociativeAlgebraElem{T}) where T end end +ConformanceTests._implements(::Type{FreeAssociativeAlgebraElem{T}}, f::typeof(is_unit)) where T = is_domain_type(T) || _implements_directly(T, f) + ############################################################################### # # Hashing diff --git a/src/MPoly.jl b/src/MPoly.jl index b7b21ff5fa..a0f5b27dcb 100644 --- a/src/MPoly.jl +++ b/src/MPoly.jl @@ -437,6 +437,8 @@ function is_unit(f::T) where {T <: MPolyRingElem} return constant_term_is_unit # handles the case that there is no constant term end +ConformanceTests._implements(::Type{MPolyRingElem{T}}, ::typeof(is_unit)) where T = _implements(T, is_unit) && _implements(T, is_nilpotent) + function content(a::MPolyRingElem{T}) where T <: RingElement z = zero(coefficient_ring(a)) for c in coefficients(a) @@ -452,12 +454,16 @@ function is_nilpotent(f::T) where {T <: MPolyRingElem} return all(is_nilpotent, coefficients(f)) end +ConformanceTests._implements(::Type{MPolyRingElem{T}}, ::typeof(is_nilpotent)) where T = _implements(T, is_nilpotent) + function is_zero_divisor(x::MPolyRingElem{T}) where T <: RingElement is_domain_type(T) && return is_zero(x) return is_zero_divisor(content(x)) end +ConformanceTests._implements(::Type{MPolyRingElem{T}}, ::typeof(is_zero_divisor)) where T = _implements(T, is_zero_divisor) + function is_zero_divisor_with_annihilator(a::MPolyRingElem{T}) where T <: RingElement f, b = is_zero_divisor_with_annihilator(content(a)) return f, parent(a)(b) diff --git a/src/MatRing.jl b/src/MatRing.jl index 9c2655bb76..6a3d271e83 100644 --- a/src/MatRing.jl +++ b/src/MatRing.jl @@ -54,11 +54,15 @@ is_unit(a::MatRingElem{T}) where T <: RingElement = is_unit(det(a)) is_unit(a::MatRingElem{T}) where T <: FieldElement = rank(a) == degree(a) +ConformanceTests._implements(::Type{MatRingElem{T}}, ::typeof(is_unit)) where {T <: RingElement} = _implements(T, is_unit) + # proof over a commutative ring: use adj(A)*A = det(A)*I = A*adj(A) is_zero_divisor(a::MatRingElem{T}) where T <: RingElement = is_zero_divisor(det(a)) is_zero_divisor(a::MatRingElem{T}) where T <: FieldElement = rank(a) != degree(a) +ConformanceTests._implements(::Type{MatRingElem{T}}, ::typeof(is_zero_divisor)) where {T <: RingElement} = _implements(T, is_zero_divisor) + function is_zero_divisor_with_annihilator(a::MatRingElem{T}) where T <: RingElement f, b = is_zero_divisor_with_annihilator(det(a)) throw(NotImplementedError(:adj, a)) #return f, b*adj(A) diff --git a/src/NCRings.jl b/src/NCRings.jl index 523f859d93..ec6de92c3e 100644 --- a/src/NCRings.jl +++ b/src/NCRings.jl @@ -175,6 +175,8 @@ function is_nilpotent(a::T) where {T <: NCRingElement} throw(NotImplementedError(:is_nilpotent, a)) end +ConformanceTests._implements(::Type{T}, f::typeof(is_nilpotent)) where {T <: NCRingElement} = is_domain_type(T) || _implements_directly(T, f) + ############################################################################### # diff --git a/src/Poly.jl b/src/Poly.jl index 5bedc34ee4..86d9cbb9f3 100644 --- a/src/Poly.jl +++ b/src/Poly.jl @@ -243,6 +243,8 @@ function is_unit(f::T) where {T <: PolyRingElem} return true end +ConformanceTests._implements(::Type{PolynomialElem{T}}, ::typeof(is_unit)) where T = _implements(T, is_unit) && _implements(T, is_nilpotent) + function is_nilpotent(f::T) where {T <: PolyRingElem} # Makes essential use of the fact that sum of 2 nilpotents is nilpotent. # This is true when the coeffs are commutative. @@ -252,9 +254,12 @@ function is_nilpotent(f::T) where {T <: PolyRingElem} return all(is_nilpotent, coefficients(f)) end +ConformanceTests._implements(::Type{PolynomialElem{T}}, ::typeof(is_nilpotent)) where T = _implements(T, is_nilpotent) is_zero_divisor(a::PolynomialElem) = is_zero_divisor(content(a)) +ConformanceTests._implements(::Type{PolynomialElem{T}}, ::typeof(is_zero_divisor)) where T = _implements(T, is_zero_divisor) + function is_zero_divisor_with_annihilator(a::PolyRingElem{T}) where T <: RingElement f, b = is_zero_divisor_with_annihilator(content(a)) return f, parent(a)(b) diff --git a/src/algorithms/GenericFunctions.jl b/src/algorithms/GenericFunctions.jl index cf3df80d78..b5d8d46f53 100644 --- a/src/algorithms/GenericFunctions.jl +++ b/src/algorithms/GenericFunctions.jl @@ -429,6 +429,8 @@ function is_zero_divisor(a::T) where T <: RingElement return is_zero(a) && !is_trivial(parent(a)) end +ConformanceTests._implements(::Type{T}, f::typeof(is_zero_divisor)) where {T} = is_domain_type(T) || _implements_directly(T, f) + @doc raw""" is_zero_divisor_with_annihilator(a::T) where T <: RingElement @@ -456,6 +458,8 @@ function factor(a) throw(NotImplementedError(:factor, a)) end +ConformanceTests._implements(::Type{T}, f::typeof(factor)) where {T} = _implements_directly(T, f) + @doc raw""" factor_squarefree(a::T) where T <: RingElement -> Fac{T} @@ -466,6 +470,8 @@ function factor_squarefree(a) throw(NotImplementedError(:factor_squarefree, a)) end +ConformanceTests._implements(::Type{T}, f::typeof(factor_squarefree)) where {T} = _implements_directly(T, f) + @doc raw""" is_irreducible(a::RingElement) @@ -479,6 +485,8 @@ function is_irreducible(a) return length(af) == 1 && all(isone, values(af.fac)) end +ConformanceTests._implements(::Type{T}, ::typeof(is_irreducible)) where {T} = _implements(T, is_unit) && _implements(T, factor) + @doc raw""" is_squarefree(a::RingElement) @@ -493,3 +501,4 @@ function is_squarefree(a) return all(isone, values(af.fac)) end +ConformanceTests._implements(::Type{T}, ::typeof(is_squarefree)) where {T} = _implements(T, is_unit) && _implements(T, factor_squarefree) diff --git a/src/algorithms/LaurentPoly.jl b/src/algorithms/LaurentPoly.jl index ada0ca8822..094086de4e 100644 --- a/src/algorithms/LaurentPoly.jl +++ b/src/algorithms/LaurentPoly.jl @@ -159,6 +159,10 @@ function is_nilpotent(f::T) where {T <: LaurentPolyRingElem} return is_nilpotent(f.poly); end +ConformanceTests._implements(::Type{LaurentPolyRingElem{T}}, ::typeof(is_unit)) where T = _implements(T, is_unit) && _implements(T, is_nilpotent) + +ConformanceTests._implements(::Type{LaurentPolyRingElem{T}}, ::typeof(is_nilpotent)) where T = _implements(T, is_nilpotent) + ############################################################################### # diff --git a/src/generic/LaurentMPoly.jl b/src/generic/LaurentMPoly.jl index 5869a5827b..db5450de5e 100644 --- a/src/generic/LaurentMPoly.jl +++ b/src/generic/LaurentMPoly.jl @@ -126,14 +126,18 @@ function is_unit(f::T) where {T <: LaurentMPolyRingElem} return unit_seen end +ConformanceTests._implements(::Type{LaurentMPolyRingElem{T}}, ::typeof(is_unit)) where T = _implements(T, is_unit) && _implements(T, is_nilpotent) function is_nilpotent(f::T) where {T <: LaurentMPolyRingElem} return is_nilpotent(f.mpoly); end +ConformanceTests._implements(::Type{LaurentMPolyRingElem{T}}, ::typeof(is_nilpotent)) where T = _implements(T, is_nilpotent) is_zero_divisor(p::LaurentMPolyWrap) = is_zero_divisor(p.mpoly) +ConformanceTests._implements(::Type{LaurentMPolyRingElem{T}}, ::typeof(is_zero_divisor)) where T = _implements(T, is_zero_divisor) + function is_zero_divisor_with_annihilator(p::LaurentMPolyWrap) f, b = is_zero_divisor_with_annihilator(p.mpoly) return f, LaurentMPolyWrap(parent(p), b)