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
130 changes: 97 additions & 33 deletions src/FunctionWrappersWrappers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,38 @@ abstract type AbstractCacheMode end
"""
NoCache()

No caching — every fallback call goes through dynamic dispatch (`obj[](arg...)`),
incurring 1 allocation per call.
Disable fallback wrapper caching.

Fallback calls use dynamic dispatch through the original function each time.

# Returns
- `NoCache`: A cache mode for `FunctionWrappersWrapper`.
"""
struct NoCache <: AbstractCacheMode end

"""
SingleCache()

Cache a single `FunctionWrapper` for the last-seen argument types. After the first
fallback call, subsequent calls with the same types are zero-allocation. If called with
different types, the cache is replaced (1 alloc on miss). This is the recommended default.
Cache one fallback `FunctionWrapper` for the most recent argument tuple type.

Repeated fallback calls with the same argument tuple type reuse the cached
wrapper. Calls with a different tuple type replace the cache entry.

# Returns
- `SingleCache`: A cache mode for `FunctionWrappersWrapper`.
"""
struct SingleCache <: AbstractCacheMode end

"""
DictCache()

Cache `FunctionWrapper`s in a `Dict` keyed by argument type. Handles multiple
non-isbits types without thrashing. Slightly higher lookup overhead than `SingleCache`.
Cache fallback `FunctionWrapper`s in a dictionary keyed by argument tuple type.

Use this mode when fallback calls are expected for several different
non-isbits argument tuple types.

# Returns
- `DictCache`: A cache mode for `FunctionWrappersWrapper`.
"""
struct DictCache <: AbstractCacheMode end

Expand All @@ -45,24 +58,37 @@ abstract type AbstractFallbackPolicy end
"""
Strict()

Never fall back — throw `NoFunctionWrapperFoundError` if no wrapper matches.
Disable fallback calls.

If no wrapped signature matches a call, `FunctionWrappersWrapper` throws a
`NoFunctionWrapperFoundError`.

# Returns
- `Strict`: A fallback policy for `FunctionWrappersWrapper`.
"""
struct Strict <: AbstractFallbackPolicy end

"""
AllowAll()

Always fall back to the original function when no wrapper matches.
Always call the original function when no wrapped signature matches.

# Returns
- `AllowAll`: A fallback policy for `FunctionWrappersWrapper`.
"""
struct AllowAll <: AbstractFallbackPolicy end

"""
AllowNonIsBits()

Fall back only when argument types contain non-isbits elements (e.g., `BigFloat`,
`SparseConnectivityTracer` types). Throws `NoFunctionWrapperFoundError` for isbits
type mismatches (e.g., `Float32` when `Float64` was expected), which catches bugs.
This is the recommended default.
Call the original function only when a mismatched argument tuple contains
non-isbits element types.

This policy keeps isbits type mismatches strict, while still supporting
non-isbits values such as `BigFloat` and tracer types.

# Returns
- `AllowNonIsBits`: A fallback policy for `FunctionWrappersWrapper`.
"""
struct AllowNonIsBits <: AbstractFallbackPolicy end

