- 编程语言
- 编译器
- 语言运行时
- 开发工具
【免费下载链接】unison
A friendly programming language from the future
在 Unison 中,术语(term)与类型(type)的名称由其内容哈希(content hash)决定,递归定义的哈希依赖彼此。本文基于unison-src/transcripts/errors/incomplete-data-element-ordering-error.md这一错误 transcript,结合unison-hashing-v2的哈希实现源码,完整讲解 Unison 编译器在何种场景下会拒绝把相互递归定义写入代码库、其底层检测机制(IncompleteElementOrderingError/ Bug 引用E253299)以及可行的规避写法。读完你既能复现该报错,也能理解 Unison 内容寻址哈希为保唯一性所做的防御设计。
一、背景:内容寻址与“循环定义组”哈希
Unison 采用内容寻址:一段定义保存进代码库后,其全局唯一标识是内容的哈希值,所有对它的引用都以哈希传递。因此哈希算法必须满足一个硬性约束——每一个合法定义都必须对应唯一且稳定的哈希。
对于相互递归的定义(如type A引用B、type B引用A),它们组成一个强连通分量(SCC)。哈希这类“循环”时,不能逐一定义孤立地算哈希,而必须把整个环作为一个整体:
- 先对环内所有元素各自计算哈希;
- 依据这些哈希给环内元素排出一个规范顺序(canonical ordering);
- 把规范顺序后的元素哈希依次累积,得到整个环的哈希。
这一流程的实现在 unison-hashing-v2/src/Unison/Hashing/V2/ABT.hs 中:hashComponents(L106-L134)负责把一组定义按强连通分量切分并逐一哈希,hashComponent(L73-L100)负责对单个环“排序 + 哈希”。
关键点在于第 2 步:规范顺序要求环内每个元素排出的哈希互不相同。如果环内存在两个结构等价(仅内部引用不同)的元素,它们的哈希相同,就无法确定唯一的规范顺序——这正是本文要讲的报错场景。
二、错误场景:相互递归的数据类型(原文档核心示例)
原文档给出的第一个用例是两个相互引用的structural type:
structural type CycleA = OneA Int CycleB structural type CycleB = OneB Int CycleA在 transcript 中,前置操作为:
scratch/main> builtins.merge随后把上述两个类型加入代码库时,应报错。原文档对此的解释是:
这是因为环中每个元素彼此之间完全相同,唯一的区别是它们内部的引用(each element in the cycle is identical to one another except for the internal references)。
观察CycleA与CycleB:两者都只有一个构造器、构造器首字段都是Int、都只引用对方——剥离“名字与内部引用”之后,二者的结构骨架完全一致。
三、同类错误的术语版:相互递归的函数
同样的问题也存在于术语层面。姊妹 transcript unison-src/transcripts/errors/incomplete-term-element-ordering-error.md 给出了一个do块中相互调用的函数对:
foo = do bar () bar = do foo ()两个函数体都只调用对方,除内部引用外结构完全相同,因此触发的是同一条IncompleteElementOrderingError(User "bar", User "foo"无法被完全排序)。
四、触发后的实际输出
以数据版本为例,运行该 transcript 得到的实际输出见 incomplete-data-element-ordering-error.output.md,核心内容为:
🐞 Sorry, you've encountered a weird situation that we are aware of and are currently working on a fix for. I'll explain what happened and how you can work around it. The following cyclic definition sets could not be completely ordered: * User "CycleA", User "CycleB" This happens when multiple definitions in a mutually recursive cycle have a very similar structure. You can work around this by restructuring them to be less similar, e.g. by adding a pure expression to distinguish them, like: _ = "this is the foo definition"注意:该错误消息末尾附带了 Bug 引用号E253299,提示这类情况是 Unison 已知问题、官方正在修复中。也就是说,这并非用户写法的语法错误,而是哈希排序阶段的编译器防御性失败(reportBug "E253299"即用于生成这段“已知 Bug + 规避建议”的消息)。
五、底层机制:doHashCycle如何发现“结构等价元素”
报错消息来自unison-hashing-v2中HashingWarning类型的IncompleteElementOrderingError构造子:
-- unison-hashing-v2/src/Unison/Hashing/V2/ABT.hs, L33-L38 data HashingWarning = -- | two or more component elements can not be completely ordered with respect to one another -- https://github.com/unisonweb/unison/issues/2787 IncompleteElementOrderingError (NonEmpty (NonEmpty String {- Each list is a set of structurally equivalent component elements -})) deriving stock (Eq, Ord) deriving anyclass (Exception)其注释明确写着:“两个或多个组件元素无法彼此完全排序”,且每个内层列表就是一组“结构等价(structurally equivalent)的组件元素”。
检测发生在环哈希的核心函数doHashCycle(L176-L217)。它的工作方式如下:
- 构造“环内环境”
permutationEnv = Left names : env(L197),使环内每个名字都能被解析为环境索引; - 在环内环境下对每个成员计算哈希
namedHashes(L198-L200),这些哈希不包含成员自身的名字,因此结构等价的成员会算出完全相同的哈希; - 按哈希排序得到规范顺序
permutedNames(L202-L209); - 通过
structurallyEquivalentElements(L210-L217)按哈希分组(Map.fromListWith (<>)),凡是某个哈希值对应超过一个名字的组,即视为一组结构等价元素; - 每组等价元素生成一条
IncompleteElementOrderingError(L190-L192),并以元组累积器把警告收集起来。
而crashOnHashingWarning(L68-L71)负责把收集到的第一条警告直接抛为异常,中止哈希流程:
crashOnHashingWarning :: (HasCallStack) => ([HashingWarning], a) -> a crashOnHashingWarning = \case ([], a) -> a (hf : _, _) -> throw hf源码注释(L186-L189)点明了排序阶段的意图:“确保我们用于排序组件的所有哈希都是唯一的;如果不唯一,我们就得到了环中元素的不完全排序(incomplete ordering)。”
六、为什么这些定义必须被拒绝:哈希唯一性论证
原文档给出了拒绝这类定义的根本理由:
我们不能允许这些术语进入代码库,因为在某些情况下,会存在多个合法且不同的组件却获得相同的哈希(in certain cases there are multiple valid distinct components which would receive the same hash)。
结合源码可以还原其推理链:
- 环哈希的最终值,是“按成员哈希排好序后逐项累积”得到的;
- 当两个成员哈希相同(结构等价),排序结果只取决于输入顺序,即规范顺序不唯一;
- 对于同一组逻辑等价但顺序不同的定义,可能产生不同哈希;反之,不同来源的定义也可能因哈希相同而冲突;
- 内容寻址体系以“哈希唯一对应一个定义”为前提,一旦允许这种歧义进入代码库,引用解析、去重与合并都会产生不可预知的结果。
因此,编译器宁可选择在哈希阶段抛错(crashOnHashingWarning抛出IncompleteElementOrderingError),也不允许把这类环写入代码库。这一设计把“宁可误杀、不可放过”体现在了哈希排序的前置检查中。类似Show实例与错误渲染在 codebase2/codebase-sqlite/U/Codebase/Sqlite/HashHandle.hs 也有一套镜像实现,说明该防御机制贯穿哈希核心与 SQLite 代码库两套实现。
七、错误消息解读与规避方法
错误消息的每一段都有明确含义:
| 消息片段 | 含义 |
|---|---|
The following cyclic definition sets could not be completely ordered:后列出的* User "CycleA", User "CycleB" | 无法完全排序的具体环及其中结构等价的成员集合 |
This happens when multiple definitions in a mutually recursive cycle have a very similar structure. | 触发条件:互递归环内多个定义结构过于相似 |
You can work around this by restructuring them to be less similar ... adding a pure expression to distinguish them | 官方给出的临时规避手段 |
Bug reference: E253299 | 该报错属于 Unison 已知 Bug(unison-hashing-v2/src/Unison/Hashing/V2/ABT.hs 中reportBug "E253299"生成),官方已在修复计划中 |
规避写法:让环内成员在结构上可区分。最直接的方式是给其中一个定义附加一个“纯表达式”作为锚点,例如在被报错的定义之后追加:
_ = "this is the foo definition"这样环内两个成员在哈希时便不再共享相同结构,规范顺序得以唯一确定,定义可以被正常写入代码库。这属于临时 workaround,待官方修复(参考 unison-hashing-v2/src/Unison/Hashing/V2/ABT.hs 中“希望未来能彻底杜绝该错误”的注释)后即可移除。
八、调用链与相关源码
理解报错在完整哈希流程中的位置,有助于排查与复现:
- 数据类型声明(本文主场景):
Unison.Hashing.V2.DataDeclaration.hashDecls0(unison-hashing-v2/src/Unison/Hashing/V2/DataDeclaration.hs)先把每个DataDeclaration转换为 ABT,再调用Reference.Util.hashComponents,最终落到ABT.hashComponents与doHashCycle;其中crashOnHashingWarning在 L61 直接参与。 - 术语(函数)版本:
Unison.Hashing.V2.Term.hashTerm/hashTermComponents(unison-hashing-v2/src/Unison/Hashing/V2/Term.hs 与 L122 均调用ReferenceUtil.hashComponents)。 - 文件级入口:用户通过
add等命令添加定义时,parser-typechecker/src/Unison/UnisonFile.hs 会对整个 Unison 文件调用Hashing.crashOnHashingWarning $ Hashing.hashTermComponents ...——这也是为什么上述报错会在添加定义时以异常形式直接打断操作。 - 同族错误 transcript:incomplete-term-element-ordering-error.md 及其 输出文件 展示了术语版的完整报错与规避建议,可作为数据声明版的对照实验。
如果你在实际开发中遇到Bug reference: E253299,可按本文第八节的调用链在unison-hashing-v2内定位哈希环节,并按第七节的建议先以“附加纯表达式”的方式解除阻塞,待编译器后续修复该排序歧义后即可回归正常写法。
- 编程语言
- 编译器
- 语言运行时
- 开发工具
【免费下载链接】unison
A friendly programming language from the future
相关推荐
告别哈希冲突:Abseil哈希框架的可扩展实现与自定义哈希实战
告别哈希冲突:Abseil哈希框架的可扩展实现与自定义哈希实战 你是否还在为哈希冲突头疼?是否想让自定义类型轻松支持哈希功能?本文将带你深入了解Abseil哈希
标准库后端深度解密Open WebUI:构建企业级AI助手平台的5大核心技术架构
深度解密Open WebUI:构建企业级AI助手平台的5大核心技术架构 Open WebUI是一个功能强大的开源AI平台,为企业提供可扩展、自托管的人工智能解决
人工智能大模型AI 应用RAGAI Agent本地部署交互助手后端前端用 while 循环重写 for 循环:JavaScript 循环结构等价转换实战指南
用 while 循环重写 for 循环:JavaScript 循环结构等价转换实战指南 导读 for 与 while 是 JavaScript 中最常用的两种循
文档教程前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考