Skip to content

fix: Distinguish unloaded extensions from dispatch errors - #189

Closed
mvanhorn wants to merge 1 commit into
SymbolicML:masterfrom
mvanhorn:fix/68-extension-loaded-errors
Closed

fix: Distinguish unloaded extensions from dispatch errors#189
mvanhorn wants to merge 1 commit into
SymbolicML:masterfrom
mvanhorn:fix/68-extension-loaded-errors

Conversation

@mvanhorn

@mvanhorn mvanhorn commented Aug 5, 2026

Copy link
Copy Markdown

DynamicExpressions currently implements several optional-package entry points as catch-all methods that always report that the corresponding extension is not loaded. Once SymbolicUtils, Zygote, or Bumper is loaded, an unsupported argument combination can still fall through to that catch-all and produce the same message, incorrectly blaming extension loading instead of method dispatch. The issue proposes an extension-presence predicate plus separate internal generics so the package can distinguish these cases without adding runtime overhead, and the maintainer acknowledged the problem. The supplied timeline contains no prior closed attempts or competing open pull requests.

Summary

Add a shared, Val-dispatched extension-presence predicate in src/ExtensionInterface.jl, and make each affected public/core entry point check its extension state before delegating to a separate internal generic. Update the SymbolicUtils, Zygote, and Bumper extension modules to mark themselves loaded and attach their existing implementations to those internal generics, preserving current successful behavior and unloaded-package guidance. When an extension is loaded but no implementation matches, allow normal Julia dispatch to surface a MethodError rather than emitting the false “please load” message.

Verification

  • Before loading optional dependencies, node_to_symbolic, symbolic_to_node, Zygote-backed differentiation, and Bumper-backed evaluation retain their current package-specific guidance.
  • After loading SymbolicUtils, Zygote, and Bumper, each supported entry point still reaches its extension implementation and returns the same result as before.
  • After an extension is loaded, passing an unsupported argument shape to each affected entry point raises a dispatch error and does not claim that the dependency is missing.
  • Loading one optional dependency marks only its own extension as available; absent extensions continue to use their existing helpful error paths.

Fixes #68

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results (Julia v1)

