1 | //===- TestOpProperties.cpp - Test all properties-related APIs ------------===// |
---|---|
2 | // |
3 | // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
4 | // See https://llvm.org/LICENSE.txt for license information. |
5 | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
6 | // |
7 | //===----------------------------------------------------------------------===// |
8 | |
9 | #include "mlir/IR/Attributes.h" |
10 | #include "mlir/IR/OpDefinition.h" |
11 | #include "mlir/IR/OperationSupport.h" |
12 | #include "mlir/Parser/Parser.h" |
13 | #include "gtest/gtest.h" |
14 | #include <optional> |
15 | |
16 | using namespace mlir; |
17 | |
18 | namespace { |
19 | /// Simple structure definining a struct to define "properties" for a given |
20 | /// operation. Default values are honored when creating an operation. |
21 | struct TestProperties { |
22 | int a = -1; |
23 | float b = -1.; |
24 | std::vector<int64_t> array = {-33}; |
25 | /// A shared_ptr to a const object is safe: it is equivalent to a value-based |
26 | /// member. Here the label will be deallocated when the last operation |
27 | /// referring to it is destroyed. However there is no pool-allocation: this is |
28 | /// offloaded to the client. |
29 | std::shared_ptr<const std::string> label; |
30 | MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(TestProperties) |
31 | }; |
32 | |
33 | bool operator==(const TestProperties &lhs, TestProperties &rhs) { |
34 | return lhs.a == rhs.a && lhs.b == rhs.b && lhs.array == rhs.array && |
35 | lhs.label == rhs.label; |
36 | } |
37 | |
38 | /// Convert a DictionaryAttr to a TestProperties struct, optionally emit errors |
39 | /// through the provided diagnostic if any. This is used for example during |
40 | /// parsing with the generic format. |
41 | static LogicalResult |
42 | setPropertiesFromAttribute(TestProperties &prop, Attribute attr, |
43 | function_ref<InFlightDiagnostic()> emitError) { |
44 | DictionaryAttr dict = dyn_cast<DictionaryAttr>(attr); |
45 | if (!dict) { |
46 | emitError() << "expected DictionaryAttr to set TestProperties"; |
47 | return failure(); |
48 | } |
49 | auto aAttr = dict.getAs<IntegerAttr>("a"); |
50 | if (!aAttr) { |
51 | emitError() << "expected IntegerAttr for key `a`"; |
52 | return failure(); |
53 | } |
54 | auto bAttr = dict.getAs<FloatAttr>("b"); |
55 | if (!bAttr || |
56 | &bAttr.getValue().getSemantics() != &llvm::APFloatBase::IEEEsingle()) { |
57 | emitError() << "expected FloatAttr for key `b`"; |
58 | return failure(); |
59 | } |
60 | |
61 | auto arrayAttr = dict.getAs<DenseI64ArrayAttr>("array"); |
62 | if (!arrayAttr) { |
63 | emitError() << "expected DenseI64ArrayAttr for key `array`"; |
64 | return failure(); |
65 | } |
66 | |
67 | auto label = dict.getAs<mlir::StringAttr>("label"); |
68 | if (!label) { |
69 | emitError() << "expected StringAttr for key `label`"; |
70 | return failure(); |
71 | } |
72 | |
73 | prop.a = aAttr.getValue().getSExtValue(); |
74 | prop.b = bAttr.getValue().convertToFloat(); |
75 | prop.array.assign(arrayAttr.asArrayRef().begin(), |
76 | arrayAttr.asArrayRef().end()); |
77 | prop.label = std::make_shared<std::string>(label.getValue()); |
78 | return success(); |
79 | } |
80 | |
81 | /// Convert a TestProperties struct to a DictionaryAttr, this is used for |
82 | /// example during printing with the generic format. |
83 | static Attribute getPropertiesAsAttribute(MLIRContext *ctx, |
84 | const TestProperties &prop) { |
85 | SmallVector<NamedAttribute> attrs; |
86 | Builder b{ctx}; |
87 | attrs.push_back(Elt: b.getNamedAttr("a", b.getI32IntegerAttr(prop.a))); |
88 | attrs.push_back(Elt: b.getNamedAttr("b", b.getF32FloatAttr(prop.b))); |
89 | attrs.push_back(Elt: b.getNamedAttr("array", b.getDenseI64ArrayAttr(prop.array))); |
90 | attrs.push_back(Elt: b.getNamedAttr( |
91 | "label", b.getStringAttr(prop.label ? *prop.label : "<nullptr>"))); |
92 | return b.getDictionaryAttr(attrs); |
93 | } |
94 | |
95 | inline llvm::hash_code computeHash(const TestProperties &prop) { |
96 | // We hash `b` which is a float using its underlying array of char: |
97 | unsigned char const *p = reinterpret_cast<unsigned char const *>(&prop.b); |
98 | ArrayRef<unsigned char> bBytes{p, sizeof(prop.b)}; |
99 | return llvm::hash_combine(args: prop.a, args: llvm::hash_combine_range(R&: bBytes), |
100 | args: llvm::hash_combine_range(R: prop.array), |
101 | args: StringRef(*prop.label)); |
102 | } |
103 | |
104 | /// A custom operation for the purpose of showcasing how to use "properties". |
105 | class OpWithProperties : public Op<OpWithProperties> { |
106 | public: |
107 | // Begin boilerplate |
108 | MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(OpWithProperties) |
109 | using Op::Op; |
110 | static ArrayRef<StringRef> getAttributeNames() { return {}; } |
111 | static StringRef getOperationName() { |
112 | return "test_op_properties.op_with_properties"; |
113 | } |
114 | // End boilerplate |
115 | |
116 | // This alias is the only definition needed for enabling "properties" for this |
117 | // operation. |
118 | using Properties = TestProperties; |
119 | static std::optional<mlir::Attribute> getInherentAttr(MLIRContext *context, |
120 | const Properties &prop, |
121 | StringRef name) { |
122 | return std::nullopt; |
123 | } |
124 | static void setInherentAttr(Properties &prop, StringRef name, |
125 | mlir::Attribute value) {} |
126 | static void populateInherentAttrs(MLIRContext *context, |
127 | const Properties &prop, |
128 | NamedAttrList &attrs) {} |
129 | static LogicalResult |
130 | verifyInherentAttrs(OperationName opName, NamedAttrList &attrs, |
131 | function_ref<InFlightDiagnostic()> emitError) { |
132 | return success(); |
133 | } |
134 | }; |
135 | |
136 | /// A custom operation for the purpose of showcasing how discardable attributes |
137 | /// are handled in absence of properties. |
138 | class OpWithoutProperties : public Op<OpWithoutProperties> { |
139 | public: |
140 | // Begin boilerplate. |
141 | MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(OpWithoutProperties) |
142 | using Op::Op; |
143 | static ArrayRef<StringRef> getAttributeNames() { |
144 | static StringRef attributeNames[] = {StringRef("inherent_attr")}; |
145 | return ArrayRef(attributeNames); |
146 | }; |
147 | static StringRef getOperationName() { |
148 | return "test_op_properties.op_without_properties"; |
149 | } |
150 | // End boilerplate. |
151 | }; |
152 | |
153 | // A trivial supporting dialect to register the above operation. |
154 | class TestOpPropertiesDialect : public Dialect { |
155 | public: |
156 | MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(TestOpPropertiesDialect) |
157 | static constexpr StringLiteral getDialectNamespace() { |
158 | return StringLiteral("test_op_properties"); |
159 | } |
160 | explicit TestOpPropertiesDialect(MLIRContext *context) |
161 | : Dialect(getDialectNamespace(), context, |
162 | TypeID::get<TestOpPropertiesDialect>()) { |
163 | addOperations<OpWithProperties, OpWithoutProperties>(); |
164 | } |
165 | }; |
166 | |
167 | constexpr StringLiteral mlirSrc = R"mlir( |
168 | "test_op_properties.op_with_properties"() |
169 | <{a = -42 : i32, |
170 | b = -4.200000e+01 : f32, |
171 | array = array<i64: 40, 41>, |
172 | label = "bar foo"}> : () -> () |
173 | )mlir"; |
174 | |
175 | TEST(OpPropertiesTest, Properties) { |
176 | MLIRContext context; |
177 | context.getOrLoadDialect<TestOpPropertiesDialect>(); |
178 | ParserConfig config(&context); |
179 | // Parse the operation with some properties. |
180 | OwningOpRef<Operation *> op = parseSourceString(sourceStr: mlirSrc, config); |
181 | ASSERT_TRUE(op.get() != nullptr); |
182 | auto opWithProp = dyn_cast<OpWithProperties>(Val: op.get()); |
183 | ASSERT_TRUE(opWithProp); |
184 | { |
185 | std::string output; |
186 | llvm::raw_string_ostream os(output); |
187 | opWithProp.print(os); |
188 | ASSERT_STREQ("\"test_op_properties.op_with_properties\"() " |
189 | "<{a = -42 : i32, " |
190 | "array = array<i64: 40, 41>, " |
191 | "b = -4.200000e+01 : f32, " |
192 | "label = \"bar foo\"}> : () -> ()\n", |
193 | output.c_str()); |
194 | } |
195 | // Get a mutable reference to the properties for this operation and modify it |
196 | // in place one member at a time. |
197 | TestProperties &prop = opWithProp.getProperties(); |
198 | prop.a = 42; |
199 | { |
200 | std::string output; |
201 | llvm::raw_string_ostream os(output); |
202 | opWithProp.print(os); |
203 | StringRef view(output); |
204 | EXPECT_TRUE(view.contains("a = 42")); |
205 | EXPECT_TRUE(view.contains("b = -4.200000e+01")); |
206 | EXPECT_TRUE(view.contains("array = array<i64: 40, 41>")); |
207 | EXPECT_TRUE(view.contains("label = \"bar foo\"")); |
208 | } |
209 | prop.b = 42.; |
210 | { |
211 | std::string output; |
212 | llvm::raw_string_ostream os(output); |
213 | opWithProp.print(os); |
214 | StringRef view(output); |
215 | EXPECT_TRUE(view.contains("a = 42")); |
216 | EXPECT_TRUE(view.contains("b = 4.200000e+01")); |
217 | EXPECT_TRUE(view.contains("array = array<i64: 40, 41>")); |
218 | EXPECT_TRUE(view.contains("label = \"bar foo\"")); |
219 | } |
220 | prop.array.push_back(x: 42); |
221 | { |
222 | std::string output; |
223 | llvm::raw_string_ostream os(output); |
224 | opWithProp.print(os); |
225 | StringRef view(output); |
226 | EXPECT_TRUE(view.contains("a = 42")); |
227 | EXPECT_TRUE(view.contains("b = 4.200000e+01")); |
228 | EXPECT_TRUE(view.contains("array = array<i64: 40, 41, 42>")); |
229 | EXPECT_TRUE(view.contains("label = \"bar foo\"")); |
230 | } |
231 | prop.label = std::make_shared<std::string>(args: "foo bar"); |
232 | { |
233 | std::string output; |
234 | llvm::raw_string_ostream os(output); |
235 | opWithProp.print(os); |
236 | StringRef view(output); |
237 | EXPECT_TRUE(view.contains("a = 42")); |
238 | EXPECT_TRUE(view.contains("b = 4.200000e+01")); |
239 | EXPECT_TRUE(view.contains("array = array<i64: 40, 41, 42>")); |
240 | EXPECT_TRUE(view.contains("label = \"foo bar\"")); |
241 | } |
242 | } |
243 | |
244 | // Test diagnostic emission when using invalid dictionary. |
245 | TEST(OpPropertiesTest, FailedProperties) { |
246 | MLIRContext context; |
247 | context.getOrLoadDialect<TestOpPropertiesDialect>(); |
248 | std::string diagnosticStr; |
249 | context.getDiagEngine().registerHandler(handler: [&](Diagnostic &diag) { |
250 | diagnosticStr += diag.str(); |
251 | return success(); |
252 | }); |
253 | |
254 | // Parse the operation with some properties. |
255 | ParserConfig config(&context); |
256 | |
257 | // Parse an operation with invalid (incomplete) properties. |
258 | OwningOpRef<Operation *> owningOp = |
259 | parseSourceString(sourceStr: "\"test_op_properties.op_with_properties\"() " |
260 | "<{a = -42 : i32}> : () -> ()\n", |
261 | config); |
262 | ASSERT_EQ(owningOp.get(), nullptr); |
263 | EXPECT_STREQ( |
264 | "invalid properties {a = -42 : i32} for op " |
265 | "test_op_properties.op_with_properties: expected FloatAttr for key `b`", |
266 | diagnosticStr.c_str()); |
267 | diagnosticStr.clear(); |
268 | |
269 | owningOp = parseSourceString(sourceStr: mlirSrc, config); |
270 | Operation *op = owningOp.get(); |
271 | ASSERT_TRUE(op != nullptr); |
272 | Location loc = op->getLoc(); |
273 | auto opWithProp = dyn_cast<OpWithProperties>(Val: op); |
274 | ASSERT_TRUE(opWithProp); |
275 | |
276 | OperationState state(loc, op->getName()); |
277 | Builder b{&context}; |
278 | NamedAttrList attrs; |
279 | attrs.push_back(newAttribute: b.getNamedAttr("a", b.getStringAttr( "foo"))); |
280 | state.propertiesAttr = attrs.getDictionary(&context); |
281 | { |
282 | auto emitError = [&]() { |
283 | return op->emitError(message: "setting properties failed: "); |
284 | }; |
285 | auto result = state.setProperties(op, emitError); |
286 | EXPECT_TRUE(result.failed()); |
287 | } |
288 | EXPECT_STREQ("setting properties failed: expected IntegerAttr for key `a`", |
289 | diagnosticStr.c_str()); |
290 | } |
291 | |
292 | TEST(OpPropertiesTest, DefaultValues) { |
293 | MLIRContext context; |
294 | context.getOrLoadDialect<TestOpPropertiesDialect>(); |
295 | OperationState state(UnknownLoc::get(&context), |
296 | "test_op_properties.op_with_properties"); |
297 | Operation *op = Operation::create(state); |
298 | ASSERT_TRUE(op != nullptr); |
299 | { |
300 | std::string output; |
301 | llvm::raw_string_ostream os(output); |
302 | op->print(os); |
303 | StringRef view(output); |
304 | EXPECT_TRUE(view.contains("a = -1")); |
305 | EXPECT_TRUE(view.contains("b = -1")); |
306 | EXPECT_TRUE(view.contains("array = array<i64: -33>")); |
307 | } |
308 | op->erase(); |
309 | } |
310 | |
311 | TEST(OpPropertiesTest, Cloning) { |
312 | MLIRContext context; |
313 | context.getOrLoadDialect<TestOpPropertiesDialect>(); |
314 | ParserConfig config(&context); |
315 | // Parse the operation with some properties. |
316 | OwningOpRef<Operation *> op = parseSourceString(sourceStr: mlirSrc, config); |
317 | ASSERT_TRUE(op.get() != nullptr); |
318 | auto opWithProp = dyn_cast<OpWithProperties>(Val: op.get()); |
319 | ASSERT_TRUE(opWithProp); |
320 | Operation *clone = opWithProp->clone(); |
321 | |
322 | // Check that op and its clone prints equally |
323 | std::string opStr; |
324 | std::string cloneStr; |
325 | { |
326 | llvm::raw_string_ostream os(opStr); |
327 | op.get()->print(os); |
328 | } |
329 | { |
330 | llvm::raw_string_ostream os(cloneStr); |
331 | clone->print(os); |
332 | } |
333 | clone->erase(); |
334 | EXPECT_STREQ(opStr.c_str(), cloneStr.c_str()); |
335 | } |
336 | |
337 | TEST(OpPropertiesTest, Equivalence) { |
338 | MLIRContext context; |
339 | context.getOrLoadDialect<TestOpPropertiesDialect>(); |
340 | ParserConfig config(&context); |
341 | // Parse the operation with some properties. |
342 | OwningOpRef<Operation *> op = parseSourceString(sourceStr: mlirSrc, config); |
343 | ASSERT_TRUE(op.get() != nullptr); |
344 | auto opWithProp = dyn_cast<OpWithProperties>(Val: op.get()); |
345 | ASSERT_TRUE(opWithProp); |
346 | llvm::hash_code reference = OperationEquivalence::computeHash(op: opWithProp); |
347 | TestProperties &prop = opWithProp.getProperties(); |
348 | prop.a = 42; |
349 | EXPECT_NE(reference, OperationEquivalence::computeHash(opWithProp)); |
350 | prop.a = -42; |
351 | EXPECT_EQ(reference, OperationEquivalence::computeHash(opWithProp)); |
352 | prop.b = 42.; |
353 | EXPECT_NE(reference, OperationEquivalence::computeHash(opWithProp)); |
354 | prop.b = -42.; |
355 | EXPECT_EQ(reference, OperationEquivalence::computeHash(opWithProp)); |
356 | prop.array.push_back(x: 42); |
357 | EXPECT_NE(reference, OperationEquivalence::computeHash(opWithProp)); |
358 | prop.array.pop_back(); |
359 | EXPECT_EQ(reference, OperationEquivalence::computeHash(opWithProp)); |
360 | } |
361 | |
362 | TEST(OpPropertiesTest, getOrAddProperties) { |
363 | MLIRContext context; |
364 | context.getOrLoadDialect<TestOpPropertiesDialect>(); |
365 | OperationState state(UnknownLoc::get(&context), |
366 | "test_op_properties.op_with_properties"); |
367 | // Test `getOrAddProperties` API on OperationState. |
368 | TestProperties &prop = state.getOrAddProperties<TestProperties>(); |
369 | prop.a = 1; |
370 | prop.b = 2; |
371 | prop.array = {3, 4, 5}; |
372 | Operation *op = Operation::create(state); |
373 | ASSERT_TRUE(op != nullptr); |
374 | { |
375 | std::string output; |
376 | llvm::raw_string_ostream os(output); |
377 | op->print(os); |
378 | StringRef view(output); |
379 | EXPECT_TRUE(view.contains("a = 1")); |
380 | EXPECT_TRUE(view.contains("b = 2")); |
381 | EXPECT_TRUE(view.contains("array = array<i64: 3, 4, 5>")); |
382 | } |
383 | op->erase(); |
384 | } |
385 | |
386 | constexpr StringLiteral withoutPropertiesAttrsSrc = R"mlir( |
387 | "test_op_properties.op_without_properties"() |
388 | {inherent_attr = 42, other_attr = 56} : () -> () |
389 | )mlir"; |
390 | |
391 | TEST(OpPropertiesTest, withoutPropertiesDiscardableAttrs) { |
392 | MLIRContext context; |
393 | context.getOrLoadDialect<TestOpPropertiesDialect>(); |
394 | ParserConfig config(&context); |
395 | OwningOpRef<Operation *> op = |
396 | parseSourceString(sourceStr: withoutPropertiesAttrsSrc, config); |
397 | ASSERT_EQ(llvm::range_size(op->getDiscardableAttrs()), 1u); |
398 | EXPECT_EQ(op->getDiscardableAttrs().begin()->getName().getValue(), |
399 | "other_attr"); |
400 | |
401 | EXPECT_EQ(op->getAttrs().size(), 2u); |
402 | EXPECT_TRUE(op->getInherentAttr("inherent_attr") != std::nullopt); |
403 | EXPECT_TRUE(op->getDiscardableAttr("other_attr") != Attribute()); |
404 | |
405 | std::string output; |
406 | llvm::raw_string_ostream os(output); |
407 | op->print(os); |
408 | StringRef view(output); |
409 | EXPECT_TRUE(view.contains("inherent_attr = 42")); |
410 | EXPECT_TRUE(view.contains("other_attr = 56")); |
411 | |
412 | OwningOpRef<Operation *> reparsed = parseSourceString(sourceStr: os.str(), config); |
413 | auto trivialHash = [](Value v) { return hash_value(arg: v); }; |
414 | auto hash = [&](Operation *operation) { |
415 | return OperationEquivalence::computeHash( |
416 | op: operation, hashOperands: trivialHash, hashResults: trivialHash, |
417 | flags: OperationEquivalence::Flags::IgnoreLocations); |
418 | }; |
419 | EXPECT_TRUE(hash(op.get()) == hash(reparsed.get())); |
420 | } |
421 | |
422 | } // namespace |
423 |
Definitions
- TestProperties
- operator==
- setPropertiesFromAttribute
- getPropertiesAsAttribute
- computeHash
- OpWithProperties
- getAttributeNames
- getOperationName
- getInherentAttr
- setInherentAttr
- populateInherentAttrs
- verifyInherentAttrs
- OpWithoutProperties
- getAttributeNames
- getOperationName
- TestOpPropertiesDialect
- getDialectNamespace
- TestOpPropertiesDialect
- mlirSrc
Improve your Profiling and Debugging skills
Find out more