
013 使用TableGen定义Dialect、Operation与Type从一次凌晨三点的崩溃说起上周调试一个自定义Dialect的lowering pass,凌晨三点,MLIR的assertion failure弹出来,提示我某个Operation的verifier没通过。我盯着屏幕看了十分钟,发现是TableGen里一个字段拼写错了——hasCustomAssemblyFormat写成了hasCustomAssembleyFormat。编译器不会告诉你拼写错误,它只会默默生成一个默认的parser,然后在你运行的时候炸掉。这种坑我踩过不止一次。TableGen是MLIR生态里最强大的代码生成工具,但也是最容易让人翻车的地方。今天这篇笔记,我们就从实战角度,把TableGen定义Dialect、Operation和Type的流程拆开揉碎,顺便把我踩过的坑都标出来。为什么非要用TableGen?你可能觉得,手写C++类定义Operation不就行了?确实可以,但你会很快发现:每个Operation需要定义参数、结果、属性、verifier、parser、printer、builder……一个简单的加法Op,手写C++可能要上百行,而且极易出错。TableGen让你用声明式语法描述这些结构,自动生成大量样板代码。更关键的是,TableGen生成的代码天然与MLIR的框架集成——ODS(Operation Definition Specification)层帮你处理了Dialect注册、Operation的traits推断、类型约束检查。你只需要关注业务逻辑。第一步:定义Dialect先搭架子。创建一个MyDialect.td文件,这是所有定义的入口。// 别把include路径写错,我见过有人把"mlir/IR/DialectBase.td"写成"DialectBase.td" include "mlir/IR/DialectBase.td" // 定义Dialect,名字必须和C++ namespace一致 def My_Dialect : Dialect { let name = "my"; let cppNamespace = "::mlir::my"; let summary = "我的第一个自定义Dialect"; let description = [{ 这个Dialect用于演示TableGen定义流程, 包含一些简单的算术和逻辑操作。 }]; // 这里有个坑:如果你后续要定义Type,必须声明useDefaultAttributePrinterParser // 否则生成的parser/printer会找不到你的自定义类型 let useDefaultAttributePrinterParser = 1; }注意let name = "my",这个字符串会作为Operation名字的前缀,比如my.add。如果你写成let name = "My",那Operation就变成My.add,大小写敏感,别搞混。第二步:定义OperationDialect搭好了,开始定义具体的Operation。新建一个MyOps.td文件,include刚才的Dialect定义。include "mlir/IR/OpBase.td" include "MyDialect.td" // 定义一个加法操作 def My_AddOp : My_Op"add" { let summary = "加法操作"; let description = [{ 执行两个整数的加法,结果类型与输入相同。 注意:目前只支持整数类型,不支持浮点。 }]; // 定义输入参数 let arguments = (ins I32:$lhs, // 左操作数,必须是32位整数 I32:$rhs // 右操作数,必须是32位整数 ); // 定义输出结果 let results = (outs I32:$result // 输出也是32位整数 ); // 这里踩过坑:如果你不指定builders,TableGen会生成默认的 // 但默认builder要求参数顺序和arguments定义顺序一致 // 如果你后续想加一个带属性的builder,必须显式声明 let builders = [ OpBuilder(ins "Value":$lhs, "Value":$rhs) ]; // 自定义汇编格式,让打印更友好 let hasCustomAssemblyFormat = 1; // 自动推导结果类型,这里我们让结果类型等于输入类型 let hasVerifier = 1; }看到My_Op"add"这个写法了吗?My_Op是一个在Dialect定义中自动生成的基类,"add"就是Operation的名字。最终生成的C++类名是AddOp,在my命名空间下。关于类型约束上面用了I32,这是MLIR预定义的类型。但实际项目中,你可能需要更灵活的类型约束。比如支持任意整数类型:def My_AddOp : My_Op"add" { let arguments = (ins AnyInteger:$lhs, AnyInteger:$rhs ); let results = (outs AnyInteger:$result ); // 这里有个技巧:用SameTypeConstraint确保输入输出类型一致 let hasFolder = 1; }AnyInteger是ODS提供的类型约束之一。还有AnyType、AnyFloat、SignlessInteger等。如果你需要自定义类型约束,可以用TypeConstraint定义,但那是进阶话题,后面再聊。第三步:实现自定义AssemblyFormat上面我们设置了hasC