news 2026/10/23 22:25:22

Unison 可计算文档控制台渲染实战:docFormatConsole 与 GHC “strange closure error“ 的转录复现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Unison 可计算文档控制台渲染实战:docFormatConsole 与 GHC “strange closure error“ 的转录复现
  • 编程语言
  • 编译器
  • 语言运行时
  • 开发工具

【免费下载链接】unison

A friendly programming language from the future

项目地址:https://gitcode.com/gh_mirrors/un/unison
点击查看免费下载

本文基于 Unison 开源仓库中的 UCM 交互转录测试文件 bug-strange-closure.md,深入剖析 Unison 可计算文档(computable documentation)在控制台渲染(docFormatConsole+Pretty.get)下的完整工作方式,以及一个"反编译问题"与 GHC 非确定性崩溃 "strange closure error" 的复现路径。读完本文,你将掌握 Doc2 文档模型的全部核心元素、display与手动渲染的差异,以及如何在转录测试中定位此类运行时崩溃。

转录文件背景:这是一份"幂等"(idempotent)UCM 转录测试

Unison 仓库用 UCM(Unison Codebase Manager)转录文件(transcripts)作为可执行的交互式测试用例:文件中的ucm代码块会被 UCM 逐条执行,输出被记录为.output.md对照文件。位于unison-src/transcripts/idempotent/目录下的转录是"幂等"的,即反复运行应得到完全一致的输出,用于守护代码库操作(add、undo等)与渲染行为的稳定性。

bug-strange-closure.md 就是其中之一,它专门复现并记录了两类问题:

  1. 对一份名为doc.guide的文档执行display时存在反编译(decompilation)问题——文档本身无法通过默认路径被正确展示;
  2. 通过rendered = Pretty.get (docFormatConsole doc.guide)手工渲染后可以正常展示,但直接求值rendered会以非确定性(non-deterministic)的方式触发 GHC 的 "strange closure error" 崩溃。

转录开头通过两行 UCM 命令建立运行环境:

> builtins.mergeio lib.builtins > load unison-src/transcripts-using-base/doc.md.files/syntax.u

builtins.mergeio把内置的 IO 相关定义合并进lib.builtins命名空间,为后续渲染所需的Pretty、ConsoleText等库提供依赖;load则把含doc.guide等文档定义的文件加载进 scratch 区。

doc.guide:Unison 可计算文档的能力全景

doc.guide是一份刻意编写的"文档的文档",几乎覆盖了 Doc2 文档模型的每一种元素。它在转录中的首次display输出就是全文的渲染结果,也是理解整个问题的最小上下文:

> display doc.guide # Unison computable documentation # Basic formatting Paragraphs are separated by one or more blanklines. Sections have a title and 0 or more paragraphs or other section elements. Text can be bold, *italicized*, ~~strikethrough~~, or `monospaced` (or `monospaced`). You can link to Unison terms, types, and external URLs: * An external url * Some is a term link; Optional is a type link * A named type link and a named term link. Term links are handy for linking to other documents! You can use `{{ .. }}` to escape out to regular Unison syntax, for instance __not bold__. This is useful for creating documents programmatically or just including other documents. *Next up:* lists # Lists # Bulleted lists Bulleted lists can use `+`, `-`, or `*` for the bullets (though the choice will be normalized away by the pretty-printer). They can be nested, to any depth: * A * B * C * C1 * C2 # Numbered lists 1. A 2. B 3. C The first number of the list determines the starting number in the rendered output. The other numbers are ignored: 10. A 11. B 12. C Numbered lists can be nested as well, and combined with bulleted lists: 1. Wake up. * What am I doing here? * In this nested list. 2. Take shower. 3. Get dressed. # Evaluation Expressions can be evaluated inline, for instance `2`. Blocks of code can be evaluated as well, for instance: id x = x id (sqr 10) ⧨ 100 also: match 1 with 1 -> "hi" _ -> "goodbye" ⧨ "hi" To include a typechecked snippet of code without evaluating it, you can do: use Nat * cube : Nat -> Nat cube x = x * x * x # Including Unison source code Unison definitions can be included in docs. For instance: structural type Optional a = None | Some a sqr : Nat -> Nat sqr x = use Nat * x * x Some rendering targets also support folded source: structural type Optional a = None | Some a sqr : Nat -> Nat sqr x = use Nat * x * x You can also include just a signature, inline, with `sqr : Nat -> Nat`, or you can include one or more signatures as a block: sqr : Nat -> Nat Nat.+ : Nat -> Nat -> Nat Or alternately: List.map : (a ->{e} b) -> [a] ->{e} [b] # Inline snippets You can include typechecked code snippets inline, for instance: * `f x Nat.+ sqr 1` - the `2` says to ignore the first two arguments when rendering. In richer renderers, the `sqr` link will be clickable. * If your snippet expression is just a single function application, you can put it in double backticks, like so: `sqr x`. This is equivalent to `sqr x`. # Non-Unison code blocks Use three or more single quotes to start a block with no syntax highlighting: ``` raw _____ _ | | |___|_|___ ___ ___ | | | | |_ -| . | | |_____|_|_|_|___|___|_|_| ``` You can use three or more backticks plus a language name for blocks with syntax highlighting: ``` Haskell -- A fenced code block which isn't parsed by Unison reverse = foldl (flip (:)) [] ``` ``` Scala // A fenced code block which isn't parsed by Unison def reverseA = xs.foldLeft(Nil : List[A])((acc,a) => a +: acc) ``` There are also asides, callouts, tables, tooltips, and more. These don't currently have special syntax; just use the `{{ }}` syntax to call these functions directly. docAside : Doc2 -> Doc2 docCallout : Optional Doc2 -> Doc2 -> Doc2 docBlockquote : Doc2 -> Doc2 docTooltip : Doc2 -> Doc2 -> Doc2 docTable : [[Doc2]] -> Doc2 This is an aside. ( Some extra detail that doesn't belong in main text. ) | This is an important callout, with no icon. | 🌻 | | This is an important callout, with an icon. The text | wraps onto multiple lines. > "And what is the use of a book," thought Alice, "without > pictures or conversation?" > > *Lewis Carroll, Alice's Adventures in Wonderland* Hover over me a b A longer paragraph that will split onto multiple lines, such that this row occupies multiple lines in the rendered table. Some text More text Zounds!

