From ccb6fb93271776cf6658cc353ed5aca4ec0be3eb Mon Sep 17 00:00:00 2001 From: Toby Driscoll Date: Mon, 29 Jun 2026 17:08:49 -0400 Subject: [PATCH 1/3] make point methods type-stable by making curve/path types more concrete --- src/arcs.jl | 9 ++++++--- src/circles.jl | 6 +++--- src/curves.jl | 26 +++++++++++++++----------- src/doublyconnected.jl | 10 +++++----- src/lines.jl | 11 +++++++---- src/paths.jl | 33 +++++++++++++++++++-------------- src/polygons.jl | 31 ++++++++++++++++--------------- src/rays.jl | 11 ++++++----- src/segments.jl | 9 +++++---- src/simplyconnected.jl | 8 ++++---- 10 files changed, 86 insertions(+), 68 deletions(-) diff --git a/src/arcs.jl b/src/arcs.jl index 4ea49f9..b81a74e 100644 --- a/src/arcs.jl +++ b/src/arcs.jl @@ -4,12 +4,15 @@ Each `Arc` type is parameterized according to the common type of its complex input arguments. """ -struct Arc{T} <: AbstractCurve{T} - circle::Circle{T} +struct Arc{T,Z<:AnyComplex{T}} <: AbstractCurve{T} + circle::Circle{T,Z} start::T # specified as positive fraction of 1 ccw rotation from positive real delta::T - Arc{T}(c, s, d) where {T} = new(convert_real_type(T, c), T(s), T(d)) + Arc{T,Z}(c::Circle{T,Z}, s, d) where {T,Z} = new{T,Z}(c, T(s), T(d)) end +# Determine Z from the (possibly converted) circle. +Arc{T}(c, s, d) where {T} = _arc(convert_real_type(T, c), T(s), T(d)) +_arc(c::Circle{T,Z}, s, d) where {T,Z} = Arc{T,Z}(c, s, d) # Untyped constructors """ diff --git a/src/circles.jl b/src/circles.jl index db2d752..d275d15 100644 --- a/src/circles.jl +++ b/src/circles.jl @@ -4,14 +4,14 @@ Each `Circle` type is parameterized according to the common type of its complex input arguments. """ -struct Circle{T} <: AbstractClosedCurve{T} - center::AnyComplex{T} +struct Circle{T,Z<:AnyComplex{T}} <: AbstractClosedCurve{T} + center::Z radius::T ccw::Bool function Circle{T}(z, r, ccw=true) where {T} Tz = complex(convert_real_type(T, z)) Tr = convert(T, r) - return new(Tz, Tr, ccw) + return new{T,typeof(Tz)}(Tz, Tr, ccw) end end """ diff --git a/src/curves.jl b/src/curves.jl index c58292a..ce52d9c 100644 --- a/src/curves.jl +++ b/src/curves.jl @@ -52,9 +52,10 @@ isoutside(C::AbstractClosedCurve) = z -> isoutside(z, C) """ (type) Smooth curve defined by an explicit function of a real paramerter in [0,1]. """ -struct Curve{T} <: AbstractCurve{T} - point::Function - tangent::Function +struct Curve{T,F,G} <: AbstractCurve{T} + point::F + tangent::G + Curve{T}(p, q) where {T} = new{T,typeof(p),typeof(q)}(p, q) end """ @@ -167,14 +168,17 @@ end """ (type) Smooth closed curve defined by an explicit function of a real paramerter in [0,1]. """ -struct ClosedCurve{T} <: AbstractClosedCurve{T} - curve::Curve{T} - function ClosedCurve{T}(c::Curve; tol=tolerance(T)) where T<:AbstractFloat - @assert isapprox(point(c, T(0)), point(c, T(1)); rtol=tol, atol=tol) "Curve does not close" - new(convert_real_type(T, c)) - end - ClosedCurve{T}(f::Function, args...) where T = ClosedCurve{T}(Curve{T}(f, args...)) +struct ClosedCurve{T,F,G} <: AbstractClosedCurve{T} + curve::Curve{T,F,G} + ClosedCurve{T,F,G}(c::Curve{T,F,G}) where {T,F,G} = new{T,F,G}(c) end +function ClosedCurve{T}(c::Curve; tol=tolerance(T)) where T<:AbstractFloat + @assert isapprox(point(c, T(0)), point(c, T(1)); rtol=tol, atol=tol) "Curve does not close" + return _closedcurve(convert_real_type(T, c)) +end +# Function barrier to capture the concrete point/tangent types. +_closedcurve(c::Curve{T,F,G}) where {T,F,G} = ClosedCurve{T,F,G}(c) +ClosedCurve{T}(f::Function, args...) where T = ClosedCurve{T}(Curve{T}(f, args...)) """ ClosedCurve(f; tol=) @@ -204,7 +208,7 @@ Base.:-(C::ClosedCurve) = ClosedCurve(-C.curve) Base.:*(C::ClosedCurve, z::Number) = ClosedCurve(C.curve * z) Base.inv(C::ClosedCurve) = ClosedCurve(inv(C.curve)) -convert_real_type(T::Type{<:Real}, C::ClosedCurve{S}) where S = ClosedCurve{T}(C.point, C.tangent) +convert_real_type(T::Type{<:Real}, C::ClosedCurve{S}) where S = ClosedCurve{T}(convert_real_type(T, C.curve)) Base.promote_rule(::Type{<:ClosedCurve{T}}, ::Type{<:ClosedCurve{S}}) where {T,S} = ClosedCurve{promote_type(T,S)} include("lines.jl") diff --git a/src/doublyconnected.jl b/src/doublyconnected.jl index 51a0ca9..c74f1ed 100644 --- a/src/doublyconnected.jl +++ b/src/doublyconnected.jl @@ -64,10 +64,10 @@ end Region between two concentric circles. """ -struct Annulus{T} <: AbstractDoublyConnectedRegion{T} - outer::Circle{T} - inner::Circle{T} - function Annulus{T}(outer::Circle{T}, inner::Circle{T}) where T +struct Annulus{T,Z<:AnyComplex{T}} <: AbstractDoublyConnectedRegion{T} + outer::Circle{T,Z} + inner::Circle{T,Z} + function Annulus{T}(outer::Circle{T,Z}, inner::Circle{T,Z}) where {T,Z} @assert(outer.center ≈ inner.center) if isinside(Inf, outer) outer = reverse(outer) @@ -75,7 +75,7 @@ struct Annulus{T} <: AbstractDoublyConnectedRegion{T} if isoutside(Inf, inner) inner = reverse(inner) end - new(outer, inner) + new{T,Z}(outer, inner) end end diff --git a/src/lines.jl b/src/lines.jl index ff62e67..60b0f2f 100644 --- a/src/lines.jl +++ b/src/lines.jl @@ -5,10 +5,13 @@ Each `Line` type is parameterized according to the common type of its complex input arguments. """ -struct Line{T} <: AbstractClosedCurve{T} - base::Union{T,AnyComplex{T}} - direction::Union{T,AnyComplex{T}} - Line{T}(a, b) where {T} = new(convert_real_type(T, a), sign(convert_real_type(T, b - a))) +struct Line{T,Z<:Union{T,AnyComplex{T}}} <: AbstractClosedCurve{T} + base::Z + direction::Z + function Line{T}(a, b) where {T} + base, dir = promote(convert_real_type(T, a), sign(convert_real_type(T, b - a))) + new{T,typeof(base)}(base, dir) + end end """ Line(a, b) diff --git a/src/paths.jl b/src/paths.jl index 9e7fe44..b53fa3f 100644 --- a/src/paths.jl +++ b/src/paths.jl @@ -122,25 +122,30 @@ function angles(P::AbstractPath{T}) where T return θ end -Base.conj(p::AbstractPath) = typeof(p)(conj.(curves(p))) +# Reconstruct a path of the same kind from a new set of curves. Uses only the +# real-type parameter so the curve-vector type parameter is recomputed (the new +# curves may differ in type from the originals). +rebuild(p::AbstractPath{T}) where {T} = (typeof(p).name.wrapper){T} -Base.reverse(p::AbstractPath) = typeof(p)(reverse(reverse.(curves(p)))) +Base.conj(p::AbstractPath) = rebuild(p)(conj.(curves(p))) + +Base.reverse(p::AbstractPath) = rebuild(p)(reverse(reverse.(curves(p)))) function Base.:+(p::AbstractPath{T}, z::Number) where T - return typeof(p)([c + convert_real_type(T, z) for c in curves(p)]) + return rebuild(p)([c + convert_real_type(T, z) for c in curves(p)]) end Base.:+(z::Number, p::AbstractPath) = p + z -Base.:-(p::AbstractPath) = typeof(p)([-c for c in curves(p)]) +Base.:-(p::AbstractPath) = rebuild(p)([-c for c in curves(p)]) Base.:-(p::AbstractPath, z::Number) = p + (-z) Base.:-(z::Number, p::AbstractPath) = (-p) + z -Base.:*(p::AbstractPath, z::Number) = typeof(p)([c * z for c in curves(p)]) -Base.:*(z::Number, p::AbstractPath) = typeof(p)([z * c for c in curves(p)]) +Base.:*(p::AbstractPath, z::Number) = rebuild(p)([c * z for c in curves(p)]) +Base.:*(z::Number, p::AbstractPath) = rebuild(p)([z * c for c in curves(p)]) -Base.:/(p::AbstractPath, z::Number) = typeof(p)([c / z for c in curves(p)]) +Base.:/(p::AbstractPath, z::Number) = rebuild(p)([c / z for c in curves(p)]) Base.:/(z::Number, p::AbstractPath) = z * inv(p) -inv(p::AbstractPath) = typeof(p)([inv(c) for c in curves(p)]) +inv(p::AbstractPath) = rebuild(p)([inv(c) for c in curves(p)]) """ isapprox(P1::AbstractPath,R2::AbstractPath) @@ -260,14 +265,14 @@ isoutside(P::AbstractClosedPath) = z -> isoutside(z, P) (type) Path Generic implementation of an `AbstractPath`. """ -struct Path{T} <: AbstractPath{T} - curve::Vector{AbstractCurve{T}} +struct Path{T,V<:AbstractVector{<:AbstractCurve{T}}} <: AbstractPath{T} + curve::V function Path{T}(c::AbstractVector{<:AbstractCurve{T}}; tol::Real=tolerance(T)) where {T} n = length(c) for k = 1:n-1 @assert isapprox(point(c[k], 1), point(c[k+1], 0), rtol=tol, atol=tol) "Curve endpoints do not match for pieces $(k) and $(k+1)" end - new{T}(c) + new{T,typeof(c)}(c) end end """ @@ -302,13 +307,13 @@ end (type) ClosedPath Generic implementation of an `AbstractClosedPath`. """ -struct ClosedPath{T} <: AbstractClosedPath{T} - curve::Vector{AbstractCurve{T}} +struct ClosedPath{T,V<:AbstractVector{<:AbstractCurve{T}}} <: AbstractClosedPath{T} + curve::V function ClosedPath{T}(p::AbstractVector{<:AbstractCurve{T}}; tol=tolerance(T)) where {T} q = Path{T}(p) zi, zf = point(q, 0), point(q, length(q)) @assert isapprox(zi, zf, rtol=tol, atol=tol) || (isinf(zi) && isinf(zf)) "Path endpoints do not match" - new{T}(p) + new{T,typeof(p)}(p) end end """ diff --git a/src/polygons.jl b/src/polygons.jl index d3094f0..06a4214 100644 --- a/src/polygons.jl +++ b/src/polygons.jl @@ -17,7 +17,7 @@ side(p::AbstractCircularPolygon, args...) = curve(p, args...) function reverse(p::AbstractCircularPolygon) @assert sum(isinf.(vertices(p))) < 2 "Reversal is not a propoer polygon" - typeof(p)(reverse(reverse.(sides(p)))) + rebuild(p)(reverse(reverse.(sides(p)))) end function winding(p::AbstractCircularPolygon, z::Number) @@ -69,13 +69,13 @@ end (type) CircularPolygon Type for closed paths consisting entirely of arcs, segments, and rays. """ -struct CircularPolygon{T} <: AbstractCircularPolygon{T} - path::ClosedPath{T} - function CircularPolygon{T}(p::ClosedPath{T}) where {T} +struct CircularPolygon{T,V<:AbstractVector{<:AbstractCurve{T}}} <: AbstractCircularPolygon{T} + path::ClosedPath{T,V} + function CircularPolygon{T}(p::ClosedPath{T,V}) where {T,V} # Continuity and closure have been checked to make a closed path valid = isa.(curves(p), Union{Arc, Segment, Ray}) @assert all(valid) "All sides must be an Arc, Segment, or Ray" - new(p) + new{T,V}(p) end function CircularPolygon{T}(p::AbstractVector{<:AbstractCurve{T}}; kw...) where {T} return CircularPolygon{T}(ClosedPath(p; kw...)) @@ -121,13 +121,13 @@ ispositive(p::CircularPolygon) = winding(1 / p, 0) < 0 (type) Polygon Type for closed paths consisting entirely of segments and rays. """ -struct Polygon{T} <: AbstractPolygon{T} - path::ClosedPath{T} - function Polygon{T}(p::AbstractClosedPath{T}) where {T} +struct Polygon{T,V<:AbstractVector{<:AbstractCurve{T}}} <: AbstractPolygon{T} + path::ClosedPath{T,V} + function Polygon{T}(p::ClosedPath{T,V}) where {T,V} # Assumes continuity and closure have been checked previously valid = isa.(curves(p), Union{Segment{T}, Ray{T}}) @assert all(valid) "All sides must be a Segment or Ray of the same base type" - new(p) + new{T,V}(p) end function Polygon{T}(p::PathLike{T}; kw...) where {T} Polygon{T}(ClosedPath{T}(p; kw...)) @@ -294,20 +294,21 @@ ispositive(p::Polygon) = sum(angles(p) / π .- 1) < 0 ## Special polygon types and constructors -struct Rectangle{T} <: AbstractPolygon{T} - center::AnyComplex{T} +struct Rectangle{T,Z<:AnyComplex{T},V<:AbstractVector{<:AbstractCurve{T}}} <: AbstractPolygon{T} + center::Z radii::SVector{2,T} rotation::T # angle about the center point - polygon::Polygon{T} + polygon::Polygon{T,V} Rectangle(center::T, args...) where {T<:Real} = Rectangle(complex(center), args...) - function Rectangle(center::AnyComplex{S}=0.0, radii::AbstractVector{T}=[-1.0, 1.0], rotation::U=0) where {S<:Real,T<:Real,U<:Real} + function Rectangle(center::AnyComplex{S}=0.0, radii::AbstractVector{T}=[1.0, 1.0], rotation::U=0) where {S<:Real,T<:Real,U<:Real} # rotation is in radians @assert (length(radii) == 2) && all(radii .>= 0) R = promote_type(S, T, U) + c = complex(convert_real_type(R, center)) ρ = cis(convert(R, rotation)) - z = @. center + ρ * complex(R(radii[1]) * [-1, 1, 1, -1], R(radii[2]) * [-1, -1, 1, 1]) + z = @. c + ρ * complex(R(radii[1]) * [-1, 1, 1, -1], R(radii[2]) * [-1, -1, 1, 1]) p = Polygon(z) - new{R}(center, radii, rotation, p) + new{R,typeof(c),typeof(curves(p))}(c, radii, rotation, p) end end diff --git a/src/rays.jl b/src/rays.jl index 8cf4672..e44c37e 100644 --- a/src/rays.jl +++ b/src/rays.jl @@ -3,12 +3,13 @@ (type) Ray{T<:AnyComplex} in the complex plane Each `Ray` type is parameterized according to the common type of its complex input arguments. """ -struct Ray{T} <: AbstractCurve{T} - base::AnyComplex{T} +struct Ray{T,Z<:AnyComplex{T}} <: AbstractCurve{T} + base::Z angle::T reverse::Bool function Ray{T}(a, d, rev=false) where {T} - new(complex(convert_real_type(T, a)), mod2pi(T(d)), rev) + base = complex(convert_real_type(T, a)) + new{T,typeof(base)}(base, mod2pi(T(d)), rev) end end @@ -38,9 +39,9 @@ function point(R::Ray{T}, t::Real) where {T} if t == 0 R.base elseif t == 1 - T(Inf) + typeof(R.base)(Inf) else - R.base + t / (1 - t) * cis(R.angle) + R.base + typeof(R.base)(t / (1 - t) * cis(R.angle)) end end diff --git a/src/segments.jl b/src/segments.jl index c751275..c2b0495 100644 --- a/src/segments.jl +++ b/src/segments.jl @@ -4,12 +4,13 @@ Each `Segment` type is parameterized according to the common type of its complex input arguments. """ -struct Segment{T} <: AbstractCurve{T} - za::Union{T,AnyComplex{T}} - zb::Union{T,AnyComplex{T}} +struct Segment{T,Z<:Union{T,AnyComplex{T}}} <: AbstractCurve{T} + za::Z + zb::Z function Segment{T}(a, b) where {T} @assert isfinite(a) && isfinite(b) - new(convert_real_type(T, a), convert_real_type(T, b)) + za, zb = promote(convert_real_type(T, a), convert_real_type(T, b)) + new{T,typeof(za)}(za, zb) end end diff --git a/src/simplyconnected.jl b/src/simplyconnected.jl index 639b828..42029e1 100644 --- a/src/simplyconnected.jl +++ b/src/simplyconnected.jl @@ -110,7 +110,7 @@ function isapprox( end # disks -const Disk{T} = InteriorSimplyConnectedRegion{T, Circle{T}} +const Disk{T} = InteriorSimplyConnectedRegion{T, <:Circle{T}} """ disk(C::Circle) Construct the disk interior to `C`. @@ -133,7 +133,7 @@ show(io::IO, R::Disk) = print(io, "Disk") # COV_EXCL_STOP # quads -const Quad{T} = InteriorSimplyConnectedRegion{T, Rectangle{T}} +const Quad{T} = InteriorSimplyConnectedRegion{T, <:Rectangle{T}} """ quad(R::Rectangle) Construct the rectangle interior to `R`. @@ -148,7 +148,7 @@ end # COV_EXCL_STOP # half-planes -const Halfplane{T} = SimplyConnectedRegion{T, Line{T}} +const Halfplane{T} = SimplyConnectedRegion{T, <:Line{T}} """ halfplane(L::Line) Construct the half-plane to the left of `L`. @@ -174,4 +174,4 @@ end (type) PolygonalRegion Representation of a simply connected region bounded by a polygon. """ -const PolygonalRegion{T} = SimplyConnectedRegion{T, Polygon{T}} +const PolygonalRegion{T} = SimplyConnectedRegion{T, <:Polygon{T}} From bb62615681cb8a6d79bab33e65a6c0937607baba Mon Sep 17 00:00:00 2001 From: Toby Driscoll Date: Mon, 29 Jun 2026 17:17:40 -0400 Subject: [PATCH 2/3] added tests for type stability --- test/runtests.jl | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/runtests.jl b/test/runtests.jl index e00cca7..2f96b28 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -629,6 +629,28 @@ end @test arclength(Shapes.spiral(3, 1)) ≈ 172.17394523 end +# `point` and the tangent/normal accessors must infer a concrete complex return +# type. `@inferred` throws if inference fails or widens to an abstract type, so +# these guard against regressions in the curve/path type parameterization. +# Note: only homogeneous (uniform-coordinate) paths/polygons are stable; a +# genuinely heterogeneous path has element type `AbstractCurve` and returns `Any` +# by design, so such cases are intentionally excluded here. +@testset "Type stability using $T" for T in (Float64, BigFloat) + o = complex(T(0)) + leaves = (Circle(o, T(1)), Arc(complex(T(1)), 1im + o, -1 + o), + Segment(o, 1im + o), Line(o, 1 + 1im + o), Ray(o, T(1))) + for C in leaves, f in (point, tangent, unittangent, unitnormal) + @test (@inferred f(C, T(3) / 10)) isa Complex{T} + end + poly = Polygon([o, 1 + o, 1 + 1im + o, 1im + o]) + @test (@inferred point(poly, T(7) / 4)) isa Complex{T} + @test (@inferred point(Rectangle(o, T[1, 1]), T(1) / 2)) isa Complex{T} + upath = Path([Segment(o, 1 + o), Segment(1 + o, 1 + 1im + o)]) + @test (@inferred point(upath, T(1) / 2)) isa Complex{T} + cpath = ClosedPath([Segment(o, 1 + o), Segment(1 + o, 1im + o), Segment(1im + o, o)]) + @test (@inferred point(cpath, T(3) / 2)) isa Complex{T} +end + @testset "Utilities" begin @test isempty(CR.realroots(1, 2, 2)) @test length(CR.realroots(1, -4, 4)) == 1 From 190d7e41577995d2e268c03b132cdf0a5549b43b Mon Sep 17 00:00:00 2001 From: Toby Driscoll Date: Mon, 29 Jun 2026 17:40:29 -0400 Subject: [PATCH 3/3] fix pre-existing bug in docs example --- docs/src/numbers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/numbers.md b/docs/src/numbers.md index 2a16507..c1b005f 100644 --- a/docs/src/numbers.md +++ b/docs/src/numbers.md @@ -20,7 +20,7 @@ In addition, the package supports different floating-point types underlying any ```@example examples using DoubleFloats -seg = Segment{DoubleFloat}(-1, 1) +seg = Segment{Double64}(-1, 1) seg(2//3) ```