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
1 change: 1 addition & 0 deletions _typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
locale = "en"
extend-ignore-re = [
"ser",
"mis-",
]

[files]
Expand Down
10 changes: 5 additions & 5 deletions src/Components/constraints_accessors.jl
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ See also: [`CTModels.Components.boundary_constraints_nl`](@ref), [`CTModels.Comp
"""
function path_constraints_nl(
model::ConstraintsModel{TP,<:Tuple,<:Tuple,<:Tuple,<:Tuple}
) where {TP}
) where {TP<:Tuple}
return model.path_nl
end

Expand All @@ -48,7 +48,7 @@ See also: [`CTModels.Components.path_constraints_nl`](@ref), [`CTModels.Componen
"""
function boundary_constraints_nl(
model::ConstraintsModel{<:Tuple,TB,<:Tuple,<:Tuple,<:Tuple}
) where {TB}
) where {TB<:Tuple}
return model.boundary_nl
end

Expand All @@ -64,7 +64,7 @@ See also: [`CTModels.Components.control_constraints_box`](@ref), [`CTModels.Comp
"""
function state_constraints_box(
model::ConstraintsModel{<:Tuple,<:Tuple,TS,<:Tuple,<:Tuple}
) where {TS}
) where {TS<:Tuple}
return model.state_box
end

Expand All @@ -80,7 +80,7 @@ See also: [`CTModels.Components.state_constraints_box`](@ref), [`CTModels.Compon
"""
function control_constraints_box(
model::ConstraintsModel{<:Tuple,<:Tuple,<:Tuple,TC,<:Tuple}
) where {TC}
) where {TC<:Tuple}
return model.control_box
end

Expand All @@ -96,7 +96,7 @@ See also: [`CTModels.Components.state_constraints_box`](@ref), [`CTModels.Compon
"""
function variable_constraints_box(
model::ConstraintsModel{<:Tuple,<:Tuple,<:Tuple,<:Tuple,TV}
) where {TV}
) where {TV<:Tuple}
return model.variable_box
end

Expand Down
2 changes: 1 addition & 1 deletion src/Components/functors.jl
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ g = CoercedTrajectory(t -> [t, 2t], identity)
g(0.5) # returns [0.5, 1.0]
```
"""
struct CoercedTrajectory{F,C} <: Function
struct CoercedTrajectory{F,C<:Union{typeof(only),typeof(identity)}} <: Function
inner::F
coerce::C
end
Expand Down
10 changes: 7 additions & 3 deletions src/Init/init_functors.jl
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,15 @@ f = MergedTrajectory(base, comps, 2, :state)
f(0.5) # returns [0.0, sin(0.5)]
```
"""
struct MergedTrajectory{F,C} <: Function
struct MergedTrajectory{F,C<:AbstractDict{Int,<:Function}} <: Function
base::F
comps::C # Dict{Int,Function}
dim::Int
role::Symbol

function MergedTrajectory{F,C}(base::F, comps::C, dim::Int, role::Symbol) where {F,C}
function MergedTrajectory{F,C}(
base::F, comps::C, dim::Int, role::Symbol
) where {F,C<:AbstractDict{Int,<:Function}}
for i in keys(comps)
if !(1 <= i <= dim)
throw(
Expand All @@ -133,7 +135,9 @@ struct MergedTrajectory{F,C} <: Function
end
end

function MergedTrajectory(base::F, comps::C, dim::Int, role::Symbol) where {F,C}
function MergedTrajectory(
base::F, comps::C, dim::Int, role::Symbol
) where {F,C<:AbstractDict{Int,<:Function}}
return MergedTrajectory{F,C}(base, comps, dim, role)
end

Expand Down
2 changes: 1 addition & 1 deletion src/Models/model.jl
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ end

Traits.has_time_dependence_trait(::Model) = true

Traits.time_dependence(::Model{TD}) where {TD} = TD
Traits.time_dependence(::Model{TD}) where {TD<:TimeDependence} = TD

Traits.has_variable_dependence_trait(::Model) = true

Expand Down
67 changes: 67 additions & 0 deletions test/suite/meta/test_where_bounds.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""
Regression guard for the `where`-clause bound-dropping pitfall (see
`.reports/2026-07-12_alias-where-bounds-audit.md` and the Handbook rule in
`philosophy/types-traits-interfaces.md#aliases-and-where`).

A `where {X}` clause that names a type parameter without repeating the bound the
struct already declares for it silently widens it to `<:Any`. This is invisible
via `isa` on concrete instances, but it breaks Julia's method-specificity ranking
the moment a competing method exists — either mis-dispatching silently or throwing
`MethodError: ... is ambiguous`.

CTModels has no parametric aliases, so the CTFlows-style `Alias <: Parent` guard
does not apply. Instead this test asserts, for each method whose `where`-clause was
tightened, that the induced `TypeVar`'s upper bound is the intended bound and not
`Any`. A future edit that drops one of these bounds again fails loudly here.
"""

module TestWhereBounds

import Test: Test
import CTBase.Traits: Traits
import CTModels.Components: Components
import CTModels.Models: Models

const VERBOSE = isdefined(Main, :TestData) ? Main.TestData.VERBOSE : true
const SHOWTIMING = isdefined(Main, :TestData) ? Main.TestData.SHOWTIMING : true

# Upper bound of the outermost `where`-var of the method dispatched for `argtypes`.
# Returns `nothing` when the signature is not a `UnionAll` (i.e. no `where`-var at
# all), which fails the `=== expected` assertions below just as a widened bound does.
function _where_ub(f, argtypes::Type{<:Tuple})
m = which(f, argtypes)
return m.sig isa UnionAll ? m.sig.var.ub : nothing
end

function test_where_bounds()
Test.@testset "where-clause bound-dropping regression guard" verbose = VERBOSE showtiming =
SHOWTIMING begin

# The 5 ConstraintsModel accessors: each restricts 4 params to <:Tuple in the
# type-parameter position and extracts the 5th via a `where`-clause that must
# repeat the same <:Tuple bound (constraints_accessors.jl). `which` on a bare
# ConstraintsModel argument selects the ConstraintsModel-specific method (the
# convenience Model-forwarding overload does not match a ConstraintsModel).
Test.@testset "ConstraintsModel accessors — where-var ub === Tuple" begin
CM = Tuple{Components.ConstraintsModel}
Test.@test _where_ub(Components.path_constraints_nl, CM) === Tuple
Test.@test _where_ub(Components.boundary_constraints_nl, CM) === Tuple
Test.@test _where_ub(Components.state_constraints_box, CM) === Tuple
Test.@test _where_ub(Components.control_constraints_box, CM) === Tuple
Test.@test _where_ub(Components.variable_constraints_box, CM) === Tuple
end

# Model's time-dependence trait reads TD from the type parameter; the method's
# `where`-clause must repeat the `TD<:TimeDependence` bound the Model struct
# declares (model.jl).
Test.@testset "Model time_dependence — where-var ub === TimeDependence" begin
Test.@test _where_ub(Traits.time_dependence, Tuple{Models.Model}) ===
Components.TimeDependence
end
end
end

end # module

# CRITICAL: Redefine in outer scope for TestRunner
test_where_bounds() = TestWhereBounds.test_where_bounds()