Skip to content

fix: prevent toexpr from rewriting Const values via st.rewrites#870

Merged
AayushSabharwal merged 1 commit intoJuliaSymbolics:masterfrom
ChrisRackauckas-Claude:fix/cse-const-rewrite-aliasing
Feb 28, 2026
Merged

fix: prevent toexpr from rewriting Const values via st.rewrites#870
AayushSabharwal merged 1 commit intoJuliaSymbolics:masterfrom
ChrisRackauckas-Claude:fix/cse-const-rewrite-aliasing

Conversation

@ChrisRackauckas-Claude
Copy link
Contributor

Summary

  • Fixes a regression in v4.18.4 where toexpr(O::BasicSymbolic{T}, st) incorrectly rewrites Const values through st.rewrites, causing hash-consed constants to be aliased with function argument references
  • The old code had a guard (issym(O) || iscall(O)) in substitute_name that prevented constants from being rewritten; the v4.18.4 refactor dropped this guard
  • Adds regression test that verifies constants in expression bodies are not aliased with constant-valued function arguments

Root Cause

In v4.18.4, PR #862 refactored toexpr for performance. The new toexpr(O::BasicSymbolic{T}, st) checks haskey(st.rewrites, O) for all BasicSymbolic types, including Const. The old code routed through substitute_name which had a guard:

function substitute_name(O, st)
    if (issym(O) || iscall(O)) && haskey(st.rewrites, O)  # <-- guard prevents Const lookup
        st.rewrites[O]
    else
        O
    end
end

When build_function is called with argument arrays containing constant values (e.g., zeros from erased cache variables in DifferentiationInterface.jl), handle_let_pair! adds these constants to st.rewrites:

st.rewrites[Const(0)] = :(ˍ₋arg2[12])

Due to hash consing, all Const(0) values are the same object. So when toexpr encounters a literal 0 in the expression body (e.g., off-diagonal entries of a sparse Jacobian), it finds the rewrite and replaces it with ˍ₋arg2[12]. At runtime, arg2 contains actual (non-zero) data, producing incorrect results.

Impact

This broke DifferentiationInterface.jl's Symbolics backend when computing Jacobians with cache contexts:

Test plan

  • SymbolicUtils test suite passes (16876 pass, 3 pre-existing broken)
  • Symbolics.jl test suite passes (pre-existing PyCall segfault only)
  • DifferentiationInterface cachified Jacobian tests pass (620/620)
  • Added regression test in test/code.jl that reproduces the exact scenario

🤖 Generated with Claude Code

In v4.18.4, the refactored `toexpr(O::BasicSymbolic{T}, st)` function
started checking `st.rewrites` for all BasicSymbolic types including
Const values. The old code had a guard `(issym(O) || iscall(O))` in
`substitute_name` that prevented constants from being rewritten.

This caused a bug where hash-consed constant values (e.g., Const(0))
that appeared both in an expression body AND in function argument
arrays (via DestructuredArgs with create_bindings=false) would be
incorrectly aliased. The constant 0 in the expression would be
replaced with a reference to the argument position (e.g., arg2[12]),
producing wrong results at runtime when that argument had non-zero
values.

This manifested as incorrect jacobian computations in
DifferentiationInterface.jl when using Symbolics.jl's build_function
with cache contexts (where cache variables are erased to zero before
building the function).

Fixes JuliaSymbolics/Symbolics.jl#1811

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
@github-actions
Copy link
Contributor

Benchmark Results (Julia vlts)

