| // Copyright 2026 The IREE Authors | |
| // | |
| // Licensed under the Apache License v2.0 with LLVM Exceptions. | |
| // See https://llvm.org/LICENSE.txt for license information. | |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | |
| #include "compiler/plugins/input/StableHLO/Conversion/TargetRegistry.h" | |
| #include "mlir/IR/BuiltinOps.h" | |
| #include "mlir/Transforms/GreedyPatternRewriteDriver.h" | |
| namespace mlir::iree_compiler::stablehlo { | |
| #define GEN_PASS_DEF_REWRITEREGISTEREDCUSTOMCALLSANDCOMPOSITESPASS | |
| #include "compiler/plugins/input/StableHLO/Conversion/TargetRegistryPasses.h.inc" | |
| LogicalResult | |
| TargetRegistry::addCustomCall(StringRef name, CustomCallHandler handler, | |
| DialectRegistration registerDialects) { | |
| if (name.empty() || !handler) { | |
| return failure(); | |
| } | |
| auto guardedHandler = [handler = std::move(handler)]( | |
| mlir::stablehlo::CustomCallOp op, | |
| PatternRewriter &rewriter) -> LogicalResult { | |
| // Tokens and futures can occur inside tuples. Check every leaf so nested | |
| // values cannot bypass the ordering and completion restrictions below. | |
| auto hasOrderingType = [](Type type) { | |
| SmallVector<Type> leaves; | |
| if (auto tuple = dyn_cast<TupleType>(type)) { | |
| tuple.getFlattenedTypes(leaves); | |
| } else { | |
| leaves.push_back(type); | |
| } | |
| return llvm::any_of(leaves, [](Type leaf) { | |
| return isa<mlir::stablehlo::TokenType, mlir::stablehlo::FutureType>( | |
| leaf); | |
| }); | |
| }; | |
| // Registered dispatch supports pure value rewrites. Effects and | |
| // asynchronous completion need ordering guarantees; aliases need storage | |
| // identity. These contracts are outside this registry, so leave such calls | |
| // unchanged. | |
| if (op.getHasSideEffect() || !op.getOutputOperandAliases().empty() || | |
| llvm::any_of(op.getOperandTypes(), hasOrderingType) || | |
| llvm::any_of(op.getResultTypes(), hasOrderingType)) { | |
| return failure(); | |
| } | |
| return handler(op, rewriter); | |
| }; | |
| return success( | |
| customCalls | |
| .try_emplace(name, | |
| Entry<CustomCallHandler>{std::move(guardedHandler), | |
| std::move(registerDialects)}) | |
| .second); | |
| } | |
| LogicalResult | |
| TargetRegistry::addComposite(StringRef name, int32_t version, | |
| CompositeHandler handler, | |
| DialectRegistration registerDialects) { | |
| if (name.empty() || !handler) { | |
| return failure(); | |
| } | |
| return success( | |
| composites[name] | |
| .emplace(version, | |
| Entry<CompositeHandler>{std::move(handler), | |
| std::move(registerDialects)}) | |
| .second); | |
| } | |
| const TargetRegistry::CustomCallHandler * | |
| TargetRegistry::lookupCustomCall(StringRef name) const { | |
| auto it = customCalls.find(name); | |
| return it == customCalls.end() ? nullptr : &it->second.handler; | |
| } | |
| const TargetRegistry::CompositeHandler * | |
| TargetRegistry::lookupComposite(StringRef name, int32_t version) const { | |
| auto versions = composites.find(name); | |
| if (versions == composites.end()) { | |
| return nullptr; | |
| } | |
| auto it = versions->second.find(version); | |
| return it == versions->second.end() ? nullptr : &it->second.handler; | |
| } | |
| void TargetRegistry::registerDependentDialects( | |
| DialectRegistry &dialects) const { | |
| for (const auto &customCall : customCalls) { | |
| if (customCall.second.registerDialects) { | |
| customCall.second.registerDialects(dialects); | |
| } | |
| } | |
| for (const auto &versions : composites) { | |
| for (const auto &[version, entry] : versions.second) { | |
| if (entry.registerDialects) { | |
| entry.registerDialects(dialects); | |
| } | |
| } | |
| } | |
| } | |
| namespace { | |
| using RegistrySnapshot = std::shared_ptr<const TargetRegistry>; | |
| struct CustomCallDispatch final | |
| : OpRewritePattern<mlir::stablehlo::CustomCallOp> { | |
| CustomCallDispatch(MLIRContext *context, RegistrySnapshot registry) | |
| : OpRewritePattern(context), registry(std::move(registry)) {} | |
| LogicalResult matchAndRewrite(mlir::stablehlo::CustomCallOp op, | |
| PatternRewriter &rewriter) const override { | |
| auto *handler = registry->lookupCustomCall(op.getCallTargetName()); | |
| return handler ? (*handler)(op, rewriter) : failure(); | |
| } | |
| RegistrySnapshot registry; | |
| }; | |
| struct CompositeDispatch final | |
| : OpRewritePattern<mlir::stablehlo::CompositeOp> { | |
| CompositeDispatch(MLIRContext *context, RegistrySnapshot registry) | |
| : OpRewritePattern(context), registry(std::move(registry)) {} | |
| LogicalResult matchAndRewrite(mlir::stablehlo::CompositeOp op, | |
| PatternRewriter &rewriter) const override { | |
| auto *handler = registry->lookupComposite(op.getName(), op.getVersion()); | |
| if (!handler) { | |
| return failure(); | |
| } | |
| // Ops the handler creates carry the composite's name and numerical profile | |
| // so later diagnostics can name their origin. | |
| std::string provenance = ("composite " + op.getName()).str(); | |
| if (auto profile = op.getCompositeAttributes().getAs<StringAttr>( | |
| "numerical_profile")) { | |
| provenance += (" (" + profile.getValue() + ")").str(); | |
| } | |
| OpBuilder::Listener *listener = rewriter.getListener(); | |
| ProvenanceListener provenanceListener(listener, | |
| rewriter.getStringAttr(provenance)); | |
| rewriter.setListener(&provenanceListener); | |
| LogicalResult result = (*handler)(op, rewriter); | |
| rewriter.setListener(listener); | |
| return result; | |
| } | |
| // Adds the composite's name to the locations of newly inserted operations. | |
| // Handlers must use the supplied rewriter: a separate builder would bypass | |
| // both this listener and the greedy rewrite driver. | |
| struct ProvenanceListener final : RewriterBase::ForwardingListener { | |
| ProvenanceListener(OpBuilder::Listener *listener, StringAttr provenance) | |
| : ForwardingListener(listener), provenance(provenance) {} | |
| void notifyOperationInserted(Operation *op, | |
| OpBuilder::InsertPoint previous) override { | |
| // A moved op keeps its location. | |
| if (!previous.isSet()) { | |
| op->setLoc(NameLoc::get(provenance, op->getLoc())); | |
| } | |
| ForwardingListener::notifyOperationInserted(op, previous); | |
| } | |
| StringAttr provenance; | |
| }; | |
| RegistrySnapshot registry; | |
| }; | |
| void populatePatterns(RewritePatternSet &patterns, RegistrySnapshot registry) { | |
| patterns.add<CustomCallDispatch, CompositeDispatch>(patterns.getContext(), | |
| registry); | |
| } | |
| class RewriteRegisteredCustomCallsAndCompositesPass final | |
| : public impl::RewriteRegisteredCustomCallsAndCompositesPassBase< | |
| RewriteRegisteredCustomCallsAndCompositesPass> { | |
| public: | |
| explicit RewriteRegisteredCustomCallsAndCompositesPass( | |
| const TargetRegistry ®istry) | |
| : registry(std::make_shared<const TargetRegistry>(registry)) {} | |
| RewriteRegisteredCustomCallsAndCompositesPass( | |
| const RewriteRegisteredCustomCallsAndCompositesPass &) = default; | |
| void getDependentDialects(DialectRegistry &dialects) const override { | |
| Base::getDependentDialects(dialects); | |
| registry->registerDependentDialects(dialects); | |
| } | |
| void runOnOperation() override { | |
| RewritePatternSet patterns(&getContext()); | |
| populatePatterns(patterns, registry); | |
| if (failed(applyPatternsGreedily(getOperation(), std::move(patterns)))) { | |
| signalPassFailure(); | |
| } | |
| } | |
| private: | |
| RegistrySnapshot registry; | |
| }; | |
| } // namespace | |
| void populateTargetRewritePatterns(RewritePatternSet &patterns, | |
| const TargetRegistry ®istry) { | |
| populatePatterns(patterns, std::make_shared<const TargetRegistry>(registry)); | |
| } | |
| std::unique_ptr<Pass> createRewriteRegisteredCustomCallsAndCompositesPass( | |
| const TargetRegistry ®istry) { | |
| return std::make_unique<RewriteRegisteredCustomCallsAndCompositesPass>( | |
| registry); | |
| } | |
| } // namespace mlir::iree_compiler::stablehlo |