Documentation Comments
Osprey has a single documentation system with two idiomatic surface
spellings — one for each language flavor. Both
spellings lower to the same structured DocComment attached to the
canonical AST, so the type checker, the LSP, and the doc exporter never see a
flavor difference ([FLAVOR-BOUNDARY]). A /// comment in the Default flavor and
a (** … *) comment in the ML flavor that carry the same body produce a
byte-identical doc model and byte-identical rendered output.
This chapter is the authoritative contract for that system: the sigils, the body markup, the recognised sections, executable examples, and the shared model.
- Design Rationale
- The Shared Doc Model
- Default-Flavor Sigil
- ML-Flavor Sigil
- Body Markup
- Recognised Sections
- Executable Examples (Doctests)
- Attachment Rules
- Export & Tooling
- Worked Example — One Function, Both Flavors
- Status
- References
Design Rationale
The design is not arbitrary — each decision traces to the literature on documentation systems and program comprehension (full citations in References).
- Documentation is written for humans first, co-located with the code. This is Knuth's founding thesis of literate programming [Knuth 1984]: "let us concentrate rather on explaining to human beings what we want a computer to do." Doc comments live at the definition site and are authored as prose, not as machine instructions.
- Keep the mechanism simple and language-agnostic. Ramsey's noweb [Ramsey 1994] showed that a documentation tool should be minimal and not bound to one language's quirks. Osprey applies this across flavors: one body language, one model, two thin sigils — the flavor is a below-the-model detail.
- Structure must reflect what developers actually need. Empirical studies of API learning [Robillard 2009; Robillard & DeLine 2011] find that the dominant obstacle is missing usage examples and missing intent/rationale, not missing type signatures (the compiler already has those). Osprey therefore gives examples and prose first-class, recognised sections.
- Prevent the known documentation failure modes. Uddin & Robillard's taxonomy of how API documentation fails [Uddin & Robillard 2015] enumerates incompleteness, ambiguity, incorrectness, obsolescence, and fragmentation. Osprey's structured sections attack incompleteness and fragmentation; its executable examples attack obsolescence directly — a rotted example fails the build.
- Examples should be executable, not inert. Documentation that the machine runs and checks measurably aids comprehension [Wrenn & Krishnamurthi 2019] and cannot silently drift from the code — the design principle behind Scribble's binding-aware prose [Flatt, Barzilay & Findler 2009]. Osprey doctests are extracted into the existing golden-output example harness.
- Comments carry real comprehension weight. That documentation causally improves program comprehension is established experimentally [Woodfield, Dunsmore & Shen 1981; Tenny 1988], and quality is measurable [Steidl, Hummel & Jürgens 2013] — motivation for defining a precise model rather than leaving "good docs" to taste.
The Shared Doc Model
[DOC-MODEL] Both flavors lower into one structure on the canonical AST. It
replaces the earlier free-text doc: Option<String> field.
/// A structured documentation comment. BOTH flavors' surface syntaxes
/// (`///` / `//!` in Default, `(** … *)` in ML) lower into this one model;
/// the body markup is identical across flavors — only the sigil differs.
pub struct DocComment {
/// First sentence / first paragraph — the one-line summary.
pub summary: String,
/// Full Markdown body EXCLUDING the recognised structured sections.
/// `[Symbol]` intra-doc links are left unresolved here (a later pass
/// resolves them against the symbol table).
pub body: String,
/// `# Parameters` bullets (name → Markdown text), in author order.
pub params: Vec<(String, String)>,
/// `# Returns`.
pub returns: Option<String>,
/// `# Raises` / `# Errors` — effect or error name → text. Aligns with
/// Osprey's `!Effect` rows and `Result<T, E>` convention.
pub raises: Vec<(String, String)>,
/// Extracted ```osprey``` examples with optional expected output.
pub examples: Vec<DocExample>,
/// `# See also` — `[Symbol]` references and external links.
pub see_also: Vec<String>,
/// `# Since` — version introduced.
pub since: Option<String>,
/// `# Deprecated` — present ⇒ deprecated; text is the reason.
pub deprecated: Option<String>,
/// Module/inner (`//!`) authorship, optional.
pub author: Option<String>,
/// Whether this was an outer or an inner/module doc.
pub scope: DocScope,
}
pub struct DocExample {
/// The Osprey snippet, compiled under the enclosing file's flavor.
pub code: String,
/// Expected stdout for the golden harness, when an `output` block follows.
pub expected_output: Option<String>,
/// Compile-only (no run) for type-level examples.
pub run: bool,
}
pub enum DocScope { Outer, Inner }
params, raises, and see_also are ordered vectors, not maps: rendering
order is the author's order. body keeps [Symbol] links unresolved so the
doc-body parser stays flavor- and symbol-table-agnostic.
Default-Flavor Sigil
[DOC-SIGIL-DEFAULT] The Default flavor uses /// for an outer doc
comment (documents the declaration that follows) and //! for an inner
doc comment (documents the enclosing module or file). This is the
modern-family consensus — Rust, Swift, C#, and F# all spell outer docs /// —
so it reads native to the languages the Default flavor emulates.
/// Doubles its argument.
fn double(x) = x * 2
//! Arithmetic helpers used across the billing pipeline.
module Billing { … }
/// out-prioritises the ordinary // line comment by maximal munch, so an
ordinary comment is never mistaken for a doc comment. A blank /// line is a
paragraph break in the body.
ML-Flavor Sigil
[DOC-SIGIL-ML] The ML flavor uses (** … *) — the OCaml/odoc convention,
where the doc sigil is a specialisation of the ordinary (* … *) block
comment (double star). It is instantly legible to any OCaml, F#, or SML reader,
and — being a block form — is layout-friendly: its content is one contiguous
region the offside-rule lexer skips wholesale, without forcing a per-line
prefix that would fight indentation.
(** Doubles its argument. *)
double x = x * 2
Disambiguation from ordinary comments and ASCII-art banners:
(**opens a doc comment only when it is followed by content that is not immediately another*or the close. This matches odoc.(*not followed by*is an ordinary comment.- The empty
(**)and all-star banners such as(*****)are ordinary comments, never docs. (* … *)block comments nest; a doc comment nests the same way, so a commented-out region inside a doc closes correctly.
The ML flavor's ordinary comment forms remain // (line) and (* … *) (block).
Body Markup
[DOC-BODY-MARKDOWN] The doc body is CommonMark Markdown, identical in both
flavors. Only the surrounding sigil differs; everything inside is the same
markup, parsed by one flavor-neutral parser and rendered by one backend.
Two additions on top of plain CommonMark, both shared by both flavors:
- Intra-doc links
[DOC-LINK]—[Symbol]with no URL resolves against Osprey's symbol table (the rustdoc convention). Bare ([helper]) and dotted ([Console.emit], resolving to the owner) forms are supported; a[text](url)Markdown link is left alone. In the editor, hovering a[Symbol]link shows the referenced element's own hover. Inline code that is not a link uses backticks. - Example fences — a
```ospreyfenced block is an executable example (see Doctests); an optional following```outputfence is its expected stdout.
Markdown is chosen deliberately over the bespoke markups of ocamldoc ({b },
{[ ]}) and Haddock (/em/, @code@): Markdown is the lingua franca of
every modern best-in-class system (rustdoc, KDoc, DocC, TSDoc), and the bespoke
ML markups are the least-loved parts of those systems. ML programmers gain
Markdown; they lose nothing they valued.
Recognised Sections
[DOC-SECTIONS] Structure is convention-based, not tag-based: a small set
of recognised Markdown # headings lower into the model's fields; everything
else is free body. This keeps the body a clean Markdown document with no tag
sigil, and degrades gracefully — an unrecognised heading is just prose.
| Heading (case-insensitive) | Lowers to | Form |
|---|---|---|
# Parameters |
params |
- name: description bullets |
# Returns |
returns |
prose |
# Raises / # Errors |
raises |
- Name: description bullets |
# Examples |
examples |
```osprey / ```output fences |
# See also |
see_also |
[Symbol] links / URLs |
# Since |
since |
a version string |
# Deprecated |
deprecated |
reason prose |
As a concession to ML muscle memory, the ocamldoc/Javadoc block tags
@param name, @return, @raise Name, @see, @since, and @deprecated are
accepted as aliases that lower into the same fields. The heading convention
is the canonical, documented form; the tags exist so an OCaml/F# author is never
surprised.
Executable Examples (Doctests)
[DOC-DOCTEST-HARNESS] A ```osprey fenced block in a doc comment is an
executable example. An optional ```output fence immediately after it is
the example's expected stdout.
Doctests are extracted into the existing differential golden harness
(crates/diff_examples.sh) — there is no second
test runner. The extractor emits each example as a synthetic .osp/.ospml
program (compiled under the enclosing file's flavor) plus a .expectedoutput
from the output fence, and the harness compiles, runs, and byte-compares as it
does for every examples/tested/ program. A doc example whose output drifts
fails CI, which is exactly the obsolescence defense the design calls for
[Uddin & Robillard 2015; Wrenn & Krishnamurthi 2019].
An example with no output fence is compiled but not run (a type-level or
illustrative snippet); DocExample.run records which.
Attachment Rules
[DOC-ATTACH] A doc comment documents the declaration immediately following
it. Attachment is before-only in both flavors — Osprey does not adopt
Haddock's attach-after -- ^ form, which doubles the parsing surface and
creates "which declaration owns this?" ambiguity. Every documentable
declaration is covered:
- functions,
let/mutbindings,typedeclarations,effectdeclarations,externdeclarations, and modules; - and, granularly, type variants, record fields, effect operations, and
function parameters (the last also expressible via
# Parameters).
A doc comment separated from its declaration by a blank line still attaches; a doc comment with no following declaration is a warning (a dangling doc).
Export & Tooling
[DOC-EXPORT] The doc system feeds three consumers from the one model:
- LSP hover renders the
DocCommentas Markdown beneath the signature (0020-LanguageServerAndEditors.md[LSP-HOVER-DOCS]). osprey --docs --docs-dir <dir>exports Markdown-with-front-matter pages. This already exports built-in functions from the same render model (BuiltinDocView); the user-declaration path emits the same page shape fromDocComment, so built-in and user docs are visually uniform.- The website
generate-docspipeline consumes that Markdown unchanged.
Built-in documentation and user documentation therefore share one render model
and one exporter — the BuiltinDocView shape and DocComment converge on the
same rendered page.
Worked Example — One Function, Both Flavors
The same function, documented in each flavor, lowering to the identical
DocComment.
Default flavor (.osp):
/// Divides `numerator` by `denominator`, rounding toward zero.
///
/// Use [safeDivide] when a zero denominator should be a recoverable
/// `Result` rather than a raised effect.
///
/// # Parameters
/// - numerator: The value being divided.
/// - denominator: The divisor. Must be non-zero.
///
/// # Returns
/// The integer quotient.
///
/// # Raises
/// - DivByZero: When `denominator` is `0`.
///
/// # Examples
/// ```osprey
/// print(divide(7, 2))
/// ```
/// ```output
/// 3
/// ```
///
/// # Since
/// 0.4.0
fn divide(numerator, denominator) = intDiv(numerator, denominator)
ML flavor (.ospml):
(** Divides [numerator] by [denominator], rounding toward zero.
Use [safeDivide] when a zero denominator should be a recoverable
[Result] rather than a raised effect.
# Parameters
- numerator: The value being divided.
- denominator: The divisor. Must be non-zero.
# Returns
The integer quotient.
# Raises
- DivByZero: When [denominator] is [0].
# Examples
```osprey
print (divide 7 2)
```
```output
3
```
# Since
0.4.0 *)
divide numerator denominator = intDiv (numerator, denominator)
Both lower to:
DocComment {
summary: "Divides `numerator` by `denominator`, rounding toward zero.",
body: "Use [safeDivide] when a zero denominator should be a recoverable …",
params: [("numerator", "The value being divided."),
("denominator", "The divisor. Must be non-zero.")],
returns: Some("The integer quotient."),
raises: [("DivByZero", "When `denominator` is `0`.")],
examples: [DocExample { code: "print(divide(7, 2))",
expected_output: Some("3\n"), run: true }],
since: Some("0.4.0"),
..
}
The two surfaces differ only in the sigil (/// per line vs one (** … *)
block) and in idiomatic inline-code habit — each reads native to its family,
and the lowered model is the same.
Status
Implemented — capture, model, hover, and links are live; doctest-harness
wiring and user-declaration --docs export remain.
Shipped and tested:
- The structured
DocComment/DocExample/DocScopemodel (crates/osprey-ast/src/doc.rs), on thedocfield of every declaration form (fn,let/mut,type,effect,extern,module). - The shared flavor-neutral body parser
(crates/osprey-syntax/src/docparse.rs):
summary/body split, recognised
#sections,@tagaliases,[Symbol]link extraction, andosprey/outputdoctest extraction. One parser, both flavors. - Default
///+//!lowering for all six declaration forms (default/lower.rs). - ML
(** … *)lexing (nesting, banner/empty disambiguation), CST/parser threading, and lowering (ml/lexer.rs, ml/lower.rs) — this closes the[FLAVOR-LOWER-CONTRACT]breach: the ML flavor previously dropped every comment and hardcodeddoc: None. - Cross-flavor equivalence is machine-checked: a
///Default function and its(** *)ML twin lower to an identicalDocComment(crates/osprey-cli/tests/cross_flavor_equiv.rs). - LSP hover renders the structured
DocComment(summary, body, sections) for every declaration kind in both flavors, and a[Symbol]intra-doc link under the cursor hovers to the referenced element — bare ([helper]) and dotted ([Console.emit]) (crates/osprey-lsp/src/features.rs).
Remaining (tracked in plan 0018 Phase 3):
- Doctest execution: the extractor populates
DocExamples; wiring them into the golden harness so their output is byte-checked is not yet done. osprey --docsexporting user declarations (it exports builtins today).//!module-scope grammar in the Default tree-sitter grammar (the lexer and lowerer already recognise//!; adding the grammar attachment point is the remainder).
References
Full academic grounding for the design decisions above. Citations verified against primary bibliographic records; where a detail is unverifiable it is flagged rather than invented.
Literate programming — the lineage.
- Knuth, D. E. (1984). "Literate Programming." The Computer Journal, 27(2), 97–111. DOI: 10.1093/comjnl/27.2.97. The founding thesis: programs are literature addressed to humans; code and explanatory prose are interwoven at the source.
- Knuth, D. E. (1983). The WEB System of Structured Documentation (Stanford CS Report STAN-CS-83-980). The tangle/weave architecture and automatic cross-referencing — the concrete system behind the 1984 paper.
- Ramsey, N. (1994). "Literate Programming Simplified." IEEE Software, 11(5), 97–105. DOI: 10.1109/52.311070. Argues for a minimal, language-independent documentation mechanism (the noweb tool) — the principle behind one shared model across flavors.
API documentation — design and empirical evidence.
- Kramer, D. (1999). "API documentation from source code comments: a case study of Javadoc." Proc. SIGDOC '99, 147–153. ACM. DOI: 10.1145/318372.318577. The design rationale for authoring API docs as structured, co-located source comments.
- Robillard, M. P. (2009). "What Makes APIs Hard to Learn? Answers from Developers." IEEE Software, 26(6), 27–34. DOI: 10.1109/MS.2009.193. The dominant obstacle to learning an API is missing examples and missing intent — the evidence for first-class example and rationale sections.
- Robillard, M. P., & DeLine, R. (2011). "A field study of API learning obstacles." Empirical Software Engineering, 16(6), 703–732. DOI: 10.1007/s10664-010-9150-8. The journal-length evidence base for documenting intent, examples, and task→API mappings.
- Uddin, G., & Robillard, M. P. (2015). "How API Documentation Fails." IEEE Software, 32(4), 68–75. DOI: 10.1109/MS.2014.80. A taxonomy of documentation failure modes — incompleteness, ambiguity, incorrectness, obsolescence, fragmentation — that the structured sections and doctests are designed to prevent.
Executable / semantically-integrated documentation.
- Wrenn, J., & Krishnamurthi, S. (2019). "Executable Examples for Programming Problem Comprehension." Proc. ICER '19, 131–139. ACM. DOI: 10.1145/3291279.3339416. Empirical support that machine-checked examples improve comprehension — the case for doctests.
- Flatt, M., Barzilay, E., & Findler, R. B. (2009). "Scribble: closing the book
on ad hoc documentation tools." Proc. ICFP '09, 109–120. ACM. DOI:
10.1145/1596550.1596569. The
definitive argument for binding-aware documentation where cross-references are
resolved and examples run — the basis for
[Symbol]intra-doc links and executable examples.
Comments and program comprehension.
- Woodfield, S. N., Dunsmore, H. E., & Shen, V. Y. (1981). "The effect of modularization and comments on program comprehension." Proc. ICSE '81, 215–223. IEEE Press. (Pre-DOI proceedings; cite by pages.) The classic controlled experiment showing comments causally improve comprehension.
- Tenny, T. (1988). "Program readability: procedures versus comments." IEEE Transactions on Software Engineering, 14(9), 1271–1279. DOI: 10.1109/32.6171. Comments carry real comprehension weight, complementary to structure.
- Steidl, D., Hummel, B., & Jürgens, E. (2013). "Quality analysis of source code comments." Proc. ICPC '13, 83–92. IEEE. DOI: 10.1109/ICPC.2013.6613836. A machine-assessable model of comment quality — motivation for a precise model over undefined "good docs."
A note on Markdown. Osprey's doc body is CommonMark. Markdown has no seminal peer-reviewed source; its lineage is Gruber's 2004 informal specification and the later CommonMark specification, cited here as informal specifications, explicitly distinct from the peer-reviewed sources above. The executable and binding-aware aspects of the design rest on the scholarly sources (Wrenn & Krishnamurthi; Flatt, Barzilay & Findler), not on Markdown itself.