RFD-0029 — Doc comments and ox doc
Question
What is Argon’s API-reference documentation surface — comment syntax, parsed shape, intra-doc link semantics, doc-test treatment, visibility rules — and where does it live in the toolchain?
Context
Argon’s narrative documentation lives in the Argon book, an mdBook deployed at book.argon-lang.org. That covers the language tutorial + reference + the “For Agents” section. What it does not cover is the per-package API reference: the public concepts, metatypes, relations, rules, queries, mutations, and computations that authors export from their packages, with their doc strings, signatures, and cross-links rendered into a browseable surface.
Cargo + rustdoc is the reference design here — every public Rust item gets a doc page derived from /// comments, with intra-doc links resolved through the compiler’s name resolver, and code blocks executed as doc-tests. The Argon equivalent is ox doc. Today the lexer recognizes /// and //! (oxc::syntax::lexer) and the tree-sitter grammar admits doc_comment as a top-level statement, but elaboration drops them — they’re CST trivia with no structural attachment to declarations. Nothing reads them downstream.
This RFD locks the surface before any of that wiring lands.
Decision
Adopt the rustdoc model with three deliberate departures: (a) the parsed doc IR is the load-bearing artifact, not the HTML; (b) doc comments attach during elaboration, alongside pub visibility analysis; (c) ox doc is a thin renderer over a stable JSON IR that other tools (semver-check, doc-coverage, alternative renderers, the LSP InfoView) consume directly.
Comment syntax
| Form | Role | Attachment |
|---|---|---|
/// … | Outer doc comment. | Concatenates with contiguous /// runs immediately preceding a declaration; attaches to that declaration. |
//! … | Inner doc comment. | Valid only at module top (before the first declaration). Concatenates with contiguous //! runs and attaches to the enclosing module. Anywhere else → OW0815 MisplacedInnerDoc. |
// … | Regular line comment. | Trivia. Never doc. |
/* … */ | Block comment (no doc form). | Trivia. Argon does not have /** … */ — keeping one outer-doc form keeps the surface tight. |
Doc strings on items with no public visibility (pub keyword absent) elaborate but stay out of the rendered surface unless --document-private-items is passed (mirrors cargo).
Markdown flavor
CommonMark + the standard pulldown-cmark extensions Argon needs:
- Tables (
|---|---|). - Footnotes (
[^id]). - Strikethrough (
~~text~~). - Task lists (
- [ ] item). - Heading anchors auto-generated from heading text (kebab-cased).
Smart-punctuation, math (KaTeX), and other extensions are deliberately not enabled at the doc-IR layer. Renderers may opt in (the book already enables KaTeX via mdBook config); doc strings stay portable across renderers.
Code blocks
Fenced code blocks tagged argon are first-class doc tests:
| Tag | Behavior |
|---|---|
```argon | Run. Elaborates through the same capture-mode runner that powers fixture { }. Elaboration error → test fails. |
```argon,ignore | Render only. Not executed; surfaces as a syntax-highlighted block. For partial snippets, illustrative non-Argon code, etc. |
```argon,no_run | Parse + elaborate, do not test-execute. Useful for snippets that rely on external runtime state (a kernel-backed query against a live tenant). |
```argon,setup | Setup boilerplate. Concatenated as a prelude into every argon block in the same /// run. Not a runnable test on its own. |
```argon,fail | Expected to fail elaboration. Passes if elaboration produces any diagnostic; fails if the snippet elaborates clean. |
Lines starting with # (hash-space) inside any argon code block are hidden from rendered output but kept in the executable body. Mirrors rustdoc; lets authors elide setup ceremony in published docs while keeping examples runnable.
Code blocks tagged with anything other than argon[,…] (e.g. ```rust, ```text, ```) render verbatim and are never executed.
Intra-doc link syntax
Two equivalent forms:
`[Name]`— bare reference.`` [`Name`] ``— back-tick reference (matches rustdoc convention).
Name resolves through the same name resolver oxc::elaborate::resolve_imports uses, against the documented item’s lexical scope:
- Bare identifier (
[Concept]) — local module first, thenuse-imported paths, then package root. - Path (
[pkg::module::Concept]) — fully qualified. - Method-style (
[Concept::axis_value]) — concept-qualified field, axis, or relation reference. (v1: only resolves to declared concept members; deeper structural references queue for v2.)
Unresolved → OW0816 BrokenDocLink. Markdown links written [text](url) are explicit hrefs, never auto-resolved.
Visibility
| Item | Default in ox doc | Override |
|---|---|---|
pub <metatype-name> / pub rel / pub metarel / pub decorator / pub frame / pub query / pub mutation / pub derive | Documented. | #[doc(hidden)] opts out. |
Items without pub (private to package). | Hidden. | --document-private-items includes them. |
Items with #[doc(hidden)]. | Hidden. | --document-private-items includes them. |
#[doc(hidden)] is an attribute on the declaration, parsed alongside #[unproven] / #[assumed] / #[theorem]. It does not affect elaboration, name resolution, or runtime — only doc rendering.
Doc IR (JSON, stable)
The parsed shape lives in oxc-protocol::core::doc:
DocBlock { markdown: String, code_blocks: Vec<DocCodeBlock>, links: Vec<DocLink>, source_range: Range }DocCodeBlock { kind: DocCodeKind, body: String, hidden_lines: Vec<u32>, source_range: Range }DocCodeKind = Run | Ignore | NoRun | Setup | FailDocLink { text: String, target: ResolvedDocTarget, source_range: Range }ResolvedDocTarget = Item { qualified_path: String } | External { href: String } | UnresolvedDocVisibility = Public | Hidden | Private
pub docs: Option<DocBlock> lives on every doc-eligible Core* item: CoreConcept, CoreRule, CoreMetatype, CoreMetarel, CoreDecorator, CoreFrame, CoreQuery, CoreMutation, CoreCompute, CoreAxiom. Module-level docs live on LoweredModule.docs.
The JSON serialization of DocBlock is the wire shape ox doc --emit-json produces. It’s the substrate three renderers consume:
ox docHTML renderer — static site, mirrors rustdoc’s information architecture.- LSP InfoView + hover — same
DocBlock, rendered into LSPMarkupContent. - MCP
argon_doctool — agent-facing JSON pass-through (Phase 4).
The JSON commits to SemVer 1.0 in a follow-up RFD once ox doc ships and shape lands stable.
Diagnostics
| Code | Trigger |
|---|---|
OW0815 MisplacedInnerDoc | //! outside module top. |
OW0816 BrokenDocLink | Intra-doc link target unresolved. |
OW0817 InvalidDocCodeBlockTag | Fenced block tagged argon,<unknown> — surfaces author typos in the kind tag. |
All three are warnings, not errors — bad docs don’t break builds. ox doc --deny-warnings (mirrors cargo doc --deny-warnings via RUSTDOCFLAGS) escalates them to fatal for CI.
Rationale
Comment syntax matches Rust verbatim. /// outer + //! inner is the convention every Rust developer already has internalized; the UFO team is moving from OntoUML and a Rust-shaped surface flattens the learning curve. Block-doc form (/** */) is omitted — one form per concern (RFD-0018 idiom).
JSON IR before HTML. The technical reference Ivan provided is unambiguous: the highest-leverage artifact is the stable typed IR. HTML is one consumer; semver-check, doc-coverage tooling, alternative renderers, and the LSP InfoView all benefit from the same shape. Rustdoc’s --output-format json (still nightly-only after years) is the exact lesson — design it as the primary surface from day one.
Doc tests through capture-mode elaboration. Argon already has the runner (oxc::test_runner) with capture-mode for fixture { }. Doc tests reuse it: each ```argonblock is a synthetic fixture whose body is<setup_blocks_concatenated>\n<this_block>. No new elaboration path. The 5-status TestStatus` taxonomy (RFD-0008) covers doc-test outcomes too.
Visibility default is pub only. Anything else and packages would leak internals into their public docs by accident; that’s the rustdoc default for the same reason. --document-private-items is the escape hatch (cargo-equivalent).
Markdown extensions are limited. Math, smart-punctuation, custom directives, and other heavy extensions create source-portability problems and tie doc strings to the renderer that supports them. Doc strings should be readable in cat, in a code review, and in the editor’s hover panel — keeping the flavor close to portable CommonMark serves all three.
Cross-link resolution piggybacks on oxc::elaborate. This is Section 4.1 of the technical reference: do this through the real resolver or it will break under re-exports, generics-as-functors (RFD-0009), and standpoint-imported names. No separate doc-time resolver.
Consequences
- New protocol module
oxc-protocol::core::doc. ts-rs bindings + JSON Schema generated byoxc-codegenon the same drift gate as diagnostics. - New diagnostic codes:
OW0815,OW0816,OW0817. - New
#[doc(hidden)]attribute parsed by the existing AttrList pass; ignored by elaboration, consumed only by the doc renderer. Option<DocBlock>field added to tenCore*types +LoweredModule; existing Salsa queries gain a doc-string input edge.- Doc-string parsing depends on
pulldown-cmark(workspace dep — already in the tree viamdbook). - Phase 1 of the
ox docrollout (this RFD’s wiring) ships ahead of the HTML renderer; the JSON IR + diagnostics are useful on day one for IDE hover bodies and doc-coverage tooling. - Cross-package linking semantics (Section 4.4 of the technical reference) defer to a follow-up RFD landing with the HTML renderer — Phase 1 emits qualified paths in
ResolvedDocTarget::Item; the renderer decides URL layout.
Historical lineage
None — new surface.