这段输出集中体现了 Doc2 文档模型的若干关键语义:

  • 段落与节:段落由空行分隔,节由标题加若干段落组成;
  • 行内样式:**bold**、*italicized*、~~strikethrough~~、`monospaced`均受支持;
  • 链接:既可以链接外部 URL,也可以链接 Unison 术语(term)与类型(type),命名链接(named link)在更丰富的渲染器中可点击跳转;
  • {{ .. }}转义:用{{ .. }}逃逸回普通 Unison 语法,可用于程序化生成文档或内嵌其他文档;
  • 列表:项目符号列表的+/-/*会被 pretty-printer 归一化;编号列表的起始号由第一个数字决定,其余数字被忽略;列表可任意嵌套、可与符号列表混用;
  • 求值:支持Eval(整段代码求值并显示⧨结果)与EvalInline(行内求值,如2)两种方式;
  • 源码展示:支持包含类型定义/函数定义(Source)、可折叠源码(FoldedSource)、行内签名(SignatureInline)与签名块(Signature)、行内示例(Example,其数字参数表示渲染时忽略前 N 个参数,如f x Nat.+ sqr 1的2);
  • 非 Unison 代码块:三个及以上单引号开头的块不做语法高亮(raw),三个及以上反引号加语言名则带高亮(如 Haskell、Scala),且内容不会被 Unison 解析;
  • 高级元素:docAside、docCallout、docBlockquote、docTooltip、docTable等目前没有专用语法,统一通过{{ }}调用对应函数实现。

反编译问题:为什么display doc.guide不可靠

转录在第一次display doc.guide之后紧跟一行重要注解:

We can display the guide before and after adding it to the codebase

...(随后> add,将doc.guide加入代码库)

But we can't display this due to a decompilation problem.

也就是说,尽管转录中两次display doc.guide(加入代码库前后)都打印出了上面那份完整渲染文本,但这份转录本身就是为了记录一个已知缺陷而写的:display的默认路径对这份文档存在反编译(decompilation)问题——文档的某些特殊形式(SpecialForm)无法通过常规的反编译渲染管线还原成可读文本,因此不能依赖display来可靠展示复杂文档。

这解释了为什么转录随后绕开了display,改用在 Unison 层手工调用渲染函数的方式:

rendered = Pretty.get (docFormatConsole doc.guide)

UCM 对这段 scratch 代码的类型检查结果是:

Loading changes detected in scratch.u. + rendered : Annotated () (Either SpecialForm ConsoleText) Run `update` to apply these changes to your codebase.

rendered的类型是Annotated () (Either SpecialForm ConsoleText)——这正是Pretty文档的底层表示:一棵带()注解(用于布局、缩进等)的节点树,每个叶子要么是Left SpecialForm(不可打印的特殊形式),要么是Right ConsoleText(可直接输出的终端文本)。docFormatConsole的作用就是把Doc2翻译成这棵树。

源码级透视:docFormatConsole 与 ConsoleText

docFormatConsole不是魔法,它在当前仓库中有完整的 Unison 源码实现,位于 IOSource.hs。该文件第 391 行把它注册为运行时可直接调用的内置引用:

doc2FormatConsoleRef = termNamed "syntax.docFormatConsole"

其签名(IOSource.hs 第 923 行)与核心结构如下:

syntax.docFormatConsole : Doc2 -> Pretty (Either SpecialForm ConsoleText)

实现中定义了一个内部go函数,对Doc2的每个构造子逐一映射为Pretty文档(IOSource.hs 第 930-979 行):

  • Word t→ 直接输出文本t;
  • Code d→ 包裹在`中;
  • CodeBlock typ d→ 输出```+ 类型名 + 换行 + 内容 +```;
  • Italic、Strikethrough→ 分别以*、~~包裹(对Paragraph结构做了首尾分组处理);
  • Doc2.Bold d→ 用ConsoleText.Bold映射整段内容;
  • Style、Anchor→ 透传;
  • Blockquote→ 每行前缀>(Pretty.indent);
  • Blankline→\n\n,Linebreak→\n;
  • Tooltip→ 只渲染内部内容(当前控制台渲染器丢弃提示文本);
  • Aside→ 以()包裹并用BrightBlack前景色弱化;
  • Callout None d→ 前缀|;Callout (Some icon) d→ 用ConsoleText.Bold渲染图标行、空行再渲染正文;
  • Table rows→ 交给Pretty.table;
  • Folded→ 拼接摘要与详情;
  • Paragraph ds→Pretty.wrap自动折行;
  • BulletedList ds→ 每项*+ 缩进,Pretty.sepBy换行分隔;
  • NumberedList n ds→ 用Nat.toText、Text.alignRightWith计算并右对齐编号宽度,实现1.、10.的等宽对齐。

与docFormatConsole配套的ConsoleText与ANSI.Color类型也在同文件定义(IOSource.hs 第 883-894 行):ConsoleText由Plain、Foreground、Background、Bold、Underline、Invert构造,ANSI.Color提供 16 种终端颜色。这解释了上面display rendered输出中Right (ConsoleText.Bold (Plain "Unison"))这类叶子的来源——rendered求值结果正是docFormatConsole生成的整棵Annotated树,终端渲染层再把它解释成带样式的彩色文本。

手工渲染可以工作:display rendered

转录随后执行> display rendered,输出与display doc.guide完全一致(全文与上面那份渲染文本相同),证明经docFormatConsole+Pretty.get手工渲染的文档内容是正确的,绕开了反编译问题。之后再次> add把rendered加入代码库,再次> display rendered输出不变;再执行> undo撤销这次添加:

> undo Here are the changes I undid Added definitions: 1. rendered : Annotated () (Either SpecialForm ConsoleText)

GHC "strange closure error":非确定性崩溃的复现

问题出在最后一步。转录用> rendered直接求值这个巨大的Annotated结构,而不是用display渲染它:

rendered = Pretty.get (docFormatConsole doc.guide) > rendered

这一次,UCM 返回的是一棵未经渲染的原始数据结构,而不是排版后的文本。转录中记录了这条命令的开头部分:

Loading changes detected in scratch.u. + rendered : Annotated () (Either SpecialForm ConsoleText) Run `update` to apply these changes to your codebase. 3 | > rendered ⧩ Annotated.Group () (Annotated.Append () [ Indent () (Lit () (Right (Plain "# "))) (Lit () (Right (Plain " "))) (Annotated.Group () (Wrap () (Annotated.Append () [ Lit () (Right (ConsoleText.Bold (Plain "Unison"))) , ...

可以看到doc.guide的每个字词都被拆成了独立的Lit () (Right (Plain "…"))叶子,并以Annotated.Group/Annotated.Append/Indent/Wrap层层嵌套——这份文档的渲染树规模相当庞大(转录原文仅这一个求值输出就占据了数千行)。

转录对此问题的定性非常明确:

And then this sometimes generates a GHC crash "strange closure error" but doesn't seem deterministic.

也就是说,直接求值这个巨大的闭包结构时,GHC 有时会抛出 "strange closure error" 并崩溃,而且该崩溃不是确定性的——同样的输入,不同运行可能崩溃、也可能不崩溃。从仓库现状看,该问题没有任何修复记录或对应源码补丁,本转录文件就是当前仓库中唯一记录该崩溃的测试用例。从其"幂等"目录属性可以推断,该转录在 CI 中需要输出稳定一致,而这类非确定性崩溃正是幂等转录测试要盯防的对象:凡是结果无法稳定复现的求值路径,都会被转录系统标记出来,防止回归时被静默吞掉。

结合 IOSource.hs 的实现可以理解崩溃为何"难以避免":docFormatConsole会对整棵Doc2递归生成Pretty文档,而Pretty.get是纯求值(无任何惰性截断),一旦把生成的巨型Annotated树整体求值并打印,会在运行时栈/堆上构造大量相互嵌套的闭包(Lit、Wrap、Indent、Append、Group层层包裹),此时遇到 GHC 在闭包标记或 GC 路径上的已知缺陷,就会以 "strange closure error" 形式崩溃。这也是为何转录演示的正确姿势是用display rendered走终端渲染管线(渲染层按需消费Pretty文档),而不是用> rendered把整棵树裸打印出来。

如何查看与复现

  • 完整转录与全部交互输出:见 bug-strange-closure.md;
  • 渲染函数源码与ConsoleText/ANSI.Color类型定义:见 unison-runtime/src/Unison/Runtime/IOSource.hs(docFormatConsole自第 923 行起);
  • 同主题的其他文档转录,可横向对照 Doc2 的不同渲染目标:如 doc2.md、doc-formatting.md、doc2markdown.md(Markdown 渲染)与 api-doc-rendering.md;
  • Doc2 的语法级定义与解析测试,可参考 unison-syntax/test/Unison/Test/Doc.hs。

本地复现路径:克隆仓库后进入unison-cli组件运行转录测试(scripts/test.sh或cabal驱动的转录套件会执行unison-src/transcripts/idempotent/下的用例),对照.output.md检查输出。值得注意的是,由于该崩溃"非确定性",反复运行bug-strange-closure.md用例即可观察到偶发的 GHC "strange closure error",这正是该转录存在的意义:把一个难以稳定复现的运行时缺陷固化成可重复触发的测试,供后续修复时验证。

小结

bug-strange-closure.md用一份近乎"文档功能全集"的doc.guide同时验证了三件事:Unison 可计算文档(Doc2)在控制台端的完整表达能力;docFormatConsole把Doc2翻译成Pretty (Either SpecialForm ConsoleText)的底层机制;以及display反编译缺陷与 GHC 非确定性 "strange closure error" 这两个已知问题的复现方式。对 Unison 运行时、文档系统或转录测试框架感兴趣的同学,这是一份少有的、同时覆盖"功能正确性"与"崩溃复现"两面的第一手素材。

  • 编程语言
  • 编译器
  • 语言运行时
  • 开发工具

【免费下载链接】unison

A friendly programming language from the future

项目地址:https://gitcode.com/gh_mirrors/un/unison
点击查看免费下载

相关推荐

上一篇:Budibase 内嵌 Apache CouchDB Helm Chart 部署与配置全指南:集群安装、密钥管理、升级迁移与参数详解
下一篇:Pydantic 严格模式(Strict Mode)完全指南:从字段级到全局的严格校验配置与源码剖析

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

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

XMLViewer XML查看器:从选型到避坑,高效处理XML配置文件

简介:XMLViewer 是一款面向开发人员与运维人员的轻量级 XML 查看工具,主要解决日常排查 XML 文件语法是否正确、结构是否清晰的问题。安装完成后,只需右键点击 XML 文件并选择“View”即可快速打开查看,省去手动配置编辑器的麻烦&…

作者头像 李华
网站建设 2026/10/14 3:26:43

Java 异常处理实战案例集:50 个高频异常的现象、根因、修复与预防

Java 异常处理实战案例集:50 个高频异常的现象、根因、修复与预防 异常处理是 Java 面试与答辩的必考题,但多数教程只讲语法不讲「为什么会炸」。这套案例集把 50 个高频异常按 8 大家族归类,每个案例固定四段式:现象&#xff08…

作者头像 李华
网站建设 2026/10/16 1:35:46

【精品大数据】基于大数据的零售交易者行为特征与生存状况数据分析及可视化 (附源码资料)数据分析,可视化大屏_毕设选题推荐_SPark_数据挖掘_Hadoop

💖💖作者:计算机毕业设计江挽 💙💙个人简介:曾长期从事计算机专业培训教学,本人也热爱上课教学,语言擅长Java、微信小程序、Python、Golang、安卓Android等,开发项目包括…

作者头像 李华
网站建设 2026/10/18 9:52:13

2400源表+LabVIEW实现高精度I-V曲线扫描

1. 这不是“点几下就能出图”的事:2400源表LabVIEW扫I-V曲线的真实门槛你搜“2400源表 LabVIEW I-V曲线”,页面跳出一堆“安装失败”“路径报错”“找不到驱动”的帖子,还有人把“条码扫描”“京瓷打印机扫描”“局域网IP扫描软件”混进来——…

作者头像 李华
网站建设 2026/10/23 8:34:31

Claude Code + cc-switch + Skills 搭建出一个完整的 AI Agent 编程环境

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 20:40:08

PyCharm 2025.3.3 安装到配置全攻略:解释器、虚拟环境与汉化一次搞定

装好 PyCharm 却打不开项目、写了一下午代码发现压根没保存、新建文件时解释器直接报红——这些场景在我带过的每个新手身上几乎都出现过。很多时候并不是代码本身的问题,而是从安装环节就开始埋雷了。这篇教程就专门写给第一次接触 PyCharm 的朋友,目标…

作者头像 李华