Skip to content

The generated promoting constructor for @data has an unbound type parameter #94

Description

@SebastianM-C

Since 0.3.10, @data emits a "promoting" variant constructor for parametric self-referential fields (the fix for #34). When a variant has two or more bare self-referential fields and no other field pins the type parameter, that generated method has an unbound type parameter, so Test.detect_unbound_args fails for any package defining such an ADT.

MWE

import Moshi
using Moshi.Data: @data
using Test

@data Tree{T} begin
    Empty()
    Leaf(T)
    Node(Tree.Type{T}, Tree.Type{T})
end

Test.detect_unbound_args(Tree; recursive = true)

Output on Moshi 0.3.12 / Julia 1.12.7:

1-element Vector{Method}:
 Main.Tree.Node(var"1"::Union{Main.Tree.var"typeof(Tree)"{T}, Main.Tree.var"typeof(Tree)"{Union{}}},
                var"2"::Union{Main.Tree.var"typeof(Tree)"{T}, Main.Tree.var"typeof(Tree)"{Union{}}}) where T
   @ Main.Tree ~/.julia/packages/Moshi/jb5et/src/data/emit/cons.jl:203

If both arguments take the Tree.Type{Union{}} branch, nothing determines T.

As far as I can tell this is a static-analysis problem, not a runtime bug.
Because the variant has ≥2 self-refs, emit_each_variant_cons_inferred emits both the exact and the promoting constructor, and the exact one claims the all-bottom case:

e, l = Tree.Empty(), Tree.Leaf(3)

Tree.Node(l, e)   # -> Tree.Type{Int64}   dispatches to cons.jl:203 (promoting)  ← what #34 needs
Tree.Node(e, e)   # -> Tree.Type{Union{}} dispatches to cons.jl:176 (exact, T = Union{})

So the one argument shape that leaves T unbound in the promoting signature is exactly the shape the more-specific exact method already handles. The method is selectable only when at least one position is non-bottom, and then T is pinned.
detect_unbound_args works on signatures alone and can't see that.

Downstream impact

SymbolicUtils.jl caps Moshi = "0.3.6 - 0.3.9" for this reason (SymbolicUtils#1042):
its BasicSymbolicImpl.Div variant has num::BasicSymbolicImpl.Type{T} and den::BasicSymbolicImpl.Type{T}. Its full test suite passes on 0.3.12; only the QA lane fails. Any package with a binary-node ADT and an Aqua QA lane will hit this.

I have some possible suggestions from Claude, but I'm not sure if they are useful:

  1. Bind every self-ref position independently and reconcile in the body, e.g.
    Node(a::Tree.Type{A}, b::Tree.Type{B}) where {A, B}, resolving T from the
    non-bottom parameters and throwing when they're irreconcilable. No unbound
    parameters, and it subsumes the exact constructor. Downside: it accepts
    Node(::Tree.Type{Int}, ::Tree.Type{Float64}) at the signature level, so the
    rejection moves from dispatch to a runtime error.

  2. Emit one promoting method per self-ref position, with that position pinned to
    Tree.Type{T} and the rest left as the union. For two self-refs the existing
    exact method resolves the ambiguity at the fully-pinned point. This gets murky for
    three or more self-refs, where partial-bottom combinations are genuinely ambiguous.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions