Detect value-independent branches: const-fold, splat-follow, is_leaf_sig#62
Merged
ChrisRackauckas merged 1 commit intoJun 30, 2026
Conversation
`hasbranching` previously reported many false positives (and, behind splat forwarders, false negatives) on real SciML/MTK ODE right-hand sides. Three value-independent constructs were mis-scored: 1. Compile-time-constant branches. A `GotoIfNot` whose condition inference proves `Core.Const` (e.g. `x isa T` on a concretely-typed field, the `ODEFunction` functor's `if f.f isa AbstractSciMLOperator`, or ML-library device/type-introspection dispatch, SciML#46) can never be taken differently under a tracing AD. Such branches are now skipped. A literal `true`/`false` condition written directly in source is still a genuine branch and is kept. 2. Branches behind splatted calls. `g(args...)` lowers to `Core._apply_iterate(iter, g, groups...)`, burying the real callee as an argument of a `Core` builtin, so the scan dead-ended at the forwarder and missed every branch behind it. This is the SciML/MTK RHS shape (`ODEFunction` -> `GeneratedFunctionWrapper` -> `RuntimeGeneratedFunction` -> `generated_callfunc`, each an `f(args...)` forwarder). The scan now follows the apply through to the real callee, recovering its argument types from the (concrete) splatted tuple types. 3. Argument-type-dependent plumbing. Adds `is_leaf_sig(sig)`, a signature-level counterpart to `is_leaf` consulted during recursion, for exemptions that depend on the argument types rather than just the function value (e.g. selecting a buffer by integer index inside a parameter container, where the branch is on a constant-at-each-call-site index that the recursion widens). All existing tests pass unchanged; regression tests added for each case. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
ChrisRackauckas
added a commit
to SciML/SciMLBase.jl
that referenced
this pull request
Jun 30, 2026
…#1413) `hasbranching` (FunctionProperties.jl) is used by SciMLSensitivity to decide whether a ReverseDiff tape can be compiled for an ODE right-hand side. It is called on `unwrapped_f(prob.f)`, i.e. an `AbstractSciMLFunction`, and analyzes whatever callable it is handed. Handed the SciMLFunction functor, it scans dispatch plumbing rather than the user's RHS: the operator-selection branch (`if f.f isa AbstractSciMLOperator`) and, for an MTK-compiled RHS, splat forwarding into a generated function. The result is that even a branch-free linear RHS reports `true`, and (without the companion FunctionProperties changes) genuine branches behind the forwarder are missed. This weakdep extension defines FunctionProperties.hasbranching(f::AbstractSciMLFunction, args...) = FunctionProperties.hasbranching(f.f, args...) so the analysis targets the wrapped RHS `f.f`. Loaded only when both packages are present; no new hard dependency. Stacked on SciML/FunctionProperties.jl#62 (the value-independent-branch detection that the unwrapped analysis relies on); compat set to that release. Co-authored-by: ChrisRackauckas-Claude <accounts@chrisrackauckas.com>
ChrisRackauckas-Claude
pushed a commit
to ChrisRackauckas-Claude/FunctionProperties.jl
that referenced
this pull request
Jul 3, 2026
Promotes FunctionProperties to a stable 1.0 public API surface -- `hasbranching` and `is_leaf` -- and makes constant-propagation-aware analysis the single mechanism for suppressing value-independent branches. Analysis change (follow-up to SciML#62): the type recursion is the source of truth; when it reports a branch inside a call carrying `Core.Const` arguments, the callee is re-inferred with those constants preserved (no optimizer, so no library/structural branches are inlined into view) and, only if that folds the branch, the finding is refuted. This is a strict refinement -- it can only downgrade a reported branch to branch-free -- so it adds no false positives on arbitrary code (broadcast, ComponentArrays), unlike a "run const-prop everywhere" approach. It replaces the `is_leaf_sig` hook, so no per-container override is needed (e.g. split-mode MTK `getindex(::MTKParameters, ::Int)`). The refutation uses `Base.Compiler`/`Core.Compiler` internals whose API differs across versions, so it is functionally gated: a probe folds a constant-decided branch fixture at first use and only activates if the fold works; otherwise, and on any inference failure, the analysis is exactly the plain type recursion. Verified: lts 1.10.11 (gated off), 1.12.6, 1.13.0-rc1 all green; MTK RHS table correct through the `ODEFunction` with no container-specific overrides; docs build clean. Also fixes the docs config: `deploydocs` pointed at MultiScaleArrays.jl, and the docs environment pinned `FunctionProperties = "0.1.2"`. BREAKING (1.0.0): removes the exported `is_leaf_sig` (public in 0.1.7). Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
ChrisRackauckas-Claude
pushed a commit
to ChrisRackauckas-Claude/FunctionProperties.jl
that referenced
this pull request
Jul 3, 2026
Promotes FunctionProperties to a stable 1.0 public API surface -- `hasbranching` and `is_leaf` -- and makes constant-propagation-aware analysis the single mechanism for suppressing value-independent branches. Analysis change (follow-up to SciML#62): the type recursion is the source of truth; when it reports a branch inside a call carrying `Core.Const` arguments, the callee is re-inferred with those constants preserved (no optimizer, so no library/structural branches are inlined into view) and, only if that folds the branch, the finding is refuted. This is a strict refinement -- it can only downgrade a reported branch to branch-free -- so it adds no false positives on arbitrary code (broadcast, ComponentArrays), unlike a "run const-prop everywhere" approach. It replaces the `is_leaf_sig` hook, so no per-container override is needed (e.g. split-mode MTK `getindex(::MTKParameters, ::Int)`). The refutation uses `Base.Compiler`/`Core.Compiler` internals whose API differs across versions, so it is functionally gated: a probe folds a constant-decided branch fixture at first use and only activates if the fold works; otherwise, and on any inference failure, the analysis is exactly the plain type recursion. Verified: lts 1.10.11 (gated off), 1.12.6, 1.13.0-rc1 all green; MTK RHS table correct through the `ODEFunction` with no container-specific overrides; docs build clean. Also fixes the docs config: `deploydocs` pointed at MultiScaleArrays.jl, and the docs environment pinned `FunctionProperties = "0.1.2"`. BREAKING (1.0.0): removes the exported `is_leaf_sig` (public in 0.1.7). Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
hasbranchingreports false positives (and, behind splat forwarders, false negatives) on real SciML/MTK ODE right-hand sides — a purely linearẋ = x − a·xcomes backtrue, needlessly disabling ReverseDiff tape compilation in SciMLSensitivity. Three value-independent constructs were mis-scored; this PR teaches the scan to ignore them, with one general mechanism per case and no per-container overrides.1. Compile-time-constant branches are skipped
A
GotoIfNotwhose condition inference provesCore.Const(anx isa Ttest on a concretely-typed field — theODEFunctionfunctor'sif f.f isa AbstractSciMLOperator— or the device/type-introspection dispatch inside ML library layers, #46) can never be taken differently under a tracing AD. A literaltrue/falsecondition written directly in source is not skipped — that stays a genuine branch.2. Branches behind splatted calls are followed
g(args...)lowers toCore._apply_iterate(iter, g, groups...), burying the real callee as an argument of aCorebuiltin, so the old scan dead-ended at the forwarder. That's the SciML/MTK RHS shape (ODEFunction→GeneratedFunctionWrapper→RuntimeGeneratedFunction→generated_callfunc), so a registeredrelubehind it was missed. The scan now follows the apply.3. Branches a constant argument decides are refuted
Ordinary recursion widens arguments to types, so a branch selecting a buffer by a literal index (split-mode MTK
getindex(::MTKParameters, ::Int)) looks value-dependent even though every real call site folds it. The type recursion stays the source of truth. Only when it reports a branch inside a call carryingCore.Constarguments does the callee get re-inferred with those constants preserved (no optimizer, so no library/structural branches are inlined into view); if that folds the branch, the finding is refuted.This is a strict refinement: it can only downgrade a reported branch to branch-free, never the reverse. So it never introduces a false positive on arbitrary code — verified against broadcast and
ComponentArray-based neural-network RHS, which an earlier "run const-prop everywhere" approach broke (const inference propagates a constant intogetproperty(::ComponentArray, ::Symbol)'s specialized path and surfaces an unfolded index branch). Refuting only found branches sidesteps that entirely.Version robustness (lts / 1 / pre)
The constant refutation uses
Base.Compiler/Core.Compilerinternals whose API differs across versions (theInferenceStateconstruction and inferred-source location already differ between 1.12 and 1.13). It is functionally gated: a probe runs a constant-decided-branch fixture through the const inference at first use and only activates the refutation if the fold actually works; otherwise the analysis is exactly the plain type recursion. Any inference failure is caught and leaves the branch reported (conservative).Verified by running the suite on each channel:
End to end
The MTK RHS table through the
ODEFunction(linear/power branch-free,relubranchy, both split modes) is correct with no container-specific overrides — the constant refutation handles the split-modeMTKParameterscase generically. (Requires the companion unwrap sohasbranchinglooks through the SciMLFunction wrapper: SciML/SciMLBase.jl#1413.)Tests
Existing tests pass unchanged; added regression tests for each mechanism (const
isaskipped while real inner branch kept; splat-forwarded branch detected; constant-index branch refuted where the compiler cooperates, genuine and dynamic-index branches always kept). Runic clean.Semver
Additive / false-positive fix, non-breaking:
0.1.6→0.1.7.