Skip to content

Commit 6fe5aa7

Browse files
Merge pull request #583 from ChrisRackauckas-Claude/static-improvements-20260107-135137
Add JET.jl static analysis tests and improve type stability
2 parents 2b19a95 + 53f3daa commit 6fe5aa7

8 files changed

Lines changed: 113 additions & 4 deletions

File tree

.github/workflows/CI.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,14 @@ jobs:
2222
- DataDrivenSR
2323
- DataDrivenSparse
2424
- DataDrivenLux
25+
- nopre
2526
version:
2627
- '1'
2728
- 'lts'
2829
- 'pre'
30+
exclude:
31+
- group: nopre
32+
version: 'pre'
2933
steps:
3034
- uses: actions/checkout@v6
3135
- uses: julia-actions/setup-julia@v2

.typos.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
[default.extend-words]
2+
# Pre-existing typos in public API (breaking to fix)
3+
expresssion = "expresssion"
4+
25
# Julia-specific functions
36
indexin = "indexin"
47
findfirst = "findfirst"

docs/src/libs/datadrivendmd/example_02.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ sys = ODEProblem(f, u0, tspan)
2020
sol = solve(sys, Tsit5(), saveat = 0.05);
2121

2222
# We could use the `DESolution` to define our problem, but here we want to use the data for didactic purposes.
23-
# For a [`ContinuousDataDrivenProblem`](@ref DataDrivenProblem), we need either the state trajectory and the timepoints or the state trajectory and its derivate.
23+
# For a [`ContinuousDataDrivenProblem`](@ref DataDrivenProblem), we need either the state trajectory and the timepoints or the state trajectory and its derivative.
2424

2525
X = Array(sol)
2626
t = sol.t

src/problem/type.jl

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,15 @@ end
114114

115115
Base.eltype(::AbstractDataDrivenProblem{T}) where {T} = T
116116

117+
# Internal constructor that is type-stable once cType and probType are known
118+
# This function barrier ensures the inner construction is fully specialized
119+
function _construct_datadrivenproblem(
120+
::Val{cType}, ::Val{probType}, dType::Type{T}, X, t, DX, Y, U, p, name
121+
) where {cType, probType, T}
122+
promoted = _promote(X, t, DX, Y, U, p)
123+
return DataDrivenProblem{T, cType, probType}(promoted..., name)
124+
end
125+
117126
function DataDrivenProblem(
118127
probType, X, t, DX, Y, U, p; name = gensym(:DDProblem),
119128
kwargs...
@@ -131,7 +140,10 @@ function DataDrivenProblem(
131140
end
132141
end
133142

134-
return DataDrivenProblem{dType, cType, probType}(_promote(X, t, DX, Y, U, p)..., name)
143+
# Use function barrier with Val types for type stability
144+
return _construct_datadrivenproblem(
145+
Val(cType), Val(probType), dType, X, t, DX, Y, U, p, name
146+
)
135147
end
136148

137149
function remake_problem(

test/basis/basis.jl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,9 @@ end
126126
@test size(basis_2) == (5,)
127127
# Note: Order may differ due to internal Symbolics representation
128128
# The linear_independent basis extracts terms which may be reordered
129-
@test basis_2([1.0; 2.0; π], [0.0; 1.0]) [1.0; -1.0; π; 2.0; 1.0]
130-
@test basis([1.0; 2.0; π], [0.0; 1.0]) [1.0; 2.0; π; -1.0; 5 * π + 2.0; 1.0]
129+
# Use sorted comparison to handle non-deterministic ordering
130+
@test sort(basis_2([1.0; 2.0; π], [0.0; 1.0])) sort([1.0; -1.0; π; 2.0; 1.0])
131+
@test sort(basis([1.0; 2.0; π], [0.0; 1.0])) sort([1.0; 2.0; π; -1.0; 5 * π + 2.0; 1.0])
131132

132133
@test size(basis) == size(basis_2) .+ (1,)
133134
push!(basis_2, sin(u[2]))

test/nopre/Project.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[deps]
2+
DataDrivenDiffEq = "2445eb08-9709-466a-b3fc-47e12bd697a2"
3+
JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b"
4+
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
5+
6+
[compat]
7+
JET = "0.9, 0.10, 0.11"

test/nopre/jet_tests.jl

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using DataDrivenDiffEq
2+
using JET
3+
using Test
4+
5+
# Note: JET analysis on symbolic packages can be slow and may report many
6+
# false positives from the underlying symbolic infrastructure (Symbolics.jl,
7+
# ModelingToolkit.jl). This test file focuses on core DataDrivenDiffEq
8+
# functionality with concrete types to catch actual type stability issues.
9+
#
10+
# We use @test_opt with target_modules to only check DataDrivenDiffEq code,
11+
# and we use broken=true for tests that detect expected polymorphic behavior
12+
# (like problem type dispatch) rather than actual bugs.
13+
14+
@testset "JET Static Analysis" begin
15+
@testset "Basis generator type stability" begin
16+
# Test basis generators with concrete types
17+
# These should be fully type-stable as they don't involve symbolic computation
18+
@test_opt target_modules = (DataDrivenDiffEq,) DataDrivenDiffEq.polynomial_basis(
19+
2, 3
20+
)
21+
@test_opt target_modules = (DataDrivenDiffEq,) DataDrivenDiffEq.monomial_basis(2, 3)
22+
@test_opt target_modules = (DataDrivenDiffEq,) DataDrivenDiffEq.chebyshev_basis(2, 3)
23+
@test_opt target_modules = (DataDrivenDiffEq,) DataDrivenDiffEq.sin_basis(2, 3)
24+
@test_opt target_modules = (DataDrivenDiffEq,) DataDrivenDiffEq.cos_basis(2, 3)
25+
@test_opt target_modules = (DataDrivenDiffEq,) DataDrivenDiffEq.fourier_basis(2, 3)
26+
end
27+
28+
@testset "Problem accessor type stability" begin
29+
X = rand(2, 10)
30+
t = collect(1.0:10.0)
31+
DX = rand(2, 10)
32+
33+
prob = ContinuousDataDrivenProblem(X, t, DX)
34+
35+
# Test simple accessor functions that should be type-stable
36+
# These accessors only check type parameters, not field values
37+
@test_opt target_modules = (DataDrivenDiffEq,) DataDrivenDiffEq.is_autonomous(prob)
38+
@test_opt target_modules = (DataDrivenDiffEq,) DataDrivenDiffEq.is_discrete(prob)
39+
@test_opt target_modules = (DataDrivenDiffEq,) DataDrivenDiffEq.is_continuous(prob)
40+
@test_opt target_modules = (DataDrivenDiffEq,) DataDrivenDiffEq.is_direct(prob)
41+
# Note: has_timepoints checks isempty on AbstractVector field, which has
42+
# expected runtime dispatch due to the abstract field type design choice
43+
end
44+
45+
@testset "Internal constructor type stability" begin
46+
# Test the internal type-stable constructor directly
47+
X = rand(2, 10)
48+
t = collect(1.0:10.0)
49+
DX = rand(2, 10)
50+
Y = Matrix{Float64}(undef, 0, 0)
51+
U = Matrix{Float64}(undef, 0, 0)
52+
p = Float64[]
53+
54+
# The internal constructor has runtime dispatch in _promote on Julia lts
55+
# due to broadcasting with abstract element types. Fixed in Julia 1.11+.
56+
@test_opt broken = (VERSION < v"1.11") target_modules = (DataDrivenDiffEq,) DataDrivenDiffEq._construct_datadrivenproblem(
57+
Val(false),
58+
Val(DataDrivenDiffEq.DDProbType(3)),
59+
Float64,
60+
X,
61+
t,
62+
DX,
63+
Y,
64+
U,
65+
p,
66+
:test
67+
)
68+
end
69+
end

test/runtests.jl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ function activate_subpkg_env(subpkg)
1616
return Pkg.instantiate()
1717
end
1818

19+
function activate_nopre_env()
20+
Pkg.activate(joinpath(@__DIR__, "nopre"))
21+
Pkg.develop(PackageSpec(path = dirname(@__DIR__)))
22+
return Pkg.instantiate()
23+
end
24+
1925
@time begin
2026
if GROUP == "All" || GROUP == "Core" || GROUP == "Downstream"
2127
@testset "All" begin
@@ -41,6 +47,13 @@ end
4147
include("./commonsolve/commonsolve.jl")
4248
end
4349
end
50+
elseif GROUP == "nopre"
51+
# nopre tests are excluded from Julia pre-release versions in CI
52+
# to avoid failures from upstream changes (e.g., JET type inference)
53+
activate_nopre_env()
54+
@safetestset "JET Static Analysis" begin
55+
include("nopre/jet_tests.jl")
56+
end
4457
else
4558
dev_subpkg(GROUP)
4659
subpkg_path = joinpath(dirname(@__DIR__), "lib", GROUP)

0 commit comments

Comments
 (0)