改进 OCaml 文档工具链

上周,我们 发布 了一个新的 OCaml 文档生成器的 alpha 版本,codoc 0.2.0。在 2014 年 OCaml 研讨会演示文稿中 (摘要幻灯片视频),我们提到了文档的“模块墙”,这次尝试要解决它。要试用它,只需按照该存储库中 README 中的说明进行操作,或者 浏览一些当前工具默认输出的样本。请记住,codoc 及其组成库仍在紧张开发中,尚未完成所有功能。

codoc 的目标是提供一套广泛有用的工具来生成 OCaml 文档。特别是,我们正在努力

  1. 涵盖 OCaml 的所有语言特性
  2. 提供准确的名称解析和链接
  3. 支持不同包之间的交叉链接
  4. 公开我们用于构建 codoc 的组件的接口
  5. 为工具本身提供一个无魔法的命令行界面
  6. 减少外部依赖关系,并与其他工具默认集成

我们还没有在工具堆栈的所有层面上都实现这些目标,但我们正在接近。codoc 0.2.0 现在可以使用了(尽管在某些方面,比如默认 CSS,还有些粗糙)。这篇文章概述了新系统的体系结构,以便更容易理解构建它的设计决策。

文档的五个阶段

从 OCaml 源代码生成文档有五个阶段。这里我们将描述每个阶段在过去(使用 OCamldoc)如何处理,现在(使用我们目前的原型)如何处理,以及将来(使用我们正在开发的工具的最终版本)如何处理。

将注释与定义关联

第一步是将 .ml.mli 文件中的各种文档注释与它们对应的定义相关联。

过去

将注释与定义关联由 OCamldoc 工具处理,该工具分两步完成此操作。首先,它使用常规的 OCaml 解析器或 camlp4 解析文件,就像在正常编译中一样。它使用第一步中的语法树,然后重新解析文件,查找注释。第二次解析由语法树中的位置信息引导;例如,如果有一个定义在第 5 行结束,那么 OCamldoc 将从第 6 行开始查找要附加到该定义的注释。

用于附加注释的规则非常复杂,并且依赖于空格。这使得使用单个解析器解析文件并附加注释变得很困难。特别是,很难以一种不会对 camlp4 扩展造成很多问题的方式做到这一点。这就是 OCamldoc 分两步完成此过程的原因。

这种两步法的缺点是它假设任何预处理器的输入都是可以被编译器/工具创建文档合理读取的东西,但这并不总是正确的。

现在

