Using parametric type inside outer parametric constructor

Uhm… Well, this is even minimal:

julia> struct Foo{T,S<:T} end

julia> Foo{T}(x) where T = Foo{T,T}()

julia> Foo{Float64}(1)
ERROR: UndefVarError: T not defined

Not here:

               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.5.3 (2020-11-09)
 _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
|__/                   |

julia> struct Foo{T<:Real, S<:AbstractMatrix{<:T}} 
               data::S
               n::Int

               function Foo{T, S}(data, n) where {T<:Real, S<:AbstractMatrix{<:T}}
                       Base.require_one_based_indexing(data)
                       new{T, S}(data, n)
               end
       end

julia> function Foo(A::AbstractMatrix{T}, n::Int) where {T<:Real}
               return Foo{T, typeof(A)}(A, n)
       end
Foo

julia> function Foo{T}(::UndefInitializer, n::Int) where {T<:Real}
               d = fld(n * (n + 1), 2)
               A = Matrix{T}(undef, d, d)
               return Foo(A, n)
       end

julia> Foo{Float64}(undef, 2)
ERROR: UndefVarError: T not defined
Stacktrace:
 [1] Foo{Float64,S} where S<:(AbstractArray{var"#s1",2} where var"#s1"<:Float64)(::UndefInitializer, ::Int64) at ./REPL[3]:3
 [2] top-level scope at REPL[4]:1

julia>

Yeah, I also ran the original code on 1.4.0, but with the AbstractArray inheritance removed, and I still got the UndefVarError.

Wow, nice example! That really boils it down.

I have no idea what’s going on anymore. :joy:

Slightly more minimal:

julia> struct Foo{T, S <: T} end

julia> Foo{T}() where {T} = T

julia> Foo{Integer}()
ERROR: UndefVarError: T not defined
Stacktrace:
 [1] Foo{Integer,S} where S<:Integer() at ./REPL[2]:1
 [2] top-level scope at REPL[3]:1

You see, the function gets specialized to Foo{Integer,S}, but then T in the function is not defined. The problem is the T inside, which looses its meaning when the method is created:

julia> struct Foo{T,S<:T} end

julia> Foo{T}() where T = 1

julia> Foo{Integer}()
1

julia> Foo{T}() where T = x

julia> Foo{Integer}()
ERROR: UndefVarError: x not defined


I’m inclined to think this is a bug. I don’t understand why this works:

julia> struct Bar{T, S} end

julia> Bar{T}() where {T} = T

julia> Bar{Integer}()
Integer

but this doesn’t:

julia> struct Foo{T, S <: T} end

julia> Foo{T}() where {T} = T

julia> Foo{Integer}()
ERROR: UndefVarError: T not defined
Stacktrace:
 [1] Foo{Integer,S} where S<:Integer() at ./REPL[2]:1
 [2] top-level scope at REPL[3]:1

I think @Henrique_Becker gave a nice explanation: in your second example, Foo{Integer, Int} would be allowed, but then there’s no way to know what both T and S means within Foo{T}() where {T} = .... In order words, T and S are “connected” in your first example, but “disconnected” in the second. Or am I missing something?

I think it is a type-inference problem:

julia> @code_typed Foo{Int}()
CodeInfo(
1 ─     return $(Expr(:static_parameter, 1))
) => Type{T} where T<:(Union{Int64, S} where S<:Int64)


Of course T is Int64 if T<:Union{Int64,S} and S<:Int64. (I don’t quite get why there is the T<:Union{Int64,S}, really… is that right?).

My mistake, I copied the code and forgot to change the commented lines.

It seems clear to me that what I pointed in your original MWE cannot be exactly same that affected OP’s MWE. In OP’s MWE the problem happens because somehow in the first function called T is not recognized within the body of the first function called.

@schneiderfelipe Than for your kind words, but while I think I got it right for @lmiq MWE, I do not think I solved your original problem.

Now… this is funny:

julia> struct Foo{T<:Real, S<:AbstractMatrix{<:T}}
               data::S
               n::Int
       
               function Foo{T, S}(data, n) where {T<:Real, S<:AbstractMatrix{<:T}}
                   println("Foo{T, S}(data, n)")
                   Base.require_one_based_indexing(data)
                   new{T, S}(data, n)
               end
       end
       
       function Foo(A::AbstractMatrix{T}, n::Int) where {T<:Real}
           println("Foo(A, n)")
           return Foo{T, typeof(A)}(A, n)
       end
       
       # Changed the signature below to take both T and S even if S is not used
       function Foo{T, S}(::UndefInitializer, n::Int) where {T<:Real, S <: AbstractMatrix{<:T}}
           println("Foo{T, S}(undef, n)")
           d = fld(n * (n + 1), 2)
       
           # The next two lines should substitute each other,
           # but the commented one throws an error.
       
           A = Matrix{T}(undef, d, d)  # UndefVarError: T not defined
           # A = zeros(d, d)
       
           return Foo(A, n)
       end

julia> Foo{Float64, Matrix{Float64}}(undef, 2)
Foo{T, S}(undef, n)
Foo(A, n)
Foo{T, S}(data, n)
Foo{Float64,Array{Float64,2}}([0.0 0.0 0.0; 0.0 0.0 0.0; 0.0 0.0 0.0], 2)

Why? Why the hell?

My guess now is that, maybe, just maybe, we are not allowed to define an outer constructor taking less type parameters than the struct itself?

It seems to me that the behavior of the MWE is determined by some code lowering and/or other implementation details that are not specified in the manual. I still feel that the current behavior of the MWE is incorrect, but of course I could be wrong.

We might have to file a Github issue and have the experts weigh in.

or just mark one of them here?

I’m not sure I want to tag a bunch of people here… :joy:

https://github.com/JuliaLang/julia/issues/39280

This feels like a plausible explanation. I vaguely recall that static parameters are only ever assigned by the compiler if they are uniquely identified. The compiler needs to find, if possible, a unique T in order for T to be available in the method body. Somehow the constraint on S introduces a weird edge case and the compiler just gives up. Ironically, it is smart enough to know that

julia> (Union{Int64,S} where {S<:Int64}) == Int64
true

but not smart enough to conclude the static type must be Int64. I think Jeff mentioned this issue in What’s bad about Julia? where types can have extents that just drive the compiler crazy.

Thanks for opening the issue @CameronBieganek!