Compilation-order-dependent type inference

I have a fairly complex package where I’m observing some strange type inference behaviour. I expect that reducing my real problem to an MWE would be tricky, so instead I’m taking the liberty of presenting the problem in the context of my package. Complete source code is available on GitHub - subnero1/MapMaths.jl at f9965d75d44b1e1276c40cfc1058c1427db3022f · GitHub (the linked commit, not the version currently registered in the General registry).

Ok, so here’s the problem: @code_warntype tells me that my method involves non-concrete types.

julia> @code_warntype cmap(CartesianX(), Coordinate(GeocentricLonLatAlt(), 0, 1, 0), Wgs84());
# ...
Body::Any
# ...
5 ┄ %29 = MapMaths.bump_coord::Core.Const(MapMaths.bump_coord)
│         (##279 = (%29)(system, coord, datum))
# ...
│   %33 = ##279::Any
# ...

Specifically, it looks like Julia failed to infer the return type of bump_coord(). However, if I run @code_warntype on bump_coord(), I’m getting a fully inferred result.

julia> @code_warntype MapMaths.bump_coord(CartesianX(), Coordinate(GeocentricLonLatAlt(), 0, 1, 0), Wgs84());
# ...
Body::Coordinate{CartesianX, Tuple{Float64}}
# ...

Also, if I restart my Julia session and then run @code_warntype MapMaths.bump_coord() before @code_warntype cmap(), then even the latter is fully inferred.

julia> @code_warntype MapMaths.bump_coord(CartesianX(), Coordinate(GeocentricLonLatAlt(), 0, 1, 0), Wgs84());
# ...
Body::Coordinate{CartesianX, Tuple{Float64}}
# ...

julia> @code_warntype cmap(CartesianX(), Coordinate(GeocentricLonLatAlt(), 0, 1, 0), Wgs84());
# ...
Body::Coordinate{CartesianX, Tuple{Float64}}
# ... 

So it looks like what’s happening here is that under some circumstances, Julia decides that type inference of some methods may not be worthwhile. Is there a way I can force Julia to always do full inference for certain methods?

At first glance, cmap and bump_coord are mutually recursive. If the concrete input types change throughout unbounded recursive calls, type inference gives up at some point to avoid running forever; JET.@report_opt or Cthulhu.@descend can report this. If that’s the root cause, it’s plausible that bump_coord call happens to fall within the type inference heuristics but the outer cmap call does not and relies on the former call’s inference to be cached first. It’s also worth noting that reflection methods recompute type inference, and the stateful results may not match what the (also statefully) compiled code implies by its performance.

I don’t know of a way to tweak type inference heuristics, and as far as I know, there’s no good way to solve the general problem on the language level. There are decidable type inference systems, but languages seem to trend toward Turing-complete compile-time computation for more expressive power. At that point, you either restrict type inference e.g. mandate explicit return types, let the compiler hang, error at some heuristic limit, or fall back to wider types and more runtime work. On the level of your package, it’s hypothetically possible for you to move information from the type system to runtime, but that obviously costs you compile-time optimization elsewhere and may not be worth it overall.