[REFACTOR][IR] Unify PrimExpr with Expr typed view#19910
Conversation
There was a problem hiding this comment.
Code Review
This pull request refactors the TVM IR and Relax type systems by unifying primitive expressions under the base Expr class with a PrimType annotation, replacing the separate PrimExpr class. It also introduces Type::Missing() as a sentinel for unpopulated type information. The review feedback highlights a potential compiler crash in src/relax/op/op.cc when DeriveCallRetType returns Type::Missing(), and suggests several code simplifications, such as using ffi::downcast in C++ and utilizing the is_prim_expr helper in Python instead of manual type checks.
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.
5167e05 to
c90fc7b
Compare
|
Follow-up for the latest review batch is pushed in
|
4c01b4e to
fd154de
Compare
PrimExpr is now a typed view over Expr values with PrimType results, and Call is shared across IR dialects. Migration guide: - In C++, narrow a generic Expr with as_or_throw<PrimExpr>(); use direct GetRef<PrimExpr> only where the concrete node invariant guarantees a PrimType result. - Construct shared ir::Call with an explicit result Type. Primitive-valued calls carry PrimType and can then be viewed as PrimExpr. - Represent unavailable type information with Type::Missing(), and check IsMissing() before typed traversal or narrowing. - In Python, use tvm.ir.is_prim_expr(value), or an explicit Expr plus PrimType check at a deliberate API boundary, instead of nominal PrimExpr isinstance checks. - Store shared expression collections as Expr and perform checked element narrowing only at primitive-only consumers; likewise replace visitor and overload assumptions tied to a nominal PrimExpr node class.
fd154de to
a03f59f
Compare
Use is_prim_expr consistently at Python primitive-expression boundaries and remove annotations made redundant by the shared Expr hierarchy. Use checked as_or_throw narrowing for generic C++ expressions and checked array conversion when every collection element must be primitive.
Summary
PrimExpra typed C++ view overExprvalues whoseExprNode::tyisPrimType, instead of using a separate runtime node class as the proof of primitive-ness.ir::Callnode for Relax, TIRX, and primitive-valued calls, while keeping primitive-only APIs explicit at their semantic boundaries.Exprsurface for primitive-typed values soisinstancebehavior does not imply a nominal primitive-expression subclass.Design Rationale
The main advantage of this change is that common expression nodes such as
Callcan be unified without specializing each one toPrimType. A singleir::Callcan represent a Relax tensor call, a Relax scalar call, or a primitive-valued intrinsic call; the result type stored inExprNode::tydetermines whether that particular value can be viewed asPrimExpr.This keeps the IR node hierarchy focused on expression structure rather than result-type categories. Nodes that are intrinsically primitive, such as integer and floating-point literals or TIRX primitive operators, still have strongly typed C++ APIs and data structures. General nodes whose result type may vary, such as
Call, remain generalExprnodes and are narrowed toPrimExpronly where primitive-only semantics are required.The PR also keeps the compatibility surface practical: C++ primitive-only APIs continue to accept
PrimExpr, Python exposes a compatibility predicate for checking the primitive typed category, and visitors/printers use one naturalCallpath rather than duplicating Relax and primitive call handling. Missing expression types are represented explicitly withType::Missing()so constructors can leave type inference to later analysis without relying on nullableTypevalues.