Files
danos/docs/coding-standards.md
T
Daniel Samson d5d15cefcb Decode PCI/ACPI device identities and name their class codes as enums
Two related changes to make device identities legible in the boot log and in
the code that matches on them.

Logging: the pci-bus driver decodes each function's class/subclass/prog-IF
triple to human names (via the existing pci-class module), and the acpi
service appends each _HID's human name (via acpi-ids) to its report line. So
"class 0x01 (Mass Storage Controller) subclass 0x06 (Serial ATA Controller)
progif 0x01 (AHCI 1.0)" reads straight off the log when writing a driver.

Naming: a new coding standard ("Named values, not magic numbers") says a value
with meaning gets a name, prefer an enum for value sets. Applied:
- pci-class is refactored from u8-switch tables into a BaseClass enum plus
  per-class SubClass/ProgIf enums with name() methods (the usb-ids shape). The
  public className/subclassName/progIfName(u8...) API is unchanged, so the
  hardware-byte decoders (pci-bus, the kernel dump) are untouched; output is
  byte-identical.
- the device-manager builds the xHCI class triple from named parts instead of
  a bare 0x0C0330.
- the acpi service's _CRS walk names its resource-descriptor tags as
  SmallResourceType/LargeResourceType enums, and the _HID integer decode uses
  the AML module's existing *_opcode constants (now re-exported from aml.zig)
  rather than bare 0x0A/0xFF/... literals.
2026-07-13 05:05:25 +01:00

195 lines
9.8 KiB
Markdown

