← All posts

Cure v0.29.0 :: Make Documentation Great

by Aleksei Matiushkin

release documentation cure-doc stdlib site repl tooling

v0.28.0 was the release where the compiler started talking back -- emitting every parse error in one pass, suggesting "did you mean?" everywhere, offering interactive fixes for type errors, recording FSM transitions for replay, and rendering colour diffs for the formatter. That put the feedback loops in place. What it did not do was fix the fact that the documentation surface had fallen behind the language it documented.

v0.29.0 does exactly that. Nothing else was the primary target.

The theme is Make Documentation Great.

The uncomfortable truth

Before any new tooling: the old cure doc was a line-based HTML renderer that produced one HTML page per module and no way to bring them together. There was no sidebar, no index, no theme toggle, no filter. Fenced code blocks in ## docstrings were emitted as <pre><code>...</code></pre> with Cure source treated as plain text. Blank-line-separated ## blocks in the same docstring tripped the parser with an unexpected doc_comment error. The stdlib modules under lib/std/ had one-line summaries on each function and nothing else.

It was easier to read the .cure sources directly than to run cure doc and browse the output.

v0.29.0 rebuilds that pipeline end to end.

cure doc: the two-pane layout

$ cure doc
Generating documentation for 34 files
Documentation written to _build/cure/doc/ (34 modules, 2 extras)

The output now looks like ExDoc. A persistent left sidebar lists orphan pages (anything you put in [doc].extras) followed by every module, optionally grouped via [doc.groups_for_modules]. A keyboard- focusable (/) filter input narrows the list in place. A theme toggle honours prefers-color-scheme by default but lets the reader override it.

