LLZK 3.0.0
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 copies the value from an array at the specified position. The
270 result is a value copy, not a pointer or alias to the array element's storage.
271
272 Example of 1-dimensional array:
273 ```llzk
274 %i = arith.constant 0 : index
275 %0 = array.new %a, %b, %c : !array.type<3 x !felt.type>
276 // %1 is now equal to %a
277 %1 = array.read %0[%i] : !array.type<3 x !felt.type>, !felt.type
278 ```
279
280 Example of 3-dimensional array:
281 ```llzk
282 %i = arith.constant 0 : index
283 %j = arith.constant 1 : index
284 %k = arith.constant 4 : index
285 %0 = array.new ... : !array.type<3,10,15 x !felt.type>
286 // %1 is now equal to %a
287 %1 = array.read %0[%i, %j, %k] : !array.type<3,10,15 x !felt.type>, !felt.type
288 ```
289 }];
290
291 let arguments = (ins LLZK_ArrayType:$arr_ref, Variadic<Index>:$indices);
292 let results = (outs ArrayElemType:$result);
293
294 let assemblyFormat = [{
295 $arr_ref `[` $indices `]` `:` type($arr_ref) `,` type($result) attr-dict
296 }];
297
298 let hasVerifier = 1;
299}
300
301def LLZK_WriteArrayOp
302 : ScalarArrayAccessOp<
303 "write", false, [ArrayElemTypeUnifyWith<"arr_ref", "rvalue">]> {
304 let summary = "write scalar to an array";
305 let description = [{
306 This operation copies a value into an array at the specified position. It does
307 not create a pointer or alias between the source value and the array element's
308 storage.
309
310 Example of 1-dimensional array:
311 ```llzk
312 %i = arith.constant 0 : index
313 %0 = felt.const 42
314 %1 = array.new %a, %b, %c : !array.type<3 x !felt.type>
315 // The array now is [%0, %b, %c]
316 array.write %1[%i] = %0 : !array.type<3 x !felt.type>, !felt.type
317 ```
318
319 Example of 2-dimensional array:
320 ```llzk
321 %i = arith.constant 0 : index
322 %j = arith.constant 0 : index
323 %0 = felt.const 42
324 %1 = array.new %a, %b, %c, %d : !array.type<2,2 x !felt.type>
325 // The array now is [[%0, %b], [%c, %d]]
326 array.write %1[%i, %j] = %0 : !array.type<2,2 x !felt.type>, !felt.type
327 ```
328 }];
329
330 let arguments = (ins LLZK_ArrayType:$arr_ref, Variadic<Index>:$indices,
331 ArrayElemType:$rvalue);
332
333 let assemblyFormat = [{
334 $arr_ref `[` $indices `]` `=` $rvalue `:` type($arr_ref) `,` type($rvalue) attr-dict
335 }];
336
337 let hasVerifier = 1;
338}
339
340def LLZK_ExtractArrayOp
341 : RangedArrayAccessOp<"extract",
342 true, [InferTypeOpAdaptorWithIsCompatible]> {
343 let summary = "read subarray from a multi-dimensional array";
344 let description = [{
345 This operation takes an N-dimensional array and K indices and extracts the
346 (N-K)-dimensional array by applying the given indices to the first `K`
347 dimensions of the array. Error if `K >= N`. Use `array.read` instead if `K == N`.
348
349 Extracting a 1-D array from 3-D array by selecting the index of 2 dimensions:
350 ```llzk
351 %i = arith.constant 1 : index
352 %0 = array.new ... : !array.type<3,10,15 x !felt.type>
353 %1 = array.extract %0[%i,%i] : !array.type<3,10,15 x !felt.type>, !array.type<15 x !felt.type>
354 ```
355
356 Extracting 1-D arrays for subcomponents:
357 ```llzk
358 scf.for %iv = %lb to %up step %step {
359 %p = array.extract %in[%iv] : !array.type<@N,2 x !felt.type>
360 %c = array.read %a[%iv] : !array.type<@N x !struct.type<@SubC>>, !struct.type<@SubC>
361 function.call @SubC::@constrain(%c, %p) : (!struct.type<@SubC>, !array.type<2 x !felt.type>) -> ()
362 }
363 ```
364 }];
365
366 let arguments = (ins LLZK_ArrayType:$arr_ref, Variadic<Index>:$indices);
367 let results = (outs LLZK_ArrayType:$result);
368
369 let assemblyFormat = [{
370 $arr_ref `[` $indices `]` `:` type($arr_ref) attr-dict
371 }];
372}
373
374def LLZK_InsertArrayOp : RangedArrayAccessOp<"insert", false> {
375 let summary = "write subarray into a multi-dimensional array";
376 let description = [{
377 This operation takes an N-dimensional array, K indices, and an (N+K)-dimensional
378 array and inserts the N-dimensional array into the (N+K)-dimensional array at the
379 position specified by applying the given indices to the first `K` dimensions of
380 the (N+K)-dimensional array. Use `array.write` instead if `N == 0` (LLZK array type
381 must have at least 1 dimension so a 0-dimensional array cannot exist anyway).
382
383 Inserting 1-D arrays into a 2-D array:
384 ```llzk
385 %c = array.new : !array.type<2,3 x index>
386 // Array %c is uninitialized [[?, ?, ?], [?, ?, ?]]
387 %0 = arith.constant 0 : index
388 %a = array.new %r, %s, %t : !array.type<3 x index>
389 array.insert %c[%0] = %a : !array.type<2,3 x index>, !array.type<3 x index>
390 // Array %c is now [[%r, %s, %t], [?, ?, ?]]
391 %1 = arith.constant 1 : index
392 %b = array.new %x, %y, %z : !array.type<3 x index>
393 array.insert %c[%1] = %b : !array.type<2,3 x index>, !array.type<3 x index>
394 // Array %c is now [[%r, %s, %t], [%x, %y, %z]]
395 ```
396 }];
397
398 let arguments = (ins LLZK_ArrayType:$arr_ref, Variadic<Index>:$indices,
399 LLZK_ArrayType:$rvalue);
400
401 let assemblyFormat = [{
402 $arr_ref `[` $indices `]` `=` $rvalue `:` type($arr_ref) `,` type($rvalue) attr-dict
403 }];
404
405 let hasVerifier = 1;
406}
407
408def LLZK_ArrayLengthOp
409 : ArrayDialectOp<"len", [Pure,
410 DeclareOpInterfaceMethods<SymbolUserOpInterface>,
411 DeclareOpInterfaceMethods<ArrayRefOpInterface>]> {
412 let summary = "return the length of an array";
413 let description = [{
414 This operation returns the size of the specified dimension of an array.
415
416 Example:
417 ```llzk
418 %a = array.new : !array.type<2,3 x !felt.type>
419 %0 = arith.constant 0 : index
420 %x = array.len %a, %0 : !array.type<2,3 x !felt.type> // result is 2
421 %1 = arith.constant 1 : index
422 %y = array.len %a, %1 : !array.type<2,3 x !felt.type> // result is 3
423 ```
424 }];
425
426 let arguments = (ins LLZK_ArrayType:$arr_ref, Index:$dim);
427 let results = (outs Index:$length);
428
429 let assemblyFormat = [{ $arr_ref `,` $dim `:` type($arr_ref) attr-dict }];
430
431 let extraClassDeclaration = [{
432 /// Gets the type of the referenced base array.
433 inline ::llzk::array::ArrayType getArrRefType() {
434 return ::llvm::cast<ArrayRefOpInterface>(getOperation()).getArrRefType();
435 }
436 }];
437}
438
439#endif // LLZK_ARRAY_OPS