LLZK 2.1.1
An open-source IR for Zero Knowledge (ZK) circuits
Loading...
Searching...
No Matches
Ops.td
Go to the documentation of this file.
1//===-- Ops.td ---------------------------------------------*- tablegen -*-===//
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#ifndef LLZK_ARRAY_OPS
11#define LLZK_ARRAY_OPS
12
13include "llzk/Dialect/Array/IR/Dialect.td"
14include "llzk/Dialect/Array/IR/OpInterfaces.td"
15include "llzk/Dialect/Array/IR/Types.td"
16include "llzk/Dialect/Shared/DiscardableAllocationOpInterfaces.td"
17include "llzk/Dialect/Shared/OpTraits.td"
18
19include "mlir/IR/OpAsmInterface.td"
20include "mlir/IR/OpBase.td"
21include "mlir/IR/SymbolInterfaces.td"
22include "mlir/Interfaces/SideEffectInterfaces.td"
23
24class ArrayDialectOp<string mnemonic, list<Trait> traits = []>
25 : Op<ArrayDialect, mnemonic, traits>;
26
27class ArrayAccessOpBase<string mnemonic, list<Trait> traits = []>
28 : ArrayDialectOp<
29 mnemonic,
30 traits#[DeclareOpInterfaceMethods<SymbolUserOpInterface>,
31 DeclareOpInterfaceMethods<ArrayAccessOpInterface>]> {
32 let extraClassDeclaration = [{
33 /// Gets the type of the referenced base array.
34 inline ::llzk::array::ArrayType getArrRefType() {
35 return ::llvm::cast<ArrayAccessOpInterface>(getOperation()).getArrRefType();
36 }
37 }];
38}
39
40// isRead: read(1) vs write(0) ops
41class ScalarArrayAccessOp<string mnemonic, bit isRead, list<Trait> traits = []>
42 : ArrayAccessOpBase<
43 mnemonic,
44 traits#[DeclareOpInterfaceMethods<DestructurableAccessorOpInterface>,
45 DeclareOpInterfaceMethods<PromotableMemOpInterface>,
46 DeclareOpInterfaceMethods<
47 DiscardableAllocationAccessorOpInterface>]> {
48 let extraClassDefinition = [{
49 /// Required by DestructurableAllocationOpInterface / SROA pass
50 bool $cppClass::canRewire(const ::mlir::DestructurableMemorySlot &slot,
51 ::llvm::SmallPtrSetImpl<::mlir::Attribute> &usedIndices,
52 ::mlir::SmallVectorImpl<::mlir::MemorySlot> &mustBeSafelyUsed,
53 const ::mlir::DataLayout &dataLayout) {
54 return ::llvm::cast<ArrayAccessOpInterface>(getOperation())
55 .canRewire(slot, usedIndices, mustBeSafelyUsed, dataLayout);
56 }
57
58 /// Required by DestructurableAllocationOpInterface / SROA pass
59 ::mlir::DeletionKind $cppClass::rewire(const ::mlir::DestructurableMemorySlot &slot,
60 ::llvm::DenseMap<::mlir::Attribute, ::mlir::MemorySlot> &subslots,
61 ::mlir::OpBuilder &builder, const ::mlir::DataLayout &dataLayout) {
62 return ::llvm::cast<ArrayAccessOpInterface>(getOperation())
63 .rewire(slot, subslots, builder, dataLayout);
64 }
65
66 /// Required by PromotableMemOpInterface / mem2reg pass
67 bool $cppClass::loadsFrom(const ::mlir::MemorySlot &slot) {
68 return }]#!if(isRead, "getArrRef() == slot.ptr", "false")#[{;
69 }
70
71 /// Required by PromotableMemOpInterface / mem2reg pass
72 bool $cppClass::storesTo(const ::mlir::MemorySlot &slot) {
73 return }]#!if(isRead, "false", "getArrRef() == slot.ptr")#[{;
74 }
75
76 /// Required by PromotableAllocationOpInterface / mem2reg pass
77 ::mlir::Value $cppClass::getStored(const ::mlir::MemorySlot &, ::mlir::OpBuilder &,
78 ::mlir::Value, const ::mlir::DataLayout &) {
79 }]#!if(isRead,
80 "llvm_unreachable(\"getStored() should not be called on "
81 "$cppClass\")",
82 "return getRvalue()")#[{;
83 }
84
85 /// Return `true` if the op is a read, `false` if it's a write.
86 bool $cppClass::isRead() {
87 return }]#!if(isRead, "true", "false")#[{;
88 }
89
90 /// Required by DiscardableAllocationAccessorOpInterface / unused allocation cleanup.
91 bool $cppClass::loadsFromDiscardableAllocation(::mlir::Value ptr) {
92 return }]#!if(isRead, "getArrRef() == ptr", "false")#[{;
93 }
94
95 /// Required by DiscardableAllocationAccessorOpInterface / unused allocation cleanup.
96 bool $cppClass::storesToDiscardableAllocation(::mlir::Value ptr) {
97 return }]#!if(isRead, "false",
98 "getArrRef() == ptr && getRvalue() != ptr")#[{;
99 }
100
101 /// Required by DiscardableAllocationAccessorOpInterface / unused allocation cleanup.
102 bool $cppClass::canEraseAsDeadStoreTo(::mlir::Value ptr, const ::mlir::DataLayout &) {
103 return }]#!if(isRead, "false",
104 "getArrRef() == ptr && getRvalue() != ptr")#[{;
105 }
106 }];
107}
108
109// isRead: read(1) vs write(0) ops
110class RangedArrayAccessOp<string mnemonic, bit isRead, list<Trait> traits = []>
111 : ArrayAccessOpBase<mnemonic, traits> {
112 let extraClassDefinition = [{
113 /// Return `true` if the op is a read, `false` if it's a write.
114 bool $cppClass::isRead() {
115 return }]#!if(isRead, "true", "false")#[{;
116 }
117 }];
118}
119
120//===------------------------------------------------------------------===//
121// Array operations
122//===------------------------------------------------------------------===//
123
124def LLZK_CreateArrayOp
125 : ArrayDialectOp<
126 "new",
127 [MemoryEffects<[MemAlloc<DiscardableAllocationResource>]>,
128 AttrSizedOperandSegments, VerifySizesForMultiAffineOps<1>,
129 DeclareOpInterfaceMethods<SymbolUserOpInterface>,
130 DeclareOpInterfaceMethods<OpAsmOpInterface, ["getAsmResultNames"]>,
131 DeclareOpInterfaceMethods<PromotableAllocationOpInterface>,
132 DeclareOpInterfaceMethods<DestructurableAllocationOpInterface>,
133 VariadicTypesMatchWith<
134 "operand types match result type", "result", "elements",
135 "resultTypeToElementsTypes($_self)", "std::equal_to<>()">]> {
136 let summary = "create a new array";
137 let description = [{
138 This operation creates a fresh mutable array with the given elements.
139 Even when two `array.new` operations have identical operands and result
140 types, they are distinct allocations; writes through one result must not
141 affect reads through the other.
142
143 The arguments are passed as a flat array but get arranged in
144 row-major order according the shape declared in the type.
145
146 Examples:
147 ```llzk
148 %0 = array.new %a, %b, %c : !array.type<3 x !felt.type>
149
150 // Create an array of the specified shape, initialized with the given
151 // values assigned in row-major order: [[%a, %b], [%c, %d]]
152 %1 = array.new %a, %b, %c, %d : !array.type<2,2 x !felt.type>
153
154 // Create an uninitialized array: [[?, ?], [?, ?], [?, ?]]
155 %2 = array.new : !array.type<3,2 x !felt.type>
156 ```
157
158 The values used to construct the array must have type that exactly matches
159 the element type of the specified array type. This is true even if a `tvar`
160 type is used. In other words, cannot mix `tvar<@X>` with `tvar<@Y>` or any
161 concrete type. In such a scenario, first create an uninitialized array, as
162 shown in the examples above, and then use `array.write` to write each element
163 of the array.
164
165 Implementation note: This restriction exists due to a combination of:
166 (1) we have chosen to infer the type of `$elements` from the `$result`
167 ArrayType, via parseInferredArrayType(), rather than requiring the type of
168 every element be listed in the assembly format and,
169 (2) within the parser for an Op, there is no way to get the Value instances
170 for the operands aside from `OpAsmParser::resolveOperands()` which requires
171 the type of every operand to be known and ends up comparing the expected
172 to actual type via `operator==`. Thus, there is no way for this to be
173 successful apart from all elements having the exact type inferred in (1).
174
175 Also note that `std::equal_to` is used in the `VariadicTypesMatchWith`
176 trait on this Op so that the verifier function mirrors the aforementioned
177 restriction in the parser.
178
179 In some cases, the length of an uninitialized array depends on the value
180 of the loop induction variable (i.e., each iteration creates an array with
181 a different size/shape). In that case, an AffineMapAttr can be used to
182 specify the dimension size in the ArrayType and the optional instantiation
183 parameter list of this operation must be used to instatiate all AffineMap
184 used in the array dimensions.
185
186 Examples:
187 ```llzk
188 // Create an uninitialized array with dimension size defined by AffineMap
189 #IdxToLen = affine_map<(i) -> (5*i+1)>
190 %3 = array.new {(%i)} : !array.type<#IdxToLen x index>
191
192 // Create an uninitialized array with multiple dimensions defined by
193 // AffineMap. The list of instantiation parameters are assigned to
194 // the AffineMap dimensions left-to-right.
195 #M1 = affine_map<(i)[c] -> (c+i)>
196 #M3 = affine_map<(m,n) -> (5*m+n)>
197 %4 = array.new{(%i)[%c], (%m,%n)} : !array.type<#M1,2,#M3 x i1>
198 ```
199 }];
200
201 // See `VerifySizesForMultiAffineOps` for more explanation of these arguments.
202 let arguments = (ins
203 // Elements to initialize the array. Length is either zero or equal to
204 // the length of the flattened/linearized result ArrayType.
205 Variadic<ArrayElemType>:$elements,
206 // List of AffineMap operand groups where each group provides the
207 // arguments to instantiate the next (left-to-right) AffineMap used as an
208 // array dimension in the result ArrayType.
209 VariadicOfVariadic<Index, "mapOpGroupSizes">:$mapOperands,
210 // Within each group in '$mapOperands', denotes the number of values that
211 // are AffineMap "dimensional" arguments with the remaining values being
212 // AffineMap "symbolic" arguments.
213 DefaultValuedAttr<DenseI32ArrayAttr, "{}">:$numDimsPerMap,
214 // Denotes the size of each variadic group in '$mapOperands'.
215 DenseI32ArrayAttr:$mapOpGroupSizes);
216 let results = (outs LLZK_ArrayType:$result);
217
218 // Define builders manually so inference of operand layout attributes is not
219 // circumvented.
220 let skipDefaultBuilders = 1;
221 let builders = [OpBuilder<(ins "::llzk::array::ArrayType":$result,
222 CArg<"::mlir::ValueRange", "{}">:$elements)>,
223 OpBuilder<(ins "::llzk::array::ArrayType":$result,
224 "::llvm::ArrayRef<::mlir::ValueRange>":$mapOperands,
225 "::mlir::DenseI32ArrayAttr":$numDimsPerMap)>,
226 OpBuilder<
227 (ins "::llzk::array::ArrayType":$result,
228 "::llvm::ArrayRef<::mlir::ValueRange>":$mapOperands,
229 "::llvm::ArrayRef<int32_t>":$numDimsPerMap),
230 [{
231 build($_builder, $_state, result, mapOperands, odsBuilder.getDenseI32ArrayAttr(numDimsPerMap));
232 }]>];
233
234 // This uses the custom parseInferredArrayType function to compute the type
235 // of '$elements' to match the type of '$result', except when '$elements'
236 // is empty, then the type of '$elements' must also be empty (size == 0).
237 // The if-then-else has '$elements' second so that an empty '$elements' list
238 // can be parsed when neither of these is specified.
239 let assemblyFormat = [{
240 ( `{` custom<MultiDimAndSymbolList>($mapOperands, $numDimsPerMap)^ `}` ) : ( $elements )?
241 `:` type($result)
242 `` custom<InferredArrayType>(type($elements), ref($elements), ref(type($result)))
243 custom<AttrDictWithWarnings>(attr-dict, prop-dict)
244 }];
245
246 let extraClassDeclaration = [{
247 private:
248 static ::mlir::ParseResult parseInferredArrayType(::mlir::OpAsmParser &parser,
249 ::llvm::SmallVector<::mlir::Type,1> &elementsTypes,
250 ::mlir::ArrayRef<::mlir::OpAsmParser::UnresolvedOperand> elements,
251 ::mlir::Type resultType
252 );
253
254 static void printInferredArrayType(::mlir::OpAsmPrinter &printer, CreateArrayOp,
255 ::mlir::TypeRange, ::mlir::OperandRange, ::mlir::Type
256 );
257
258 static ::llvm::SmallVector<::mlir::Type> resultTypeToElementsTypes(::mlir::Type resultType);
259 }];
260
261 let hasVerifier = 1;
262}
263
264def LLZK_ReadArrayOp
265 : ScalarArrayAccessOp<
266 "read", true, [ArrayTypeElemsUnifyWithResultCustomInfer<"arr_ref">]> {
267 let summary = "read scalar from an array";
268 let description = [{
269 This operation reads the value from an array at the specified position.
270
271 Example of 1-dimensional array:
272 ```llzk
273 %i = arith.constant 0 : index
274 %0 = array.new %a, %b, %c : !array.type<3 x !felt.type>
275 // %1 is now equal to %a
276 %1 = array.read %0[%i] : !array.type<3 x !felt.type>, !felt.type
277 ```
278
279 Example of 3-dimensional array:
280 ```llzk
281 %i = arith.constant 0 : index
282 %j = arith.constant 1 : index
283 %k = arith.constant 4 : index
284 %0 = array.new ... : !array.type<3,10,15 x !felt.type>
285 // %1 is now equal to %a
286 %1 = array.read %0[%i, %j, %k] : !array.type<3,10,15 x !felt.type>, !felt.type
287 ```
288 }];
289
290 let arguments = (ins LLZK_ArrayType:$arr_ref, Variadic<Index>:$indices);
291 let results = (outs ArrayElemType:$result);
292
293 let assemblyFormat = [{
294 $arr_ref `[` $indices `]` `:` type($arr_ref) `,` type($result) attr-dict
295 }];
296
297 let hasVerifier = 1;
298}
299
300def LLZK_WriteArrayOp
301 : ScalarArrayAccessOp<
302 "write", false, [ArrayElemTypeUnifyWith<"arr_ref", "rvalue">]> {
303 let summary = "write scalar to an array";
304 let description = [{
305 This operation writes a value into an array at the specified position.
306
307 Example of 1-dimensional array:
308 ```llzk
309 %i = arith.constant 0 : index
310 %0 = felt.const 42
311 %1 = array.new %a, %b, %c : !array.type<3 x !felt.type>
312 // The array now is [%0, %b, %c]
313 array.write %1[%i] = %0 : !array.type<3 x !felt.type>, !felt.type
314 ```
315
316 Example of 2-dimensional array:
317 ```llzk
318 %i = arith.constant 0 : index
319 %j = arith.constant 0 : index
320 %0 = felt.const 42
321 %1 = array.new %a, %b, %c, %d : !array.type<2,2 x !felt.type>
322 // The array now is [[%0, %b], [%c, %d]]
323 array.write %1[%i, %j] = %0 : !array.type<2,2 x !felt.type>, !felt.type
324 ```
325 }];
326
327 let arguments = (ins LLZK_ArrayType:$arr_ref, Variadic<Index>:$indices,
328 ArrayElemType:$rvalue);
329
330 let assemblyFormat = [{
331 $arr_ref `[` $indices `]` `=` $rvalue `:` type($arr_ref) `,` type($rvalue) attr-dict
332 }];
333
334 let hasVerifier = 1;
335}
336
337def LLZK_ExtractArrayOp
338 : RangedArrayAccessOp<"extract",
339 true, [InferTypeOpAdaptorWithIsCompatible]> {
340 let summary = "read subarray from a multi-dimensional array";
341 let description = [{
342 This operation takes an N-dimensional array and K indices and extracts the
343 (N-K)-dimensional array by applying the given indices to the first `K`
344 dimensions of the array. Error if `K >= N`. Use `array.read` instead if `K == N`.
345
346 Extracting a 1-D array from 3-D array by selecting the index of 2 dimensions:
347 ```llzk
348 %i = arith.constant 1 : index
349 %0 = array.new ... : !array.type<3,10,15 x !felt.type>
350 %1 = array.extract %0[%i,%i] : !array.type<3,10,15 x !felt.type>, !array.type<15 x !felt.type>
351 ```
352
353 Extracting 1-D arrays for subcomponents:
354 ```llzk
355 scf.for %iv = %lb to %up step %step {
356 %p = array.extract %in[%iv] : !array.type<@N,2 x !felt.type>
357 %c = array.read %a[%iv] : !array.type<@N x !struct.type<@SubC>>, !struct.type<@SubC>
358 function.call @SubC::@constrain(%c, %p) : (!struct.type<@SubC>, !array.type<2 x !felt.type>) -> ()
359 }
360 ```
361 }];
362
363 let arguments = (ins LLZK_ArrayType:$arr_ref, Variadic<Index>:$indices);
364 let results = (outs LLZK_ArrayType:$result);
365
366 let assemblyFormat = [{
367 $arr_ref `[` $indices `]` `:` type($arr_ref) attr-dict
368 }];
369}
370
371def LLZK_InsertArrayOp : RangedArrayAccessOp<"insert", false> {
372 let summary = "write subarray into a multi-dimensional array";
373 let description = [{
374 This operation takes an N-dimensional array, K indices, and an (N+K)-dimensional
375 array and inserts the N-dimensional array into the (N+K)-dimensional array at the
376 position specified by applying the given indices to the first `K` dimensions of
377 the (N+K)-dimensional array. Use `array.write` instead if `N == 0` (LLZK array type
378 must have at least 1 dimension so a 0-dimensional array cannot exist anyway).
379
380 Inserting 1-D arrays into a 2-D array:
381 ```llzk
382 %c = array.new : !array.type<2,3 x index>
383 // Array %c is uninitialized [[?, ?, ?], [?, ?, ?]]
384 %0 = arith.constant 0 : index
385 %a = array.new %r, %s, %t : !array.type<3 x index>
386 array.insert %c[%0] = %a : !array.type<2,3 x index>, !array.type<3 x index>
387 // Array %c is now [[%r, %s, %t], [?, ?, ?]]
388 %1 = arith.constant 1 : index
389 %b = array.new %x, %y, %z : !array.type<3 x index>
390 array.insert %c[%1] = %b : !array.type<2,3 x index>, !array.type<3 x index>
391 // Array %c is now [[%r, %s, %t], [%x, %y, %z]]
392 ```
393 }];
394
395 let arguments = (ins LLZK_ArrayType:$arr_ref, Variadic<Index>:$indices,
396 LLZK_ArrayType:$rvalue);
397
398 let assemblyFormat = [{
399 $arr_ref `[` $indices `]` `=` $rvalue `:` type($arr_ref) `,` type($rvalue) attr-dict
400 }];
401
402 let hasVerifier = 1;
403}
404
405def LLZK_ArrayLengthOp
406 : ArrayDialectOp<"len", [Pure,
407 DeclareOpInterfaceMethods<SymbolUserOpInterface>,
408 DeclareOpInterfaceMethods<ArrayRefOpInterface>]> {
409 let summary = "return the length of an array";
410 let description = [{
411 This operation returns the size of the specified dimension of an array.
412
413 Example:
414 ```llzk
415 %a = array.new : !array.type<2,3 x !felt.type>
416 %0 = arith.constant 0 : index
417 %x = array.len %a, %0 : !array.type<2,3 x !felt.type> // result is 2
418 %1 = arith.constant 1 : index
419 %y = array.len %a, %1 : !array.type<2,3 x !felt.type> // result is 3
420 ```
421 }];
422
423 let arguments = (ins LLZK_ArrayType:$arr_ref, Index:$dim);
424 let results = (outs Index:$length);
425
426 let assemblyFormat = [{ $arr_ref `,` $dim `:` type($arr_ref) attr-dict }];
427
428 let extraClassDeclaration = [{
429 /// Gets the type of the referenced base array.
430 inline ::llzk::array::ArrayType getArrRefType() {
431 return ::llvm::cast<ArrayRefOpInterface>(getOperation()).getArrRefType();
432 }
433 }];
434}
435
436#endif // LLZK_ARRAY_OPS