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 match(InsertArrayOp op) const override { return failure(legal(op)); }
342
343 void
344 rewrite(InsertArrayOp op, OpAdaptor adaptor, ConversionPatternRewriter &rewriter) const override {
345 ArrayType at = splittableArray(op.getRvalue().getType());
346 rewriteImpl<SMALL_TO_LARGE>(
347 llvm::cast<ArrayAccessOpInterface>(op.getOperation()), at, adaptor.getRvalue(),
348 adaptor.getArrRef(), rewriter
349 );
350 rewriter.eraseOp(op);
351 }
352};
353
355class SplitExtractArrayOp : public OpConversionPattern<ExtractArrayOp> {
356public:
357 using OpConversionPattern<ExtractArrayOp>::OpConversionPattern;
358
359 static bool legal(ExtractArrayOp op) {
360 return !containsSplittableArrayType(op.getResult().getType());
361 }
362
363 LogicalResult match(ExtractArrayOp op) const override { return failure(legal(op)); }
364
365 void rewrite(
366 ExtractArrayOp op, OpAdaptor adaptor, ConversionPatternRewriter &rewriter
367 ) const override {
368 ArrayType at = splittableArray(op.getResult().getType());
369 // Generate `CreateArrayOp` in place of the current op.
370 auto newArray = rewriter.replaceOpWithNewOp<CreateArrayOp>(op, at);
371 rewriteImpl<LARGE_TO_SMALL>(
372 llvm::cast<ArrayAccessOpInterface>(op.getOperation()), at, newArray, adaptor.getArrRef(),
373 rewriter
374 );
375 }
376};
377
379class SplitInitFromCreateArrayOp : public OpConversionPattern<CreateArrayOp> {
380public:
381 using OpConversionPattern<CreateArrayOp>::OpConversionPattern;
382
383 static bool legal(CreateArrayOp op) { return op.getElements().empty(); }
384
385 LogicalResult match(CreateArrayOp op) const override { return failure(legal(op)); }
386
387 void
388 rewrite(CreateArrayOp op, OpAdaptor adaptor, ConversionPatternRewriter &rewriter) const override {
389 // Remove elements from `op`
390 rewriter.modifyOpInPlace(op, [&op]() { op.getElementsMutable().clear(); });
391 // Generate an individual write for each initialization element
392 rewriter.setInsertionPointAfter(op);
393 Location loc = op.getLoc();
394 ArrayIndexGen idxGen = ArrayIndexGen::from(op.getType());
395 for (auto [i, init] : llvm::enumerate(adaptor.getElements())) {
396 // Convert the linear index 'i' into a multi-dim index
397 std::optional<SmallVector<Value>> multiDimIdxVals =
398 idxGen.delinearize(llzk::checkedCast<int64_t>(i), loc, rewriter);
399 // ASSERT: CreateArrayOp verifier ensures the number of elements provided matches the full
400 // linear array size so delinearization of `i` will not fail.
401 assert(multiDimIdxVals.has_value());
402 // Create the write
403 rewriter.create<WriteArrayOp>(loc, op.getResult(), ValueRange(*multiDimIdxVals), init);
404 }
405 }
406};
407
409class SplitArrayInFuncDefOp : public OpConversionPattern<FuncDefOp> {
410public:
411 using OpConversionPattern<FuncDefOp>::OpConversionPattern;
412
413 inline static bool legal(FuncDefOp op) {
414 return !containsSplittableArrayType(op.getArgumentTypes()) &&
415 !containsSplittableArrayType(op.getResultTypes());
416 }
417
418 LogicalResult match(FuncDefOp op) const override { return failure(legal(op)); }
419
420 void rewrite(FuncDefOp op, OpAdaptor, ConversionPatternRewriter &rewriter) const override {
421 // Update in/out types of the function to replace arrays with scalars
422 class Impl : public FunctionTypeConverter {
423 SmallVector<size_t> originalInputIdxToSize, originalResultIdxToSize;
424 SplitFunctionNameInfo inputNameInfo;
425 SplitFunctionNameInfo resultNameInfo;
426
427 protected:
428 SmallVector<Type> convertInputs(ArrayRef<Type> origTypes) override {
429 return splitArrayType(origTypes, &originalInputIdxToSize);
430 }
431 SmallVector<Type> convertResults(ArrayRef<Type> origTypes) override {
432 return splitArrayType(origTypes, &originalResultIdxToSize);
433 }
434 ArrayAttr convertInputAttrs(ArrayAttr origAttrs, SmallVector<Type> newTypes) override {
436 origAttrs, originalInputIdxToSize, newTypes, ARG_NAME_ATTR_NAME,
437 inputNameInfo.originalNames, inputNameInfo.existingNames,
438 inputNameInfo.splitNameSuffixes
439 );
440 }
441 ArrayAttr convertResultAttrs(ArrayAttr origAttrs, SmallVector<Type> newTypes) override {
443 origAttrs, originalResultIdxToSize, newTypes, RES_NAME_ATTR_NAME,
444 resultNameInfo.originalNames, resultNameInfo.existingNames,
445 resultNameInfo.splitNameSuffixes
446 );
447 }
448
453 void processBlockArgs(Block &entryBlock, RewriterBase &rewriter) override {
454 OpBuilder::InsertionGuard guard(rewriter);
455 rewriter.setInsertionPointToStart(&entryBlock);
456
457 for (unsigned i = 0; i < entryBlock.getNumArguments();) {
458 Value oldV = entryBlock.getArgument(i);
459 if (ArrayType at = splittableArray(oldV.getType())) {
460 Location loc = oldV.getLoc();
461 // Generate `CreateArrayOp` and replace uses of the argument with it.
462 auto newArray = rewriter.create<CreateArrayOp>(loc, at);
463 rewriter.replaceAllUsesWith(oldV, newArray);
464 // Remove the argument from the block
465 entryBlock.eraseArgument(i);
466 // For all indices in the ArrayType (i.e., the element count), generate a new block
467 // argument and a write of that argument to the new array.
468 std::optional<SmallVector<ArrayAttr>> allIndices = at.getSubelementIndices();
469 assert(allIndices); // follows from legal() check
470 assert(std::cmp_equal(allIndices->size(), at.getNumElements()));
471 for (ArrayAttr subIdx : allIndices.value()) {
472 BlockArgument newArg = entryBlock.insertArgument(i, at.getElementType(), loc);
473 ArrayAccessOpInterface::genWrite(rewriter, loc, newArray, subIdx, newArg);
474 ++i;
475 }
476 } else {
477 ++i;
478 }
479 }
480 }
481
482 public:
483 Impl(FuncDefOp op) {
484 ArrayAttr resultAttrs = op.getAllResultAttrs();
485 inputNameInfo = collectSplitFunctionNameInfo(op.getArgumentTypes(), [&](unsigned i) {
486 return op.getArgNameAttr(i);
487 }, getSplitArrayIndexSuffixes);
488 resultNameInfo = collectSplitFunctionNameInfo(op.getResultTypes(), [&](unsigned i) {
489 return getAttrAtIndexWithName(resultAttrs, i, RES_NAME_ATTR_NAME);
490 }, getSplitArrayIndexSuffixes);
491 }
492 };
493 Impl(op).convert(op, rewriter);
494 }
495};
496
498class SplitArrayInReturnOp : public OpConversionPattern<ReturnOp> {
499public:
500 using OpConversionPattern<ReturnOp>::OpConversionPattern;
501
502 inline static bool legal(ReturnOp op) {
503 return !containsSplittableArrayType(op.getOperands().getTypes());
504 }
505
506 LogicalResult match(ReturnOp op) const override { return failure(legal(op)); }
507
508 void rewrite(ReturnOp op, OpAdaptor adaptor, ConversionPatternRewriter &rewriter) const override {
509 processInputOperands(adaptor.getOperands(), op.getOperandsMutable(), op, rewriter);
510 }
511};
512
514class SplitArrayInCallOp : public OpConversionPattern<CallOp> {
515public:
516 using OpConversionPattern<CallOp>::OpConversionPattern;
517
518 inline static bool legal(CallOp op) {
519 return !containsSplittableArrayType(op.getArgOperands().getTypes()) &&
520 !containsSplittableArrayType(op.getResultTypes());
521 }
522
523 LogicalResult match(CallOp op) const override { return failure(legal(op)); }
524
525 void rewrite(CallOp op, OpAdaptor adaptor, ConversionPatternRewriter &rewriter) const override {
526 // Create new CallOp with split results first so, then process its inputs to split types
527 CallOp newCall = newCallOpWithSplitResults(op, adaptor, rewriter);
528 processInputOperands(
529 newCall.getArgOperands(), newCall.getArgOperandsMutable(), newCall, rewriter
530 );
531 }
532};
533
535class ReplaceKnownArrayLengthOp : public OpConversionPattern<ArrayLengthOp> {
536public:
537 using OpConversionPattern<ArrayLengthOp>::OpConversionPattern;
538
540 static std::optional<llvm::APInt> getDimSizeIfKnown(Value dimIdx, ArrayType baseArrType) {
541 if (baseArrType.hasStaticShape()) {
542 llvm::APInt idxAP;
543 if (mlir::matchPattern(dimIdx, mlir::m_ConstantInt(&idxAP))) {
544 std::optional<int64_t> signedIdx = idxAP.trySExtValue();
545 if (!signedIdx || *signedIdx < 0) {
546 return std::nullopt;
547 }
548 size_t idx = llzk::checkedCast<size_t>(*signedIdx);
549 ArrayRef<Attribute> dimSizes = baseArrType.getDimensionSizes();
550 if (idx >= dimSizes.size()) {
551 return std::nullopt;
552 }
553 Attribute dimSizeAttr = dimSizes[idx];
554 if (mlir::matchPattern(dimSizeAttr, mlir::m_ConstantInt(&idxAP))) {
555 return idxAP;
556 }
557 }
558 }
559 return std::nullopt;
560 }
561
562 inline static bool legal(ArrayLengthOp op) {
563 // rewrite() can only work with constant dim size, i.e., must consider it legal otherwise
564 return !getDimSizeIfKnown(op.getDim(), op.getArrRefType()).has_value();
565 }
566
567 LogicalResult match(ArrayLengthOp op) const override { return failure(legal(op)); }
568
569 void
570 rewrite(ArrayLengthOp op, OpAdaptor adaptor, ConversionPatternRewriter &rewriter) const override {
571 ArrayType arrTy = dyn_cast<ArrayType>(adaptor.getArrRef().getType());
572 assert(arrTy); // must have array type per ODS spec of ArrayLengthOp
573 std::optional<llvm::APInt> len = getDimSizeIfKnown(adaptor.getDim(), arrTy);
574 assert(len.has_value()); // follows from legal() check
575 rewriter.replaceOpWithNewOp<arith::ConstantIndexOp>(op, llzk::fromAPInt(len.value()));
576 }
577};
578
580using MemberInfo = std::pair<StringAttr, Type>;
582using LocalMemberReplacementMap = DenseMap<ArrayAttr, MemberInfo>;
584using MemberReplacementMap = DenseMap<StructDefOp, DenseMap<StringAttr, LocalMemberReplacementMap>>;
585
587class SplitArrayInMemberDefOp : public OpConversionPattern<MemberDefOp> {
588 SymbolTableCollection &tables;
589 MemberReplacementMap &repMapRef;
590
591public:
592 SplitArrayInMemberDefOp(
593 MLIRContext *ctx, SymbolTableCollection &symTables, MemberReplacementMap &memberRepMap
594 )
595 : OpConversionPattern<MemberDefOp>(ctx), tables(symTables), repMapRef(memberRepMap) {}
596
597 inline static bool legal(MemberDefOp op) { return !containsSplittableArrayType(op.getType()); }
598
599 LogicalResult match(MemberDefOp op) const override { return failure(legal(op)); }
600
601 void rewrite(MemberDefOp op, OpAdaptor, ConversionPatternRewriter &rewriter) const override {
602 StructDefOp inStruct = op->getParentOfType<StructDefOp>();
603 assert(inStruct);
604 LocalMemberReplacementMap &localRepMapRef = repMapRef[inStruct][op.getSymNameAttr()];
605
606 ArrayType arrTy = dyn_cast<ArrayType>(op.getType());
607 assert(arrTy); // follows from legal() check
608 auto subIdxs = arrTy.getSubelementIndices();
609 assert(subIdxs.has_value());
610 Type elemTy = arrTy.getElementType();
611
612 SymbolTable &structSymbolTable = tables.getSymbolTable(inStruct);
613 for (ArrayAttr idx : subIdxs.value()) {
614 // Create scalar version of the member
615 MemberDefOp newMember = rewriter.create<MemberDefOp>(
616 op.getLoc(), op.getSymNameAttr(), elemTy, op.getSignal(), op.getColumn()
617 );
618 newMember.setPublicAttr(op.hasPublicAttr());
619 // Use SymbolTable to give it a unique name and store to the replacement map
620 localRepMapRef[idx] = std::make_pair(structSymbolTable.insert(newMember), elemTy);
621 }
622 rewriter.eraseOp(op);
623 }
624};
625
627class SplitArrayInMemberWriteOp : public SplitAggregateInMemberRefOp<
628 SplitArrayInMemberWriteOp, MemberWriteOp, void *, ArrayAttr> {
629public:
631 SplitArrayInMemberWriteOp, MemberWriteOp, void *, ArrayAttr>::SplitAggregateInMemberRefOp;
632
633 static bool legal(MemberWriteOp op) {
634 return !containsSplittableArrayType(op.getVal().getType());
635 }
636
637 static void *genHeader(MemberWriteOp, ConversionPatternRewriter &) { return nullptr; }
638
639 static void forId(
640 Location loc, void *, ArrayAttr idx, MemberInfo newMember, OpAdaptor adaptor,
641 ConversionPatternRewriter &rewriter
642 ) {
643 Value scalarRead = ArrayAccessOpInterface::genRead(rewriter, loc, adaptor.getVal(), idx);
644 rewriter.create<MemberWriteOp>(
645 loc, adaptor.getComponent(), FlatSymbolRefAttr::get(newMember.first), scalarRead
646 );
647 }
648};
649
652class SplitArrayInMemberReadOp
654 SplitArrayInMemberReadOp, MemberReadOp, CreateArrayOp, ArrayAttr> {
655public:
657 SplitArrayInMemberReadOp, MemberReadOp, CreateArrayOp,
659
660 static bool legal(MemberReadOp op) {
661 return !containsSplittableArrayType(op.getResult().getType());
662 }
663
664 static CreateArrayOp genHeader(MemberReadOp op, ConversionPatternRewriter &rewriter) {
665 CreateArrayOp newArray =
666 rewriter.create<CreateArrayOp>(op.getLoc(), llvm::cast<ArrayType>(op.getType()));
667 rewriter.replaceAllUsesWith(op, newArray);
668 return newArray;
669 }
670
671 static void forId(
672 Location loc, CreateArrayOp newArray, ArrayAttr idx, MemberInfo newMember, OpAdaptor adaptor,
673 ConversionPatternRewriter &rewriter
674 ) {
675 MemberReadOp scalarRead = rewriter.create<MemberReadOp>(
676 loc, newMember.second, adaptor.getComponent(), newMember.first
677 );
678 ArrayAccessOpInterface::genWrite(rewriter, loc, newArray, idx, scalarRead);
679 }
680};
681
683static void baseTargetSetup(ConversionTarget &target) {
684 target.addLegalDialect<
689 scf::SCFDialect>();
690 target.addLegalOp<ModuleOp>();
691}
692
694class NondetToNewArray : public OpConversionPattern<NonDetOp> {
695 using OpConversionPattern<NonDetOp>::OpConversionPattern;
696 LogicalResult matchAndRewrite(
697 NonDetOp nondetOp, OpAdaptor, ConversionPatternRewriter &rewriter
698 ) const override {
699 if (auto at = dyn_cast<ArrayType>(nondetOp.getType())) {
700 auto wildcardTy = llvm::cast<ArrayType>(replaceAffineMapArrayDimsWithWildcards(at));
701 auto newArray = rewriter.create<CreateArrayOp>(nondetOp.getLoc(), wildcardTy);
702 if (wildcardTy == at) {
703 rewriter.replaceOp(nondetOp, newArray);
704 } else {
705 auto cast = rewriter.create<UnifiableCastOp>(nondetOp.getLoc(), at, newArray);
706 rewriter.replaceOp(nondetOp, cast.getResult());
707 }
708 return success();
709 }
710 return failure();
711 }
712};
713
715static LogicalResult step0(ModuleOp modOp) {
716 MLIRContext *ctx = modOp.getContext();
717 RewritePatternSet patterns {ctx};
718 patterns.add<NondetToNewArray>(ctx);
719 ConversionTarget target {*ctx};
720
721 baseTargetSetup(target);
722 target.addDynamicallyLegalOp<NonDetOp>([](NonDetOp op) { return !isa<ArrayType>(op.getType()); });
723
724 return applyFullConversion(modOp, target, std::move(patterns));
725}
726
728static LogicalResult
729step1(ModuleOp modOp, SymbolTableCollection &symTables, MemberReplacementMap &memberRepMap) {
730 MLIRContext *ctx = modOp.getContext();
731
732 RewritePatternSet patterns(ctx);
733
734 patterns.add<SplitArrayInMemberDefOp>(ctx, symTables, memberRepMap);
735
736 ConversionTarget target(*ctx);
737 baseTargetSetup(target);
738 target.addDynamicallyLegalOp<MemberDefOp>(SplitArrayInMemberDefOp::legal);
739
740 LLVM_DEBUG(llvm::dbgs() << "Begin step 1: split array-type members\n";);
741 return applyFullConversion(modOp, target, std::move(patterns));
742}
743
746static LogicalResult
747step2(ModuleOp modOp, SymbolTableCollection &symTables, const MemberReplacementMap &memberRepMap) {
748 MLIRContext *ctx = modOp.getContext();
749
750 RewritePatternSet patterns(ctx);
751 patterns.add<
752 // clang-format off
753 SplitInitFromCreateArrayOp,
754 SplitInsertArrayOp,
755 SplitExtractArrayOp,
756 SplitArrayInFuncDefOp,
757 SplitArrayInReturnOp,
758 SplitArrayInCallOp,
759 ReplaceKnownArrayLengthOp
760 // clang-format on
761 >(ctx);
762
763 patterns.add<
764 // clang-format off
765 SplitArrayInMemberWriteOp,
766 SplitArrayInMemberReadOp
767 // clang-format on
768 >(ctx, symTables, memberRepMap);
769
770 ConversionTarget target(*ctx);
771 baseTargetSetup(target);
772 target.addDynamicallyLegalOp<CreateArrayOp>(SplitInitFromCreateArrayOp::legal);
773 target.addDynamicallyLegalOp<InsertArrayOp>(SplitInsertArrayOp::legal);
774 target.addDynamicallyLegalOp<ExtractArrayOp>(SplitExtractArrayOp::legal);
775 target.addDynamicallyLegalOp<FuncDefOp>(SplitArrayInFuncDefOp::legal);
776 target.addDynamicallyLegalOp<ReturnOp>(SplitArrayInReturnOp::legal);
777 target.addDynamicallyLegalOp<CallOp>(SplitArrayInCallOp::legal);
778 target.addDynamicallyLegalOp<ArrayLengthOp>(ReplaceKnownArrayLengthOp::legal);
779 target.addDynamicallyLegalOp<MemberWriteOp>(SplitArrayInMemberWriteOp::legal);
780 target.addDynamicallyLegalOp<MemberReadOp>(SplitArrayInMemberReadOp::legal);
781
782 LLVM_DEBUG(llvm::dbgs() << "Begin step 2: update/split other array ops\n";);
783 return applyFullConversion(modOp, target, std::move(patterns));
784}
785
787inline static ArrayAttr getIndexAsAttr(ArrayAccessOpInterface op) {
789}
790
792static bool mayWriteToIndex(WriteArrayOp writeOp, ArrayAttr index) {
793 ArrayAttr writeIndex = getIndexAsAttr(writeOp);
794 return !writeIndex || writeIndex == index;
795}
796
798static bool hasEarlierWriteInBlock(ReadArrayOp readOp, ArrayAttr readIndex) {
799 Value arrRef = readOp.getArrRef();
800 for (Operation &op : *readOp->getBlock()) {
801 if (&op == readOp.getOperation()) {
802 return false;
803 }
804
805 if (auto writeOp = dyn_cast<WriteArrayOp>(&op)) {
806 if (writeOp.getArrRef() == arrRef && mayWriteToIndex(writeOp, readIndex)) {
807 return true;
808 }
809 continue;
810 }
811
812 // Writes nested inside earlier operations may conditionally clobber the read's value.
813 if (op.walk([arrRef, readIndex](WriteArrayOp writeOp) {
814 if (writeOp.getArrRef() == arrRef && mayWriteToIndex(writeOp, readIndex)) {
815 return WalkResult::interrupt();
816 }
817 return WalkResult::advance();
818 }).wasInterrupted()) {
819 return true;
820 }
821 }
822 return false;
823}
824
826static std::optional<WriteArrayOp> findPrecedingWriteForIfRead(ReadArrayOp readOp) {
827 ArrayAttr readIndex = getIndexAsAttr(readOp);
828 if (!readIndex) {
829 return std::nullopt;
830 }
831
832 // Only handle reads that are direct children of an `scf.if` branch.
833 auto ifOp = readOp->getParentOfType<scf::IfOp>();
834 if (!ifOp || readOp->getBlock()->getParentOp() != ifOp.getOperation()) {
835 return std::nullopt;
836 }
837 if (hasEarlierWriteInBlock(readOp, readIndex)) {
838 return std::nullopt;
839 }
840
841 Block *ifBlock = ifOp->getBlock();
842 if (!ifBlock) {
843 return std::nullopt;
844 }
845
846 Value arrRef = readOp.getArrRef();
847 WriteArrayOp replacement;
848 for (Operation &op : *ifBlock) {
849 if (&op == ifOp.getOperation()) {
850 break;
851 }
852
853 if (auto writeOp = dyn_cast<WriteArrayOp>(&op)) {
854 if (writeOp.getArrRef() != arrRef) {
855 continue;
856 }
857
858 if (mayWriteToIndex(writeOp, readIndex)) {
859 ArrayAttr writeIndex = getIndexAsAttr(writeOp);
860 replacement = writeIndex == readIndex ? writeOp : WriteArrayOp();
861 }
862 continue;
863 }
864
865 // A nested write before the `scf.if` may overwrite the current candidate.
866 if (op.walk([arrRef, readIndex, &replacement](WriteArrayOp writeOp) {
867 if (writeOp.getArrRef() == arrRef && mayWriteToIndex(writeOp, readIndex)) {
868 replacement = WriteArrayOp();
869 return WalkResult::interrupt();
870 }
871 return WalkResult::advance();
872 }).wasInterrupted()) {
873 continue;
874 }
875 }
876
877 return replacement ? std::make_optional(replacement) : std::nullopt;
878}
879
882static void step3(ModuleOp modOp) {
883 SmallVector<std::pair<ReadArrayOp, Value>> replacements;
884 modOp.walk([&replacements](ReadArrayOp readOp) {
885 if (std::optional<WriteArrayOp> writeOp = findPrecedingWriteForIfRead(readOp)) {
886 replacements.emplace_back(readOp, writeOp->getRvalue());
887 }
888 });
889
890 for (auto [readOp, value] : replacements) {
891 readOp.getResult().replaceAllUsesWith(value);
892 readOp.erase();
893 }
894}
895
897class PassImpl : public llzk::array::impl::ArrayToScalarPassBase<PassImpl> {
898 using Base = ArrayToScalarPassBase<PassImpl>;
899 using Base::Base;
900
901 void runOnOperation() override {
902 ModuleOp module = getOperation();
903
904 if (failed(step0(module))) {
905 return signalPassFailure();
906 }
907 LLVM_DEBUG({
908 llvm::dbgs() << "After step 0:\n";
909 module.dump();
910 });
911
912 {
913 // This is divided into 2 steps to simplify the implementation for member-related ops. The
914 // issue is that the conversions for member read/write expect the mapping of array index to
915 // member name+type to already be populated for the referenced member (although this could be
916 // computed on demand if desired but it complicates the implementation a bit).
917 SymbolTableCollection symTables;
918 MemberReplacementMap memberRepMap;
919 if (failed(step1(module, symTables, memberRepMap))) {
920 return signalPassFailure();
921 }
922 LLVM_DEBUG({
923 llvm::dbgs() << "After step 1:\n";
924 module.dump();
925 });
926
927 if (failed(step2(module, symTables, memberRepMap))) {
928 return signalPassFailure();
929 }
930 LLVM_DEBUG({
931 llvm::dbgs() << "After step 2:\n";
932 module.dump();
933 });
934 }
935
936 step3(module);
937 LLVM_DEBUG({
938 llvm::dbgs() << "After step 3:\n";
939 module.dump();
940 });
941
942 OpPassManager nestedPM(ModuleOp::getOperationName());
943 // Use SROA (Destructurable* interfaces) to split each array with linear size `N` into `N`
944 // arrays of size 1. This is necessary because the mem2reg pass cannot deal with indexing
945 // and splitting up memory, i.e., it can only convert scalar memory access into SSA values.
947 // The mem2reg pass converts all of the size-1 array allocation and access into SSA values.
949 // Cleanup allocations made dead by memory promotion.
951 RemoveUnusedDiscardableAllocationsPassOptions {
952 .allocatorOpName = CreateArrayOp::getOperationName().str()
953 }
954 ));
955 // Cleanup SSA values made dead by removing allocations and writes.
956 nestedPM.addPass(createRemoveDeadValuesWorkaroundPass());
957 if (failed(runPipeline(nestedPM, module))) {
958 signalPassFailure();
959 return;
960 }
961 LLVM_DEBUG({
962 llvm::dbgs() << "After SROA+Mem2Reg pipeline:\n";
963 module.dump();
964 });
965 }
966};
967
968} // 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:566
::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:870
::llvm::ArrayRef<::mlir::Type > getResultTypes()
Required by FunctionOpInterface.
Definition Ops.h.inc:874
::mlir::MutableOperandRange getOperandsMutable()
Definition Ops.cpp.inc:1169
::mlir::Operation::operand_range getOperands()
Definition Ops.h.inc:1011
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:81
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