Expand Down Expand Up @@ -90,14 +116,20 @@ _make_cache_storage(::DictCache) = DictCacheStorage()
"""
FunctionWrappersWrapper{FW, P, CS}

A wrapper around a tuple of `FunctionWrapper`s that dispatches calls to the
matching wrapper based on argument types. When no wrapper matches, behavior is
controlled by the fallback policy `P` and cache mode `CS`.
A callable wrapper around one or more `FunctionWrappers.FunctionWrapper`s.

Calls dispatch to the wrapped signature matching the argument tuple type. If no
signature matches, fallback behavior is determined by policy `P` and cache
storage `CS`.

# Fields
- `fw`: Tuple of `FunctionWrappers.FunctionWrapper`s.
- `cache_storage`: Storage used by the fallback path.

# Type parameters
- `FW`: Tuple type of `FunctionWrapper`s
- `P`: Fallback policy (`Strict`, `AllowAll`, or `AllowNonIsBits`)
- `CS`: Cache storage type (`NoCacheStorage`, `SingleCacheStorage`, `DictCacheStorage`)
# Type Parameters
- `FW`: Tuple type of the wrapped `FunctionWrapper`s.
- `P`: Fallback policy type, such as `Strict`, `AllowAll`, or `AllowNonIsBits`.
- `CS`: Cache storage type selected from the cache mode.
"""
struct FunctionWrappersWrapper{FW, P, CS}
fw::FW
Expand All @@ -114,15 +146,18 @@ TruncatedStacktraces.@truncate_stacktrace FunctionWrappersWrapper
"""
FunctionWrappersWrapper{FW, P, CS}(f)

Create a `FunctionWrappersWrapper` when the type parameters are specified.
Create a `FunctionWrappersWrapper` with explicit type parameters.

# Arguments
- `f`: The function to wrap
- `f`: Function to wrap.

# Type parameters
- `FW`: Tuple type of `FunctionWrapper`s
- `P`: Fallback policy (`Strict`, `AllowAll`, or `AllowNonIsBits`)
- `CS`: Cache storage type (`NoCacheStorage`, `SingleCacheStorage`, `DictCacheStorage`)
# Type Parameters
- `FW`: Tuple type of the wrapped `FunctionWrapper`s.
- `P`: Fallback policy type, such as `Strict`, `AllowAll`, or `AllowNonIsBits`.
- `CS`: Cache storage type selected from the cache mode.

# Returns
- `FunctionWrappersWrapper{FW, P, CS}`: A callable wrapper around `f`.
"""
function FunctionWrappersWrapper{FW, P, CS}(f) where {K, FW <: NTuple{K, Any}, P, CS}
fw = ntuple(i -> FW.parameters[i](f), Val(K))
Expand All @@ -133,16 +168,27 @@ end
"""
FunctionWrappersWrapper(f, argtypes, rettypes; cache=SingleCache(), policy=AllowNonIsBits())

Create a `FunctionWrappersWrapper` with configurable fallback behavior.
Create a callable wrapper for `f` over the given argument and return types.

# Arguments
- `f`: The function to wrap
- `argtypes`: Tuple of argument type signatures (e.g., `(Tuple{Float64, Float64},)`)
- `rettypes`: Tuple of return types (e.g., `(Float64,)`)
- `f`: Function to wrap.
- `argtypes`: Tuple of argument tuple types, such as `(Tuple{Float64, Float64},)`.
- `rettypes`: Tuple of return types corresponding to `argtypes`, such as `(Float64,)`.

# Keyword Arguments
- `cache`: Fallback cache mode. Defaults to `SingleCache()`.
- `policy`: Fallback policy. Defaults to `AllowNonIsBits()`.

# Returns
- `FunctionWrappersWrapper`: A callable wrapper around `f`.

# Examples
```jldoctest
julia> fw = FunctionWrappersWrapper(+, (Tuple{Float64, Float64},), (Float64,));

# Keywords
- `cache`: Cache mode for fallback path — `NoCache()`, `SingleCache()` (default), or `DictCache()`
- `policy`: Fallback policy — `Strict()`, `AllowAll()`, or `AllowNonIsBits()` (default)
julia> fw(1.0, 2.0)
3.0
```
"""
function FunctionWrappersWrapper(
f::F, argtypes::Tuple{Vararg{Any, K}}, rettypes::Tuple{Vararg{Type, K}};
Expand Down Expand Up @@ -278,13 +324,25 @@ end
unwrap(fww::FunctionWrappersWrapper)

Return the original function that was wrapped.

# Arguments
- `fww`: Wrapper to inspect.

# Returns
- The function captured by `fww`.
"""
unwrap(fww::FunctionWrappersWrapper) = first(fww.fw).obj[]

"""
wrapped_signatures(fww::FunctionWrappersWrapper)

Return a tuple of the argument type signatures that the wrapper can dispatch on.

# Arguments
- `fww`: Wrapper to inspect.

# Returns
- A tuple of argument tuple types stored in `fww`.
"""
function wrapped_signatures(fww::FunctionWrappersWrapper)
return map(fw -> typeof(fw).parameters[2], fww.fw)
Expand All @@ -294,6 +352,12 @@ end
wrapped_return_types(fww::FunctionWrappersWrapper)

Return a tuple of the return types for each wrapped function signature.

# Arguments
- `fww`: Wrapper to inspect.

# Returns
- A tuple of return types corresponding to `wrapped_signatures(fww)`.
"""
function wrapped_return_types(fww::FunctionWrappersWrapper)
return map(fw -> typeof(fw).parameters[1], fww.fw)
Expand Down
57 changes: 57 additions & 0 deletions test/qa/qa.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,60 @@ run_qa(
),
),
)

function public_api_names(mod::Module)
public_names = Set(Symbol.(names(mod; all = false, imported = false)))
if isdefined(Base, :ispublic)
for name in names(mod; all = true, imported = false)
Base.ispublic(mod, name) && push!(public_names, Symbol(name))
end
end
delete!(public_names, nameof(mod))
return sort!(collect(public_names))
end

function has_source_docstring(mod::Module, name::Symbol)
if isdefined(Docs, :hasdoc)
return Docs.hasdoc(mod, name)
end

obj = getproperty(mod, name)
(obj isa Function || obj isa Type) || return false
return !startswith(strip(string(Docs.doc(obj))), "No documentation found.")
end

function docs_include_public_api(mod::Module, public_names)
package_root = pkgdir(mod)
isnothing(package_root) && return false

docs_src = joinpath(package_root, "docs", "src")
isdir(docs_src) || return false

markdown = join(
read(path, String)
for path in readdir(docs_src; join = true) if endswith(path, ".md")
)
module_name = string(nameof(mod))
autodocs_pattern = Regex(
"```@autodocs[\\s\\S]*?Modules\\s*=\\s*\\[[^\\]]*\\b" *
module_name *
"\\b[^\\]]*\\]"
)
occursin(autodocs_pattern, markdown) && return true

docs_blocks = [match.match for match in eachmatch(r"```@docs[\s\S]*?```", markdown)]
return all(public_names) do name
any(block -> occursin(string(name), block), docs_blocks)
end
end

@testset "Public API documentation" begin
public_names = public_api_names(FunctionWrappersWrappers)
undocumented_names = filter(
name -> !has_source_docstring(FunctionWrappersWrappers, name),
public_names
)

@test isempty(undocumented_names)
@test docs_include_public_api(FunctionWrappersWrappers, public_names)
end
Loading