1//===-- Ops.td ---------------------------------------------*- tablegen -*-===//
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
8//===----------------------------------------------------------------------===//
10#ifndef LLZK_POLYMORPHIC_OPS
11#define LLZK_POLYMORPHIC_OPS
13include "llzk/Dialect/Polymorphic/IR/Dialect.td"
14include "llzk/Dialect/Polymorphic/IR/Types.td"
15include "llzk/Dialect/Polymorphic/IR/OpInterfaces.td"
16include "llzk/Dialect/Shared/OpTraits.td"
18include "mlir/IR/OpBase.td"
19include "mlir/IR/RegionKindInterface.td"
20include "mlir/IR/SymbolInterfaces.td"
21include "mlir/Interfaces/ControlFlowInterfaces.td"
22include "mlir/Interfaces/SideEffectInterfaces.td"
24class PolymorphicDialectOp<string mnemonic, list<Trait> traits = []>
25 : Op<PolymorphicDialect, mnemonic, traits>;
28 : PolymorphicDialectOp<"template", [HasParent<"::mlir::ModuleOp">, Symbol,
29 LLZKSymbolTable, IsolatedFromAbove,
30 NoRegionArguments, NoTerminator,
32 let summary = "defines polymorphic functions or structs";
34 The `poly.template` allows defining polymorphic templated functions and structs.
35 The body contains the definitions of the template's parameters along with the
36 function and/or struct definitions that utilize those parameters in their bodies.
40 poly.template @TemplateName {
44 function.def @f(%inp: !array.type<8,5,@N x !felt.type>) -> !array.type<@N x !felt.type> {
45 // function header and body can use parameters @N and @T
48 struct.def @StructName {
49 function.def @compute() -> !struct.type<@TemplateName::@StructName> {
52 function.def @constrain(%self: !struct.type<@TemplateName::@StructName>) {
59 The order of `poly.param` definitions in the template body determines the order that
60 template parameters must be listed in the parameter list of a `struct.type` referring
61 to a struct nested within the template. In the example above, the type of `@StructName`
62 is `!struct.type<@TemplateName::@StructName<[@N, @T]>>`.
64 Transform passes may attach the discardable generic attribute `poly.name_pattern` to a partial
65 template with `P` current `poly.param` operations. Its value is an `ArrayAttr` of `P + 1`
66 literal `StringAttr` chunks; gap `i`, between chunks `i` and `i + 1`, corresponds to current
67 parameter `i`. The chunks are carried as structured metadata and are never recovered by
68 interpreting `sym_name`; without the attribute, every byte of the current `sym_name` is opaque
69 source or legacy text. When a transform closes the final gap, it removes the attribute rather
70 than retaining a terminal one-chunk state.
75 let arguments = (ins SymbolNameAttr:$sym_name);
77 let regions = (region SizedRegion<1>:$bodyRegion);
79 let assemblyFormat = [{ $sym_name $bodyRegion attr-dict }];
81 let extraClassDeclaration = [{
82 /// Return ops of type `OpT` within the body region.
83 /// Ops are returned in the order they are defined in the IR.
84 template <TemplateSymbolBindingOp OpT>
85 inline ::llvm::iterator_range<::mlir::Region::op_iterator<OpT>> getConstOps() {
86 return getBodyRegion().getOps<OpT>();
89 /// Return `true` if there are ops of type `OpT` within the body region.
90 template <TemplateSymbolBindingOp OpT>
91 inline bool hasConstOps() {
92 return !getConstOps<OpT>().empty();
95 /// Return the number of ops of type `OpT` within the body region.
96 template <TemplateSymbolBindingOp OpT>
97 inline size_t numConstOps() {
98 return llvm::range_size(getConstOps<OpT>());
101 /// Return the names of all ops of type `OpT` within the body region in the order they
102 /// are defined. The names are returned as `FlatSymbolRefAttr` but the more general
103 /// `Attribute` type is used in the return type since that's usually what's needed.
104 template <TemplateSymbolBindingOp OpT>
105 ::llvm::SmallVector<::mlir::Attribute> getConstNames() {
106 return ::llvm::to_vector(::llvm::map_range(getConstOps<OpT>(), [](auto p) -> ::mlir::Attribute {
107 return ::mlir::FlatSymbolRefAttr::get(p.getNameAttr());
111 /// Return `true` if there is an op of type `OpT` with the given name within the body region.
112 template <TemplateSymbolBindingOp OpT>
113 inline bool hasConstNamed(::mlir::StringRef find) {
114 return ::llvm::any_of(getConstOps<OpT>(), [&](OpT op) {
115 return op.getName() == find;
119 /// Return `true` if there is an op of type `OpT` with the given name within the body region.
120 template <TemplateSymbolBindingOp OpT>
121 inline bool hasConstNamed(::mlir::StringAttr find) {
122 return hasConstNamed<OpT>(find.strref());
125 /// Return `true` if there is an op of type `OpT` with the given name within the body region.
126 template <TemplateSymbolBindingOp OpT>
127 inline bool hasConstNamed(::mlir::FlatSymbolRefAttr find) {
128 return hasConstNamed<OpT>(find.getRootReference());
131 /// Return the op of type `OpT` with the given name within the body region if it exists, else `nullptr`.
132 template <TemplateSymbolBindingOp OpT>
133 inline OpT getConstNamed(::mlir::StringRef find) {
134 auto range = getConstOps<OpT>();
135 auto it = ::llvm::find_if(range, [&find](OpT op) { return op.getName() == find; });
136 return it != range.end() ? *it : OpT{};
139 /// Return the op of type `OpT` with the given name within the body region if it exists, else `nullptr`.
140 template <TemplateSymbolBindingOp OpT>
141 inline OpT getConstNamed(::mlir::StringAttr find) {
142 return getConstNamed<OpT>(find.strref());
145 /// Return the op of type `OpT` with the given name within the body region if it exists, else `nullptr`.
146 template <TemplateSymbolBindingOp OpT>
147 inline OpT getConstNamed(::mlir::FlatSymbolRefAttr find) {
148 return getConstNamed<OpT>(find.getRootReference());
153def LLZK_TemplateParamOp
154 : PolymorphicDialectOp<
155 "param", [HasParent<"::llzk::polymorphic::TemplateOp">,
156 DeclareOpInterfaceMethods<TemplateSymbolBindingOpInterface>,
157 DeclareOpInterfaceMethods<SymbolUserOpInterface>]> {
158 let summary = "declares a parameter of a polymorphic template";
160 Declares a parameter of a `poly.template` that can be used by the function and/or struct
161 definitions within the template. Each parameter can have an optional type restriction.
165 poly.template @TemplateName {
167 poly.param @F : !felt.type
168 // To restrict a parameter to accept only a type, use `!poly.tvar` with the parameter's own name.
169 poly.param @T : !poly.tvar<@T>
174 let arguments = (ins SymbolNameAttr:$sym_name,
175 OptionalAttr<TypeAttrOf<ConstReadType>>:$type_opt);
177 let assemblyFormat = [{ $sym_name (`:` $type_opt^)? attr-dict }];
180def LLZK_TemplateExprOp
181 : PolymorphicDialectOp<
182 "expr", [HasParent<"::llzk::polymorphic::TemplateOp">,
183 DeclareOpInterfaceMethods<TemplateSymbolBindingOpInterface>,
184 DeclareOpInterfaceMethods<SymbolUserOpInterface>,
185 IsolatedFromAbove, NoRegionArguments, SingleBlock]> {
186 let summary = "declares a named expression in a polymorphic template";
188 Declares an expression over parameters of a `poly.template` that can be used just like
189 a parameter within the function and/or struct definitions within the template.
191 The body of a `poly.expr` cannot contain any symbols defined via `poly.expr` to prevent
192 cyclic initialization. The body must also have no side effects to ensure it can be safely
193 duplicated if needed. This means operations such as read/write to globals or function
194 calls are not allowed in the body of a `poly.expr`.
198 poly.template @TemplateName {
199 poly.expr @ExprName {
200 %0 = some_op %param1, %param2 : (!felt.type, !felt.type) -> !felt.type
201 poly.yield %0 : !felt.type
207 let arguments = (ins SymbolNameAttr:$sym_name);
208 let regions = (region SizedRegion<1>:$initializerRegion);
210 let assemblyFormat = [{ $sym_name $initializerRegion attr-dict }];
212 let hasRegionVerifier = 1;
214 let extraClassDeclaration = [{
215 /// Returns the type of the `poly.yield` op in the initializer region.
216 ::mlir::Type getType();
221 : PolymorphicDialectOp<
222 "yield", [HasParent<"::llzk::polymorphic::TemplateExprOp">,
223 ReturnLike, Terminator]> {
224 let summary = "expr initialization yield and termination operation";
226 This operation yields an SSA value from a `poly.expr` initialization
227 region and terminates the region.
230 let arguments = (ins ConstReadType:$val);
232 let assemblyFormat = [{ $val `:` type($val) attr-dict }];
236 : PolymorphicDialectOp<"read_const", [Pure, DeclareOpInterfaceMethods<
237 SymbolUserOpInterface>]> {
238 let summary = "read value of a template parameter";
240 This operation reads the value from the named constant parameter of the template
241 in which this op appears. The op itself puts a type restriction on the value,
242 but leaves it to a later type-checking pass to ensure the template parameters are
243 instantiated with types matching the uses of the parameter within the template.
247 // Read a `!felt.type` value from struct parameter "@A"
248 %0 = poly.read_const @A : !felt.type
249 // Read a value from struct parameter "@B" where its type is
250 // specified by struct parameter "@T"
251 %1 = poly.read_const @B : !poly.tvar<@T>
255 let arguments = (ins FlatSymbolRefAttr:$const_name);
256 let results = (outs ConstReadType:$val);
258 let assemblyFormat = [{ $const_name `:` type($val) attr-dict }];
261def LLZK_UnifiableCastOp : PolymorphicDialectOp<"unifiable_cast", [Pure]> {
262 let summary = "cast between two unifiable types";
264 This operation reinterprets a value as a different type with the restriction
265 that the input and output types of the cast are unifiable.
267 Most ops that accept LLZK types accept unifiable types as input and thus there
268 is no need for casting between types. This op is meant to be used in situations where
269 is not possible to modify the given or the target type and they are different but unifiable.
270 For example, inside a conversion pattern the driver may introduce `unrealized_conversion_cast`
271 operations if the types are not equal. This will happen regardless of whether the two types unify.
272 This cast can be introduced instead of the default cast operation to satisfy MLIR's assumptions
277 %0 = some_other_op : !array.type<@N x !felt.type>
278 %1 = unifiable_cast %0 : (!array.type<@N x @felt.type>) -> !array.type<affine_map<()[s0, s1] -> (s0 + s1)> x !felt.type>
282 let arguments = (ins AnyLLZKType:$input);
283 let results = (outs AnyLLZKType:$result);
284 let assemblyFormat = [{
285 $input `:` functional-type($input, results) attr-dict
291def LLZK_ApplyMapOp : PolymorphicDialectOp<"applymap", [Pure]> {
292 let summary = "apply an AffineMap";
294 This operation applies an AffineMap to a list of SSA values, yielding a single
295 SSA value. The number of dimension and symbol arguments must be equal to the
296 respective number of dimensional and symbolic inputs to the AffineMap; the
297 AffineMap has to be one-dimensional, and so this operation always returns one
298 value. The input operands and result all have `index` type.
302 #map10 = affine_map<(d0, d1) -> (d0 floordiv 8 + d1 floordiv 128)>
304 %1 = poly.applymap(%s, %t) #map10
309 %2 = poly.applymap(%42)[%n] affine_map<(i)[s0] -> (i+s0)>
313 let arguments = (ins AffineMapAttr:$map, Variadic<Index>:$mapOperands,
315 let results = (outs Index);
317 // Define builders manually so inference of `numDims` attribute is not
319 let skipDefaultBuilders = 1;
320 let builders = [OpBuilder<(ins "::mlir::AffineMapAttr":$map,
321 CArg<"::mlir::ValueRange", "{}">:$mapOperands),
323 $_state.addOperands(mapOperands);
324 Properties &props = $_state.getOrAddProperties<Properties>();
326 props.setNumDims($_builder.getIntegerAttr($_builder.getIndexType(),
327 map.getAffineMap().getNumDims()));
328 $_state.addTypes($_builder.getIndexType());
330 OpBuilder<(ins "::mlir::AffineMap":$map,
331 CArg<"::mlir::ValueRange", "{}">:$mapOperands),
333 build($_builder, $_state, ::mlir::AffineMapAttr::get(map), mapOperands);
335 OpBuilder<(ins "::mlir::AffineExpr":$expr,
336 CArg<"::mlir::ValueRange", "{}">:$mapOperands),
338 auto map = ::mlir::AffineMap::inferFromExprList({expr}, $_builder.getContext()).front();
339 build($_builder, $_state, map, mapOperands);
342 let assemblyFormat = [{
343 custom<DimAndSymbolList>($mapOperands, $numDims) $map attr-dict
349 let extraClassDeclaration = [{
350 /// Returns the affine map to be applied by this operation.
351 ::mlir::AffineMap inline getAffineMap() { return getMap(); }
353 /// Returns the affine value map computed from this operation.
354 ::mlir::affine::AffineValueMap getAffineValueMap() {
355 return ::mlir::affine::AffineValueMap(getAffineMap(), getOperands(), getResult());
358 /// Returns all dimension operands.
359 ::mlir::ValueRange getDimOperands() {
360 return ::mlir::OperandRange{
361 getOperands().begin(),
362 getOperands().begin() + getMap().getNumDims()};
365 /// Returns all symbol operands.
366 ::mlir::ValueRange getSymbolOperands() {
367 return ::mlir::OperandRange{
368 getOperands().begin() + getMap().getNumDims(),
369 getOperands().end()};
374#endif // LLZK_POLYMORPHIC_OPS