`x !== nothing` incorrectly reported by `@code_lowered` and Cthulhu.jl?

Not sure if this is a bug, or I’m missing something about understanding the output of @code_lowered and Cthulhu’s @descend. The following REPL snippet shows a very simple function, which if called with argument nothing does nothing. @code_llvm shows that the compiler can infer this as I would expect (having a function body that is just ret void), but @code_lowered marks lines as ‘dynamic’ and Cthulhu marks the line as ‘runtime’ (which gets red-highlighted in the REPL) - both of these look like worrying warnings that I would think I should fix to get good performance, but it seems to me like these are false positives. Am I missing something?

               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.12.1 (2025-10-17)
 _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org release
|__/                   |

julia> using Cthulhu

julia> function foo(x)
       if x !== nothing
       println("hello")
       end
       end
foo (generic function with 1 method)

julia> @code_llvm foo(nothing)
; Function Signature: foo(Nothing)
;  @ REPL[2]:1 within `foo`
define void @julia_foo_3953() #0 {
top:
;  @ REPL[2] within `foo`
  ret void
}

julia> @code_lowered foo(nothing)
CodeInfo(
1 ─ %1 = Main.:!==
│   %2 = Main.nothing
│   %3 =   dynamic (%1)(x, %2)
└──      goto #3 if not %3
2 ─ %5 = Main.println
│   %6 =   dynamic (%5)("hello")
└──      return %6
3 ─      return nothing
)

julia> @descend foo(nothing)
foo(x) @ Main REPL[2]:1
1 function foo(x::Core.Const(nothing))::Core.Const(nothing)
2 if (x::Core.Const(nothing) !== nothing::Core.Const(nothing))::Core.Const(false)
3 println("hello")
4 end
5 end
Select a call to descend into or ↩ to ascend. [q]uit. [b]ookmark.
Toggles: [w]arn, [h]ide type-stable statements, [t]ype annotations, [s]yntax highlight for Source/LLVM/Native, [j]ump to source always.
Show: [S]ource code, [A]ST, [T]yped code, [L]LVM IR, [N]ative code
Actions: [E]dit source code, [R]evise and redisplay
 • runtime  x::Core.Const(nothing) !== nothing::Core.Const(nothing)
   ↩
1 Like

Only just saw the line near the end of Cthulhu’s README suggesting to use ‘Typed’ mode - that show what I’d have expected, just a return nothing

julia> @descend foo1(nothing)
foo1(bar) @ Main REPL[1]:1
1 function foo1(bar::Core.Const(nothing))::Core.Const(nothing)
2 if (bar::Core.Const(nothing) !== nothing::Core.Const(nothing))::Core.Const(false)
3 println("hello")
4 end
5 end
Select a call to descend into or ↩ to ascend. [q]uit. [b]ookmark.
Toggles: [w]arn, [h]ide type-stable statements, [t]ype annotations, [s]yntax highlight for Source/LLVM/Native, [j]ump to source always.
Show: [S]ource code, [A]ST, [T]yped code, [L]LVM IR, [N]ative code
Actions: [E]dit source code, [R]evise and redisplay
 • runtime  bar::Core.Const(nothing) !== nothing::Core.Const(nothing)
   ↩

foo1(bar) @ Main REPL[1]:1
Body::Core.Const(nothing)
 1 ─     return nothing                                                     │
Select a call to descend into or ↩ to ascend. [q]uit. [b]ookmark.
Toggles: [w]arn, [h]ide type-stable statements, [t]ype annotations, [s]yntax highlight for Source/LLVM/Native, [j]ump to source always, [o]ptimize, [d]ebuginfo, [r]emarks, [e]ffects, e[x]ception types, [i]nlining costs.
Show: [S]ource code, [A]ST, [T]yped code, [L]LVM IR, [N]ative code
Actions: [E]dit source code, [R]evise and redisplay
Advanced: dump [P]arams cache.
 • ↩

You likely wanted @code_typed, which will default to showing you the lowered IR after type inference and optimization.

Note that if I’d used === instead of !==, I would get the results I expect with Cthulhu (no ‘runtime’ annotations), which I why I think this is a bug related to !==. @code_lowered still says ‘dynamic’ on a couple of lines though

julia> using Cthulhu

julia> function bar(x)
       if x === nothing
       println("hello")
       end
       end
bar (generic function with 1 method)

julia> @code_lowered bar(1)
CodeInfo(
1 ─ %1 = Main.:(===)
│   %2 = Main.nothing
│   %3 =   dynamic (%1)(x, %2)
└──      goto #3 if not %3
2 ─ %5 = Main.println
│   %6 =   dynamic (%5)("hello")
└──      return %6
3 ─      return nothing
)

julia> @code_lowered bar(nothing)
CodeInfo(
1 ─ %1 = Main.:(===)
│   %2 = Main.nothing
│   %3 =   dynamic (%1)(x, %2)
└──      goto #3 if not %3
2 ─ %5 = Main.println
│   %6 =   dynamic (%5)("hello")
└──      return %6
3 ─      return nothing
)

julia> @descend bar(nothing)
bar(x) @ Main REPL[2]:1
1 function bar(x::Core.Const(nothing))::Core.Const(nothing)
2 if (x::Core.Const(nothing) === nothing::Core.Const(nothing))::Core.Const(true)
3 println("hello")
4 end
5 end
Select a call to descend into or ↩ to ascend. [q]uit. [b]ookmark.
Toggles: [w]arn, [h]ide type-stable statements, [t]ype annotations, [s]yntax highlight for Source/LLVM/Native, [j]ump to source always.
Show: [S]ource code, [A]ST, [T]yped code, [L]LVM IR, [N]ative code
Actions: [E]dit source code, [R]evise and redisplay
 • %6 = println(::String)::Core.Const(nothing)
   ↩

@xal it’s true that @code_typed shows the results I’d expect. My main issue is that Cthulhu.@descend gives misleading (?) information - in a more complicated code, @descend is much easier to use than @code_typed!

I haven’t switched to 1.12 in practice yet, but it appears at first glance that @code_lowered reports dynamic for calls in general:

julia> @code_lowered 1+1
CodeInfo(
1 ─ %1 = Base.add_int
│   %2 =   dynamic (%1)(x, y)
└──      return %2
)

julia> @code_lowered sinpi(3)
CodeInfo(
1 ─ %1  = Base.Math.:>=
│   %2  =   dynamic (%1)(x, 0)
└──       goto #3 if not %2
2 ─ %4  = Base.Math.zero
│   %5  = Base.Math.float
│   %6  =   dynamic (%5)(x)
│   %7  =   dynamic (%4)(%6)
└──       return %7
3 ─ %9  = Base.Math.:-
│   %10 = Base.Math.zero
│   %11 = Base.Math.float
│   %12 =   dynamic (%11)(x)
│   %13 =   dynamic (%10)(%12)
│   %14 =   dynamic (%9)(%13)
└──       return %14
)

Don’t really see the point of that tag so far.

1 Like

Thanks @Benny - I can ignore the ‘dynamic’ then. So the only question left is why Cthulhu is inconsistent between !== and ===, with the !== output looking wrong.