Skip to content

inference: fold TypeVar components in Union apply_type_tfunc#17

Open
adienes wants to merge 5 commits into
masterfrom
kwalloc
Open

inference: fold TypeVar components in Union apply_type_tfunc#17
adienes wants to merge 5 commits into
masterfrom
kwalloc

Conversation

@adienes

@adienes adienes commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Keyword arguments declared with sugared unions like Union{Nothing,<:Integer} cost several allocations (~200ns) per call, because lowering splices the declared-type expression into the keyword sorter for the isa check, and the Union head of apply_type_tfunc had no PartialTypeVar case — so the runtime _typevar/apply_type/UnionAll construction never folded.

This teaches the Union branch of apply_type_tfunc to:

  • fold PartialTypeVar components into Type{Union{..., tv}}, preserving the TypeVar identity needed for the later abstract_call_unionall rewrap, after which the isa guard folds and the construction is DCE'd;
  • fold Const TypeVar components to Const (their object identity is known);
  • handle the collapse case soundly: Union{Union{}, tv} is the bare TypeVar object at runtime, not a Type, so results distinguish value TypeVars (TypeVar), symbolic TypeVars from Type{B} components (Type{ty}), and unknown components (Union{Type,TypeVar}). The pre-existing hasnonType → Type shortcut was unsound for exactly this reason.

apply_type_nothrow now validates Union components against Union{Type,TypeVar} instead of blindly returning true, and ⊑(::ConstsLattice) learns to compare PartialTypeVar against types wider than plain TypeVar.

The merge with master (past JuliaLang#62001's TypeEgal kind) surfaced one semantic interaction: the sound Union{Type,TypeVar} widening reaches promote_typejoin (whose typejoin call infers Any), degrading eltype(::Tuple) inference now that _compute_eltype is reachable in the equality-keyed world. Since promote_typejoin always returns a Type at runtime, a ::Type assertion on its return value restores the precision.

With this, the keyword sorter for a Union{Nothing,<:Integer} kwarg contains no _typevar calls or foreigncalls, and both repros from the issue run allocation-free.

Fixes JuliaLang#53917

This pull request was written with the assistance of generative AI (Claude).

🤖 Generated with Claude Code

adienes and others added 5 commits June 11, 2026 22:16
The Union branch of `apply_type_tfunc` did not handle `PartialTypeVar`
and widened to plain `Type`, so UnionAlls constructed in lowered code -
e.g. the keyword sorter's argtype check for
`f(v; y::Union{Nothing,<:Integer}=nothing)` - were rebuilt on every
call: a fresh TypeVar/Union/UnionAll plus a dynamic `isa`, costing 3
allocations and ~200ns per kwcall. Fold `PartialTypeVar` components
into `Type{Union{..., tv}}` (mirroring `_apply_type_tfunc`) so the
`isa` folds and the dead construction is eliminated.

Required adjacent fixes:
- the `Type` widening was unsound for possible TypeVar components:
  `Core.apply_type(Union, Union{}, tv)` returns the bare TypeVar
  object; widen to `Union{Type,TypeVar}` like the 2-argument case
  already does. Symbolic TypeVars from `Type{B}` components still
  yield `Type`, since they stand for a type equal to the TypeVar.
- `apply_type_nothrow` returned `true` for every Union application,
  including throwing ones like `apply_type(Union, ::Any)`; validate
  that each component is a `Type` or `TypeVar` instead.

Fixes JuliaLang#53917

This change was written with the assistance of generative AI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Unlike `PartialTypeVar` components, whose runtime TypeVar object is
created fresh on every execution, a `Const` TypeVar has known object
identity, so `apply_type(Union, ...)` over it can be folded to a
`Const` result: the runtime call computes the same function of egal
inputs, and type egal compares Unions structurally with TypeVars by
identity. This matches how `_apply_type_tfunc` already treats `Const`
TypeVar parameters for non-`Union` heads.

This change was written with the assistance of generative AI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The `ConstsLattice` partial order only accepted `PartialTypeVar ⊑
TypeVar` exactly, returning `false` for any wider right-hand side such
as `Union{Type,TypeVar}` or `Any`. Since the instance of a
`PartialTypeVar` is a runtime TypeVar object, fall back to comparing
its widened type in the wider lattice instead. This makes the
`PartialTypeVar` special case in `apply_type_nothrow` unnecessary.

This change was written with the assistance of generative AI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts:
#	Compiler/src/tfuncs.jl
#	Compiler/test/inference.jl

Besides the textual conflicts in apply_type_tfunc (kwalloc's
PartialTypeVar/mayTypeVar handling vs. the TypeEq/TypeEgal split from JuliaLang#62001),
this merge resolves a semantic conflict: kwalloc soundly widens `Union{...}`
with a not-provably-`Type` argument to `Union{Type,TypeVar}`, which after
JuliaLang#62001 surfaces in `promote_typejoin` (whose `typejoin` result infers `Any`)
and degraded `eltype(::Tuple)` inference from `Type` to `Union{TypeVar,Type}`.
Since `promote_typejoin` always returns a `Type` at runtime, assert that on its
return value to restore the precision.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The `Union` head of `apply_type_tfunc` widens its result to `Union{Type,TypeVar}`
whenever a component may be a bare `TypeVar` at runtime, but the vararg
early-return still pinned a `Union` head with a trailing `Vararg` argtype to
`Type`. Since `Union{T}` is `T`, such a call can return a bare `TypeVar`, so the
inferred `Type` was unsound; widen it to `Union{Type,TypeVar}` to match the
non-vararg path.

Also simplify the folding loop: `hasvaluetv` is redundant with the already
computed `mayTypeVar` on every path that reaches the second loop, and the
`mayTypeVar` update recomputed `hasintersect(ai, TypeVar)`. In the
`⊑(::ConstsLattice)` method for `PartialTypeVar`, reuse `widenconst` and keep the
`b === TypeVar` fast path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: Unnecessary allocations when including <: for abstract type in Union for a keyword argument

1 participant