# Coding standards
Conventions for danos source. The overriding one, from which most of the rest follows:
> **Names are spelled out in full. An identifier is not abbreviated unless the
> abbreviation is an acronym.**
`interruptDispatch`, not `intDisp`. `message_len`, not `message_len` (`msg` expands, `len`
is a Zig idiom — see the exceptions). `devices_broker`, not `devices_broker`. `scheduler`, not
`sched`. The cost of a longer name is paid once, at the keyboard; the cost of a
cryptic one is paid every time the code is read, by everyone who reads it. In a
microkernel whose whole argument is that a human can hold each piece in their head,
that trade is not close.
## The rule, precisely
**Acronyms and initialisms stay.** They *are* the full name — expanding them would make
the code worse, not better. `IPC`, `MMIO`, `DMA`, `IRQ`, `TSS`, `GDT`, `IDT`, `APIC`,
`GSI`, `HPET`, `ACPI`, `PCI`, `EOI`, `BAR`, `ECAM`, `MSI`, `CPU`, `ELF`, `ABI`, `UEFI`,
`MMU`, `TLB`, `ISR`, `ISA`, `GAS`, `HAL`, `PMM`, `VMM`, `VFS`, `HID`, `HCD`, `SMP`,
`AML`, `MADT`, `MCFG`, `FADT`, `RSDP`, `XSDT`, `RSDT`, `GOP`, `EDID`, `TSC`, `PIT`,
`RTC`, `LAPIC`, `SIPI`. In code they carry whatever case the surrounding convention
demands: `Hal` the type, `hal` the variable, `mapMmio` the function.
**Everything else is spelled out.** If it's a word with letters removed, restore them:
| Abbreviation | Full |
|---|---|
| `proto` | `protocol` |
| `msg` | `message` |
| `desc` | `descriptor` |
| `res` | `resource` |
| `recv` | `receive` |
| `buf` | `buffer` |
| `cur` | `current` |
| `src` / `dst` | `source` / `destination` |
| `idx` | `index` |
| `addr` | `address` |
| `reg` | `register` |
| `prev` | `previous` |
| `cfg` / `config` | `configuration` |
| `arch` | `architecture` |
| `sched` | `scheduler` |
| `dev` | `device` |
| `sys` / `syscall` | `system` / `system_call` |
| `info` | `information` |
| `dt` | `device_tree` |
| `ep` | `endpoint` |
| `rt` | `runtime` |
| `func` | `function` |
| `phys` / `virt` | `physical` / `virtual` |
| `wq` | `wait_queue` |
This list is illustrative, not exhaustive. The rule is the rule; when you meet a new
abbreviation, expand it.
## Exceptions
Three, and only three.
1. **Foreign ABI names are spelled exactly as the ABI spells them — but only inside
the layer that *is* that ABI.** A function that *is* the C or POSIX interface keeps
its name: `fopen`, `fwrite`, `fread`, `malloc`, `calloc`, `realloc`, `free`,
`memcpy`, `mmap`, `munmap`, `open`, `read`, `write`, `close`, `lseek`, `stat`,
`errno`, `O_CREAT`. We don't get to rename `fwrite` to `fileWrite` — it wouldn't be
`fwrite` any more.
**This exception is scoped to one place: `library/posix/`.** A file under
`library/posix/` *is* the foreign ABI, so it keeps the ABI's spellings — that is the
whole rule for that directory. **Everywhere else, Zig/danos naming applies with no
POSIX exception**, so there is nothing to get wrong: if you're not in
`library/posix/`, expand it. A concept POSIX also has gets a danos name outside that
layer — the VFS wire protocol carries a `FileStatus`, not a `Stat`, and a `create`
flag, not `O_CREAT`; `library/posix/` is what maps `stat`→`status` and
`O_CREAT`→`create` at the boundary. (The `syscall` *wrappers* elsewhere are not an
exception to this — they wrap the private danos ABI, so they use danos names.)
2. **Zig idioms are spelled the way Zig spells them.** Three names are the language's,
not ours, and are left alone:
- **`init` / `deinit`** — the constructor convention (`std.ArrayList.init`), not a
shortening of "initialize".
- **`len` / `ptr`** — the slice field names (`slice.len`, `slice.ptr`). Our own
structs use bare `len`/`ptr` fields to mirror them, so a reader carries one
mental model. (Compounds still expand: a field is `message_len`, not
`message_length` — `len` is kept, `msg` is not.)
- The builtins (`@min`, `@max`, `@memcpy`) and `allocator.alloc` / `.create` are
Zig's spelling.
The rule governs the names *we* coin.
3. **Single-letter variables in a trivial local scope.** `for (items) |item, i|` may
keep `i`; a coordinate may be `x`, `y`. The moment the scope is big enough that the
letter's meaning isn't obvious on sight, give it a real name. When in doubt, name it.
That's all — no Unix-abbreviation exception. The source directories are full words
(`system`, `library`, not `src`/`lib`), and there is no daemon `d` suffix: a driver
lives in `system/drivers/` and a service in `system/services/`, so the *location*
already says what it is. Encoding the role in the name too (`busd`, `vfsd`) is
redundant — the program is just `bus`, `vfs`. Don't put in a name what its directory
already tells you.
## A note on collisions
Two identifiers can legitimately expand to the same word. When they do, keep both
meaningful by renaming one to its *specific* identity rather than the generic
expansion. Two cases resolved this way:
- The `config` module (compile-time tunables — `maximum_cpus`, `timer_hz`) would
collide with `cfg` (a `PlatformConfiguration` value) at `configuration`. The module
became **`parameters`**, which is what it holds.
- The kernel `device.zig` module would collide with `dev` (a device value) at
`device`. The module alias became **`device_model`**, which is what it is — the
device data model (`Device`, `DeviceTree`, `ResourceKind`).
- The `Namespace` module alias (`ns`/`nsp` across the AML files) collides with a
`Namespace` **instance**. Resolved by dropping the module alias entirely — the two
types it provided are imported directly (`const Node = @import("namespace.zig").Node;`)
— which frees `namespace` for the instance.
A related case is one abbreviation with two meanings. In the AML code, `op` means
**opcode** (`opcodes.zig`, the `*_opcode` constants) but `Op` in `BinaryOperation` /
`LogicOperation` means **operation** — distinguished by case. The per-opcode parser
handlers, formerly `opName`/`opField`, are `parseName`/`parseField`: they *parse* the
opcode's structure, which says what they do without overloading "op".
## Case and file names
Within those spelling rules, follow Zig's own conventions:
- **Types** — `PascalCase`: `DeviceDescriptor`, `Endpoint`, `WaitQueue`.
- **Functions** — `camelCase`: `mapUserDeviceInto`, `notifyFromIsr`.
- **Variables, fields, constants** — `snake_case`: `message_length`, `devices_broker`,
`notify_badge_bit`.
**File names are `kebab-case`.** A file named for a multi-word thing hyphenates it:
`device-tree.zig`, `ipc-synchronous.zig`, `vfs-protocol.zig`, `devices-broker.zig`. A
single word or acronym needs no hyphen: `scheduler.zig`, `paging.zig`, `apic.zig`,
`idt.zig`. (The module *alias* a file is imported under still follows the code
conventions above — `snake_case` — because it's an identifier, not a filename.)
**A sub-project's entry point repeats its directory's name** — `init/init.zig`,
`runtime/runtime.zig`, `hpet/hpet.zig` — and the sub-project is addressed by the
*directory* (`system/services/init`, `library/runtime`), with the repeated leaf
resolving away. See the repository-layout section of [README.md](README.md).
## Named values, not magic numbers
The naming rule has a twin: **a value with meaning gets a name, too.** The same
principle drives both — a reader should never have to leave the code to understand it.
An abbreviated *name* forces a reader to guess; a bare *number* forces them worse, out
to a spec or a header or a comment three files away, to learn what the value even *is*.
If `0x0C` is the PCI serial-bus class, the code says `BaseClass.serial_bus`, not `0x0C`;
if `0x04` is the ACPI IRQ resource descriptor, it says `SmallResourceType.irq`, not
`0x04`. The number is an implementation detail of the name — recorded once, where the
name is defined, and never spelled again at a use site.
**Prefer an `enum`** when the values form a set (device classes, AML opcodes, resource
descriptor types, states): the type then also says *which* set a value belongs to, and
the compiler rejects a value from the wrong one. A lone `pub const` with a descriptive
name suffices for a one-off (`const large_descriptor_bit = 0x80`). Reach for the enum
the moment code elsewhere compares against, packs, or produces the value — a packed PCI
class triple is written from named parts (`.serial_bus`, `.usb`, `.xhci`), never as
`0x0C_03_30` under a comment that decodes the bytes.
The exceptions are the numbers that carry no hidden meaning: `0` and `1` as plain zero
and one, an index step, a field width, a bit shift. `x + 1`, `buffer[0]`, and `<< 8`
need no christening — there is nothing to look up. The test is exactly the naming test:
*would a reader have to look this up to know what it means?* If yes, name it. This is
what `opcodes.zig`'s `*_opcode` constants, `acpi-ids`'s `HardwareId`, and `pci-class`'s
class enums already are — reference data defined once and named everywhere it is used.
## Why acronyms are the line
Because an acronym has no letters to restore. `MMIO` doesn't become "memory mapped
input output" in code — that expansion is what the acronym *is for*. But `msg` is just
`message` with three letters stolen, and stealing them buys nothing a reader wants. The
test for "is this an abbreviation I must expand" is simply: *is there a longer word this
is a clipped form of?* If yes, write the word. If it's an initialism standing in for a
phrase, leave it.
## Zen of Zig
* Communicate intent precisely.
* Edge cases matter.
* Favor reading code over writing code.
* Only one obvious way to do things.
* Runtime crashes are better than bugs.
* Compile errors are better than runtime crashes.
* Incremental improvements.
* Avoid local maximums.
* Reduce the amount one must remember.
* Focus on code rather than style.
* Resource allocation may fail; resource deallocation must succeed.
* Memory is a resource.
* Together we serve the users.