LLZK 3.0.0
An open-source IR for Zero Knowledge (ZK) circuits
Loading...
Searching...
No Matches
ArrayToScalarPass.cpp
Go to the documentation of this file.
1//===-- ArrayToScalarPass.cpp -----------------------------------*- C++ -*-===//
2//
3// Part of the LLZK Project, under the Apache License v2.0.
4// See LICENSE.txt for license information.
5// Copyright 2025 Veridise Inc.
6// SPDX-License-Identifier: Apache-2.0
7//
8//===----------------------------------------------------------------------===//
19///
20/// 2. Run a dialect conversion that does the following:
21///
22/// - Replace `MemberReadOp` and `MemberWriteOp` targeting the members that were split in step 1
23/// so they instead perform scalar reads and writes from the new members. The transformation is
24/// local to the current op. Therefore, when replacing the `MemberReadOp` a new array is
25/// created locally and all uses of the `MemberReadOp` are replaced with the new array Value,
26/// then each scalar member read is followed by scalar write into the new array. Similarly,
27/// when replacing a `MemberWriteOp`, each element in the array operand needs a scalar read
28/// from the array followed by a scalar write to the new member. Making only local changes
29/// keeps this step simple and later steps will optimize.
30///
31/// - Replace `ArrayLengthOp` with the constant size of the selected dimension.
32///
33/// - Remove element initialization from `CreateArrayOp` and instead insert a list of
34/// `WriteArrayOp` immediately following.
35///
36/// - Desugar `InsertArrayOp` and `ExtractArrayOp` into their element-wise scalar reads/writes.
37///
38/// - Split arrays to scalars in `FuncDefOp`, `CallOp`, and `ReturnOp` and insert the necessary
39/// create/read/write ops so the changes are as local as possible (just as described for
40/// `MemberReadOp` and `MemberWriteOp`)
41///
42/// 3. Replace branch-local reads (in `scf.if`) with the value written by a same-index write op that
43/// dominates the parent `scf.if` (because the passes below cannot handle that case).
44///
45/// 4. Run MLIR "sroa" pass to split each array with linear size `N` into `N` arrays of size 1
46/// (to prepare for "mem2reg" pass because its API cannot deal with splitting up memory).
47///
48/// 5. Run MLIR "mem2reg" pass to convert all of the size 1 array allocation and access into SSA
49/// values. This pass also runs several standard optimizations so the final result is condensed.
50///
51/// 6. Remove array allocations that become unread after memory promotion, then remove SSA values
52/// made dead by that cleanup.
53///
54/// Note: This transformation imposes a "last write wins" semantics on array elements. If
55/// different/configurable semantics are added in the future, some additional transformation would
56/// be necessary before/during this pass so that multiple writes to the same index can be handled
57/// properly while they still exist.
58///
59/// Note: This transformation will introduce a `nondet` op when there exists a read from an array
60/// index that was not earlier written to.
88#include "llzk/Util/Concepts.h"
89
90#include <mlir/Dialect/SCF/IR/SCF.h>
91#include <mlir/IR/BuiltinOps.h>
92#include <mlir/Pass/PassManager.h>
93#include <mlir/Transforms/DialectConversion.h>
94#include <mlir/Transforms/Passes.h>
95
96#include <llvm/Support/Debug.h>
97
98#include <optional>
99
100// Include the generated base pass class definitions.
101namespace llzk::array {
102#define GEN_PASS_DEF_ARRAYTOSCALARPASS
104} // namespace llzk::array
105
106using namespace mlir;
107using namespace llzk;
108using namespace llzk::array;
109using namespace llzk::component;
110using namespace llzk::function;
111using namespace llzk::polymorphic;
112
113#define DEBUG_TYPE "llzk-array-to-scalar"
114
115namespace {
116
118inline ArrayType splittableArray(ArrayType at) {
119 return at.hasStaticShape() && !llvm::isa<NoneType>(at.getElementType()) ? at : nullptr;
120}
121
123inline ArrayType splittableArray(Type t) {
124 if (ArrayType at = dyn_cast<ArrayType>(t)) {
125 return splittableArray(at);
126 } else {
127 return nullptr;
128 }
129}
130
133inline bool containsSplittableArrayType(ArrayRef<Type> types) {
134 for (Type t : types) {
135 if (splittableArray(t)) {
136 return true;
137 }
138 }
139 return false;
140}
141
143template <typename T> bool containsSplittableArrayType(ValueTypeRange<T> types) {
144 for (Type t : types) {
145 if (splittableArray(t)) {
146 return true;
147 }
148 }
149 return false;
150}
151
154size_t splitArrayTypeTo(Type t, SmallVector<Type> &collect) {
155 if (ArrayType at = splittableArray(t)) {
156 size_t size = llzk::checkedCast<size_t>(at.getNumElements());
157 collect.append(size, at.getElementType());
158 return size;
159 } else {
160 collect.push_back(t);
161 return 1;
162 }
163}
164
166template <typename TypeCollection>
167inline void splitArrayTypeTo(
168 TypeCollection types, SmallVector<Type> &collect, SmallVector<size_t> *originalIdxToSize
169) {
170 for (Type t : types) {
171 size_t count = splitArrayTypeTo(t, collect);
172 if (originalIdxToSize) {
173 originalIdxToSize->push_back(count);
174 }
175 }
176}
177
180template <typename TypeCollection>
181inline SmallVector<Type>
182splitArrayType(TypeCollection types, SmallVector<size_t> *originalIdxToSize = nullptr) {
183 SmallVector<Type> collect;
184 splitArrayTypeTo(types, collect, originalIdxToSize);
185 return collect;
186}
187
189static std::string formatSplitArrayIndexSuffix(ArrayAttr index) {
190 std::string suffix;
191 llvm::raw_string_ostream os(suffix);
192 for (Attribute attr : index) {
193 os << '[';
194 attr.print(os, true);
195 os << ']';
196 }
197 return suffix;
198}
199
201static SmallVector<std::string> getSplitArrayIndexSuffixes(Type type) {
202 SmallVector<std::string> suffixes;
203 if (ArrayType at = splittableArray(type)) {
204 std::optional<SmallVector<ArrayAttr>> indices = at.getSubelementIndices();
205 assert(indices.has_value() && "static-shape arrays must provide subelement indices");
206 suffixes.reserve(indices->size());
207 for (ArrayAttr index : *indices) {
208 suffixes.push_back(formatSplitArrayIndexSuffix(index));
209 }
210 }
211 return suffixes;
212}
213
215CallOp newCallOpWithSplitResults(
216 CallOp oldCall, CallOp::Adaptor adaptor, ConversionPatternRewriter &rewriter
217) {
218 OpBuilder::InsertionGuard guard(rewriter);
219 rewriter.setInsertionPointAfter(oldCall);
220
221 Operation::result_range oldResults = oldCall.getResults();
223 oldCall.getLoc(), splitArrayType(oldResults.getTypes()), oldCall, adaptor.getMapOperands(),
224 adaptor.getArgOperands(), rewriter
225 );
226
227 auto newResults = newCall.getResults().begin();
228 for (Value oldVal : oldResults) {
229 if (ArrayType at = splittableArray(oldVal.getType())) {
230 Location loc = oldVal.getLoc();
231 // Generate `CreateArrayOp` and replace uses of the result with it.
232 auto newArray = rewriter.create<CreateArrayOp>(loc, at);
233 rewriter.replaceAllUsesWith(oldVal, newArray);
234
235 // For all indices in the ArrayType (i.e., the element count), write the next
236 // result from the new CallOp to the new array.
237 std::optional<SmallVector<ArrayAttr>> allIndices = at.getSubelementIndices();
238 assert(allIndices); // follows from legal() check
239 assert(std::cmp_equal(allIndices->size(), at.getNumElements()));
240 for (ArrayAttr subIdx : allIndices.value()) {
241 ArrayAccessOpInterface::genWrite(rewriter, loc, newArray, subIdx, *newResults);
242 newResults++;
243 }
244 } else {
245 rewriter.replaceAllUsesWith(oldVal, *newResults);
246 newResults++;
247 }
248 }
249 // erase the original CallOp
250 rewriter.eraseOp(oldCall);
251
252 return newCall;
253}
254
257void processInputOperand(
258 Location loc, Value operand, SmallVector<Value> &newOperands,
259 ConversionPatternRewriter &rewriter
260) {
261 if (ArrayType at = splittableArray(operand.getType())) {
262 std::optional<SmallVector<ArrayAttr>> indices = at.getSubelementIndices();
263 assert(indices.has_value() && "passed earlier hasStaticShape() check");
264 for (ArrayAttr index : indices.value()) {
265 newOperands.push_back(ArrayAccessOpInterface::genRead(rewriter, loc, operand, index));
266 }
267 } else {
268 newOperands.push_back(operand);
269 }
270}
271
273void processInputOperands(
274 ValueRange operands, MutableOperandRange outputOpRef, Operation *op,
275 ConversionPatternRewriter &rewriter
276) {
277 SmallVector<Value> newOperands;
278 for (Value v : operands) {
279 processInputOperand(op->getLoc(), v, newOperands, rewriter);
280 }
281 rewriter.modifyOpInPlace(op, [&outputOpRef, &newOperands]() {
282 outputOpRef.assign(ValueRange(newOperands));
283 });
284}
285
286namespace {
287
288enum Direction : std::uint8_t {
290 SMALL_TO_LARGE,
292 LARGE_TO_SMALL,
293};
294
297template <Direction dir>
298inline void rewriteImpl(
299 ArrayAccessOpInterface op, ArrayType smallType, Value smallArr, Value largeArr,
300 ConversionPatternRewriter &rewriter
301) {
302 assert(smallType); // follows from legal() check
303 Location loc = op.getLoc();
304 MLIRContext *ctx = op.getContext();
305
306 ArrayAttr indexAsAttr = op.indexOperandsToAttributeArray();
307 assert(indexAsAttr); // follows from legal() check
308
309 // For all indices in the ArrayType (i.e., the element count), read from one array into the other
310 // (depending on direction flag).
311 std::optional<SmallVector<ArrayAttr>> subIndices = smallType.getSubelementIndices();
312 assert(subIndices); // follows from legal() check
313 assert(std::cmp_equal(subIndices->size(), smallType.getNumElements()));
314 for (ArrayAttr indexingTail : subIndices.value()) {
315 SmallVector<Attribute> joined;
316 joined.append(indexAsAttr.begin(), indexAsAttr.end());
317 joined.append(indexingTail.begin(), indexingTail.end());
318 ArrayAttr fullIndex = ArrayAttr::get(ctx, joined);
319
320 if constexpr (dir == Direction::SMALL_TO_LARGE) {
321 auto init = ArrayAccessOpInterface::genRead(rewriter, loc, smallArr, indexingTail);
322 ArrayAccessOpInterface::genWrite(rewriter, loc, largeArr, fullIndex, init);
323 } else if constexpr (dir == Direction::LARGE_TO_SMALL) {
324 auto init = ArrayAccessOpInterface::genRead(rewriter, loc, largeArr, fullIndex);
325 ArrayAccessOpInterface::genWrite(rewriter, loc, smallArr, indexingTail, init);
326 }
327 }
328}
329
330} // namespace
331
333class SplitInsertArrayOp : public OpConversionPattern<InsertArrayOp> {
334public:
335 using OpConversionPattern<InsertArrayOp>::OpConversionPattern;
336
337 static bool legal(InsertArrayOp op) {
338 return !containsSplittableArrayType(op.getRvalue().getType());
339 }
340
341 LogicalResult matchAndRewrite(
342 InsertArrayOp op, OpAdaptor adaptor, ConversionPatternRewriter &rewriter
343 ) const override {
344 if (legal(op)) {
345 return failure();
346 }
347 ArrayType at = splittableArray(op.getRvalue().getType());
348 rewriteImpl<SMALL_TO_LARGE>(
349 llvm::cast<ArrayAccessOpInterface>(op.getOperation()), at, adaptor.getRvalue(),
350 adaptor.getArrRef(), rewriter
351 );
352 rewriter.eraseOp(op);
353 return success();
354 }
355};
356
358class SplitExtractArrayOp : public OpConversionPattern<ExtractArrayOp> {
359public:
360 using OpConversionPattern<ExtractArrayOp>::OpConversionPattern;
361
362 static bool legal(ExtractArrayOp op) {
363 return !containsSplittableArrayType(op.getResult().getType());
364 }
365
366 LogicalResult matchAndRewrite(
367 ExtractArrayOp op, OpAdaptor adaptor, ConversionPatternRewriter &rewriter
368 ) const override {
369 if (legal(op)) {
370 return failure();
371 }
372 ArrayType at = splittableArray(op.getResult().getType());
373 // Generate `CreateArrayOp` in place of the current op.
374 auto newArray = rewriter.replaceOpWithNewOp<CreateArrayOp>(op, at);
375 rewriteImpl<LARGE_TO_SMALL>(
376 llvm::cast<ArrayAccessOpInterface>(op.getOperation()), at, newArray, adaptor.getArrRef(),
377 rewriter
378 );
379 return success();
380 }
381};
382
384class SplitInitFromCreateArrayOp : public OpConversionPattern<CreateArrayOp> {
385public:
386 using OpConversionPattern<CreateArrayOp>::OpConversionPattern;
387
388 static bool legal(CreateArrayOp op) { return op.getElements().empty(); }
389
390 LogicalResult matchAndRewrite(
391 CreateArrayOp op, OpAdaptor adaptor, ConversionPatternRewriter &rewriter
392 ) const override {
393 if (legal(op)) {
394 return failure();
395 }
396 // Remove elements from `op`
397 rewriter.modifyOpInPlace(op, [&op]() { op.getElementsMutable().clear(); });
398 // Generate an individual write for each initialization element
399 rewriter.setInsertionPointAfter(op);
400 Location loc = op.getLoc();
401 ArrayIndexGen idxGen = ArrayIndexGen::from(op.getType());
402 for (auto [i, init] : llvm::enumerate(adaptor.getElements())) {
403 // Convert the linear index 'i' into a multi-dim index
404 std::optional<SmallVector<Value>> multiDimIdxVals =
405 idxGen.delinearize(llzk::checkedCast<int64_t>(i), loc, rewriter);
406 // ASSERT: CreateArrayOp verifier ensures the number of elements provided matches the full
407 // linear array size so delinearization of `i` will not fail.
408 assert(multiDimIdxVals.has_value());
409 // Create the write
410 rewriter.create<WriteArrayOp>(loc, op.getResult(), ValueRange(*multiDimIdxVals), init);
411 }
412 return success();
413 }
414};
415
417class SplitArrayInFuncDefOp : public OpConversionPattern<FuncDefOp> {
418public:
419 using OpConversionPattern<FuncDefOp>::OpConversionPattern;
420
421 inline static bool legal(FuncDefOp op) {
422 return !containsSplittableArrayType(op.getArgumentTypes()) &&
423 !containsSplittableArrayType(op.getResultTypes());
424 }
425
426 LogicalResult
427 matchAndRewrite(FuncDefOp op, OpAdaptor, ConversionPatternRewriter &rewriter) const override {
428 if (legal(op)) {
429 return failure();
430 }
431 // Update in/out types of the function to replace arrays with scalars
432 class Impl : public FunctionTypeConverter {
433 SmallVector<size_t> originalInputIdxToSize, originalResultIdxToSize;
434 SplitFunctionNameInfo inputNameInfo;
435 SplitFunctionNameInfo resultNameInfo;
436
437 protected:
438 SmallVector<Type> convertInputs(ArrayRef<Type> origTypes) override {
439 return splitArrayType(origTypes, &originalInputIdxToSize);
440 }
441 SmallVector<Type> convertResults(ArrayRef<Type> origTypes) override {
442 return splitArrayType(origTypes, &originalResultIdxToSize);
443 }
444 ArrayAttr convertInputAttrs(ArrayAttr origAttrs, SmallVector<Type> newTypes) override {
446 origAttrs, originalInputIdxToSize, newTypes, ARG_NAME_ATTR_NAME,
447 inputNameInfo.originalNames, inputNameInfo.existingNames,
448 inputNameInfo.splitNameSuffixes
449 );
450 }
451 ArrayAttr convertResultAttrs(ArrayAttr origAttrs, SmallVector<Type> newTypes) override {
453 origAttrs, originalResultIdxToSize, newTypes, RES_NAME_ATTR_NAME,
454 resultNameInfo.originalNames, resultNameInfo.existingNames,
455 resultNameInfo.splitNameSuffixes
456 );
457 }
458
463 void processBlockArgs(Block &entryBlock, RewriterBase &rewriter) override {
464 OpBuilder::InsertionGuard guard(rewriter);
465 rewriter.setInsertionPointToStart(&entryBlock);
466
467 for (unsigned i = 0; i < entryBlock.getNumArguments();) {
468 Value oldV = entryBlock.getArgument(i);
469 if (ArrayType at = splittableArray(oldV.getType())) {
470 Location loc = oldV.getLoc();
471 // Generate `CreateArrayOp` and replace uses of the argument with it.
472 auto newArray = rewriter.create<CreateArrayOp>(loc, at);
473 rewriter.replaceAllUsesWith(oldV, newArray);
474 // Remove the argument from the block
475 entryBlock.eraseArgument(i);
476 // For all indices in the ArrayType (i.e., the element count), generate a new block
477 // argument and a write of that argument to the new array.
478 std::optional<SmallVector<ArrayAttr>> allIndices = at.getSubelementIndices();
479 assert(allIndices); // follows from legal() check
480 assert(std::cmp_equal(allIndices->size(), at.getNumElements()));
481 for (ArrayAttr subIdx : allIndices.value()) {
482 BlockArgument newArg = entryBlock.insertArgument(i, at.getElementType(), loc);
483 ArrayAccessOpInterface::genWrite(rewriter, loc, newArray, subIdx, newArg);
484 ++i;
485 }
486 } else {
487 ++i;
488 }
489 }
490 }
491
492 public:
493 Impl(FuncDefOp op) {
494 ArrayAttr resultAttrs = op.getAllResultAttrs();
495 inputNameInfo = collectSplitFunctionNameInfo(op.getArgumentTypes(), [&](unsigned i) {
496 return op.getArgNameAttr(i);
497 }, getSplitArrayIndexSuffixes);
498 resultNameInfo = collectSplitFunctionNameInfo(op.getResultTypes(), [&](unsigned i) {
499 return getAttrAtIndexWithName(resultAttrs, i, RES_NAME_ATTR_NAME);
500 }, getSplitArrayIndexSuffixes);
501 }
502 };
503 Impl(op).convert(op, rewriter);
504 return success();
505 }
506};
507
509class SplitArrayInReturnOp : public OpConversionPattern<ReturnOp> {
510public:
511 using OpConversionPattern<ReturnOp>::OpConversionPattern;
512
513 inline static bool legal(ReturnOp op) {
514 return !containsSplittableArrayType(op.getOperands().getTypes());
515 }
516
517 LogicalResult matchAndRewrite(
518 ReturnOp op, OpAdaptor adaptor, ConversionPatternRewriter &rewriter
519 ) const override {
520 if (legal(op)) {
521 return failure();
522 }
523 processInputOperands(adaptor.getOperands(), op.getOperandsMutable(), op, rewriter);
524 return success();
525 }
526};
527
529class SplitArrayInCallOp : public OpConversionPattern<CallOp> {
530public:
531 using OpConversionPattern<CallOp>::OpConversionPattern;
532
533 inline static bool legal(CallOp op) {
534 return !containsSplittableArrayType(op.getArgOperands().getTypes()) &&
535 !containsSplittableArrayType(op.getResultTypes());
536 }
537
538 LogicalResult matchAndRewrite(
539 CallOp op, OpAdaptor adaptor, ConversionPatternRewriter &rewriter
540 ) const override {
541 if (legal(op)) {
542 return failure();
543 }
544 // Create new CallOp with split results first so, then process its inputs to split types
545 CallOp newCall = newCallOpWithSplitResults(op, adaptor, rewriter);
546 processInputOperands(
547 newCall.getArgOperands(), newCall.getArgOperandsMutable(), newCall, rewriter
548 );
549 return success();
550 }
551};
552
554class ReplaceKnownArrayLengthOp : public OpConversionPattern<ArrayLengthOp> {
555public:
556 using OpConversionPattern<ArrayLengthOp>::OpConversionPattern;
557
559 static std::optional<llvm::APInt> getDimSizeIfKnown(Value dimIdx, ArrayType baseArrType) {
560 if (baseArrType.hasStaticShape()) {
561 llvm::APInt idxAP;
562 if (mlir::matchPattern(dimIdx, mlir::m_ConstantInt(&idxAP))) {
563 std::optional<int64_t> signedIdx = idxAP.trySExtValue();
564 if (!signedIdx || *signedIdx < 0) {
565 return std::nullopt;
566 }
567 size_t idx = llzk::checkedCast<size_t>(*signedIdx);
568 ArrayRef<Attribute> dimSizes = baseArrType.getDimensionSizes();
569 if (idx >= dimSizes.size()) {
570 return std::nullopt;
571 }
572 Attribute dimSizeAttr = dimSizes[idx];
573 if (mlir::matchPattern(dimSizeAttr, mlir::m_ConstantInt(&idxAP))) {
574 return idxAP;
575 }
576 }
577 }
578 return std::nullopt;
579 }
580
581 inline static bool legal(ArrayLengthOp op) {
582 // rewrite() can only work with constant dim size, i.e., must consider it legal otherwise
583 return !getDimSizeIfKnown(op.getDim(), op.getArrRefType()).has_value();
584 }
585
586 LogicalResult matchAndRewrite(
587 ArrayLengthOp op, OpAdaptor adaptor, ConversionPatternRewriter &rewriter
588 ) const override {
589 if (legal(op)) {
590 return failure();
591 }
592 ArrayType arrTy = dyn_cast<ArrayType>(adaptor.getArrRef().getType());
593 assert(arrTy); // must have array type per ODS spec of ArrayLengthOp
594 std::optional<llvm::APInt> len = getDimSizeIfKnown(adaptor.getDim(), arrTy);
595 assert(len.has_value()); // follows from legal() check
596 rewriter.replaceOpWithNewOp<arith::ConstantIndexOp>(op, llzk::fromAPInt(len.value()));
597 return success();
598 }
599};
600
602using MemberInfo = std::pair<StringAttr, Type>;
604using LocalMemberReplacementMap = DenseMap<ArrayAttr, MemberInfo>;
606using MemberReplacementMap = DenseMap<StructDefOp, DenseMap<StringAttr, LocalMemberReplacementMap>>;
607
609class SplitArrayInMemberDefOp : public OpConversionPattern<MemberDefOp> {
610 SymbolTableCollection &tables;
611 MemberReplacementMap &repMapRef;
612
613public:
614 SplitArrayInMemberDefOp(
615 MLIRContext *ctx, SymbolTableCollection &symTables, MemberReplacementMap &memberRepMap
616 )
617 : OpConversionPattern<MemberDefOp>(ctx), tables(symTables), repMapRef(memberRepMap) {}
618
619 inline static bool legal(MemberDefOp op) { return !containsSplittableArrayType(op.getType()); }
620
621 LogicalResult
622 matchAndRewrite(MemberDefOp op, OpAdaptor, ConversionPatternRewriter &rewriter) const override {
623 if (legal(op)) {
624 return failure();
625 }
626 StructDefOp inStruct = op->getParentOfType<StructDefOp>();
627 assert(inStruct);
628 LocalMemberReplacementMap &localRepMapRef = repMapRef[inStruct][op.getSymNameAttr()];
629
630 ArrayType arrTy = dyn_cast<ArrayType>(op.getType());
631 assert(arrTy); // follows from legal() check
632 auto subIdxs = arrTy.getSubelementIndices();
633 assert(subIdxs.has_value());
634 Type elemTy = arrTy.getElementType();
635
636 SymbolTable &structSymbolTable = tables.getSymbolTable(inStruct);
637 for (ArrayAttr idx : subIdxs.value()) {
638 // Create scalar version of the member
639 MemberDefOp newMember = rewriter.create<MemberDefOp>(
640 op.getLoc(), op.getSymNameAttr(), elemTy, op.getSignal(), op.getColumn()
641 );
642 newMember.setPublicAttr(op.hasPublicAttr());
643 // Use SymbolTable to give it a unique name and store to the replacement map
644 localRepMapRef[idx] = std::make_pair(structSymbolTable.insert(newMember), elemTy);
645 }
646 rewriter.eraseOp(op);
647 return success();
648 }
649};
650
652class SplitArrayInMemberWriteOp : public SplitAggregateInMemberRefOp<
653 SplitArrayInMemberWriteOp, MemberWriteOp, void *, ArrayAttr> {
654public:
656 SplitArrayInMemberWriteOp, MemberWriteOp, void *, ArrayAttr>::SplitAggregateInMemberRefOp;
657
658 static bool legal(MemberWriteOp op) {
659 return !containsSplittableArrayType(op.getVal().getType());
660 }
661
662 static void *genHeader(MemberWriteOp, ConversionPatternRewriter &) { return nullptr; }
663
664 static void forId(
665 Location loc, void *, ArrayAttr idx, MemberInfo newMember, OpAdaptor adaptor,
666 ConversionPatternRewriter &rewriter
667 ) {
668 Value scalarRead = ArrayAccessOpInterface::genRead(rewriter, loc, adaptor.getVal(), idx);
669 rewriter.create<MemberWriteOp>(
670 loc, adaptor.getComponent(), FlatSymbolRefAttr::get(newMember.first), scalarRead
671 );
672 }
673};
674
677class SplitArrayInMemberReadOp
679 SplitArrayInMemberReadOp, MemberReadOp, CreateArrayOp, ArrayAttr> {
680public:
682 SplitArrayInMemberReadOp, MemberReadOp, CreateArrayOp,
684
685 static bool legal(MemberReadOp op) {
686 return !containsSplittableArrayType(op.getResult().getType());
687 }
688
689 static CreateArrayOp genHeader(MemberReadOp op, ConversionPatternRewriter &rewriter) {
690 CreateArrayOp newArray =
691 rewriter.create<CreateArrayOp>(op.getLoc(), llvm::cast<ArrayType>(op.getType()));
692 rewriter.replaceAllUsesWith(op, newArray);
693 return newArray;
694 }
695
696 static void forId(
697 Location loc, CreateArrayOp newArray, ArrayAttr idx, MemberInfo newMember, OpAdaptor adaptor,
698 ConversionPatternRewriter &rewriter
699 ) {
700 MemberReadOp scalarRead = rewriter.create<MemberReadOp>(
701 loc, newMember.second, adaptor.getComponent(), newMember.first
702 );
703 ArrayAccessOpInterface::genWrite(rewriter, loc, newArray, idx, scalarRead);
704 }
705};
706
708static void baseTargetSetup(ConversionTarget &target) {
709 target.addLegalDialect<
714 scf::SCFDialect>();
715 target.addLegalOp<ModuleOp>();
716}
717
719class NondetToNewArray : public OpConversionPattern<NonDetOp> {
720 using OpConversionPattern<NonDetOp>::OpConversionPattern;
721 LogicalResult matchAndRewrite(
722 NonDetOp nondetOp, OpAdaptor, ConversionPatternRewriter &rewriter
723 ) const override {
724 if (auto at = dyn_cast<ArrayType>(nondetOp.getType())) {
725 auto wildcardTy = llvm::cast<ArrayType>(replaceAffineMapArrayDimsWithWildcards(at));
726 auto newArray = rewriter.create<CreateArrayOp>(nondetOp.getLoc(), wildcardTy);
727 if (wildcardTy == at) {
728 rewriter.replaceOp(nondetOp, newArray);
729 } else {
730 auto cast = rewriter.create<UnifiableCastOp>(nondetOp.getLoc(), at, newArray);
731 rewriter.replaceOp(nondetOp, cast.getResult());
732 }
733 return success();
734 }
735 return failure();
736 }
737};
738
740static LogicalResult step0(ModuleOp modOp) {
741 MLIRContext *ctx = modOp.getContext();
742 RewritePatternSet patterns {ctx};
743 patterns.add<NondetToNewArray>(ctx);
744 ConversionTarget target {*ctx};
745
746 baseTargetSetup(target);
747 target.addDynamicallyLegalOp<NonDetOp>([](NonDetOp op) { return !isa<ArrayType>(op.getType()); });
748
749 return applyFullConversion(modOp, target, std::move(patterns));
750}
751
753static LogicalResult
754step1(ModuleOp modOp, SymbolTableCollection &symTables, MemberReplacementMap &memberRepMap) {
755 MLIRContext *ctx = modOp.getContext();
756
757 RewritePatternSet patterns(ctx);
758
759 patterns.add<SplitArrayInMemberDefOp>(ctx, symTables, memberRepMap);
760
761 ConversionTarget target(*ctx);
762 baseTargetSetup(target);
763 target.addDynamicallyLegalOp<MemberDefOp>(SplitArrayInMemberDefOp::legal);
764
765 LLVM_DEBUG(llvm::dbgs() << "Begin step 1: split array-type members\n";);
766 return applyFullConversion(modOp, target, std::move(patterns));
767}
768
771static LogicalResult
772step2(ModuleOp modOp, SymbolTableCollection &symTables, const MemberReplacementMap &memberRepMap) {
773 MLIRContext *ctx = modOp.getContext();
774
775 RewritePatternSet patterns(ctx);
776 patterns.add<
777 // clang-format off
778 SplitInitFromCreateArrayOp,
779 SplitInsertArrayOp,
780 SplitExtractArrayOp,
781 SplitArrayInFuncDefOp,
782 SplitArrayInReturnOp,
783 SplitArrayInCallOp,
784 ReplaceKnownArrayLengthOp
785 // clang-format on
786 >(ctx);
787
788 patterns.add<
789 // clang-format off
790 SplitArrayInMemberWriteOp,
791 SplitArrayInMemberReadOp
792 // clang-format on
793 >(ctx, symTables, memberRepMap);
794
795 ConversionTarget target(*ctx);
796 baseTargetSetup(target);
797 target.addDynamicallyLegalOp<CreateArrayOp>(SplitInitFromCreateArrayOp::legal);
798 target.addDynamicallyLegalOp<InsertArrayOp>(SplitInsertArrayOp::legal);
799 target.addDynamicallyLegalOp<ExtractArrayOp>(SplitExtractArrayOp::legal);
800 target.addDynamicallyLegalOp<FuncDefOp>(SplitArrayInFuncDefOp::legal);
801 target.addDynamicallyLegalOp<ReturnOp>(SplitArrayInReturnOp::legal);
802 target.addDynamicallyLegalOp<CallOp>(SplitArrayInCallOp::legal);
803 target.addDynamicallyLegalOp<ArrayLengthOp>(ReplaceKnownArrayLengthOp::legal);
804 target.addDynamicallyLegalOp<MemberWriteOp>(SplitArrayInMemberWriteOp::legal);
805 target.addDynamicallyLegalOp<MemberReadOp>(SplitArrayInMemberReadOp::legal);
806
807 LLVM_DEBUG(llvm::dbgs() << "Begin step 2: update/split other array ops\n";);
808 return applyFullConversion(modOp, target, std::move(patterns));
809}
810
812inline static ArrayAttr getIndexAsAttr(ArrayAccessOpInterface op) {
814}
815
817static bool mayWriteToIndex(WriteArrayOp writeOp, ArrayAttr index) {
818 ArrayAttr writeIndex = getIndexAsAttr(writeOp);
819 return !writeIndex || writeIndex == index;
820}
821
823static bool hasEarlierWriteInBlock(ReadArrayOp readOp, ArrayAttr readIndex) {
824 Value arrRef = readOp.getArrRef();
825 for (Operation &op : *readOp->getBlock()) {
826 if (&op == readOp.getOperation()) {
827 return false;
828 }
829
830 if (auto writeOp = dyn_cast<WriteArrayOp>(&op)) {
831 if (writeOp.getArrRef() == arrRef && mayWriteToIndex(writeOp, readIndex)) {
832 return true;
833 }
834 continue;
835 }
836
837 // Writes nested inside earlier operations may conditionally clobber the read's value.
838 if (op.walk([arrRef, readIndex](WriteArrayOp writeOp) {
839 if (writeOp.getArrRef() == arrRef && mayWriteToIndex(writeOp, readIndex)) {
840 return WalkResult::interrupt();
841 }
842 return WalkResult::advance();
843 }).wasInterrupted()) {
844 return true;
845 }
846 }
847 return false;
848}
849
851static std::optional<WriteArrayOp> findPrecedingWriteForIfRead(ReadArrayOp readOp) {
852 ArrayAttr readIndex = getIndexAsAttr(readOp);
853 if (!readIndex) {
854 return std::nullopt;
855 }
856
857 // Only handle reads that are direct children of an `scf.if` branch.
858 auto ifOp = readOp->getParentOfType<scf::IfOp>();
859 if (!ifOp || readOp->getBlock()->getParentOp() != ifOp.getOperation()) {
860 return std::nullopt;
861 }
862 if (hasEarlierWriteInBlock(readOp, readIndex)) {
863 return std::nullopt;
864 }
865
866 Block *ifBlock = ifOp->getBlock();
867 if (!ifBlock) {
868 return std::nullopt;
869 }
870
871 Value arrRef = readOp.getArrRef();
872 WriteArrayOp replacement;
873 for (Operation &op : *ifBlock) {
874 if (&op == ifOp.getOperation()) {
875 break;
876 }
877
878 if (auto writeOp = dyn_cast<WriteArrayOp>(&op)) {
879 if (writeOp.getArrRef() != arrRef) {
880 continue;
881 }
882
883 if (mayWriteToIndex(writeOp, readIndex)) {
884 ArrayAttr writeIndex = getIndexAsAttr(writeOp);
885 replacement = writeIndex == readIndex ? writeOp : WriteArrayOp();
886 }
887 continue;
888 }
889
890 // A nested write before the `scf.if` may overwrite the current candidate.
891 if (op.walk([arrRef, readIndex, &replacement](WriteArrayOp writeOp) {
892 if (writeOp.getArrRef() == arrRef && mayWriteToIndex(writeOp, readIndex)) {
893 replacement = WriteArrayOp();
894 return WalkResult::interrupt();
895 }
896 return WalkResult::advance();
897 }).wasInterrupted()) {
898 continue;
899 }
900 }
901
902 return replacement ? std::make_optional(replacement) : std::nullopt;
903}
904
907static void step3(ModuleOp modOp) {
908 SmallVector<std::pair<ReadArrayOp, Value>> replacements;
909 modOp.walk([&replacements](ReadArrayOp readOp) {
910 if (std::optional<WriteArrayOp> writeOp = findPrecedingWriteForIfRead(readOp)) {
911 replacements.emplace_back(readOp, writeOp->getRvalue());
912 }
913 });
914
915 for (auto [readOp, value] : replacements) {
916 readOp.getResult().replaceAllUsesWith(value);
917 readOp.erase();
918 }
919}
920
922class PassImpl : public llzk::array::impl::ArrayToScalarPassBase<PassImpl> {
923 using Base = ArrayToScalarPassBase<PassImpl>;
924 using Base::Base;
925
926 void runOnOperation() override {
927 ModuleOp module = getOperation();
928
929 if (failed(step0(module))) {
930 return signalPassFailure();
931 }
932 LLVM_DEBUG({
933 llvm::dbgs() << "After step 0:\n";
934 module.dump();
935 });
936
937 {
938 // This is divided into 2 steps to simplify the implementation for member-related ops. The
939 // issue is that the conversions for member read/write expect the mapping of array index to
940 // member name+type to already be populated for the referenced member (although this could be
941 // computed on demand if desired but it complicates the implementation a bit).
942 SymbolTableCollection symTables;
943 MemberReplacementMap memberRepMap;
944 if (failed(step1(module, symTables, memberRepMap))) {
945 return signalPassFailure();
946 }
947 LLVM_DEBUG({
948 llvm::dbgs() << "After step 1:\n";
949 module.dump();
950 });
951
952 if (failed(step2(module, symTables, memberRepMap))) {
953 return signalPassFailure();
954 }
955 LLVM_DEBUG({
956 llvm::dbgs() << "After step 2:\n";
957 module.dump();
958 });
959 }
960
961 step3(module);
962 LLVM_DEBUG({
963 llvm::dbgs() << "After step 3:\n";
964 module.dump();
965 });
966
967 OpPassManager nestedPM(ModuleOp::getOperationName());
968 // Use SROA (Destructurable* interfaces) to split each array with linear size `N` into `N`
969 // arrays of size 1. This is necessary because the mem2reg pass cannot deal with indexing
970 // and splitting up memory, i.e., it can only convert scalar memory access into SSA values.
972 // The mem2reg pass converts all of the size-1 array allocation and access into SSA values.
974 // Cleanup allocations made dead by memory promotion.
976 RemoveUnusedDiscardableAllocationsPassOptions {
977 .allocatorOpName = CreateArrayOp::getOperationName().str()
978 }
979 ));
980 // Cleanup SSA values made dead by removing allocations and writes.
981 nestedPM.addPass(createRemoveDeadValuesWorkaroundPass());
982 if (failed(runPipeline(nestedPM, module))) {
983 signalPassFailure();
984 return;
985 }
986 LLVM_DEBUG({
987 llvm::dbgs() << "After SROA+Mem2Reg pipeline:\n";
988 module.dump();
989 });
990 }
991};
992
993} // namespace
Provides SpecializedSROA<AllocOpTy> and SpecializedMem2Reg<AllocOpTy>: pass templates that replicate ...
General helper for converting a FuncDefOp by changing its input and/or result types and the associate...
Common implementation for handling MemberWriteOp and MemberReadOp while destructuring an aggregate ty...
static void genWrite(::mlir::OpBuilder &bldr, ::mlir::Location loc, ::mlir::Value arrayRef, ::mlir::ValueRange indices, ::mlir::Value value)
Create an array.write or array.insert for one concrete element or subarray.
::mlir::Value genRead(::mlir::OpBuilder &bldr, ::mlir::Location loc, ::mlir::Value arrayRef, ::mlir::ValueRange indices)
Create an array.read or array.extract for one concrete element or subarray.
::mlir::ArrayAttr indexOperandsToAttributeArray()
Returns the multi-dimensional indices of the array access as an Attribute array or a null pointer if ...
Definition Ops.cpp:216
Helper for converting between linear and multi-dimensional indexing with checks to ensure indices are...
static ArrayIndexGen from(ArrayType)
Construct new ArrayIndexGen. Will assert if hasStaticShape() is false.
std::optional< llvm::SmallVector< mlir::Value > > delinearize(int64_t, mlir::Location, mlir::OpBuilder &) const
inline ::llzk::array::ArrayType getArrRefType()
Gets the type of the referenced base array.
Definition Ops.h.inc:192
::mlir::TypedValue<::mlir::IndexType > getDim()
Definition Ops.h.inc:150
std::optional<::llvm::SmallVector<::mlir::ArrayAttr > > getSubelementIndices() const
Return a list of all valid indices for this ArrayType.
Definition Types.cpp:113
::mlir::Type getElementType() const
::llvm::ArrayRef<::mlir::Attribute > getDimensionSizes() const
::mlir::TypedValue<::llzk::array::ArrayType > getResult()
Definition Ops.h.inc:408
static constexpr ::llvm::StringLiteral getOperationName()
Definition Ops.h.inc:377
::mlir::MutableOperandRange getElementsMutable()
Definition Ops.cpp.inc:334
::mlir::Operation::operand_range getElements()
Definition Ops.h.inc:388
::mlir::TypedValue<::llzk::array::ArrayType > getResult()
Definition Ops.h.inc:613
::mlir::TypedValue<::llzk::array::ArrayType > getRvalue()
Definition Ops.h.inc:757
::mlir::TypedValue<::mlir::Type > getResult()
Definition Ops.h.inc:923
::mlir::TypedValue<::llzk::array::ArrayType > getArrRef()
Definition Ops.h.inc:899
::mlir::TypedValue<::llzk::array::ArrayType > getArrRef()
Definition Ops.h.inc:1070
bool hasPublicAttr()
Returns whether this member is a public output.
Definition Ops.h.inc:463
void setPublicAttr(bool newValue=true)
Adds or removes the unit llzk.pub attribute according to newValue.
Definition Ops.cpp:564
::mlir::StringAttr getSymNameAttr()
Definition Ops.h.inc:386
::mlir::TypedValue<::mlir::Type > getVal()
Definition Ops.h.inc:960
::llvm::SmallVector< RangeT > getMapOperands()
Definition Ops.h.inc:175
::mlir::MutableOperandRange getArgOperandsMutable()
Definition Ops.cpp.inc:223
::mlir::Operation::operand_range getArgOperands()
Definition Ops.h.inc:266
CallOpAdaptor Adaptor
Definition Ops.h.inc:205
::llvm::ArrayRef<::mlir::Type > getArgumentTypes()
Required by FunctionOpInterface.
Definition Ops.h.inc:883
::llvm::ArrayRef<::mlir::Type > getResultTypes()
Required by FunctionOpInterface.
Definition Ops.h.inc:887
::mlir::MutableOperandRange getOperandsMutable()
Definition Ops.cpp.inc:1169
::mlir::Operation::operand_range getOperands()
Definition Ops.h.inc:1024
constexpr char ARG_NAME_ATTR_NAME[]
Attribute name for source-level function argument names.
Definition Ops.h:35
constexpr char RES_NAME_ATTR_NAME[]
Attribute name for source-level function result names.
Definition Ops.h:38
std::unique_ptr<::mlir::Pass > createRemoveUnusedDiscardableAllocationsPass()
std::unique_ptr< mlir::Pass > createRemoveDeadValuesWorkaroundPass()
mlir::ArrayAttr replicateFunctionNameAttrsAsNeeded(mlir::ArrayAttr origAttrs, const llvm::SmallVector< size_t > &originalIdxToSize, const llvm::SmallVector< mlir::Type > &newTypes, llvm::StringRef functionNameAttrName, llvm::ArrayRef< std::optional< llvm::StringRef > > origNames={}, llvm::ArrayRef< llvm::StringRef > existingNames={}, llvm::ArrayRef< llvm::SmallVector< std::string > > splitNameSuffixes={})
Expand function arg/result attribute arrays to match a split signature, rewriting name attrs with the...
constexpr T checkedCast(U u) noexcept
Definition Compare.h:94
std::unique_ptr< SpecializedMem2Reg< AllocOpTy > > createSpecializedMem2RegPass()
int64_t fromAPInt(const llvm::APInt &i)
function::CallOp createCallPreservingInstantiationOperands(mlir::Location loc, mlir::TypeRange newResultTypes, function::CallOp oldCall, llvm::ArrayRef< mlir::ValueRange > mapOperands, mlir::ValueRange argOperands, mlir::ConversionPatternRewriter &rewriter)
Rebuild a function.call while preserving explicit instantiation state from oldCall.
SplitFunctionNameInfo collectSplitFunctionNameInfo(mlir::ArrayRef< mlir::Type > origTypes, GetNameAttrFn &&getNameAttr, GetSplitSuffixesFn &&getSplitSuffixes)
Collect function arg/result names and split suffixes from a list of original types.
std::unique_ptr< SpecializedSROA< AllocOpTy > > createSpecializedSROAPass()
Cached function arg/result names and split suffixes used while rewriting a function signature.
llvm::SmallVector< std::optional< llvm::StringRef > > originalNames
llvm::SmallVector< llvm::StringRef > existingNames
llvm::SmallVector< llvm::SmallVector< std::string > > splitNameSuffixes