Skip to content

Fix Julia 1.12: replace Cassette with code_typed-based branch detection#55

Merged
ChrisRackauckas merged 2 commits into
SciML:mainfrom
ChrisRackauckas-Claude:fix/julia-1-12-remove-cassette
Jun 13, 2026
Merged

Fix Julia 1.12: replace Cassette with code_typed-based branch detection#55
ChrisRackauckas merged 2 commits into
SciML:mainfrom
ChrisRackauckas-Claude:fix/julia-1-12-remove-cassette

Conversation

@ChrisRackauckas-Claude

Copy link
Copy Markdown
Contributor

Problem

Julia 1.12 changed generated functions to require CodeInfo return values instead of Expr (see changelog). Cassette v0.3.14 does not implement this change, so every call to hasbranching fails on 1.12 with:

As of Julia 1.12, generated functions must return `CodeInfo`. See `Base.generated_body_to_codeinfo`.

This is an upstream Cassette limitation with no imminent fix.

Solution

Remove the Cassette dependency entirely and replace the IR-pass approach with a direct code_typed call:

function hasbranching(f, x...)
    is_leaf(f, x...) && return false
    results = code_typed(f, Tuple{Core.Typeof.(x)...}; optimize = false)
    isempty(results) && return false
    ci = first(results)[1]
    return any(isa(s, Core.GotoIfNot) for s in ci.code)
end

code_typed(...; optimize=false) gives type-inferred but unoptimized IR — exactly the same level Cassette's pass was inspecting. The semantics are preserved:

  • x < 0 ? -x : xGotoIfNot in IR → true
  • ifelse(x < 0, -x, x) → function call, no GotoIfNotfalse
  • true ? 1 : 0GotoIfNot in unoptimized IR → true (constant folding only happens at optimize=true)
  • x1, x2 = x destructuring → GotoIfNot stays inside iterate, invisible at the caller level → false

Breaking change: extension API

The Cassette.overdub specialization pattern is replaced by is_leaf dispatch:

# Old (broken on 1.12):
function FunctionProperties.Cassette.overdub(::FunctionProperties.HasBranchingCtx, ::typeof(f), x...)
    f(x...)
end

# New:
FunctionProperties.is_leaf(::typeof(f)) = true

Changes

  • src/FunctionProperties.jl: 170 lines → 45 lines; no deps
  • Project.toml: drop Cassette and DiffRules deps; bump to v0.1.4
  • test/runtests.jl: update extension example to use is_leaf

All tests pass on Julia 1.10 and 1.12.

Note: Please ignore until reviewed by @ChrisRackauckas.

Julia 1.12 requires generated functions to return `CodeInfo`, not `Expr`
(see `Base.generated_body_to_codeinfo`). Cassette v0.3.14 does not yet
implement this change, so all `overdub` calls fail at runtime on 1.12.

Replace the entire Cassette machinery with a direct `code_typed` call:
- `code_typed(f, argtypes; optimize=false)` gives type-inferred but
  unoptimized IR, which is exactly what the pass was inspecting before.
- Scanning the resulting `CodeInfo` for `GotoIfNot` nodes reproduces
  the original semantics: value-dependent `if`/`?:` produce `GotoIfNot`,
  `ifelse()` does not, and stdlib iteration/boundscheck branches stay
  inside their callees and are invisible at the user-function level.
- The `is_leaf(f, args...) = false` dispatch replaces the Cassette
  `overdub` extension point. Users who previously added an `overdub`
  specialization now add `FunctionProperties.is_leaf(::typeof(f)) = true`.

Remove `Cassette` and `DiffRules` from deps entirely (no deps remain).
Bump version to 0.1.4.

All tests pass on Julia 1.10 and 1.12.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
@ChrisRackauckas-Claude ChrisRackauckas-Claude changed the base branch from master to main June 13, 2026 07:49
Indent the body of the `if GROUP in ("All", "Core")` block, which PR SciML#53
introduced without indentation. Runic v1.7 (run in CI via FormatCheck)
requires it.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
@ChrisRackauckas ChrisRackauckas marked this pull request as ready for review June 13, 2026 08:00
@ChrisRackauckas ChrisRackauckas merged commit 9431084 into SciML:main Jun 13, 2026
10 of 12 checks passed
ChrisRackauckas added a commit that referenced this pull request Jun 13, 2026
PR #55 exported `is_leaf` with a docstring but did not reference it in the
manual, so Documenter's `:missing_docs` check now fails on main. Add
`is_leaf` to the API reference, and replace the outdated "uses Cassette
internally" section with a description of the `code_typed`-based branch
detection.

