|
| 1 | +# Sympy to ixsimpl Migration Plan |
| 2 | + |
| 3 | +Status: **In Progress** (Phase 1 partially complete) |
| 4 | + |
| 5 | +## Current State |
| 6 | + |
| 7 | +**48 files** under `wave_lang/` import sympy directly. ixsimpl currently serves |
| 8 | +as the simplification backend via roundtrip conversion in `symbol_utils.py`. |
| 9 | +Everything else -- symbol creation, expression construction, type checking, |
| 10 | +substitution, codegen, Piecewise logic, numeric probing -- still goes through |
| 11 | +sympy despite ixsimpl having native support for most of these operations. |
| 12 | + |
| 13 | +The integration point is `wave_lang/kernel/wave/utils/symbol_utils.py`: |
| 14 | +- `simplify(expr)` tries ixsimpl first, falls back to sympy expand/cancel loop. |
| 15 | +- `ixs_simplify(expr)` does the roundtrip, returns original on conversion error. |
| 16 | +- All 12 call sites go through these two functions. No code calls ixsimpl directly. |
| 17 | + |
| 18 | +## ixsimpl API Surface |
| 19 | + |
| 20 | +Source: `ixsimpl/_ixsimpl.pyi`, `ixsimpl/__init__.py`, `ixsimpl/sympy_conv.py`. |
| 21 | + |
| 22 | +### Expression types (tag-based dispatch) |
| 23 | +`INT`, `RAT`, `SYM`, `ADD`, `MUL`, `FLOOR`, `CEIL`, `MOD`, `PIECEWISE`, `MAX`, |
| 24 | +`MIN`, `XOR`, `CMP`, `AND`, `OR`, `NOT`, `TRUE`, `FALSE` |
| 25 | + |
| 26 | +### Expr methods |
| 27 | +| Method | Purpose | |
| 28 | +|--------|---------| |
| 29 | +| `.tag` | Node type discriminator (replaces `isinstance(expr, sympy.X)`) | |
| 30 | +| `.nchildren` / `.children` / `.child(i)` | Generic structural access (replaces `.args`) | |
| 31 | +| `.sym_name` | Symbol name (replaces `str(sym)`) | |
| 32 | +| `.rat_num` / `.rat_den` | Rational numerator/denominator (replaces `.p`/`.q`) | |
| 33 | +| `.add_coeff` / `.add_nterms` / `.add_term(i)` / `.add_term_coeff(i)` | Add decomposition | |
| 34 | +| `.mul_coeff` / `.mul_nfactors` / `.mul_factor_base(i)` / `.mul_factor_exp(i)` | Mul decomposition | |
| 35 | +| `.pw_ncases` / `.pw_value(i)` / `.pw_cond(i)` | Piecewise branch access | |
| 36 | +| `.cmp_op` | Comparison operator kind | |
| 37 | +| `.subs(target, replacement)` / `.subs(mapping)` | Substitution (replaces `.subs()`/`.xreplace()`) | |
| 38 | +| `.simplify(assumptions=)` | Simplification with assumption constraints | |
| 39 | +| `.expand()` | Expression expansion | |
| 40 | +| `.to_c()` | C code generation | |
| 41 | +| `.free_symbols` | Free symbol set (Python-level, on `Expr` subclass) | |
| 42 | +| `.is_error` / `.is_parse_error` / `.is_domain_error` | Error detection | |
| 43 | +| Arithmetic operators | `+`, `-`, `*`, `/`, `>=`, `>`, `<=`, `<`, `==`, `!=` | |
| 44 | + |
| 45 | +### Context methods |
| 46 | +| Method | Purpose | |
| 47 | +|--------|---------| |
| 48 | +| `ctx.sym(name)` | Create symbol | |
| 49 | +| `ctx.int_(val)` / `ctx.rat(p, q)` | Create constants | |
| 50 | +| `ctx.true_()` / `ctx.false_()` | Boolean constants | |
| 51 | +| `ctx.eq(a, b)` / `ctx.ne(a, b)` | Equality/inequality nodes | |
| 52 | +| `ctx.parse(input)` | Parse expression from string | |
| 53 | +| `ctx.simplify_batch(exprs, assumptions=)` | Batch simplification | |
| 54 | +| `ctx.errors` / `ctx.clear_errors()` | Error reporting | |
| 55 | +| `ctx.stats()` / `ctx.stats_reset()` | Performance stats | |
| 56 | + |
| 57 | +### Free functions |
| 58 | +`floor`, `ceil`, `mod`, `max_`, `min_`, `xor_`, `and_`, `or_`, `not_`, `pw`, |
| 59 | +`same_node` |
| 60 | + |
| 61 | +### Sympy conversion layer (`ixsimpl.sympy_conv`) |
| 62 | +`from_sympy(ctx, expr)`, `to_sympy(expr, symbols=, xor_fn=)`, |
| 63 | +`extract_assumptions(ctx, expr)` |
| 64 | + |
| 65 | +Handles: all arithmetic, floor/ceil/mod, min/max, xor, piecewise, all |
| 66 | +comparisons, and/or/not, true/false. Custom `xor` Function subclass matched by |
| 67 | +name. `Abs` is the only notable missing conversion (emulable via piecewise). |
| 68 | + |
| 69 | +## Sympy Usage Categories |
| 70 | + |
| 71 | +### Already migrated to ixsimpl |
| 72 | + |
| 73 | +| Category | Notes | |
| 74 | +|----------|-------| |
| 75 | +| **Simplification** | `simplify()` and `ixs_simplify()` delegate to ixsimpl. Fallback to sympy `expand`/`cancel` only on conversion failure. | |
| 76 | + |
| 77 | +### Not yet migrated (gap analysis) |
| 78 | + |
| 79 | +| Category | Sympy API Used | ixsimpl Equivalent | Actual Gap | |
| 80 | +|----------|---------------|-------------------|------------| |
| 81 | +| **Symbol creation** | `sympy.Symbol(name, integer=True, nonneg=True)` | `ctx.sym(name)` + `extract_assumptions` | **Soft.** ixsimpl symbols carry no assumptions intrinsically; assumptions are passed separately to `simplify()`. Wave would need to track assumptions in a side table or always extract from sympy symbols during conversion. | |
| 82 | +| **Expression construction** | `Add`, `Mul`, `Pow`, `Integer`, `Rational`, `Mod(evaluate=False)`, `floor`, `ceiling`, `Min`, `Max`, `Abs`, `Piecewise` | Arithmetic ops, `ctx.int_()`, `ctx.rat()`, `floor`, `ceil`, `mod`, `max_`, `min_`, `pw` | **Soft.** Missing: `Abs` (emulate via `pw`), `evaluate=False` (ixsimpl does not eagerly evaluate, so not needed). `Pow` must be expanded to repeated multiplication (already done in `from_sympy`). | |
| 83 | +| **Piecewise / conditionals** | `sympy.Piecewise((val, cond), ...)` | `pw(*branches)`, `.pw_ncases`, `.pw_value(i)`, `.pw_cond(i)` | **None.** Full piecewise support. The `piecewise_aware_subs()` workaround in indexing.py exists only because sympy's `.subs()` triggers expensive boolean simplification on Piecewise -- ixsimpl's `.subs()` does not have this problem. | |
| 84 | +| **Structural inspection** | `isinstance(expr, sympy.X)`, `.is_number`, `.is_Atom`, `.args`, `.func` | `.tag` (tag-based dispatch), `.nchildren`, `.children`, `.child(i)`, specialized accessors | **Soft.** `isinstance` dispatch maps to `expr.tag == ixsimpl.ADD` etc. Specialized accessors (`.add_nterms`, `.mul_nfactors`) give richer decomposition than sympy's flat `.args`. Missing: `.is_number` (check `tag == INT or tag == RAT`), `.has()` (walk tree or check `free_symbols`). | |
| 85 | +| **Substitution** | `.subs()`, `.xreplace()`, `.replace(pred, fn)` | `.subs(target, repl)`, `.subs(mapping)` | **Soft.** Direct substitution is supported. Missing: `.replace(pred, fn)` (predicate-based bottom-up rewrite). Used in `_custom_simplify_once` for 4 transform passes. Would need a Python-level tree walker. | |
| 86 | +| **Free symbol inspection** | `.free_symbols`, `.has(sym)` | `.free_symbols` (Python `Expr` class) | **None.** Already implemented as a cached property on the Python `Expr` subclass. `.has()` can be trivially derived. | |
| 87 | +| **Expression decomposition** | `.as_ordered_terms()`, `.as_numer_denom()` | `.add_nterms`/`.add_term(i)`/`.add_term_coeff(i)`, `.rat_num`/`.rat_den` | **Soft.** Add decomposition is richer in ixsimpl (coefficient + terms). Numer/denom split is only used for Rational nodes (`.rat_num`/`.rat_den`). | |
| 88 | +| **Numeric probing** | `sympy.lambdify()` with custom modules | None | **Hard gap.** `lambdify` compiles sympy expressions to fast Python callables. Would need either an ixsimpl evaluator or a custom tree walker. `.to_c()` could potentially be used with ctypes but that is overkill. | |
| 89 | +| **Affine conversion** | Sympy -> MLIR AffineExpr pipeline | None (but `.tag`-based walk is possible) | **Soft.** The converter does a structural walk over the expression tree. This maps directly to ixsimpl's `.tag` + `.child(i)` API. The walk structure would be nearly identical. | |
| 90 | +| **Constraint solving** | `sympy.solve()`, `sympy.Eq()` | `ctx.eq()` for construction, no solver | **Hard gap.** `sympy.solve()` used in 2 places (`assumptions.py`, `general_utils.py`). No ixsimpl equivalent. These are niche uses. | |
| 91 | +| **Codegen / printing** | `lambdastr()` for grid dim lambdas | `.to_c()` | **Soft.** `.to_c()` generates C code from expressions. `lambdastr()` generates Python code. Different targets, but the need is similar. Grid lambdas could use `.to_c()` + compile, or a Python eval walker. | |
| 92 | +| **Type system** | `sympy.Integer`, `sympy.Rational`, `.is_Integer`, `.is_Rational` | `tag == INT`, `tag == RAT`, `.rat_num`, `.rat_den` | **None.** Tag-based dispatch replaces isinstance checks. | |
| 93 | + |
| 94 | +### Summary of gaps |
| 95 | + |
| 96 | +**Hard gaps** (no ixsimpl equivalent): |
| 97 | +1. `sympy.solve()` -- constraint solver (2 call sites, niche) |
| 98 | +2. `sympy.lambdify()` -- compiled expression evaluator (numeric probing) |
| 99 | +3. `.replace(pred, fn)` -- predicate-based rewriting (4 transforms in |
| 100 | + `_custom_simplify_once`, but these exist only as fallback when ixsimpl |
| 101 | + conversion fails, so they may become dead code as coverage improves) |
| 102 | + |
| 103 | +**Soft gaps** (ixsimpl has the feature, integration work needed): |
| 104 | +4. Symbol assumptions -- tracked separately, not on the symbol node |
| 105 | +5. `Abs` conversion -- emulate via `pw(x, x >= 0, -x, ctx.true_())` |
| 106 | +6. Affine converter -- structural walk needs porting from sympy isinstance to |
| 107 | + ixsimpl tag dispatch |
| 108 | +7. Grid lambda codegen -- `lambdastr` -> `.to_c()` or Python eval walker |
| 109 | + |
| 110 | +**No gap** (ready to use): |
| 111 | +8. Piecewise construction and inspection |
| 112 | +9. Free symbol inspection |
| 113 | +10. Substitution (direct mapping; predicate-based `.replace` excluded) |
| 114 | +11. Structural inspection via `.tag` and accessors |
| 115 | +12. Expression decomposition (add terms, mul factors, rational parts) |
| 116 | + |
| 117 | +## Migration Strategy |
| 118 | + |
| 119 | +ixsimpl is not just a simplifier -- it provides symbol creation, expression |
| 120 | +construction, structural inspection, substitution, free symbol queries, |
| 121 | +piecewise, expansion, and C codegen. The only true hard gaps vs sympy are |
| 122 | +`sympy.solve()` (2 call sites) and `lambdify()` (numeric probing). |
| 123 | + |
| 124 | +**Recommended end state: ixsimpl as the primary expression IR, sympy as a |
| 125 | +thin compatibility layer retained only for `solve()` and `lambdify()`.** |
| 126 | + |
| 127 | +### Incremental phases |
| 128 | + |
| 129 | +## Phase 1: Complete simplification migration (CURRENT) |
| 130 | + |
| 131 | +**Goal:** All simplification goes through ixsimpl. Zero calls to |
| 132 | +`sympy.simplify()`, `sympy.expand()`, `sympy.cancel()` outside the fallback path |
| 133 | +in `symbol_utils.simplify()`. |
| 134 | + |
| 135 | +**Status:** Mostly done. Remaining direct sympy simplification calls: |
| 136 | + |
| 137 | +| File | Call | Action | |
| 138 | +|------|------|--------| |
| 139 | +| `symbol_utils.py:498,502` | `sympy.expand()`, `sympy.cancel()` in fallback | Keep -- intentional fallback for unconvertible expressions. | |
| 140 | +| `symbol_utils.py:303` | `sympy.cancel(t / divisor)` in `split_sum_by_divisibility` | Route through ixsimpl if possible, else keep. | |
| 141 | +| `index_mapping_simplify.py:177` | `sympy.cancel(numer - mod_arg)` | Same -- targeted cancellation. | |
| 142 | +| `read_write.py:118` | `sympy.expand(diff)` for non-Piecewise | Replace with `simplify(diff)`. | |
| 143 | +| `schedule.py:622` | `expr.simplify()` (sympy native method) | Replace with `simplify(expr)` from symbol_utils. | |
| 144 | + |
| 145 | +**Work items:** |
| 146 | +1. Replace `expr.simplify()` call in `schedule.py` with `simplify(expr)`. |
| 147 | +2. Replace `sympy.expand(diff)` in `read_write.py:118` with `simplify(diff)`. |
| 148 | +3. Audit: ensure no other file calls sympy simplification directly. |
| 149 | +4. Track fallback frequency to identify remaining conversion gaps. |
| 150 | + |
| 151 | +## Phase 2: Centralize sympy imports behind Wave wrappers |
| 152 | + |
| 153 | +**Goal:** No production file imports sympy directly except foundation modules. |
| 154 | + |
| 155 | +**Allowed import sites:** |
| 156 | +- `wave_lang/support/indexing.py` -- type aliases, symbol creation |
| 157 | +- `wave_lang/kernel/wave/utils/symbol_utils.py` -- simplification, bounds, probing |
| 158 | +- `wave_lang/kernel/wave/mlir_converter/attr_type_converter.py` -- affine conversion |
| 159 | +- `wave_lang/kernel/wave/water_mlir/.../sympy_to_affine_converter.py` -- affine conversion |
| 160 | +- `wave_lang/kernel/compiler/wave_codegen/emitter.py` -- codegen pattern matching |
| 161 | + |
| 162 | +**Strategy:** |
| 163 | +Re-export needed sympy names from `indexing.py` and `symbol_utils.py`. |
| 164 | +Downstream files import from Wave modules, not sympy. |
| 165 | + |
| 166 | +```python |
| 167 | +# indexing.py additions |
| 168 | +from sympy import ( |
| 169 | + Integer, Rational, Mod, Piecewise, Eq, |
| 170 | + floor, ceiling, Min, Max, |
| 171 | +) |
| 172 | +``` |
| 173 | + |
| 174 | +**Work items:** |
| 175 | +1. Add re-exports to `indexing.py` for expression constructors. |
| 176 | +2. Add re-exports to `symbol_utils.py` for analysis utilities. |
| 177 | +3. File-by-file: replace `import sympy` with imports from Wave modules. |
| 178 | +4. Add a ruff rule or pre-commit check to flag direct `import sympy`. |
| 179 | + |
| 180 | +**Risk:** Low. Mechanical refactor, no behavior change. |
| 181 | + |
| 182 | +## Phase 3: Dual-IR wrapper layer |
| 183 | + |
| 184 | +**Goal:** Introduce a thin `IndexExpr` wrapper that can hold either a sympy |
| 185 | +expression or an ixsimpl `Expr`, exposing a unified API. This allows incremental |
| 186 | +migration without big-bang rewrites. |
| 187 | + |
| 188 | +**Key insight:** ixsimpl's `.tag`-based dispatch maps cleanly to sympy's |
| 189 | +`isinstance` dispatch. The wrapper translates between them: |
| 190 | + |
| 191 | +```python |
| 192 | +# Sketch -- not final API |
| 193 | +def expr_tag(expr) -> int: |
| 194 | + """Unified tag for both sympy and ixsimpl expressions.""" |
| 195 | + if isinstance(expr, ixsimpl.Expr): |
| 196 | + return expr.tag |
| 197 | + if isinstance(expr, sympy.Add): |
| 198 | + return ixsimpl.ADD |
| 199 | + if isinstance(expr, sympy.Mul): |
| 200 | + return ixsimpl.MUL |
| 201 | + ... |
| 202 | + |
| 203 | +def expr_free_symbols(expr) -> set: |
| 204 | + """Free symbols from either IR.""" |
| 205 | + return expr.free_symbols # both have this |
| 206 | + |
| 207 | +def expr_subs(expr, mapping: dict): |
| 208 | + """Substitution on either IR.""" |
| 209 | + if isinstance(expr, ixsimpl.Expr): |
| 210 | + return expr.subs(mapping) |
| 211 | + return piecewise_aware_subs(expr, mapping) |
| 212 | +``` |
| 213 | + |
| 214 | +**Work items:** |
| 215 | +1. Define wrapper functions in a new `expr_api.py` or extend `symbol_utils.py`. |
| 216 | +2. Migrate callers one pass at a time (one PR per compiler pass). |
| 217 | +3. Each pass can be tested independently. |
| 218 | + |
| 219 | +**Risk:** Medium. Need to ensure sympy/ixsimpl semantic equivalence at each call |
| 220 | +site. The conversion layer (`sympy_conv.py`) already validates this for the |
| 221 | +simplification path. |
| 222 | + |
| 223 | +## Phase 4: Port affine converter to ixsimpl |
| 224 | + |
| 225 | +**Goal:** `sympy_to_affine_converter.py` works directly on ixsimpl `Expr` nodes |
| 226 | +instead of sympy expressions. |
| 227 | + |
| 228 | +This phase has **no ordering dependency** on Phases 3 or 5. The affine converter |
| 229 | +is a self-contained structural tree walk -- it can be ported to ixsimpl tags |
| 230 | +while the rest of the compiler still uses sympy. During transition, the converter |
| 231 | +can accept ixsimpl `Expr` natively and fall back to `from_sympy()` conversion |
| 232 | +for callers still passing sympy expressions. |
| 233 | + |
| 234 | +ixsimpl's `.tag` + accessor API maps 1:1 to the current isinstance dispatch: |
| 235 | + |
| 236 | +| Current (sympy) | New (ixsimpl) | |
| 237 | +|-----------------|---------------| |
| 238 | +| `isinstance(expr, sympy.Integer)` | `expr.tag == INT` | |
| 239 | +| `isinstance(expr, sympy.Rational)` | `expr.tag == RAT` | |
| 240 | +| `isinstance(expr, sympy.Symbol)` | `expr.tag == SYM` | |
| 241 | +| `isinstance(expr, sympy.Add)` | `expr.tag == ADD` | |
| 242 | +| `isinstance(expr, sympy.Mul)` | `expr.tag == MUL` | |
| 243 | +| `isinstance(expr, sympy.floor)` | `expr.tag == FLOOR` | |
| 244 | +| `isinstance(expr, sympy.Mod)` | `expr.tag == MOD` | |
| 245 | +| `isinstance(expr, sympy.Piecewise)` | `expr.tag == PIECEWISE` | |
| 246 | +| `expr.args[0]` | `expr.child(0)` | |
| 247 | +| `int(expr)` | `int(expr)` | |
| 248 | +| `expr.p, expr.q` | `expr.rat_num, expr.rat_den` | |
| 249 | + |
| 250 | +**Work items:** |
| 251 | +1. Create `ixsimpl_to_affine_converter.py` alongside the existing converter. |
| 252 | +2. Port case-by-case, sharing the `AffineFraction` infrastructure. |
| 253 | +3. Add a `from_sympy()` shim at the entry point so callers passing sympy |
| 254 | + expressions still work during transition. |
| 255 | +4. Test both paths produce identical AffineExpr output. |
| 256 | +5. Once validated, swap the default and deprecate the sympy path. |
| 257 | + |
| 258 | +**Risk:** Medium. The converter has subtle Rational/fraction handling that needs |
| 259 | +careful porting. |
| 260 | + |
| 261 | +## Phase 5: Port emitter to tag-based dispatch |
| 262 | + |
| 263 | +**Goal:** `emitter.py` pattern-matches on ixsimpl tags instead of sympy types. |
| 264 | + |
| 265 | +The emitter currently does: |
| 266 | +```python |
| 267 | +match expr: |
| 268 | + case sympy.Add(): ... |
| 269 | + case sympy.Mul(): ... |
| 270 | + case sympy.Mod(): ... |
| 271 | + ... |
| 272 | +``` |
| 273 | + |
| 274 | +This becomes: |
| 275 | +```python |
| 276 | +tag = expr.tag |
| 277 | +if tag == ADD: |
| 278 | + ... |
| 279 | +elif tag == MUL: |
| 280 | + ... |
| 281 | +elif tag == MOD: |
| 282 | + ... |
| 283 | +``` |
| 284 | + |
| 285 | +With ixsimpl's specialized accessors, the emitter can also be cleaner -- e.g. |
| 286 | +`.add_nterms` / `.add_term(i)` instead of iterating `.args` and guessing |
| 287 | +structure. |
| 288 | + |
| 289 | +**Work items:** |
| 290 | +1. Port emitter dispatch to ixsimpl tags. |
| 291 | +2. Use `.add_*` / `.mul_*` / `.pw_*` accessors for structured decomposition. |
| 292 | +3. Keep sympy conversion as fallback during transition. |
| 293 | + |
| 294 | +**Risk:** Medium. The emitter is well-tested via LIT tests. Changes should be |
| 295 | +caught by FileCheck. |
| 296 | + |
| 297 | +## Phase 6: Remove sympy from hot paths |
| 298 | + |
| 299 | +**Goal:** Compiler passes operate on ixsimpl `Expr` natively. Sympy is only |
| 300 | +used for: |
| 301 | +- `sympy.solve()` (2 call sites in `assumptions.py`, `general_utils.py`) |
| 302 | +- `sympy.lambdify()` (numeric probing in `symbol_utils.py`) |
| 303 | +- Legacy test assertions that compare sympy expressions |
| 304 | + |
| 305 | +**What this means:** |
| 306 | +- `IndexExpr` type alias changes from `sympy.Expr` to `ixsimpl.Expr`. |
| 307 | +- Symbol creation via `ctx.sym()` instead of `sympy.Symbol()`. |
| 308 | +- Expression construction via ixsimpl operators and free functions. |
| 309 | +- Substitution via `.subs()`. |
| 310 | +- No more `piecewise_aware_subs()` workaround. |
| 311 | +- No more `evaluate=False` on Mod/floor (ixsimpl does not eagerly evaluate). |
| 312 | + |
| 313 | +**The 2 remaining sympy uses:** |
| 314 | +- `sympy.solve()`: Convert ixsimpl -> sympy at the call site, solve, convert |
| 315 | + back. This is 2 call sites total. |
| 316 | +- `lambdify()`: Either keep the sympy conversion for probing, or write a simple |
| 317 | + Python evaluator that walks ixsimpl tags. The latter is ~50 lines. |
| 318 | + |
| 319 | +**Risk:** High but contained. This is the "flip the switch" phase. Every |
| 320 | +preceding phase must be complete and validated. |
| 321 | + |
| 322 | +## What NOT to do |
| 323 | + |
| 324 | +1. **Do not try to eliminate sympy in one shot.** The 6 phases exist for a |
| 325 | + reason. Each phase is independently testable and revertible. |
| 326 | + |
| 327 | +2. **Do not add ixsimpl calls outside the centralized wrappers** (until Phase 6 |
| 328 | + flips the default IR). All ixsimpl usage should go through `symbol_utils.py` |
| 329 | + so fallback behavior is consistent. |
| 330 | + |
| 331 | +3. **Do not remove the sympy fallback path prematurely.** Until ixsimpl handles |
| 332 | + 100% of expressions the compiler produces, the fallback is a safety net. |
| 333 | + |
| 334 | +4. **Do not port the emitter and affine converter simultaneously.** These are |
| 335 | + independent subsystems; port one at a time with full test coverage between. |
| 336 | + |
| 337 | +## Dependency risks |
| 338 | + |
| 339 | +- **ixsimpl is pinned to a specific git SHA.** API changes require updating the |
| 340 | + pin and potentially the conversion layer. |
| 341 | +- **Thread safety:** ixsimpl context is not thread-safe (handled via |
| 342 | + thread-local storage). Works fine for multiprocessing. For async, verify |
| 343 | + context isolation. |
| 344 | +- **sympy version sensitivity:** The codebase works around sympy bugs (#28744 Mod |
| 345 | + auto-evaluation, floor/ceil evaluation on Max/Min arguments). Moving to ixsimpl |
| 346 | + as primary IR eliminates these workarounds entirely. |
| 347 | +- **ixsimpl `.subs()` semantics:** Need to verify it matches sympy's substitution |
| 348 | + semantics exactly, particularly for Piecewise conditions and nested |
| 349 | + replacements. The `piecewise_aware_subs` workaround exists because sympy's |
| 350 | + `.subs()` triggers boolean simplification -- if ixsimpl's `.subs()` does not |
| 351 | + have this problem, the workaround can be dropped. |
| 352 | + |
| 353 | +## Metrics to track |
| 354 | + |
| 355 | +- Number of files with direct `import sympy` (target: 5 -> 2 -> 0 non-test). |
| 356 | +- Fallback rate: how often `simplify()` hits the sympy fallback (indicates |
| 357 | + conversion coverage gaps). |
| 358 | +- sympy-to-ixsimpl conversion errors by type (identifies which expression |
| 359 | + patterns still need `from_sympy` support). |
| 360 | + |
| 361 | +## Priority order |
| 362 | + |
| 363 | +| Phase | Effort | Risk | Depends on | Impact | |
| 364 | +|-------|--------|------|------------|--------| |
| 365 | +| 1. Complete simplification migration | Low | Low | -- | Eliminates stray `sympy.simplify` calls | |
| 366 | +| 2. Centralize imports | Low | Low | -- | Creates migration chokepoint | |
| 367 | +| 3. Dual-IR wrapper layer | Medium | Medium | 2 | Enables incremental pass migration | |
| 368 | +| 4. Port affine converter | Medium | Medium | -- | Removes sympy from MLIR lowering | |
| 369 | +| 5. Port emitter | Medium | Medium | 3 | Removes sympy from codegen | |
| 370 | +| 6. Remove sympy from hot paths | High | High | 3, 4, 5 | ixsimpl becomes primary IR | |
| 371 | + |
| 372 | +Phases 1, 2, and 4 can run in parallel. Phase 4 uses the existing sympy |
| 373 | +roundtrip (`from_sympy`) as a shim, so it does not need to wait for the rest |
| 374 | +of the compiler to migrate. |
0 commit comments