Time benchmarks
master c99c312... master / c99c312...
arithmetic/2-arg mul 13.4 ± 0.39 μs 13.6 ± 0.34 μs 0.979 ± 0.038
arithmetic/addition 0.0802 ± 0.00091 ms 0.0782 ± 0.001 ms 1.02 ± 0.018
arithmetic/division 27.5 ± 0.74 μs 27 ± 0.69 μs 1.02 ± 0.038
arithmetic/multiplication 0.0616 ± 0.0017 ms 0.0617 ± 0.0015 ms 0.999 ± 0.036
overhead/acrule/a+2 2.35 ± 0.06 μs 2.47 ± 0.05 μs 0.952 ± 0.031
overhead/acrule/a+2+b 0.07 ± 0 μs 0.07 ± 0 μs 1 ± 0
overhead/acrule/a+b 4.18 ± 0.09 μs 4.37 ± 0.081 μs 0.957 ± 0.027
overhead/acrule/noop:Int 0.05 ± 0.01 μs 0.049 ± 0.01 μs 1.02 ± 0.29
overhead/acrule/noop:Sym 0.06 ± 0.01 μs 0.05 ± 0.01 μs 1.2 ± 0.31
overhead/get_degrees/large_poly 0.08 ± 0.01 μs 0.08 ± 0.001 μs 1 ± 0.13
overhead/rule/noop:Int 0.061 ± 0.01 μs 0.061 ± 0.01 μs 1 ± 0.23
overhead/rule/noop:Sym 0.08 ± 0.01 μs 0.061 ± 0.01 μs 1.31 ± 0.27
overhead/rule/noop:Term 0.08 ± 0.001 μs 0.06 ± 0.01 μs 1.33 ± 0.22
overhead/ruleset/noop:Int 30 ± 0 ns 30 ± 0 ns 1 ± 0
overhead/ruleset/noop:Sym 0.321 ± 0.01 μs 0.301 ± 0.011 μs 1.07 ± 0.051
overhead/ruleset/noop:Term 1.23 ± 0.02 μs 1.2 ± 0.011 μs 1.02 ± 0.019
overhead/simplify/noop:Int 30 ± 0 ns 30 ± 0 ns 1 ± 0
overhead/simplify/noop:Sym 30 ± 10 ns 30 ± 10 ns 1 ± 0.47
overhead/simplify/noop:Term 29 ± 0.85 μs 29.6 ± 0.75 μs 0.981 ± 0.038
overhead/simplify/randterm (+, *):serial 0.234 ± 0.0037 s 0.235 ± 0.0024 s 0.997 ± 0.019
overhead/simplify/randterm (+, *):thread 0.261 ± 0.011 s 0.266 ± 0.019 s 0.982 ± 0.083
overhead/simplify/randterm (/, *):serial 0.0847 ± 0.0018 ms 0.0849 ± 0.0014 ms 0.998 ± 0.027
overhead/simplify/randterm (/, *):thread 0.0883 ± 0.002 ms 0.0883 ± 0.0016 ms 1 ± 0.03
overhead/substitute/a 0.0415 ± 0.001 ms 0.0409 ± 0.0008 ms 1.01 ± 0.032
overhead/substitute/a,b 0.0499 ± 0.0012 ms 0.049 ± 0.00093 ms 1.02 ± 0.031
overhead/substitute/a,b,c 0.045 ± 0.001 ms 0.0436 ± 0.00084 ms 1.03 ± 0.03
polyform/easy_iszero 23.1 ± 0.44 μs 22.9 ± 0.44 μs 1.01 ± 0.027
polyform/isone 1.07 ± 0.027 ms 1.06 ± 0.021 ms 1.02 ± 0.032
polyform/isone:noop 0.08 ± 0.01 μs 0.09 ± 0.01 μs 0.889 ± 0.15
polyform/iszero 0.917 ± 0.021 ms 0.903 ± 0.019 ms 1.02 ± 0.032
polyform/iszero:noop 0.09 ± 0 μs 0.09 ± 0 μs 1 ± 0
polyform/simplify_fractions 1.16 ± 0.025 ms 1.16 ± 0.021 ms 1.01 ± 0.029
printing/large_poly 0.207 ± 0.0039 s 0.211 ± 0.003 s 0.982 ± 0.023
time_to_load 1.25 ± 0.01 s 1.27 ± 0.0071 s 0.983 ± 0.0097
Memory benchmarks
master c99c312... master / c99c312...
arithmetic/2-arg mul 0.078 k allocs: 2.72 kB 0.078 k allocs: 2.72 kB 1
arithmetic/addition 0.438 k allocs: 16 kB 0.438 k allocs: 16 kB 1
arithmetic/division 0.142 k allocs: 5.47 kB 0.142 k allocs: 5.47 kB 1
arithmetic/multiplication 0.356 k allocs: 11.7 kB 0.356 k allocs: 11.7 kB 1
overhead/acrule/a+2 0.034 k allocs: 1.25 kB 0.034 k allocs: 1.25 kB 1
overhead/acrule/a+2+b 0 allocs: 0 B 0 allocs: 0 B
overhead/acrule/a+b 0.047 k allocs: 1.8 kB 0.047 k allocs: 1.8 kB 1
overhead/acrule/noop:Int 0 allocs: 0 B 0 allocs: 0 B
overhead/acrule/noop:Sym 0 allocs: 0 B 0 allocs: 0 B
overhead/get_degrees/large_poly 2 allocs: 32 B 2 allocs: 32 B 1
overhead/rule/noop:Int 2 allocs: 0.0625 kB 2 allocs: 0.0625 kB 1
overhead/rule/noop:Sym 2 allocs: 0.0625 kB 2 allocs: 0.0625 kB 1
overhead/rule/noop:Term 2 allocs: 0.0625 kB 2 allocs: 0.0625 kB 1
overhead/ruleset/noop:Int 0 allocs: 0 B 0 allocs: 0 B
overhead/ruleset/noop:Sym 3 allocs: 0.109 kB 3 allocs: 0.109 kB 1
overhead/ruleset/noop:Term 12 allocs: 0.391 kB 12 allocs: 0.391 kB 1
overhead/simplify/noop:Int 0 allocs: 0 B 0 allocs: 0 B
overhead/simplify/noop:Sym 0 allocs: 0 B 0 allocs: 0 B
overhead/simplify/noop:Term 0.298 k allocs: 11.6 kB 0.298 k allocs: 11.6 kB 1
overhead/simplify/randterm (+, *):serial 2.31 M allocs: 0.0883 GB 2.31 M allocs: 0.0883 GB 1
overhead/simplify/randterm (+, *):thread 2.35 M allocs: 0.246 GB 2.35 M allocs: 0.246 GB 1
overhead/simplify/randterm (/, *):serial 0.844 k allocs: 30.4 kB 0.844 k allocs: 30.4 kB 1
overhead/simplify/randterm (/, *):thread 0.879 k allocs: 31.5 kB 0.879 k allocs: 31.5 kB 1
overhead/substitute/a 0.22 k allocs: 8.42 kB 0.22 k allocs: 8.42 kB 1
overhead/substitute/a,b 0.267 k allocs: 10.1 kB 0.267 k allocs: 10.1 kB 1
overhead/substitute/a,b,c 0.238 k allocs: 8.62 kB 0.238 k allocs: 8.62 kB 1
polyform/easy_iszero 0.133 k allocs: 4.58 kB 0.133 k allocs: 4.58 kB 1
polyform/isone 8.34 k allocs: 0.567 MB 8.34 k allocs: 0.567 MB 1
polyform/isone:noop 1 allocs: 16 B 1 allocs: 16 B 1
polyform/iszero 6.87 k allocs: 0.467 MB 6.87 k allocs: 0.467 MB 1
polyform/iszero:noop 1 allocs: 16 B 1 allocs: 16 B 1
polyform/simplify_fractions 8.82 k allocs: 0.59 MB 8.82 k allocs: 0.59 MB 1
printing/large_poly 1.86 M allocs: 0.082 GB 1.86 M allocs: 0.082 GB 1
time_to_load 0.153 k allocs: 14.5 kB 0.153 k allocs: 14.5 kB 1

