Zero-cost way to check if `NamedTuple` has a field?

Is there a way to check if a NamedTuple has a field :a that is elided by the compiler? For example, let t=(a=1, b=2). Then the fact that t has fields :a,:b is known at compile time (because this info is in the type of t):

julia> typeof(t)
NamedTuple{(:a, :b),Tuple{Int64,Int64}}

However both haskey(t, :a) and :a in propertynames(t) don’t seem to be elided by the compiler (I checked by looking at the @code_native of these calls).

1 Like
julia> t = (a = 3, b= 2)
(a = 3, b = 2)

julia> typeof(t)
NamedTuple{(:a, :b),Tuple{Int64,Int64}}

julia> f(t) = haskey(t, :a)
f (generic function with 1 method)

julia> @code_llvm f(t)

;  @ REPL[30]:1 within `f`
define i8 @julia_f_12916({ i64, i64 } addrspace(11)* nocapture nonnull readonly dereferenceable(16)) {
top:
  ret i8 1
}

julia> g(t) = haskey(t, :q)
g (generic function with 1 method)

julia> @code_llvm g(t)

;  @ REPL[34]:1 within `g`
define i8 @julia_g_12931({ i64, i64 } addrspace(11)* nocapture nonnull readonly dereferenceable(16)) {
top:
  ret i8 0
}
2 Likes

Maybe related: Is the first key of a NamedTuple special?

Not really relevant here since:

julia> b(t) = haskey(t, :b)
b (generic function with 1 method)

julia> @code_llvm b(t)

;  @ REPL[42]:1 within `b'
define i8 @julia_b_12946({ i64, i64 } addrspace(11)* nocapture nonnull readonly dereferenceable(16)) {
top:
  ret i8 1
}

I thought it might still be relevant as the OP also mentions in instead of haskey. And there we have

julia> @code_llvm :b in propertynames((a=2, b=3))

;  @ operators.jl:948 within `in'
; Function Attrs: uwtable
define i8 @julia_in_10617(%jl_value_t addrspace(10)* nonnull, %jl_value_t addrspace(10)* nonnull align 8 dereferenceable(16)) #0 {
top:
;  @ operators.jl:949 within `in'
; ┌ @ tuple.jl:43 within `iterate' @ tuple.jl:43
; │┌ @ tuple.jl:24 within `getindex'
    %2 = addrspacecast %jl_value_t addrspace(10)* %1 to %jl_value_t addrspace(11)*
    %3 = bitcast %jl_value_t addrspace(11)* %2 to %jl_value_t addrspace(10)* addrspace(11)*
    %value_phi7 = load %jl_value_t addrspace(10)*, %jl_value_t addrspace(10)* addrspace(11)* %3, align 8
; └└
;  @ operators.jl:950 within `in'
; ┌ @ operators.jl:83 within `=='
   %4 = addrspacecast %jl_value_t addrspace(10)* %value_phi7 to %jl_value_t addrspace(11)*
   %5 = addrspacecast %jl_value_t addrspace(10)* %0 to %jl_value_t addrspace(11)*
   %6 = icmp eq %jl_value_t addrspace(11)* %4, %5
; └
;  @ operators.jl:953 within `in'
  br i1 %6, label %L7, label %L8.lr.ph

L8.lr.ph:                                         ; preds = %top
  %7 = bitcast %jl_value_t addrspace(10)* %1 to %jl_value_t addrspace(10)* addrspace(10)*
  %8 = addrspacecast %jl_value_t addrspace(10)* addrspace(10)* %7 to %jl_value_t addrspace(10)* addrspace(11)*
  br label %L8

L7:                                               ; preds = %L24, %L8, %top
  %merge = phi i8 [ 1, %top ], [ 0, %L8 ], [ 1, %L24 ]
;  @ operators.jl:954 within `in'
  ret i8 %merge

L8:                                               ; preds = %L8.lr.ph, %L24
  %value_phi18 = phi i64 [ 2, %L8.lr.ph ], [ %11, %L24 ]
; ┌ @ tuple.jl:43 within `iterate'
   %value_phi1.off = add nsw i64 %value_phi18, -1
   %9 = icmp ugt i64 %value_phi1.off, 1
   br i1 %9, label %L7, label %L24

L24:                                              ; preds = %L8
; │┌ @ tuple.jl:24 within `getindex'
    %10 = getelementptr inbounds %jl_value_t addrspace(10)*, %jl_value_t addrspace(10)* addrspace(11)* %8, i64 %value_phi1.off
; │└
; │┌ @ int.jl:53 within `+'
    %11 = add nuw nsw i64 %value_phi18, 1
; └└
;  @ operators.jl:949 within `in'
; ┌ @ tuple.jl:43 within `iterate' @ tuple.jl:43
; │┌ @ tuple.jl:24 within `getindex'
    %value_phi = load %jl_value_t addrspace(10)*, %jl_value_t addrspace(10)* addrspace(11)* %10, align 8
; └└
;  @ operators.jl:950 within `in'
; ┌ @ operators.jl:83 within `=='
   %12 = addrspacecast %jl_value_t addrspace(10)* %value_phi to %jl_value_t addrspace(11)*
   %13 = icmp eq %jl_value_t addrspace(11)* %12, %5
; └
;  @ operators.jl:953 within `in'
  br i1 %13, label %L7, label %L8
}
2 Likes

Okay. So for now I’ll use haskey instead of in.
This seems like a compiler bug? Should someone raise an issue?

It is not a bug. But there is an issue open about it Constant propagation for tuple in operator only works for first element · Issue #28844 · JuliaLang/julia · GitHub.

Also, the problem is not only with in but with propertynames.

2 Likes