首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
Unison 的 `upgrade` 依赖后缀化修复:从歧义的 `c = y + 1` 到精确的 `c = b.y + 1`
📅 2026/10/9 10:02:58
✍️ 爱科研究院
👁 阅读 3,247
编程语言编译器语言运行时开发工具【免费下载链接】unisonA friendly programming language from the future项目地址https://gitcode.com/gh_mirrors/un/unison点击查看免费下载导读本文以 Unison 仓库中的回归测试 fix-5323.md 为线索剖析 UCMUnison Codebase Managerupgrade命令在重写库依赖时如何对被升级库的依赖者dependents进行名称后缀化suffixify渲染并深入到 Upgrade.hs 与 Name.hs 的源码实现。读完本文你将掌握 upgrade 的渲染 → 回填 → 解析替换流水线、suffixifyByName/suffixifyByHash系列算法的语义与消歧规则并能亲手复现与验证该 Bug 修复。一、问题背景upgrade 依赖者渲染为何会歧义fix-5323 的回归测试文档开篇即点明了这个 Bug 的本质This transcript demonstrates that dependents of an upgrade are suffixified properly. Previously,c b.y 1would render asc y 1(ambiguous).在 Unison 中upgrade会把代码库里的lib.old依赖整体替换为lib.new凡是直接或传递依赖lib.old中定义的 term 都会被自动重写。重写的实现方式并不是对 AST 做引用替换而是先把这些依赖者定义用去掉旧库名称的 pretty-print 环境渲染成 Unison 源码文本再用只包含新库名称的解析环境重新解析并类型检查见 Upgrade.hs 中handleUpgrade1的注释。问题就出在渲染这一步如果渲染时没有对名称做正确的后缀化suffixify一个本应写为c b.y 1的定义可能会被渲染成c y 1。当命名空间中同时存在a.y和b.y甚至还有lib.*.y时裸的y就是歧义名称——重新解析时它无法唯一确定指代哪一个引用导致升级失败或产生错误结果。fix-5323 正是把这一场景固化为 transcript防止该 Bug 回归。二、回归测试的载体UCM Transcript 的运行机制该文档位于 unison-src/transcripts/idempotent/ 目录下。Unison 的 transcript 是一种命令脚本 期望输出合一的可执行文档以ucm代码块书写要在 UCM 中执行的命令以unison代码块书写要载入 scratch 的 Unison 源码紧随其后的ucm :added-by-ucm代码块由 transcript 运行器在首次执行时回填真实输出并在后续运行时比对校验任何输出漂移都会让测试失败。从目录名idempotent可以推断这套 transcript 还要求命令可重复执行且输出保持稳定。整个 transcript 套件由仓库中的 scripts/transcripts.sh 驱动其证明脚本位于 scripts/proofs/transcripts.sh运行入口可参考 unison-cli/transcripts/Transcripts.hs。因此fix-5323.md 不仅是一份文档更是一个可执行、可回归的测试用例。三、操作序列逐步复现fix-5323 的完整流程3.1 合并内置库transcript 的第一步是把内置库builtins以lib.builtin的名义合并进当前命名空间为后续定义提供Nat等基础类型 builtins.merge lib.builtin Done.builtins.merge是 Unison 初始化新代码库的标准步骤它把编译器内置的引用集合挂到lib.builtin下作为所有用户代码的间接依赖。3.2 编写旧库 / 新库 / 依赖者三组定义接着在 scratch 文件中写入待演练的 Unison 代码old.x 17 new.x 100 a.y 18 b.y old.x 1 c b.y 1这里构造了一个典型的升级依赖链old.x/new.x同一逻辑实体在旧库与新库中的两份实现17 与 100之后会被分别移动到lib.old.x与lib.new.xb.y直接依赖old.x因此是 upgrade 的直接依赖者c依赖b.y是 upgrade 的传递依赖者a.y与b.y同名段同为y是制造名称歧义的关键——正因为有a.y存在裸的y无法唯一指代b.y这正是 Bug 得以暴露的环境。3.3 update 入库把上述定义写入代码库分支。transcript 的:added-by-ucm块展示了此时 UCM 检测到的变更注意这里使用了:added-by-ucm标记表示输出由运行器回填校验Loading changes detected in scratch.u. a.y : Nat b.y : Nat c : Nat new.x : Nat old.x : Nat Run update to apply these changes to your codebase.随后执行update将 5 个新定义提交进分支 update Okay, Im searching the branch for code that needs to be updated... Done.update的处理逻辑在 Update2.hs 的handleUpdate2中它同样涉及渲染后回填依赖者与后缀化处理与upgrade共享Cli.UpdateUtils中的nameHydratedRefIds、parseAndTypecheck等工具函数是理解 upgrade 前置知识的另一半参见 Update2.hs 中关于by name / by hash 两种后缀化的注释。3.4 将新旧定义移动进 lib 命名空间升级的对象是lib.old/lib.new两个库依赖所以先把old.x、new.x分别移动到lib.old.x、lib.new.x move old.x lib.old.x Done. move new.x lib.new.x Done.move命令底层模式名为moveAll的语义是重命名 term、type 与命名空间在 InputPatterns.hs 中定义为move foo barrenames the term, type, and namespace foo to bar。这里lib.old.x、lib.new.x这种lib.name.def的路径正是 UCM 识别库依赖libdep的标准位置——handleUpgrade会先构造lib.old与lib.new两个绝对路径并读取其分支内容Upgrade.hs。3.5 执行升级最后执行关键命令 upgrade old new I upgraded old to new.upgrade命令的完整定义在 InputPatterns.hs模式名lib.upgrade别名upgrade.lib与upgrade参数形式为upgrade old new [old2 new2...]即成对出现lib.old升级到lib.new、lib.old2升级到lib.new2可一次升级多个库。这一整段 transcript 的可验证结论是升级后b.y与c被正确重写且c的渲染结果保留了b.y这个最短无歧义后缀而不是退化为歧义的y。四、upgrade 的替换流水线渲染 → 回填 → 解析handleUpgrade的入口在 Upgrade.hs它先做参数合法性检查参数必须是偶数个否则报 takes an even number of arguments且old不能等于new否则报 I cant upgrade ... to itself!。随后handleUpgrade1执行真正的替换流程Upgrade.hs状态守卫若当前分支正处于update/upgrade/merge过程中则提前返回CantDoThatDuring对应 Output.hs避免在中间态上再次叠加操作。收集升级信息对每个(old, new)对读取lib.old的完整深定义oldDeepDefns、剥离 libdep 后的本地定义oldLocalDefns以及lib.new的本地定义newLocalDefns。计算依赖者集合通过getNamespaceDependentsOf找出当前命名空间中所有依赖旧库定义的 term/typedependents再把它们从代码库中水合出来hydrateRefs并用nameHydratedRefIds把引用与名称关联Upgrade.hs。渲染用精心构造的 pretty-print 环境见第五节把依赖者定义渲染成 Unison 源码文本由renderDefnsForUnisonFile完成Upgrade.hs。解析makeParsingEnv基于去掉全部旧库名称后的当前命名空间构造解析环境随后parseAndTypecheck把渲染出的源码重新解析并类型检查Upgrade.hs。提交类型检查通过后typecheckedUnisonFileToBranchUpdates生成分支更新并提交Upgrade.hs最终以UpgradeSuccess应答Output.hs即 transcript 中的 I upgraded old to new.。源码中的算法注释给出了一个形象的最小示例Upgrade.hslib.old.foo#oldfoo 17 lib.new.foo#newfoo 18 mything#mything #oldfoo 10 -- 依赖旧库 -- upgrade old new 时先渲染为foo 是去掉 old 后的最短无歧义后缀 mything foo 10 -- 再在只含 lib.new.foo 的解析环境中解析得到 mything#mything2 #newfoo 10 -- 引用已切换到新库fix-5323 的场景正是这个示例的依赖者名称也需要后缀化版本c依赖的b.y必须渲染为b.y而非y否则重解析时y在a.y/b.y之间无法定夺。五、后缀化的核心算法suffixify 系列函数后缀化的语义与实现位于 unison-core/src/Unison/Name.hsTries to shortenfqnto the smallest suffix that still unambiguously refers to the same name.即把全限定名缩短到仍然无歧义地指向同一名称的最短后缀。仓库提供了三个变体suffixifyByNameName.hs以名称name为判定单位通过searchDomG统计匹配该后缀的名称数量利用NamePriority加权优先级不同的名称也会计入歧义只有当matchingNameCount 1时才接受该后缀。suffixifyByHashName.hs以引用集合refs为判定单位要求后缀命中的引用集合matchingRefs allRefs与全名指代完全一致适合按哈希确定性消歧的渲染场景。suffixifyByHashNameName.hs在suffixifyByHash基础上继续缩短——若当前后缀可能指向lib之外的本地定义则继续增加片段因为本地定义可能随后在 scratch 文件里被编辑按哈希消歧在此处不生效。一个重要的消歧规则见 Name.hs 与第 623 行的注释间接依赖名称不会造成歧义——只要存在一个非间接依赖名称例如同时存在lib.base.List.map与lib.something.lib.base.Set.map时裸的map会无歧义地指向lib.base.List.map。这一规则正是 upgrade 渲染环境去掉旧库名称后求最短后缀的理论基础。六、修复落点makeOldDepPPE 的 PPE 拼接策略fix-5323 修复的关键在于handleUpgrade1中三种 pretty-print 环境的优先级拼接Upgrade.hsPPED.leftBiased [ makeOldDepPPE upgradeInfos currentDeepDefnsSansOlds, -- ① 旧库依赖专用环境 PPED.makePPED (PPE.namer (Names.fromUnconflictedReferenceIds dependents)) (PPE.suffixifyByName (Names.fromRelations currentDeepDefnsSansOlds)), -- ② 按名后缀化 PPED.makePPED (PPE.hqNamer 10 (Names.fromRelations currentDeepDefnsSansOlds)) (PPE.suffixifyByHash (Names.fromRelations currentDeepDefnsSansOlds)) -- ③ 按哈希后缀化 ]PPED.leftBiased的含义是对于某个引用最左侧提供名称的环境优先命中只有左侧环境不提供名称时才回退到右侧环境。三种环境的职责分别是①makeOldDepPPEUpgrade.hs专门处理旧库中存在的名称。对于同时出现在旧库与新建库的引用inOldAndNewNamespaces直接返回空名称列表——即这类名称不应该在依赖者的渲染文本里出现因为它将被新库名称取代对于只存在于旧库的引用onlyInOldNamespace则用带lib.old.前缀的完整旧名且不做后缀化PPE.dontSuffixify保证解析时仍能指向旧引用。② 按名后缀化对dependents中的引用用suffixifyByName在去掉旧库后的当前深名称里求最短无歧义后缀。这正是c被渲染为c b.y 1而非c y 1的保证——在a.y、b.y同时存在时y的matchingNameCount大于 1不满足suffixifyByName的isOk条件于是继续加长到b.y。③ 按哈希后缀化兜底环境用suffixifyByHash保证即使按名称无法唯一判定也能以引用哈希集合为准确定性地渲染避免歧义。值得一提的是makePrettyUnisonFileUpgrade.hs在渲染出的文件头部会写两行注释-- The definitions below no longer typecheck after upgrading. -- Please fix the errors, then run update.这表明 upgrade 的最终产物是可直接编辑的源码文本——若渲染后无法通过类型检查该文件会被写入 scratch 文件供用户手工修复随后通过update重新并入分支形成upgrade 自动重写 update 手工收尾的完整工作流。七、失败保护与边界处理除了后缀化fix-5323 所覆盖的 upgrade 机制还包含多项工程化保护类型检查失败路径若渲染出的依赖者无法通过parseAndTypecheckhandleUpgrade1会创建名为upgrade-old-to-new如upgrade-unison_base_3_0_0-to-unison_base_4_0_0的临时分支、把待修复源码写入 scratch 文件并应答UpgradeFailureOutput.hs同时允许通过upgrade.commit把临时分支合并回父分支见 InputPatterns.hs 中upgrade.commit的说明。重名去后缀unmanglemaybeUnmangleUpgrade.hs会检测新库名称中形如foo__2的后缀__数字可能是多次安装未发布依赖时由名称冲突机制生成的若去除后缀后的foo未被占用则自动把 libdep 名从foo__2还原为foo其辅助函数unsnocUnderscoreUnderscoreNumber还附带了 doctest 示例Upgrade.hs。临时分支命名findTemporaryBranchNameUpgrade.hs在单库升级时生成upgrade-old-to-new形式的分支名若因符号等导致命名失败则退化为纯字母数字的 scrubbed 版本多库同时升级时则使用通用的upgrade前缀名避免分支名过长。名称一致性守卫升级前会通过Branch.asUnconflicted断言命名空间无冲突定义、通过getBranchDeclNameLookup断言声明结构一致不一致时以IncoherentDeclDuringUpgrade回滚Upgrade.hs保证替换操作建立在干净的基础上。八、小结如何验证与使用这份修复fix-5323.md 用一条极简的依赖链old.x → b.y → c外加干扰项a.y把upgrade 依赖者后缀化这个易被忽略的细节固化成了可执行回归测试先builtins.merge lib.builtin初始化再定义old.x/new.x/a.y/b.y/c经update入库、两次move归位到lib.old.x/lib.new.x最后upgrade old new。整个过程在 transcript 运行器下每次重放都必须产出完全一致的输出。如果你想在实际代码库中复现这一行为只需按第三节的序列在 UCM 中逐步执行并观察升级完成后view c的渲染结果是否保留了b.y后缀。若要对修复本身做更深入的代码级追踪建议按以下路径阅读升级主流程Unison/Codebase/Editor/HandleInput/Upgrade.hs后缀化算法Unison/Name.hs命令定义与帮助文本Unison/CommandLine/InputPatterns.hs输出消息类型UpgradeSuccess/UpgradeFailure/CantDoThatDuringUnison/Codebase/Editor/Output.hs姊妹命令update的后缀化策略Unison/Codebase/Editor/HandleInput/Update2.hs对于希望运行整套回归测试的开发者仓库提供了 scripts/transcripts.sh 作为 transcript 测试入口fix-5323.md即与idempotent目录下其他用例一起被纳入该套件持续守护upgrade命令的渲染正确性。赞分享编程语言编译器语言运行时开发工具【免费下载链接】unisonA friendly programming language from the future项目地址https://gitcode.com/gh_mirrors/un/unison点击查看免费下载相关推荐Unison 名称渲染修复解析间接依赖不再引发同名后缀歧义fix-5267Unison 名称渲染修复解析间接依赖不再引发同名后缀歧义fix 5267 fix 5267 是 Unison 语言仓库中的一项回归修复当代码库中同时存编程语言编译器语言运行时开发工具如何快速搭建能回答业务问题的 AI 智能体llm-universe 大模型 RAG 应用完整指南如何快速搭建能回答业务问题的 AI 智能体llm universe 大模型 RAG 应用完整指南 想让 AI 基于你自己的文档来回答业务问题不一定需要从原理教程大模型人工智能RAGpnpm peer 后缀精确匹配移除依赖时不再误触发整图重新解析的 lockfile 精度修复pnpm peer 后缀精确匹配移除依赖时不再误触发整图重新解析的 lockfile 精度修复 导读 本文围绕 pnpm 仓库中的变更记录 .changes包管理器开发工具CLI上一篇Firecrawl PHP SDK 实战指南从 Scrape、Crawl 到 Laravel AI 工具的完整接入方案下一篇中国车牌生成器终极指南快速生成合规车牌图片的完整教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/10/9 10:02:58
黄白助手 第 025 个开关:禁用设置页帮助与反馈的位置、验证方法与风险边界
2026/10/9 10:02:58
MES系统解决方案:66页落地文档背后的设备集成与OEE治理实践
2026/10/9 9:57:53
t3code 全栈类型安全实战:数据库到页面的类型无断点流动
2026/10/9 10:53:14
如何用REA search在二进制中快速定位线索:字符串与符号搜索详解
2026/10/9 10:53:14
CLI 与 Mac App 无缝协作:Field Theory CLI 的 Library、便携命令与 ft install app 完整指南
2026/10/9 10:53:14
相变材料PCM选型与工程实践:从潜热参数到电池热管理应用
2026/10/9 10:53:14
视频截屏与抽帧全攻略:工具选择、画质优化与高效工作流
2026/10/9 10:53:14
Agent Skills 实战:用 SKILL.md 把 Claude API 接入 TaoToken 的配置清单
2026/10/9 10:48:12
CNN-Bi-LSTM中文情感分析实战:毕设可跑通、二次开发有抓手
2026/10/9 0:01:35
RISC-V裸机启动全流程:从复位向量到main函数的七步实现
2026/10/9 0:01:35
Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南
2026/10/9 0:01:35
EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错
2026/10/8 5:02:14
Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化
2026/10/9 1:10:43
多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系
2026/10/9 3:31:49
hindsight:面向LLM应用的事后可观测性工程实践
2026/10/8 4:30:43
我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
2026/10/9 3:32:01
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026/10/8 4:32:33
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)