Skip to content

feat(Utilities): add safe_call for type-inferred conditional fallbacks#746

Closed
haakon-e wants to merge 1 commit into
mainfrom
he/safe-when
Closed

feat(Utilities): add safe_call for type-inferred conditional fallbacks#746
haakon-e wants to merge 1 commit into
mainfrom
he/safe-when

Conversation

@haakon-e

@haakon-e haakon-e commented Jun 24, 2026

Copy link
Copy Markdown
Member

Summary

Add Utilities.safe_call, a higher-order helper for conditional values whose fallback type is inferred from the computation rather than from its inputs:

safe_call(q_ice > ϵ; otherwise = zero) do
    n_ice / q_ice
end

It returns the do block when cond is true and otherwise(FT) when it is false, where FT = Core.Compiler.return_type(f, Tuple{}).

Motivation

The recurring zero(promote_typeof(args...)) idiom has a weakness that safe_call removes:

  • promote_typeof types the fallback from the inputs, and every derivative-carrying argument must be listed by hand; omitting one returns a Union, which is non-concrete when using ForwardDiff on the GPU. safe_call takes the type from the function's range, so it cannot omit an argument.

Notes for review

  • Core.Compiler.return_type is internal and inference-directed: the fallback type is correct only where the computation is type-stable; this is required for the code to run on GPUs anyways. Where inference widens (Union type), this will fail.
  • No call site is converted here, though I have tested it on _regularised_ratio in the same module. With that, it is type-stable and allocation-free through ForwardDiff on CUDA, for the default and overridden otherwise.

@haakon-e

Copy link
Copy Markdown
Member Author

@codecov

codecov Bot commented Jun 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.97%. Comparing base (ddd64f0) to head (78b754c).

Additional details and impacted files
@@                 Coverage Diff                 @@
##           he/ad-compat-2m-p3     #746   +/-   ##
===================================================
  Coverage               92.96%   92.97%           
===================================================
  Files                      56       56           
  Lines                    2859     2862    +3     
===================================================
+ Hits                     2658     2661    +3     
  Misses                    201      201           
Components Coverage Δ
src 93.78% <100.00%> (+<0.01%) ⬆️
ext 69.47% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@haakon-e
haakon-e force-pushed the he/ad-compat-2m-p3 branch from 6436c12 to 710d1ce Compare June 25, 2026 18:01
@haakon-e
haakon-e force-pushed the he/ad-compat-2m-p3 branch from 710d1ce to ee98c2d Compare June 26, 2026 00:38
@haakon-e haakon-e changed the title feat(Utilities): add safe_when for type-inferred conditional fallbacks feat(Utilities): add safe_call for type-inferred conditional fallbacks Jun 26, 2026
@haakon-e
haakon-e force-pushed the he/ad-compat-2m-p3 branch from ee98c2d to 98e82b8 Compare June 26, 2026 01:22
@haakon-e
haakon-e force-pushed the he/ad-compat-2m-p3 branch from 98e82b8 to 6878dd5 Compare June 26, 2026 01:31
@haakon-e
haakon-e force-pushed the he/safe-when branch 2 times, most recently from 8e79cdd to 435f982 Compare June 26, 2026 01:38
@haakon-e
haakon-e force-pushed the he/ad-compat-2m-p3 branch 3 times, most recently from 11eb605 to ddd64f0 Compare July 1, 2026 06:24
`safe_call(f, cond; otherwise = zero)` returns `f()` when `cond` holds and
`otherwise(FT)` when it does not, with `FT = Core.Compiler.return_type(f, Tuple{})`
— the element type the computation itself produces. This supersedes the
`zero(promote_typeof(args...))` plus `ifelse(cond, value, zero)` pattern: the
fallback type comes from the function's range rather than an enumeration of its
domain, so it cannot miss an argument, and the closure is evaluated only when
`cond` holds, which avoids running it on inputs outside its domain.

Condition on values (`x > ϵ`) rather than on `iszero` of a differentiated
argument: a value threshold fires at the degenerate point and gives a clean
zero-derivative fallback, whereas `iszero(Dual(0, ∂))` is false and would
evaluate the closure.
Base automatically changed from he/ad-compat-2m-p3 to main July 1, 2026 22:48
@haakon-e

haakon-e commented Jul 9, 2026

Copy link
Copy Markdown
Member Author

Closing in favor of branch-free patterns: rate functions are now evaluated unconditionally, with ifelse selecting the result, rather than short-circuiting on a condition (see e.g. #757). This removes the use case for safe_call.

@haakon-e haakon-e closed this Jul 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant