- 编程语言
- 编译器
- 语言运行时
- 开发工具
【免费下载链接】unison
A friendly programming language from the future
本文基于 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 就是其中之一,它专门复现并记录了两类问题:
- 对一份名为
doc.guide的文档执行display时存在反编译(decompilation)问题——文档本身无法通过默认路径被正确展示; - 通过
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.ubuiltins.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
相关推荐
Unison 可计算文档(Computable Documents)完整指南:Doc2 语法与 UCM 实战
Unison 可计算文档(Computable Documents)完整指南:Doc2 语法与 UCM 实战 Unison 的文档不是普通的 Markdown
编程语言编译器语言运行时开发工具Unison 可计算文档(Computable Documents)完全指南:从 `{{ }}` 语法到实时求值的文档系统
Unison 可计算文档(Computable Documents)完全指南:从 {{ }} 语法到实时求值的文档系统 Unison 的文档不是独立于代码的静态
编程语言编译器语言运行时开发工具Unison 文档导出实战:用 `docs.to-html` 将命名空间内全部 Doc2 文档批量渲染为 HTML 站点
Unison 文档导出实战:用 docs.to html 将命名空间内全部 Doc2 文档批量渲染为 HTML 站点 导读 在 Unison 语言中,文档( D
编程语言编译器语言运行时开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考