我们目前的原型在编译器本身内部将注释与定义关联起来。这依赖于对 OCaml 编译器的补丁 (GitHub 上的拉取请求 #51)。注释关联由 -doc 命令行标志激活。它使用(重新编写版本的)与 OCamldoc 当前使用的相同两步算法。然后将注释作为属性附加到语法树中的适当节点。这些属性会通过类型检查器传递,并出现在 .cmt/.cmti 文件中,其他工具可以在这些文件中读取它们。

将来

我们打算放弃 OCamldoc 采用的两步法。为此,我们需要简化将注释与定义关联的规则。一个建议是使用与属性相同的规则,但这似乎过于限制。因此,我们希望采用的方法是尽可能地接近 OCamldoc 当前支持的方法,但禁止一些更模棱两可的情况。例如,

val x : int
(** Is this for x or y? *)
val y : float

可能不会在我们最终版本中得到支持。我们会注意了解这些设计决策的影响,并且希望找到一个可靠的未来解决方案。通过避免两步法,应该可以安全地始终打开注释关联,而不需要 -doc 命令行标志。

解析注释的内容

将文档注释与定义关联后,您必须解析这些注释的内容。

过去

OCamldoc 解析注释的内容。

现在

在我们目前的原型中,注释的内容在编译器中被解析,以便 .cmt/.cmti 文件中可用的文档属性包含文档的结构化表示。

将来

我们打算将解析文档注释的内容与编译器分离。这意味着文档将作为字符串存储在 .cmt/.cmti 文件中,并由外部工具解析。这将允许文档语言(及其解析器)比编译器的发布周期更快地发展。

用类型和文档表示编译单元

存储在 .cmt/.cmti 文件中的类型化语法树不是一个方便的表示形式,用于从中生成文档,因此下一步是将语法树和注释转换为某种合适的中间形式。特别是,这允许统一处理 .cmt 文件和 .cmti 文件。

过去

OCamldoc 从语法树、类型化语法树和它在早期阶段找到和解析的注释中生成中间形式。对非类型化和类型化语法树的需求是历史遗留问题,现在已经不再需要了。

现在

我们目前的原型在 doc-ock 库中创建了一个中间形式。此形式目前可以从 .cmti 文件或 .cmi 文件创建。.cmi 文件不包含完整文档所需的信息,但如果您无法使用 .cmti 文件,可以使用它们生成部分文档。

此中间形式可以使用 doc-ock-xml 序列化为 XML。

将来

在最终版本中,doc-ock 也将支持读取 .cmt 文件。

解析引用

获得文档表示形式后,您需要解析所有路径和引用,以便链接可以指向正确的位置。例如,

(* 此类型由 {!Foo} 使用 *) type t = Bar.t

路径 Bar.t 和引用 Foo 必须解析,以便文档可以包含指向相应定义的链接。

如果您要为大量包生成文档,可能存在多个名为 Foo 的模块。因此,能够确定引用指的是哪一个 Foo 非常重要。

与大多数语言不同,由于 OCaml 的强大模块系统,解析路径可能非常困难。例如,考虑以下代码

module Dep1 : sig
 module type S = sig
   class c : object
     method m : int
   end
 end
 module X : sig
   module Y : S
 end
end

module Dep2 :
 functor (Arg : sig module type S module X : sig module Y : S end end) ->
   sig
     module A : sig
       module Y : Arg.S
     end
     module B = A.Y
   end

type dep1 = Dep2(Dep1).B.c;;

在这里,它看起来像 Dep2(Dep1).B.c 将由函子 Dep2 的子模块 B 中的类型定义 c 定义。但是,Dep2.B 的类型实际上取决于 Dep2Arg 参数的类型,因此实际的定义是 Dep1 模块的模块类型 S 中的类定义。

过去

OCamldoc 使用非常简单的基于字符串的查找进行解析。这并非旨在处理模块名称不唯一的项目集合。它也不足以处理 OCaml 模块系统的更高级用法(例如,它无法解析上面示例中的路径 Dep2(Dep1).B.c)。

现在

在我们目前的原型中,路径和引用解析由 doc-ock 库执行。实现相当于 OCaml 模块系统的一个重新实现,它跟踪生成准确路径和引用所需的附加信息(它也是延迟的,以提高性能)。该系统使用 .cmti/.cmi 文件提供的摘要来解析对其他模块的引用,而不是仅仅依赖于模块的名称。

将来

doc-ock-lib 仍然有一些路径处理不正确,这些路径将得到修复,但总的来说,最终版本将与当前原型相同。

生成输出

最后,您已准备好使用这些工具生成一些输出。

过去

OCamldoc 支持多种输出格式,包括 HTML 和 LaTeX。它还包括对称为“自定义生成器”的插件的支持,允许用户添加对其他格式的支持。

现在

codoc 目前仅支持 HTML 和 XML 输出,尽管一旦接口稳定下来,额外的输出格式(如 JSON)应该很容易添加。codoc 为跟踪包层次结构、文档问题和分层本地化配置定义了一个文档索引 XML 格式。

codoc 还定义了一个可脚本化的命令行界面,允许用户访问其内部文档阶段:提取、链接和渲染。最新关于如何使用 CLI 的说明可以在 README 中找到。我们提供一个 OPAM 远程,其中包含驱动新文档引擎所需的所有新库和编译器补丁的工作版本。

将来

如前所述,codoc 及其组成库 doc-ock-libdoc-ock-xml 仍在紧张开发中,尚未完成所有功能。值得注意的是,还有一些重要的未解决问题

  1. 类和类类型文档没有生成 HTML。 (问题 codoc#9)
  2. CSS 表现不佳。(codoc#27 问题
  3. codoc HTML 不理解 --package。(codoc#42 问题
  4. opam doc 过于侵入式(暂时用于演示目的;由 (codoc#48 问题) 跟踪)
  5. 文档语法错误未在正确的阶段或足够明显的报告。(codoc#58 问题
  6. 字符集处理不正确 (doc-ock-lib#43 问题
  7. -pack 和 cmt 提取不受支持 (doc-ock-lib#35 问题doc-ock-lib#3 问题
  8. 包含/替换不受支持 (doc-ock-lib#2 问题

我们非常乐意在 https://github.com/dsheets/codoc/issues 接收错误报告和补丁。对于更广泛的建议、评论、投诉和讨论,请加入我们 Platform 邮件列表。我们希望您能告诉我们您的想法,并帮助我们构建下一代文档工具,为我们的社区提供良好的服务。