[Arith] Make Analyzer a tvm-ffi Object - #19675
Conversation
e155636 to
55f38ab
Compare
There was a problem hiding this comment.
Code Review
This pull request refactors the arithmetic Analyzer in TVM to be a first-class FFI object. The stateful Analyzer class is renamed to AnalyzerObj (inheriting from ffi::Object), and Analyzer is redefined as a lightweight, reference-counted handle (ffi::ObjectRef) wrapping it. This allows Analyzer instances to be passed across the TVM FFI boundary (e.g., between Python and C++) and shared, preserving accumulated bindings and constraints. Numerous function signatures and call sites across the codebase have been updated to accept AnalyzerObj* instead of Analyzer*, and member accesses on Analyzer now use the arrow operator (->). Additionally, Python bindings and index mapping utilities have been updated to support passing an external Analyzer instance, and corresponding tests have been added. No reviewer comments were provided, so there is no additional feedback to address.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
9697f79 to
ef6cabb
Compare
986240b to
2eed01e
Compare
2eed01e to
66191ea
Compare
8f84480 to
f07de85
Compare
f07de85 to
3478fd9
Compare
…d tests Now that arith::Analyzer is a tvm-ffi object, expose more of its stateful surface to Python and let callers share a single analyzer across FFI calls. - Add Python bindings for additional Analyzer methods. - Accept optional analyzers in iter-map, int-set, and IndexMap helper APIs. - Keep IndexMap inverse temporary bindings out of caller-provided analyzers. - Add targeted tests for optional analyzer reuse and state isolation.
3478fd9 to
23af9ce
Compare
b58d406 to
55a6199
Compare
55a6199 to
23af9ce
Compare
guan404ming
left a comment
There was a problem hiding this comment.
Great work, thanks @tlopex
…t Analyzer& After apache#19675 made arith::Analyzer an FFI object, IRMutatorWithAnalyzer and its subclasses took AnalyzerObj*, forcing every instantiation site to pass analyzer.get(). Migrate the IRMutatorWithAnalyzer family to accept the Analyzer handle directly. Changes: - IRMutatorWithAnalyzer gains a `const Analyzer&` constructor overload. The existing `AnalyzerObj*` overload is kept on purpose: RewriteSimplifier::Impl is constructed from the AnalyzerObj's own `this` mid-construction, where no Analyzer handle exists yet and GetRef on a half-built, self-owned object would be unsafe / form an ownership cycle. - The directly-instantiated subclass constructors switch AnalyzerObj* -> const Analyzer&, dropping analyzer.get() at their call sites. - For the factory-style subclasses (NoOpRemover, StmtSimplifier) the construction happens inside their static Apply()/free-function entry points, so those internal (src/, non-FFI, non-public) signatures move to const Analyzer& as well -- StmtSimplify(), RemoveNoOp() and BlockBufferAccessSimplifier::Simplify() -- removing analyzer.get() at the remaining call sites. Deliberately left as AnalyzerObj*: - The per-sub-analyzer `AnalyzerObj* parent` constructors (ConstIntBound, ModularSet, IntSet, Canonical, RewriteSimplifier): AnalyzerObj owns them by value, so a strong back-reference would be a cycle. - VTInjector: it is built from the inherited raw analyzer_ member and never wrote analyzer.get() to begin with. The field stays `AnalyzerObj* analyzer_` (borrowed), so there is no extra refcount and no ownership cycle. Because AnalyzerObj is _type_mutable, a const Analyzer& still exposes a non-const operator->, so subclass bodies (analyzer_->Bind(...)) are unchanged. No Python / FFI / public-header surface changes; runtime behavior is identical.
After apache#19675 made arith::Analyzer an FFI object, IRMutatorWithAnalyzer and its subclasses took AnalyzerObj*, forcing every instantiation site to pass analyzer.get(). Migrate the IRMutatorWithAnalyzer family to accept the Analyzer handle directly. Changes: - IRMutatorWithAnalyzer gains a `const Analyzer&` constructor overload. The existing `AnalyzerObj*` overload is kept on purpose: RewriteSimplifier::Impl is constructed from the AnalyzerObj's own `this` mid-construction, where no Analyzer handle exists yet and GetRef on a half-built, self-owned object would be unsafe / form an ownership cycle. - The directly-instantiated subclass constructors switch AnalyzerObj* -> const Analyzer&, dropping analyzer.get() at their call sites. - For the factory-style subclasses (NoOpRemover, StmtSimplifier) the construction happens inside their static Apply()/free-function entry points, so those internal (src/, non-FFI, non-public) signatures move to const Analyzer& as well -- StmtSimplify(), RemoveNoOp() and BlockBufferAccessSimplifier::Simplify() -- removing analyzer.get() at the remaining call sites. Deliberately left as AnalyzerObj*: - The per-sub-analyzer `AnalyzerObj* parent` constructors (ConstIntBound, ModularSet, IntSet, Canonical, RewriteSimplifier): AnalyzerObj owns them by value, so a strong back-reference would be a cycle. - VTInjector: it is built from the inherited raw analyzer_ member and never wrote analyzer.get() to begin with. The field stays `AnalyzerObj* analyzer_` (borrowed), so there is no extra refcount and no ownership cycle. Because AnalyzerObj is _type_mutable, a const Analyzer& still exposes a non-const operator->, so subclass bodies (analyzer_->Bind(...)) are unchanged. No Python / FFI / public-header surface changes; runtime behavior is identical.
After apache#19675 made arith::Analyzer an FFI object, IRMutatorWithAnalyzer and its subclasses took AnalyzerObj*, forcing every instantiation site to pass analyzer.get(). Migrate the IRMutatorWithAnalyzer family to accept the Analyzer handle directly. Changes: - IRMutatorWithAnalyzer gains a `const Analyzer&` constructor overload. The existing `AnalyzerObj*` overload is kept on purpose: RewriteSimplifier::Impl is constructed from the AnalyzerObj's own `this` mid-construction, where no Analyzer handle exists yet and GetRef on a half-built, self-owned object would be unsafe / form an ownership cycle. - The directly-instantiated subclass constructors switch AnalyzerObj* -> const Analyzer&, dropping analyzer.get() at their call sites. - For the factory-style subclasses (NoOpRemover, StmtSimplifier) the construction happens inside their static Apply()/free-function entry points, so those internal (src/, non-FFI, non-public) signatures move to const Analyzer& as well -- StmtSimplify(), RemoveNoOp() and BlockBufferAccessSimplifier::Simplify() -- removing analyzer.get() at the remaining call sites. Deliberately left as AnalyzerObj*: - The per-sub-analyzer `AnalyzerObj* parent` constructors (ConstIntBound, ModularSet, IntSet, Canonical, RewriteSimplifier): AnalyzerObj owns them by value, so a strong back-reference would be a cycle. - VTInjector: it is built from the inherited raw analyzer_ member and never wrote analyzer.get() to begin with. The field stays `AnalyzerObj* analyzer_` (borrowed), so there is no extra refcount and no ownership cycle. Because AnalyzerObj is _type_mutable, a const Analyzer& still exposes a non-const operator->, so subclass bodies (analyzer_->Bind(...)) are unchanged. No Python / FFI / public-header surface changes; runtime behavior is identical.
This pr is the follow-up pr to #19675. IRMutatorWithAnalyzer and its subclasses took AnalyzerObj*, so callers had to pass analyzer.get(). This adds a const Analyzer& constructor and migrates the family (and their Apply() / free-function entry points) to take the handle directly, dropping .get() at the call sites. The original AnalyzerObj* overload is kept because RewriteSimplifier::Impl is constructed from the AnalyzerObj's own this during the analyzer's construction, where no Analyzer handle exists yet. And the same dual-overload pattern already used by arith::ConstraintContext.
This PR makes
arith::Analyzera first-class tvm-ffi object.The implementation splits the previous concrete
Analyzerclass into:AnalyzerObj, the mutable object node that owns analyzer state, sub-analyzers, caches, and bindingsAnalyzer, a reference-countedObjectRefhandle that can be passed across the tvm-ffi boundaryThis allows Python and C++ to share the same analyzer instance, so bindings, constraints, and cached facts can persist across FFI calls.
Public APIs that accept an analyzer now use
const arith::Analyzer&, while internal helper APIs that only borrow the object continue to useAnalyzerObj*.