LLZK 3.0.0
An open-source IR for Zero Knowledge (ZK) circuits
Loading...
Searching...
No Matches
LLZKInlineFreeFunctionsPass.cpp
Go to the documentation of this file.
1//===-- LLZKInlineFreeFunctionsPass.cpp -------------------------*- C++ -*-===//
2//
3// Part of the LLZK Project, under the Apache License v2.0.
4// See LICENSE.txt for license information.
5// Copyright 2026 Project LLZK
6// SPDX-License-Identifier: Apache-2.0
7//
8//===----------------------------------------------------------------------===//
13//===----------------------------------------------------------------------===//
14
22#include "llzk/Util/Walk.h"
23
24#include <mlir/Dialect/Func/IR/FuncOps.h>
25#include <mlir/IR/BuiltinOps.h>
26#include <mlir/IR/SymbolTable.h>
27#include <mlir/Transforms/Inliner.h>
28#include <mlir/Transforms/InliningUtils.h>
29
30#include <llvm/ADT/DenseSet.h>
31#include <llvm/ADT/SCCIterator.h>
32#include <llvm/ADT/STLExtras.h>
33#include <llvm/ADT/SmallVector.h>
34#include <llvm/Support/Debug.h>
35
36namespace llzk {
37#define GEN_PASS_DEF_INLINEFREEFUNCTIONSPASS
39} // namespace llzk
40
41#define DEBUG_TYPE "llzk-inline-free-functions"
42
43using namespace mlir;
44using namespace llzk;
45using namespace llzk::function;
46
47namespace {
48
51static void walkTypeSymbolRefs(
52 Type type, function_ref<void(SymbolRefAttr)> rootRef,
53 function_ref<void(SymbolRefAttr)> unsupportedRef
54);
55
58static void walkAttrSymbolRefs(
59 Attribute attr, function_ref<void(SymbolRefAttr)> rootRef,
60 function_ref<void(SymbolRefAttr)> unsupportedRef
61) {
62 attr.walk<WalkOrder::PreOrder>([rootRef, unsupportedRef](TypeAttr typeAttr) {
63 walkTypeSymbolRefs(typeAttr.getValue(), rootRef, unsupportedRef);
64 return WalkResult::skip();
65 }, [unsupportedRef](SymbolRefAttr ref) { unsupportedRef(ref); });
66}
67
68static void walkTypeSymbolRefs(
69 Type type, function_ref<void(SymbolRefAttr)> rootRef,
70 function_ref<void(SymbolRefAttr)> unsupportedRef
71) {
72 type.walk<WalkOrder::PreOrder>([rootRef, unsupportedRef](component::StructType structType) {
73 rootRef(structType.getNameRef());
74 if (ArrayAttr params = structType.getParams()) {
75 walkAttrSymbolRefs(params, rootRef, unsupportedRef);
76 }
77 return WalkResult::skip();
78 }, [unsupportedRef](SymbolRefAttr ref) { unsupportedRef(ref); });
79}
80
83static bool isInlinableFreeFunction(FuncDefOp func, ModuleOp root, SymbolTableCollection &tables) {
84 Operation *parent = func->getParentOp();
85 if (parent == root) {
86 // A normal MLIR call uses nearest-symbol lookup after cloning, so a struct
87 // method can capture a callee that originally resolved at the root. Keep
88 // such helpers in place until general symbol rebasing is implemented.
89 bool hasFuncCall = walkContains<func::CallOp>(func);
90 LLVM_DEBUG({
91 if (hasFuncCall) {
92 llvm::dbgs() << "[" DEBUG_TYPE "] cannot inline " << func.getSymName() << " at "
93 << func.getLoc()
94 << ": body contains func.call whose callee could be captured after cloning\n";
95 }
96 });
97 return !hasFuncCall;
98 }
99
100 // Struct methods are handled by the struct inliner, not this pass. Also
101 // reject functions that are not owned by this module, such as definitions
102 // resolved through include.from.
103 if (!llvm::isa<ModuleOp, polymorphic::TemplateOp>(parent)) {
104 // Don't bother with debug message in this case. It's just checking the very
105 // basic case of whether the function is a free function.
106 return false;
107 }
108 if (!root->isAncestor(func.getOperation())) {
109 LLVM_DEBUG(
110 llvm::dbgs() << "[" DEBUG_TYPE "] cannot inline " << func.getSymName() << " at "
111 << func.getLoc() << ": function is defined outside the root module\n"
112 );
113 return false;
114 }
115
116 // TODO: This is a temporary allowance for a common frontend pattern: a
117 // helper nested in a namespace-like template or module whose symbol
118 // references can all be resolved from the root module. General
119 // cross-symbol-table inlining must rebase symbol references in the cloned
120 // body so that they continue to resolve to the same definitions.
121 bool hasUnsupportedSymbolRef = false;
122 func.walk([&](Operation *op) {
123 auto detectUnsupportedSymbolRef = [&](SymbolRefAttr ref) {
124 LLVM_DEBUG(
125 llvm::dbgs() << "[" DEBUG_TYPE "] cannot inline " << func.getSymName()
126 << ": unsupported scope-sensitive symbol reference " << ref << " in "
127 << op->getName() << " at " << op->getLoc() << '\n'
128 );
129 hasUnsupportedSymbolRef = true;
130 };
131 auto detectMissingRootSymbolRef = [&](SymbolRefAttr ref) {
132 if (!tables.lookupSymbolIn(root, ref)) {
133 LLVM_DEBUG(
134 llvm::dbgs() << "[" DEBUG_TYPE "] cannot inline " << func.getSymName()
135 << ": symbol reference " << ref << " in " << op->getName() << " at "
136 << op->getLoc() << " does not resolve in the root module\n"
137 );
138 hasUnsupportedSymbolRef = true;
139 }
140 };
141 walkAttrSymbolRefs(
142 op->getDiscardableAttrDictionary(), detectMissingRootSymbolRef, detectUnsupportedSymbolRef
143 );
144 if (Attribute properties = op->getPropertiesAsAttribute()) {
145 for (NamedAttribute property : llvm::cast<DictionaryAttr>(properties)) {
146 StringAttr name = property.getName();
147 // Member names resolve through the component's StructType, not the
148 // surrounding symbol table. The StructType itself is scanned below.
149 if (auto memberRef = llvm::dyn_cast<component::MemberRefOpInterface>(op)) {
150 if (name == memberRef.getMemberNameAttrName()) {
151 continue;
152 }
153 }
154 // LLZK calls and global references intentionally resolve these
155 // properties from the root module. Other properties, including
156 // template parameters, retain their ordinary scope-sensitive checks.
157 bool usesRootLookup = false;
158 if (auto call = llvm::dyn_cast<CallOp>(op)) {
159 usesRootLookup = name == call.getCalleeAttrName();
160 } else if (auto globalRef = llvm::dyn_cast<global::GlobalRefOpInterface>(op)) {
161 usesRootLookup = name == globalRef.getNameRefAttrName();
162 }
163 if (usesRootLookup) {
164 // Check the complete path, not its nested FlatSymbolRefAttr components:
165 // those resolve within the preceding symbol, not from the root.
166 detectMissingRootSymbolRef(llvm::cast<SymbolRefAttr>(property.getValue()));
167 } else {
168 walkAttrSymbolRefs(
169 property.getValue(), detectMissingRootSymbolRef, detectUnsupportedSymbolRef
170 );
171 }
172 }
173 }
174 for (Type type : llvm::concat<Type>(op->getOperandTypes(), op->getResultTypes())) {
175 walkTypeSymbolRefs(type, detectMissingRootSymbolRef, detectUnsupportedSymbolRef);
176 }
177 for (Region &region : op->getRegions()) {
178 for (Block &block : region) {
179 for (BlockArgument arg : block.getArguments()) {
180 walkTypeSymbolRefs(arg.getType(), detectMissingRootSymbolRef, detectUnsupportedSymbolRef);
181 }
182 }
183 }
184 });
185 return !hasUnsupportedSymbolRef;
186}
187
191static bool isInlinableCallSite(CallOp call, ModuleOp root) {
192 if (!call->getParentOfType<FuncDefOp>()) {
193 LLVM_DEBUG(
194 llvm::dbgs() << "[" DEBUG_TYPE "] cannot inline call to " << call.getCallee() << " at "
195 << call.getLoc() << ": call is outside a function.def body\n"
196 );
197 return false;
198 }
199 if (call->getParentOfType<ModuleOp>() != root) {
200 LLVM_DEBUG(
201 llvm::dbgs() << "[" DEBUG_TYPE "] cannot inline call to " << call.getCallee() << " at "
202 << call.getLoc() << ": call is in a nested module\n"
203 );
204 return false;
205 }
206 return true;
207}
208
213static llvm::DenseSet<Operation *> collectRecursiveFunctions(const llzk::CallGraph &cg) {
214 llvm::DenseSet<Operation *> recursive;
215 for (auto scc = llvm::scc_begin(&cg); !scc.isAtEnd(); ++scc) {
216 if (scc->size() == 1 && !scc.hasCycle()) {
217 continue;
218 }
219 for (const llzk::CallGraphNode *node : *scc) {
220 if (!node->isExternal()) {
221 LLVM_DEBUG(
222 llvm::dbgs() << "[" DEBUG_TYPE "] cannot inline "
223 << SymbolTable::getSymbolName(node->getCalledFunction().getOperation())
224 << " at " << node->getCalledFunction().getLoc()
225 << ": function participates in a call cycle\n"
226 );
227 recursive.insert(node->getCalledFunction().getOperation());
228 }
229 }
230 }
231 return recursive;
232}
233
236static FuncDefOp resolveFreeCallee(
237 CallOp call, ModuleOp root, SymbolTableCollection &tables,
238 const llvm::DenseSet<Operation *> &skippedCallees
239) {
240 auto tgtRes = call.getCalleeTarget(tables);
241 if (failed(tgtRes)) {
242 LLVM_DEBUG(
243 llvm::dbgs() << "[" DEBUG_TYPE "] cannot inline call to " << call.getCallee() << " at "
244 << call.getLoc() << ": callee target resolution failed\n"
245 );
246 return nullptr;
247 }
248 FuncDefOp callee = tgtRes->get();
249 if (!isInlinableFreeFunction(callee, root, tables)) {
250 return nullptr;
251 }
252 if (callee.isExternal()) {
253 LLVM_DEBUG(
254 llvm::dbgs() << "[" DEBUG_TYPE "] cannot inline call to " << call.getCallee() << " at "
255 << call.getLoc() << ": callee is external and has no body\n"
256 );
257 return nullptr;
258 }
259 if (skippedCallees.contains(callee)) {
260 LLVM_DEBUG(
261 llvm::dbgs() << "[" DEBUG_TYPE "] cannot inline call to " << call.getCallee() << " at "
262 << call.getLoc() << ": callee was skipped due to a call cycle or an earlier "
263 << "inlining failure\n"
264 );
265 return nullptr;
266 }
267 return callee;
268}
269
271struct FreeFunctionCall {
272 CallOp call;
273 FuncDefOp callee;
274};
275
278static SmallVector<FreeFunctionCall> collectFreeFunctionCalls(
279 ModuleOp mod, SymbolTableCollection &tables, const llvm::DenseSet<Operation *> &skippedCallees
280) {
281 SmallVector<FreeFunctionCall> calls;
282 mod.walk([&](CallOp call) {
283 if (!isInlinableCallSite(call, mod)) {
284 return;
285 }
286 if (FuncDefOp callee = resolveFreeCallee(call, mod, tables, skippedCallees)) {
287 calls.push_back({call, callee});
288 }
289 });
290 return calls;
291}
292
295static SmallVector<FuncDefOp> collectUnusedHelpers(ModuleOp mod) {
296 SymbolUseGraph useGraph(mod.getOperation());
297 SmallVector<FuncDefOp> unusedFunctions;
298 mod.walk([&](FuncDefOp func) {
299 if (func.isExternal() || !llvm::isa<ModuleOp, polymorphic::TemplateOp>(func->getParentOp())) {
300 return;
301 }
302 const SymbolUseGraphNode *node = useGraph.lookupNode(func);
303 if (!node || !node->hasPredecessor()) {
304 unusedFunctions.push_back(func);
305 }
306 });
307 return unusedFunctions;
308}
309
310class PassImpl : public llzk::impl::InlineFreeFunctionsPassBase<PassImpl> {
311 using Base = InlineFreeFunctionsPassBase<PassImpl>;
312 using Base::Base;
313
314 void runOnOperation() override {
315 ModuleOp mod = getOperation();
316 SymbolTableCollection tables;
317 InlinerInterface inliner(&getContext());
318 // Seeded with recursive functions; grows with callees that cannot be
319 // inlined.
320 llvm::DenseSet<Operation *> skippedCallees =
321 collectRecursiveFunctions(getAnalysis<CallGraphAnalysis>().getCallGraph());
323 inlineCalls(mod, tables, inliner, skippedCallees);
324 removeUnusedFunctions(mod);
325 }
326
327 /// Collects the current free-function call sites, then inlines them.
328 /// Iterates until no inlinable calls remain: inlined bodies can expose new
329 /// calls, but skipped callees are excluded, so every exposed call chain is
330 /// finite. Best effort: a callee whose inlining fails is skipped with a
331 /// warning, leaving its remaining call sites in place while processing
332 /// continues with other callees.
333 void inlineCalls(
334 ModuleOp mod, SymbolTableCollection &tables, InlinerInterface &inliner,
335 llvm::DenseSet<Operation *> &skippedCallees
336 ) {
337 InlinerConfig inlinerConfig;
338 SmallVector<FreeFunctionCall> callsToInline =
339 collectFreeFunctionCalls(mod, tables, skippedCallees);
340 for (;;) {
341 LLVM_DEBUG({
342 llvm::dbgs() << "[" DEBUG_TYPE "] round found " << callsToInline.size()
343 << " free-function call site(s) to inline\n";
344 });
345
346 if (callsToInline.empty()) {
347 break;
348 }
349
350 for (auto [call, callee] : callsToInline) {
351 if (skippedCallees.contains(callee)) {
352 LLVM_DEBUG(
353 llvm::dbgs() << "[" DEBUG_TYPE "] cannot inline call to " << call.getCallee()
354 << " at " << call.getLoc()
355 << ": an earlier inlining attempt for this callee failed\n"
356 );
357 continue;
358 }
359 if (failed(inlineCall(
360 inliner, inlinerConfig.getCloneCallback(), call, callee, callee.getCallableRegion(),
361 true
362 ))) {
363 LLVM_DEBUG(
364 llvm::dbgs() << "[" DEBUG_TYPE "] cannot inline call to " << call.getCallee()
365 << " at " << call.getLoc() << ": MLIR inlineCall failed\n"
366 );
367 call.emitWarning("failed to inline free function call; skipping this callee");
368 skippedCallees.insert(callee);
369 continue;
370 }
371 call.erase();
372 }
373
374 callsToInline = collectFreeFunctionCalls(mod, tables, skippedCallees);
376 }
377
378
379
380 void removeUnusedFunctions(ModuleOp mod) {
381 SmallVector<FuncDefOp> toErase = collectUnusedHelpers(mod);
382 while (!toErase.empty()) {
383 for (FuncDefOp func : toErase) {
384 func.erase();
385 }
386 toErase = collectUnusedHelpers(mod);
387 }
388 }
389};
390} // namespace
#define DEBUG_TYPE
This is a simple port of the mlir::CallGraphNode with llzk::CallGraph as a friend class,...
Definition CallGraph.h:36
This is a port of mlir::CallGraph that has been adapted to use the custom symbol lookup helpers (see ...
Definition CallGraph.h:162
bool hasPredecessor() const
Return true if this node has any predecessors.
Builds a graph structure representing the relationships between symbols and their uses.
const SymbolUseGraphNode * lookupNode(mlir::ModuleOp pathRoot, mlir::SymbolRefAttr path) const
Return the existing node for the symbol reference relative to the given module, else nullptr.
::mlir::SymbolRefAttr getCallee()
Definition Ops.cpp.inc:566
::mlir::FailureOr<::llzk::SymbolLookupResult<::llzk::function::FuncDefOp > > getCalleeTarget(::mlir::SymbolTableCollection &tables)
Resolve and return the target FuncDefOp for this CallOp.
Definition Ops.cpp:1037
::llvm::StringRef getSymName()
Definition Ops.cpp.inc:1224
ExpressionValue mod(const llvm::SMTSolverRef &solver, const ExpressionValue &lhs, const ExpressionValue &rhs)