#Specification Style
This guide defines how the chapters are written: the numbered language
chapters in lang/, the stdlib chapters in
std/, and the CLI chapter in cli/.
The goal is a reference that is precise but quick to read: one rule per
sentence, the common case first, and every rule citable by a stable ID.
Data Types and Enums is the pilot chapter written in this style. The other chapters are restyled one at a time, and a restyle must not change what the chapter means.
#Section Template
Each section, or subsection, follows this order:
- Definition. One sentence that says what the section defines.
- Example. A minimal example of the common case.
- Rules. A numbered list, one rule per item, each with a
rule ID. A rule that names a diagnostic ends with
"Error:
code.", and a rule that names a runtime panic category ends with "Panic:code." - Error examples. A code block in which each rejected line ends with an
# error: codecomment. See Examples. - Why. An optional rationale callout, set apart from the rules. See Callouts.
- See also. Links to related sections, in place of inline parenthetical cross-references.
A section may add ### or #### subsections for separate topics. Headings
that already exist, and so their anchors, never change: the website, the
anchor checker, audits, and conformance cases link to them.
#Writing Rules
- One topic per paragraph. A paragraph has at most four sentences and about 60 words.
- One rule per sentence, and at most 25 words per sentence.
- Use the normative vocabulary that the specification README defines. Do not invent synonyms for it.
- Put anything enumerative in a table: limits, forms, spellings, or cases.
- Put the common case first. Obsolete forms and edge cases come last in their section.
- Write a defined term in bold where it is defined, and add it to the glossary.
- Put a diagnostic code or panic category at the end of its rule, never in the middle of a sentence.
- Keep rationale out of rules. It goes in a Why callout.
The style lint in spec/check.sh warns about a paragraph over 90 words or a
sentence over 35 words. Those limits are a backstop; aim for the targets
above.
#Normative Vocabulary
The words must, must not, should, should not, and may, and the phrases is an error, is invalid, and is rejected, are defined once, in Normative Vocabulary.
Prefer these forms:
| Write | Instead of |
|---|---|
"X is an error. Error: code." |
"X is diagnosed as code because ..." |
"X must be Y." |
"X has to be Y", "X needs to be Y" |
"X may be Y." |
"X can be Y", "X is allowed to be Y" |
"X is invalid." (no code named) |
"X is not permitted" |
A restyle keeps the original verb when changing it could change the meaning. For example, the pilot keeps "cannot" in three rules.
A new term needs a reason no existing word serves: prefer the trait name or the established term (Rust, Go, Swift, Kotlin, Haskell, or PL textbook vocabulary, or a name the language itself spells) over a coined one. When several existing words fit, use the plainest that says what it means.
#Rule IDs
Every normative rule in a restyled chapter carries a rule ID: a stable,
unique, dotted name, like the Rust Reference's r[items.fn.generics].
#Marker
A rule ID marker is r[, the ID, and ], followed by a space. It opens a
numbered list item, a paragraph, or a table cell:
1. r[data.field.unique] Field names must be unique within the struct.
A marker anywhere else in prose is an error, so a marker can never hide in
the middle of a sentence. On GitHub the marker shows as plain text. The
website renders it as a small link beside the rule, whose HTML id is r-
followed by the ID, as in
r-data.embed.width.
#Syntax And Hierarchy
An ID has two or more segments separated by dots. Each segment is lowercase kebab-case: a letter, then letters, digits, and single hyphens.
- The first segment is the chapter prefix from the table below.
- The second segment names the topic, usually the section, as in
data.embedordata.enum. - Further segments name the rule. A rule that refines another extends its
ID, as in
data.embed.depthanddata.embed.depth.self.
Segments describe content, never position. An ID contains no section number or list index, so reordering a list or a chapter renumbers nothing.
| Chapter | Prefix |
|---|---|
| Lexical Structure | lex |
| Grammar | grammar |
| Names and Scopes | names |
| Type System | types |
| Expressions | expr |
| Control Flow | flow |
| Functions | fn |
| Data Types and Enums | data |
| Traits | trait |
| Modules | module |
| Requirements and Suspension | req |
| Variadic Generics | pack |
| Values And References | ref |
| Annotations | annot |
std/cmp.md |
std-cmp |
std/collections.md |
std-collections |
std/console.md |
std-console |
std/host.md |
std-host |
std/fs.md |
std-fs |
std/path.md |
std-path |
std/random.md |
std-random |
std/option.md |
std-option |
std/result.md |
std-result |
std/num.md |
std-num |
std/error.md |
std-error |
std/encoding.md |
std-encoding |
std/digest.md |
std-digest |
std/format.md |
std-format |
std/hash.md |
std-hash |
std/iter.md |
std-iter |
std/json.md |
std-json |
std/serde.md |
std-serde |
std/cli.md |
std-cli |
std/regex.md |
std-regex |
std/process.md |
std-process |
std/http.md |
std-http |
std/server-sdk.md |
std-server-sdk |
std/net.md |
std-net |
std/ops.md |
std-ops |
std/testing.md |
std-testing |
std/text.md |
std-text |
std/time.md |
std-time |
std/task.md |
std-task |
| Command Line | cli |
A stdlib chapter's prefix is std- and its module name. The CLI tier's
one chapter, cli/command-line.md, uses cli. The
stdlib chapter table lists each chapter; a later
move task adds its file.
spec/tools/spec-prose.ts holds the same table for the checks; change both
together.
#Stability
- An ID never changes once it is committed. Rewording a rule, moving it within its chapter, or renaming a heading keeps the ID.
- A rule whose meaning changes gets a new ID, and the old ID is retired. A citation of the old ID then never silently changes meaning.
- A deleted rule's ID is retired. A rule that moves to another chapter is retired and gets an ID with the new chapter's prefix.
- A retired ID is never reused, and IDs are never renumbered.
- A retired ID is deleted from the specification, not listed. The commit message for the change names it, with its replacement when there is one. The commit message records what a pass applied; GitHub issues hold the open questions.
#Citing A Rule
- A link cites a rule by its anchor, as in
08-data-and-enums.md#r-data.embed.width. The anchor checker accepts rule anchors wherever it accepts heading anchors. - The
specificationcolumn ofconformance/cases.tsvmay name a rule anchor instead of a section anchor. - A fixture's
# test:line, an audit, or an implementation's diagnostic text may name a rule ID, such as "see data.embed.width".
#Checks
spec/check.sh runs spec/check-spec-style.ts, which fails when an ID is
malformed, misplaced, outside a chapter, without its chapter's prefix, or
duplicated anywhere in the specification. The rule inventory
diff (see Restyling A Chapter) reports a retired ID
as lost, and fails when an added ID appears in the history of the
chapters, numbered or stdlib, which means it was retired before.
#Examples
Every text fence in a numbered chapter has one row in
conformance/examples.tsv, keyed by its position among the chapter's text
fences. Adding, moving, or splitting an example means updating those rows.
An error example is a text fence in which each rejected line ends with
# error: code, or with a bare # error when the chapter names no code for
that rule. A valid line may sit beside it with an ordinary comment:
struct User:
mut name: string # error: mutable-field-modifier
mut Base # error: mutable-field-modifier
friend: *mut User # valid: the type grants mutable access- An error example is classified
mixedinexamples.tsv, and names the invalid fixtures that demonstrate its errors. - The overlap check requires most of the example's salient names to occur in those fixtures, so take names from the fixtures.
- A code after
# error:must be a code in the Diagnostics table.
The website shows an error example under an "Error example" tab and tints each marked line.
#Callouts
A callout is a block quote that opens with a bold label:
> **Why.** Access through an embedded field already follows its container.
> **Why.**gives the rationale for the rules before it.> **Note.**states a consequence or gives advice.
Both are explanatory, as Conformance Language says of notes: they add no requirement. A sentence that states a rule belongs in the rule list, not in a callout.
#Tables
A table may hold rules. The first cell of each rule row opens with its marker, and the website renders the table as a rule table:
| Rule | Limit | Error | Reported on |
| --- | --- | --- | --- |
| r[data.embed.width] Width | A struct may declare at most three embedded fields. | `too-many-embedded-fields` | the fourth embedded field |
#Restyling A Chapter
-
Rewrite the chapter in this style, with a rule ID on every rule.
-
Update the chapter's rows in
conformance/examples.tsv. -
Run the rule inventory diff against the committed version:
node --experimental-strip-types spec/tools/rule-inventory.ts --diff --all \ main:spec/lang/08-data-and-enums.md spec/lang/08-data-and-enums.md --out /tmp/08-diff.md -
The diff must report nothing lost: no diagnostic code, no example line, and no rule ID. Examples may move or split, and error examples may be added.
-
Compare the old and new normative content sentence by sentence. The report pairs each old sentence with its closest new one, and marks weak pairs.
-
Preserve meaning exactly. Add, drop, or change no rule. Record a genuine ambiguity or contradiction for the owner instead of resolving it.
-
Run
bash spec/check.shandpnpm run website:build.
Without --diff, the tool prints one chapter's inventory: its codes,
normative sentences, examples, rule IDs, and prose statistics.
#Before And After
This excerpt from Struct Declarations shows the change. Before, one paragraph mixed five topics and put a code in the middle of a sentence:
Field names must be unique within the struct. Every field has an explicit type.
Fields have no standalone `mut` modifier. `friend: &mut User` declares a field
whose type grants mutable access through that reference; `mut friend: User`
is invalid. An embedded field is written without `mut`: `Base` embeds `Base`,
and `mut Base` is a `mutable-field-modifier` error, because access to an
embedded field already follows its container
([Data Embedding](#data-embedding)).
An ordinary named field may have a default expression. ...
After, the topic has its own subsection, a rule list, an error example, and a Why callout:
### Fields
1. r[data.field.unique] Field names must be unique within the struct. Error: `duplicate-field`.
2. r[data.field.typed] Every field has an explicit type.
3. r[data.field.no-mut-modifier] Fields have no standalone `mut` modifier: `mut friend: User` is an error. Error: `mutable-field-modifier`.
4. r[data.field.mut-type] `friend: *mut User` declares a field whose type grants mutable access through that reference.
5. r[data.field.embedded-no-mut] An embedded field is written without `mut`: `Base` embeds `Base`, and `mut Base` is an error. Error: `mutable-field-modifier`.
```text
struct User:
mut name: string # error: mutable-field-modifier
mut Base # error: mutable-field-modifier
friend: *mut User # valid: the type grants mutable access
```
> **Why.** Access through an embedded field already follows its container.