# Partial world-splitting

**URL:** <https://discourse.julialang.org/t/partial-world-splitting/131114>\
**Category:** Internals & Design\
**Created:** [July 27, 2025, 7:15pm UTC](https://discourse.julialang.org/t/partial-world-splitting/131114 "2025-07-27T19:15:15Z")\
**Posts on this page:** 9\
**Page:** 1

<div class="post-metadata">

**Author:** ![cstjean](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cstjean/32/1444_2.png) [@cstjean](https://discourse.julialang.org/u/cstjean)\
**Post date:** [July 27, 2025, 7:15pm UTC](https://discourse.julialang.org/t/partial-world-splitting/131114/1 "2025-07-27T19:15:15Z")

</div>

@topolarity’s talk, [Can we achieve “invalidation freedom” for Julia? :: JuliaCon 2025 :: pretalx](https://pretalx.com/juliacon-2025/talk/DCDEQV/) made me think some more about world-splitting. If I understand correctly, a call that infers to `foo(::String, ::Any)`, where there are 2 candidate methods, will currently world-split to (the equivalent of)

```julia
if y isa Int
    @invoke foo(x::String, y::Int)::Float64
else
    @invoke foo(x::String, y::Float64)::Float64
end

```

This needs to be recompiled when a new `foo` method appears in a downstream package. But I didn’t quite get why @StefanKarpinski’s suggestion to add a dynamic call as a fallback wouldn’t work out. This would be:

```julia
if y isa Int
    @invoke foo(x::String, y::Int)::String
elseif y isa Float64
    @invoke foo(x::String, y::Float64)::String
else
    @dynamic_call foo(x::String, y::Any)::String
end

```

This looks like a good idea to me? In particular, we could change the rule to “World-splitting only considers methods that are defined in the call-site’s module or in its imported dependencies”. Because that’s the set of methods that are loaded during precompilation, the precompiled code wouldn’t change (beyond adding the fallback). And all methods of `foo` that are defined downstream and infer to `String` would no longer invalidate the above code.

Did I miss anything?

---

<div class="post-metadata">

**Author:** ![kristoffer.carlsson](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/kristoffer.carlsson/32/22_2.png) [@kristoffer.carlsson](https://discourse.julialang.org/u/kristoffer.carlsson)\
**Post date:** [July 27, 2025, 9:54pm UTC](https://discourse.julialang.org/t/partial-world-splitting/131114/2 "2025-07-27T21:54:25Z")

</div>

One reason is because you don’t know the return type of the dynamic fallback call, so you can no longer infer the type of `foo(x::String, y::Any)` if you have that present.

---

<div class="post-metadata">

**Author:** ![danielwe](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/danielwe/32/35657_2.png) [@danielwe](https://discourse.julialang.org/u/danielwe)\
**Post date:** [July 27, 2025, 11:03pm UTC](https://discourse.julialang.org/t/partial-world-splitting/131114/3 "2025-07-27T23:03:24Z")

</div>

Another might be that, as pointed out by the emcee (Tim Holy?), if the caller isn’t precompiled, the runtime performance characteristics would be dependent on load and execution order and thus unpredictable. That is, unless you enforce your rule,

> [@cstjean](#):
>
> “World-splitting only considers methods that are defined in the call-site’s module or in its imported dependencies”

even for methodinstances that are JIT compiled at runtime, but that seems like it would require that the compiler know all about code loading and dependencies, which I assume is not the case at the moment and sounds like it might not be a desireable/feasible coupling. (Caveat: all this is way above my pay grade; this is just how I parsed that part of the talk/QA.)

---

<div class="post-metadata">

**Author:** ![cstjean](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cstjean/32/1444_2.png) [@cstjean](https://discourse.julialang.org/u/cstjean)\
**Post date:** [July 28, 2025, 7:03pm UTC](https://discourse.julialang.org/t/partial-world-splitting/131114/4 "2025-07-28T19:03:49Z")

</div>

> [@kristoffer.carlsson](#):
>
> One reason is because you don’t know the return type of the dynamic fallback call, so you can no longer infer the type of `foo(x::String, y::Any)` if you have that present.

The idea would be to optimistically assume that the return type of future methods will be of the same type as the existing methods, hence the `::String` annotation at the end:

```julia
@dynamic_call foo(x::String, y::Any)::String

```

so it would infer identically.

In other words,

- _Current world-splitting_ assumes that all methods of `foo` are already known. It is invalidated whenever a new `foo` method appears.
- _World-splitting with optimistically-typed fallback_ assumes that all future methods of `foo` will return a `String`. It is invalidated whenever a new `foo` method does not infer to `String`.

It’s not a panacea, but it would likely solve the `convert(::Type{String}, x)` example from @topolarity in practice.

---

<div class="post-metadata">

**Author:** ![topolarity](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/topolarity/32/209470_2.png) [@topolarity](https://discourse.julialang.org/u/topolarity)\
**Post date:** [July 29, 2025, 2:22pm UTC](https://discourse.julialang.org/t/partial-world-splitting/131114/5 "2025-07-29T14:22:10Z")

</div>

Yeah, it’s a decent idea in the vein of “have the compiler guess / infer the interface” for an “extensible” call.

We’d need a bit of extra compiler machinery to be able to notice that a new registered method doesn’t invalidate the “interface” the compiler decided to infer here, but I think it’s a sensible proposal.

> It’s not a panacea, but it would likely solve the `convert(::Type{String}, x)` example

Yeah, it does ultimately end up in the “heuristic” space. It has a couple of likely failures:

1. The compiler still has to “guess” at the interface here. It might do that successfully for, e.g., a concrete return type, but anything else is likely to be a wrong guess, which invalidates.
2. New methods that return `::String` but don’t _infer_ as returning `::String` (i.e. inference was imprecise) will still cause invalidation.

---

<div class="post-metadata">

**Author:** ![jules](https://avatars.discourse-cdn.com/v4/letter/j/41988e/32.png) [@jules](https://discourse.julialang.org/u/jules)\
**Post date:** [July 29, 2025, 2:27pm UTC](https://discourse.julialang.org/t/partial-world-splitting/131114/6 "2025-07-29T14:27:48Z")

</div>

After the discussions at JuliaCon I was wondering if a simple way to recover optimizations for “final interfaces” would be to allow closing abstract types. It would then not be allowed to add any other subtypes to it, making dispatch on it behave like that on a union of all its current subtypes. That would be an easy way to recover performance in cases where people were relying on world splitting in this case:

```julia-auto
abstract type Abs end
struct ConcreteEither <: Abs end
struct ConcreteOr <: Abs end
close_type!(Abs)

f(x::Vector{Abs}) = ... # this could now not be invalidated anymore

```

---

<div class="post-metadata">

**Author:** ![mbauman](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mbauman/32/31082_2.png) [@mbauman](https://discourse.julialang.org/u/mbauman)\
**Post date:** [July 29, 2025, 2:29pm UTC](https://discourse.julialang.org/t/partial-world-splitting/131114/7 "2025-07-29T14:29:41Z")

</div>

I think this is quite clever and probably would help “cure” many invalidations. It’d be interesting to collect data on exactly how many invalidations are of this flavor.

My hunch, though, is that there are probably just as many with a slightly different — and harder to divine — pattern:

```julia-auto
if y isa Int
    @invoke foo(x::String, y::Int)::Int
elseif y isa Float64
    @invoke foo(x::String, y::Float64)::Float64
else

```

Here, you’d want to extrapolate the dynamic call to:

```julia-auto
    @dynamic_call foo(x::String, y::Any)::typeof(y)

```

---

<div class="post-metadata">

**Author:** ![cstjean](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cstjean/32/1444_2.png) [@cstjean](https://discourse.julialang.org/u/cstjean)\
**Post date:** [July 29, 2025, 3:27pm UTC](https://discourse.julialang.org/t/partial-world-splitting/131114/8 "2025-07-29T15:27:51Z")

</div>

But this is a case where `y`’s type is not inferred, right? That’s why we split the world on `y`’s type. So the `::typeof(y)` wouldn’t help anything, I think?

I would go with `::Union{Int, Float64}` in that case. And then if that gets invalidated (because the new method infers to `Complex64`), give up and go straight to a dynamic call.

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [July 29, 2025, 3:40pm UTC](https://discourse.julialang.org/t/partial-world-splitting/131114/9 "2025-07-29T15:40:36Z")

</div>

> [@cstjean](#):
>
> ```julia-auto
> if y isa Int
> @invoke foo(x::String, y::Int)::Float64
> else
> @invoke foo(x::String, y::Float64)::Float64
> end
> 
> ```

Assuming that those signatures reflect the 2 candidate methods, wouldn’t there need to be a `y isa Float64` check and a 3rd branch throwing a `MethodError` for any other type of `y`? This would instead make the last `invoke` throw a `TypeError` for an unsuitable input type.

> [@cstjean](#):
>
> The idea would be to optimistically assume that the return type of future methods will be of the same type as the existing methods, hence the `::String` annotation at the end… It is invalidated whenever a new `foo` method does not infer to `String` .

At first glance, I’d have two concerns.  
(1) This would introduce a dependence of the compiled code on which methods were present at time of compilation. In that example, we compiled the caller when `foo` had 2 candidate methods, thus 2 `@invoke` branches and a `@dynamic_call` fallback. If I write a 3rd method, I use the same code’s fallback branch. Say we saved the caller and the now 3 `foo` methods to a file and evaluate them into another session. The compiled code would have 3 `@invoke` branches instead (don’t know what the limit to candidate methods are, but I think we’re close to it). I don’t know if this is actually the case, but I was under the impression that the intent is to have the same compiled code for the same state of methods no matter how or when we get there; it seems like reflection and profiling is only feasible this way.

(2) We wouldn’t need to invalidate the caller’s code, but we do have to check whether to do that. We’d need to check the callers rather than the `foo` function because we generally can’t know the candidate methods or their calls’ return types that the caller inferred; one caller’s branch can look very different from another caller’s branch. I don’t know how invalidation is implemented, but can this check be done alongside invalidation without much overhead? Can this check and possible invalidation be done on demand instead of all at once upon defining a method?
