news 2026/9/11 16:41:59

Mojo 编译器泛型处理内幕:解析器中泛型的四个非显而易见机制(IRAIDAI / DCRTODS / PSTIAIRAID / STCHDDDOS)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Mojo 编译器泛型处理内幕:解析器中泛型的四个非显而易见机制(IRAIDAI / DCRTODS / PSTIAIRAID / STCHDDDOS)

Mojo 编译器泛型处理内幕:解析器中泛型的四个非显而易见机制(IRAIDAI / DCRTODS / PSTIAIRAID / STCHDDDOS)

【免费下载链接】mojoThe Modular Platform (includes MAX & Mojo)项目地址: https://gitcode.com/GitHub_Trending/mo/mojo

本文聚焦 Mojo 编译器源码仓库中Mojo/docs/compiler/arcana/Generics.md所记录的泛型解析核心机制:IndexRefAttrInterface(基于索引的参数引用)、深度(depth)与索引(index)语义、ParameterScopeTypeInterface对深度的作用,以及"同型不同深度"带来的相等性比较陷阱。读完本文,你将理解 Mojo 编译器如何在解析阶段用"相对位置"代替参数名来描述泛型引用,掌握编写或阅读编译器泛型相关代码(如IndexParameterReplacerParameterReplacerParameterEvaluator)时必备的深度感知(depth-aware)思维,并清楚为何 MLIR 内置的.walk不能用于此类遍历。

引言:为什么泛型处理在解析器中如此"非显而易见"

Mojo 是一门支持强参数多态(parametric polymorphism)的语言,其泛型参数(如T: AnyTypeN: Int)在编译器前端解析、KGEN 方言生成、求值等各阶段都被大量引用。而泛型处理的难点在于:同一个类型或表达式里引用的参数,可能来自不同层级的嵌套签名,且这些引用需要在不依赖参数名的情况下保持语义一致、可判等。

Generics.md是 Mojo 编译器"秘辛"(arcana)系列文档之一,它用四个缩略词概括了解析器处理泛型时的四个非显而易见之处:

  • IRAIDAIIndexRefAttrInterface、深度与索引(Depths and Indexes)
  • DCRTODS:深度不能引用 Op 声明的签名(Depths Cannot Refer To Op-Declared Signatures)
  • PSTIAIRAIDParameterScopeTypeInterface影响IndexRefAttrInterface的深度
  • STCHDDDOS:同一类型在不同作用域可以有不同的深度(Same Type Can Have Different Depths Depending On Scope)

本文将以这四个机制为骨架,结合仓库中 KGENDialect 与 MojoParser 的源码实现(Mojo/include/Mojo/KGENDialect/KGENAttrInterfaces.tdMojo/include/Mojo/KGENDialect/KGENAttrs.tdMojo/include/Mojo/KGENDialect/KGENTypeInterfaces.tdMojo/include/Mojo/KGENDialect/ParameterReplacer.h)逐一展开。

IRAIDAI:IndexRefAttrInterface、深度与索引

两个等价的别名

考虑以下两个 Mojo 别名(alias),它们的类型是完全相等的:

alias A: defT: AnyType->None = ... alias B: defY: AnyType->None = ...

直觉上我们知道AB是同一个函数类型——参数名TY只是占位符,实际语义没有差别。但要让编译器"知道"它们相等,一个简洁的办法是:把名字擦除,改用相对位置来描述参数引用。于是上面的两个别名可以写成:

alias A: def_: AnyType)->None = ... alias B: def_: AnyType)->None = ...

这里*(0,0)就是一个"索引式参数引用"(index-based parameter reference),其中第一个分量是深度(depth),第二个分量是索引(index)。

IndexRefAttrInterface的两个字段

在 KGENDialect 的 TableGen 接口定义中(Mojo/include/Mojo/KGENDialect/KGENAttrInterfaces.td),IndexRefAttrInterface的官方描述指出:这是一种用一对整数、完全不涉及名字的相对参数引用方案,其价值在于"即便参数名不同,也能判定两个类型是否相等":

"Index-based parameter references are a relative parameter referencing scheme that uses a pair of integers to reference parameters in a way that doesn't involve names. This is useful for later knowing if two types are equal, even if they have different parameter names."

IndexRefAttrInterface的所有实现都包含两个字段:

字段含义取值范围
depth目标参数声明位于"第几层外层签名"(ParameterScopeTypeInterface/ParameterScopeAttrInterface)中。0表示最近的包含签名,1表示再往上一层非负整数
index该参数声明在目标签名中的序号(第几个输入参数)非负整数

接口还暴露了三个方法(对应 TableGen 中的InterfaceMethod):getDepth()返回深度、getIndex()返回索引、replace()在不改变被引用参数语义的前提下修改 depth/index 并携带新的子元素。

