Skip to content

Self-Documenting Code

Learn how to write documentation that the compiler understands.

Time: 25 minutes Level: Beginner Prerequisites: Tutorial 1 (Hello World to Production) What you’ll learn: Doc comments, structured tags, doctests, documentation coverage, and query predicates


Most languages treat documentation as text blobs stapled to functions. Grep for a pattern, hope the comments are up to date, move on.

Janus treats documentation as structured data in the ASTDB — the same semantic database the compiler uses for type checking and code generation. Your doc comments are parsed, tagged, and stored as columnar rows. They survive renames. They are queryable. They are enforceable in CI.

“Documentation is not decoration. It is structured evidence.”


Documentation comments use triple slashes. Place them immediately before a declaration:

/// Add two numbers and return the result.
func add(a: i64, b: i64) -> i64 do
return a + b
end

The first sentence becomes the summary — the one-liner that appears in module overviews and search results. Everything after the first blank /// line becomes the description.

/// Calculate the factorial of n.
///
/// Uses iterative multiplication to avoid stack overflow
/// on large inputs. Returns 1 for n <= 1.
func factorial(n: i64) -> i64 do
var result = 1
var i = 2
while i <= n do
result = result * i
i = i + 1
end
return result
end

Save the file as math.jan and run:

Terminal window
janus doc math.jan

Output:

## Functions
### `factorial(n: i64) -> i64`
Calculate the factorial of n.
Uses iterative multiplication to avoid stack overflow
on large inputs. Returns 1 for n <= 1.
---
### `add(a: i64, b: i64) -> i64`
Add two numbers and return the result.

What you learned:

  • /// creates a doc comment
  • The first sentence is the summary
  • janus doc generates Markdown from your source

The essential three: @param, @returns, @error

Section titled “The essential three: @param, @returns, @error”

Raw prose is good. Structured tags are better. They tell the compiler (and your teammates) exactly what each parameter does, what comes back, and what can go wrong.

/// Opens a file at the given path and returns a handle.
///
/// This function validates the path and checks permissions
/// before attempting to open the file.
///
/// @param path The filesystem path to open
/// @param mode Read or write mode
/// @returns File handle on success
/// @error FsError.NotFound Path does not exist
/// @error FsError.PermissionDenied Insufficient permissions
/// @capability CapFsRead Required for read mode
/// @since 0.3.0
/// @complexity O(1) amortized
///
/// ```janus
/// let f = open("data.txt", "r")
/// defer f.close()
/// ```
func open(path: str, mode: str) !File do
println("opening file")
end
TagSyntaxPurpose
@param@param name DescriptionDocument a parameter
@returns@returns DescriptionDocument the return value
@error@error ErrorType DescriptionDocument an error variant
@capability@capability CapName DescriptionRequired capability token
@since@since versionVersion the item was introduced
@deprecated@deprecated ReasonMark as deprecated with guidance
@safety@safety ExplanationSafety invariants the caller must uphold
@complexity@complexity O(...)Algorithmic complexity
@see@see identifierCross-reference another declaration

Run janus doc on the file above:

Terminal window
janus doc file_ops.jan

Output:

### `open(path: str, mode: str) !File`
Opens a file at the given path and returns a handle.
This function validates the path and checks permissions
before attempting to open the file.
**Parameters:**
| Name | Description |
|------|-------------|
| `path` | The filesystem path to open |
| `mode` | Read or write mode |
**Returns:** File handle on success
**Errors:**
| Error | Description |
|-------|-------------|
| `FsError.NotFound` | Path does not exist |
| `FsError.PermissionDenied` | Insufficient permissions |
**Capabilities:** `CapFsRead` -- Required for read mode
**Since:** 0.3.0
**Complexity:** O(1) amortized

Tags are not freeform text. They are parsed into structured fields that tooling can consume, validate, and query.

What you learned:

  • @param, @returns, @error document the function contract
  • Tags like @capability, @since, @deprecated add metadata
  • janus doc renders structured sections (Parameters table, Errors table, etc.)

Code examples inside doc comments are not just for humans. They are first-class AST nodes that the compiler parses, type-checks, and can execute.

/// Format a greeting message.
///
/// ```janus
/// let msg = greet("Markus")
/// assert(msg == "Hello, Markus!")
/// ```
func greet(name: str) -> str do
return "Hello, " ++ name ++ "!"
end

The fenced ```janus block inside the doc comment is extracted during the doc extraction pass and treated as a doctest. Unlike regex-extracted doctests in other languages, these are parsed into AST nodes and participate in type checking.

Test blocks placed immediately after a function are also linked as that function’s doctest:

/// Clamp a value to a range.
func clamp(val: i64, lo: i64, hi: i64) -> i64 do
if val < lo do return lo end
if val > hi do return hi end
return val
end
test "clamp basics" do
assert(clamp(5, 0, 10) == 5)
assert(clamp(-1, 0, 10) == 0)
assert(clamp(99, 0, 10) == 10)
end

The compiler sees the test block as a child node of clamp in the ASTDB. Renaming clamp updates the link automatically via CID resolution.

Terminal window
janus test --doc # Run all doctests
janus test --doc --check # Verify they compile without executing

What you learned:

  • Fenced ```janus blocks inside doc comments become doctests
  • Adjacent test blocks are linked to the preceding function
  • janus test --doc executes doctests