Time benchmarks
master f7ea8b7... master / f7ea8b7...
eval/ComplexF32/evaluation 7.97 ± 0.57 ms 8.02 ± 0.59 ms 0.994 ± 0.1
eval/ComplexF64/evaluation 11.4 ± 0.8 ms 11.4 ± 1 ms 0.995 ± 0.11
eval/Float32/derivative 11 ± 1.7 ms 11.1 ± 2 ms 0.99 ± 0.24
eval/Float32/derivative_turbo 11 ± 1.6 ms 11 ± 1.8 ms 0.995 ± 0.22
eval/Float32/evaluation 2.61 ± 0.29 ms 2.64 ± 0.28 ms 0.99 ± 0.15
eval/Float32/evaluation_bumper 0.635 ± 0.017 ms 0.644 ± 0.02 ms 0.987 ± 0.04
eval/Float32/evaluation_turbo 0.543 ± 0.033 ms 0.559 ± 0.037 ms 0.97 ± 0.086
eval/Float32/evaluation_turbo_bumper 0.634 ± 0.018 ms 0.644 ± 0.018 ms 0.984 ± 0.039
eval/Float64/derivative 14.2 ± 3.8 ms 14.2 ± 3.7 ms 0.996 ± 0.37
eval/Float64/derivative_turbo 14.1 ± 3.9 ms 14.1 ± 3.6 ms 1 ± 0.38
eval/Float64/evaluation 3.09 ± 0.34 ms 3.13 ± 0.39 ms 0.988 ± 0.16
eval/Float64/evaluation_bumper 1.33 ± 0.047 ms 1.34 ± 0.045 ms 0.989 ± 0.048
eval/Float64/evaluation_turbo 1.05 ± 0.082 ms 1.12 ± 0.099 ms 0.943 ± 0.11
eval/Float64/evaluation_turbo_bumper 1.32 ± 0.045 ms 1.34 ± 0.043 ms 0.989 ± 0.046
utils/combine_operators/break_sharing 29.6 ± 2.3 μs 30.7 ± 3.6 μs 0.963 ± 0.14
utils/convert/break_sharing 25.9 ± 6.8 μs 26.7 ± 7 μs 0.969 ± 0.36
utils/convert/preserve_sharing 0.1 ± 0.0065 ms 0.101 ± 0.0059 ms 0.99 ± 0.086
utils/copy/break_sharing 29.7 ± 7.3 μs 28.9 ± 8.1 μs 1.03 ± 0.38
utils/copy/preserve_sharing 0.0998 ± 0.0073 ms 0.102 ± 0.0076 ms 0.978 ± 0.1
utils/count_constant_nodes/break_sharing 9.36 ± 0.63 μs 9.63 ± 0.95 μs 0.971 ± 0.12
utils/count_constant_nodes/preserve_sharing 0.0858 ± 0.0049 ms 0.0849 ± 0.0053 ms 1.01 ± 0.085
utils/count_depth/break_sharing 10.4 ± 0.84 μs 10.1 ± 0.66 μs 1.02 ± 0.11
utils/count_nodes/break_sharing 9.41 ± 0.52 μs 9.58 ± 0.7 μs 0.982 ± 0.09
utils/count_nodes/preserve_sharing 0.086 ± 0.0051 ms 0.0869 ± 0.005 ms 0.989 ± 0.082
utils/get_set_constants!/break_sharing 30.8 ± 6.1 μs 25.2 ± 5.5 μs 1.22 ± 0.36
utils/get_set_constants!/preserve_sharing 0.181 ± 0.011 ms 0.183 ± 0.012 ms 0.986 ± 0.089
utils/get_set_constants_parametric 0.0412 ± 0.0059 ms 0.04 ± 0.0055 ms 1.03 ± 0.2
utils/has_constants/break_sharing 5.11 ± 0.48 μs 4.9 ± 0.44 μs 1.04 ± 0.14
utils/has_operators/break_sharing 2.57 ± 0.3 μs 2.43 ± 0.26 μs 1.06 ± 0.17
utils/hash/break_sharing 20.8 ± 1.1 μs 21.4 ± 1.3 μs 0.973 ± 0.079
utils/hash/preserve_sharing 0.102 ± 0.0067 ms 0.103 ± 0.0058 ms 0.99 ± 0.086
utils/index_constant_nodes/break_sharing 26 ± 4 μs 27.7 ± 6 μs 0.939 ± 0.25
utils/index_constant_nodes/preserve_sharing 0.101 ± 0.0051 ms 0.102 ± 0.0057 ms 0.987 ± 0.074
utils/is_constant/break_sharing 5.39 ± 0.64 μs 5.09 ± 0.56 μs 1.06 ± 0.17
utils/simplify_tree/break_sharing 28.8 ± 3.8 μs 27.1 ± 3.1 μs 1.06 ± 0.19
utils/simplify_tree/preserve_sharing 0.113 ± 0.0051 ms 0.113 ± 0.0054 ms 1 ± 0.066
utils/string_tree/break_sharing 0.464 ± 0.017 ms 0.473 ± 0.021 ms 0.982 ± 0.057
utils/string_tree/preserve_sharing 0.557 ± 0.018 ms 0.57 ± 0.021 ms 0.977 ± 0.047
time_to_load 0.17 ± 0.0022 s 0.167 ± 0.0005 s 1.02 ± 0.013
Memory benchmarks
master f7ea8b7... master / f7ea8b7...
eval/ComplexF32/evaluation 0.966 k allocs: 2.47 MB 0.984 k allocs: 2.52 MB 0.982
eval/ComplexF64/evaluation 0.99 k allocs: 5.04 MB 0.996 k allocs: 5.07 MB 0.994
eval/Float32/derivative 4.63 k allocs: 17.4 MB 4.65 k allocs: 17.5 MB 0.995
eval/Float32/derivative_turbo 4.72 k allocs: 17.8 MB 4.65 k allocs: 17.5 MB 1.01
eval/Float32/evaluation 0.975 k allocs: 1.27 MB 0.978 k allocs: 1.27 MB 0.997
eval/Float32/evaluation_bumper 0.303 k allocs: 0.393 MB 0.303 k allocs: 0.393 MB 1
eval/Float32/evaluation_turbo 0.948 k allocs: 1.23 MB 0.966 k allocs: 1.26 MB 0.981
eval/Float32/evaluation_turbo_bumper 0.303 k allocs: 0.393 MB 0.303 k allocs: 0.393 MB 1
eval/Float64/derivative 4.78 k allocs: 0.035 GB 4.83 k allocs: 0.0353 GB 0.991
eval/Float64/derivative_turbo 4.81 k allocs: 0.0352 GB 4.77 k allocs: 0.0349 GB 1.01
eval/Float64/evaluation 0.996 k allocs: 2.55 MB 0.99 k allocs: 2.53 MB 1.01
eval/Float64/evaluation_bumper 0.303 k allocs: 0.771 MB 0.303 k allocs: 0.771 MB 1
eval/Float64/evaluation_turbo 1 k allocs: 2.57 MB 0.987 k allocs: 2.53 MB 1.02
eval/Float64/evaluation_turbo_bumper 0.303 k allocs: 0.771 MB 0.303 k allocs: 0.771 MB 1
utils/combine_operators/break_sharing 4 allocs: 0.953 kB 4 allocs: 0.953 kB 1
utils/convert/break_sharing 2 k allocs: 0.123 MB 2 k allocs: 0.123 MB 1
utils/convert/preserve_sharing 2.4 k allocs: 0.192 MB 2.4 k allocs: 0.192 MB 1
utils/copy/break_sharing 2 k allocs: 0.123 MB 2 k allocs: 0.123 MB 1
utils/copy/preserve_sharing 2.4 k allocs: 0.192 MB 2.4 k allocs: 0.192 MB 1
utils/count_constant_nodes/break_sharing 4 allocs: 0.953 kB 4 allocs: 0.953 kB 1
utils/count_constant_nodes/preserve_sharing 0.404 k allocs: 0.0696 MB 0.404 k allocs: 0.0696 MB 1
utils/count_depth/break_sharing 4 allocs: 0.953 kB 4 allocs: 0.953 kB 1
utils/count_nodes/break_sharing 4 allocs: 0.953 kB 4 allocs: 0.953 kB 1
utils/count_nodes/preserve_sharing 0.404 k allocs: 0.0696 MB 0.404 k allocs: 0.0696 MB 1
utils/get_set_constants!/break_sharing 0.898 k allocs: 25.2 kB 0.898 k allocs: 25.2 kB 1
utils/get_set_constants!/preserve_sharing 1.7 k allocs: 0.138 MB 1.7 k allocs: 0.138 MB 1
utils/get_set_constants_parametric 1.42 k allocs: 0.0663 MB 1.42 k allocs: 0.0663 MB 1
utils/has_constants/break_sharing 4 allocs: 0.203 kB 4 allocs: 0.203 kB 1
utils/has_operators/break_sharing 4 allocs: 0.203 kB 4 allocs: 0.203 kB 1
utils/hash/break_sharing 0.104 k allocs: 2.52 kB 0.104 k allocs: 2.52 kB 1
utils/hash/preserve_sharing 0.504 k allocs: 0.0711 MB 0.504 k allocs: 0.0711 MB 1
utils/index_constant_nodes/break_sharing 2.1 k allocs: 0.094 MB 2.1 k allocs: 0.094 MB 1
utils/index_constant_nodes/preserve_sharing 2.5 k allocs: 0.163 MB 2.5 k allocs: 0.163 MB 1
utils/is_constant/break_sharing 4 allocs: 0.203 kB 4 allocs: 0.203 kB 1
utils/simplify_tree/break_sharing 0.104 k allocs: 2.52 kB 0.104 k allocs: 2.52 kB 1
utils/simplify_tree/preserve_sharing 0.504 k allocs: 0.0711 MB 0.504 k allocs: 0.0711 MB 1
utils/string_tree/break_sharing 11.9 k allocs: 0.999 MB 11.9 k allocs: 0.999 MB 1
utils/string_tree/preserve_sharing 12.3 k allocs: 1.07 MB 12.3 k allocs: 1.07 MB 1
time_to_load 0.145 k allocs: 11 kB 0.145 k allocs: 11 kB 1