对应到具体属性实现(Mojo/include/Mojo/KGENDialect/KGENAttrs.td),#kgen.param.index.ref的 MLIR 文本形式即#kgen.param.index.ref<depth, index> : type,例如:

// 最近签名中的第二个输入参数 #kgen.param.index.ref<0, 1> : index // 下一层外层签名的第一个输入参数 #kgen.param.index.ref<1, 0> : !lit.struct<@Int>

非零深度示例:SIMD[N, D]中的N

文档给出了一个体现非零深度的典型例子:

alias bar: def D: DType, N: Int, f: def[Y: AnyType->None ](...) = ...

这里SIMD[N, D]中的N是一个#kgen.param.index.ref<1, 0> : !lit.struct<@Int>(有时写成*(1,0))。原因在于:N并没有引用"最近的包含签名"(即f所在的那个def[Y: AnyType]),而是引用它的外层签名(bar自身的签名)。在参数引用与参数声明之间隔着1 层签名,所以其深度为1,索引为0Nbar签名的第 2 个参数,从 0 起算)。

原文档特别提醒:计算depth时要格外小心,很容易算错

并非所有参数引用都用索引

需要澄清的是:不是所有参数引用都采用索引与深度。还有一种传统的ParamDeclRefAttr#kgen.param.decl.ref),它仍然按名字引用参数。其定义见Mojo/include/Mojo/KGENDialect/KGENAttrs.td

// 对名为 "p"、类型为 i1 的参数的引用 #kgen.param.decl.ref<"p"> : i1

ParamDeclRefAttr是一个带类型的属性,包含被引用参数的类型和名字,其类型必须始终与被引用参数的类型一致。至于什么时候必须用哪种引用,就是下一节 DCRTODS 的主题。

DCRTODS:深度不能引用 Op 声明的签名

一个反直觉的例子

考虑下面的代码:

def fooX: AnyType: alias bar: defY: AnyType = ...

按照 IRAIDAI 的直觉,你可能会认为bar的类型是defAnyType, *(0,0))——因为"类型一般不会包含ParamDeclRefAttr"。但这个推断是错误的

bar的真实类型是:

defAnyType)

也就是说,Y的引用被替换成了*(0,0),而X的引用保持为名字X不变。为什么?

规则:看参数声明由谁发出

关键规则只有一条:

  • 当引用的是某个 Op 的参数声明时,使用ParamDeclRefAttr(按名字)。
  • 当引用的是某个 Type 或 Attr 的参数声明时,使用ParamIndexRefAttr(按深度+索引)。

回到例子,把涉及的参数声明逐一归类:

参数声明由谁声明引用方式
Xdef fooOpParamDeclRefAttr(保留名字X
baralias barOpParamDeclRefAttr
Y: AnyTypefnTypeParamIndexRefAttr*(0,0)

Xbar都是由 Op 声明的,因此用ParamDeclRefAttr按名字引用;Y是由函数类型声明的,因此用ParamIndexRefAttr按索引引用。

Mojo/include/Mojo/KGENDialect/KGENAttrs.td中,ParamIndexRefAttr的注释也给出了同样的反例(comptime zork版本),并明确标注了禁止出现的写法:

def fooX: AnyType: comptime zork: def...( # 不允许: #kgen.param.index.ref<1, 0> : !lit.struct<@Int> )->None = ...

经验法则

原文档给出的经验法则非常简洁:深度(depth)只能指向包含它的GeneratorTypeGeneratorAttr;对于其他一切(比如X这样的 Op 参数),使用ParamDeclRefAttr

这条规则也是后续所有深度处理代码(PSTIAIRAID、STCHDDDOS)的前提。

PSTIAIRAID:ParameterScopeTypeInterface影响IndexRefAttrInterface的深度

深度的两种情形

回顾 IRAIDAI:depth == 0表示引用最近的外层签名,例如:

alias A: def[T: AnyType->None = ...

其中x: TT就是*(0,0)(深度 0,索引 0)。

depth == 1表示引用再外层一层的签名,例如前面SIMD[N, D]中的N

alias bar: def D: DType, N: Int, f: def[Y: AnyType->None ](...) = ...

机制:所有签名都继承ParameterScopeTypeInterface

为了管理深度,编译器让所有"会声明参数的签名类型"统一继承ParameterScopeTypeInterfaceMojo/include/Mojo/KGENDialect/KGENTypeInterfaces.td),代码在处理深度时专门监视该接口。

该接口的官方描述用一个更完整的例子说明了其语义:

def foo[T: AnyType](): comptime bork: def T: AnyType, inner_f: def[Y: AnyType -> None ] -> None = ...

bork:之后的def是一个kgen.generator,它实现了ParameterScopeTypeInterface。描述中还明确指出:

ParameterScopeTypeInterface也会导致其内部(即便是间接)包含的ParamIndexRefAttr/ImplicitOriginRefAttr等的depth字段变大,见 PSTIAIRAID。

也就是说,每进入一层签名,内部引用的深度就加 1——这正是"深度感知"遍历的基础。同理,属性一侧对应ParameterScopeAttrInterfaceMojo/include/Mojo/KGENDialect/KGENAttrInterfaces.td),它让属性内部的ParamIndexRefAttr可以引用所在作用域声明的参数。

深度感知搜索(depth-aware searching)

文档给出的例子:

alias bar: def[ T: AnyType, L: List[T], f: defY: AnyType->None ](...) = ...

它被解释为:

alias bar: def[ _: AnyType, _: List[*(0,0)], _: def_: AnyType, List[*(1,0)])->None ](...) = ...

