Skip to content

Fix lru_cache type-conflation in dace.symbolic.simplify#2404

Open
Spielopoly wants to merge 4 commits into
spcl:mainfrom
Spielopoly:symbolic_lru_cache_fix
Open

Fix lru_cache type-conflation in dace.symbolic.simplify#2404
Spielopoly wants to merge 4 commits into
spcl:mainfrom
Spielopoly:symbolic_lru_cache_fix

Conversation

@Spielopoly

Copy link
Copy Markdown

Summary

dace.symbolic.simplify is memoized with functools.lru_cache. lru_cache
keys its entries by hash/==, and in Python booleans are integers:

>>> hash(True) == hash(1)
True
>>> True == sympy.Integer(1)
True

As a result the cache cannot tell simplify(True) apart from
simplify(sympy.Integer(1)) — they collapse onto a single entry. Whichever is
evaluated first wins, and the other caller gets the wrong-typed result:

>>> from dace.symbolic import simplify
>>> simplify(True)                 # caches sympy.true under a key equal to 1
True
>>> simplify(sympy.Integer(1))     # cache hit -> returns the *boolean*
True                               # expected: 1  (a sympy.Integer)
>>> type(simplify(sympy.Integer(1)))
<class 'sympy.logic.boolalg.BooleanTrue'>

The same conflation applies to False / Integer(0).

Impact

The corruption is process-global and order dependent: any code path that
feeds a plain Python bool to simplify (for example a comparison like
simplify(s == 1), which evaluates to True/False before reaching
simplify) poisons the cache for every later simplify(sympy.Integer(1)) /
simplify(sympy.Integer(0)).

Downstream, this surfaces as a hard crash in memlet-volume propagation. For a
single-iteration map the propagated volume is sympy.Integer(1), so
propagate_subset (dace/sdfg/propagation.py) calls simplify(Integer(1)),
gets back BooleanTrue, and the Memlet.volume setter rejects it:

TypeError: Property volume must be a literal or symbolic expression,
got: <class 'sympy.logic.boolalg.BooleanTrue'>

Because it depends on evaluation order across the whole process, the failure is
intermittent and hard to attribute — an unrelated SDFG breaks long after the
bool was simplified.

Fix

Construct the cache with typed=True so arguments of different types are cached
under distinct entries, even when they hash and compare equal:

-@lru_cache(maxsize=2048)
+@lru_cache(maxsize=2048, typed=True)
 def simplify(expr: SymbolicType) -> SymbolicType:
     return sympy.simplify(expr)

With typed=True, simplify(True) (a bool) and simplify(sympy.Integer(1))
(a sympy.Integer) occupy separate cache slots, so neither can return the
other's result.

Testing

New regression test: tests/simplify_cache_typed_test.py. It covers:

  • simplify(True) then simplify(Integer(1)) (and the reverse) stay correctly
    typed — both directions, for True/1 and False/0.
  • Normal symbolic expressions still simplify and are still served from the cache
    (cache_info().hits).
  • End-to-end: after a bool is passed to simplify, memlet-volume propagation
    over a single-iteration map still succeeds (this reproduced the original
    TypeError verbatim before the fix).

simplify() is memoized with @lru_cache, which keys entries with hash().
In Python, booleans are integers (hash(True) == hash(1), True ==
sympy.Integer(1)), so the cache cannot distinguish simplify(True) from
simplify(sympy.Integer(1)) — they collapse onto one entry. Whichever is
evaluated first wins; the other caller gets a wrong-typed result.

This is process-global and order-dependent: any path feeding a plain
bool to simplify (e.g. a comparison simplify(s == 1))
poisons the cache so later simplify(Integer(1)) returns BooleanTrue.
Downstream this crashes memlet-volume propagation for single-iteration
maps (volume == Integer(1)) with "TypeError: Property volume must be a
literal or symbolic expression, got BooleanTrue".

Fix: Construct the cache with typed=True so arguments of different types get
distinct entries even when they hash and compare equal
Copilot AI review requested due to automatic review settings June 14, 2026 14:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds a regression test suite and fixes a long-standing functools.lru_cache key-collision issue in dace.symbolic.simplify by using typed caching, preventing bool/sympy.Integer cache poisoning.

Changes:

  • Switch dace.symbolic.simplify to @lru_cache(..., typed=True) to separate cache entries by argument type.
  • Add regression tests covering True/1 and False/0 cache collisions.
  • Add an end-to-end repro test ensuring memlet volume propagation is not broken by prior boolean simplification.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
dace/symbolic.py Enables typed LRU caching for simplify to prevent bool/sympy.Integer conflation.
tests/simplify_cache_typed_test.py Adds focused regression + end-to-end tests to prevent cache poisoning regressions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +58 to +61
result = dace.symbolic.simplify(True)
assert isinstance(result, Boolean), \
f"expected a sympy boolean, got {type(result).__name__}: {result!r}"
assert bool(result) is True
Comment on lines +81 to +84
result = dace.symbolic.simplify(False)
assert isinstance(result, Boolean), \
f"expected a sympy boolean, got {type(result).__name__}: {result!r}"
assert bool(result) is False
Comment on lines +46 to +50
result = dace.symbolic.simplify(sympy.Integer(1))
assert isinstance(result, sympy.Integer), \
f"expected sympy.Integer, got {type(result).__name__}: {result!r}"
assert not isinstance(result, Boolean)
assert result == 1
ThrudPrimrose and others added 2 commits June 15, 2026 16:01
Importing fparser's Fortran2008 before Fortran2003 triggers a
Fortran2008 -> Fortran2003 -> label_do_stmt_r816 -> Fortran2008.Loop_Control
back-import that fails on Python 3.14 with recent fparser (0.2.3+):
`ImportError: cannot import name 'Loop_Control' from partially
initialized module 'fparser.two.Fortran2008'`.

Import Fortran2003 first to break the cycle, and relax the requirements
pin from `fparser==0.1.4` to `fparser>0.1.3` so the fixed newer fparser
can be used.
@ThrudPrimrose ThrudPrimrose self-assigned this Jun 19, 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.

3 participants