7.7 KiB
Coding Best Practices — C
The standard code in this repo is held to. PRs are reviewed against it, and review
comments cite it by rule number (blocker: rule 3, this file is 1,400 lines).
Rule numbers are frozen. New rules append; they never renumber.
The PR process itself — size, evidence, comment conventions — is in
../CONTRIBUTING.md.
Rules
1. Descriptive names. Every name says what it is at a glance — files, functions,
types, variables. The test: would you know what it is from the tree alone, without
opening it? fc_tensor_matmul(), not lookup(). Abbreviate only what is
universal (id, url, http, kv, ctx).
2. Small functions — ~100 lines of body. Doc comments don't count. Past ~100, look for the extraction point. One blank line between logical chunks, never two.
3. Small files — under 1,000 lines of code. Comments and headers don't count. Prefer more files with fewer functions. Every function in a file must belong there.
4. Library-first. Write every function as if it will be extracted into a standalone library — self-contained, minimal dependencies, no hard-coded project values in reusable logic. Headers define the contract, sources hold the implementation. Extraction should mean moving files, not rewriting them.
5. Folder nesting is navigation. Group by domain, not by file type. Each directory has one clear purpose. A directory holding one file is usually wrong.
6. Error transparency. Never swallow an error. Return a code or log it — never both nothing. No empty error paths. Callers get a typed error, not a guess.
7. Delete dead code. Don't comment it out, don't leave unused functions or includes. Git has it if you need it back.
8. Prove a test can fail. A test you have never seen fail is not evidence. Break the code it covers, watch it go red, report the failure count.
Apple's
makeis 3.81 (2006) and compares mtimes at one-second granularity. A source edited in the same second as its object is reportedup to dateand is not rebuilt — so the obvious mutation loop can be running the previous binary and reporting a vacuous gate that isn't. When proving a test red,rmthe specific.oand the linked binary first. Switching build configs needsrm -rf buildentirely.
File headers
Every source file opens with the SPDX pair, then the header block:
/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
/**
* @file attention.c
* @brief Multi-head attention, KV cache, and RoPE.
*
* @status None
* @issues None
* @todo None
*/
@status / @issues / @todo are always present, None when empty — the empty slot is
the reminder that the slot exists. When you work in a file, clear resolved entries and
reset to None.
Required on every new file. Existing coverage is partial and is being backfilled; until that lands, CI enforces the rule on added files only.
Exempt: test files, generated sources, and third-party-derived files (which keep their upstream attribution — a Foundry block may go below it, never in place of it).
Documentation
Public functions are documented in the header, which is the API contract:
/**
* Compute scaled dot-product attention into a caller-owned output tensor.
*
* @param out Output tensor, shape [seq, dim]
* @param q, k, v Query, key, value tensors
* @param mask Causal mask, or NULL for full attention
* @return 0 on success, negative error code on failure
*
* Thread-safe: yes (internal locking)
* Reentrant: no
* Signal-safe: no
*/
The three safety annotations are always present. TBD is an acceptable answer; omitting
them is not. Internal helpers need docs only when the name and signature don't carry it.
Inline comments explain why, not what. The code already says what it does.
/* K80 has no __ldg for 8-byte types — split the load or it serialises */
Layout
include/ and src/ mirror each other. snake_case filenames that name their contents
(tensor_matmul.c, not tm.c). One header + one source per logical unit. Tests mirror the
source tree.
Every public symbol carries its module prefix — fc_, tm_ (tensor), ft_ (tokenizer,
ternarize). Anything not declared in a header is static. No exceptions.
C conventions
Formatting. 4 spaces, no tabs. 100 columns. char *ptr — the star binds to the
variable. Opening brace on the same line, for functions and control flow alike.
int fc_tensor_add(Tensor *out, const Tensor *a, const Tensor *b) {
if (!out || !a || !b)
return -FC_EINVAL;
for (size_t i = 0; i < a->count; i++) {
if (!shapes_match(a, b))
return -FC_ESHAPE;
}
return 0;
}
Omit braces on single-statement bodies — unless any branch of an if/else chain needs
them, in which case every branch gets them.
Includes, four groups separated by blank lines: this module's own header first (it catches missing dependencies in that header at compile time), then standard library, then system/platform, then project headers.
Errors. Functions return int (0 success, negative error) or a pointer (NULL on
failure). Use goto-cleanup for anything acquiring more than one resource — one exit path,
deterministic teardown. Error codes are a prefixed enum with a string conversion function.
Memory. sizeof(*ptr), never sizeof(Type). calloc or explicit zeroing. Document
ownership in the function doc — who allocates, who frees. Every _open/_create/_new
has a matching _close/_destroy/_free. free(p); p = NULL;. Arena-allocate groups
sharing a lifetime. Never return a stack address.
const. Every pointer parameter is const unless the function must modify what it
points at.
Strings and buffers. sprintf, strcpy, strcat, gets, and unbounded scanf("%s")
are banned — use snprintf, strlcpy, strlcat, fgets, width-limited scanf. Always
pass buffer sizes explicitly; check the snprintf return for truncation.
Initialize everything. int rc = 0;, char *buf = NULL;, struct foo f = {0};.
Declare close to first use, except where goto-cleanup requires it earlier.
Opaque types at public API boundaries — the struct definition stays in the .c.
Callers go through the API, never into fields.
Header guards use the project prefix (FOUNDRY_ATTENTION_H), with the guard name
repeated in the #endif comment, and extern "C" in every public header.
Asserts document invariants that must never be false if the code is correct. Don't assert things that legitimately fail — I/O, allocation, user input — those get error codes.
Compiler. The standard, modelled on SQLite and comparable C projects:
-std=c11 -Wall -Wextra -Werror -pedantic
plus -Wshadow, -Wstrict-prototypes, -Wformat=2, -Wnull-dereference,
-Wconversion, -fstack-protector-strong. Zero warnings, always.
The build does not yet meet this — it is -Wall -Wextra -std=c11 today. The standard above is the
target the code is held to, not a description of the current Makefile.
Logging
| Level | Use |
|---|---|
ERROR |
operation failed, cannot continue |
WARN |
unexpected but recoverable |
INFO |
major state transitions, operation boundaries |
DEBUG |
decision points — which branch, and why |
TRACE |
entry/exit, raw values, wire data |
DEBUG and TRACE compile out in release builds. Log at boundaries and decisions, never
inside hot loops at INFO or above. Never log secrets, raw binary, or the success of
trivial operations. One line per message, greppable, with timestamp, level, and source
location.
The test: if you would add a printf to debug it, a log_debug should already be there.