Skip to content

Make Vector{Union{}} arraylike, not dictlike - #79

Draft
krynju wants to merge 2 commits into
JuliaServices:mainfrom
krynju:kg/union-bottom-vector-not-dictlike
Draft

krynju wants to merge 2 commits into
JuliaServices:mainfrom
krynju:kg/union-bottom-vector-not-dictlike

Conversation

@krynju

@krynju krynju commented Oct 7, 2026 •

Copy link
Copy Markdown

Union{} is a subtype of every type, so Vector{Union{}} <: AbstractVector{<:Pair} holds and

dictlike(::Type{<:AbstractVector{<:Pair}}) = true

matches it, while arraylike(::Type{<:AbstractArray}) = true matches it too. The two traits disagree for a type that can only ever be an empty array, and make checks dictlike first, so such vectors are built through makedict (and make! on one goes through the dict path as well).

This adds a more specific method:

dictlike(::Type{<:AbstractVector{Union{}}}) = false

Before / after:

julia> StructUtils.dictlike(Vector{Union{}}), StructUtils.arraylike(Vector{Union{}})
(true, true)    # before
(false, true)   # after

julia> StructUtils.dictlike(Vector{<:Pair}), StructUtils.dictlike(Vector{Pair{String,Int}})
(true, true)    # unchanged

Observable behaviour is nearly identical since the vector is always empty: make(Vector{Union{}}, []) and make(Vector{Union{}}, Dict()) still return Union{}[], and a non-empty source still throws. The point is trait consistency for downstream consumers that branch on dictlike before arraylike the way make does. This surfaced while fixing JuliaIO/JSON.jl#499, where the same Union{} <: Pair subtyping made JSON.jl write Union{}[] as {} (fixed on the JSON.jl side in JuliaIO/JSON.jl#500; that fix does not depend on this one).

The other Pair-vector special cases here (applyeach, _keytype, _valtype) use the invariant AbstractVector{Pair{K,V}} and are not affected. Test.detect_ambiguities(StructUtils) is unchanged (1 default, 51 with ambiguous_bottom=true). Full test suite passes locally on Julia 1.12.7, including the trim-compile stage.

Second commit, from a self-review pass:

  • make(Union{}, source) (reached e.g. via make(Vector{Union{}}, [1])) used to fall into the T <: Tuple branch and die inside fieldcount(Union{}) with "The empty type does not have a well-defined number of fields". It now throws a dedicated, type-free ArgumentError("cannot make a value of type Union{} because it has no instances") from a @noinline helper, same shape as _absentfield_error. The T === Union{} check constant-folds away for every other T.
  • The dictlike docstring now mentions the AbstractVector{<:Pair} rule and the Union{} exception.
  • Tests: swapped two Union{Vector{Union{}}, Dict} selection assertions that passed with or without the fix (union disambiguation only consults arraylike) for a make! check and the exact error messages.

One thing I left for you to decide: dictlike(::Type{<:AbstractVector{<:Pair}}) is covariant, but every consumer of a dictlike Pair vector (applyeach, _keytype/_valtype, addkeyval!) only handles the invariant AbstractVector{Pair{K,V}}, so e.g. make(Vector{Pair}, Dict("a" => 1)) fails on liftkey(Int, "a") and make(Vector{<:Pair}, ...) has no (undef, 0) constructor. Tightening the rule to AbstractVector{Pair{K,V}} where {K,V} would also make the Union{} special case unnecessary, but it changes what dictlike(Vector{<:Pair}) reports, so I kept the narrower fix here.

🤖 Generated with Claude Code

`Union{}` is a subtype of every type, so `Vector{Union{}}` matched
`dictlike(::Type{<:AbstractVector{<:Pair}}) = true` while `arraylike`
was also `true` for it. `make` checks `dictlike` first, so an empty
`Union{}`-element vector (e.g. JSON3.jl's empty arrays) was built via
the dict path. Add a more specific `dictlike` method returning `false`
so the traits agree that such a vector is an (always empty) array.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`make(Union{}, source)` fell into the `T <: Tuple` branch (Union{} is a
subtype of everything) and failed inside `fieldcount` with an unrelated
"does not have a well-defined number of fields" message. Check for the
bottom type up front and throw a dedicated, type-free ArgumentError.

Also mention the AbstractVector{<:Pair} rule and the Vector{Union{}}
exception in the `dictlike` docstring, and replace two union-selection
tests that passed with or without the fix by `make!` and error-message
checks.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

1 participant