@code_warntype on keyword arguments

Noticed this when obsessing over type-stability, wonder if what’s below in the code block is considered as an unexpected behavior.

In short, @code_warntype is happy with foo(x::Vector{<:Real}), but mad about bar(; x::Vector{<:Real}).
(It probably has to do with how keyword arguments are ignored on multi-dispatch, and can be “fixed” by defining as bar(; x::Vector{T}) where {T<:Real}....)

julia> function foo(x::Vector{<:Real})
         println(x);
       end
foo (generic function with 1 method)

julia> function bar(;x::Vector{<:Real})
         println(x);
       end
bar (generic function with 1 method)

julia> @code_warntype foo([3.])
MethodInstance for foo(::Vector{Float64})
  from foo(x::Vector{<:Real}) in Main at REPL[1]:1
Arguments
  #self#::Core.Const(foo)
  x::Vector{Float64}
Body::Nothing
1 ─ %1 = Main.println(x)::Core.Const(nothing)
└──      return %1


julia> @code_warntype bar(x=[3.])
MethodInstance for (::var"#bar##kw")(::NamedTuple{(:x,), Tuple{Vector{Float64}}}, ::typeof(bar))
  from (::var"#bar##kw")(::Any, ::typeof(bar)) in Main at REPL[2]:1
Arguments
  _::Core.Const(var"#bar##kw"())
  @_2::NamedTuple{(:x,), Tuple{Vector{Float64}}}
  @_3::Core.Const(bar)
Locals
  @_4::TypeVar
  @_5::Union{}
  x::Vector{Float64}
  @_7::Vector{Float64}
Body::Nothing
1 ─ %1  = Base.haskey(@_2, :x)::Core.Const(true)
│         Core.typeassert(%1, Core.Bool)
│   %3  = Base.getindex(@_2, :x)::Vector{Float64}
│   %4  = Core.TypeVar(Symbol("#s3"), Main.Real)::Core.Compiler.PartialTypeVar(var"#s3"<:Real, true, true)
│         (@_4 = %4)
│   %6  = @_4::Core.Compiler.PartialTypeVar(var"#s3"<:Real, true, true)
│   %7  = Core.apply_type(Main.Vector, @_4::Core.Compiler.PartialTypeVar(var"#s3"<:Real, true, true))::**Type{Array{var"#s3"<:Real, 1}}**
│   %8  = Core.UnionAll(%6, %7)::Type{Vector{<:Real}}
│   %9  = (%3 isa %8)::Core.Const(true)
│         Core.typeassert(%9, Core.Bool)
└──       goto #3
2 ─       Core.Const(:(Core.TypeVar(Symbol("#s4"), Main.Real)))
│         Core.Const(:(@_5 = %12))
│         Core.Const(:(@_5))
│         Core.Const(:(Core.apply_type(Main.Vector, @_5)))
│         Core.Const(:(Core.UnionAll(%14, %15)))
│         Core.Const(:(%new(Core.TypeError, Symbol("keyword argument"), :x, %16, %3)))
└──       Core.Const(:(Core.throw(%17)))
3 ┄       (@_7 = %3)
└──       goto #5
4 ─       Core.Const(:(Core.UndefKeywordError(:x)))
└──       Core.Const(:(@_7 = Core.throw(%21)))
5 ┄ %23 = @_7::Vector{Float64}
│         (x = %23)
│   %25 = (:x,)::Core.Const((:x,))
│   %26 = Core.apply_type(Core.NamedTuple, %25)::Core.Const(NamedTuple{(:x,)})
│   %27 = Base.structdiff(@_2, %26)::Core.Const(NamedTuple())
│   %28 = Base.pairs(%27)::Core.Const(Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}}())
│   %29 = Base.isempty(%28)::Core.Const(true)
│         Core.typeassert(%29, Core.Bool)
└──       goto #7
6 ─       Core.Const(:(Base.kwerr(@_2, @_3)))
7 ┄ %33 = Main.:(var"#bar#1")(x, @_3)::Core.Const(nothing)
└──       return %33

julia>

Thanks for taking a look into this!

Interesting. Native code (via @code_native) looks pretty similar too me:

        .text
        .file   "foo"
        .globl  julia_foo_700                   # -- Begin function julia_foo_700
        .p2align        4, 0x90
        .type   julia_foo_700,@function
julia_foo_700:                          # @julia_foo_700
; ┌ @ 1.jl:1 within `foo`   
        .cfi_startproc
# %bb.0:                                # %top
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset %rbp, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register %rbp
        subq    $32, %rsp
; │ @ 1.jl:2 within `foo`   
        movabsq $j_println_702, %rax
        callq   *%rax
        addq    $32, %rsp
        popq    %rbp
        retq
.Lfunc_end0:
        .size   julia_foo_700, .Lfunc_end0-julia_foo_700
        .cfi_endproc
; └
                                        # -- End function
        .section        ".note.GNU-stack","",@progbits
