| name | motoko-doc-strings |
| description | Add `///` doc strings to public objects in Motoko modules so that `mo-doc` produces useful HTML/Markdown documentation. Covers placement rules, formatting, code examples, common pitfalls, and a verification workflow. |
Motoko doc strings
Purpose
Add triple-slash (///) doc comments to every public declaration in Motoko
source files so that mo-doc renders meaningful documentation pages. This
skill captures the conventions used in mo:core (caffeinelabs/motoko-core)
and the lessons learned applying them across the motoko-bitcoin library.
When to Use
- The user asks to "add doc strings", "document the public API", or
"generate docs" for a Motoko package.
- Reviewing or polishing a library before release / publication on MOPS.
- After adding new public symbols and noticing missing entries in the
generated
docs/.
What Counts as a "Public Object"
Document every declaration that ends up in the rendered docs. In a typical
module { ... } file these are:
public type ...
public let ...
public func ...
public class ...
- The module itself (
module { ... } or module Name { ... })
- For a
public class, every public let, public var, and
public func member inside the class body.
- For a nested
public module, recurse and apply the same rules.
Private declarations (func, let, class without public) and helper
types should NOT receive /// comments — they don't appear in mo-doc
output and the noise hurts readability.
Comprehensiveness by Location
How thorough a doc string needs to be depends on where the file lives:
- Inside
src/internal/ (some packages use src/private/ for the
same role — apply the same rule) — doc strings can be brief. These
modules are implementation details that end users are not expected to
call directly, so a short one-liner stating what a declaration does is
usually enough. Trap/error notes can be omitted unless the behavior is
surprising to another maintainer.
- Anywhere else under
src/ (i.e. outside src/internal/) — doc
strings MUST be comprehensive. These are the public API surface that
users will call, so they need every detail: argument units and formats,
size/range constraints, return-value semantics, full failure behavior
(Traps / Errors paragraphs), and runnable examples where helpful.
Apply the full "User-Perspective Read-Through" checklist at the end of
this skill to every declaration here.
Doc String Format
mo-doc parses lines that start with /// as Markdown documentation
attached to the next declaration.
/// Brief one-line description ending in a period.
///
/// Optional longer paragraph(s) describing semantics, edge cases,
/// and return values. Refer to parameters by `name` in backticks.
///
/// Example:
/// ```motoko include=import
/// let result = Module.func(arg);
/// ```
public func func(arg : T) : U { ... };
Conventions used in mo:core:
Where to put the module-level doc string
The module-level /// block must sit at the very top of the file,
before the import statements, with one blank line separating it from
the imports and one blank line between the imports and the module { ... }
line. This matches the layout used in dfinity/motoko-core and is the
only placement that mo-doc actually attaches to the module page —
a doc block placed between the imports and module { ... } is silently
ignored and the rendered module page will have no description.
All .mo files MUST have a module-level /// doc string at the very
top of the file (before the import statements). This applies to every
Motoko source file, not just modules with public APIs — having a doc
block at the top ensures mo-doc renders a meaningful module page and
helps maintainers quickly understand the file's purpose.
Special case for Mixins: This rule is especially important for files
using the mixin keyword. Even though mo-doc currently skips files
with the mixin keyword due to syntax differences, the doc string MUST
still be placed at the very top of the file to ensure compatibility with
future tool updates and maintainer readability.
/// One-line module summary.
///
/// Longer description.
///
/// ```motoko name=import
/// import Foo "mo:pkg/Foo";
/// ```
import Bar "mo:core/Bar";
import Baz "mo:core/Baz";
module {
// ...
}
For named modules (module Name { ... }, e.g. src/bitcoin/Script.mo)
the same rule applies: doc block at the top of the file, then a blank
line, then imports, then a blank line, then module Name { ... }.
Where to put per-declaration comments
Place /// lines immediately above the declaration with no blank line
between them. If there is an existing legacy // ... comment, put the
/// block above the legacy comment (the legacy comment can stay as
implementation notes).
/// Public API description goes here.
// Legacy implementation notes can stay below the doc string.
public func encode(input : [Nat8]) : Text { ... };
Common Pitfalls
1. apply_patch and literal \n
When inserting multi-line content, write actual newlines in the patch body
(one + per real line). NEVER use the escape sequence \n inside
inserted text — it will be written as the literal two characters and break
the file.
2. Don't accidentally insert into the middle of a function
apply_patch matches on context. When the surrounding context is too
short or appears multiple times, the inserted block may land inside a
function body. After inserting docs, run a quick syntax check (e.g.
mo-doc or moc --check) to catch this. A telltale sign is a
syntax error [M0001], unexpected token 'import' message — that means a
new module-level block was placed inside a loop/func body.
3. Class member documentation order
mo-doc renders class fields and methods in source order, but the class's
own description block is shown after the member list when the class doc
appears above the constructor. To keep the class summary at the top of
the rendered class page, place the /// block on the line directly
preceding public class Name(...).
4. Re-exported types
Re-exported types like
public type Signature = Types.Signature; still need a one-line ///
description so they are not rendered as "(no description)".
5. Skip private helpers and constants
Don't add /// to let, func, or type declarations that lack
public. They never appear in the output and the comments add visual
noise.
6. module Name { ... } named modules
Files like src/bitcoin/Script.mo use the form module Script { ... }
instead of the bare module { ... }. The same top-of-file placement rule
applies — the module-level /// block goes at the very start of the
file (before the imports), not on the line directly preceding
module Script {.
7. Preserve unfamiliar parameter syntax verbatim
If a function signature uses syntax you don't recognise — e.g.
self : [var T] (Motoko's method-dispatch hook that enables
x.func(...) call sites) or key : (implicit : T -> Nat32) (the
implicit parameter feature) — do NOT rewrite it. These shapes are
load-bearing: they affect how call sites in examples work
(users.bucketSort<User>(...) only compiles because the first
parameter is named self), and a doc pass that "cleans them up" can
silently invalidate every example you just wrote.
8. Module doc must be at the top of the file
mo-doc only treats a /// block as the module description when it
appears at the very beginning of the file, ahead of the import
statements. A doc block placed between the imports and module { ... }
compiles fine but produces an empty module description in the rendered
HTML. If you find an existing project with module docs adjacent to
module { ... }, relocate them to the top of the file (a small Python
script that finds the trailing /// block before module and prepends
it to the file works well for batch migration).
8. Documenting Record Types
For record types with multiple members, documenting each member with a
per-line /// can make the code and the rendered documentation very
verbose. Instead, prefer documenting all members in a single block above
the type declaration. Use backticks for member names.
/// An HTTP request.
/// `method`: HTTP method (e.g., "GET", "POST").
/// `url`: Request URL.
/// `headers`: List of headers.
/// `body`: Request body.
public type Request = {
method : Text;
url : Text;
headers : [(Text, Text)];
body : Blob;
};
Workflow
-
Inventory public declarations:
grep -RInE "public (type|func|class|let)" src
-
For each .mo file:
- Add a module-level
/// block (with a name=import example) at the beginning of the file, even before the block of import statements.
- Add
/// blocks above every public declaration inside the top-level modules.
- Recurse into nested public declarations. For instance public members of public classes need doc strings. Public members of public modules need doc strings. And so on.
-
Re-scan to catch anything missed:
awk '
FNR==1{prev=""}
{
if ($0 ~ /^[ \t]*public[ \t]+(type|func|class|let)([ \t(]|$)/) {
p=prev; gsub(/^[ \t]+|[ \t]+$/, "", p);
if (p !~ /^\/{3}/) printf "%s:%d:%s\n", FILENAME, FNR, $0;
}
if ($0 !~ /^[ \t]*$/) prev=$0;
}
' src/*.mo src/**/*.mo
Empty output = all public declarations are preceded by a /// line.
Portability: use [ \t]+ (and explicit [ \t(] boundaries) instead of
[[:space:]]+ / \b. BSD/macOS awk silently fails to match POSIX classes
here — the script returns empty output and looks like a pass. The cost of
this miss is a full iteration to diagnose.
Known false positives (don't waste edits "fixing" these — mo-doc itself
handles both cases correctly):
- A legacy
// ... comment between the /// block and the public ...
line. The skill's format guidance explicitly allows this layout, but the
audit awk only looks at the immediately previous non-blank line. Verify
by reading the rendered HTML before assuming a real miss.
- A
public func / public let inside a /* ... */ block comment.
The awk has no notion of block comments, so commented-out code shows up
as missing. Cross-check by grep-ing the file for /* near the flagged
line.
-
Generate docs:
mkdir -p docs $(find src -mindepth 1 -type d | sed 's|^src|docs|')
mo-doc --source src --output docs --format html
No output = success. Two failure modes to watch for:
- A
Fatal error: exception Sys_error("docs/.../X.html: No such file or directory") means an output subdirectory is missing.
- Any
Skipping ... line indicates a syntax error that must be
fixed (often a stray apply_patch corruption).
-
Spot-check the rendered output:
docs/index.html — every module should appear in the listing.
- Each
docs/<Module>.html — module description, types, functions,
and class members should all show their text.
Tips for Writing Useful Descriptions
- Describe what the function does and what it returns, not how it
is implemented.
- For low-level helpers (
readBE32, writeLE64, etc.) a one-liner stating
the byte order, width, and offset semantics is sufficient.
- For domain types (
SighashType, WitnessProgram, OutPoint) name the
spec or BIP that defines the format and link to it.
- Keep examples short and self-contained; prefer literal byte arrays over
reading from external sources.
Files with many similar public lets (constants tables)
Each public let needs its own /// line — there is no syntax for a shared
doc block. For files like FIPS round-constant tables (e.g. SHA256's
K00..K63, SHA512's K00..K79), doing N individual Edit calls is
slow and noisy. Read the file once and rewrite it whole with Write,
emitting one /// <name> round constant K<NN>. line per entry. A
module-level /// at the top can carry the shared context (spec section,
purpose) so the per-entry lines stay terse.
Documenting Error and Trap Behavior (REQUIRED)
Every public function doc string MUST describe its full failure behavior.
Readers cannot tell from a type signature alone whether a function traps,
returns null, returns #err, or simply produces a wrong-but-defined
result on bad input — the doc string is the only place this contract is
recorded.
What to look for in the implementation
Scan the function body (and every helper it calls) for:
Runtime.trap(...) / Debug.trap(...) / Prim.trap(...) calls.
assert ...; statements (a failed assert traps).
- Pattern matches that are non-exhaustive in practice (e.g. a
switch on
?T whose null branch traps, or a case (#err _) Runtime.trap ...).
- Implicit traps from the standard library: out-of-bounds array indexing
(
a[i] when i >= a.size()), Nat subtraction underflow, division by
zero, Option.unwrap on null.
- Explicit error returns:
?T returning null, Result<T, E> returning
#err, variant returns like { #ok; #err }.
What to write
For each failure mode, state in the doc:
- The condition — what input or state triggers it, in user-facing
terms ("when
input contains a character outside the Base58
alphabet", not "when mapBase58[c] == 255").
- The outcome —
traps, returns null, returns #err(...), etc.
- For
Result/option returns, list every distinct error case
separately when the variants carry meaning.
Use a dedicated Traps and/or Errors paragraph (or both) at the end of
the doc, after the example and before the runtime/space notes.
CRITICAL: Use the wording "Traps" (capitalised, present tense) for
unrecoverable failures (e.g., assert, trap). This makes the failure
modes highly visible in rendered docs and easily greppable.
/// Decodes a Base58-encoded string into the original byte array.
///
/// ```motoko include=import
/// let bytes = Base58.decode("StV1DL6CwTryKyV");
/// ```
///
/// Traps if `encoded` contains any character that is not in the Base58
/// alphabet (i.e. not in
/// `123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz`).
public func decode(encoded : Text) : [Nat8] { ... };
For graceful errors:
/// Parses a WIF-encoded private key.
///
/// Returns `#err(msg)` when:
/// - the input is not valid Base58Check (bad checksum or alphabet),
/// - the decoded payload has an unexpected length (must be 33 or 34 bytes),
/// - the version byte does not match `network`.
public func decode(wif : Text, network : Network) : Result<PrivateKey, Text> { ... };
Pure / total functions
If a function genuinely cannot fail (e.g. Array.size, a pure arithmetic
helper that uses fixed-width types and total operators), say so explicitly
with a one-liner like "Never traps." or omit the failure section
entirely — but only after auditing the body to confirm.
Worked examples from motoko-bitcoin
Base58.decode — traps on any character outside the alphabet (not a
Result, no graceful fallback).
Base58Check.decode — returns ?[Nat8]; null on bad alphabet, bad
length, or checksum mismatch. Document each.
Bech32.decode — returns Result<_, Text> with distinct messages for
invalid characters, mixed case, bad checksum, length out of range,
invalid HRP. Mention each error category, not just "returns #err".
Bip32.derivePath — traps on hardened derivation from a public key.
Fp.inverse (and the / operator) — traps when the value is zero.
Affine.add / Jacobi.add — document behaviour at the point at
infinity and for equal/opposite inputs.
- Transaction serialization (
Transaction.toBytes, TxInput.toBytes,
etc.) — note any size limits that would cause Nat32/Nat64
conversion traps.
Audit workflow for an existing file
- List every
public func and public class member.
- For each, read the body and follow the call graph one level into
private helpers.
- Note every
trap, assert, Result/Option return, and any implicit
trap source (subtraction, indexing, division).
- Update the doc string to enumerate the conditions.
- Re-run
mo-doc and visually scan the rendered HTML for sections that
still lack a "Traps" / "Errors" paragraph on a non-trivial function.
Final Step: User-Perspective Read-Through (REQUIRED)
After every public declaration has a doc string, do one more pass. Read
each doc string from the perspective of a first-time user of the API who
has not seen the implementation. For every doc, ask:
- What is the unit / format of each argument and the return value?
(bytes vs. bits, big- vs. little-endian, satoshis vs. BTC, raw vs.
DER-encoded, compressed vs. uncompressed, 0-based vs. 1-based, …)
- For Motoko array parameters, is
[T] (immutable) vs. [var T]
(mutable) clear? Examples that mix Array.tabulate (returns [T])
with VarArray.repeat (returns [var T]) routinely confuse
newcomers — spell out which shape the function expects.
- What are the size or range constraints on each input?
- Which BIP / RFC / spec defines the format, and is it linked?
- For mutating methods, what state changes? Is the receiver still usable
afterward?
- For functions that take a callback or proxy (e.g. an ECDSA signer), what
is the expected input/output shape of the callback?
- For variant returns, what does each variant mean semantically (not just
what tag it carries)?
- Are domain-specific terms ("witness program", "tap leaf", "sighash",
"scriptPubKey") used without a one-line explanation or link?
- For constants (
EMPTY_WITNESS, dustThreshold), what is the value and
why does it have the value it has?
- For re-exported types, where is the actual definition (and is the link
there)?
- If you removed the function name, would the description still be
unambiguous? If two near-identical functions exist (e.g.
toBytes vs.
toBytesIgnoringWitness, encode vs. encodeUncompressed), is the
difference between them spelled out?
If any question remains unanswered, extend the doc string to address it.
Prefer one extra sentence in the doc over forcing the user to read the
source. Do this pass file by file; it usually surfaces 2–5 missing facts
per non-trivial module.