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.
-
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.
-
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.
-
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
Summary
Strategies.describe(id, registry)throwsFieldErrorfor any strategy whose type has threeor more type parameters.
_strategy_base_nameunwraps exactly oneUnionAlllayer and thenassumes a
DataTypeunderneath, which only holds for strategies with exactly two.The first strategy in the ecosystem to cross that line is
CTSolvers.Integrators.SciML(fourtype parameters), so
describe(:sciml, registry)is currently unusable — seecontrol-toolbox/CTSolvers.jl (companion issue). CTBase's own
DifferentiationInterfacehastwo, 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:Partially applying a parametric type leaves one
UnionAlllayer per remaining freeparameter. So
T.bodyis aDataTypeonly when exactly one parameter is left, i.e. when thestrategy has two in total. With three or more,
T.bodyis still aUnionAlland has no.name.The third method —
nameof, which is correct at any depth — is never reached, because::UnionAllis more specific than::Type.Reproduction
Fake strategies differing only in type-parameter count:
T{CPU}_strategy_base_namenameofP1DataType"P1"P1P2UnionAll"P2"P2P3UnionAllFieldErrorP3P4UnionAllFieldErrorP4Real types, same measurement:
CTSolvers.Solvers.IpoptCTBase.Differentiation.DifferentiationInterfaceCTSolvers.Integrators.SciMLFieldErrorBlast radius beyond the strategy itself
describe(:cpu, registry)anddescribe(:gpu, registry)break too, on any registry thatcontains such a strategy: the parameter branch (
_describe_parameter_registry) walks everyregistered 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:
Suggested fix
Delete the
::UnionAllmethod and let the genericnameofone handle it — verified to givethe right answer for all five real types above, at any depth. (Or peel to the bottom in a
while, butnameofalready 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.
Parametric-arity matrix. Fake strategies with 1, 2, 3 and 4 type parameters, asserting
describeworks for each. The existing suite only ever exercises 1- and 2-parameterstrategies, which is exactly the range where the bug is invisible.
DifferentiationInterfacehas nodescribetest at all, despite being a strategy thatCTBase itself owns. It is currently only reachable through CTFlows' flow registry, so CTBase
has no coverage of its own strategy. Worth a registry +
describetest here.Parameter position.
create_registrybuildsstrategy_type{param_type}, and everyStrategies.parameterimplementation in the ecosystem reads slot 1(
parameter(::Type{<:X{P}}) where {P} = P). So the strategy parameter must be the firsttype parameter. This is a real contract, and it is neither documented nor validated. It
does fail loudly today, which is good, but unhelpfully:
Tight{O<:StrategyOptions, P<:Union{CPU,GPU}}TypeErroratcreate_registryLoose{A, P<:Union{CPU,GPU}}parameter()throwsNotImplementedNeither error mentions the actual rule. Suggest either validating it in
create_registrywith a proper
IncorrectArgument("the strategy parameter must be the first typeparameter"), or stating it in the
AbstractStrategycontract docs — and covering bothspellings with tests.
(Note on reading the repro: minimal fakes that do not implement
metadata/optionsthrowNotImplementedfromdescriberegardless. The discriminator for this bug isFieldErrorvsNotImplemented, not pass vs fail.)References
describe(:sciml)anddescribe(:di)work from a single entry point