[Mlir-commits] [mlir] [mlir][transform] Allow zero-sized targets in expandTargetSpecification (PR #222949)

Stanislav Bardyuk llvmlistbot at llvm.org
Fri Sep 11 06:56:40 PDT 2026


https://github.com/kodlan created https://github.com/llvm/llvm-project/pull/222949

`transform.get_result %op[all]` (and `get_operand`) hand the payload op's result/operand count to `expandTargetSpecification`, which asserts that the count is positive. A payload op without results, like a call to a void
function, trips the assertion: 
```mlir
%0 = transform.structured.match ops{["func.call"]} in %arg0 : (!transform.any_op) -> !transform.any_op
%1 = transform.get_result %0[all] : (!transform.any_op) -> !transform.any_value
```

The same helper serves the Linalg matchers, so `match.structured.input %op[all]` on a generic without inputs and `match.structured.dim %op[all]` on a rank-0 generic abort the same way.

The body already does the right thing for a count of zero: `[all]` and `[except(...)]` give an empty list, and any explicit position produces the usual "position overflow" silenceable failure. Only the precondition is too strict, so relax it to non-negative and fix the doc comment. No caller changes: an empty position list simply contributes nothing to the
resulting handle, which matches how an empty target handle behaves. As a consequence qualified matchers like `match.structured.dim %op[all] reduction` succeed vacuously on a rank-0 op, which is the documented
reading of "all" (and what release builds already did). Two typos in the same doc comment are fixed on the way.

Tests: `get_result [all]` and `get_operand [all]` on ops without results/operands give an empty handle, `get_result [0]` and `[-1]` on an op without results are diagnosed, and the two Linalg matchers on a rank-0 generic without inputs match with empty lists.

Fixes #203440

>From 66be8ad07b586ef7a660533cbc21ead68c764c8e Mon Sep 17 00:00:00 2001
From: Stanislav Bardyuk <sbardyuk at google.com>
Date: Fri, 11 Sep 2026 13:52:54 +0000
Subject: [PATCH] [mlir][transform] Allow zero-sized targets in
 expandTargetSpecification

transform.get_result %op[all] (and get_operand) pass the payload op's
result/operand count to expandTargetSpecification, which asserted that
the count is positive. A payload op without results, such as a call to
a void function, tripped the assertion. The Linalg matchers
match.structured.dim/input/init share the helper, so a generic without
inputs or loops aborted the same way.

The body already handles a count of zero: [all] and [except(...)] give
an empty list and any explicit position produces the usual position
overflow or underflow failure. Relax the precondition to non-negative
and fix the doc comment.

Fixes #203440
---
 .../Transform/Interfaces/MatchInterfaces.h    |  6 +-
 .../Transform/Interfaces/MatchInterfaces.cpp  |  2 +-
 .../Dialect/Linalg/match-ops-interpreter.mlir | 50 ++++++++++++
 .../Dialect/Transform/test-interpreter.mlir   | 79 +++++++++++++++++++
 4 files changed, 133 insertions(+), 4 deletions(-)

diff --git a/mlir/include/mlir/Dialect/Transform/Interfaces/MatchInterfaces.h b/mlir/include/mlir/Dialect/Transform/Interfaces/MatchInterfaces.h
index 43abfbd6c070a..1896b35587355 100644
--- a/mlir/include/mlir/Dialect/Transform/Interfaces/MatchInterfaces.h
+++ b/mlir/include/mlir/Dialect/Transform/Interfaces/MatchInterfaces.h
@@ -201,13 +201,13 @@ LogicalResult verifyTransformMatchDimsOp(Operation *op, ArrayRef<int64_t> raw,
 /// Populates `result` with the positional identifiers relative to `maxNumber`.
 /// If `isAll` is set, the result will contain all numbers from `0` to
 /// `maxNumber - 1` inclusive regardless of `rawList`. Otherwise, negative
-/// values from `rawList` are  are interpreted as counting backwards from
-/// `maxNumber`, i.e., `-1` is interpreted a `maxNumber - 1`, while positive
+/// values from `rawList` are interpreted as counting backwards from
+/// `maxNumber`, i.e., `-1` is interpreted as `maxNumber - 1`, while positive
 /// numbers remain as is. If `isInverted` is set, populates `result` with those
 /// values from the `0` to `maxNumber - 1` inclusive range that don't appear in
 /// `rawList`. If `rawList` contains values that are greater than or equal to
 /// `maxNumber` or less than `-maxNumber`, produces a silenceable error at the
-/// given location. `maxNumber` must be positive. If `rawList` contains
+/// given location. `maxNumber` must be non-negative. If `rawList` contains
 /// duplicate numbers or numbers that become duplicate after negative value
 /// remapping, emits a silenceable error.
 DiagnosedSilenceableFailure
diff --git a/mlir/lib/Dialect/Transform/Interfaces/MatchInterfaces.cpp b/mlir/lib/Dialect/Transform/Interfaces/MatchInterfaces.cpp
index 38f0dcfa189ff..e95c5037b7b7e 100644
--- a/mlir/lib/Dialect/Transform/Interfaces/MatchInterfaces.cpp
+++ b/mlir/lib/Dialect/Transform/Interfaces/MatchInterfaces.cpp
@@ -104,7 +104,7 @@ LogicalResult transform::verifyTransformMatchDimsOp(Operation *op,
 DiagnosedSilenceableFailure transform::expandTargetSpecification(
     Location loc, bool isAll, bool isInverted, ArrayRef<int64_t> rawList,
     int64_t maxNumber, SmallVectorImpl<int64_t> &result) {
-  assert(maxNumber > 0 && "expected size to be positive");
+  assert(maxNumber >= 0 && "expected size to be non-negative");
   assert(!(isAll && isInverted) && "cannot invert all");
   if (isAll) {
     result = llvm::to_vector(llvm::seq<int64_t>(0, maxNumber));
diff --git a/mlir/test/Dialect/Linalg/match-ops-interpreter.mlir b/mlir/test/Dialect/Linalg/match-ops-interpreter.mlir
index 5b58a9464a76f..bb8dd37ab120c 100644
--- a/mlir/test/Dialect/Linalg/match-ops-interpreter.mlir
+++ b/mlir/test/Dialect/Linalg/match-ops-interpreter.mlir
@@ -1141,3 +1141,53 @@ module attributes { transform.target_tag = "start_here" } {
     return %result : tensor<8x32x32x16xf32>
   }
 }
+
+// -----
+
+// A rank-0 generic without inputs has zero loops and zero inputs; matching
+// [all] of them produces empty lists rather than failing.
+module attributes { transform.with_named_sequence } {
+  transform.named_sequence @match_empty(%arg0: !transform.any_op {transform.readonly})
+      -> (!transform.any_op, !transform.param<i64>, !transform.any_value) {
+    %outs:2 = transform.match.structured failures(propagate) %arg0
+      : (!transform.any_op) -> (!transform.param<i64>, !transform.any_value) {
+    ^bb0(%arg1: !transform.any_op):
+      %0 = transform.match.structured.dim %arg1[all] : (!transform.any_op) -> !transform.param<i64>
+      %1 = transform.match.structured.input %arg1[all] : (!transform.any_op) -> !transform.any_value
+      transform.match.structured.yield %0, %1 : !transform.param<i64>, !transform.any_value
+    }
+    transform.yield %arg0, %outs#0, %outs#1 : !transform.any_op, !transform.param<i64>, !transform.any_value
+  }
+
+  transform.named_sequence @print_empty(%arg0: !transform.any_op {transform.readonly},
+                                        %dims: !transform.param<i64> {transform.readonly},
+                                        %inputs: !transform.any_value {transform.readonly}) {
+    %nd = transform.num_associations %dims : (!transform.param<i64>) -> !transform.param<i64>
+    %ni = transform.num_associations %inputs : (!transform.any_value) -> !transform.param<i64>
+    transform.debug.emit_remark_at %arg0, "matched empty" : !transform.any_op
+    transform.debug.emit_param_as_remark %nd, "num dims" at %arg0 : !transform.param<i64>, !transform.any_op
+    transform.debug.emit_param_as_remark %ni, "num inputs" at %arg0 : !transform.param<i64>, !transform.any_op
+    transform.yield
+  }
+
+  transform.named_sequence @__transform_main(%arg0: !transform.any_op {transform.consumed}) {
+    transform.foreach_match in %arg0
+      @match_empty -> @print_empty
+      : (!transform.any_op) -> !transform.any_op
+    transform.yield
+  }
+
+  func.func @rank_zero_no_inputs(%out: tensor<f32>) -> tensor<f32>
+      attributes { transform.target_tag = "start_here" } {
+    // expected-remark @below {{matched empty}}
+    // expected-remark @below {{num dims 0}}
+    // expected-remark @below {{num inputs 0}}
+    %result = linalg.generic {indexing_maps = [affine_map<() -> ()>], iterator_types = []}
+        outs(%out : tensor<f32>) {
+    ^bb0(%o: f32):
+      %c = arith.constant 1.0 : f32
+      linalg.yield %c : f32
+    } -> tensor<f32>
+    return %result : tensor<f32>
+  }
+}
diff --git a/mlir/test/Dialect/Transform/test-interpreter.mlir b/mlir/test/Dialect/Transform/test-interpreter.mlir
index e05b7024e3876..4128a07ed848d 100644
--- a/mlir/test/Dialect/Transform/test-interpreter.mlir
+++ b/mlir/test/Dialect/Transform/test-interpreter.mlir
@@ -1795,6 +1795,85 @@ module attributes {transform.with_named_sequence} {
 
 // -----
 
+// A payload op without results contributes nothing to the value handle.
+
+func.func private @void_callee(i32)
+func.func @get_all_results_of_op_without_results(%arg0: i32) {
+  call @void_callee(%arg0) : (i32) -> ()
+  return
+}
+
+module attributes {transform.with_named_sequence} {
+  transform.named_sequence @__transform_main(%arg1: !transform.any_op) {
+    %call = transform.structured.match ops{["func.call"]} in %arg1 : (!transform.any_op) -> !transform.any_op
+    %results = transform.get_result %call[all] : (!transform.any_op) -> !transform.any_value
+    %p = transform.num_associations %results : (!transform.any_value) -> !transform.param<i64>
+    // expected-remark @below {{0}}
+    transform.debug.emit_param_as_remark %p : !transform.param<i64>
+    transform.yield
+  }
+}
+
+// -----
+
+func.func private @void_callee(i32)
+func.func @get_result_of_op_without_results(%arg0: i32) {
+  // expected-note @below {{while considering positions of this payload operation}}
+  call @void_callee(%arg0) : (i32) -> ()
+  return
+}
+
+module attributes {transform.with_named_sequence} {
+  transform.named_sequence @__transform_main(%arg1: !transform.any_op) {
+    %call = transform.structured.match ops{["func.call"]} in %arg1 : (!transform.any_op) -> !transform.any_op
+    // expected-error @below {{position overflow 0 (updated from 0) for maximum 0}}
+    %result = transform.get_result %call[0] : (!transform.any_op) -> !transform.any_value
+    transform.debug.emit_remark_at %result, "call result" : !transform.any_value
+    transform.yield
+  }
+}
+
+// -----
+
+func.func private @void_callee(i32)
+func.func @get_last_result_of_op_without_results(%arg0: i32) {
+  // expected-note @below {{while considering positions of this payload operation}}
+  call @void_callee(%arg0) : (i32) -> ()
+  return
+}
+
+module attributes {transform.with_named_sequence} {
+  transform.named_sequence @__transform_main(%arg1: !transform.any_op) {
+    %call = transform.structured.match ops{["func.call"]} in %arg1 : (!transform.any_op) -> !transform.any_op
+    // expected-error @below {{position underflow -1 (updated from -1)}}
+    %result = transform.get_result %call[-1] : (!transform.any_op) -> !transform.any_value
+    transform.debug.emit_remark_at %result, "call result" : !transform.any_value
+    transform.yield
+  }
+}
+
+// -----
+
+// A payload op without operands contributes nothing to the value handle.
+
+func.func @get_all_operands_of_op_without_operands() -> index {
+  %c = arith.constant 0 : index
+  return %c : index
+}
+
+module attributes {transform.with_named_sequence} {
+  transform.named_sequence @__transform_main(%arg1: !transform.any_op) {
+    %cst = transform.structured.match ops{["arith.constant"]} in %arg1 : (!transform.any_op) -> !transform.any_op
+    %operands = transform.get_operand %cst[all] : (!transform.any_op) -> !transform.any_value
+    %p = transform.num_associations %operands : (!transform.any_value) -> !transform.param<i64>
+    // expected-remark @below {{0}}
+    transform.debug.emit_param_as_remark %p : !transform.param<i64>
+    transform.yield
+  }
+}
+
+// -----
+
 module @named_inclusion attributes { transform.with_named_sequence } {
 
   transform.named_sequence @foo(%arg0: !transform.any_op {transform.readonly}) -> () {



More information about the Mlir-commits mailing list