Skip to content

describe() throws FieldError for strategies with 3+ type parameters (:sciml); no arity coverage, no describe test for DifferentiationInterface #516

Description

@ocots

Summary

Strategies.describe(id, registry) throws FieldError for any strategy whose type has three
or more type parameters
. _strategy_base_name unwraps exactly one UnionAll layer and then
assumes a DataType underneath, which only holds for strategies with exactly two.

The first strategy in the ecosystem to cross that line is CTSolvers.Integrators.SciML (four
type parameters), so describe(:sciml, registry) is currently unusable — see
control-toolbox/CTSolvers.jl (companion issue). CTBase's own DifferentiationInterface has
two, i.e. it sits exactly one parameter below the break, which is why nothing caught this.

Root cause

src/Strategies/api/describe_registry.jl:496-506:

_strategy_base_name(T::DataType) = string(T.name.name)
_strategy_base_name(T::UnionAll) = string(T.body.name.name)   # peels exactly ONE layer
_strategy_base_name(T::Type)     = string(nameof(T))          # correct — and unreachable

Partially applying a parametric type leaves one UnionAll layer per remaining free
parameter. So T.body is a DataType only when exactly one parameter is left, i.e. when the
strategy has two in total. With three or more, T.body is still a UnionAll and has no
.name.

The third method — nameof, which is correct at any depth — is never reached, because
::UnionAll is more specific than ::Type.

Reproduction

Fake strategies differing only in type-parameter count:

using CTBase: CTBase
const S = CTBase.Strategies
abstract type FakeFamily <: S.AbstractStrategy end
struct P1{P<:Union{S.CPU,S.GPU}}          <: FakeFamily end
struct P2{P<:Union{S.CPU,S.GPU},O}        <: FakeFamily end
struct P3{P<:Union{S.CPU,S.GPU},O,Q}      <: FakeFamily end
struct P4{P<:Union{S.CPU,S.GPU},O,Q,R}    <: FakeFamily end
type total params T{CPU} _strategy_base_name nameof
P1 1 DataType "P1" P1
P2 2 UnionAll "P2" P2
P3 3 UnionAll FieldError P3
P4 4 UnionAll FieldError P4

Real types, same measurement:

strategy params result
CTSolvers.Solvers.Ipopt 1
CTBase.Differentiation.DifferentiationInterface 2
CTSolvers.Integrators.SciML 4 FieldError

Blast radius beyond the strategy itself

describe(:cpu, registry) and describe(:gpu, registry) break too, on any registry that
contains such a strategy: the parameter branch (_describe_parameter_registry) walks every
registered strategy to list which ones support the parameter, and hits the same call. So one
undescribable strategy takes the parameter introspection down with it.

Measured on the two registries in play:

describe(:cpu,   flow_registry())  # ‼ FieldError  (contains SciML)
describe(:cpu,   solve_registry()) # ✅            (no strategy above 2 params)

Suggested fix

Delete the ::UnionAll method and let the generic nameof one handle it — verified to give
the right answer for all five real types above, at any depth. (Or peel to the bottom in a
while, but nameof already does exactly that.)

Missing test coverage

This is the part worth more than the one-line fix — the bug is trivial, the gap that let it
through is not.

  1. Parametric-arity matrix. Fake strategies with 1, 2, 3 and 4 type parameters, asserting
    describe works for each. The existing suite only ever exercises 1- and 2-parameter
    strategies, which is exactly the range where the bug is invisible.

  2. DifferentiationInterface has no describe test at all, despite being a strategy that
    CTBase itself owns. It is currently only reachable through CTFlows' flow registry, so CTBase
    has no coverage of its own strategy. Worth a registry + describe test here.

  3. Parameter position. create_registry builds strategy_type{param_type}, and every
    Strategies.parameter implementation in the ecosystem reads slot 1
    (parameter(::Type{<:X{P}}) where {P} = P). So the strategy parameter must be the first
    type parameter
    . This is a real contract, and it is neither documented nor validated. It
    does fail loudly today, which is good, but unhelpfully:

    fake declaration result
    Tight {O<:StrategyOptions, P<:Union{CPU,GPU}} TypeError at create_registry
    Loose {A, P<:Union{CPU,GPU}} registry builds; parameter() throws NotImplemented

    Neither error mentions the actual rule. Suggest either validating it in create_registry
    with a proper IncorrectArgument ("the strategy parameter must be the first type
    parameter"), or stating it in the AbstractStrategy contract docs — and covering both
    spellings with tests.

(Note on reading the repro: minimal fakes that do not implement metadata/options throw
NotImplemented from describe regardless. The discriminator for this bug is
FieldError vs NotImplemented, not pass vs fail.)

References

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions