LLZK 3.0.0
An open-source IR for Zero Knowledge (ZK) circuits
Loading...
Searching...
No Matches
SharedImpl.cpp
Go to the documentation of this file.
1//===-- SharedImpl.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//===----------------------------------------------------------------------===//
9
10#include "SharedImpl.h"
11
26
27#include <llvm/ADT/DenseMap.h>
28#include <llvm/ADT/STLExtras.h>
29#include <llvm/ADT/SmallVector.h>
30#include <llvm/Support/Debug.h>
31
32#define DEBUG_TYPE "poly-dialect-shared"
33
34using namespace mlir;
35
36namespace {
37
39constexpr char OPEN_SLOT_MARKER = '\x1A';
40
41} // namespace
42
44
45FailureOr<InstantiationLayout> buildInstantiationLayout(
46 TemplateOp parentTemplate, ArrayAttr callParams,
47 const DenseMap<Attribute, Attribute> &paramNameToConcrete
48) {
49 MLIRContext *ctx = parentTemplate.getContext();
50 SmallVector<Attribute> paramNames = parentTemplate.getConstNames<TemplateParamOp>();
51 SmallVector<StringAttr> sourceChunks;
52
53 if (Attribute rawPattern = parentTemplate->getDiscardableAttr(TEMPLATE_NAME_PATTERN_ATTR)) {
54 auto pattern = llvm::dyn_cast<ArrayAttr>(rawPattern);
55 if (!pattern) {
56 return parentTemplate.emitOpError()
57 << "expected '" << TEMPLATE_NAME_PATTERN_ATTR << "' to be an ArrayAttr";
58 }
59 if (pattern.size() != paramNames.size() + 1) {
60 return parentTemplate.emitOpError()
61 << "expected '" << TEMPLATE_NAME_PATTERN_ATTR << "' to contain "
62 << paramNames.size() + 1 << " literal chunk(s) for " << paramNames.size()
63 << " template parameter(s), but found " << pattern.size();
64 }
65 for (auto [index, chunk] : llvm::enumerate(pattern)) {
66 auto stringChunk = llvm::dyn_cast<StringAttr>(chunk);
67 if (!stringChunk) {
68 return parentTemplate.emitOpError() << "expected '" << TEMPLATE_NAME_PATTERN_ATTR
69 << "' element " << index << " to be a StringAttr";
70 }
71 sourceChunks.push_back(stringChunk);
72 }
73 } else {
74 std::string firstChunk = parentTemplate.getSymName().str();
75 if (!paramNames.empty()) {
76 firstChunk.push_back('_');
77 }
78 sourceChunks.push_back(StringAttr::get(ctx, firstChunk));
79 for (size_t i = 1; i < paramNames.size(); ++i) {
80 sourceChunks.push_back(StringAttr::get(ctx, "_"));
81 }
82 if (!paramNames.empty()) {
83 sourceChunks.push_back(StringAttr::get(ctx, ""));
84 }
85 }
86
87 SmallVector<Attribute> remainingNames;
88 SmallVector<Attribute> concreteKeyEntries;
89 for (Attribute paramName : paramNames) {
90 auto concreteIt = paramNameToConcrete.find(paramName);
91 if (concreteIt == paramNameToConcrete.end() || !concreteIt->second) {
92 remainingNames.push_back(paramName);
93 continue;
94 }
95
96 concreteKeyEntries.push_back(paramName);
97 concreteKeyEntries.push_back(concreteIt->second);
98 }
99
100 ArrayAttr rewrittenCallParams = nullptr;
101 if (!isNullOrEmpty(callParams) && !remainingNames.empty()) {
102 assert(callParams.size() == paramNames.size() && "template parameter arity already verified");
103 SmallVector<Attribute> remainingCallParams;
104 for (auto [paramName, attr] : llvm::zip_equal(paramNames, callParams.getValue())) {
105 auto concreteIt = paramNameToConcrete.find(paramName);
106 if (concreteIt == paramNameToConcrete.end() || !concreteIt->second) {
107 remainingCallParams.push_back(attr);
108 }
109 }
110 rewrittenCallParams = ArrayAttr::get(ctx, remainingCallParams);
111 }
112
113 // Refine chunks in parameter order. A concrete gap is absorbed into the current literal chunk;
114 // an open gap starts the next chunk. This preserves empty chunks and therefore exact P+1 shape.
115 SmallVector<std::string> refinedChunkValues;
116 refinedChunkValues.push_back(sourceChunks.front().getValue().str());
117 for (size_t i = 0; i < paramNames.size(); ++i) {
118 auto concreteIt = paramNameToConcrete.find(paramNames[i]);
119 if (concreteIt != paramNameToConcrete.end() && concreteIt->second) {
120 Attribute binding = concreteIt->second;
121 refinedChunkValues.back() += BuildShortTypeString::from(binding);
122 refinedChunkValues.back() += sourceChunks[i + 1].getValue().str();
123 } else {
124 refinedChunkValues.push_back(sourceChunks[i + 1].getValue().str());
125 }
126 }
127 assert(
128 refinedChunkValues.size() == remainingNames.size() + 1 &&
129 "every unbound parameter creates one remaining gap"
130 );
131
132 std::string renderedName;
133 for (auto [index, chunk] : llvm::enumerate(refinedChunkValues)) {
134 if (index != 0) {
135 renderedName.push_back(OPEN_SLOT_MARKER);
136 }
137 renderedName += chunk;
138 }
139
140 SmallVector<Attribute> refinedPattern;
141 refinedPattern.reserve(refinedChunkValues.size());
142 for (const std::string &chunk : refinedChunkValues) {
143 refinedPattern.push_back(StringAttr::get(ctx, chunk));
144 }
145
146 return InstantiationLayout {
147 std::move(remainingNames), ArrayAttr::get(ctx, concreteKeyEntries), std::move(renderedName),
148 rewrittenCallParams, ArrayAttr::get(ctx, refinedPattern),
149 };
150}
151
152void setInstantiationNamePattern(TemplateOp templateOp, ArrayAttr namePattern) {
153 if (namePattern) {
154 templateOp->setDiscardableAttr(TEMPLATE_NAME_PATTERN_ATTR, namePattern);
155 } else {
156 templateOp->removeDiscardableAttr(TEMPLATE_NAME_PATTERN_ATTR);
157 }
158}
159
160std::string buildOpaqueInstantiationName(StringRef baseName, ArrayRef<Attribute> concreteAttrs) {
161 std::string result = baseName.str();
162 if (!concreteAttrs.empty()) {
163 result.push_back('_');
164 result += BuildShortTypeString::from(concreteAttrs);
165 }
166 return result;
167}
168
169} // namespace llzk::polymorphic::detail
170
171ConversionTarget llzk::polymorphic::detail::newBaseTarget(MLIRContext *ctx) {
172 ConversionTarget target(*ctx);
173 target.addLegalDialect<
178 llzk::string::StringDialect, arith::ArithDialect, scf::SCFDialect>();
179 target.addLegalOp<ModuleOp>();
180 return target;
181}
182
184 ModuleOp root, const llzk::SymbolDefTree &symDefTree, const llzk::SymbolUseGraph &symUseGraph
185)
186 : rootMod(root), defTree(symDefTree), useGraph(symUseGraph) {}
187
189 if (llvm::isa<llzk::component::StructDefOp>(op)) {
190 return true;
191 }
192 if (llzk::function::FuncDefOp fdef = llvm::dyn_cast<llzk::function::FuncDefOp>(op)) {
193 return !fdef.isInStruct();
194 }
195 return false;
196}
197
199 ModuleOp root, const llzk::SymbolDefTree &symDefTree, const llzk::SymbolUseGraph &symUseGraph,
200 DenseSet<SymbolRefAttr> &&tryToErasePaths
201)
202 : CleanupBase(root, symDefTree, symUseGraph) {
203 // Convert the set of paths targeted for erasure into a set of cleanup-candidate definitions.
204 for (SymbolRefAttr path : tryToErasePaths) {
205 LLVM_DEBUG(llvm::dbgs() << "[FromEraseSet] path to erase: " << path << '\n';);
206 Operation *lookupFrom = rootMod.getOperation();
207 auto res = lookupSymbolIn(tables, path, Within(), lookupFrom);
208 assert(succeeded(res) && "inputs must be valid symbol references");
209 assert(isErasableDefinition(res->get()) && "inputs must be cleanup candidates");
210 if (!res->viaInclude()) { // do not remove if it's from another source file
211 SymbolOpInterface op = llvm::cast<SymbolOpInterface>(res->get());
212 LLVM_DEBUG(llvm::dbgs() << "[FromEraseSet] added op to the erase set: " << op << '\n';);
213 tryToErase.insert(op);
214 } else {
215 LLVM_DEBUG(
216 llvm::dbgs() << "[FromEraseSet] ignored op because it comes from an include: "
217 << res->get() << '\n';
218 );
219 }
220 }
221}
222
224 // Collect the subset of 'tryToErase' that has no remaining uses.
225 for (SymbolOpInterface sym : tryToErase) {
226 collectSafeToErase(sym);
227 }
228 // The `visitedPlusSafetyResult` may contain child FuncDefOp within an erased StructDefOp, so
229 // reduce the map to only top-level erase targets before erasing in a separate loop.
230 for (auto &it : llvm::make_early_inc_range(visitedPlusSafetyResult)) {
231 if (!it.second || !tryToErase.contains(it.first)) {
232 visitedPlusSafetyResult.erase(it.first);
233 }
234 }
235 for (auto &[sym, _] : visitedPlusSafetyResult) {
236 LLVM_DEBUG(llvm::dbgs() << "[EraseIfUnused] removing: " << sym.getNameAttr() << '\n');
237 sym.erase();
238 }
239 return success();
240}
241
242bool llzk::polymorphic::detail::FromEraseSet::collectSafeToErase(SymbolOpInterface check) {
243 assert(check); // pre-condition
244
245 // If previously visited, return the safety result.
246 auto visited = visitedPlusSafetyResult.find(check);
247 if (visited != visitedPlusSafetyResult.end()) {
248 return visited->second;
249 }
250
251 // If it's an erasable definition that is not in `tryToErase` then it cannot be erased.
252 if (isErasableDefinition(check.getOperation()) && !tryToErase.contains(check)) {
253 visitedPlusSafetyResult[check] = false;
254 return false;
255 }
256
257 // Otherwise, temporarily mark as safe b/c a node cannot keep itself live (and this prevents
258 // the recursion from getting stuck in an infinite loop).
259 visitedPlusSafetyResult[check] = true;
260
261 // Check if it's safe according to both the def tree and use graph.
262 // Note: Every symbol must have a def node, but symbols with no references do not have use
263 // nodes. Those are safe from the use-graph perspective.
264 if (collectSafeToErase(defTree.lookupNode(check))) {
265 const auto *useNode = useGraph.lookupNode(check);
266 if (!useNode || collectSafeToErase(useNode)) {
267 return true;
268 }
269 }
270
271 // Otherwise, revert the safety decision and return it.
272 visitedPlusSafetyResult[check] = false;
273 return false;
274}
275
276bool llzk::polymorphic::detail::FromEraseSet::collectSafeToErase(
277 const llzk::SymbolDefTreeNode *check
278) {
279 assert(check); // pre-condition
280 if (const llzk::SymbolDefTreeNode *p = check->getParent()) {
281 if (SymbolOpInterface checkOp = p->getOp()) { // safe if parent is root
282 return collectSafeToErase(checkOp);
283 }
284 }
285 return true;
286}
287
288bool llzk::polymorphic::detail::FromEraseSet::collectSafeToErase(
289 const llzk::SymbolUseGraphNode *check
290) {
291 assert(check); // pre-condition
292 for (const llzk::SymbolUseGraphNode *p : check->predecessorIter()) {
293 if (SymbolOpInterface checkOp = cachedLookup(p)) { // safe if via IncludeOp
294 if (!collectSafeToErase(checkOp)) {
295 return false;
296 }
297 }
298 }
299 return true;
300}
301
302SymbolOpInterface
303llzk::polymorphic::detail::FromEraseSet::cachedLookup(const llzk::SymbolUseGraphNode *node) {
304 assert(node && "must provide a node"); // pre-condition
305 // Check for cached result
306 auto fromCache = lookupCache.find(node);
307 if (fromCache != lookupCache.end()) {
308 return fromCache->second;
309 }
310 // Otherwise, perform lookup and cache
311 auto lookupRes = node->lookupSymbol(tables);
312 assert(succeeded(lookupRes) && "graph contains node with invalid path");
313 assert(lookupRes->get() != nullptr && "lookup must return an Operation");
314 // If loaded via an IncludeOp it's not in the current AST anyway so ignore.
315 // NOTE: The SymbolUseGraph does contain nodes for struct parameters which cannot cast to
316 // SymbolOpInterface. However, those will always be leaf nodes in the SymbolUseGraph and
317 // therefore will not be traversed by this analysis so directly casting is fine.
318 SymbolOpInterface actualRes =
319 lookupRes->viaInclude() ? nullptr : llvm::cast<SymbolOpInterface>(lookupRes->get());
320 // Cache and return
321 lookupCache[node] = actualRes;
322 assert((!actualRes == lookupRes->viaInclude()) && "not found iff included"); // post-condition
323 return actualRes;
324}
325
327 llzk::array::ArrayType inputTy, Type convertedElemTy
328) {
329 SmallVector<Attribute> mergedDims(inputTy.getDimensionSizes());
330 while (auto nestedArrTy = llvm::dyn_cast<llzk::array::ArrayType>(convertedElemTy)) {
331 llvm::append_range(mergedDims, nestedArrTy.getDimensionSizes());
332 convertedElemTy = nestedArrTy.getElementType();
333 }
334 return llzk::array::ArrayType::get(convertedElemTy, mergedDims);
335}
336
337#undef DEBUG_TYPE
#define check(x)
Definition Ops.cpp:286
Common private implementation for poly dialect passes.
This file defines methods symbol lookup across LLZK operations and included files.
static std::string from(mlir::Type type)
Return a brief string representation of the given LLZK type.
Definition TypeHelper.h:53
Builds a tree structure representing the symbol table structure.
mlir::FailureOr< SymbolLookupResultUntyped > lookupSymbol(mlir::SymbolTableCollection &tables, bool reportMissing=true) const
Builds a graph structure representing the relationships between symbols and their uses.
static ArrayType get(::mlir::Type elementType, ::llvm::ArrayRef<::mlir::Attribute > dimensionSizes)
Definition Types.cpp.inc:83
::llvm::ArrayRef<::mlir::Attribute > getDimensionSizes() const
::llvm::SmallVector<::mlir::Attribute > getConstNames()
Return the names of all ops of type OpT within the body region in the order they are defined.
Definition Ops.h.inc:943
::llvm::StringRef getSymName()
Definition Ops.cpp.inc:1059
const SymbolUseGraph & useGraph
Definition SharedImpl.h:78
mlir::SymbolTableCollection tables
Definition SharedImpl.h:69
CleanupBase(mlir::ModuleOp root, const SymbolDefTree &symDefTree, const SymbolUseGraph &symUseGraph)
mlir::LogicalResult eraseUnusedDefinitions()
FromEraseSet(mlir::ModuleOp root, const SymbolDefTree &symDefTree, const SymbolUseGraph &symUseGraph, llvm::DenseSet< mlir::SymbolRefAttr > &&tryToErasePaths)
Note: paths in tryToErase should be relative to root.
FailureOr< InstantiationLayout > buildInstantiationLayout(TemplateOp parentTemplate, ArrayAttr callParams, const DenseMap< Attribute, Attribute > &paramNameToConcrete)
array::ArrayType flattenInstantiatedArrayType(array::ArrayType inputTy, mlir::Type convertedElemTy)
Merge nested array dimensions produced by replacing an array element type.
void setInstantiationNamePattern(TemplateOp templateOp, ArrayAttr namePattern)
std::string buildOpaqueInstantiationName(StringRef baseName, ArrayRef< Attribute > concreteAttrs)
bool isErasableDefinition(mlir::Operation *op)
Return true iff op is a cleanup candidate.
mlir::ConversionTarget newBaseTarget(mlir::MLIRContext *ctx)
Return a new ConversionTarget allowing all LLZK-required dialects.
constexpr llvm::StringLiteral TEMPLATE_NAME_PATTERN_ATTR
Metadata carried across transformation passes preserving the literal chunks of a partially-instantiat...
Definition Ops.h:34
bool isNullOrEmpty(mlir::ArrayAttr a)
mlir::FailureOr< SymbolLookupResultUntyped > lookupSymbolIn(mlir::SymbolTableCollection &tables, mlir::SymbolRefAttr symbol, Within &&lookupWithin, mlir::Operation *origin, bool reportMissing=true)
Groups the information needed after concrete parameters have been chosen to decide how to name a new ...
Definition SharedImpl.h:136