Specifications / Language

Documentation Comments

Default /// comments and ML (** ... *) comments lower to the same structured documentation model. Equivalent comment bodies therefore have the same representation after flavor lowering ([FLAVOR-BOUNDARY]).

Compiler and language rules are separate from integration tooling. See Documentation tools for the extracted contract.

Shared model [DOC-MODEL]

DocComment stores:

  • a one-line summary from the first paragraph and the remaining Markdown in body;
  • ordered parameter, error, example, and related-link entries;
  • optional return, version, deprecation, and author text; and
  • the comment scope.

DocExample stores the Osprey source, optional expected output, and whether the example is runnable. Both syntax flavors use this model.

Default sigil [DOC-SIGIL-DEFAULT]

In Default files, consecutive /// lines document the declaration that follows. The marker and one optional following space are removed. A blank /// line separates paragraphs.

/// Doubles its argument.
fn double(x) = x * 2

Ordinary // comments do not produce documentation.

ML sigil [DOC-SIGIL-ML]

In ML files, (** ... *) documents the declaration that follows.

(** Doubles its argument. *)
double x = x * 2

ML block comments nest. (** starts documentation only when content follows the opener; (**) and all-star banners such as (*****) remain ordinary comments. An unterminated documentation comment is a syntax error.

Each prose line drops its leading whitespace and one optional * continuation marker. A line inside a fenced code block drops only the margin its opening fence carried, so an example keeps the indentation ML layout depends on.

Inner sigil [DOC-SIGIL-INNER]

//! documents the scope that encloses it rather than the declaration that follows. It is spelled the same in both flavors: only the outer sigil differs. Consecutive //! lines form one comment, and the marker plus one optional following space is removed from each.

A //! block is legal as the first item of a file, of a namespace brace body, or of a module body, and it lowers to that scope's own documentation. A file-scoped namespace name; header opens no body, so the file's //! documents the file.

//! Payment primitives shared by the billing modules.

/// Converts cents to a display string.
fn format(cents) = "${cents}"

An outer and an inner comment describe the same scope from opposite sides, so a scope may carry both at once and neither replaces the other.

/// What callers need to know about the module.
module Ledger {
    //! What a maintainer reading the body needs to know.
    export let entries = []
}

//! written anywhere else documents nothing and is rejected. It is never treated as an ordinary // comment: a discarded documentation comment is indistinguishable from one that was never written. Nor is any of it read as code — the text after the marker is comment text wherever the marker sits. Both flavors reject it with the same error at the position of the //!, naming the flavor's own outer sigil as the alternative: `//!` documents the enclosing file, namespace or module; write it as the first item of one, or use `///` to document the declaration that follows ((** … *) in ML).

Body markup [DOC-BODY-MARKDOWN]

The stripped body is Markdown in both flavors and passes through one parser. The first paragraph becomes a whitespace-normalized summary. Text after the first blank line becomes the body, except for recognized sections and aliases described below.

[Name] and dotted [Owner.member] forms are symbol links. Standard Markdown links such as [text](https://example.com) are not symbol links. In LSP hover, hovering a symbol link shows the referenced declaration; a dotted link resolves to its owner declaration.

Recognized sections [DOC-SECTIONS]

The following case-insensitive level-one headings populate structured fields:

Heading Field Content
# Parameters / # Params params - name: description bullets
# Returns / # Return returns prose
# Raises / # Errors raises - name: description bullets
# Examples / # Example examples Osprey and output fences
# See also / # See see_also comma- or line-separated entries
# Since since prose
# Deprecated deprecated prose

The aliases @param, @return, @returns, @raise, @raises, @throws, @see, @since, @deprecated, and @author populate the same fields. Unrecognized Markdown remains text in its current region.

Declaration attachment [DOC-ATTACH]

An outer documentation comment attaches to the following declaration. Both flavors attach docs to functions, let/mut bindings, types, effects, externs, modules, and signatures, including declarations inside modules. Only these declaration forms receive a documentation field.

An inner comment attaches instead to the scope containing it — the file, a namespace, or a module ([DOC-SIGIL-INNER]) — and is recorded separately from that scope's outer comment.

Docs do not attach separately to variants, fields, or parameters. A parameter is documented in its function's # Parameters section, and hovering the parameter shows that entry (LSP-HOVER-PARAM-DOCS).

Effect operations [DOC-EFFECT-OP]

An effect operation is the one nested form that carries its own documentation. A /// (Default) or (** … *) (ML) block written directly above an operation line attaches to that operation, and both flavors lower it to the same EffectOperation::doc. An operation documents ITSELF: the owning effect's doc describes the capability as a whole and serves only as a fallback for an undocumented operation. Without per-operation docs, sibling operations hovered identically and said nothing about the operation under the cursor ([LSP-HOVER-EFFECT-OPERATIONS]).

The operation's recorded position anchors on its NAME, not on its declaration node, so a leading doc block does not drag the position up onto the comment and defeat position-based hover resolution.