Verified locally: `julia --project=docs docs/make.jl` builds cleanly.

Co-authored-by: Chris Rackauckas (Claude) <accounts@chrisrackauckas.com>
@topolarity

topolarity commented Jun 24, 2026

Copy link
Copy Markdown

@ChrisRackauckas FYI I took a peek since you mentioned this in the meeting today

It looks like this PR only checks the immediate function and doesn't consider any callees - I assume that's a problem for correctness

@ChrisRackauckas

Copy link
Copy Markdown
Member

Oh good point, since it needs to check all functions that aren't inlined. Okay, that's not too hard though, and can still avoid doing a full cassette approach.

ChrisRackauckas added a commit that referenced this pull request Jun 27, 2026
Wire all six ExplicitImports.jl checks (no_implicit_imports,
no_stale_explicit_imports, all_explicit_imports_via_owners,
all_qualified_accesses_via_owners, all_qualified_accesses_are_public,
all_explicit_imports_are_public) into the QA group through
SciMLTesting's `run_qa(...; ExplicitImports = ExplicitImports,
explicit_imports = true, ei_kwargs = ...)`.

The four standard checks and `no_implicit_imports` pass with no
configuration. The two public-API checks flag two internal `Core`
accesses inherent to `hasbranching` (a compiler-introspection
utility): `Core.GotoIfNot` (the IR node it scans for) and
`Core.Typeof` (used to build the dispatch signature tuple for
`code_typed`). Neither has a public equivalent — `typeof` differs
from `Core.Typeof` on type-valued arguments and `Base.typesof` is
itself non-public — so they are added to a minimal, documented
per-check ignore-list rather than rewritten.

Also fold Aqua and JET into the same `run_qa` call and drop the two
stale `@test_broken false` placeholders (issues from #54): Aqua now
passes with deps_compat enabled (root Project.toml carries compat for
all test extras) and JET typo-mode passes (Cassette was replaced with
`code_typed` in #55), so those placeholders would now error as
unexpected passes.

Test deps: add ExplicitImports (compat 1.15) to test/qa/Project.toml
and bump SciMLTesting floor to 1.4 (run_qa ExplicitImports support).

Verified locally on Julia 1.12 (`GROUP=QA Pkg.test`): QA group 18/18
pass, all six ExplicitImports checks green.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ChrisRackauckas added a commit that referenced this pull request Jun 27, 2026
PR #55 replaced the Cassette-based pass with a single `code_typed(...;
optimize = false)` scan that only inspects the immediate IR of `f`. That made
`hasbranching` miss value-dependent branches that live in a helper the entry
function calls but that does not get inlined — a false negative. For
SciMLSensitivity that is the dangerous direction: a missed branch lets a
ReverseDiff tape be compiled on a branchy function, silently producing wrong
gradients.

Restore the recursive semantics of the old Cassette pass, but on type-inferred
IR instead of a Cassette context:

- Scan the entry function's IR for `GotoIfNot`, then recurse through statically
  resolved calls into the callees' IR.
- Treat `Base`, `Core`, and stdlib methods as leaves (detected via
  `Base.moduleroot` / `Sys.STDLIB`). Their internal branches are structural /
  compile-time, not value-dependent user logic; recursing into matrix multiply,
  broadcasting, or `getindex` bounds checks would reintroduce the false
  positives the old code suppressed with a hand-curated leaf list. This module
  rule subsumes that list.
- `is_leaf` overrides now also stop recursion into the marked callee.
- Unexpanded generated functions return `Method` rather than `CodeInfo` and are
  treated as leaves.

Tests cover a branch behind a `@noinline` helper, a branch-free nested helper,
and an `is_leaf`-opted-out helper, alongside the existing broadcast / matmul /
neural-ODE false-positive cases. Passes on Julia 1.10 and 1.12.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
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.

3 participants