LLZK 3.0.0
An open-source IR for Zero Knowledge (ZK) circuits
Loading...
Searching...
No Matches
OpCAPITestGen.cpp
Go to the documentation of this file.
1//===- OpCAPITestGen.cpp - C API test generator for operations ------------===//
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// OpCAPITestGen generates unit tests for the C API operations generated by
11// OpCAPIGen. These are link-time tests that ensure all generated functions
12// compile and link properly, using a pattern where the C API function is
13// wrapped in a conditional that is always false but the compiler still ensures
14// the function within will link correctly.
15//
16// Test Strategy:
17// - Each test creates a dummy operation from a different dialect (arith.constant)
18// - Tests then call the generated C API functions inside an if statement that
19// checks if the dummy op is of the target type (always false)
20// - The compiler still verifies the function signatures and the linker ensures
21// the symbols are defined, even though the code never executes at runtime
22//
23// These tests will catch the following kinds of issues:
24// - Functions declared but not defined (link errors)
25// - Signature mismatches between header and implementation
26// - Missing functions in the build system
27// - ABI compatibility issues
28// - Refactoring breaks
29//
30// However, the following issues will NOT be caught:
31// - Generator logic bugs (if generator is wrong, tests will be wrong too)
32// - Runtime behavior
33// - Semantic correctness
34//
35//===----------------------------------------------------------------------===//
36
37#include "CAPIGenRegistration.h"
38#include "CommonCAPIGen.h"
39#include "OpCAPIParamHelper.h"
40
41#include <mlir/TableGen/GenInfo.h>
42#include <mlir/TableGen/Operator.h>
43
44#include <llvm/ADT/StringExtras.h>
45#include <llvm/Support/CommandLine.h>
46#include <llvm/Support/FormatVariadic.h>
47#include <llvm/TableGen/Record.h>
48#include <llvm/TableGen/TableGenBackend.h>
49
50using namespace mlir;
51using namespace mlir::tblgen;
52
53namespace {
54
60struct OpTestGenerator : public TestGenerator {
63 OpTestGenerator(llvm::raw_ostream &outputStream) : TestGenerator("Operation", outputStream) {}
64
70 std::string genCleanup() const override { return "mlirOperationDestroy"; };
71
74 void genBuildOpTests(const Operator &op) const {
75 static constexpr char fmt[] = R"(
77TEST_F({1}OperationLinkTests, {0}_{2}_Build) {{
78 // Returns an `arith.constant` op, which will never match the {2} dialect check.
79 auto testOp = createIndexOperation();
80
81 // This condition is always false, so the function is never actually called.
82 // We only verify it compiles and links correctly.
83 if ({0}OperationIsA_{1}_{2}(testOp)) {{
84 MlirOpBuilder builder = mlirOpBuilderCreate(context);
85 MlirLocation location = mlirLocationUnknownGet(context);
86{3}
87 (void){0}{1}_{2}Build(builder, location{4});
88 // No need to destroy builder or op since this code never runs.
89 }
90
91 mlirOperationDestroy(testOp);
92}
93
94struct {2}BuildFuncHelper : public TestAnyBuildFuncHelper<CAPITest> {
95 virtual bool callIsA(MlirOperation op) override { return {0}OperationIsA_{1}_{2}(op); }
99 static std::unique_ptr<{2}BuildFuncHelper> get();
100
101protected:
102 {2}BuildFuncHelper() = default;
103};
107TEST_F(CAPITest, {1}_{2}_build_pass) { {2}BuildFuncHelper::get()->run(*this); }
108)";
109
110 assert(!className.empty() && "className must be set");
111 os << llvm::formatv(
112 fmt,
113 getFunctionPrefix(), // {0}
114 dialectNameCapitalized, // {1}
115 className, // {2}
116 generateBuildDummyParams(op), // {3}
117 generateBuildParamList(op) // {4}
118 );
119 }
120
123 void genOperandTests(const Operator &op) const {
124 static constexpr char OperandGetterTest[] = R"(
125TEST_F({1}OperationLinkTests, {0}_{2}_Get{3}) {{
126 auto testOp = createIndexOperation();
127
128 if ({0}OperationIsA_{1}_{2}(testOp)) {{
129 (void){0}{1}_{2}Get{3}(testOp);
130 }
131
132 mlirOperationDestroy(testOp);
133}
134)";
135
136 static constexpr char OperandSetterTest[] = R"(
137TEST_F({1}OperationLinkTests, {0}_{2}_Set{3}) {{
138 auto testOp = createIndexOperation();
139
140 if ({0}OperationIsA_{1}_{2}(testOp)) {{
141 auto dummyValue = mlirOperationGetResult(testOp, 0);
142 {0}{1}_{2}Set{3}(testOp, dummyValue);
143 }
144
145 mlirOperationDestroy(testOp);
146}
147)";
148
149 static constexpr char VariadicOperandGetterTest[] = R"(
150TEST_F({1}OperationLinkTests, {0}_{2}_Get{3}Count) {{
151 auto testOp = createIndexOperation();
152
153 if ({0}OperationIsA_{1}_{2}(testOp)) {{
154 (void){0}{1}_{2}Get{3}Count(testOp);
155 }
156
157 mlirOperationDestroy(testOp);
158}
159
160TEST_F({1}OperationLinkTests, {0}_{2}_Get{3}At) {{
161 auto testOp = createIndexOperation();
162
163 if ({0}OperationIsA_{1}_{2}(testOp)) {{
164 (void){0}{1}_{2}Get{3}At(testOp, 0);
165 }
166
167 mlirOperationDestroy(testOp);
168}
169)";
170
171 static constexpr char VariadicOperandSetterTest[] = R"(
172TEST_F({1}OperationLinkTests, {0}_{2}_Set{3}_Variadic) {{
173 auto testOp = createIndexOperation();
174
175 if ({0}OperationIsA_{1}_{2}(testOp)) {{
176 auto dummyValue = mlirOperationGetResult(testOp, 0);
177 MlirValue values[] = {{dummyValue};
178 {0}{1}_{2}Set{3}(testOp, 1, values);
179 }
180
181 mlirOperationDestroy(testOp);
182}
183)";
184
185 static constexpr char VariadicOfVariadicOperandSetterTest[] = R"(
186TEST_F({1}OperationLinkTests, {0}_{2}_Set{3}_VariadicOfVariadic) {{
187 auto testOp = createIndexOperation();
188
189 if ({0}OperationIsA_{1}_{2}(testOp)) {{
190 auto dummyValue = mlirOperationGetResult(testOp, 0);
191 MlirValueRange groups[1];
192 groups[0].values = &dummyValue;
193 groups[0].size = 1;
194 {0}{1}_{2}Set{3}(testOp, 1, groups);
195 }
196
197 mlirOperationDestroy(testOp);
198}
199)";
200 assert(!className.empty() && "className must be set");
201
202 for (int i = 0, e = op.getNumOperands(); i < e; ++i) {
203 const auto &operand = op.getOperand(i);
204 std::string capName = toPascalCase(operand.name);
205 if (operand.isVariadic()) {
207 os << llvm::formatv(
208 VariadicOperandGetterTest,
209 getFunctionPrefix(), // {0}
210 dialectNameCapitalized, // {1}
211 className, // {2}
212 capName // {3}
213 );
214 }
216 os << llvm::formatv(
217 operand.isVariadicOfVariadic() ? VariadicOfVariadicOperandSetterTest
218 : VariadicOperandSetterTest,
219 getFunctionPrefix(), // {0}
220 dialectNameCapitalized, // {1}
221 className, // {2}
222 capName // {3}
223 );
224 }
225 } else {
227 os << llvm::formatv(
228 OperandGetterTest,
229 getFunctionPrefix(), // {0}
230 dialectNameCapitalized, // {1}
231 className, // {2}
232 capName // {3}
233 );
234 }
236 os << llvm::formatv(
237 OperandSetterTest,
238 getFunctionPrefix(), // {0}
239 dialectNameCapitalized, // {1}
240 className, // {2}
241 capName // {3}
242 );
243 }
244 }
245 }
246 }
247
250 void genAttributeTests(const Operator &op) const {
251 static constexpr char AttributeGetterTest[] = R"(
252TEST_F({1}OperationLinkTests, {0}_{2}_Get{3}Attr) {{
253 auto testOp = createIndexOperation();
254
255 if ({0}OperationIsA_{1}_{2}(testOp)) {{
256 (void){0}{1}_{2}Get{3}(testOp);
257 }
258
259 mlirOperationDestroy(testOp);
260}
261)";
262
263 static constexpr char AttributeSetterTest[] = R"(
264TEST_F({1}OperationLinkTests, {0}_{2}_Set{3}Attr) {{
265 auto testOp = createIndexOperation();
266
267 if ({0}OperationIsA_{1}_{2}(testOp)) {{
268 {0}{1}_{2}Set{3}(testOp, createIndexAttribute());
269 }
270
271 mlirOperationDestroy(testOp);
272}
273)";
274 assert(!className.empty() && "className must be set");
275
276 for (const auto &namedAttr : op.getAttributes()) {
277 std::string capName = toPascalCase(namedAttr.name);
279 os << llvm::formatv(
280 AttributeGetterTest,
281 getFunctionPrefix(), // {0}
282 dialectNameCapitalized, // {1}
283 className, // {2}
284 capName // {3}
285 );
286 }
288 os << llvm::formatv(
289 AttributeSetterTest,
290 getFunctionPrefix(), // {0}
291 dialectNameCapitalized, // {1}
292 className, // {2}
293 capName // {3}
294 );
295 }
296 }
297 }
298
301 void genResultTests(const Operator &op) const {
302 static constexpr char ResultGetterTest[] = R"(
303TEST_F({1}OperationLinkTests, {0}_{2}_Get{3}) {{
304 auto testOp = createIndexOperation();
305
306 if ({0}OperationIsA_{1}_{2}(testOp)) {{
307 (void){0}{1}_{2}Get{3}(testOp);
308 }
309
310 mlirOperationDestroy(testOp);
311}
312)";
313
314 static constexpr char VariadicResultGetterTest[] = R"(
315TEST_F({1}OperationLinkTests, {0}_{2}_Get{3}Count) {{
316 auto testOp = createIndexOperation();
317
318 if ({0}OperationIsA_{1}_{2}(testOp)) {{
319 (void){0}{1}_{2}Get{3}Count(testOp);
320 }
321
322 mlirOperationDestroy(testOp);
323}
324
325TEST_F({1}OperationLinkTests, {0}_{2}_Get{3}At) {{
326 auto testOp = createIndexOperation();
327
328 if ({0}OperationIsA_{1}_{2}(testOp)) {{
329 (void){0}{1}_{2}Get{3}At(testOp, 0);
330 }
331
332 mlirOperationDestroy(testOp);
333}
334)";
335 assert(!className.empty() && "className must be set");
336
337 for (int i = 0, e = op.getNumResults(); i < e; ++i) {
338 const auto &result = op.getResult(i);
339 llvm::StringRef name = result.name;
340 std::string capName = name.empty() ? llvm::formatv("Result{0}", i).str() : toPascalCase(name);
341
342 if (result.isVariadic()) {
343 os << llvm::formatv(
344 VariadicResultGetterTest,
345 getFunctionPrefix(), // {0}
346 dialectNameCapitalized, // {1}
347 className, // {2}
348 capName // {3}
349 );
350 } else {
351 os << llvm::formatv(
352 ResultGetterTest,
353 getFunctionPrefix(), // {0}
354 dialectNameCapitalized, // {1}
355 className, // {2}
356 capName // {3}
357 );
358 }
359 }
360 }
361
364 void genRegionTests(const Operator &op) const {
365 static constexpr char RegionGetterTest[] = R"(
366TEST_F({1}OperationLinkTests, {0}_{2}_Get{3}Region) {{
367 auto testOp = createIndexOperation();
368
369 if ({0}OperationIsA_{1}_{2}(testOp)) {{
370 (void){0}{1}_{2}Get{3}(testOp);
371 }
372
373 mlirOperationDestroy(testOp);
374}
375)";
376
377 static constexpr char VariadicRegionGetterTest[] = R"(
378TEST_F({1}OperationLinkTests, {0}_{2}_Get{3}Count) {{
379 auto testOp = createIndexOperation();
380
381 if ({0}OperationIsA_{1}_{2}(testOp)) {{
382 (void){0}{1}_{2}Get{3}Count(testOp);
383 }
384
385 mlirOperationDestroy(testOp);
386}
387
388TEST_F({1}OperationLinkTests, {0}_{2}_Get{3}At) {{
389 auto testOp = createIndexOperation();
390
391 if ({0}OperationIsA_{1}_{2}(testOp)) {{
392 (void){0}{1}_{2}Get{3}At(testOp, 0);
393 }
394
395 mlirOperationDestroy(testOp);
396}
397)";
398 assert(!className.empty() && "className must be set");
399
400 for (unsigned int i = 0, e = op.getNumRegions(); i < e; ++i) {
401 const auto &region = op.getRegion(i);
402 llvm::StringRef name = region.name;
403 std::string capName = name.empty() ? llvm::formatv("Region{0}", i).str() : toPascalCase(name);
404
405 if (region.isVariadic()) {
406 os << llvm::formatv(
407 VariadicRegionGetterTest,
408 getFunctionPrefix(), // {0}
409 dialectNameCapitalized, // {1}
410 className, // {2}
411 capName // {3}
412 );
413 } else {
414 os << llvm::formatv(
415 RegionGetterTest,
416 getFunctionPrefix(), // {0}
417 dialectNameCapitalized, // {1}
418 className, // {2}
419 capName // {3}
420 );
421 }
422 }
423 }
424
427 void genCompleteRecord(const Operator &op) {
428 const Dialect &defDialect = op.getDialect();
429
430 // Generate for the selected dialect only
431 if (defDialect.getName() != getDialectName()) {
432 return;
433 }
434
435 this->setNamespaceAndClassName(defDialect, op.getCppClassName());
436
437 if (getGenIsA()) {
438 this->genIsATest();
439 }
440 if (getGenOpBuild() && !op.skipDefaultBuilders()) {
441 this->genBuildOpTests(op);
442 }
444 this->genOperandTests(op);
445 }
447 this->genAttributeTests(op);
448 }
449 if (getGenOpRegionGetters()) {
450 this->genRegionTests(op);
451 }
452 if (getGenOpResultGetters()) {
453 this->genResultTests(op);
454 }
456 for (const ExtraMethod &method : getCAPIExposedOpMethods(op)) {
457 this->genExtraMethod(method);
458 }
459 }
460 }
461
462private:
466 static std::string generateBuildDummyParams(const Operator &op) {
467 struct : GenStringFromOpPieces {
468 void genHeader(llvm::raw_ostream &os) override {
469 // Declare dummyValue first
470 os << " auto dummyValue = mlirOperationGetResult(testOp, 0);\n";
471 }
472 void genResult(
473 llvm::raw_ostream &os, const NamedTypeConstraint &result, const std::string &resultName
474 ) override {
475 if (result.isVariadic()) {
476 os << llvm::formatv(
477 " auto {0}TypeArray = createIndexType();\n"
478 " MlirType {0}Types[] = {{{0}TypeArray};\n"
479 " intptr_t {0}Size = 0;\n",
480 resultName
481 );
482 } else {
483 os << llvm::formatv(" auto {0}Type = createIndexType();\n", resultName);
484 }
485 }
486 void genOperand(llvm::raw_ostream &os, const NamedTypeConstraint &operand) override {
487 // per `generateParamList()` only need to create something additional in case
488 // of variadic operand, otherwise `dummyValue` is used directly.
489 if (operand.isVariadic()) {
490 os << llvm::formatv(
491 " MlirValue {0}Values[] = {{dummyValue};\n"
492 " intptr_t {0}Size = 0;\n",
493 operand.name
494 );
495 }
496 }
497 void genAttribute(llvm::raw_ostream &os, const NamedAttribute &attr) override {
498 std::string rhs;
499 std::optional<std::string> attrType = tryCppTypeToCapiType(attr.attr.getStorageType());
500 if (attrType.has_value() && attrType.value() == "MlirIdentifier") {
501 rhs = "mlirOperationGetName(testOp)";
502 } else {
503 rhs = "createIndexAttribute()";
504 }
505 os << llvm::formatv(" auto {0}Attr = {1};\n", attr.name, rhs);
506 }
507 void genRegion(llvm::raw_ostream &os, const mlir::tblgen::NamedRegion &region) override {
508 if (region.isVariadic()) {
509 os << llvm::formatv(" unsigned {0}Count = 0;\n", region.name);
510 }
511 }
512 } paramsStringGenerator;
513 return paramsStringGenerator.gen(op);
514 }
515
519 static std::string generateBuildParamList(const Operator &op) {
520 struct : GenStringFromOpPieces {
521 void genResult(
522 llvm::raw_ostream &os, const NamedTypeConstraint &result, const std::string &resultName
523 ) override {
524 if (result.isVariadic()) {
525 os << llvm::formatv(", {0}Size, {0}Types", resultName);
526 } else {
527 os << llvm::formatv(", {0}Type", resultName);
528 }
529 }
530 void genOperand(llvm::raw_ostream &os, const NamedTypeConstraint &operand) override {
531 if (operand.isVariadic()) {
532 os << llvm::formatv(", {0}Size, {0}Values", operand.name);
533 } else {
534 os << ", dummyValue";
535 }
536 }
537 void genAttribute(llvm::raw_ostream &os, const NamedAttribute &attr) override {
538 os << llvm::formatv(", {0}Attr", attr.name);
539 }
540 void genRegion(llvm::raw_ostream &os, const mlir::tblgen::NamedRegion &region) override {
541 if (region.isVariadic()) {
542 os << llvm::formatv(", {0}Count", region.name);
543 }
544 }
545 } paramsStringGenerator;
546 return paramsStringGenerator.gen(op);
547 }
548};
549
550} // namespace
551
553static bool emitOpCAPITests(const llvm::RecordKeeper &records, raw_ostream &os) {
554 // Generate file header
555 emitSourceFileHeader("Op C API Tests", os, records);
556
557 // Create generator
558 OpTestGenerator generator(os);
559
560 // Generate test class prologue
561 generator.genTestClassPrologue();
562
563 // Generate tests for each operation
564 for (const auto *def : records.getAllDerivedDefinitions("Op")) {
565 Operator op(def);
566 generator.genCompleteRecord(op);
567 }
568
569 return false;
570}
571
573 static mlir::GenRegistration genOpCAPITests(
574 "gen-op-capi-tests", "Generate operation C API unit tests", &emitOpCAPITests
575 );
576}
llvm::cl::opt< bool > & getGenOpOperandGetters()
Return the GenOpOperandGetters option, registering it on first access.
std::optional< std::string > tryCppTypeToCapiType(StringRef cppType)
Convert C++ type to MLIR C API type.
llvm::cl::opt< std::string > & getFunctionPrefix()
Return the FunctionPrefix option, registering it on first access.
llvm::cl::opt< bool > & getGenOpRegionGetters()
Return the GenOpRegionGetters option, registering it on first access.
llvm::cl::opt< bool > & getGenOpResultGetters()
Return the GenOpResultGetters option, registering it on first access.
llvm::cl::opt< std::string > & getDialectName()
Return the DialectName option, registering it on first access.
llvm::cl::opt< bool > & getGenOpAttributeSetters()
Return the GenOpAttributeSetters option, registering it on first access.
llvm::cl::opt< bool > & getGenOpBuild()
Return the GenOpBuild option, registering it on first access.
llvm::cl::opt< bool > & getGenOpAttributeGetters()
Return the GenOpAttributeGetters option, registering it on first access.
llvm::cl::opt< bool > & getGenExtraClassMethods()
Return the GenExtraClassMethods option, registering it on first access.
llvm::cl::opt< bool > & getGenIsA()
Return the GenIsA option, registering it on first access.
llvm::cl::opt< bool > & getGenOpOperandSetters()
Return the GenOpOperandSetters option, registering it on first access.
llvm::SmallVector< ExtraMethod > getCAPIExposedOpMethods(const mlir::tblgen::Operator &op)
Return public operation methods explicitly requested by DeclareOpInterfaceMethods traits and declared...
std::string toPascalCase(mlir::StringRef str)
Convert names separated by underscore or colon to PascalCase.
void registerOpCAPITestGenerator()
Register the operation C API test generator before command-line parsing.
Generator for common test implementation file elements.