十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

roc 模式穷尽性检查:名义类型构造器参数的类型参数替换机制

roc 模式穷尽性检查:名义类型构造器参数的类型参数替换机制 roc 模式穷尽性检查名义类型构造器参数的类型参数替换机制【免费下载链接】rocA fast, friendly, functional language.项目地址: https://gitcode.com/GitHub_Trending/ro/roc本文以 roc 编译器贡献文档 005_type_parameter_substitution.md 为主体讲解穷尽性检查exhaustiveness checking中“名义类型构造器参数类型参数替换”这一技术点当match的 scrutinee 是Try(I64, Str)这样的名义类型应用时如何把底层模板backing type中的类型参数a、e替换为实际类型实参I64、Str从而得到构造器Ok/Err的真实负载类型。读完本文你将理解该替换问题为什么存在、文档中记录的原始实现算法如何工作以及当前仓库源码中这一机制如何经由openNominalBacking→instantiateNominalBacking的“opening”操作落地。问题背景名义类型与穷尽性检查roc 的模式匹配要求match分支必须穷尽exhaustive。对标签联合类型[Ok(I64), Err(Str)]而言穷尽性算法需要知道“某个构造器tag的负载参数是什么类型”这样才能按构造器特化模式矩阵、判断通配符是否可居、某个分支是否冗余。问题出在名义类型nominal type上。roc 的内置类型Try本质是一个带类型参数的命名联合Try a e : [Ok(a), Err(e)] myTry : Try(I64, Str)对myTry这类类型应用type application类型存储里记录的是“名义外壳”名义名 类型实参而不是直接展开成[Ok(I64), Err(Str)]。因此当穷尽性检查器要查询构造器Ok的参数类型时不能直接读到I64而是会读到模板变量a。文档给出的标准求解步骤是发现Ok在底层类型中的参数类型是a找到a是Try的第一个类型参数查myTry的第一个类型实参即I64返回I64作为Ok的参数类型。这一步骤看似简单但它是穷尽性检查正确处理Try(I64, Str)、Try(Try(I64, Str), Str)、(Try(I64, Str), I64)等一切含名义类型的模式的前提。替换错误会直接导致漏报非穷尽的match、误报冗余分支、或通配符 inhabitedness可居性判断出错。名义类型的内部结构理解替换机制需要先了解名义类型在类型存储中的表示。从 src/check/exhaustive.zig 与 src/check/unify.zig 中的调用方式可以确认一个名义类型NominalType由三部分构成编译器通过三个访问器拆开它type_store.sliceNominalArgs(nominal)取出类型实参列表例如Try(I64, Str)返回[I64, Str]名义声明表lookupNominalDecl(nominal)找到该应用的声明Try的定义Try a e : [Ok(a), Err(e)]类型实例化模块用声明模板 实参生成“展开后的 backing 类型”。一个可辅助理解的层次结构是名义类型: Try(I64, Str) ├── 名称: Try ├── 类型实参: [I64, Str] └── 底层模板: [Ok(a), Err(e)] (其中 a、e 为形参) 替换实例化之后: [Ok(I64), Err(Str)]模板中的a、e是类型存储中的普通变量flex/rigid var它们本身没有“我是 Try 的第一个形参”的自描述信息——这正是文档中算法需要“按首次出现顺序收集形参再按位置映射”的根本原因。文档记录的实现按位置收集形参并替换文档将实现定位于 src/check/exhaustive.zig核心是两个函数。以下内容继承自文档并逐点展开。collectTypeParamsFromBackingType()按序收集类型参数fn collectTypeParamsFromBackingType( type_store: *TypeStore, backing_var: Var, ) error{OutOfMemory}![]const Var该辅助函数遍历底层类型结构按首次出现的顺序收集所有唯一的 flex/rigid 变量即类型参数文档对其行为有四条明确约束使用带显式栈的深度优先遍历不递归避免深类型嵌套导致栈溢出用“已见变量”集合去重——同一个参数重复出现如Pair(a, a)只记录一次返回顺序即声明顺序模板中第一个遇到的形参就是第一个类型参数这保证了后续“按位置映射”的正确性覆盖所有类型结构形态标签联合、元组、记录、函数、嵌套名义类型。为什么必须按“首次出现顺序”收集因为替换映射是位置式的第i个收集到的形参对应第i个类型实参。只有收集顺序与形参声明顺序一致param[i] - nom_args[i]的映射才成立。getCtorArgTypes() 的名义类型分支替换主体文档记录的名义类型处理分支如下引自文档保留其原始注释与逻辑.nominal_type |nominal| { const backing_var type_store.getNominalBackingVar(nominal); const nom_args type_store.sliceNominalArgs(nominal); const backing_args getCtorArgTypes(type_store, backing_var, tag_id); // If no substitution needed, return as-is if (nom_args.len 0 or backing_args.len 0) { return backing_args; } // Check if any backing args are still type parameters (flex/rigid) var needs_substitution false; for (backing_args) |arg| { const arg_resolved type_store.resolveVar(arg); if (arg_resolved.desc.content .flex or arg_resolved.desc.content .rigid) { needs_substitution true; break; } } if (!needs_substitution) { return backing_args; // Already substituted by unification } // Collect type parameters and build substitution map const type_params collectTypeParamsFromBackingType(type_store, backing_var); // Substitute: param[i] - nom_args[i] for (backing_args, 0..) |arg, i| { const arg_resolved type_store.resolveVar(arg); if (arg_resolved.desc.content .flex or arg_resolved.desc.content .rigid) { // Find which parameter index this is for (type_params, 0..) |param, param_idx| { if (type_store.resolveVar(param).var_ arg_resolved.var_) { result[i] nom_args[param_idx]; break; } } } else { result[i] arg; } } return result; }工作流程六个步骤文档给出的整体流程与上述代码一一对应取底层类型参数递归调用getCtorArgTypes从 backing 类型中取出该构造器的参数类型对Try(I64, Str)的Ok得到[a]提前退出没有名义实参、或底层参数为空时直接返回不做任何替换检查是否仍含类型参数若底层参数中已无 flex/rigid 变量说明统一unification阶段已经完成替换直接复用避免重复劳动收集类型参数遍历 backing 类型按顺序找到全部形参变量构建替换映射把每个形参按位置映射到对应的名义类型实参a - I64、e - Str应用替换对底层参数中仍是类型参数的位置做替换非参数位置模板里写死的具体类型原样保留。第 3 步的“早退”设计值得注意类型推断与穷尽性检查共享同一个TypeStore如果之前的统一操作已经把模板变量绑定成具体类型替换是幂等无操作直接返回可以省掉一次全模板遍历。边界情况处理文档明确列举了五类边界情况它们是这类算法正确性的关键边界情况行为说明已被统一的类型直接返回底层参数推断阶段已完成替换幂等处理多个参数支持任意数量如Result(A, B, C)同一参数多次出现按形参索引正确映射如Pair a : [Pair(a, a)]中两个位置都替换为同一实参收集阶段 OOM回退返回未替换的参数保守处理conservative fallback参数个数与实参个数不匹配返回未替换参数防止越界宁可少替换不可错替换其中“同一参数多次出现”这一条在实现上要求替换映射以形参变量身份解析后的根变量为键而不是以“底层参数位置”为键——否则Pair(a, a)的两个位置需要各自独立查找形参索引。当前源码中的落地从手工替换到声明式 opening需要说明的是上述代码是文档记录的该特性实现阶段的方案而当前仓库源码中的getCtorArgTypes已经完成了一次重构——替换职责不再由穷尽性检查器手写遍历完成而是下沉到类型存储的“名义类型 opening”操作中。理解这一演进对读者把握当前代码行为很重要。当前的 getCtorArgTypes打开即已替换当前 src/check/exhaustive.zig 中的getCtorArgTypes签名为fn getCtorArgTypes( type_store: *TypeStore, builtin_idents: BuiltinIdents, type_var: Var, tag_id: TagId, ) std.mem.Allocator.Error!CtorArgTypes它对不同类型结构分别处理标签联合沿扩展链extension chain定位到tag_id所在层并返回该 tag 的args带环检测别名alias递归跟踪其 backing元组、记录作为单构造器类型直接返回元素/字段类型。而名义类型分支src/check/exhaustive.zig现在的写法是.nominal_type |nominal| { // The opening operation instantiates the declarations // backing template with the applications actual args already // substituted for its formals, so the constructor args it // yields are the concrete payload types—no positional // substitution needed. const backing_var (try openNominalBacking(type_store, builtin_idents, nominal)) orelse return .none; return try getCtorArgTypes(type_store, builtin_idents, backing_var, tag_id); }源码注释直接点明了设计意图opening 操作实例化出的 backing 模板已经把实际实参替换进了形参因此构造器参数天然是具体类型不再需要文档中描述的位置式替换。这与文档记录的“收集形参 → 逐位替换”是同一问题的两种实现策略文档方案在穷尽性检查器内部做替换当前方案把替换统一交给类型层。openNominalBacking带缓存的实例化入口当前实现的关键函数是 src/check/exhaustive.zig 中的openNominalBacking/// Filters out uninhabited constructors at construction time. /// The explicit declaration-backed opening operation (issue #9983) for /// exhaustiveness analysis: instantiate the nominal applications backing /// template with its actual args substituted for the declarations formals. fn openNominalBacking( type_store: *TypeStore, builtin_idents: BuiltinIdents, nominal: types.NominalType, ) error{OutOfMemory}!?Var从源码结构看其工作方式与文档算法的“形参→实参”映射完全等价但由类型层保证正确性通过lookupNominalDecl(nominal)找到名义声明sliceNominalArgs(nominal)取出实参以声明, 解析后的实参根变量为键查缓存builtin_idents.open_cache——同一个Try(I64, Str)在一个穷尽性检查入口内只实例化一次递归展开的 backing 收敛到同一张类型图从而让各类遍历器的 seen-set 能够终止缓存未命中时调用types.instantiate.instantiateNominalBacking定义于 src/types/instantiate.zig用实参对声明形参做实例化产出一份“分析专用的临时 backing”analysis-only scratch检查器在每次穷尽性入口结束后回填其 region 信息返回实例化后的 backing 变量交给getCtorArgTypes继续递归。值得对照的是统一器unifier中存在同一个概念的实现——src/check/unify.zig 中的Unifier.openNominalBacking注释同样指向 issue #9983 的“declaration-backed opening operation”。它额外处理了 fresh 变量的登记region/rank 簿记与“仅纯结构才可持久共享”的约束实例化产生了新变量的 opening 不得跨调用复用否则会把一次统一中的绑定带入下一次。两处实现共享同一套实例化内核说明“名义应用 → backing 模板实参替换”已被抽象为类型层的统一原语穷尽性检查只是其调用方之一。这一重构在效果上覆盖了文档列出的全部边界情况多个参数与重复参数由实例化内核按声明处理“已统一/已替换”的早退等价于当前needs_substitution检查被 opening 的缓存命中所取代实参根变量相同的重复查询直接复用同一张图。替换机制在穷尽性算法中的调用位置文档所述替换并非孤立的工具函数而是 Maranget 穷尽性算法roc 的实现见 src/check/exhaustive.zig 模块头注释以“模式矩阵”逐列递归特化中列类型推导的一环。典型调用链是ColumnTypes.specializeByConstructorsrc/check/exhaustive.zig在第一列上按tag_id特化时调用getCtorArgTypes(type_store, builtin_idents, self.types[0], tag_id)若该类型是名义类型进入 opening 路径把Try(I64, Str)的 backing 展开为已替换的[Ok(I64), Err(Str)]取得Ok的负载类型[I64]后校验其与模式期望的元数arity一致然后把负载类型前置插入列类型数组继续对负载列递归检查穷尽性若负载类型不可居如Err([])中的[]该构造器分支会被 inhabitedness 机制过滤Err分支被标记为冗余。因此文档的替换机制直接决定了两类诊断的正确性Try(I64, Str)缺少Err分支时的“非穷尽”报告以及Try(I64, [])上Err分支的“冗余”报告。测试用例文档场景与仓库测试的对应文档列出实现需正确处理的五类场景。仓库中的测试文件 src/check/test/exhaustiveness_test.zig 提供了对应的一手验证摘录如下双参数名义类型文档场景 2Try(I64, Str)Ok实参应为I64、Err实参应为Str// test exhaustive - all tag variants covered for Try第 25 行起 \\x : Try(I64, Str) match x { Ok a - a Err e - e }嵌套名义类型文档场景 4外层与内层的类型参数都必须独立替换Try(Try(I64, Str), Str)要求内外两层Ok/Err全部覆盖// test exhaustive - nested Try patterns fully covered第 310 行起 \\x : Try(Try(I64, Str), Str) match x { Ok (Ok a) - a Ok (Err e) - e Err e - e }若替换只做了一层或层间串扰内层Err的 inhabitedness 判断会出错non-exhaustive - nested Try missing inner Err第 338 行附近一类的测试就会失败。空错误类型替换与 inhabitedness 联动Try(I64, [])中Err的负载是[]空联合不可居只需Ok分支即穷尽// test exhaustive - empty error type means only Ok needed第 367 行起 \\x : Try(I64, []) match x { Ok a - a }元组与名义类型混合文档场景 5“复杂结构”(Try(I64, Str), I64)要求替换结果能作为元组元素类型正确参与多列检查// test exhaustive - tuple with Try第 346 行起 \\x : (Try(I64, Str), I64)此外redundant - pattern after wildcard第 158 行起基于Try(I64, Str)验证了替换后分支覆盖关系的计算。文档场景 1单参数Box(I64)、场景 3Pair(a, a)重复参数在当前测试文件中未找到与文档描述完全同名的独立用例但从源码结构看二者走的正是同一条getCtorArgTypes→openNominalBacking路径由实例化内核的按声明替换保证。内存管理文档对内存行为的说明在实现重构后仍然成立且值得单独成节因为它解释了这类“每次查询都产生数据”的辅助函数为何不显式释放内存文档原始方案中替换结果数组从type_store.gpa分配不显式释放依赖穷尽性检查所用的 arena 分配器整体回收当前实现中openNominalBacking产生的实例化 backing 被源码注释明确标注为“analysis-only scratch”由检查器在每个穷尽性入口结束exhaustiveness entry point后回填 region 并随TypeStore/arena 生命周期回收getCtorArgTypes中临时使用的seen_exts等哈希表则走defer deinit()显式清理见 src/check/exhaustive.zig。这种“入口级 arena 查询级 defer”的组合使得替换路径在深嵌套名义类型下既不会泄漏也不会因为逐节点释放拖慢递归展开。小结围绕 005_type_parameter_substitution.md 这条主线可以把结论收敛为三点问题本质名义类型应用与底层模板之间隔着一层“形参→实参”的映射穷尽性检查只有拿到替换后的具体负载类型才能正确特化模式矩阵、判断 inhabitedness、报告冗余与非穷尽分支两种等价策略文档记录了“按首次出现顺序收集形参、按位置替换”的检查器内算法含 OOM 回退、计数不匹配回退等保守边界处理当前仓库源码将同一职责下沉为openNominalBackinginstantiateNominalBacking的声明式 openingsrc/check/exhaustive.zig、src/types/instantiate.zig并带 (声明, 实参根) 粒度缓存统一器中也有对应实现src/check/unify.zig验证闭环src/check/test/exhaustiveness_test.zig 中围绕Try(I64, Str)、嵌套Try、空错误类型、元组组合的测试用例覆盖了文档列出的全部核心场景可作为修改该路径时的回归基准。对贡献者而言若要改动名义类型的穷尽性处理正确的切入点不是手工构造替换映射而是理解并复用 opening 操作——它已同时服务于统一与穷尽性检查两条路径改动时还需注意其缓存键声明 解析后的实参根与递归终止性seen-set 依赖固定图结构这两个不变量。【免费下载链接】rocA fast, friendly, functional language.项目地址: https://gitcode.com/GitHub_Trending/ro/roc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表