https://github.com/makslevental created https://github.com/llvm/llvm-project/pull/208935
Builds on the source/target materialization C bindings (#208934) to expose the 1:N dialect-conversion functionality through the MLIR C API. Continues the buildout of the dialect-conversion C bindings (follows #206146 and #206161). > [!NOTE] > Stacked on top of #208934 — review/land that one first. This PR is based on > that branch, so its diff shows only the 1:N changes; it will auto-retarget to > `main` once the base merges. ## Changes - 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. >From a72f8fe9a668b6c4d6ea9d12e57762ddf7e8ec2c Mon Sep 17 00:00:00 2001 From: makslevental <[email protected]> Date: Tue, 30 Jun 2026 11:48:08 -0700 Subject: [PATCH 1/3] [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 e074fc96f2aeb..b3e3eb6e88b23 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 //===----------------------------------------------------------------------===// @@ -601,32 +612,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 @@ -647,6 +732,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 92456d6d8a435..e3dcede2d42b9 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 //===----------------------------------------------------------------------===// @@ -664,25 +680,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()), @@ -690,24 +740,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)); } //===----------------------------------------------------------------------===// @@ -747,6 +864,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 c1624c54eb461..0212ede03c3e6 100644 --- a/mlir/test/CAPI/rewrite.c +++ b/mlir/test/CAPI/rewrite.c @@ -833,7 +833,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. @@ -880,6 +1051,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}; @@ -908,6 +1085,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 @@ -951,6 +1130,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"); @@ -967,7 +1172,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); @@ -1015,6 +1220,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); @@ -1033,6 +1755,13 @@ int main(void) { testConversionTargetDynamicLegality(ctx); testTypeConverterSourceMaterialization(ctx); testTypeConverterTargetMaterialization(ctx); + testTypeConverter1ToNTargetMaterialization(ctx); + testTypeConverter1ToNConversionDeclineRollback(ctx); + testTypeConverter1ToNTargetMaterializationDecline(ctx); + testTypeConverterMultiInputSourceMaterialization(ctx); + testConversionReplaceOpWithMultipleRanges(ctx); + testTypeConverter1ToNOperandRequires1ToNCallback(ctx); + testTypeConverter1ToNConversionErasure(ctx); mlirContextDestroy(ctx); return 0; >From 9e37905bf39aa8698a65e3e506013ec71d0070ca Mon Sep 17 00:00:00 2001 From: makslevental <[email protected]> Date: Tue, 30 Jun 2026 12:15:17 -0700 Subject: [PATCH 2/3] [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 0212ede03c3e6..97409b73dbb2e 100644 --- a/mlir/test/CAPI/rewrite.c +++ b/mlir/test/CAPI/rewrite.c @@ -1059,7 +1059,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); @@ -1176,7 +1176,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); @@ -1490,7 +1491,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 10dc9b93188c302ba4ddbfd2c518d1bd9f928be5 Mon Sep 17 00:00:00 2001 From: makslevental <[email protected]> Date: Tue, 30 Jun 2026 12:45:23 -0700 Subject: [PATCH 3/3] [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(); }; _______________________________________________ llvm-branch-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-branch-commits
