Check type stability of a struct

We usually speak of type stability for functions and we also have the tools to determine that (like @code_warntype).
But say I would like to check whether a struct is type-stable, in the sense that it is well defined and no field is of an abstract type.
For the small structs it is trivial, but for long composite nested structs it is quite hard to inspect it manually.
Is there a tool to handle this automatically ?

example:

struct Alpha{T,R}
  x::T
  y::R
end

a1 = Alpha{Real, Int}(1,2)
a2 = Alpha{Int,Int}(1,2)

# desired hypothetical function
istypestable(a1) #returns false
istypestable(a2) #returns true
1 Like

Not sure what exactly mean with fields being “well defined” (they always are), but this should do what you’re looking for:

julia> allconcrete(x::T) where T = all(isconcretetype, fieldtypes(T))
allconcrete (generic function with 1 method)

julia> allconcrete(a1)
false

julia> allconcrete(a2)
true

Note that this works through reflection via fieldtypes.

“type stable” as a term only makes sense in the context of a function using an object, since type stability only refers to some function whose return type depends on the value of its arguments instead of its types. It’s true that abstractly typed fields can lead to type instabilities in functions using that type, but I’m not sure we should refer to structs with non-concretely typed fields as “type unstable”. There are valid reasons for having a field typed as e.g. Any - for example when the field can take on a diverse number of types and specializing on it would not help (or even hinder) performance.

4 Likes

It better be recursive, like allconcrete(::Type{T}) where T = isconcretetype(T) && all(allconcrete, fieldtypes(T)).

yes, there are cases like

cc = Alpha{Int, Alpha{Int, Real}}(1, Alpha{Int,Real}(2,3))

where only the recursive function will give the correct result.

However both fail to detect

julia> abv = Alpha{Vector{Real}, Int}(Vector{Real}([1,2,3]), 3)
Alpha{Vector{Real}, Int64}(Real[1, 2, 3], 3

because Vector{Real} |> fieldtypes is empty.

You’re not going to get around specializing the code in general, since type parameters are not necessarily linked to the field types.

I understand that there can be the case that a parameter type might not be linked to a specific field, and thus there will be no negative impact in performance for abstract types.
e.g.:

struct Beta{T} end;
b = Beta{Number}()

#desired behavior
istypestable(b) # returns true

In these cases, the parameter is mostly used for specialization.

Maybe it’s useful to translate the problem in the function domain.
E.g. I could write something like this, in order to recursively evaluate all fields from the struct:

@inline function teststab(a::R) where R
    if isprimitivetype(R)
        return a
    else
        for x in getfield.([a], fieldnames(R))
            testalpha(x)
        end
    end
end

upon which I can call code_warntype

Unfortunately this version doesn’t really work.
E.g. the following should be red-free since it’s type stable

The problem is that the compiler inside the for loop doesn’t specialize on the type of each field x although it should be statically retrievable.
Any ideas how to proceed ?