This is an automated email from the ASF dual-hosted git repository.
tqchen pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm-tirx.git
The following commit(s) were added to refs/heads/main by this push:
new a23416382 [DOCS][Rust] Explain generic #[dispatch] in the Rust IR
guide (#182)
a23416382 is described below
commit a23416382953b7a8cece2aaab77a7b2557901c1a
Author: Tianqi Chen <[email protected]>
AuthorDate: Tue Nov 3 20:57:01 2026 -0500
[DOCS][Rust] Explain generic #[dispatch] in the Rust IR guide (#182)
Rewrite "Dispatching on Node Types" in the Rust IR guide to introduce
`#[dispatch]` on its own, independent of the structural utilities:
- Why it exists: dispatching over many kinds of IR nodes is common;
`match_any!` provides the machinery and `#[dispatch]` builds the
dispatcher from one method per node type.
- A minimal three-handler `Printer` example that parenthesizes nested
additions, with its real output.
- The simplified `match_any!` and `Dispatcher` code the attribute
expands to.
- The rule that each handler takes the matched value plus a leading
prefix of the remaining arguments.
The `#[dispatch(walk)]` `Counter` example moves into "Structural Walk",
where it shows a dispatcher driving a walk and the fallback for node
types without a handler. The constructs table and Quick Reference
describe `#[dispatch]` as generating a match over the prefix methods.
---
docs/ir_guides/working_with_ir_in_rust.rst | 132 +++++++++++++++++++-------
examples/ir_guides/rust/src/lib.rs | 42 ++++++++
examples/ir_guides/working_with_ir_in_rust.py | 3 +
3 files changed, 143 insertions(+), 34 deletions(-)
diff --git a/docs/ir_guides/working_with_ir_in_rust.rst
b/docs/ir_guides/working_with_ir_in_rust.rst
index 94b6a8786..fd80dd7b8 100644
--- a/docs/ir_guides/working_with_ir_in_rust.rst
+++ b/docs/ir_guides/working_with_ir_in_rust.rst
@@ -115,7 +115,7 @@ key constructs are:
* - |match_any!|_
- Branch on the node type
* - |dispatch|_
- - Process each node type in its own method, with shared state
+ - Write one method per node type; the attribute generates the match
The subsections below introduce each construct.
@@ -281,8 +281,9 @@ The output is:
((x * 2) + 1)
<other>
-Each arm names an object type and borrows the matching node with ``ref``;
-the first matching arm runs. The ``AddObj`` and ``MulObj`` arms recurse into
+Each *arm* of the match pairs a pattern with code; here each pattern names
+an object type and borrows the matching node with ``ref``. The first
+matching arm runs. The ``AddObj`` and ``MulObj`` arms recurse into
the operands, the ``VarObj`` and ``IntImmObj`` arms end the recursion, and
``x - 1`` falls to the ``_`` arm because ``describe`` has no arm for
subtraction.
@@ -301,54 +302,88 @@ types it knows, and ``_`` lets programs with new types
pass through.
tensors, shapes, and typed arrays. Plain values such as integers take
the ``_`` arm in this example.
-A match looks at one node at a time; to process every node in a program
-while keeping state, we dispatch.
+``describe`` puts parentheses around every operation. To add them only
+where they are needed, each node needs to know whether it is nested, so
+let's give each node type its own method that takes that context.
.. _rust-dispatch:
Dispatching on Node Types
~~~~~~~~~~~~~~~~~~~~~~~~~
-|dispatch|_ **lets one struct process each node type in its own method,
-with shared state.** A compiler pass often needs one piece of logic per
-node type and state shared across them; ``#[dispatch]`` puts both on one
-struct. It works like a trait whose methods all have default bodies: we
-write methods for the node types we care about, and every other type gets
-the default behavior.
-
-``#[dispatch(walk, (AnyView<'_>, DefRegionKind), Result<WalkResult>)]``
-on an ``impl`` block turns its ``walk_*`` methods into callbacks for
-|structural_walk|_, which visits every node. Each method borrows the nodes
-of its parameter type, and the methods are tried in source order. The
-attribute declares the argument tuple and ``Result<WalkResult>`` return type;
-handlers that
-do not need the trailing ``DefRegionKind`` may omit it. Let's count the
-variables and additions:
+A common need in a compiler is to dispatch over many kinds of IR nodes:
+run different logic depending on which kind a value is. ``match_any!``
+provides the machinery, and |dispatch|_ **makes such dispatchers simpler to
+build**: write one method per node type, and the attribute generates the
+match that picks between them. Let's print additions with parentheses only
+where they nest:
.. literalinclude:: ../../examples/ir_guides/rust/src/lib.rs
:language: rust
- :start-after: // [counter.begin]
- :end-before: // [counter.end]
+ :start-after: // [printer.begin]
+ :end-before: // [printer.end]
-We walk ``x * 2 + 1`` with it. ``WalkResult::Advance`` continues into
-the node's children, and ``WalkOrder::PreOrder`` visits a node before its
-children:
+We print ``x + 1 + 2``:
.. literalinclude:: ../../examples/ir_guides/rust/src/lib.rs
:language: rust
- :start-after: // [count.begin]
- :end-before: // [count.end]
+ :start-after: // [print.begin]
+ :end-before: // [print.end]
The output is:
.. code-block:: text
- variables: 1, additions: 1
+ (x + 1) + 2
+
+The ``#[dispatch(print, (AnyView<'_>, bool), String)]`` attribute makes the
+``print_*`` methods handlers and generates
+``dispatch(value, nested) -> Option<String>``. The first element of the
+tuple is the value to match; the rest are passed to the handlers.
+``print_add`` passes ``nested = true`` to its operands, so the inner
+addition gets parentheses and the outer one does not. ``show`` is an
+ordinary method: it calls ``dispatch`` and turns ``None``, which means no
+handler matched, into ``"<other>"``.
+
+**The attribute is sugar for a match.** Simplified, ``#[dispatch]``
+generates a ``match_any!`` and a ``Dispatcher`` implementation:
+
+.. code-block:: rust
+
+ impl Printer {
+ pub fn dispatch(&self, value: AnyView<'_>, nested: bool) ->
Option<String> {
+ match_any! {
+ value {
+ AddObj(ref node) => Some(self.print_add(node,
nested).into()),
+ VarObj(ref node) => Some(self.print_var(node).into()),
+ IntImmObj(ref node) => Some(self.print_imm(node).into()),
+ _ => None,
+ }
+ }
+ }
+ }
+
+ impl Dispatcher<(AnyView<'_>, bool), Option<String>> for &Printer {
+ fn dispatch_tuple(self, (value, nested): (AnyView<'_>, bool)) ->
Option<String> {
+ Printer::dispatch(self, value, nested)
+ }
+ }
-**Node types without a method fall back to the default traversal**: the
-walk continues into their children, so new types pass through. Here the
-multiplication and the constants have no method, and the walk still
-reaches ``x`` inside the multiplication.
+Each handler becomes one arm, in source order, and its parameter type
+selects the nodes it matches. The attribute appends ``_ => None``, so a
+type without a handler returns ``None`` instead of failing. The
+``Dispatcher`` implementation lets generic code call the same match
+through one trait.
+
+**A handler takes only the arguments it needs.** It receives the matched
+value and a leading prefix of the remaining arguments, in declaration
+order: ``print_add`` takes ``nested``, while ``print_var`` and
+``print_imm`` omit it.
+
+The structural utilities below accept the same kind of dispatcher: walk,
+visit, map, and mutate each call the dispatcher on the nodes they reach.
+The prefix only names the methods; the utility that calls the dispatcher
+decides what happens to the result.
.. _rust-structural-utilities:
@@ -446,6 +481,34 @@ Python prints the ``dest.name`` of the store it receives.
The output is:
``structural_walk`` returns ``Ok(Some(interrupt))`` when a callback stops
it, with the payload in ``interrupt.value``, and ``Ok(None)`` otherwise.
+A walk also accepts a |dispatch|_ struct, which keeps state across node
+types. ``#[dispatch(walk, (AnyView<'_>, DefRegionKind), Result<WalkResult>)]``
+matches the arguments and result of a walk callback. Let's count the
+variables and additions:
+
+.. literalinclude:: ../../examples/ir_guides/rust/src/lib.rs
+ :language: rust
+ :start-after: // [counter.begin]
+ :end-before: // [counter.end]
+
+We walk ``x * 2 + 1`` with it, visiting each node before its children:
+
+.. literalinclude:: ../../examples/ir_guides/rust/src/lib.rs
+ :language: rust
+ :start-after: // [count.begin]
+ :end-before: // [count.end]
+
+The output is:
+
+.. code-block:: text
+
+ variables: 1, additions: 1
+
+**Node types without a handler fall back to the default traversal**: the
+dispatcher returns ``None``, and the walk continues into their children,
+so new types pass through. Here the multiplication and the constants have
+no handler, and the walk still reaches ``x`` inside the multiplication.
+
Definitions and Uses
~~~~~~~~~~~~~~~~~~~~
@@ -527,7 +590,7 @@ Visitors and Mutators in Rust
-----------------------------
Stateful traversals use the same four structural utilities. State lives
-on a |dispatch|_ struct, as in Dispatching on Node Types.
+on a |dispatch|_ struct, as in Structural Walk.
The ``map``, ``visit``, and ``mutate`` prefixes group ``map_*``,
``visit_*``, and ``mutate_*`` methods. Each attribute declares its
argument tuple and return type; mutation uses ``MutationView`` as its
@@ -635,7 +698,8 @@ Quick Reference
* - ``Function::get_global("ffi.StructuralEqual")``
- Compares by structure
* - |dispatch|_\ ``(prefix, (Carrier, ...), ResultType)``
- - Turns the methods of a struct into typed callbacks
+ - Generates ``dispatch``, a ``match_any!`` over the ``prefix_*``
+ methods of a struct
* - |StructuralVisitorView|_, |StructuralMutatorView|_
- Recurse with the same dispatcher and current definition region
* - |static_init_block!|_, |register_global_typed|_
diff --git a/examples/ir_guides/rust/src/lib.rs
b/examples/ir_guides/rust/src/lib.rs
index d52d2edb8..1883f3b31 100644
--- a/examples/ir_guides/rust/src/lib.rs
+++ b/examples/ir_guides/rust/src/lib.rs
@@ -107,6 +107,43 @@ fn matching() -> Result<Strings> {
}
// [matching.end]
+// [printer.begin]
+struct Printer;
+
+#[dispatch(print, (AnyView<'_>, bool), String)]
+impl Printer {
+ fn print_add(&self, add: &AddObj, nested: bool) -> String {
+ let text = format!("{} + {}", self.show(&add.a, true),
self.show(&add.b, true));
+ if nested {
+ format!("({text})")
+ } else {
+ text
+ }
+ }
+
+ fn print_var(&self, var: &VarObj) -> String {
+ var.name.as_str().to_owned()
+ }
+
+ fn print_imm(&self, imm: &IntImmObj) -> String {
+ imm.value.to_string()
+ }
+
+ fn show(&self, expr: &Expr, nested: bool) -> String {
+ let text = self.dispatch(AnyView::from(expr), nested);
+ text.unwrap_or_else(|| "<other>".to_owned())
+ }
+}
+// [printer.end]
+
+// [print.begin]
+fn print() -> Result<tvm_ffi::String> {
+ let x = Var::new("x", PrimType::int(32), UNKNOWN_LOC);
+ let expr = (&x + 1 + 2).try_into()?;
+ Ok(Printer.show(&expr, false).into())
+}
+// [print.end]
+
// [walk.begin]
fn written_tensors(func: Function) -> Result<Strings> {
let mut writes = Vec::new();
@@ -293,6 +330,11 @@ tvm_ffi::static_init_block! {
matching,
"Describe expressions by matching their nodes.",
)?;
+ tvm_ffi::Function::register_global_typed(
+ "tirx_ir_guide_examples.rust.print",
+ print,
+ "Print an addition with a dispatch struct.",
+ )?;
tvm_ffi::Function::register_global_typed(
"tirx_ir_guide_examples.rust.count",
count,
diff --git a/examples/ir_guides/working_with_ir_in_rust.py
b/examples/ir_guides/working_with_ir_in_rust.py
index 6a0a341bc..de8d38176 100644
--- a/examples/ir_guides/working_with_ir_in_rust.py
+++ b/examples/ir_guides/working_with_ir_in_rust.py
@@ -50,6 +50,9 @@ assert list(tirx_ir_guide_examples.rust.casts()) == [
]
assert list(tirx_ir_guide_examples.rust.matching()) == ["((x * 2) + 1)",
"<other>"]
+print(tirx_ir_guide_examples.rust.print())
+assert tirx_ir_guide_examples.rust.print() == "(x + 1) + 2"
+
print(tirx_ir_guide_examples.rust.count())
assert tirx_ir_guide_examples.rust.count() == "variables: 1, additions: 1"