Writing documentation is half the job. Enforcing it is the other half. Janus ships a built-in documentation linter:

Terminal window
janus doc math.jan --check

Example output:

Documentation Coverage Report
=============================
File: math.jan
Coverage: 75.0% (3/4 declarations documented)
Issues:
[MISSING_DOC] func helper() at line 47 -- no doc comment
[MISSING_PARAM] func factorial(n: i64) at line 39 -- missing @param for 'n'
[MISSING_RETURNS] func add(a: i64, b: i64) at line 5 -- missing @returns tag
Summary: 3 issues found
CheckDescription
Missing docsPublic declarations without /// comments
Missing @paramParameters not documented
Missing @returnsReturn values not documented
Deprecated without reason@deprecated tags missing migration guidance
Stale @seeCross-references pointing to renamed or removed items

The --check flag returns exit code 1 if any issues are found. Add it to your CI pipeline:

Terminal window
# In your CI script
janus doc src/ --check || exit 1

This enforces documentation quality the same way you enforce test coverage — automatically, on every commit.

What you learned:

  • janus doc --check reports documentation coverage
  • It catches missing docs, missing tags, and stale references
  • Gate it in CI to enforce documentation standards

Step 5: Query Predicates — The Superpower (5 min)

Section titled “Step 5: Query Predicates — The Superpower (5 min)”

Here is where Janus documentation diverges from every other language. Because doc comments are stored as structured ASTDB rows, you can query them with predicates.

Terminal window
janus query --doc "<predicate>" <file_or_directory>
AtomMatches
funcFunction declarations
structStruct declarations
enumEnum declarations
constConstant declarations
traitTrait declarations
has_docDeclarations with doc comments
is_deprecatedItems marked @deprecated
has_doctestItems with embedded code examples
has_param_docFunctions with @param tags
has_return_docFunctions with @returns tags
has_error_docFunctions with @error tags

Combine atoms with and, or, not, and parentheses:

Terminal window
# Find undocumented functions
janus query --doc "func and not has_doc" src/
# Find deprecated items anywhere in the project
janus query --doc "is_deprecated" src/
# Find functions missing parameter docs
janus query --doc "func and has_doc and not has_param_doc" src/
# Find documented functions that include examples
janus query --doc "func and has_doctest" src/
# Find undocumented structs or enums
janus query --doc "(struct or enum) and not has_doc" src/

Suppose you are preparing a release and need to find every deprecated function in your codebase:

Terminal window
janus query --doc "is_deprecated" src/

Output:

src/math.jan:39 func factorial(n: i64) !i64
@deprecated Use math.gamma() instead for better precision
Found 1 deprecated item(s).

This is documentation as data. You do not grep for missing docs — you query a semantic database.

What you learned:

  • janus query --doc searches declarations by documentation properties
  • Atoms like has_doc, is_deprecated, has_doctest filter by metadata
  • Combinators (and, or, not) compose into precise queries
  • This replaces ad-hoc grep scripts with structured semantic queries

In most languages, documentation is a string blob attached to an AST node. You can render it, but you cannot reason about it programmatically. If a function is renamed, the docs might reference the old name. If a parameter is added, nothing reminds you to document it.

Janus takes a different approach:

  • ASTDB-native — Doc comments are columnar rows in the same database the compiler uses. They are not an afterthought bolted onto the AST.
  • CID-linked — Each doc entry is bound to its declaration by content ID. Docs survive renames, file moves, and refactors because the link is semantic, not textual.
  • Queryable — Tooling and CI can enforce documentation standards through predicates, not regex.
  • Machine-readable — The UTCP output format makes docs consumable by AI agents and language servers without custom parsing.

Elixir’s ExDoc built an excellent documentation culture. But ExDoc treats docs as string blobs on module attributes — rich for humans, opaque for machines. Rustdoc’s missing_docs lint catches undocumented items, but cannot query for “functions with examples” or “deprecated items missing migration guidance.”

Janus combines the cultural emphasis on documentation with architectural enforcement. The compiler does not just store your docs — it understands them.


  • Wrote doc comments with /// and multi-line descriptions
  • Used structured tags (@param, @returns, @error, @capability, @since, @deprecated)
  • Created embedded doctests with fenced code blocks
  • Linked adjacent test blocks to functions
  • Ran janus doc to generate structured Markdown output
  • Used janus doc --check to lint documentation coverage
  • Queried the ASTDB with janus query --doc to find undocumented or deprecated items

  • Read the janus doc reference for the full tag reference, output formats (HTML, JSON, UTCP), and symbolic markers
  • Explore janus doc --format=html for browsable documentation sites
  • Add janus doc --check to your CI pipeline

Your documentation is now as honest as your code.

“Syntactic Honesty applies to prose as much as to programs. If the docs can lie, the system is already compromised.”