如果我们要查找代码中所有对T的引用,不能简单地在整棵树上找*(0,0)——那样会同时命中第一个T(正确)和内层签名中Y的引用(错误)。正确做法是:在递归搜索过程中记录当前处于第几层签名。在外层签名中找*(0,0),一旦搜索下潜进内层签名,就改为找*(1,0)

这正是文档所说的"深度感知搜索"(depth-aware searching),它在处理任何"可能(哪怕间接)包含签名"的对象时都至关重要。

结论:慎用AttrTypeReplacerAttrTypeWalker,提防 MLIR 内置.walk

文档给出两条极具实战价值的警告:

  1. 三思而后用AttrTypeReplacerAttrTypeWalker——它们不是深度感知的。应当优先考虑IndexParameterReplacerParameterReplacerParameterEvaluatorParserParameterEvaluatorIndexRefRemapper等深度感知工具。
  2. MLIR 的内置.walk方法不是深度感知的!(原文用了加粗强调)——直接用它遍历带签名嵌套的结构会算错深度。

IndexParameterReplacer为例(Mojo/include/Mojo/KGENDialect/ParameterReplacer.h),其类注释与实现印证了上述设计:

"把这个depth传给replaceImpl是这个类的核心目的,它让replaceImpl的实现知道在递归遍历中目前深入签名作用域多少层。例如,它们可以检查depth == 0判断是否在原始作用域,或检查indexRef.getDepth() == depth判断该 indexRef 是否引用原始作用域的参数声明。"

doReplace的递归实现展示了深度递增的关键逻辑:

// Increment depth when looking inside signatures, see PSTIAIRAID. if constexpr (std::is_base_of_v<Attribute, T>) if (isa<ParameterScopeAttrInterface>(value)) ++depth; if constexpr (std::is_base_of_v<Type, T>) if (isa<ParameterScopeTypeInterface>(value)) ++depth;

即:每当递归进入一个实现了ParameterScopeAttrInterface/ParameterScopeTypeInterface的子对象,就把当前深度加一,再向下传递。这与 PSTIAIRAID 描述的语义完全吻合,也解释了为什么这些 replacer 能正确处理嵌套签名而 MLIR 原生的.walk不能。

STCHDDDOS:同一类型在不同作用域可有不同深度

同型不同 depth

继续沿用 PSTIAIRAID 的例子:

alias bar: def[ T: AnyType, L: List[T], f: defY: AnyType->None ](...) = ...

解释后:

alias bar: def[ _: AnyType, _: List[*(0,0)], _: def_: AnyType, List[*(1,0)])->None ](...) = ...

