Mojo 编译器中的 Variadics 深度解析:VariadicList、VariadicPack 与底层 MLIR 表示

发布时间:2026/9/11 6:47:56
Mojo 编译器中的 Variadics 深度解析:VariadicList、VariadicPack 与底层 MLIR 表示 Mojo 编译器中的 Variadics 深度解析VariadicList、VariadicPack 与底层 MLIR 表示【免费下载链接】mojoThe Modular Platform (includes MAX Mojo)项目地址: https://gitcode.com/GitHub_Trending/mo/mojo本文是 Mojo 编译器内部实现compiler internals层面的变参variadics技术指南核心内容源自 Mojo/docs/compiler/overviews/Variadics.md并结合 stdlib 源码与 KGEN/LIT 方言的 MLIR 定义进行佐证。你将理解VariadicList、VariadicListMem、VariadicPack三种变参机制的异同掌握!kgen.pack、!kgen.variadic、!lit.ref.pack等底层表示的分工并学会区分 variadic-parameterized、variadic-referring、variadic-capturing 三类函数——这对阅读编译器代码、编写复杂泛型 API 以及调试变参相关问题时都至关重要。阅读前提本文面向编译器内部视角。如果只是想使用 Mojo 的变参语法建议先阅读公开的 variadics 文档Mojo 官方手册中关于变参参数的章节再回到本文探究其底层原理。三种变参形态速览TL;DRMojo 的变参并非只有一种形态编译器内部将其划分为三类对应三个不同的运行时表示VariadicList本质是单一可寄存器传递register-passable类型的运行时数组所有元素在内存中连续排列。典型用法def foo(*args: Int): ...。VariadicListMem本质是指针的运行时数组。与VariadicList类似但它存储的是指针因此指向的元素也可以是 memory-only 类型即不能放进 SSA 寄存器的类型。典型用法def foo(*args: Spaceship): ...。VariadicPack本质是指针构成的元组tuple指向一组共享某个公共 trait 的异构对象。典型用法def foo*arg_types: Stringable: ...。一个常见的疑虑是变参全部走间接寻址indirection会不会低效比如往一个 variadic pack 里传入 Int、Float 这样的读取值并不希望它们被间接引用。文档明确指出这一层开销会在 elaboration 之后的参数约定降级argument convention lowering阶段被消除最终运行时是高效直接的——这与 C 的实现方式不同。也就是说编译器内部先以统一、简单的方式建模变参再在后续 pipeline 中针对具体调用点做优化。VariadicList 与 VariadicPack 的核心区别从概念上讲两者泾渭分明VariadicList 是同质元素的数组VariadicList存寄存器可传递的值本身VariadicListMem存指针指向的值可以是 memory 类型。VariadicPack 是异构值的元组元素一律通过指针间接引用。文档归纳了四条主要差异这也是理解 Mojo 变参类型系统最重要的框架1. 长度何时可知VariadicList的长度是运行时值——除非列表本身是参数parameter此时因为所有元素也是参数长度在编译期comptime可知。VariadicPack的长度在编译期也因此在运行期恒可知因为 elaborator 会针对调用方的每一组具体类型列表实例化函数类型列表本身就是参数。2. 同质 vs 异构Elaboration 之后VariadicList是同质的运行时是数组VariadicPack是异构的运行时是指针元组。Elaboration 之前VariadicList同质VariadicPack可以视作同质——所有元素共享一个公共 trait从这个角度看是同质的但概念上它们指向的是各种不同类型的对象。3. 如何迭代VariadicList直接用普通for循环即可长度是运行时值元素同质。VariadicPack由于各元素类型可能不同必须使用comptime for、pack.each或pack.each_idx在编译期展开迭代。4. 如何索引VariadicList可以在编译期或运行期索引因为它本质就是一个运行时数组。VariadicPack只能在编译期索引因为它本质是元组编译器必须知道每个位置的具体类型才能生成正确的访问代码。底层参与者kgen.pack、lit.ref.pack、kgen.variadic文档给出了一张参与者清单这些是 Mojo 编译管线中真实存在的 MLIR 类型/结构名称语义说明!kgen.pack异构元素列表元素在内存中紧挨着存放类似于std::tupleMojo 的Tuple只是对它的包装。解析器不直接使用它而是使用!lit.ref.pack!lit.ref.pack异构指针列表指针在内存中紧挨着存放类似指针的std::tuple。VariadicPack包装它适用于变参场景kgen.variadic同质元素列表例如kgen.variadicInt32就是 int 列表也可以是某个 trait 的同质列表如kgen.variadicAnyType在 elaboration 之后可以变成异构列表Tuple包装kgen.pack用户可见的元组类型VariadicPack包装lit.ref.pack其元素类型由编译期的kgen.variadic类型列表描述VariadicList包装kgen.variadic仅可用于 trivial 类型文档建议在未来废弃改用VariadicListMemVariadicListMem也包装kgen.variadic因为追踪 origins可以用于 memory 类型文档特别强调了一个容易误解的点不存在lit.pack因为它与kgen.pack冗余。另外VariadicPack内部并不包含运行时的kgen.variadic元素——它包含的是编译期类型列表kgen.variadic所描述的一堆指针lit.ref.pack。kgen.variadic 的双重角色kgen.variadic在文档中出现频率很高且看似神秘其角色可以这样澄清它本质上就是一个数组把它当作数组看待即可。VariadicList/VariadicListMem包含一个运行时值的kgen.variadic数组。VariadicPack包含一个运行时的lit.ref.pack指针元组但使用编译期的kgen.variadic类型数组来描述这些值的类型。一句话总结VariadicList在运行时使用值的kgen.variadicVariadicPack在编译期使用类型的kgen.variadic描述运行时lit.ref.pack中各元素的类型。源码佐证!kgen.param_list、!kgen.struct 与 !lit.ref.pack在 Mojo/include/Mojo/KGENDialect/KGENTypes.td 中定义了!kgen.param_list类型a homogeneously typed list of zero or more elements并且携带原始参数约定owned/read/mut 等。而!kgen.struct同文件 L766-L812描述的是元素类型按声明顺序排列的结构体其元素类型存储为TypedAttr既可以是已解析的具体ParamListAttr也可以是 elaboration 之前的参数化表达式如变参参数引用它还带有一个isParamPack布尔标志参数约定降级阶段会据此判断何时可以展平flatten一个 pack。在 Mojo/include/Mojo/LITDialect/LITTypes.td 中!lit.ref.pack被定义为一组异构的引用references其运行时的语义表示正是!kgen.struct[!lit.reff32, lt, !lit.refString, lt] isParamPack即一包引用组成的结构体。这里也印证了文档的说法解析器parser生成的是!lit.ref.pack而不是直接使用!kgen.pack。在 stdlib 侧Mojo/stdlib/std/builtin/variadics.mojo 中VariadicPack的_mlir_type正是!lit.ref.pack:param_listelement_trait Ts, origin——即用编译期类型列表Ts与公共 traitelement_trait参数化的一包引用。而同文件 L1108-L1134 中的VariadicList则持有Span[Pointer[element_type, origin], ...]即元素指针的 Span运行时以数组形式存在因此支持运行时下标与普通for迭代。从 VariadicPack 加载到 SSA 值两种读取方式读取一个kgen.pack有两种方式文档给出了明确的使用建议一次性整体加载例如VariadicPack.get_loaded_kgen_pack。把整个 pack 一次性加载为一个 SSA 值。取单个元素指针再解引用例如Tuple.__getitem__的做法。拿到某个元素的指针再加载该指针。编译器内部几乎总是采用第二种方式。第一种方式仅在 FFI 场景下有用——比如把一串元素交给printf之类的 C 变参函数。原因有二如果元素类型是 memory-only 类型整体加载操作是未定义的memory-only 类型无法加载进 SSA 寄存器。只有当所有元素都可寄存器传递时才能做整体加载把 memory 类型加载进 SSA 寄存器本身就是不可能的。因此get_loaded_kgen_pack只在调用 C 变参函数之类的特定 hack 位置使用。在 stdlib 源码 variadics.mojo 中get_loaded_kgen_pack的实现正是通过kgen.struct.load_indirect一次性加载所有元素其注释明确写着Useful for FFI, such as calling printf. Otherwise, avoid this if possible.对 FFI 有用如调用 printf否则尽可能避免。而VariadicPack.__getitem_param__L1531-L1545则是通过lit.ref.pack.extract提取单个元素的引用——这正是文档推荐的第二种路径。VariadicList 的元素类型不能是 traitVariadicList的元素类型不能是 trait因为普通参数的类型不能是 trait 类型——Mojo 不支持存在类型existentials后者依赖动态分发dynamic dispatch和行为behavior。而VariadicPack的元素类型可以是 traitVariadicPack在 elaboration 之后可以是异构的每个元素的具体类型各不相同但都符合同一 trait因此允许以 trait 作为元素约束。Variadics 是运行时值除开 comptime/解释器/优化器等特殊情况外变参在运行时都有地址VariadicList包装kgen.variadic概念上类似于 Java 的变参——参数被收集进一个指针数组。正因为它运行时确实存在一个数组它的索引才能接受运行时参数参见VariadicListMem的公开文档。VariadicPack包装lit.ref.pack也一样但它在运行时降级为一个结构体其__get__的索引是泛型参数编译期常量。VariadicList之所以是这种形态是因为编译器刻意不针对通过VariadicList传入的所有值做实例化——它保持为运行时数组从而避免为每个调用点生成独立的代码。kgen.pack 与 kgen.struct 的区别kgen.pack比kgen.struct更通用kgen.struct要求元素类型列表是已知的例如[Int, F32]kgen.pack允许该列表是参数化的即允许元素elements这种形式。kgen.pack是参数化的它以一个TypedAttr作为参数描述内容对于Tuple而言这个内容参数就是一个kgen.variadic。在 KGENTypes.td 的!kgen.struct描述中可以看到其元素类型存储可以是parametric expression (e.g., a variadic parameter reference) before elaboration正是这种参数化能力的体现。VariadicPack 的元素类型只能是 trait看VariadicPack的定义stdlib 源码中的实际版本比文档略有演进增加了is_owned参数struct VariadicPack[ elt_is_mutable: Bool, origin: Origin[mutelt_is_mutable], element_trait: type_of(AnyType), //, is_owned: Bool, *Ts: element_trait, ](Copyable where (not is_owned, ...), RegisterPassable, Sized):其中element_trait: type_of(AnyType)意味着element_trait可以是任何 trait——但不能是某个具体 struct 或 int。换句话说VariadicPack的每个元素都必须由 trait 约束这是它与VariadicList在类型约束上的又一关键分野。星号*的三种含义在 Mojo 变参语法中*出现在不同位置代表完全不同的机制。文档用下面这段示例说明def bar(*vlist: Int): ... def foo*arg_types: MyTrait: ...*vlist: Int产生一个VariadicList[Int]同质运行时数组。*arg_types: MyTrait产生一个kgen.variadic本质是数组的类型每个类型都符合MyTrait——这是编译期类型列表。*args: *arg_types产生一个VariadicPack其元素类型由arg_types这个kgen.variadic描述内部包含一个kgen.pack装着这些类型的值。三种星号分别作用于值列表运行时数组、类型列表编译期参数、以及以类型列表描述的一包值。这正是 Mojo 变参区别于其他语言的关键设计把值的变参和类型的变参统一在一个语法模型里。变参参数化 / 变参引用 / 变参捕获函数PPPRPCF文档引入了三个容易混淆的概念并给出了对照示例def variadic_parameterized*arg_types: MyTrait: ... struct VariadicStruct[*arg_types: MyTrait]: def variadic_referring(*args: *arg_types): ... def variadic_capturing[ *arg_types: MyTrait, //, func: def (*args: *arg_types) - Int, ]() - VariadicStruct[*arg_types]: ...variadic-parameterized变参参数化函数可以接收任意参数。例如一个Int加一个Bool六个Spaceship什么都不传甚至七十三个TurtleDove。variadic-referring变参引用函数不能随便接收参数只能接收与VariadicStruct实例化时完全相同的参数集合即复用 struct 的arg_types。variadic-capturing变参捕获函数可以接收任何函数参数值这些参数的实参会被读入arg_types参数声明。日常所说的变参函数variadic function在这三者之间常常产生歧义。在深入任何涉及变参函数的代码之前务必先厘清自己讨论的是哪一种。文档还提示关于 variadic-capturing 函数的更多细节见下一节的 IFAIAV。将函数实参推断进变参IFAIAV函数类型特性IFAIAVInferring Function Args Into A Variadic描述的是一个函数可以把传入的fn参数值的实参类型读出来并推断进一个kgen.variadic参数声明。示例源自测试用例 CAIASVdef device_func(a: Int, b: Bool) - Int: ... value struct DeviceFunction[*arg_types: MyTrait]: def call(self, *args: *arg_types) - Int: ... def infer_variadic[ *arg_types: MyTrait, //, # paraphrased func: def (*args: *arg_types) - Int, ]() - DeviceFunction[*arg_types]: return DeviceFunction[*arg_types]() def main(): var thing infer_variadic[device_func]() var result1 thing.call(42, True)注意infer_variadic并不是 variadic-parameterized 函数见上一节 PPPRPCF 的分类它属于variadic-capturing 函数。这个例子做了三件事main把一个类型为def(Int, Bool)的函数作为参数值传给infer_variadic的输入参数func其期望类型为def(*:*arg_types)。在传参过程中infer_variadic的调用点推断出arg_types variadic(Int, Bool)。调用点随之知道应当返回DeviceFunction[variadic(Int, Bool)]。从thing.call这一行可以看到实际效果它强制要求用户传给call的参数类型与device_func的形参类型完全一致从而在编译期就保证了类型安全。这个特性有时也被称为函数类型function types特性。综合一份编译器内部的变参速查清单把文档与源码结合可以归纳出以下便于记忆的要点三种用户态变参类型VariadicList同质值数组仅 trivial 类型建议废弃、VariadicListMem指针数组支持 memory 类型、VariadicPack异构指针元组元素须符合公共 trait。四种底层表示!kgen.pack异构元素包Tuple的底层、!lit.ref.pack异构引用包VariadicPack的底层、kgen.variadic同质列表既是运行时值数组也是编译期类型数组、!kgen.struct已知元素类型列表的结构体带isParamPack标志。迭代规则VariadicList用普通forVariadicPack用comptime for或each/each_idx。索引规则VariadicList编译期/运行期皆可索引VariadicPack仅编译期索引。读取规则几乎总是取元素指针再加载只有 FFI如printf场景才整体加载 pack。函数分类先分清 variadic-parameterized / variadic-referring / variadic-capturing再讨论变参函数行为。性能模型变参的间接寻址会在 elaboration 后的参数约定降级阶段被优化消除不必担心运行时开销。这份清单可以直接作为阅读 Mojo/lib/Elaborator 与 Mojo/lib/KGENToLLVM 相关源码时的参照地图。理解这些底层表示与分工是深入 Mojo 编译器变参实现、乃至参与其标准库与语言特性开发的基础。【免费下载链接】mojoThe Modular Platform (includes MAX Mojo)项目地址: https://gitcode.com/GitHub_Trending/mo/mojo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询