Retrieve kwarg declaration type

If I define a method:

f(x; y::Int) = x * y

How can I retrieve the ::Int annotation on y, via reflection? I’ve poked around some of the method table properties, but can’t see it anywhere. It’s definitely stored somewhere though, since calling:

f(3, y=3.0)

returns
ERROR: TypeError: in keyword argument y, expected **Int64**, got a value of type Float64

This?

julia>  f(x :: Int) = x

julia> methods(f)
# 1 method for generic function "f":
[1] f(x::Int64) in Main at REPL[1]:1


That does not address the OP’s question.

Ah, you mean because

julia> f(;x::Int) = x
f (generic function with 1 method)

julia> methods(f)
# 1 method for generic function "f":
[1] f(; x) in Main at REPL[1]:1

I didn’t realize that (that the type is not shown there for kwargs).

Sorry.

1 Like

I don’t have a satisfactory answer for you @Benjia, though maybe someone else does.

Poking around, it seems that the method by which julia does type assertion on keyword arguments is very different from those in positional arguments and it’s not exactly easy to find by standard reflection tools. Here’s how you could conceivably do it though:

First, let’s look at what actually happens when you call a function with a keyword argument (it’s more subtle than you might guess!)

julia> f(x; y::Int) = x * y
f (generic function with 1 method)

julia> g() = f(1; y=3.0)
g (generic function with 1 method)

julia> @code_lowered g()
CodeInfo(
1 ─ %1 = (:y,)
│   %2 = Core.apply_type(Core.NamedTuple, %1)
│   %3 = Core.tuple(3.0)
│   %4 = (%2)(%3)
│   %5 = Core.kwfunc(Main.f)
│   %6 = (%5)(%4, Main.f, 1)
└──      return %6
)

Okay, so what this says is that f(1; y=3.0) is actually doing

Core.kwfunc(f)(NamedTuple{(:y,)}((3,)), f, 1)

Okay, so let’s take a peek inside this call and see what it does:

julia> @code_lowered Core.kwfunc(f)(NamedTuple{(:y,)}((3,)), f, 1)
CodeInfo(
1 ── %1  = Base.haskey(@_2, :y)
└───       goto #6 if not %1
2 ── %3  = Base.getindex(@_2, :y)
│    %4  = %3 isa Main.Int
└───       goto #4 if not %4
3 ──       goto #5
4 ── %7  = %new(Core.TypeError, Symbol("keyword argument"), :y, Main.Int, %3)
└───       Core.throw(%7)
5 ┄─       @_6 = %3
└───       goto #7
6 ── %11 = Core.UndefKeywordError(:y)
└───       @_6 = Core.throw(%11)
7 ┄─       y = @_6
│    %14 = (:y,)
│    %15 = Core.apply_type(Core.NamedTuple, %14)
│    %16 = Base.structdiff(@_2, %15)
│    %17 = Base.pairs(%16)
│    %18 = Base.isempty(%17)
└───       goto #9 if not %18
8 ──       goto #10
9 ──       Base.kwerr(@_2, @_3, x)
10 ┄ %22 = Main.:(var"#f#69")(y, @_3, x)
└───       return %22
)

Yikes. If you spend enough time staring at this, you will see that what this says is that it checks in y isa Int:

2 ── %3  = Base.getindex(@_2, :y)
│    %4  = %3 isa Main.Int
└───       goto #4 if not %4
3 ──       goto #5

and if it’s not an int, then it goes to

4 ── %7  = %new(Core.TypeError, Symbol("keyword argument"), :y, Main.Int, %3)
└───       Core.throw(%7)

which is saying ‘throw an error’. I’m not sure if there’s an automated way to discover this.

4 Likes

Interesting, thanks for going into the detail. It would be quite annoying if this is only stored in the generated function’s code. I could probably parse the CodeInfo, but that seems a bit horrible.

1 Like