[llvm-branch-commits] [mlir] [mlir-c] Add 1:N TypeConverter conversion and materialization bindings (PR #208935)
Maksim Levental via llvm-branch-commits
llvm-branch-commits at lists.llvm.org
Sat Jul 18 11:41:45 PDT 2026
https://github.com/makslevental updated https://github.com/llvm/llvm-project/pull/208935
>From abb0b4ec86a0d6c5217d0211419b4b09863c500a Mon Sep 17 00:00:00 2001
From: makslevental <maksim.levental at gmail.com>
Date: Tue, 30 Jun 2026 11:48:08 -0700
Subject: [PATCH 1/6] [mlir-c] Add 1:N TypeConverter conversion and
materialization bindings
Builds on the source/target materialization C bindings:
- Target materialization callbacks now receive `originalType` (split from the
previously-shared source/target callback typedef), exposing a documented C++
capability that was otherwise unreachable from C.
- 1:N type conversion: `mlirTypeConverterAdd1ToNConversion` plus an opaque
results accumulator (`MlirTypeConverterConversionResults` /
`mlirTypeConverterConversionResultsAppend`). A declining callback's appended
types are rolled back so the driver's "try the next conversion" invariant
holds.
- 1:N target materialization: `mlirTypeConverterAdd1ToNTargetMaterialization`,
whose callback fills a caller-allocated `outputs` buffer. A success that
leaves any output null is treated as a decline rather than handing the driver
a null-containing result.
- `mlirConversionPatternRewriterReplaceOpWithMultiple` for 1:N value
replacement, which can drive a source materialization with nInputs > 1.
- An optional `matchAndRewrite1ToN` callback on `MlirConversionPatternCallbacks`
for patterns consuming 1:N-remapped operands; when null the driver falls back
to the 1:1 dispatch.
Adds end-to-end tests in mlir/test/CAPI/rewrite.c covering originalType
observation, source/target materialization decline paths, 1:N conversion
(including append-then-decline rollback and erasure), 1:N target
materialization (including the unfilled-outputs decline), multi-input source
materialization, multi-range replaceOpWithMultiple, and the 1:1-only-pattern
vs 1:N-operand failure.
---
mlir/include/mlir-c/Rewrite.h | 108 +++-
mlir/lib/CAPI/Transforms/Rewrite.cpp | 167 +++++-
mlir/test/CAPI/rewrite.c | 733 ++++++++++++++++++++++++++-
3 files changed, 987 insertions(+), 21 deletions(-)
diff --git a/mlir/include/mlir-c/Rewrite.h b/mlir/include/mlir-c/Rewrite.h
index 57cb979de012a..fb859bd3fdab8 100644
--- a/mlir/include/mlir-c/Rewrite.h
+++ b/mlir/include/mlir-c/Rewrite.h
@@ -501,6 +501,17 @@ mlirConversionPatternRewriterConvertRegionTypes(
MlirConversionPatternRewriter rewriter, MlirRegion region,
MlirTypeConverter typeConverter);
+/// Replace the given operation with multiple value ranges -- one range per
+/// result of `op` -- and erase it. `nRanges` must equal the number of results
+/// of `op`. `rangeSizes[i]` is the number of values in the i-th range, and
+/// `values` is the flat concatenation of all ranges (its length is the sum of
+/// `rangeSizes[0..nRanges)`). This is the 1:N replacement form; surviving type
+/// mismatches between a result and its range are reconciled by the driver with
+/// source/target materializations.
+MLIR_CAPI_EXPORTED void mlirConversionPatternRewriterReplaceOpWithMultiple(
+ MlirConversionPatternRewriter rewriter, MlirOperation op, intptr_t nRanges,
+ intptr_t *rangeSizes, MlirValue *values);
+
//===----------------------------------------------------------------------===//
/// ConversionTarget API
//===----------------------------------------------------------------------===//
@@ -616,32 +627,106 @@ mlirTypeConverterAddConversion(MlirTypeConverter typeConverter,
MlirTypeConverterConversionCallback convertType,
void *userData);
-/// Convert the given type using the given TypeConverter.
+/// Opaque accumulator for the result types of a 1:N type conversion. It is
+/// passed to a MlirTypeConverter1ToNConversionCallback, which appends converted
+/// types to it via mlirTypeConverterConversionResultsAppend.
+typedef struct MlirTypeConverterConversionResults {
+ void *ptr;
+} MlirTypeConverterConversionResults;
+
+/// Append a converted result type to the given 1:N conversion result
+/// accumulator.
+MLIR_CAPI_EXPORTED void mlirTypeConverterConversionResultsAppend(
+ MlirTypeConverterConversionResults results, MlirType type);
+
+/// Callback type for 1:N type conversion functions. For the given `type`, the
+/// callback appends zero or more converted result types to `results` (via
+/// mlirTypeConverterConversionResultsAppend) and returns success. Returning
+/// failure leaves the type unconverted and allows another conversion function
+/// to be tried; any types appended before returning failure are discarded.
+/// Appending a single type is a 1:1 conversion; appending several is a 1:N
+/// conversion; appending none (on success) erases the type.
+typedef MlirLogicalResult (*MlirTypeConverter1ToNConversionCallback)(
+ MlirType type, MlirTypeConverterConversionResults results, void *userData);
+
+/// Add a 1:N type conversion function to the given TypeConverter.
+MLIR_CAPI_EXPORTED void mlirTypeConverterAdd1ToNConversion(
+ MlirTypeConverter typeConverter,
+ MlirTypeConverter1ToNConversionCallback convertType, void *userData);
+
+/// Convert the given type using the given TypeConverter. This is the 1:1
+/// convenience form: it returns the single converted type, or a null MlirType
+/// on failure or if the type converts to anything other than exactly one type
+/// (e.g. a 1:N conversion registered via mlirTypeConverterAdd1ToNConversion, or
+/// an erasure to zero types).
MLIR_CAPI_EXPORTED MlirType
mlirTypeConverterConvertType(MlirTypeConverter typeConverter, MlirType type);
-/// Callback type for type materializations. Given a builder (passed as a
+/// Callback type for source materializations. Given a builder (passed as a
/// rewriter), the desired output type, the input values, and a location, the
/// callback must build a cast-like operation that produces a single value of
/// `outputType` and return it. Returning a null MlirValue indicates failure, in
/// which case another registered materialization may be attempted.
-typedef MlirValue (*MlirTypeConverterMaterializationCallback)(
+///
+/// Note: Source materializations are single-output -- the callback returns one
+/// value of `outputType`. (The 1:N, multiple-output form exists only for target
+/// materializations; see MlirTypeConverter1ToNTargetMaterializationCallback.)
+/// `nInputs` may be greater than one when several values are mapped back to a
+/// single value, e.g. after a 1:N replacement via
+/// mlirConversionPatternRewriterReplaceOpWithMultiple.
+typedef MlirValue (*MlirTypeConverterSourceMaterializationCallback)(
MlirRewriterBase rewriter, MlirType outputType, intptr_t nInputs,
MlirValue *inputs, MlirLocation loc, void *userData);
+/// Callback type for 1:1 target materializations. Behaves like
+/// MlirTypeConverterSourceMaterializationCallback, but additionally receives
+/// `originalType`: the original type of the SSA value being materialized.
+///
+/// `originalType` may differ from `outputType` and cannot, in general, be
+/// recovered from `outputType` and `inputs`, which is why it is passed
+/// explicitly (see TypeConverter::addTargetMaterialization in
+/// DialectConversion.h for the full rationale). It may be a null MlirType when
+/// no original type is available.
+///
+/// Note: This callback is single-output. For the 1:N (multiple-output) form,
+/// use MlirTypeConverter1ToNTargetMaterializationCallback.
+typedef MlirValue (*MlirTypeConverterTargetMaterializationCallback)(
+ MlirRewriterBase rewriter, MlirType outputType, intptr_t nInputs,
+ MlirValue *inputs, MlirLocation loc, MlirType originalType, void *userData);
+
/// Register a source materialization with the given TypeConverter. This is
/// invoked when a replacement value must be converted back to its original
/// source type because some uses persist beyond the main conversion.
MLIR_CAPI_EXPORTED void mlirTypeConverterAddSourceMaterialization(
MlirTypeConverter typeConverter,
- MlirTypeConverterMaterializationCallback callback, void *userData);
+ MlirTypeConverterSourceMaterializationCallback callback, void *userData);
/// Register a target materialization with the given TypeConverter. This is
/// invoked when a value must be converted to a target type according to a
/// pattern's type converter.
MLIR_CAPI_EXPORTED void mlirTypeConverterAddTargetMaterialization(
MlirTypeConverter typeConverter,
- MlirTypeConverterMaterializationCallback callback, void *userData);
+ MlirTypeConverterTargetMaterializationCallback callback, void *userData);
+
+/// Callback type for 1:N target materializations. Like
+/// MlirTypeConverterTargetMaterializationCallback, but produces a value for
+/// each of the `nOutputTypes` requested output types instead of a single value.
+/// The callback must fill `outputs` -- a caller-allocated array of length
+/// `nOutputTypes` -- with that many values and return success. Returning
+/// failure signals that this materialization declined (so another may be
+/// attempted); in that case `outputs` is ignored. A success that leaves any of
+/// `outputs` null is likewise treated as a decline. `originalType` carries the
+/// original type of the value being materialized and may be a null MlirType.
+typedef MlirLogicalResult (*MlirTypeConverter1ToNTargetMaterializationCallback)(
+ MlirRewriterBase rewriter, intptr_t nOutputTypes, MlirType *outputTypes,
+ intptr_t nInputs, MlirValue *inputs, MlirLocation loc,
+ MlirType originalType, MlirValue *outputs, void *userData);
+
+/// Register a 1:N target materialization with the given TypeConverter.
+MLIR_CAPI_EXPORTED void mlirTypeConverterAdd1ToNTargetMaterialization(
+ MlirTypeConverter typeConverter,
+ MlirTypeConverter1ToNTargetMaterializationCallback callback,
+ void *userData);
//===----------------------------------------------------------------------===//
/// ConversionPattern API
@@ -662,6 +747,19 @@ typedef struct {
MlirValue *operands,
MlirConversionPatternRewriter rewriter,
void *userData);
+ /// Optional callback corresponding to the 1:N
+ /// ConversionPattern::matchAndRewrite(Operation *, ArrayRef<ValueRange>, ...)
+ /// overload, used when one or more operands are remapped to several values
+ /// (e.g. under a 1:N type conversion). `operands` is the flat concatenation
+ /// of all operand ranges; there are `nRanges` ranges (one per original
+ /// operand) and `rangeSizes[i]` is the number of values in the i-th range.
+ /// When this is non-null it takes precedence; when null, the driver falls
+ /// back to the 1:1 `matchAndRewrite` above, which fails to match if any
+ /// operand was remapped 1:N.
+ MlirLogicalResult (*matchAndRewrite1ToN)(
+ MlirConversionPattern pattern, MlirOperation op, intptr_t nRanges,
+ intptr_t *rangeSizes, intptr_t nOperands, MlirValue *operands,
+ MlirConversionPatternRewriter rewriter, void *userData);
} MlirConversionPatternCallbacks;
/// Create a conversion pattern that matches the operation with the given
diff --git a/mlir/lib/CAPI/Transforms/Rewrite.cpp b/mlir/lib/CAPI/Transforms/Rewrite.cpp
index e324a848691e9..0bf2e80dddb0c 100644
--- a/mlir/lib/CAPI/Transforms/Rewrite.cpp
+++ b/mlir/lib/CAPI/Transforms/Rewrite.cpp
@@ -543,6 +543,22 @@ MlirLogicalResult mlirConversionPatternRewriterConvertRegionTypes(
*unwrap(typeConverter)));
}
+void mlirConversionPatternRewriterReplaceOpWithMultiple(
+ MlirConversionPatternRewriter rewriter, MlirOperation op, intptr_t nRanges,
+ intptr_t *rangeSizes, MlirValue *values) {
+ SmallVector<SmallVector<Value>> ranges;
+ ranges.reserve(nRanges);
+ MlirValue *cur = values;
+ for (intptr_t i = 0; i < nRanges; ++i) {
+ SmallVector<Value> range;
+ range.reserve(rangeSizes[i]);
+ for (intptr_t j = 0; j < rangeSizes[i]; ++j, ++cur)
+ range.push_back(unwrap(*cur));
+ ranges.push_back(std::move(range));
+ }
+ unwrap(rewriter)->replaceOpWithMultiple(unwrap(op), std::move(ranges));
+}
+
//===----------------------------------------------------------------------===//
/// ConversionTarget API
//===----------------------------------------------------------------------===//
@@ -672,25 +688,59 @@ void mlirTypeConverterAddConversion(
});
}
+void mlirTypeConverterConversionResultsAppend(
+ MlirTypeConverterConversionResults results, MlirType type) {
+ static_cast<SmallVectorImpl<Type> *>(results.ptr)->push_back(unwrap(type));
+}
+
+void mlirTypeConverterAdd1ToNConversion(
+ MlirTypeConverter typeConverter,
+ MlirTypeConverter1ToNConversionCallback convertType, void *userData) {
+ unwrap(typeConverter)
+ ->addConversion(
+ [convertType, userData](Type type, SmallVectorImpl<Type> &results)
+ -> std::optional<LogicalResult> {
+ size_t numPriorResults = results.size();
+ MlirTypeConverterConversionResults wrappedResults{&results};
+ MlirLogicalResult result =
+ convertType(wrap(type), wrappedResults, userData);
+ if (mlirLogicalResultIsFailure(result)) {
+ // The callback declined. Restore any types it appended so the
+ // driver's "try the next conversion" invariant holds (a declining
+ // conversion function must not mutate `results`).
+ results.truncate(numPriorResults);
+ return std::nullopt;
+ }
+ return success();
+ });
+}
+
MlirType mlirTypeConverterConvertType(MlirTypeConverter typeConverter,
MlirType type) {
return wrap(unwrap(typeConverter)->convertType(unwrap(type)));
}
namespace {
-/// Wraps a C materialization callback as a C++ materialization callback of the
-/// form `Value(OpBuilder &, Type, ValueRange, Location)`, shared by both source
-/// and target materializations. The builder is always a RewriterBase in the
-/// conversion driver, so it is safe to expose it as an MlirRewriterBase.
+/// Marshals the `inputs` ValueRange into wrapped MlirValues. Shared by the
+/// materialization wrappers below.
+SmallVector<MlirValue> wrapInputs(ValueRange inputs) {
+ SmallVector<MlirValue> wrappedInputs;
+ wrappedInputs.reserve(inputs.size());
+ for (Value v : inputs)
+ wrappedInputs.push_back(wrap(v));
+ return wrappedInputs;
+}
+
+/// Wraps a C source materialization callback as the C++ form
+/// `Value(OpBuilder &, Type, ValueRange, Location)`. The builder is always a
+/// RewriterBase in the conversion driver, so it is safe to expose it as an
+/// MlirRewriterBase.
std::function<Value(OpBuilder &, Type, ValueRange, Location)>
-wrapMaterializationCallback(MlirTypeConverterMaterializationCallback callback,
- void *userData) {
+wrapSourceMaterializationCallback(
+ MlirTypeConverterSourceMaterializationCallback callback, void *userData) {
return [callback, userData](OpBuilder &builder, Type type, ValueRange inputs,
Location loc) -> Value {
- SmallVector<MlirValue> wrappedInputs;
- wrappedInputs.reserve(inputs.size());
- for (Value v : inputs)
- wrappedInputs.push_back(wrap(v));
+ SmallVector<MlirValue> wrappedInputs = wrapInputs(inputs);
MlirValue result =
callback(wrap(static_cast<RewriterBase *>(&builder)), wrap(type),
static_cast<intptr_t>(wrappedInputs.size()),
@@ -698,24 +748,91 @@ wrapMaterializationCallback(MlirTypeConverterMaterializationCallback callback,
return mlirValueIsNull(result) ? Value() : unwrap(result);
};
}
+
+/// Wraps a C 1:1 target materialization callback as the C++ form
+/// `Value(OpBuilder &, Type, ValueRange, Location, Type)`. Selecting the
+/// 5-argument C++ overload (the one carrying `originalType`) is what makes the
+/// original type observable from C.
+std::function<Value(OpBuilder &, Type, ValueRange, Location, Type)>
+wrapTargetMaterializationCallback(
+ MlirTypeConverterTargetMaterializationCallback callback, void *userData) {
+ return [callback, userData](OpBuilder &builder, Type type, ValueRange inputs,
+ Location loc, Type originalType) -> Value {
+ SmallVector<MlirValue> wrappedInputs = wrapInputs(inputs);
+ MlirValue result =
+ callback(wrap(static_cast<RewriterBase *>(&builder)), wrap(type),
+ static_cast<intptr_t>(wrappedInputs.size()),
+ wrappedInputs.data(), wrap(loc), wrap(originalType), userData);
+ return mlirValueIsNull(result) ? Value() : unwrap(result);
+ };
+}
+
+/// Wraps a C 1:N target materialization callback as the C++ form
+/// `SmallVector<Value>(OpBuilder &, TypeRange, ValueRange, Location, Type)`.
+std::function<SmallVector<Value>(OpBuilder &, TypeRange, ValueRange, Location,
+ Type)>
+wrap1ToNTargetMaterializationCallback(
+ MlirTypeConverter1ToNTargetMaterializationCallback callback,
+ void *userData) {
+ return [callback, userData](OpBuilder &builder, TypeRange outputTypes,
+ ValueRange inputs, Location loc,
+ Type originalType) -> SmallVector<Value> {
+ SmallVector<MlirType> wrappedOutputTypes;
+ wrappedOutputTypes.reserve(outputTypes.size());
+ for (Type t : outputTypes)
+ wrappedOutputTypes.push_back(wrap(t));
+ SmallVector<MlirValue> wrappedInputs = wrapInputs(inputs);
+ SmallVector<MlirValue> wrappedOutputs(outputTypes.size(),
+ MlirValue{nullptr});
+ MlirLogicalResult result = callback(
+ wrap(static_cast<RewriterBase *>(&builder)),
+ static_cast<intptr_t>(wrappedOutputTypes.size()),
+ wrappedOutputTypes.data(), static_cast<intptr_t>(wrappedInputs.size()),
+ wrappedInputs.data(), wrap(loc), wrap(originalType),
+ wrappedOutputs.data(), userData);
+ if (mlirLogicalResultIsFailure(result))
+ return {}; // declined; another materialization may be attempted
+ SmallVector<Value> outputs;
+ outputs.reserve(wrappedOutputs.size());
+ for (MlirValue v : wrappedOutputs) {
+ // A success that left any output unfilled is an incomplete
+ // materialization. Treat it as a decline rather than returning a
+ // null-containing result (which would trip an assert in the driver).
+ if (mlirValueIsNull(v))
+ return {};
+ outputs.push_back(unwrap(v));
+ }
+ return outputs;
+ };
+}
} // namespace
void mlirTypeConverterAddSourceMaterialization(
MlirTypeConverter typeConverter,
- MlirTypeConverterMaterializationCallback callback, void *userData) {
+ MlirTypeConverterSourceMaterializationCallback callback, void *userData) {
assert(callback && "expected non-null materialization callback");
unwrap(typeConverter)
->addSourceMaterialization(
- wrapMaterializationCallback(callback, userData));
+ wrapSourceMaterializationCallback(callback, userData));
}
void mlirTypeConverterAddTargetMaterialization(
MlirTypeConverter typeConverter,
- MlirTypeConverterMaterializationCallback callback, void *userData) {
+ MlirTypeConverterTargetMaterializationCallback callback, void *userData) {
assert(callback && "expected non-null materialization callback");
unwrap(typeConverter)
->addTargetMaterialization(
- wrapMaterializationCallback(callback, userData));
+ wrapTargetMaterializationCallback(callback, userData));
+}
+
+void mlirTypeConverterAdd1ToNTargetMaterialization(
+ MlirTypeConverter typeConverter,
+ MlirTypeConverter1ToNTargetMaterializationCallback callback,
+ void *userData) {
+ assert(callback && "expected non-null materialization callback");
+ unwrap(typeConverter)
+ ->addTargetMaterialization(
+ wrap1ToNTargetMaterializationCallback(callback, userData));
}
//===----------------------------------------------------------------------===//
@@ -755,6 +872,28 @@ class ExternalConversionPattern : public mlir::ConversionPattern {
userData));
}
+ LogicalResult
+ matchAndRewrite(Operation *op, ArrayRef<ValueRange> operands,
+ ConversionPatternRewriter &rewriter) const override {
+ // Without a 1:N callback, defer to the default behavior, which dispatches
+ // to the 1:1 matchAndRewrite above or fails to match on a 1:N mapping.
+ if (!callbacks.matchAndRewrite1ToN)
+ return dispatchTo1To1(*this, op, operands, rewriter);
+ SmallVector<intptr_t> rangeSizes;
+ rangeSizes.reserve(operands.size());
+ std::vector<MlirValue> wrappedOperands;
+ for (ValueRange range : operands) {
+ rangeSizes.push_back(static_cast<intptr_t>(range.size()));
+ for (Value val : range)
+ wrappedOperands.push_back(wrap(val));
+ }
+ return unwrap(callbacks.matchAndRewrite1ToN(
+ wrap(static_cast<const mlir::ConversionPattern *>(this)), wrap(op),
+ static_cast<intptr_t>(rangeSizes.size()), rangeSizes.data(),
+ static_cast<intptr_t>(wrappedOperands.size()), wrappedOperands.data(),
+ wrap(&rewriter), userData));
+ }
+
private:
MlirConversionPatternCallbacks callbacks;
void *userData;
diff --git a/mlir/test/CAPI/rewrite.c b/mlir/test/CAPI/rewrite.c
index 6615364825e95..441e4759a24e2 100644
--- a/mlir/test/CAPI/rewrite.c
+++ b/mlir/test/CAPI/rewrite.c
@@ -860,7 +860,178 @@ static MlirValue buildCastMaterialization(MlirRewriterBase rewriter,
return mlirOperationGetResult(castOp, 0);
}
-// Conversion pattern for `test.source`: replaces it with a `test.source_i64`
+// Source materialization callback that always declines (returns a null value)
+// and records that it was consulted. Used to exercise the "this materialization
+// declined, try the next one" fallback path.
+static MlirValue
+declineSourceMaterialization(MlirRewriterBase rewriter, MlirType outputType,
+ intptr_t nInputs, MlirValue *inputs,
+ MlirLocation loc, void *userData) {
+ (void)rewriter;
+ (void)outputType;
+ (void)nInputs;
+ (void)inputs;
+ (void)loc;
+ intptr_t *declined = (intptr_t *)userData;
+ if (declined)
+ (*declined)++;
+ return (MlirValue){NULL};
+}
+
+// Source materialization callback that records the number of inputs it was
+// invoked with (into userData) before building the `test.cast`. Used to verify
+// that a 1:N replacement drives a source materialization with nInputs > 1.
+static MlirValue buildSourceCastRecordInputs(MlirRewriterBase rewriter,
+ MlirType outputType,
+ intptr_t nInputs,
+ MlirValue *inputs,
+ MlirLocation loc, void *userData) {
+ intptr_t *observedInputs = (intptr_t *)userData;
+ if (observedInputs)
+ *observedInputs = nInputs;
+ MlirOperationState state =
+ mlirOperationStateGet(mlirStringRefCreateFromCString("test.cast"), loc);
+ mlirOperationStateAddOperands(&state, nInputs, inputs);
+ mlirOperationStateAddResults(&state, 1, &outputType);
+ MlirOperation castOp = mlirOperationCreate(&state);
+ mlirRewriterBaseInsert(rewriter, castOp);
+ return mlirOperationGetResult(castOp, 0);
+}
+
+// 1:1 target materialization callback. Builds a `test.cast` like
+// buildCastMaterialization, but additionally inspects `originalType`: the
+// counter passed as userData is only bumped when `originalType` is the expected
+// original (i32) type, so a passing test proves the original type is observable
+// from C.
+static MlirValue buildTargetCast(MlirRewriterBase rewriter, MlirType outputType,
+ intptr_t nInputs, MlirValue *inputs,
+ MlirLocation loc, MlirType originalType,
+ void *userData) {
+ intptr_t *counter = (intptr_t *)userData;
+ if (counter && mlirTypeIsAInteger(originalType) &&
+ mlirIntegerTypeGetWidth(originalType) == 32)
+ (*counter)++;
+ MlirOperationState state =
+ mlirOperationStateGet(mlirStringRefCreateFromCString("test.cast"), loc);
+ mlirOperationStateAddOperands(&state, nInputs, inputs);
+ mlirOperationStateAddResults(&state, 1, &outputType);
+ MlirOperation castOp = mlirOperationCreate(&state);
+ mlirRewriterBaseInsert(rewriter, castOp);
+ return mlirOperationGetResult(castOp, 0);
+}
+
+// 1:N type conversion callback: maps i32 -> (i16, i16) and leaves every other
+// type unchanged (1:1 identity).
+static MlirLogicalResult splitI32(MlirType type,
+ MlirTypeConverterConversionResults results,
+ void *userData) {
+ (void)userData;
+ if (mlirTypeIsAInteger(type) && mlirIntegerTypeGetWidth(type) == 32) {
+ MlirType i16 = mlirIntegerTypeGet(mlirTypeGetContext(type), 16);
+ mlirTypeConverterConversionResultsAppend(results, i16);
+ mlirTypeConverterConversionResultsAppend(results, i16);
+ } else {
+ mlirTypeConverterConversionResultsAppend(results, type);
+ }
+ return mlirLogicalResultSuccess();
+}
+
+// 1:N type conversion callback: erases i32 (converts it to zero types) and
+// leaves every other type unchanged (1:1 identity).
+static MlirLogicalResult eraseI32(MlirType type,
+ MlirTypeConverterConversionResults results,
+ void *userData) {
+ (void)userData;
+ if (mlirTypeIsAInteger(type) && mlirIntegerTypeGetWidth(type) == 32)
+ return mlirLogicalResultSuccess(); // append nothing -> erase
+ mlirTypeConverterConversionResultsAppend(results, type);
+ return mlirLogicalResultSuccess();
+}
+
+// 1:N type conversion callback: builds one `test.cast` per requested
+// output type, each consuming the (single) input, and fills `outputs`
+// accordingly. Bumps the counter passed as userData.
+static MlirLogicalResult buildSplitCast(MlirRewriterBase rewriter,
+ intptr_t nOutputTypes,
+ MlirType *outputTypes, intptr_t nInputs,
+ MlirValue *inputs, MlirLocation loc,
+ MlirType originalType,
+ MlirValue *outputs, void *userData) {
+ intptr_t *counter = (intptr_t *)userData;
+ // The original type must be the i32 operand type; it cannot be recovered from
+ // the (i16) output types alone, so observing it here proves it is propagated.
+ if (counter && mlirTypeIsAInteger(originalType) &&
+ mlirIntegerTypeGetWidth(originalType) == 32)
+ (*counter)++;
+ for (intptr_t i = 0; i < nOutputTypes; ++i) {
+ MlirOperationState state =
+ mlirOperationStateGet(mlirStringRefCreateFromCString("test.cast"), loc);
+ mlirOperationStateAddOperands(&state, nInputs, inputs);
+ mlirOperationStateAddResults(&state, 1, &outputTypes[i]);
+ MlirOperation castOp = mlirOperationCreate(&state);
+ mlirRewriterBaseInsert(rewriter, castOp);
+ outputs[i] = mlirOperationGetResult(castOp, 0);
+ }
+ return mlirLogicalResultSuccess();
+}
+
+// 1:N type conversion callback that appends bogus result types and then
+// declines (returns failure). The binding must roll back the appended types so
+// they do not pollute a subsequently-tried conversion function.
+static MlirLogicalResult appendThenDeclineConversion(
+ MlirType type, MlirTypeConverterConversionResults results, void *userData) {
+ intptr_t *counter = (intptr_t *)userData;
+ if (counter)
+ (*counter)++;
+ MlirType i8 = mlirIntegerTypeGet(mlirTypeGetContext(type), 8);
+ // Append more (and differently-typed) entries than the real conversion would,
+ // so any leak is observable as a wrong type/arity downstream.
+ mlirTypeConverterConversionResultsAppend(results, i8);
+ mlirTypeConverterConversionResultsAppend(results, i8);
+ mlirTypeConverterConversionResultsAppend(results, i8);
+ return mlirLogicalResultFailure();
+}
+
+// 1:N target materialization that always declines (returns failure) and records
+// that it was consulted. Exercises the "decline, try the next" fallback.
+static MlirLogicalResult declineTargetMaterialization(
+ MlirRewriterBase rewriter, intptr_t nOutputTypes, MlirType *outputTypes,
+ intptr_t nInputs, MlirValue *inputs, MlirLocation loc,
+ MlirType originalType, MlirValue *outputs, void *userData) {
+ (void)rewriter;
+ (void)nOutputTypes;
+ (void)outputTypes;
+ (void)nInputs;
+ (void)inputs;
+ (void)loc;
+ (void)originalType;
+ (void)outputs;
+ intptr_t *counter = (intptr_t *)userData;
+ if (counter)
+ (*counter)++;
+ return mlirLogicalResultFailure();
+}
+
+// 1:N target materialization that returns success but leaves `outputs`
+// unfilled (all null). The binding must treat this incomplete fill as a decline
+// rather than handing the driver a null-containing result.
+static MlirLogicalResult partialTargetMaterialization(
+ MlirRewriterBase rewriter, intptr_t nOutputTypes, MlirType *outputTypes,
+ intptr_t nInputs, MlirValue *inputs, MlirLocation loc,
+ MlirType originalType, MlirValue *outputs, void *userData) {
+ (void)rewriter;
+ (void)nOutputTypes;
+ (void)outputTypes;
+ (void)nInputs;
+ (void)inputs;
+ (void)loc;
+ (void)originalType;
+ (void)outputs;
+ intptr_t *counter = (intptr_t *)userData;
+ if (counter)
+ (*counter)++;
+ return mlirLogicalResultSuccess();
+}
// op whose result has the widened (i64) type. Because the original result type
// (i32) differs from the replacement type (i64), persisting uses force the
// framework to insert a source materialization.
@@ -946,6 +1117,12 @@ void testTypeConverterSourceMaterialization(MlirContext ctx) {
intptr_t materializationCounter = 0;
mlirTypeConverterAddSourceMaterialization(converter, buildCastMaterialization,
&materializationCounter);
+ // Register a second materialization that always declines. Because
+ // materializations are tried most-recently-added first, this one runs first,
+ // returns null, and the framework must fall back to buildCastMaterialization.
+ intptr_t declinedCounter = 0;
+ mlirTypeConverterAddSourceMaterialization(
+ converter, declineSourceMaterialization, &declinedCounter);
MlirRewritePatternSet patterns = mlirRewritePatternSetCreate(ctx);
MlirConversionPatternCallbacks callbacks = {NULL, NULL, convertSource};
@@ -974,6 +1151,8 @@ void testTypeConverterSourceMaterialization(MlirContext ctx) {
assert(mlirLogicalResultIsSuccess(result));
assert(materializationCounter > 0 &&
"source materialization callback must be invoked");
+ assert(declinedCounter > 0 &&
+ "declining materialization must be consulted before the fallback");
mlirOperationDump(moduleOp);
// clang-format off
@@ -1019,6 +1198,32 @@ static MlirLogicalResult convertConsumer(MlirConversionPattern pattern,
return mlirLogicalResultSuccess();
}
+// 1:N conversion pattern for `test.consumer`: builds `test.consumer_legal` from
+// the flattened remapped operands. Under the i32 -> (i16, i16) conversion the
+// single operand is remapped to two values, so this is invoked via the 1:N
+// matchAndRewrite hook with nOperands == 2.
+static MlirLogicalResult
+convertConsumer1ToN(MlirConversionPattern pattern, MlirOperation op,
+ intptr_t nRanges, intptr_t *rangeSizes, intptr_t nOperands,
+ MlirValue *operands, MlirConversionPatternRewriter rewriter,
+ void *userData) {
+ (void)pattern;
+ (void)nRanges;
+ (void)rangeSizes;
+ (void)userData;
+ MlirLocation loc = mlirOperationGetLocation(op);
+ MlirOperationState state = mlirOperationStateGet(
+ mlirStringRefCreateFromCString("test.consumer_legal"), loc);
+ mlirOperationStateAddOperands(&state, nOperands, operands);
+ MlirOperation newOp = mlirOperationCreate(&state);
+
+ MlirRewriterBase base = mlirPatternRewriterAsBase(
+ mlirConversionPatternRewriterAsPatternRewriter(rewriter));
+ mlirRewriterBaseInsert(base, newOp);
+ mlirRewriterBaseEraseOp(base, op);
+ return mlirLogicalResultSuccess();
+}
+
void testTypeConverterTargetMaterialization(MlirContext ctx) {
// CHECK-LABEL: @testTypeConverterTargetMaterialization
fprintf(stderr, "@testTypeConverterTargetMaterialization\n");
@@ -1035,7 +1240,7 @@ void testTypeConverterTargetMaterialization(MlirContext ctx) {
MlirTypeConverter converter = mlirTypeConverterCreate();
mlirTypeConverterAddConversion(converter, widenI32ToI64, NULL);
intptr_t materializationCounter = 0;
- mlirTypeConverterAddTargetMaterialization(converter, buildCastMaterialization,
+ mlirTypeConverterAddTargetMaterialization(converter, buildTargetCast,
&materializationCounter);
MlirRewritePatternSet patterns = mlirRewritePatternSetCreate(ctx);
@@ -1085,6 +1290,523 @@ void testTypeConverterTargetMaterialization(MlirContext ctx) {
fprintf(stderr, "testTypeConverterTargetMaterialization: PASSED\n");
}
+void testTypeConverter1ToNTargetMaterialization(MlirContext ctx) {
+ // CHECK-LABEL: @testTypeConverter1ToNTargetMaterialization
+ fprintf(stderr, "@testTypeConverter1ToNTargetMaterialization\n");
+
+ // `test.consumer` takes an i32 from the (legal, unconverted) `test.producer`.
+ // The 1:N conversion maps i32 -> (i16, i16), so converting `test.consumer`
+ // requires its operand as two i16 values, which triggers a 1:N target
+ // materialization producing two values from the single i32.
+ const char *moduleString = "%0 = \"test.producer\"() : () -> i32\n"
+ "\"test.consumer\"(%0) : (i32) -> ()\n";
+ MlirModule module =
+ mlirModuleCreateParse(ctx, mlirStringRefCreateFromCString(moduleString));
+ MlirOperation moduleOp = mlirModuleGetOperation(module);
+
+ MlirTypeConverter converter = mlirTypeConverterCreate();
+ mlirTypeConverterAdd1ToNConversion(converter, splitI32, NULL);
+ intptr_t materializationCounter = 0;
+ mlirTypeConverterAdd1ToNTargetMaterialization(converter, buildSplitCast,
+ &materializationCounter);
+
+ MlirRewritePatternSet patterns = mlirRewritePatternSetCreate(ctx);
+ MlirConversionPatternCallbacks callbacks = {NULL, NULL, NULL,
+ convertConsumer1ToN};
+ MlirConversionPattern pattern = mlirOpConversionPatternCreate(
+ mlirStringRefCreateFromCString("test.consumer"), 1, ctx, converter,
+ callbacks, NULL, 0, NULL);
+ mlirRewritePatternSetAdd(patterns,
+ mlirConversionPatternAsRewritePattern(pattern));
+ MlirFrozenRewritePatternSet frozen = mlirFreezeRewritePattern(patterns);
+
+ MlirConversionTarget target = mlirConversionTargetCreate(ctx);
+ mlirConversionTargetAddIllegalOp(
+ target, mlirStringRefCreateFromCString("test.consumer"));
+ mlirConversionTargetAddLegalOp(
+ target, mlirStringRefCreateFromCString("test.producer"));
+ mlirConversionTargetAddLegalOp(
+ target, mlirStringRefCreateFromCString("test.consumer_legal"));
+ mlirConversionTargetAddLegalOp(target,
+ mlirStringRefCreateFromCString("test.cast"));
+ mlirConversionTargetAddLegalOp(
+ target, mlirStringRefCreateFromCString("builtin.module"));
+
+ MlirConversionConfig config = mlirConversionConfigCreate();
+ MlirLogicalResult result =
+ mlirApplyPartialConversion(moduleOp, target, frozen, config);
+ assert(mlirLogicalResultIsSuccess(result));
+ assert(materializationCounter > 0 &&
+ "1:N target materialization callback must be invoked");
+
+ mlirOperationDump(moduleOp);
+ // clang-format off
+ // CHECK: %[[v:.*]] = "test.producer"() : () -> i32
+ // CHECK: %[[c0:.*]] = "test.cast"(%[[v]]) : (i32) -> i16
+ // CHECK: %[[c1:.*]] = "test.cast"(%[[v]]) : (i32) -> i16
+ // CHECK: "test.consumer_legal"(%[[c0]], %[[c1]]) : (i16, i16) -> ()
+ // clang-format on
+
+ mlirConversionConfigDestroy(config);
+ mlirConversionTargetDestroy(target);
+ mlirFrozenRewritePatternSetDestroy(frozen);
+ mlirTypeConverterDestroy(converter);
+ mlirModuleDestroy(module);
+
+ // CHECK: testTypeConverter1ToNTargetMaterialization: PASSED
+ fprintf(stderr, "testTypeConverter1ToNTargetMaterialization: PASSED\n");
+}
+
+void testTypeConverter1ToNConversionDeclineRollback(MlirContext ctx) {
+ // CHECK-LABEL: @testTypeConverter1ToNConversionDeclineRollback
+ fprintf(stderr, "@testTypeConverter1ToNConversionDeclineRollback\n");
+
+ // Same setup as the 1:N target materialization test, but with an extra
+ // conversion that appends bogus types and then declines. It is registered
+ // last, so it is tried first; the binding must roll back its appended types
+ // so the i32 -> (i16, i16) conversion still produces exactly two i16 values
+ // (not i8s, and not three of them).
+ const char *moduleString = "%0 = \"test.producer\"() : () -> i32\n"
+ "\"test.consumer\"(%0) : (i32) -> ()\n";
+ MlirModule module =
+ mlirModuleCreateParse(ctx, mlirStringRefCreateFromCString(moduleString));
+ MlirOperation moduleOp = mlirModuleGetOperation(module);
+
+ MlirTypeConverter converter = mlirTypeConverterCreate();
+ mlirTypeConverterAdd1ToNConversion(converter, splitI32, NULL);
+ intptr_t declineCounter = 0;
+ mlirTypeConverterAdd1ToNConversion(converter, appendThenDeclineConversion,
+ &declineCounter);
+ intptr_t materializationCounter = 0;
+ mlirTypeConverterAdd1ToNTargetMaterialization(converter, buildSplitCast,
+ &materializationCounter);
+
+ MlirRewritePatternSet patterns = mlirRewritePatternSetCreate(ctx);
+ MlirConversionPatternCallbacks callbacks = {NULL, NULL, NULL,
+ convertConsumer1ToN};
+ MlirConversionPattern pattern = mlirOpConversionPatternCreate(
+ mlirStringRefCreateFromCString("test.consumer"), 1, ctx, converter,
+ callbacks, NULL, 0, NULL);
+ mlirRewritePatternSetAdd(patterns,
+ mlirConversionPatternAsRewritePattern(pattern));
+ MlirFrozenRewritePatternSet frozen = mlirFreezeRewritePattern(patterns);
+
+ MlirConversionTarget target = mlirConversionTargetCreate(ctx);
+ mlirConversionTargetAddIllegalOp(
+ target, mlirStringRefCreateFromCString("test.consumer"));
+ mlirConversionTargetAddLegalOp(
+ target, mlirStringRefCreateFromCString("test.producer"));
+ mlirConversionTargetAddLegalOp(
+ target, mlirStringRefCreateFromCString("test.consumer_legal"));
+ mlirConversionTargetAddLegalOp(target,
+ mlirStringRefCreateFromCString("test.cast"));
+ mlirConversionTargetAddLegalOp(
+ target, mlirStringRefCreateFromCString("builtin.module"));
+
+ MlirConversionConfig config = mlirConversionConfigCreate();
+ MlirLogicalResult result =
+ mlirApplyPartialConversion(moduleOp, target, frozen, config);
+ assert(mlirLogicalResultIsSuccess(result));
+ assert(declineCounter > 0 && "declining conversion must be consulted");
+
+ mlirOperationDump(moduleOp);
+ // clang-format off
+ // CHECK: %[[v:.*]] = "test.producer"() : () -> i32
+ // CHECK: %[[c0:.*]] = "test.cast"(%[[v]]) : (i32) -> i16
+ // CHECK: %[[c1:.*]] = "test.cast"(%[[v]]) : (i32) -> i16
+ // CHECK: "test.consumer_legal"(%[[c0]], %[[c1]]) : (i16, i16) -> ()
+ // clang-format on
+
+ mlirConversionConfigDestroy(config);
+ mlirConversionTargetDestroy(target);
+ mlirFrozenRewritePatternSetDestroy(frozen);
+ mlirTypeConverterDestroy(converter);
+ mlirModuleDestroy(module);
+
+ // CHECK: testTypeConverter1ToNConversionDeclineRollback: PASSED
+ fprintf(stderr, "testTypeConverter1ToNConversionDeclineRollback: PASSED\n");
+}
+
+void testTypeConverter1ToNTargetMaterializationDecline(MlirContext ctx) {
+ // CHECK-LABEL: @testTypeConverter1ToNTargetMaterializationDecline
+ fprintf(stderr, "@testTypeConverter1ToNTargetMaterializationDecline\n");
+
+ // Register three 1:N target materializations. Tried most-recently-added
+ // first: one that returns failure, one that returns success but leaves the
+ // outputs unfilled (must be treated as a decline), and finally the real one.
+ // The conversion must still succeed via the last, and all three must run.
+ const char *moduleString = "%0 = \"test.producer\"() : () -> i32\n"
+ "\"test.consumer\"(%0) : (i32) -> ()\n";
+ MlirModule module =
+ mlirModuleCreateParse(ctx, mlirStringRefCreateFromCString(moduleString));
+ MlirOperation moduleOp = mlirModuleGetOperation(module);
+
+ MlirTypeConverter converter = mlirTypeConverterCreate();
+ mlirTypeConverterAdd1ToNConversion(converter, splitI32, NULL);
+ intptr_t buildCounter = 0, partialCounter = 0, declineCounter = 0;
+ mlirTypeConverterAdd1ToNTargetMaterialization(converter, buildSplitCast,
+ &buildCounter);
+ mlirTypeConverterAdd1ToNTargetMaterialization(
+ converter, partialTargetMaterialization, &partialCounter);
+ mlirTypeConverterAdd1ToNTargetMaterialization(
+ converter, declineTargetMaterialization, &declineCounter);
+
+ MlirRewritePatternSet patterns = mlirRewritePatternSetCreate(ctx);
+ MlirConversionPatternCallbacks callbacks = {NULL, NULL, NULL,
+ convertConsumer1ToN};
+ MlirConversionPattern pattern = mlirOpConversionPatternCreate(
+ mlirStringRefCreateFromCString("test.consumer"), 1, ctx, converter,
+ callbacks, NULL, 0, NULL);
+ mlirRewritePatternSetAdd(patterns,
+ mlirConversionPatternAsRewritePattern(pattern));
+ MlirFrozenRewritePatternSet frozen = mlirFreezeRewritePattern(patterns);
+
+ MlirConversionTarget target = mlirConversionTargetCreate(ctx);
+ mlirConversionTargetAddIllegalOp(
+ target, mlirStringRefCreateFromCString("test.consumer"));
+ mlirConversionTargetAddLegalOp(
+ target, mlirStringRefCreateFromCString("test.producer"));
+ mlirConversionTargetAddLegalOp(
+ target, mlirStringRefCreateFromCString("test.consumer_legal"));
+ mlirConversionTargetAddLegalOp(target,
+ mlirStringRefCreateFromCString("test.cast"));
+ mlirConversionTargetAddLegalOp(
+ target, mlirStringRefCreateFromCString("builtin.module"));
+
+ MlirConversionConfig config = mlirConversionConfigCreate();
+ MlirLogicalResult result =
+ mlirApplyPartialConversion(moduleOp, target, frozen, config);
+ assert(mlirLogicalResultIsSuccess(result));
+ assert(declineCounter > 0 && "failing materialization must be consulted");
+ assert(partialCounter > 0 &&
+ "unfilled (incomplete) materialization must be consulted");
+ assert(buildCounter > 0 && "fallback materialization must run");
+
+ mlirOperationDump(moduleOp);
+ // clang-format off
+ // CHECK: %[[v:.*]] = "test.producer"() : () -> i32
+ // CHECK: %[[c0:.*]] = "test.cast"(%[[v]]) : (i32) -> i16
+ // CHECK: %[[c1:.*]] = "test.cast"(%[[v]]) : (i32) -> i16
+ // CHECK: "test.consumer_legal"(%[[c0]], %[[c1]]) : (i16, i16) -> ()
+ // clang-format on
+
+ mlirConversionConfigDestroy(config);
+ mlirConversionTargetDestroy(target);
+ mlirFrozenRewritePatternSetDestroy(frozen);
+ mlirTypeConverterDestroy(converter);
+ mlirModuleDestroy(module);
+
+ // CHECK: testTypeConverter1ToNTargetMaterializationDecline: PASSED
+ fprintf(stderr,
+ "testTypeConverter1ToNTargetMaterializationDecline: PASSED\n");
+}
+
+// Conversion pattern for `test.producer`: replaces its single i32 result with
+// two i16 values (built by two `test.piece` ops) via replaceOpWithMultiple.
+// Because a legal use (`test.user`) still wants the original i32, the framework
+// must insert a source materialization with two inputs to reconcile the 1:N
+// replacement.
+static MlirLogicalResult
+convertProducerToMultiple(MlirConversionPattern pattern, MlirOperation op,
+ intptr_t nOperands, MlirValue *operands,
+ MlirConversionPatternRewriter rewriter,
+ void *userData) {
+ (void)pattern;
+ (void)nOperands;
+ (void)operands;
+ (void)userData;
+ MlirContext ctx = mlirOperationGetContext(op);
+ MlirLocation loc = mlirOperationGetLocation(op);
+ MlirType i16 = mlirIntegerTypeGet(ctx, 16);
+
+ MlirRewriterBase base = mlirPatternRewriterAsBase(
+ mlirConversionPatternRewriterAsPatternRewriter(rewriter));
+
+ MlirValue pieces[2];
+ for (int i = 0; i < 2; ++i) {
+ MlirOperationState state = mlirOperationStateGet(
+ mlirStringRefCreateFromCString("test.piece"), loc);
+ mlirOperationStateAddResults(&state, 1, &i16);
+ MlirOperation pieceOp = mlirOperationCreate(&state);
+ mlirRewriterBaseInsert(base, pieceOp);
+ pieces[i] = mlirOperationGetResult(pieceOp, 0);
+ }
+
+ // The op has a single result, so there is one range, carrying two values.
+ intptr_t rangeSizes[1] = {2};
+ mlirConversionPatternRewriterReplaceOpWithMultiple(rewriter, op, 1,
+ rangeSizes, pieces);
+ return mlirLogicalResultSuccess();
+}
+
+void testTypeConverterMultiInputSourceMaterialization(MlirContext ctx) {
+ // CHECK-LABEL: @testTypeConverterMultiInputSourceMaterialization
+ fprintf(stderr, "@testTypeConverterMultiInputSourceMaterialization\n");
+
+ // `test.producer` produces an i32 consumed by the (legal) `test.user`.
+ // The pattern replaces the producer's result with two i16 values, so the
+ // i32-wanting `test.user` forces a source materialization that receives both
+ // values as inputs (nInputs == 2).
+ const char *moduleString = "%0 = \"test.producer\"() : () -> i32\n"
+ "\"test.user\"(%0) : (i32) -> ()\n";
+ MlirModule module =
+ mlirModuleCreateParse(ctx, mlirStringRefCreateFromCString(moduleString));
+ MlirOperation moduleOp = mlirModuleGetOperation(module);
+
+ MlirTypeConverter converter = mlirTypeConverterCreate();
+ intptr_t observedInputs = 0;
+ mlirTypeConverterAddSourceMaterialization(
+ converter, buildSourceCastRecordInputs, &observedInputs);
+
+ MlirRewritePatternSet patterns = mlirRewritePatternSetCreate(ctx);
+ MlirConversionPatternCallbacks callbacks = {NULL, NULL,
+ convertProducerToMultiple};
+ MlirConversionPattern pattern = mlirOpConversionPatternCreate(
+ mlirStringRefCreateFromCString("test.producer"), 1, ctx, converter,
+ callbacks, NULL, 0, NULL);
+ mlirRewritePatternSetAdd(patterns,
+ mlirConversionPatternAsRewritePattern(pattern));
+ MlirFrozenRewritePatternSet frozen = mlirFreezeRewritePattern(patterns);
+
+ MlirConversionTarget target = mlirConversionTargetCreate(ctx);
+ mlirConversionTargetAddIllegalOp(
+ target, mlirStringRefCreateFromCString("test.producer"));
+ mlirConversionTargetAddLegalOp(target,
+ mlirStringRefCreateFromCString("test.piece"));
+ mlirConversionTargetAddLegalOp(target,
+ mlirStringRefCreateFromCString("test.cast"));
+ mlirConversionTargetAddLegalOp(target,
+ mlirStringRefCreateFromCString("test.user"));
+ mlirConversionTargetAddLegalOp(
+ target, mlirStringRefCreateFromCString("builtin.module"));
+
+ MlirConversionConfig config = mlirConversionConfigCreate();
+ MlirLogicalResult result =
+ mlirApplyPartialConversion(moduleOp, target, frozen, config);
+ assert(mlirLogicalResultIsSuccess(result));
+ assert(observedInputs == 2 &&
+ "source materialization must receive both replacement values");
+
+ mlirOperationDump(moduleOp);
+ // clang-format off
+ // CHECK: %[[a:.*]] = "test.piece"() : () -> i16
+ // CHECK: %[[b:.*]] = "test.piece"() : () -> i16
+ // CHECK: %[[c:.*]] = "test.cast"(%[[a]], %[[b]]) : (i16, i16) -> i32
+ // CHECK: "test.user"(%[[c]]) : (i32) -> ()
+ // clang-format on
+
+ mlirConversionConfigDestroy(config);
+ mlirConversionTargetDestroy(target);
+ mlirFrozenRewritePatternSetDestroy(frozen);
+ mlirTypeConverterDestroy(converter);
+ mlirModuleDestroy(module);
+
+ // CHECK: testTypeConverterMultiInputSourceMaterialization: PASSED
+ fprintf(stderr, "testTypeConverterMultiInputSourceMaterialization: PASSED\n");
+}
+
+// Conversion pattern for a two-result `test.multi`: replaces it via
+// replaceOpWithMultiple with two ranges (one per result), exercising the
+// nRanges > 1 marshalling path. Each result is replaced 1:1 with a same-typed
+// value so no materialization is needed.
+static MlirLogicalResult
+convertMultiResult(MlirConversionPattern pattern, MlirOperation op,
+ intptr_t nOperands, MlirValue *operands,
+ MlirConversionPatternRewriter rewriter, void *userData) {
+ (void)pattern;
+ (void)nOperands;
+ (void)operands;
+ (void)userData;
+ MlirContext ctx = mlirOperationGetContext(op);
+ MlirLocation loc = mlirOperationGetLocation(op);
+ MlirType i32 = mlirIntegerTypeGet(ctx, 32);
+ MlirType i64 = mlirIntegerTypeGet(ctx, 64);
+ MlirRewriterBase base = mlirPatternRewriterAsBase(
+ mlirConversionPatternRewriterAsPatternRewriter(rewriter));
+
+ MlirValue vals[2];
+ MlirType types[2] = {i32, i64};
+ const char *names[2] = {"test.r0", "test.r1"};
+ for (int i = 0; i < 2; ++i) {
+ MlirOperationState state =
+ mlirOperationStateGet(mlirStringRefCreateFromCString(names[i]), loc);
+ mlirOperationStateAddResults(&state, 1, &types[i]);
+ MlirOperation newOp = mlirOperationCreate(&state);
+ mlirRewriterBaseInsert(base, newOp);
+ vals[i] = mlirOperationGetResult(newOp, 0);
+ }
+
+ // Two ranges (one per result), each carrying a single value.
+ intptr_t rangeSizes[2] = {1, 1};
+ mlirConversionPatternRewriterReplaceOpWithMultiple(rewriter, op, 2,
+ rangeSizes, vals);
+ return mlirLogicalResultSuccess();
+}
+
+void testConversionReplaceOpWithMultipleRanges(MlirContext ctx) {
+ // CHECK-LABEL: @testConversionReplaceOpWithMultipleRanges
+ fprintf(stderr, "@testConversionReplaceOpWithMultipleRanges\n");
+
+ const char *moduleString = "%0:2 = \"test.multi\"() : () -> (i32, i64)\n"
+ "\"test.use\"(%0#0, %0#1) : (i32, i64) -> ()\n";
+ MlirModule module =
+ mlirModuleCreateParse(ctx, mlirStringRefCreateFromCString(moduleString));
+ MlirOperation moduleOp = mlirModuleGetOperation(module);
+
+ MlirTypeConverter converter = mlirTypeConverterCreate();
+
+ MlirRewritePatternSet patterns = mlirRewritePatternSetCreate(ctx);
+ MlirConversionPatternCallbacks callbacks = {NULL, NULL, convertMultiResult,
+ NULL};
+ MlirConversionPattern pattern = mlirOpConversionPatternCreate(
+ mlirStringRefCreateFromCString("test.multi"), 1, ctx, converter,
+ callbacks, NULL, 0, NULL);
+ mlirRewritePatternSetAdd(patterns,
+ mlirConversionPatternAsRewritePattern(pattern));
+ MlirFrozenRewritePatternSet frozen = mlirFreezeRewritePattern(patterns);
+
+ MlirConversionTarget target = mlirConversionTargetCreate(ctx);
+ mlirConversionTargetAddIllegalOp(
+ target, mlirStringRefCreateFromCString("test.multi"));
+ mlirConversionTargetAddLegalOp(target,
+ mlirStringRefCreateFromCString("test.r0"));
+ mlirConversionTargetAddLegalOp(target,
+ mlirStringRefCreateFromCString("test.r1"));
+ mlirConversionTargetAddLegalOp(target,
+ mlirStringRefCreateFromCString("test.use"));
+ mlirConversionTargetAddLegalOp(
+ target, mlirStringRefCreateFromCString("builtin.module"));
+
+ MlirConversionConfig config = mlirConversionConfigCreate();
+ MlirLogicalResult result =
+ mlirApplyPartialConversion(moduleOp, target, frozen, config);
+ assert(mlirLogicalResultIsSuccess(result));
+
+ mlirOperationDump(moduleOp);
+ // clang-format off
+ // CHECK: %[[a:.*]] = "test.r0"() : () -> i32
+ // CHECK: %[[b:.*]] = "test.r1"() : () -> i64
+ // CHECK: "test.use"(%[[a]], %[[b]]) : (i32, i64) -> ()
+ // clang-format on
+
+ mlirConversionConfigDestroy(config);
+ mlirConversionTargetDestroy(target);
+ mlirFrozenRewritePatternSetDestroy(frozen);
+ mlirTypeConverterDestroy(converter);
+ mlirModuleDestroy(module);
+
+ // CHECK: testConversionReplaceOpWithMultipleRanges: PASSED
+ fprintf(stderr, "testConversionReplaceOpWithMultipleRanges: PASSED\n");
+}
+
+void testTypeConverter1ToNOperandRequires1ToNCallback(MlirContext ctx) {
+ // CHECK-LABEL: @testTypeConverter1ToNOperandRequires1ToNCallback
+ fprintf(stderr, "@testTypeConverter1ToNOperandRequires1ToNCallback\n");
+
+ // The operand of `test.consumer` converts 1:N (i32 -> (i16, i16)), but the
+ // pattern only provides the 1:1 matchAndRewrite (matchAndRewrite1ToN is
+ // null). The driver's 1:1 dispatch cannot represent a 1:N-remapped operand,
+ // so the pattern fails to match and the conversion fails.
+ const char *moduleString = "%0 = \"test.producer\"() : () -> i32\n"
+ "\"test.consumer\"(%0) : (i32) -> ()\n";
+ MlirModule module =
+ mlirModuleCreateParse(ctx, mlirStringRefCreateFromCString(moduleString));
+ MlirOperation moduleOp = mlirModuleGetOperation(module);
+
+ MlirTypeConverter converter = mlirTypeConverterCreate();
+ mlirTypeConverterAdd1ToNConversion(converter, splitI32, NULL);
+
+ MlirRewritePatternSet patterns = mlirRewritePatternSetCreate(ctx);
+ // Only the 1:1 matchAndRewrite is set; matchAndRewrite1ToN is null.
+ MlirConversionPatternCallbacks callbacks = {NULL, NULL, convertConsumer,
+ NULL};
+ MlirConversionPattern pattern = mlirOpConversionPatternCreate(
+ mlirStringRefCreateFromCString("test.consumer"), 1, ctx, converter,
+ callbacks, NULL, 0, NULL);
+ mlirRewritePatternSetAdd(patterns,
+ mlirConversionPatternAsRewritePattern(pattern));
+ MlirFrozenRewritePatternSet frozen = mlirFreezeRewritePattern(patterns);
+
+ MlirConversionTarget target = mlirConversionTargetCreate(ctx);
+ mlirConversionTargetAddIllegalOp(
+ target, mlirStringRefCreateFromCString("test.consumer"));
+ mlirConversionTargetAddLegalOp(
+ target, mlirStringRefCreateFromCString("test.producer"));
+ mlirConversionTargetAddLegalOp(
+ target, mlirStringRefCreateFromCString("builtin.module"));
+
+ MlirConversionConfig config = mlirConversionConfigCreate();
+ MlirLogicalResult result =
+ mlirApplyPartialConversion(moduleOp, target, frozen, config);
+ assert(mlirLogicalResultIsFailure(result) &&
+ "1:1-only pattern must fail to legalize a 1:N-remapped operand");
+
+ mlirConversionConfigDestroy(config);
+ mlirConversionTargetDestroy(target);
+ mlirFrozenRewritePatternSetDestroy(frozen);
+ mlirTypeConverterDestroy(converter);
+ mlirModuleDestroy(module);
+
+ // CHECK: testTypeConverter1ToNOperandRequires1ToNCallback: PASSED
+ fprintf(stderr, "testTypeConverter1ToNOperandRequires1ToNCallback: PASSED\n");
+}
+
+void testTypeConverter1ToNConversionErasure(MlirContext ctx) {
+ // CHECK-LABEL: @testTypeConverter1ToNConversionErasure
+ fprintf(stderr, "@testTypeConverter1ToNConversionErasure\n");
+
+ // The i32 operand of `test.consumer` is erased (converted to zero types), so
+ // the 1:N pattern is invoked with no operands and builds a nullary
+ // `test.consumer_legal`.
+ const char *moduleString = "%0 = \"test.producer\"() : () -> i32\n"
+ "\"test.consumer\"(%0) : (i32) -> ()\n";
+ MlirModule module =
+ mlirModuleCreateParse(ctx, mlirStringRefCreateFromCString(moduleString));
+ MlirOperation moduleOp = mlirModuleGetOperation(module);
+
+ MlirTypeConverter converter = mlirTypeConverterCreate();
+ mlirTypeConverterAdd1ToNConversion(converter, eraseI32, NULL);
+
+ MlirRewritePatternSet patterns = mlirRewritePatternSetCreate(ctx);
+ MlirConversionPatternCallbacks callbacks = {NULL, NULL, NULL,
+ convertConsumer1ToN};
+ MlirConversionPattern pattern = mlirOpConversionPatternCreate(
+ mlirStringRefCreateFromCString("test.consumer"), 1, ctx, converter,
+ callbacks, NULL, 0, NULL);
+ mlirRewritePatternSetAdd(patterns,
+ mlirConversionPatternAsRewritePattern(pattern));
+ MlirFrozenRewritePatternSet frozen = mlirFreezeRewritePattern(patterns);
+
+ MlirConversionTarget target = mlirConversionTargetCreate(ctx);
+ mlirConversionTargetAddIllegalOp(
+ target, mlirStringRefCreateFromCString("test.consumer"));
+ mlirConversionTargetAddLegalOp(
+ target, mlirStringRefCreateFromCString("test.producer"));
+ mlirConversionTargetAddLegalOp(
+ target, mlirStringRefCreateFromCString("test.consumer_legal"));
+ mlirConversionTargetAddLegalOp(
+ target, mlirStringRefCreateFromCString("builtin.module"));
+
+ MlirConversionConfig config = mlirConversionConfigCreate();
+ MlirLogicalResult result =
+ mlirApplyPartialConversion(moduleOp, target, frozen, config);
+ assert(mlirLogicalResultIsSuccess(result));
+
+ mlirOperationDump(moduleOp);
+ // clang-format off
+ // CHECK: "test.consumer_legal"() : () -> ()
+ // clang-format on
+
+ mlirConversionConfigDestroy(config);
+ mlirConversionTargetDestroy(target);
+ mlirFrozenRewritePatternSetDestroy(frozen);
+ mlirTypeConverterDestroy(converter);
+ mlirModuleDestroy(module);
+
+ // CHECK: testTypeConverter1ToNConversionErasure: PASSED
+ fprintf(stderr, "testTypeConverter1ToNConversionErasure: PASSED\n");
+}
+
int main(void) {
MlirContext ctx = mlirContextCreate();
mlirContextSetAllowUnregisteredDialects(ctx, true);
@@ -1104,6 +1826,13 @@ int main(void) {
testTypeConverterConversionStates(ctx);
testTypeConverterSourceMaterialization(ctx);
testTypeConverterTargetMaterialization(ctx);
+ testTypeConverter1ToNTargetMaterialization(ctx);
+ testTypeConverter1ToNConversionDeclineRollback(ctx);
+ testTypeConverter1ToNTargetMaterializationDecline(ctx);
+ testTypeConverterMultiInputSourceMaterialization(ctx);
+ testConversionReplaceOpWithMultipleRanges(ctx);
+ testTypeConverter1ToNOperandRequires1ToNCallback(ctx);
+ testTypeConverter1ToNConversionErasure(ctx);
mlirContextDestroy(ctx);
return 0;
>From 0f794b4cd5943c2861be125579b8ecc0b19226af Mon Sep 17 00:00:00 2001
From: makslevental <maksim.levental at gmail.com>
Date: Tue, 30 Jun 2026 12:15:17 -0700
Subject: [PATCH 2/6] [mlir-c] Fix -Wmissing-field-initializers in rewrite.c
test
The new matchAndRewrite1ToN field left three existing
MlirConversionPatternCallbacks initializers under-initialized, which
fails the CI build under -Werror=-Wmissing-field-initializers.
---
mlir/test/CAPI/rewrite.c | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/mlir/test/CAPI/rewrite.c b/mlir/test/CAPI/rewrite.c
index 441e4759a24e2..ad333a2fdb637 100644
--- a/mlir/test/CAPI/rewrite.c
+++ b/mlir/test/CAPI/rewrite.c
@@ -1125,7 +1125,7 @@ void testTypeConverterSourceMaterialization(MlirContext ctx) {
converter, declineSourceMaterialization, &declinedCounter);
MlirRewritePatternSet patterns = mlirRewritePatternSetCreate(ctx);
- MlirConversionPatternCallbacks callbacks = {NULL, NULL, convertSource};
+ MlirConversionPatternCallbacks callbacks = {NULL, NULL, convertSource, NULL};
MlirConversionPattern pattern = mlirOpConversionPatternCreate(
mlirStringRefCreateFromCString("test.source"), 1, ctx, converter,
callbacks, NULL, 0, NULL);
@@ -1244,7 +1244,8 @@ void testTypeConverterTargetMaterialization(MlirContext ctx) {
&materializationCounter);
MlirRewritePatternSet patterns = mlirRewritePatternSetCreate(ctx);
- MlirConversionPatternCallbacks callbacks = {NULL, NULL, convertConsumer};
+ MlirConversionPatternCallbacks callbacks = {NULL, NULL, convertConsumer,
+ NULL};
MlirConversionPattern pattern = mlirOpConversionPatternCreate(
mlirStringRefCreateFromCString("test.consumer"), 1, ctx, converter,
callbacks, NULL, 0, NULL);
@@ -1560,7 +1561,7 @@ void testTypeConverterMultiInputSourceMaterialization(MlirContext ctx) {
MlirRewritePatternSet patterns = mlirRewritePatternSetCreate(ctx);
MlirConversionPatternCallbacks callbacks = {NULL, NULL,
- convertProducerToMultiple};
+ convertProducerToMultiple, NULL};
MlirConversionPattern pattern = mlirOpConversionPatternCreate(
mlirStringRefCreateFromCString("test.producer"), 1, ctx, converter,
callbacks, NULL, 0, NULL);
>From 56262f342585e47b6ffd5153e788b12a8514b493 Mon Sep 17 00:00:00 2001
From: makslevental <maksim.levental at gmail.com>
Date: Tue, 30 Jun 2026 12:45:23 -0700
Subject: [PATCH 3/6] [mlir-c] Value-initialize MlirConversionPatternCallbacks
in Python bindings
The Python conversion-pattern binding left the struct default-initialized,
so the newly-added optional matchAndRewrite1ToN field held an indeterminate
pointer. The driver's null check then read garbage and jumped into it,
segfaulting mlir/test/python/rewrite.py. Value-initialize the struct so
optional callbacks default to null.
---
mlir/lib/Bindings/Python/Rewrite.cpp | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/mlir/lib/Bindings/Python/Rewrite.cpp b/mlir/lib/Bindings/Python/Rewrite.cpp
index 750c7b1e4b1d8..2a9b0ea1c218d 100644
--- a/mlir/lib/Bindings/Python/Rewrite.cpp
+++ b/mlir/lib/Bindings/Python/Rewrite.cpp
@@ -226,7 +226,9 @@ void PyRewritePatternSet::addConversion(nb::handle root,
std::string opName = operationNameFromObject(root);
MlirStringRef rootName = mlirStringRefCreate(opName.data(), opName.size());
- MlirConversionPatternCallbacks callbacks;
+ // Value-initialize so optional callbacks (e.g. matchAndRewrite1ToN) default
+ // to null rather than an indeterminate pointer.
+ MlirConversionPatternCallbacks callbacks{};
callbacks.construct = [](void *userData) {
nb::handle(static_cast<PyObject *>(userData)).inc_ref();
};
>From 785b59543b972bf05c786abf2efdfb01d38fc9f9 Mon Sep 17 00:00:00 2001
From: makslevental <maksim.levental at gmail.com>
Date: Fri, 17 Jul 2026 12:23:37 -0700
Subject: [PATCH 4/6] pre-increment
---
mlir/test/CAPI/rewrite.c | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/mlir/test/CAPI/rewrite.c b/mlir/test/CAPI/rewrite.c
index ad333a2fdb637..9da0fbf951e54 100644
--- a/mlir/test/CAPI/rewrite.c
+++ b/mlir/test/CAPI/rewrite.c
@@ -874,7 +874,7 @@ declineSourceMaterialization(MlirRewriterBase rewriter, MlirType outputType,
(void)loc;
intptr_t *declined = (intptr_t *)userData;
if (declined)
- (*declined)++;
+ ++(*declined);
return (MlirValue){NULL};
}
@@ -910,7 +910,7 @@ static MlirValue buildTargetCast(MlirRewriterBase rewriter, MlirType outputType,
intptr_t *counter = (intptr_t *)userData;
if (counter && mlirTypeIsAInteger(originalType) &&
mlirIntegerTypeGetWidth(originalType) == 32)
- (*counter)++;
+ ++(*counter);
MlirOperationState state =
mlirOperationStateGet(mlirStringRefCreateFromCString("test.cast"), loc);
mlirOperationStateAddOperands(&state, nInputs, inputs);
@@ -962,7 +962,7 @@ static MlirLogicalResult buildSplitCast(MlirRewriterBase rewriter,
// the (i16) output types alone, so observing it here proves it is propagated.
if (counter && mlirTypeIsAInteger(originalType) &&
mlirIntegerTypeGetWidth(originalType) == 32)
- (*counter)++;
+ ++(*counter);
for (intptr_t i = 0; i < nOutputTypes; ++i) {
MlirOperationState state =
mlirOperationStateGet(mlirStringRefCreateFromCString("test.cast"), loc);
@@ -982,7 +982,7 @@ static MlirLogicalResult appendThenDeclineConversion(
MlirType type, MlirTypeConverterConversionResults results, void *userData) {
intptr_t *counter = (intptr_t *)userData;
if (counter)
- (*counter)++;
+ ++(*counter);
MlirType i8 = mlirIntegerTypeGet(mlirTypeGetContext(type), 8);
// Append more (and differently-typed) entries than the real conversion would,
// so any leak is observable as a wrong type/arity downstream.
@@ -1008,7 +1008,7 @@ static MlirLogicalResult declineTargetMaterialization(
(void)outputs;
intptr_t *counter = (intptr_t *)userData;
if (counter)
- (*counter)++;
+ ++(*counter);
return mlirLogicalResultFailure();
}
@@ -1029,7 +1029,7 @@ static MlirLogicalResult partialTargetMaterialization(
(void)outputs;
intptr_t *counter = (intptr_t *)userData;
if (counter)
- (*counter)++;
+ ++(*counter);
return mlirLogicalResultSuccess();
}
// op whose result has the widened (i64) type. Because the original result type
>From 97d9dbe270808b9113cfd605d3b849bd33a5e7f1 Mon Sep 17 00:00:00 2001
From: makslevental <maksim.levental at gmail.com>
Date: Fri, 17 Jul 2026 13:08:44 -0700
Subject: [PATCH 5/6] cleanup comments
---
mlir/include/mlir-c/Rewrite.h | 20 ++------------------
mlir/lib/CAPI/Transforms/Rewrite.cpp | 12 ------------
2 files changed, 2 insertions(+), 30 deletions(-)
diff --git a/mlir/include/mlir-c/Rewrite.h b/mlir/include/mlir-c/Rewrite.h
index fb859bd3fdab8..2b44da1d648e3 100644
--- a/mlir/include/mlir-c/Rewrite.h
+++ b/mlir/include/mlir-c/Rewrite.h
@@ -505,9 +505,7 @@ mlirConversionPatternRewriterConvertRegionTypes(
/// result of `op` -- and erase it. `nRanges` must equal the number of results
/// of `op`. `rangeSizes[i]` is the number of values in the i-th range, and
/// `values` is the flat concatenation of all ranges (its length is the sum of
-/// `rangeSizes[0..nRanges)`). This is the 1:N replacement form; surviving type
-/// mismatches between a result and its range are reconciled by the driver with
-/// source/target materializations.
+/// `rangeSizes[0..nRanges)`).
MLIR_CAPI_EXPORTED void mlirConversionPatternRewriterReplaceOpWithMultiple(
MlirConversionPatternRewriter rewriter, MlirOperation op, intptr_t nRanges,
intptr_t *rangeSizes, MlirValue *values);
@@ -667,13 +665,6 @@ mlirTypeConverterConvertType(MlirTypeConverter typeConverter, MlirType type);
/// callback must build a cast-like operation that produces a single value of
/// `outputType` and return it. Returning a null MlirValue indicates failure, in
/// which case another registered materialization may be attempted.
-///
-/// Note: Source materializations are single-output -- the callback returns one
-/// value of `outputType`. (The 1:N, multiple-output form exists only for target
-/// materializations; see MlirTypeConverter1ToNTargetMaterializationCallback.)
-/// `nInputs` may be greater than one when several values are mapped back to a
-/// single value, e.g. after a 1:N replacement via
-/// mlirConversionPatternRewriterReplaceOpWithMultiple.
typedef MlirValue (*MlirTypeConverterSourceMaterializationCallback)(
MlirRewriterBase rewriter, MlirType outputType, intptr_t nInputs,
MlirValue *inputs, MlirLocation loc, void *userData);
@@ -682,12 +673,6 @@ typedef MlirValue (*MlirTypeConverterSourceMaterializationCallback)(
/// MlirTypeConverterSourceMaterializationCallback, but additionally receives
/// `originalType`: the original type of the SSA value being materialized.
///
-/// `originalType` may differ from `outputType` and cannot, in general, be
-/// recovered from `outputType` and `inputs`, which is why it is passed
-/// explicitly (see TypeConverter::addTargetMaterialization in
-/// DialectConversion.h for the full rationale). It may be a null MlirType when
-/// no original type is available.
-///
/// Note: This callback is single-output. For the 1:N (multiple-output) form,
/// use MlirTypeConverter1ToNTargetMaterializationCallback.
typedef MlirValue (*MlirTypeConverterTargetMaterializationCallback)(
@@ -754,8 +739,7 @@ typedef struct {
/// of all operand ranges; there are `nRanges` ranges (one per original
/// operand) and `rangeSizes[i]` is the number of values in the i-th range.
/// When this is non-null it takes precedence; when null, the driver falls
- /// back to the 1:1 `matchAndRewrite` above, which fails to match if any
- /// operand was remapped 1:N.
+ /// back to the 1:1 `matchAndRewrite` above.
MlirLogicalResult (*matchAndRewrite1ToN)(
MlirConversionPattern pattern, MlirOperation op, intptr_t nRanges,
intptr_t *rangeSizes, intptr_t nOperands, MlirValue *operands,
diff --git a/mlir/lib/CAPI/Transforms/Rewrite.cpp b/mlir/lib/CAPI/Transforms/Rewrite.cpp
index 0bf2e80dddb0c..50ee45fd7ae87 100644
--- a/mlir/lib/CAPI/Transforms/Rewrite.cpp
+++ b/mlir/lib/CAPI/Transforms/Rewrite.cpp
@@ -721,8 +721,6 @@ MlirType mlirTypeConverterConvertType(MlirTypeConverter typeConverter,
}
namespace {
-/// Marshals the `inputs` ValueRange into wrapped MlirValues. Shared by the
-/// materialization wrappers below.
SmallVector<MlirValue> wrapInputs(ValueRange inputs) {
SmallVector<MlirValue> wrappedInputs;
wrappedInputs.reserve(inputs.size());
@@ -731,10 +729,6 @@ SmallVector<MlirValue> wrapInputs(ValueRange inputs) {
return wrappedInputs;
}
-/// Wraps a C source materialization callback as the C++ form
-/// `Value(OpBuilder &, Type, ValueRange, Location)`. The builder is always a
-/// RewriterBase in the conversion driver, so it is safe to expose it as an
-/// MlirRewriterBase.
std::function<Value(OpBuilder &, Type, ValueRange, Location)>
wrapSourceMaterializationCallback(
MlirTypeConverterSourceMaterializationCallback callback, void *userData) {
@@ -749,10 +743,6 @@ wrapSourceMaterializationCallback(
};
}
-/// Wraps a C 1:1 target materialization callback as the C++ form
-/// `Value(OpBuilder &, Type, ValueRange, Location, Type)`. Selecting the
-/// 5-argument C++ overload (the one carrying `originalType`) is what makes the
-/// original type observable from C.
std::function<Value(OpBuilder &, Type, ValueRange, Location, Type)>
wrapTargetMaterializationCallback(
MlirTypeConverterTargetMaterializationCallback callback, void *userData) {
@@ -767,8 +757,6 @@ wrapTargetMaterializationCallback(
};
}
-/// Wraps a C 1:N target materialization callback as the C++ form
-/// `SmallVector<Value>(OpBuilder &, TypeRange, ValueRange, Location, Type)`.
std::function<SmallVector<Value>(OpBuilder &, TypeRange, ValueRange, Location,
Type)>
wrap1ToNTargetMaterializationCallback(
>From 2e42e0ca59e3f5f2e0d92340c610c7cf41d153bd Mon Sep 17 00:00:00 2001
From: makslevental <maksim.levental at gmail.com>
Date: Sat, 18 Jul 2026 11:27:12 -0700
Subject: [PATCH 6/6] [mlir-c] Expose hard-failure state in 1:N type conversion
callback
The 1:N conversion callback returned MlirLogicalResult, which the binding
could only map to success or nullopt (try-another), leaving the C++
failure() state (fail without trying another conversion) unreachable.
Return a MlirTypeConverterConversionStatus enum instead, with Success,
Failure, and Declined states mapped to success(), failure(), and
std::nullopt respectively. Add a test covering the hard-failure path.
---
mlir/include/mlir-c/Rewrite.h | 17 ++--
mlir/lib/CAPI/Transforms/Rewrite.cpp | 15 +++-
mlir/test/CAPI/rewrite.c | 111 ++++++++++++++++++++++++---
3 files changed, 122 insertions(+), 21 deletions(-)
diff --git a/mlir/include/mlir-c/Rewrite.h b/mlir/include/mlir-c/Rewrite.h
index 2b44da1d648e3..2d79315beb726 100644
--- a/mlir/include/mlir-c/Rewrite.h
+++ b/mlir/include/mlir-c/Rewrite.h
@@ -639,12 +639,17 @@ MLIR_CAPI_EXPORTED void mlirTypeConverterConversionResultsAppend(
/// Callback type for 1:N type conversion functions. For the given `type`, the
/// callback appends zero or more converted result types to `results` (via
-/// mlirTypeConverterConversionResultsAppend) and returns success. Returning
-/// failure leaves the type unconverted and allows another conversion function
-/// to be tried; any types appended before returning failure are discarded.
-/// Appending a single type is a 1:1 conversion; appending several is a 1:N
-/// conversion; appending none (on success) erases the type.
-typedef MlirLogicalResult (*MlirTypeConverter1ToNConversionCallback)(
+/// mlirTypeConverterConversionResultsAppend) and returns a status. Returning
+/// MlirTypeConverterConversionStatusSuccess accepts the appended types as the
+/// conversion (appending a single type is a 1:1 conversion; appending several
+/// is a 1:N conversion; appending none erases the type).
+/// MlirTypeConverterConversionStatusDeclined leaves the type unconverted and
+/// allows another conversion function to be tried, whereas
+/// MlirTypeConverterConversionStatusFailure fails the conversion outright
+/// without trying any further conversion function. In both non-success cases
+/// any types appended to `results` are discarded.
+typedef MlirTypeConverterConversionStatus (
+ *MlirTypeConverter1ToNConversionCallback)(
MlirType type, MlirTypeConverterConversionResults results, void *userData);
/// Add a 1:N type conversion function to the given TypeConverter.
diff --git a/mlir/lib/CAPI/Transforms/Rewrite.cpp b/mlir/lib/CAPI/Transforms/Rewrite.cpp
index 50ee45fd7ae87..879e18e584175 100644
--- a/mlir/lib/CAPI/Transforms/Rewrite.cpp
+++ b/mlir/lib/CAPI/Transforms/Rewrite.cpp
@@ -702,16 +702,25 @@ void mlirTypeConverterAdd1ToNConversion(
-> std::optional<LogicalResult> {
size_t numPriorResults = results.size();
MlirTypeConverterConversionResults wrappedResults{&results};
- MlirLogicalResult result =
+ MlirTypeConverterConversionStatus status =
convertType(wrap(type), wrappedResults, userData);
- if (mlirLogicalResultIsFailure(result)) {
+ switch (status) {
+ case MlirTypeConverterConversionStatusSuccess:
+ return success();
+ case MlirTypeConverterConversionStatusFailure:
+ // Hard failure. Restore any types the callback appended (a
+ // non-succeeding conversion function must not mutate `results`)
+ // and fail the conversion without trying another function.
+ results.truncate(numPriorResults);
+ return failure();
+ case MlirTypeConverterConversionStatusDeclined:
// The callback declined. Restore any types it appended so the
// driver's "try the next conversion" invariant holds (a declining
// conversion function must not mutate `results`).
results.truncate(numPriorResults);
return std::nullopt;
}
- return success();
+ llvm_unreachable("unknown MlirTypeConverterConversionStatus");
});
}
diff --git a/mlir/test/CAPI/rewrite.c b/mlir/test/CAPI/rewrite.c
index 9da0fbf951e54..c86d964f1418d 100644
--- a/mlir/test/CAPI/rewrite.c
+++ b/mlir/test/CAPI/rewrite.c
@@ -922,9 +922,9 @@ static MlirValue buildTargetCast(MlirRewriterBase rewriter, MlirType outputType,
// 1:N type conversion callback: maps i32 -> (i16, i16) and leaves every other
// type unchanged (1:1 identity).
-static MlirLogicalResult splitI32(MlirType type,
- MlirTypeConverterConversionResults results,
- void *userData) {
+static MlirTypeConverterConversionStatus
+splitI32(MlirType type, MlirTypeConverterConversionResults results,
+ void *userData) {
(void)userData;
if (mlirTypeIsAInteger(type) && mlirIntegerTypeGetWidth(type) == 32) {
MlirType i16 = mlirIntegerTypeGet(mlirTypeGetContext(type), 16);
@@ -933,19 +933,19 @@ static MlirLogicalResult splitI32(MlirType type,
} else {
mlirTypeConverterConversionResultsAppend(results, type);
}
- return mlirLogicalResultSuccess();
+ return MlirTypeConverterConversionStatusSuccess;
}
// 1:N type conversion callback: erases i32 (converts it to zero types) and
// leaves every other type unchanged (1:1 identity).
-static MlirLogicalResult eraseI32(MlirType type,
- MlirTypeConverterConversionResults results,
- void *userData) {
+static MlirTypeConverterConversionStatus
+eraseI32(MlirType type, MlirTypeConverterConversionResults results,
+ void *userData) {
(void)userData;
if (mlirTypeIsAInteger(type) && mlirIntegerTypeGetWidth(type) == 32)
- return mlirLogicalResultSuccess(); // append nothing -> erase
+ return MlirTypeConverterConversionStatusSuccess; // append nothing -> erase
mlirTypeConverterConversionResultsAppend(results, type);
- return mlirLogicalResultSuccess();
+ return MlirTypeConverterConversionStatusSuccess;
}
// 1:N type conversion callback: builds one `test.cast` per requested
@@ -976,9 +976,9 @@ static MlirLogicalResult buildSplitCast(MlirRewriterBase rewriter,
}
// 1:N type conversion callback that appends bogus result types and then
-// declines (returns failure). The binding must roll back the appended types so
+// declines (returns Declined). The binding must roll back the appended types so
// they do not pollute a subsequently-tried conversion function.
-static MlirLogicalResult appendThenDeclineConversion(
+static MlirTypeConverterConversionStatus appendThenDeclineConversion(
MlirType type, MlirTypeConverterConversionResults results, void *userData) {
intptr_t *counter = (intptr_t *)userData;
if (counter)
@@ -989,7 +989,23 @@ static MlirLogicalResult appendThenDeclineConversion(
mlirTypeConverterConversionResultsAppend(results, i8);
mlirTypeConverterConversionResultsAppend(results, i8);
mlirTypeConverterConversionResultsAppend(results, i8);
- return mlirLogicalResultFailure();
+ return MlirTypeConverterConversionStatusDeclined;
+}
+
+// 1:N type conversion callback that appends bogus result types and then fails
+// hard (returns Failure). Unlike a decline, this must abort the conversion
+// without trying any earlier-registered conversion function; the binding must
+// still roll back the appended types.
+static MlirTypeConverterConversionStatus appendThenFailConversion(
+ MlirType type, MlirTypeConverterConversionResults results, void *userData) {
+ intptr_t *counter = (intptr_t *)userData;
+ if (counter)
+ ++(*counter);
+ MlirType i8 = mlirIntegerTypeGet(mlirTypeGetContext(type), 8);
+ mlirTypeConverterConversionResultsAppend(results, i8);
+ mlirTypeConverterConversionResultsAppend(results, i8);
+ mlirTypeConverterConversionResultsAppend(results, i8);
+ return MlirTypeConverterConversionStatusFailure;
}
// 1:N target materialization that always declines (returns failure) and records
@@ -1428,6 +1444,76 @@ void testTypeConverter1ToNConversionDeclineRollback(MlirContext ctx) {
fprintf(stderr, "testTypeConverter1ToNConversionDeclineRollback: PASSED\n");
}
+void testTypeConverter1ToNConversionFailure(MlirContext ctx) {
+ // CHECK-LABEL: @testTypeConverter1ToNConversionFailure
+ fprintf(stderr, "@testTypeConverter1ToNConversionFailure\n");
+
+ // Same setup as the decline-rollback test, but the extra conversion returns a
+ // hard failure instead of a decline. Because it is registered last (tried
+ // first), the failure must abort the conversion of i32 without falling back
+ // to the i32 -> (i16, i16) conversion, so the whole conversion fails and the
+ // IR is left unchanged.
+ const char *moduleString = "%0 = \"test.producer\"() : () -> i32\n"
+ "\"test.consumer\"(%0) : (i32) -> ()\n";
+ MlirModule module =
+ mlirModuleCreateParse(ctx, mlirStringRefCreateFromCString(moduleString));
+ MlirOperation moduleOp = mlirModuleGetOperation(module);
+
+ MlirTypeConverter converter = mlirTypeConverterCreate();
+ mlirTypeConverterAdd1ToNConversion(converter, splitI32, NULL);
+ intptr_t failCounter = 0;
+ mlirTypeConverterAdd1ToNConversion(converter, appendThenFailConversion,
+ &failCounter);
+ mlirTypeConverterAdd1ToNTargetMaterialization(converter, buildSplitCast,
+ NULL);
+
+ MlirRewritePatternSet patterns = mlirRewritePatternSetCreate(ctx);
+ MlirConversionPatternCallbacks callbacks = {NULL, NULL, NULL,
+ convertConsumer1ToN};
+ MlirConversionPattern pattern = mlirOpConversionPatternCreate(
+ mlirStringRefCreateFromCString("test.consumer"), 1, ctx, converter,
+ callbacks, NULL, 0, NULL);
+ mlirRewritePatternSetAdd(patterns,
+ mlirConversionPatternAsRewritePattern(pattern));
+ MlirFrozenRewritePatternSet frozen = mlirFreezeRewritePattern(patterns);
+
+ MlirConversionTarget target = mlirConversionTargetCreate(ctx);
+ mlirConversionTargetAddIllegalOp(
+ target, mlirStringRefCreateFromCString("test.consumer"));
+ mlirConversionTargetAddLegalOp(
+ target, mlirStringRefCreateFromCString("test.producer"));
+ mlirConversionTargetAddLegalOp(
+ target, mlirStringRefCreateFromCString("test.consumer_legal"));
+ mlirConversionTargetAddLegalOp(target,
+ mlirStringRefCreateFromCString("test.cast"));
+ mlirConversionTargetAddLegalOp(
+ target, mlirStringRefCreateFromCString("builtin.module"));
+
+ MlirConversionConfig config = mlirConversionConfigCreate();
+ MlirLogicalResult result =
+ mlirApplyPartialConversion(moduleOp, target, frozen, config);
+ assert(mlirLogicalResultIsFailure(result) &&
+ "hard failure must abort the conversion");
+ assert(failCounter > 0 && "failing conversion must be consulted");
+
+ // The conversion failed, so the IR is rolled back and left unchanged: i32 is
+ // never split and no `test.cast` is produced.
+ mlirOperationDump(moduleOp);
+ // clang-format off
+ // CHECK: %[[v:.*]] = "test.producer"() : () -> i32
+ // CHECK: "test.consumer"(%[[v]]) : (i32) -> ()
+ // clang-format on
+
+ mlirConversionConfigDestroy(config);
+ mlirConversionTargetDestroy(target);
+ mlirFrozenRewritePatternSetDestroy(frozen);
+ mlirTypeConverterDestroy(converter);
+ mlirModuleDestroy(module);
+
+ // CHECK: testTypeConverter1ToNConversionFailure: PASSED
+ fprintf(stderr, "testTypeConverter1ToNConversionFailure: PASSED\n");
+}
+
void testTypeConverter1ToNTargetMaterializationDecline(MlirContext ctx) {
// CHECK-LABEL: @testTypeConverter1ToNTargetMaterializationDecline
fprintf(stderr, "@testTypeConverter1ToNTargetMaterializationDecline\n");
@@ -1829,6 +1915,7 @@ int main(void) {
testTypeConverterTargetMaterialization(ctx);
testTypeConverter1ToNTargetMaterialization(ctx);
testTypeConverter1ToNConversionDeclineRollback(ctx);
+ testTypeConverter1ToNConversionFailure(ctx);
testTypeConverter1ToNTargetMaterializationDecline(ctx);
testTypeConverterMultiInputSourceMaterialization(ctx);
testConversionReplaceOpWithMultipleRanges(ctx);
More information about the llvm-branch-commits
mailing list