Why is the type of factors not being inferred while multiplying an AbstractQ with a vector?

julia> using LinearAlgebra

julia> Q = qr(rand(30,30));

julia> Q2 = Q.Q;

julia> @code_warntype Q2 * rand(size(Q2,2))
MethodInstance for *(::LinearAlgebra.QRCompactWYQ{Float64, Matrix{Float64}}, ::Vector{Float64})
  from *(A::LinearAlgebra.AbstractQ, b::StridedVector{T} where T) in LinearAlgebra at /home/jb6888/julia/julia-c8b5904991/share/julia/stdlib/v1.8/LinearAlgebra/src/qr.jl:635
Arguments
  #self#::Core.Const(*)
  A::LinearAlgebra.QRCompactWYQ{Float64, Matrix{Float64}}
  b::Vector{Float64}
Locals
  bnew::Vector{Float64}
  Anew::LinearAlgebra.QRCompactWYQ{Float64, Matrix{Float64}}
  TAb::Type{Float64}
Body::Vector{Float64}
1 ─       Core.NewvarNode(:(bnew))
│   %2  = LinearAlgebra.eltype(A)::Core.Const(Float64)
│   %3  = LinearAlgebra.eltype(b)::Core.Const(Float64)
│         (TAb = LinearAlgebra.promote_type(%2, %3))
│   %5  = Core.apply_type(LinearAlgebra.AbstractMatrix, TAb::Core.Const(Float64))::Core.Const(AbstractMatrix{Float64})
│         (Anew = LinearAlgebra.convert(%5, A))
│   %7  = Base.getproperty(A, :factors)::Matrix{Float64}
│   %8  = LinearAlgebra.size(%7, 1)::Int64
│   %9  = LinearAlgebra.length(b)::Int64
│   %10 = (%8 == %9)::Bool
└──       goto #3 if not %10
2 ─       (bnew = LinearAlgebra.copy_oftype(b, TAb::Core.Const(Float64)))
└──       goto #6
3 ─ %14 = Base.getproperty(A, :factors)::Matrix{Float64}
│   %15 = LinearAlgebra.size(%14, 2)::Int64
│   %16 = LinearAlgebra.length(b)::Int64
│   %17 = (%15 == %16)::Bool
└──       goto #5 if not %17
4 ─ %19 = TAb::Core.Const(Float64)
│   %20 = Base.getproperty(A, :factors)::Matrix{Float64}
│   %21 = LinearAlgebra.size(%20, 1)::Int64
│   %22 = LinearAlgebra.length(b)::Int64
│   %23 = (%21 - %22)::Int64
│   %24 = LinearAlgebra.zeros(%19, %23)::Vector{Float64}
│         (bnew = Base.vcat(b, %24))
└──       goto #6
5 ─ %27 = Base.getproperty(A, :factors)::Any
│   %28 = LinearAlgebra.size(%27, 1)::Any
│   %29 = Base.getproperty(A, :factors)::Any
│   %30 = LinearAlgebra.size(%29, 2)::Any
│   %31 = Base.string("vector must have length either ", %28, " or ", %30)::Any
│   %32 = LinearAlgebra.DimensionMismatch(%31)::Any
└──       LinearAlgebra.throw(%32)
6 ┄ %34 = LinearAlgebra.lmul!(Anew, bnew)::Vector{Float64}
└──       return %34

while the overall operation is type-stable, there are several Any around %27 where Q2.factors is used. For some reason the type of Q2.factors is not being inferred, which is strange since

julia> typeof(Q2)
LinearAlgebra.QRCompactWYQ{Float64, Matrix{Float64}}

and the struct is defined as

struct QRCompactWYQ{S, M<:AbstractMatrix{S}} <: AbstractQ{S}
    factors::M
    T::Matrix{S}

    function QRCompactWYQ{S,M}(factors, T) where {S,M<:AbstractMatrix{S}}
        require_one_based_indexing(factors)
        new{S,M}(factors, T)
    end
end

and the type of factors should be inferred from the type of Q2. I can see this when I check

julia> @code_warntype Q2.factors
MethodInstance for getproperty(::LinearAlgebra.QRCompactWYQ{Float64, Matrix{Float64}}, ::Symbol)
  from getproperty(x, f::Symbol) in Base at Base.jl:38
Arguments
  #self#::Core.Const(getproperty)
  x::LinearAlgebra.QRCompactWYQ{Float64, Matrix{Float64}}
  f::Symbol
Body::Matrix{Float64}
1 ─      nothing
│   %2 = Base.getfield(x, f)::Matrix{Float64}
└──      return %2

but I’m not sure why it’s not inferred in the multiplication.

I imagine this is from running in global scope with global variables. If you put it inside a function it seems to infer it all fine.

f(q,a) = q.Q * a
@code_warntype f(Q,rand(size(Q.Q,2))
       )
MethodInstance for f(::LinearAlgebra.QRCompactWY{Float64, Matrix{Float64}}, ::Vector{Float64})
  from f(q, a) in Main at REPL[4]:1
Arguments
  #self#::Core.Const(f)
  q::LinearAlgebra.QRCompactWY{Float64, Matrix{Float64}}
  a::Vector{Float64}
Body::Vector{Float64}
1 ─ %1 = Base.getproperty(q, :Q)::LinearAlgebra.QRCompactWYQ{Float64, Matrix{Float64}}
│   %2 = (%1 * a)::Vector{Float64}
└──      return %2

I’m not sure if this is doing anything different, as the result of the multiplication is inferred anyway and the intermediate steps are hidden here.

Sorry, it’s a bit early, I checked with Cthulu and it’s the same for my f but then I looked correctly, the place where it isn’t inferring is the error handling. If you check the lines with ::Any they are a part of 5 block. and on the block 3 there is a goto #5 if there is a dimension mismatch, so it shouldn’t matter.