@MilesCranmer

MilesCranmer commented Aug 5, 2026

Copy link
Copy Markdown
Member

@MilesCranmerBot can you make a new PR based on this, but which implements the following fixes?

  • The big docstring for node_to_symbolic moved onto the private _node_to_symbolic in the extension, so ?node_to_symbolic at the REPL will lose its docs. Let's move it to the source definition for the symbol.
  • _is_extension_loaded should really be is_extension_loaded because its imported by other code. (I haven't done this in the past, but trying to be better going forward)
  • I don't love how the consts are now after some of the method definitions. I mean we can probably just put the is_extension_loaded at the bottom of the file, which makes more sense semantically.

Load ponytail skill

@MilesCranmerBot

Copy link
Copy Markdown
Contributor

Done: #190 — based on @mvanhorn's branch with the three fixes:

  • node_to_symbolic docstring moved back to the public entry point in ExtensionInterface.jl (so ?node_to_symbolic keeps working)
  • _is_extension_loadedis_extension_loaded
  • is_extension_loaded(::Val{...}) markers moved to the bottom of each extension module

Also fixed two issues in the new tests that would have failed CI: the varargs dispatch wrappers trip DispatchDoctor, so successful extension-dispatched calls are now wrapped in allow_unstable (matching the file's existing pattern), and the bumper check used the node-call syntax which returns only the result vector, not a (result, ok) tuple. test_initial_errors.jl passes locally.

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.

Consider a different way to error on extension not loaded

3 participants