The right pane renders the selected page with anchored entries for every public function, type, and protocol (#fn-<name>, #type-<name>, #proto-<name>), a local table of contents at the top, and "View source" links back to the corresponding .cure file on GitHub when [doc].source_url is configured.

Cure.toml drives the layout

All behaviour is declarative. Add as much or as little as you want:

[project]
name    = "my_lib"
version = "0.1.0"

[doc]
main       = "README"
title      = "My Library"
extras     = ["README.md", "CHANGELOG.md"]
source_url = "https://github.com/you/my_lib"
source_ref = "main"

[doc.groups_for_modules]
"Core"        = ["MyLib.Core"]
"Accessories" = ["MyLib.Json", "MyLib.Http"]
  • main picks the landing page -- either a module name or an extra slug. When unset, the landing page is the module index.
  • extras paths resolve relative to the directory that contains Cure.toml, so the same configuration keeps working from any sub-directory.
  • [doc.groups_for_modules] groups the sidebar; modules absent from every group fall into a trailing "Other" bucket so nothing is silently dropped.

Every key is overridable per-invocation:

cure doc --title "Release Preview" \
         --main MyLib.Core \
         --extras CHANGELOG.md

--extras is repeatable.

Markdown with Makeup highlighting

Module docstrings are parsed as Markdown and piped through a small new module, Cure.Doc.Markdown. It wraps Md.generate/1 (the pure- Elixir, NIF-free Markdown library) with two escript-safe extras:

  • Placeholder interpolation. 0.33.0 and v0.33.0 are substituted before parsing so release-sensitive copy can live inside a docstring without a preprocess step. v0.33.0 is just the bare version prefixed with v.
  • Syntax highlighting. Fenced code blocks carrying cure, elixir, or erlang are run through Makeup, with the same CSS classes the Phoenix site already uses. Unknown languages round-trip as <pre class="cure-doc-code"><code class="language-<lang>">... so downstream CSS can still target them.

The :md choice is deliberate. Earlier revisions routed HTML rendering through MDEx, the Rust NIF backing marcli. Inside an escript archive, the NIF lives under priv/native/*.so inside a single-file archive that the dynamic loader cannot mmap. Running cure doc from the escript therefore failed on every invocation outside mix run. Switching to :md is enough on its own to keep the escript production-ready.

Every stdlib module grows an ## Examples block

The stdlib under lib/std/ shipped one-line summaries and nothing else. A lot of idiom lives between those one-liners.

v0.29.0 changes the convention. Every module-level docstring now ends with an ## Examples section of fenced cure code that actually compiles. Four high-traffic Std.Core functions -- compose, map_ok, and_then, map_option -- carry per-function examples on top of the module-level block.

The examples are not pseudo-code: they round-trip through mix cure.compile_stdlib without modification. The intent is that a reader can copy any example into a REPL session or a scratch file and get the expected value out the other end.

For instance, Std.Core.compose:

## Right-to-left function composition.
## `compose(f, g)(x)` is `f(g(x))`.
##
## ## Examples
##
## ```cure
## let add1 = fn(x: Int) -> Int = x + 1
## let neg  = fn(x: Int) -> Int = 0 - x
## compose(neg, add1)(3)             # => -4
## ```
fn compose(f: B -> C, g: A -> B) -> (A -> C) =
  fn(x) -> f(g(x))

The module-level docstring on Std.Core goes further, sketching both compose and a pipeline through map_ok / and_then / unwrap_ok that would previously have required reading three files to piece together.

/stdlib on the Cure website

The Cure website now ships the same view of the standard library as first-class navigation.

  • CureSite.Stdlib walks cure/lib/std/*.cure at site compile time, tokenises each file through Cure.Compiler.Lexer, parses it through Cure.Compiler.Parser, and builds a doc map via Cure.Doc.Extractor. Modules are grouped via a curated @groups_for_modules list. A fresh mix phx.server therefore has zero I/O cost -- every doc map is a module attribute in the finished beam.
  • CureSiteWeb.StdlibController serves /stdlib (the module index) and /stdlib/:module (one page per module) with a DaisyUI-styled two-pane layout and a GitHub "View source" link.

The old hand-written site/priv/pages/standard-library.md is gone; /standard-library 301-redirects to /stdlib via the new CureSiteWeb.RedirectController so external links keep working.

Because the site shares Cure.Doc.Markdown with cure doc, the two views of the same stdlib module are visually consistent. The Examples blocks added under lib/std/*.cure appear identically on the web and in the locally-generated output.

REPL Markdown renderer upgrade

Cure.REPL.Markdown used to render docstrings one line at a time: headings were highlighted, bullets got a bullet glyph, and every other line was printed verbatim. That meant fenced code blocks broke across theme.reset escapes, numbered lists rendered without continuation indentation, blockquotes stayed as > line in raw text, and inline **bold** pairs traversed the flat renderer twice, leaving dangling asterisks when the second marker was on a different line.

v0.29.0 promotes it to a small block-aware parser. It handles:

  • ATX headings (#, ##, ###).
  • Fenced code blocks (```lang...```) -- the block boundary is now the \``marker, not the end of a line, and the whole block renders with a│ ` left rule.
  • Indented code blocks (four-space or tab indent).
  • Bullet lists (- , * ) and numbered lists (1., 2., ...).
  • Blockquotes (> ).
  • Inline backtick code (`x`), **bold**, *italic*, and [text](url) links (rendered as text (url)).
  • Horizontal rules (---, ***) and blank-line paragraph separation.

The renderer stays NIF-free, so :help and :doc keep working inside the escript archive.

Parser: stop dropping doc-comment blocks

One stubborn class of error in the v0.28 doc surface looked like this:

mod Std.List
  ## Eager, persistent, singly-linked lists.

  ## Every operation recurses over cons cells; there are no runtime
  ## arrays underneath.
  fn length(l: List(T)) -> Int = ...

The blank line between the two ## blocks terminated the first block. The second block stayed dangling as a raw :doc_comment token ahead of the next definition, and parse_expr/2 raised unexpected doc_comment.

v0.29.0 adds collect_all_doc_comments/1,2 to the parser. After collect_doc_comments/1 returns (the existing helper that gathers a contiguous run), the parser peeks past any :newline tokens and, if another :doc_comment is waiting, concatenates the two bodies with a paragraph break. The merged text is then attached to the following definition. Plain # comments that the lexer drops when preserve_comments: false are transparent to the merge.

The payoff is that module-level docstrings can be written as natural prose with blank lines between paragraphs. Every stdlib module under lib/std/ is now written in exactly that shape.

Editor and highlighter alignment

Cure ships three editor integrations, and each of them had a different picture of the grammar:

  • vicure/ (Neovim) was on the v0.17.0 grammar and did not know about the Melquiades Operator, actor, sup, app, FSM hard / soft event suffixes, pin patterns, or multi-line ADT layout.
  • vscode-cure/ (the TextMate grammar) was closer to v0.22 but still missed proof containers and @record.
  • No third-party story existed for the dozens of static sites that would like to render a Cure snippet in a blog post.

v0.29.0 closes all three gaps.

highlightjs-cure/ ships a highlight.js language description (src/languages/cure.js), a demo page, and a minified bundle (dist/cure.min.js). Drop it into any highlight.js-backed site and <pre><code class="language-cure"> blocks light up without pulling in Makeup.

vicure/ is re-aligned with the current grammar across syntax/cure.vim, indent/cure.vim, ftplugin/cure.vim, ftdetect/cure.vim, its test suite, README, and CHANGELOG.

vscode-cure/ gets the same treatment on the TextMate grammar (syntaxes/cure.tmLanguage.json), the language configuration (language-configuration.json), and the extension entry point (extension.ts), so highlighting, auto-indent, and bracket matching keep parity with vicure.

REPL: top-level declarations and session signatures

v0.28.1 and v0.28.2 quietly landed the last two pieces of a long-running REPL story -- top-level declarations that persist between submissions, and signatures that reach the type checker. Neither change was documented as a release surface at the time. v0.29.0 promotes both to first-class:

cure(1)> fn add1(a: Int, b: Int) -> Int = a + b
defined add1/2
cure(2)> add1(2, 3)
=> 5
cure(3)> :t add1(1, 2)
add1(1, 2) : Int

Under the hood, Cure.REPL.Session accumulates fn, local fn, type (ADT and alias), rec, proto, impl, and proof declarations, synthesises a stable :"Cure.Repl.Session" module on the fly, and auto-prepends use Repl.Session to every expression module the REPL compiles. Redefinitions replace the matching entry in place, keyed by kind + name + arity.

Cure.Types.Checker.infer_expr/2 grows an :extra_bindings keyword option. Cure.REPL.Session.signatures/1 projects public fn entries into the {name, {:fun, param_types, ret_type}} shape the checker expects, and forwards them to :t and :effects so session fns show their real types rather than Any.

A new :defs meta-command lists installed declarations; :reset now also clears the session and unloads the synthesised module.

docs/DOC.md and the tutorial

Documentation about documentation. docs/DOC.md is the new on-disk reference for the whole pipeline: the [doc] / [doc.groups_for_modules] tables, the --title / --main / --extras flags, Cure.Doc.Markdown placeholder interpolation and Makeup highlighting, the output layout and anchor scheme, the /stdlib site integration, and the REPL Markdown renderer.

docs/TUTORIAL.md grows a new Chapter 13 "Documenting your modules" that walks a reader from blank ## blocks to a fully configured Cure.toml [doc] section, with a working ## Examples block along the way.

docs/LANGUAGE_SPEC.md clarifies the doc-comment grammar to spell out the blank-line merging rule, the Markdown body grammar, and the placeholder interpolation.

docs/STDLIB.md grows a "Viewing the stdlib" section at the top cross-linking the web view (/stdlib), the local view (cure doc), and the REPL view (:doc).

What's next

The documentation work is done for v0.29.0. The long-horizon items remain:

  • Monomorphisation. Specialise polymorphic functions whose call sites all use concrete types. Deferred from v0.24.0.
  • Profile-guided optimisation (PGO). Feed Cure.Profiler data into the inliner and pattern-aware SMT encoder so hot paths get specialisation and cold paths stay small.
  • Time-travel for actors. v0.28.0 added @record + cure replay for FSMs. The actor surface still needs journal hooks and a deterministic scheduler for deterministic replay.
  • First-class Cure notebook format (.cnb). Literate programming in Cure with type-checked code cells. The doc pipeline from v0.29.0 is most of what this would need.
  • WASM target. Compile the Cure compiler via AtomVM so the Playground's type checker and sandbox can run entirely in the browser without a WebSocket round-trip.

The repository lives at github.com/am-kantox/cure-lang. Read the new on-disk references: docs/DOC.md, the rewritten docs/STDLIB.md intro, and docs/TUTORIAL.md Chapter 13. Browse the auto-generated standard library at cure-lang.org/stdlib or generate the same site locally with cure doc. The full CHANGELOG has every detail.