注意List[T]现在出现了两个不同的类型表示

  • List[*(0,0)](外层签名中的T
  • List[*(1,0)](内层签名中引用的外层T

尽管它们语义上是同一个类型。

结论:相等的类型不总是"指针可比较"的

文档指出:

这意味着相等的类型并不总是指针可比较的

这颇具讽刺意味——因为引入深度的初衷(按 IRAIDAI)恰恰是为了"便于判定两个类型是否相等"(源码接口描述中的 "This is useful for later knowing if two types are equal")。于是编译器需要在多处对这种"同型不同深度"做特殊处理。

比较前的两种调整策略

当需要比较List[*(0,0)]List[*(1,0)]是否相等时,必须先统一深度,二选一:

  • 递减更深签名里引用的深度:把List[*(1,0)]中的深度从 1 减为 0;
  • 递增外层签名里引用的深度:把List[*(0,0)]中的深度从 0 增为 1。

只有完成这样的深度统一之后,才能安全地比较两个类型是否相等。

典型触发场景:实例化参数化值(STCHDDDOS-A / STCHDDDOS-B)

文档给出了两个必须警惕的关键场景:

场景 A:实例化一个参数化生成器值(generator value)。例如给定一个生成器值<index> *(0,0) + 1(对任意索引类型参数,返回其加 1),可以把外层作用域的索引引用绑定进去,如bind_params(<index> *(0,0) + 1, *(0,1))。对bind_params算子做折叠,会得到一个新的、输入参数为 0 个的生成器值<> *(1,1) + 1。注意它仍然是一个生成器值;必须再执行一次显式的"实例化"步骤来解开生成器,而这次解包需要对生成器体内的所有索引引用做一次深度递减——因为此时生成器体处于少一层的生成器作用域之下。

场景 B:apply算子作用于已实例化的参数化函数。当被调方是一个已实例化的参数化函数时,产生的函数类型同样需要对其中的索引引用做深度递减(文档中称之为"up-binding")。这个操作与场景 A 是同一类问题在另一个算子上的体现。

这两个场景的共同点在于:任何跨越签名边界"解包/折叠"参数化结构的操作,都必须同步调整内部索引引用的深度,否则会产生悬挂的、语义错误的深度值。

实战总结:处理泛型深度时需要记住的清单

综合原文档与仓库实现,处理 Mojo 解析器 / KGEN 方言中的泛型时,建议按以下清单自查:

  1. 先分清引用种类:引用 Op 声明的参数用ParamDeclRefAttr(按名字),引用 Type/Attr 声明的参数用ParamIndexRefAttr(按depth+index)。深度只能指向包含它的GeneratorType/GeneratorAttr(DCRTODS)。
  2. 算深度时数清签名层数0是最近的签名,每向外一层 +1;计算时容易出错(IRAIDAI)。
  3. 遍历优先用深度感知工具:如IndexParameterReplacerParameterReplacerParameterEvaluatorParserParameterEvaluatorIndexRefRemapper;避免AttrTypeReplacer/AttrTypeWalker绝不要用 MLIR 内置.walk(PSTIAIRAID)。
  4. 比较相等性前先统一深度:要么递减深层引用,要么递增外层引用,然后再判等;要留意bind_params折叠、apply实例化等会改变深度层次的操作(STCHDDDOS-A/B)。

这些机制的最终目标,是在不依赖参数名的前提下让"形异义同"的泛型类型可以判等、替换与求值——理解depth/index这对相对坐标,是读懂 Mojo 编译器泛型前端(MojoParser 与 KGENDialect)代码的一把钥匙。感兴趣的读者可以继续在仓库中阅读Mojo/include/Mojo/KGENDialect/ParameterEvaluator.h(其中第 2 条规则专门描述ParamIndexRefAttr的替换规则)、Mojo/include/Mojo/KGENDialect/KGENParameters.h(递归遍历并替换ParamIndexRefAttr、以及ParamIndexRefAttrFinder检测器),以及Mojo/test/mojo-parser/parameters.mojo中对应的解析器测试用例来加深理解。

【免费下载链接】mojoThe Modular Platform (includes MAX & Mojo)项目地址: https://gitcode.com/GitHub_Trending/mo/mojo

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 16:41:31

基于TensorFlow与Django的个性化电影推荐系统项目实战解析

简介&#xff1a;面向计算机相关专业学生与毕业设计开发者&#xff0c;资源包以PythonDjangoTensorflow构建了带前端界面的个性化电影推荐系统&#xff0c;涵盖从数据处理、模型训练到Web展示的完整闭环&#xff0c;既可用于毕设/课设&#xff0c;也适合作为推荐系统入门到实战…

作者头像 李华
网站建设 2026/9/11 16:40:24

Tableau Desktop高DPI显示问题解决方案

1. 问题现象与常见场景 Tableau Desktop作为一款专业的数据可视化工具&#xff0c;其界面显示问题直接影响用户体验和工作效率。在实际使用中&#xff0c;用户经常会遇到界面元素显示异常的情况——要么界面元素过大&#xff0c;挤占有限的工作空间&#xff1b;要么过小&#x…

作者头像 李华
网站建设 2026/9/11 16:37:41

Anki 如何用过滤牌组(Filtered Deck)按指定顺序临时复习卡片?

Anki 如何用过滤牌组&#xff08;Filtered Deck&#xff09;按指定顺序临时复习卡片&#xff1f; 【免费下载链接】anki Anki is a smart spaced repetition flashcard program 项目地址: https://gitcode.com/GitHub_Trending/an/anki 当你需要临时离开日常计划——比如…

作者头像 李华
网站建设 2026/9/11 16:33:51

LeetCode 1515题解:Weiszfeld算法求解服务中心最佳位置

1. 问题背景与理解这道LeetCode困难题1515"服务中心的最佳位置"描述了一个典型的设施选址优化问题。题目给定平面上的一组客户点坐标&#xff0c;要求找到一个服务中心的位置&#xff0c;使得该中心到所有客户点的欧几里得距离之和最小。这在实际应用中非常常见&…

作者头像 李华