#= 1.jl:9 =# @code_native(foo([3.0])) = nothing

versus

        .text
        .file   "bar##kw"
        .globl  "julia_bar##kw_735"             # -- Begin function julia_bar##kw_735
        .p2align        4, 0x90
        .type   "julia_bar##kw_735",@function
"julia_bar##kw_735":                    # @"julia_bar##kw_735"
; ┌ @ 1.jl:5 within `bar##kw`
        .cfi_startproc
# %bb.0:                                # %top
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset %rbp, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register %rbp
        subq    $32, %rsp
; │ @ 1.jl:6 within `bar##kw`
        movq    (%rcx), %rcx
; │┌ @ 1.jl:6 within `#bar#5`
        movabsq $j_println_737, %rax
        callq   *%rax
; │└
        addq    $32, %rsp
        popq    %rbp
        retq
.Lfunc_end0:
        .size   "julia_bar##kw_735", .Lfunc_end0-"julia_bar##kw_735"
        .cfi_endproc
; └
                                        # -- End function
        .section        ".note.GNU-stack","",@progbits
#= 1.jl:10 =# @code_native(bar(; x = [3.0])) = nothing

This is on Julia 1.9.0.

I was using v1.7.
Does @code_warntype also complain on v1.9?

Complain in what sense (or about what)?. The intermediate code is considerably longer for bar than for foo but it seems it is compiled down to the same native code.

Sorry about the confusion.
On, 1.7.0, @code_warntype complains by marking the type Type{Array{var"#s3"<:Real, 1}} as red in the code block of the original post.
(I marked that part with * as **Type{Array{var"#s3"<:Real, 1}}**, since the render here doesn't show color from @code_warntype`.)

1 Like

Thanks for the clarification. I get

MethodInstance for (::var"#bar##kw")(::NamedTuple{(:x,), Tuple{Vector{Float64}}}, ::typeof(bar))
  from (::var"#bar##kw")(::Any, ::typeof(bar)) in Main at C:\Users\Win10\source\repos\julia\discourse\problem189.jl:5
Arguments
  _::Core.Const(var"#bar##kw"())
  @_2::NamedTuple{(:x,), Tuple{Vector{Float64}}}
  @_3::Core.Const(bar)
Locals
  @_4::Vector{Float64}
  @_5::TypeVar
  @_6::Union{}
  x::Vector{Float64}
Body::Nothing
1 ─       Core.NewvarNode(:(@_4))
│   %2  = Core.isdefined(@_2, :x)::Core.Const(true)
│         Core.typeassert(%2, Core.Bool)
│   %4  = Core.getfield(@_2, :x)::Vector{Float64}  
│   %5  = Core.TypeVar(Symbol("#s4"), Main.Real)::Core.Compiler.PartialTypeVar(var"#s4"<:Real, true, true)
│         (@_5 = %5)
│   %7  = @_5::Core.Compiler.PartialTypeVar(var"#s4"<:Real, true, true)
│   %8  = Core.apply_type(Main.Vector, @_5::Core.Compiler.PartialTypeVar(var"#s4"<:Real, true, true))::Type{Array{var"#s4"<:Real, 1}}
│   %9  = Core.UnionAll(%7, %8)::Type{Vector{<:Real}}
│   %10 = (%4 isa %9)::Core.Const(true)
│         Core.typeassert(%10, Core.Bool)
└──       goto #3
2 ─       Core.Const(:(Core.TypeVar(Symbol("#s5"), Main.Real)))
│         Core.Const(:(@_6 = %13))
│         Core.Const(:(@_6))
│         Core.Const(:(Core.apply_type(Main.Vector, @_6)))
│         Core.Const(:(Core.UnionAll(%15, %16)))
│         Core.Const(:(%new(Core.TypeError, Symbol("keyword argument"), :x, %17, %4)))
└──       Core.Const(:(Core.throw(%18)))
3 ┄       (@_4 = %4)
└──       goto #5
4 ─       Core.Const(:(Core.UndefKeywordError(:x)))
└──       Core.Const(:(@_4 = Core.throw(%22)))
5 ┄ %24 = @_4::Vector{Float64}
│         (x = %24)
│   %26 = (:x,)::Core.Const((:x,))
│   %27 = Core.apply_type(Core.NamedTuple, %26)::Core.Const(NamedTuple{(:x,)})
│   %28 = Base.structdiff(@_2, %27)::Core.Const(NamedTuple())
│   %29 = Base.pairs(%28)::Core.Const(Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}}())
│   %30 = Base.isempty(%29)::Core.Const(true)
│         Core.typeassert(%30, Core.Bool)
└──       goto #7
6 ─       Core.Const(:(Base.kwerr(@_2, @_3)))
7 ┄ %34 = Main.:(var"#bar#5")(x, @_3)::Core.Const(nothing)
└──       return %34

Edit: indication of @code_warntype looks similar, but is not significant?

Thanks for posting!