Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions src/Strategies/contract/abstract_strategy.jl
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,9 @@ Strategies.parameter(::Type{<:MyStrategy}) = nothing
Strategies.parameter(::Type{<:MyStrategy{P}}) where {P<:AbstractStrategyParameter} = P
```

A non-throwing 2-arg variant, `parameter(strategy_type, default)`, is also available for
callers that cannot guarantee the type they're querying implements this contract — see below.

See also: [`CTBase.Strategies.default_parameter`](@ref), [`CTBase.Strategies.AbstractStrategyParameter`](@ref)
"""
function parameter end
Expand Down Expand Up @@ -304,6 +307,55 @@ function parameter(::Type{T}) where {T<:AbstractStrategy}
)
end

"""
$(TYPEDSIGNATURES)

Return the strategy parameter type for a concrete strategy type, or `default` if the
strategy type does not implement the `parameter` contract at all.

This is the non-throwing counterpart to `parameter(strategy_type)` — the same relationship
as `get(dict, key, default)` to `dict[key]`. Use it when querying a strategy type you did
not author and cannot guarantee has overridden the (optional) `parameter` method; use the
1-arg form when the type is known to implement the contract and a missing implementation
should be a loud error.

Note this is different from a strategy that *does* implement `parameter` and legitimately
returns `nothing` (the documented, established answer for "no parameter", e.g. `Ipopt`) —
that `nothing` is returned as-is, not replaced by `default`. `default` is only substituted
when the underlying call throws `Exceptions.NotImplemented`; any other exception still
propagates.

# Arguments
- `strategy_type::Type{<:AbstractStrategy}`: The strategy type
- `default`: Value to return if `strategy_type` does not implement `parameter`

# Returns
- Whatever `parameter(strategy_type)` returns, or `default` if that call throws
`Exceptions.NotImplemented`

# Example
```julia-repl
julia> parameter(SomeThirdPartyStrategy, nothing) # doesn't throw even if unimplemented
nothing

julia> parameter(Ipopt, :fallback) # Ipopt implements parameter -> nothing, not :fallback
nothing

julia> parameter(MadNLP{CPU}, :fallback)
CPU
```

See also: [`CTBase.Strategies.parameter`](@ref), [`CTBase.Strategies.default_parameter`](@ref)
"""
function parameter(strategy_type::Type{<:AbstractStrategy}, default)
return try
parameter(strategy_type)
catch e
e isa Exceptions.NotImplemented || rethrow()
default
end
end

"""
Default implementation for `options(strategy::T)` with flexible field access.

Expand Down
38 changes: 38 additions & 0 deletions test/suite/strategies/test_abstract_strategy.jl
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ Strategies.options(strategy::FakeStrategy) = strategy.options
# Additional test struct for error handling
struct UnimplementedStrategy <: Strategies.AbstractStrategy end

# Parameterized fake, to prove a real value passes through the 2-arg parameter() unchanged.
struct FakeStrategyParam{P<:Strategies.AbstractStrategyParameter} <: Strategies.AbstractStrategy end
Strategies.parameter(::Type{<:FakeStrategyParam{P}}) where {P} = P

# Overrides parameter but throws something OTHER than NotImplemented — proves the 2-arg
# wrapper only swallows NotImplemented and rethrows everything else.
struct FakeStrategyBadParameter <: Strategies.AbstractStrategy end
function Strategies.parameter(::Type{<:FakeStrategyBadParameter})
return throw(Exceptions.IncorrectArgument("deliberately not NotImplemented"))
end

# Fake strategy with description for testing multi-line display
struct FakeStrategyWithDescription <: Strategies.AbstractStrategy
options::Strategies.StrategyOptions
Expand Down Expand Up @@ -153,6 +164,33 @@ function test_abstract_strategy()
)
end

Test.@testset "parameter(T, default) - non-throwing accessor" begin
# Falls back to default when parameter() is not implemented at all.
Test.@test Strategies.parameter(UnimplementedStrategy, nothing) === nothing
Test.@test Strategies.parameter(UnimplementedStrategy, :fallback) ===
:fallback

# A strategy that legitimately returns nothing is NOT conflated with
# "unimplemented": this just confirms the passthrough works when there is
# no exception at all.
Test.@test Strategies.parameter(FakeStrategy, :fallback) === nothing

# A real parameter value passes through unchanged.
Test.@test Strategies.parameter(FakeStrategyParam{Strategies.CPU}, :fallback) ===
Strategies.CPU

# Non-NotImplemented errors still propagate — the wrapper is not a blanket
# catch-all.
Test.@test_throws Exceptions.IncorrectArgument Strategies.parameter(
FakeStrategyBadParameter, nothing
)

# 1-arg form is unchanged: still throws for a genuinely unimplemented strategy.
Test.@test_throws Exceptions.NotImplemented Strategies.parameter(
UnimplementedStrategy
)
end

Test.@testset "Collection interface - getindex" begin
# Use build_strategy_options to properly initialize alias_map
fake_opts = Strategies.build_strategy_options(
Expand Down
Loading