@github-actions
Copy link
Contributor

Benchmark Results (Julia v1)

Time benchmarks
master c99c312... master / c99c312...
arithmetic/2-arg mul 10.6 ± 0.24 μs 10.8 ± 0.29 μs 0.981 ± 0.035
arithmetic/addition 0.0683 ± 0.00088 ms 0.0687 ± 0.00082 ms 0.994 ± 0.017
arithmetic/division 24.6 ± 0.64 μs 24.4 ± 0.57 μs 1.01 ± 0.035
arithmetic/multiplication 0.0504 ± 0.0017 ms 0.0508 ± 0.0013 ms 0.991 ± 0.041
overhead/acrule/a+2 2.2 ± 0.06 μs 2.31 ± 0.09 μs 0.956 ± 0.046
overhead/acrule/a+2+b 0.08 ± 0.009 μs 0.08 ± 0.01 μs 1 ± 0.17
overhead/acrule/a+b 3.82 ± 0.09 μs 3.96 ± 0.12 μs 0.964 ± 0.037
overhead/acrule/noop:Int 30 ± 0 ns 30 ± 0 ns 1 ± 0
overhead/acrule/noop:Sym 0.07 ± 0.001 μs 0.061 ± 0.01 μs 1.15 ± 0.19
overhead/get_degrees/large_poly 0.09 ± 0 μs 0.09 ± 0 μs 1 ± 0
overhead/rule/noop:Int 0.07 ± 0.01 μs 0.07 ± 0.01 μs 1 ± 0.2
overhead/rule/noop:Sym 0.07 ± 0 μs 0.07 ± 0 μs 1 ± 0
overhead/rule/noop:Term 0.07 ± 0 μs 0.07 ± 0 μs 1 ± 0
overhead/ruleset/noop:Int 30 ± 0 ns 30 ± 0 ns 1 ± 0
overhead/ruleset/noop:Sym 0.311 ± 0.019 μs 0.311 ± 0.01 μs 1 ± 0.069
overhead/ruleset/noop:Term 1.18 ± 0.02 μs 1.25 ± 0.031 μs 0.944 ± 0.028
overhead/simplify/noop:Int 30 ± 0 ns 30 ± 0 ns 1 ± 0
overhead/simplify/noop:Sym 30 ± 10 ns 30 ± 10 ns 1 ± 0.47
overhead/simplify/noop:Term 27.1 ± 0.61 μs 27.1 ± 0.63 μs 1 ± 0.032
overhead/simplify/randterm (+, *):serial 0.192 ± 0.028 s 0.19 ± 0.023 s 1.01 ± 0.19
overhead/simplify/randterm (+, *):thread 0.252 ± 0.078 s 0.264 ± 0.036 s 0.957 ± 0.32
overhead/simplify/randterm (/, *):serial 0.0855 ± 0.0039 ms 0.0863 ± 0.0032 ms 0.992 ± 0.058
overhead/simplify/randterm (/, *):thread 0.0955 ± 0.01 ms 0.0955 ± 0.0099 ms 1 ± 0.15
overhead/substitute/a 0.0344 ± 0.00077 ms 0.0336 ± 0.00071 ms 1.02 ± 0.032
overhead/substitute/a,b 0.0424 ± 0.00092 ms 0.0418 ± 0.00094 ms 1.01 ± 0.032
overhead/substitute/a,b,c 0.0412 ± 0.0012 ms 0.0406 ± 0.00086 ms 1.01 ± 0.036
polyform/easy_iszero 18.7 ± 0.4 μs 18.8 ± 0.4 μs 0.991 ± 0.03
polyform/isone 0.901 ± 0.018 ms 0.915 ± 0.02 ms 0.984 ± 0.029
polyform/isone:noop 0.081 ± 0.01 μs 0.08 ± 0 μs 1.01 ± 0.12
polyform/iszero 0.784 ± 0.018 ms 0.787 ± 0.017 ms 0.996 ± 0.031
polyform/iszero:noop 0.08 ± 0.001 μs 0.09 ± 0.009 μs 0.889 ± 0.09
polyform/simplify_fractions 0.974 ± 0.035 ms 0.983 ± 0.028 ms 0.99 ± 0.046
printing/large_poly 0.195 ± 0.014 s 0.192 ± 0.012 s 1.02 ± 0.099
time_to_load 1.39 ± 0.013 s 1.42 ± 0.023 s 0.977 ± 0.018
Memory benchmarks
master c99c312... master / c99c312...
arithmetic/2-arg mul 0.056 k allocs: 1.78 kB 0.056 k allocs: 1.78 kB 1
arithmetic/addition 0.3 k allocs: 10.3 kB 0.3 k allocs: 10.3 kB 1
arithmetic/division 0.132 k allocs: 4.77 kB 0.132 k allocs: 4.77 kB 1
arithmetic/multiplication 0.252 k allocs: 6.5 kB 0.252 k allocs: 6.5 kB 1
overhead/acrule/a+2 0.034 k allocs: 1.12 kB 0.034 k allocs: 1.12 kB 1
overhead/acrule/a+2+b 0 allocs: 0 B 0 allocs: 0 B
overhead/acrule/a+b 0.046 k allocs: 1.55 kB 0.046 k allocs: 1.55 kB 1
overhead/acrule/noop:Int 0 allocs: 0 B 0 allocs: 0 B
overhead/acrule/noop:Sym 0 allocs: 0 B 0 allocs: 0 B
overhead/get_degrees/large_poly 2 allocs: 32 B 2 allocs: 32 B 1
overhead/rule/noop:Int 2 allocs: 0.0625 kB 2 allocs: 0.0625 kB 1
overhead/rule/noop:Sym 2 allocs: 0.0625 kB 2 allocs: 0.0625 kB 1
overhead/rule/noop:Term 2 allocs: 0.0625 kB 2 allocs: 0.0625 kB 1
overhead/ruleset/noop:Int 0 allocs: 0 B 0 allocs: 0 B
overhead/ruleset/noop:Sym 3 allocs: 0.109 kB 3 allocs: 0.109 kB 1
overhead/ruleset/noop:Term 12 allocs: 0.391 kB 12 allocs: 0.391 kB 1
overhead/simplify/noop:Int 0 allocs: 0 B 0 allocs: 0 B
overhead/simplify/noop:Sym 0 allocs: 0 B 0 allocs: 0 B
overhead/simplify/noop:Term 0.284 k allocs: 10 kB 0.284 k allocs: 10 kB 1
overhead/simplify/randterm (+, *):serial 2.16 M allocs: 0.0754 GB 2.16 M allocs: 0.0754 GB 1
overhead/simplify/randterm (+, *):thread 2.32 M allocs: 0.237 GB 2.32 M allocs: 0.237 GB 1
overhead/simplify/randterm (/, *):serial 0.783 k allocs: 28.2 kB 0.783 k allocs: 28.2 kB 1
overhead/simplify/randterm (/, *):thread 0.918 k allocs: 0.0325 MB 0.918 k allocs: 0.0325 MB 1
overhead/substitute/a 0.172 k allocs: 6.05 kB 0.172 k allocs: 6.05 kB 1
overhead/substitute/a,b 0.223 k allocs: 7.69 kB 0.223 k allocs: 7.69 kB 1
overhead/substitute/a,b,c 0.229 k allocs: 7.81 kB 0.229 k allocs: 7.81 kB 1
polyform/easy_iszero 0.092 k allocs: 2.94 kB 0.092 k allocs: 2.94 kB 1
polyform/isone 10.9 k allocs: 0.579 MB 10.9 k allocs: 0.579 MB 1
polyform/isone:noop 1 allocs: 16 B 1 allocs: 16 B 1
polyform/iszero 8.97 k allocs: 0.48 MB 8.97 k allocs: 0.48 MB 1
polyform/iszero:noop 1 allocs: 16 B 1 allocs: 16 B 1
polyform/simplify_fractions 11.4 k allocs: 0.596 MB 11.4 k allocs: 0.596 MB 1
printing/large_poly 2.15 M allocs: 0.079 GB 2.15 M allocs: 0.079 GB 1
time_to_load 0.145 k allocs: 11 kB 0.145 k allocs: 11 kB 1

@AayushSabharwal AayushSabharwal merged commit c370e77 into JuliaSymbolics:master Feb 28, 2026
14 of 21 checks passed
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.

New version broke DI tests

3 participants