Conventions for code, documentation, and other project artifacts in the succinctly project. Each item has a unique ID for easy reference.
Much of the detailed how lives in the .claude/skills/ skills and in
CONTRIBUTING.md; this guide is the stable-ID index that lint
suppressions, code review, and those docs can point at unambiguously.
Before writing or reviewing code, documentation, or other project artifacts, identify which tags apply to the changes and search this file for those tags. Each rule has a Tags line immediately after its heading.
Search command: grep "Tags:.*<tag>" docs/STYLE_GUIDE.md returns matching rule headings.
| When you are… | Search for tags |
|---|---|
| Adding or reorganizing a module / file | module-organization, simd |
| Adding or changing a SIMD implementation | simd, unsafe, code-style |
Writing unsafe (SIMD intrinsics, raw pointers) |
unsafe, simd |
Suppressing a lint (#[allow(...)]) |
code-style, lints |
| Adding platform-gated / reference / bench-only code | code-style, simd, lints |
| Documenting a public item | documentation |
| Writing or updating tests | testing |
| Updating benchmarks or benchmark docs | benchmarks, documentation |
| Adding or editing a markdown table | documentation |
| Writing commit messages | commits |
Touching no_std boundaries or std-gated code |
module-organization |
Walking DocumentFields / DocumentElements |
code-style, testing |
| Reviewing code for style compliance | All tags relevant to the changed code |
Tags: meta
A new convention needs to be added to this style guide.
Assign the next sequential ID (currently next is STYLE-0014) and include:
- A Tags line immediately after the heading — a comma-separated list of category labels from the tag vocabulary below.
- Three subheadings:
- Situation — when this rule applies
- Guidance — what to do (with examples where helpful)
- Motivation — why this rule exists
Tag vocabulary (extend as needed):
| Tag | Covers |
|---|---|
meta |
Style guide structure and process |
module-organization |
File layout, module tree, visibility, no_std boundaries |
simd |
SIMD directory layout, feature detection, platform gating |
unsafe |
Unsafe code and // SAFETY: documentation |
code-style |
Imports, constants, function shape, lint hygiene |
lints |
Lint suppression policy and Cargo.toml [lints] |
documentation |
Doc comments, markdown, benchmark tables |
testing |
Test structure, property tests, feature-gated tests |
benchmarks |
Benchmark running and result documentation |
commits |
Commit message format and discipline |
A rule may have multiple tags. Items are ordered by ID. Do not group items under section headings; use tags for categorisation instead.
Consistent structure makes the guide scannable, and stable IDs allow code review comments, lint suppressions, and cross-references to point at specific rules unambiguously. Tags replace section headings so items can stay in strict ID order without being shuffled between sections when categories overlap.
Tags: module-organization, simd
Adding a new module, or adding a platform-specific SIMD implementation to an existing format module.
Use the named-file layout (Rust 2018+): a module with submodules lives in a file named
after the module alongside a directory of the same name (json.rs + json/). Do not
introduce new mod.rs files for a module root — but note that simd/ subdirectories in
this crate do use a mod.rs as their dispatch file (see below).
Each format module that has SIMD acceleration owns a simd/ subdirectory with one file
per instruction set, plus a mod.rs that performs runtime dispatch. The instruction-set
files present depend on what the module actually uses; the JSON module is the fullest example:
src/json/
├── ...
└── simd/
├── mod.rs # runtime dispatch + feature detection (see STYLE-0003)
├── x86.rs # x86_64 SSE2 kernel + dispatch entry
├── avx2.rs # AVX2 kernel
├── neon.rs # ARM NEON kernel
└── sve2.rs # ARM SVE2 kernel
Other modules carry the subset they need — e.g. src/yaml/simd/ adds broadword.rs (a
portable SWAR fallback) and omits avx2.rs; src/dsv/simd/ includes sse2.rs. Shared,
format-agnostic SIMD helpers live in src/util/simd/.
Every SIMD path must have a scalar (or broadword) fallback reachable when no supported
instruction set is detected. Feature-gated fallback selection is exposed through the
broadword-yaml and scalar-yaml Cargo features (see Cargo.toml).
One file per instruction set keeps each kernel's #[target_feature] scoping local and makes
git log, editor tabs, and search unambiguous. Co-locating kernels under a simd/ directory
with a single mod.rs dispatcher means the platform-selection logic lives in exactly one
place per module, and adding a new instruction set is a new file plus one dispatch arm rather
than edits scattered across the module.
Tags: unsafe, simd
Writing unsafe — almost always calling a SIMD intrinsic, or a proven-in-bounds slice
access on a hot path.
unsafe is permitted in this crate (unlike a pure-safe library) because SIMD intrinsics
are unsafe fn. Keep it disciplined:
-
Every
unsafeblock carries a// SAFETY:comment immediately above it, naming the invariant that makes it sound. For an intrinsic call that invariant is normally the feature-detection guard that gates the call:// SAFETY: has_fast_bmi2() verified BMI2 is available on this CPU. let mask = unsafe { bmi2::toggle64(quotes) };
For a bounds-eliding access, cite the check that established the bound:
// SAFETY: We verified bounds above. let word = unsafe { *self.words.get_unchecked(i) };
-
The
unsafeintrinsic kernel is only ever reached through the feature-detected dispatcher (see STYLE-0003). Never call a#[target_feature]function without the matchingis_*_feature_detected!guard on the path to it. -
Prefer a safe abstraction where one exists at no cost — reach for
unsafefor the intrinsic itself and the innermost hot loop, not for surrounding bookkeeping.
The clippy lint undocumented_unsafe_blocks enforces item 1; enabling it project-wide is
tracked in the Cargo.toml [lints] work (see STYLE-0004 and issue #205). Until then, a
missing // SAFETY: comment is a review defect even though it does not fail the build.
Intrinsics are unsafe because the caller, not the compiler, guarantees the CPU supports the
instruction. Writing the guarantee down at the call site as // SAFETY: turns an implicit
assumption into a reviewable claim, and keeps the "is this feature actually detected?" question
answerable without tracing the whole call graph.
Tags: simd
Selecting which SIMD kernel to run for the current CPU.
Detect features at runtime, not only at compile time, so a single binary runs optimally on
whatever CPU it lands on. Use the standard macros and dispatch in the module's simd/mod.rs:
#[cfg(target_arch = "x86_64")]
if std::is_x86_feature_detected!("avx2") {
// SAFETY: guarded by the detection above.
return unsafe { avx2::scan(input) };
}- Guard x86_64 kernels with
is_x86_feature_detected!and aarch64 kernels withis_aarch64_feature_detected!. - Annotate each intrinsic kernel with the matching
#[target_feature(enable = "...")]. - Cache the detection result where dispatch is hot rather than re-querying per call.
- Feature detection needs
std; theno_stdbuild (see STYLE-0011) falls back to the portable path. Some paths are additionally switchable via environment variable (e.g.SUCCINCTLY_SVE2) — document any such variable where it is read.
Runtime detection lets one artifact ship the fastest path for each machine instead of forcing a
target-cpu build. Keeping detection in the mod.rs dispatcher (rather than sprinkled through
kernels) means STYLE-0002's safety argument reduces to "the guard is right above the call."
Tags: code-style, lints
Adding a #[allow(...)] (or considering a project-wide lint override).
Every lint suppression must be traceable to a documented reason. Two cases:
-
Per-item suppression — annotate the specific item and append a citation comment naming the STYLE rule that justifies the suppression, plus a one-line specific reason:
#[allow(clippy::type_complexity)] // STYLE-0004: build-time index tuple; a named struct // would add indirection to a build-only path. fn build_bp_index(...) -> (u64, u64, /* ... */) { ... }
When the suppression is an instance of a more specific rule, cite that rule instead — e.g.
dead_codeon conditionally-compiled code cites STYLE-0005, not STYLE-0004. -
Project-wide policy — lints that should be allowed (or denied/warned) across the whole crate belong in a
Cargo.toml [lints]table with a justification comment per entry, not in a blanket crate-level#![allow(...)]that hides warnings wholesale. Adding that[lints]table (withundocumented_unsafe_blocks, a pedantic-clippy baseline, and an explicit unsafe policy) is tracked in issue #205; when it lands, each allow comment there cites the STYLE rule that motivates it, exactly as the per-item comments do.
Do not add a bare #[allow(...)] with no comment, and do not silence a warning you could fix.
An uncommented #[allow] is a decision with no recorded rationale — the next reader cannot tell
whether it is load-bearing or leftover. A STYLE-ID citation turns each suppression into a link to
the policy that permits it, so a reviewer verifies "does this match the cited rule?" instead of
re-deriving the justification. Centralising project-wide decisions in [lints] keeps the policy
auditable in one place.
Tags: code-style, simd, lints
A dead_code warning fires on code that is intentionally not reachable in the current build.
#[allow(dead_code)] is expected and correct for these recurring cases in this crate. Cite
STYLE-0005 and say which case applies:
| Case | Why it looks dead | Comment |
|---|---|---|
| Platform-gated | Compiled out on the current target_arch / feature combo (e.g. a NEON kernel in an x86_64 build) |
// STYLE-0005: platform-gated (aarch64 NEON) |
| Reference / fallback impl | A scalar or broadword implementation kept next to the SIMD one for correctness comparison or as the fallback path | // STYLE-0005: scalar reference impl kept for correctness |
| Test / bench / future-use | Helpers reachable only from tests or benchmarks, or deliberately kept for imminent future use | // STYLE-0005: used in tests |
| Retained field / complete set | A struct field kept for symmetry or a deliberately complete constant set (e.g. all jq exit codes) where only some entries are read today | // STYLE-0005: complete jq exit-code set; not all emitted yet |
Use a per-item #[allow(dead_code)] where possible. A module-level #![allow(dead_code)] is
acceptable only for a module that is entirely one of these cases (e.g. a bench-helper module) —
give it a //! doc line stating so.
Do not use #[allow(dead_code)] to silence genuinely unused code — delete that instead.
Platform-gated SIMD, paired scalar/SIMD implementations, and deliberately complete API/constant
sets mean a large amount of code is legitimately unreachable in any single build configuration, so
dead_code would otherwise be noise. Naming which case applies keeps "expected-dead" distinguishable
from "should be deleted," so the allow does not become a place for real dead code to hide.
Tags: documentation
Adding or updating documentation on a module, type, or public function.
- Module-level docs — each module file opens with a
//!summary. - Item-level docs — every public type, field, variant, and method gets
///. - Summary line style — third-person singular present indicative per
RFC 505
(
/// Returns the k-th set bit., not/// Return ...), ending with a period. - Runnable examples — non-trivial public functions should carry a
# Examplesblock. These compile and run undercargo test, doubling as regression tests.src/lib.rsalready carries such an example forBitVec; keep new public entry points to the same bar.
Third-person summaries match the standard library and rustdoc output. Doc examples are compiled,
so they cannot silently rot, and they document the intended call shape better than prose.
Tags: testing
Writing a new test.
- Unit tests live in a
#[cfg(test)] mod testsblock at the end of the source file, withuse super::*;. - Property tests use
proptestand live intests/(tests/property_tests.rs,tests/bp_properties.rs); integration tests likewise live intests/. - Large/expensive tests are gated behind Cargo features (
large-tests,huge-tests,mmap-tests) so the defaultcargo teststays fast. - Snapshot tests use
instawhere output stability matters.
For deeper patterns and anti-patterns (e.g. asserting on real behaviour rather than
tautologies), see the testing skill.
The mod tests convention gives tests access to private items and keeps them next to the code
they cover. Feature-gating the multi-gigabyte tests keeps the common test run cheap while leaving
the heavy coverage available on demand.
Tags: benchmarks, documentation
Adding, updating, or documenting benchmark results.
- Regenerate, don't hand-edit numbers. Produce results with the documented command
(
succinctly bench run <bench>, or the relevantcargo bench) and paste the actual output. - Run benchmarks sequentially, never concurrently — benchmarks need exclusive CPU, and overlapping runs produce meaningless numbers.
- Label every result table with the CPU/platform it was measured on (the existing tables use headings like "Apple M1 Max", "ARM Neoverse-V2"). Numbers without a platform are not comparable.
- Keep the summaries in sync — the performance tables in CLAUDE.md and the detailed pages under docs/benchmarks/ must not diverge; update both when a number changes, and note the regeneration command beneath the table.
The benchmark-docs skill codifies the per-platform
update procedure.
Benchmark numbers are only trustworthy if they are reproducible and attributed to hardware.
Sequential execution avoids the biggest source of noise; per-platform labelling stops readers
comparing an M4 result against a Neoverse one; keeping CLAUDE.md and docs/benchmarks/ in lock-step
prevents the headline summary from drifting away from the detail.
Tags: documentation
Creating or editing a markdown table in any project doc.
Pad every column so cell borders line up in the raw source — the header, the separator row, and all body rows share one column width. Prefer this:
| Structure | Overhead |
|--------------|----------|
| BitVec | 3–4% |
| BalancedParens | 6% |→ align to:
| Structure | Overhead |
|----------------|----------|
| BitVec | 3–4% |
| BalancedParens | 6% |The markdown-tables / format-md-tables skill
automates this — run it after editing a table rather than aligning by hand.
Fixed-width tables are readable in the raw markdown (not just when rendered), so diffs stay legible and a mis-aligned cell is an obvious signal that a column changed. Automating the alignment keeps it from being a manual chore.
Tags: commits
Writing a commit message.
Use Conventional Commits: <type>(<scope>): <description>
with an optional body and footer. Types in use: feat, fix, docs, style, refactor, perf,
test, chore. Scope names the touched area — see
.omni-dev/scopes.yaml for the canonical, enforced list. For
performance commits, put the measured speedup in the body. Full detail and examples are in
CONTRIBUTING.md; the
commit-msg skill analyses a diff and drafts a message.
A consistent, machine-parseable commit format keeps git log scannable, lets tooling derive
changelogs and scopes, and makes git bisect and history archaeology reliable.
Tags: module-organization
Adding code to the core library (anything outside the CLI / bin/).
The core crate is no_std. Preserve that:
- The crate declares
#![cfg_attr(not(any(test, feature = "std")), no_std)]andextern crate alloc;insrc/lib.rs— depend onalloc(Vec,String,Box), never onstd, in core modules. - Gate any genuinely
std-only functionality (notably runtime feature detection, threads, I/O) behind#[cfg(feature = "std")], and provide ano_stdfallback path. - CLI-only code lives behind the
clifeature (which pulls instd) — heavystddependencies belong there, not in the core modules. - Verify with
cargo build --no-default-features(andcargo testusesstd, since tests enable it via thecfg_attrabove).
no_std support lets succinctly be used in embedded and WASM contexts where std is unavailable.
The guarantee is easy to break accidentally with a stray std:: path, so keeping the std surface
behind explicit feature gates — and testing the default-features-off build — makes the boundary
enforceable rather than aspirational.
Tags: code-style, testing
Writing or editing a function in src/jq/eval.rs or src/jq/eval_generic.rs that takes a
live optional: bool and materializes a borrowed document value — any call to the
to_owned* family (to_owned, to_owned_cursor, to_owned_with_cursor,
to_owned_all, to_owned_key_shape, …) or to collect_cursors_checked.
Every such call site must either route its error through the shared suppression
machinery, or carry a // STYLE-0012: citation with a one-line specific reason —
exactly the traceability STYLE-0004 requires of a lint suppression.
Routing, in order of preference:
// eval.rs
let owned = to_owned_or_suppress!(&value, optional);
let items = to_owned_vec_or_suppress!(elements, optional);
Err(e) => return suppress_or_raise(e, optional),
// eval_generic.rs
let owned = owned_or_suppress!(to_owned(&value), optional);
Err(e) if suppresses(&e, optional) => GenericResult::None,Never hand-copy the predicate. optional && !e.is_decode_failure() has one definition,
eval::suppresses; it drifted out of sync twice already (#1902, #1934), and #2334 found
four more hand-copies of it.
Exempting, when the site genuinely must not consult optional:
// STYLE-0012: `map(f)` is `[.[] | f]` -- array construction, atomic in jq.
QueryResult::One(v) => match to_owned(&v) { .. }When the whole parameter is dead, prefer the stronger marker: name it _optional.
The unused-variable lint then enforces the claim, and the audit skips the function
entirely — this is how builtin_recurse_f/builtin_recurse_cond record their #1953
exemption.
Where the routing has to sit. The audit accepts a suppress_or_raise/suppresses(
that appears below the call, within the same function, and before the next materialization
site. That covers the ordinary shape, where a Result is collected in one statement and
folded in the next. Routing that sits further away — a deferred unwrap inside a closure
(builtin_contains/builtin_inside, #1800), a fold shared by several sibling arms
(builtin_add), a control handed out to a caller (finish_fork, sort_family_control) —
is real routing, but the audit cannot see it: say so in a // STYLE-0012: marker naming
where it happens.
Check that the shared point really is the materialization's error channel before routing
to it. sort_family_control looked like one and is not: it is also where
sort_key_generic hands out the user's own key-filter errors, so routing there would have
suppressed sort_by(error("x")) — a suppression optional never authorised — while doing
nothing at all for the decode failure it was written for (#2334 review). A marker saying
"raises regardless, and here is why routing would be wrong" is the correct answer to that
shape, not a route. The window is deliberately not widened to guess at these; a window large
enough to find them was measured picking up a suppresses(..) from neighbouring code and
silently excusing a genuine gap.
tests/jq_optional_suppression_audit.rs enforces this and names the exact file:line
of anything unrouted and unmarked.
If it fails on code you did not write. It will, and that is working as intended: a
rebase that brings a new materialization site into either evaluator fails the audit on the
branch that rebased, not the branch that wrote the site. (It happened on #2334's own first
rebase, with #2326's pending_key_stream_control arm.) Adjudicate it the same way as any
other site — route it, or mark it with the reason — and say in the commit message that it
arrived by rebase. Do not raise WINDOW_AFTER, lower the vacuity floors, or add the
function to an exclusion list to get past it.
What the audit does not cover. It keys off the optional: bool parameter, so a helper
that materializes while its caller holds the optional is invisible to it —
eval_generic.rs's path_step_generic, whose collect_cursors_checked()? propagates to a
caller that decides, is the current example. That is deliberate scoping, not an oversight:
the decision genuinely belongs to whoever has the flag. If you add a materializing helper
of that shape, say in its doc comment which caller adjudicates its error, since the audit
cannot.
This rule exists because the same bug shipped three times — #2231, #2280, #2327 — each round's review finding sites the previous sweep missed, several of them the direct sibling of a function that same PR had just fixed in the other evaluator.
The reason it recurred is that the invariant is invisible at runtime. suppresses is
optional && !e.is_decode_failure(), and every error the materialization family can raise
is decode_failure-tagged today, so routing a site and leaving it bare produce identical
output. No behavioural test can fail; a missed site costs nothing until an auditor happens
to read that exact function. #2327's own pinning test asserts the same exit code with and
without a trailing ? — it records the non-difference and by construction cannot detect a
regression.
Centralising the check did not stop the omission, because nothing forced a call site
through it. A static audit does, and a // STYLE-0012: marker turns each deliberate
exemption from prose scattered across two 100k-line files into something greppable that
the next sweep does not have to rediscover by eye. The runtime half of the guard is
EvalError::debug_assert_materialization_error, which fails loudly on the day the
"decode failures only" premise stops holding and the routing starts to matter.
Tags: code-style, testing
Writing or editing a walk that unconses [DocumentFields] or [DocumentElements] and
validates what it finds -- any of the to_owned*_at_depth family, the -e/lazy
materializers, document.rs's own key walks, or the CLI's --input-format json bridge.
Do not hand-copy the validation. Route through the shared definitions:
// The whole member rule: #1642/#1194 key resolution + #1677 delimiters.
let key = field.checked_key(&f, &map, &mut guard, is_first)?;
// The delimiter half alone, for a walk with no display-key map to guard,
// or one that must not allocate a `String` per key (#2061).
if !field.delimiters_ok::<F>(is_first) { return Err(f.malformed_member_error()); }
// Array elements (#1677); `element_gap_ok_at` when you still need the position.
if !elem_cursor.element_gap_ok(is_first) { return Err(elem_cursor.malformed_delimiter_error()); }
// The post-loop tail: `{,}`/`[,]` (#2211) and `{"a":1,}`/`[1,]` (#2243).
child_tail_gap_ok(last_elem.as_ref(), b']')?; // no container cursor in hand
container_tail_gap_ok(cursor, last_elem.as_ref(), b']')?; // container cursor in handA walk that validates its members must also validate its tail. That half is enforced
separately, by style_0013_tail_gap_is_routed_or_exempted, and exempted separately with
// STYLE-0013-TAIL: -- a // STYLE-0013: marker does not exempt the tail.
The two are split because they fail independently and the tail half is the one that gets forgotten: #2349's fix hand-copied both halves, and it was the member half that got the attention. Five of the six walks the tail rule currently exempts already carried a member marker, so a shared marker would have let a decision nobody made stand in for one that matters.
The tail rule asks nothing of a walk that does not validate members -- the unchecked
len/keys/collect_cursors fast paths, json::light's streaming writers and the
YAML-only walks are all out of scope without needing a marker each. If your walk does
check its children, end it in a tail helper:
tail_gap_ok(cursor, last.as_ref(), b'}')?; // cursor or not, dispatches for you
container_tail_gap_ok(c, last.as_ref(), b'}')?; // holds a container cursor
child_tail_gap_ok(last.as_ref(), b'}')?; // children onlyPrefer container_tail_gap_ok/tail_gap_ok when a container cursor is in scope:
child_tail_gap_ok is documented as unable to see the zero-child {,}/[,] case, and
hand-rolling its one-armed body inherits that blind spot -- which is exactly how #2594's
live divergence survived #2211.
A site that genuinely cannot route carries a // STYLE-0013: citation with a specific
reason -- the same traceability STYLE-0004 requires of a lint suppression and STYLE-0012 of
an unrouted materialization. "It is fine" is not a reason; name what the site needs that the
helper cannot give, or the issue that tracks closing the gap. The exemptions in the tree
today are json::light::stream_json_pretty (needs the resolved position, hence
element_gap_ok_at), eval_generic's validate-only walk (lazy key map, #2061 -- its
delimiter checks were the #2349 fix, added directly rather than via this route, for the
same lazy-key reason), lazy.rs's missing trailing-gap check (#2358), the
comment-preserving walk (YAML-only in production), and yq_runner.rs's two
YamlCursor-native key walks, yaml_to_owned_value and validate_yaml_display_keys
(no DocumentField to route through and no delimiters to check; they call
resolve_display_key for the shared key rule, #2519).
This rule exists because the same omission shipped four times. The member rules accumulated
over #1677, #2211 and #2243/#2262, and each round added its check to some walks and not
others -- #1975 found the CLI bridge with none of them, and #2349 is the wrong-answer bug
the drift finally produced in select/sort_by/path().
Extraction alone had already been tried and had already failed to hold:
[DocumentCursor::element_gap_ok] was pulled out by #1597's review for exactly this reason,
and five sites went on hand-rolling its four lines anyway -- including two written after
it existed. Centralising a check does not stop the omission, because nothing forces a call
site through it. That is the same finding STYLE-0012 records, and it has the same answer: a
static audit, plus a greppable marker at each deliberate exemption so the next sweep does
not have to rediscover it by eye.
Behavioural tests could not close this class either, and not by bad luck. The delimiter
rules are JSON-only -- every one is a true-returning trait default for YAML -- so a walk
reachable only from YAML shows no difference at all, and a walk whose corrupt subtree is
merely validated rather than emitted shows none either. #1962's cross-site consistency net
did drive three of the sites, and still missed this: its FuzzMalformation alphabet has
only decode-failure, structural and collision variants, and its generator emits only
well-formed containers, so it could not express a delimiter or gap corruption at any case
count (#2350). A static audit does not depend on a corruption being reachable, expressible,
or observable.