cpp-style
Read before writing, modifying or reviewing any C++ code — redundancy (the rule we care most about), modules and files, errors and defense, async, logging, naming, words, templates, API preferences.
- License 许可
- Apache-2.0
- 所属仓库
- clice-io/clice
- 最近更新
- 2026-11-13
- Stars
- ★ 1.4k
- Forks
- 89
01 前言 Front Matter
- name
- cpp-style
- description
- Read before writing, modifying or reviewing any C++ code — redundancy (the rule we care most about), modules and files, errors and defense, async, logging, naming, words, templates, API preferences.
03 SKILL.md 正文
clice C++ Coding Style
Redundancy Is a Defect
This is the convention we care most about. Redundant code is not a style nit — it actively misleads: every guard implies the guarded state can occur, every branch implies it can be taken, every parameter implies a caller needs it. When that implication is false, the reader wastes time defending against ghosts.
- Every branch must be reachable. Before adding a guard or fallback, prove the state can actually occur — construct the input that hits it. If you can't, don't write it.
- No speculative generality. No parameters, options, hooks, or abstraction layers for hypothetical future callers. Add them when the second real caller arrives.
- One way to do each thing. Don't leave an old path alive next to its replacement "just in case" — migrate all callers and delete it in the same change.
- Re-read after every change. Edits leave residue: conditions that
became constant, variables read once, branches that now collapse,
elseafterreturn, a helper with one remaining caller. Fold them before you're done — simplification that removes a concept beats one that merely shortens lines. - Delete, don't comment out. Git history is the archive.
- One behavior difference is one options field. Two behaviors of the
same logic are selected by a boolean in an options struct
(
SemanticsOptions{.all_files = true}), not by a subclass, a dummy collaborator, or a parallel code path. - Cross-cutting invariants get a single writer. Accounting, budgets, ownership and visibility guarantees live in one type whose methods are the only transitions and whose doc comment lists the invariants; before coding, grep every site the invariant touches and funnel them through one helper so a missing site is discoverable. Per-site patches never catch up with a reviewer who re-derives the invariant from the whole diff.
Files & Organization
- clice is one C++20 module,
clice. An interface is a partition,<stem>.cppmbeside its source, named after its layer and then what it is — usually its path undersrc/(module clice:vfs.path;,unittests.*underunittests/);pixi run check-layerschecks the layer. A part grown into a directory keeps its interface there under the part's own name (feature/hover/hover.cppmisfeature.hover), beside its implementation units and the internal partitions they share (feature.hover.card). Sources are implementation units.cpp, entry points.cc(clice.cc,src/driver/*.cc). - A module unit opens with its module declaration, then one
import :<partition>;per partition it names. The partition:support.preludeimportsstd.compat, the C library's names past it (libc) and the third-party module (modules/), and gives the namespaces clice writes their short names inclice—ranges,views,chrono,this_thread,ipc,lsp,protocol,codec,json,fs,sys,async,cli,meta,zest. The primary interface (src/module.cppm) re-exports it, which brings it to every implementation unit; every partition imports it. Code insideclicewrites those names (ranges::sort,protocol::Range,async::task), neverstd::ranges::orkotatsu::ipc::protocol::, and no file declares them again. - No module carries a macro: every compilation force-includes
support/macros.h, which holds clice's own after including the libraries' (modules/macros.h). A unit opens a global module fragment (module;) only for a header no module carries, a platform header. .his left for what a module cannot carry — macros (support/macros.h) and platform headers — with#pragma once, never include guards.- File names are
snake_case. - Every file sees the names of all it imports: a file-local helper can
clash with a name of another partition or lose overload resolution and
ADL to a library function (
llvm::join) no header used to bring in. - File-local helpers: a single one is
static; a cluster of them goes in one anonymous namespace.
Data & Types
structby default, even for types with methods.classonly when there is a real invariant that private access protects.enum classwith an explicit underlying type (e.g.: std::uint8_t); document each enumerator with///when its meaning is not obvious.- Prefer designated initializers (
{.field = value}) for aggregate construction. - West const:
const T&, neverT const&. - A class lays out its public members first, then its private member
functions under one
private:, then its data members under a secondprivate:— functions and fields never interleave. Data members stand one blank line apart, each one's comment on the lines above it, never trailing it.
Errors, Exceptions & Defense
- The project builds with
-fno-exceptions -fno-rtti: nothrow/try/catch, nodynamic_cast/typeid. Polymorphic inspection usesllvm::isa<>/cast<>/dyn_cast<>. - Fallible synchronous operations return
std::expected<T, E>withE=std::stringorstd::error_code; async code uses kotatsu's outcome types.llvm::Erroronly where an LLVM API forces it. assertpins preconditions; impossible branches end instd::unreachable()— neverllvm_unreachable(project convention).- Invalid external input (broken source code, malformed requests) degrades gracefully — empty result, input passed through — it never crashes the server. But graceful degradation is not scattered null checks: per the redundancy rule, defend only against states that can occur. The Debug (ASan + assertions) test surface is the safety net.
- A gap a real workflow can reach that the maintainer accepted rather
than fixed lives in the code, not on the board:
// LIMITATION:where a fix would go, saying what goes wrong and who reaches it.FIXMEis for what is still to be fixed. When a user hits a limitation, it becomes an issue again.
Error Handling (control flow)
- Scope an error variable with an
ifinit-statement when nothing after theifneeds it:if (auto result = foo(); !result.has_value()). - A type with
operator boolneeds no condition:if (auto err = foo()), neverif (auto err = foo(); err). - When the success path needs the variable, declare it as a plain local
and return early on the error — never an init-statement that forces the
success path into an
else.
Concurrency & Async
- Async code is kotatsu coroutines (
async::task,co_await) — no callback style. A public interface may stay synchronous and drive a coroutine internally when the caller has no event loop (seeToolchain). - Review coroutine code by interleavings, not function by function: list
every
co_awaitsuspension point, the events that can fire while it is suspended (crash, respawn, dispatch, cancellation, another coroutine resuming), and trace the shared state through each chain; then check that what the frame captured before suspending is still valid when it resumes. Bugs here are always cross-function interleavings; a function-level read finds none of them. - Cancellation flows down the ownership tree through
task_group::cancel(); tokens and sources exist only to cross an ownership boundary — the serve mode shutdown source inMasterServer, the batch commands' inLifetime(driver/run.cppm). Graceful shutdown is not cancellation — kotatsu has no shield, a cancel cascades all the way down — so a shutdown is a cancellable serving phase bounded bywith_token, then a non-cancellable drain that joins each task in order. cancellation_token::wait()completes by cancelling itself. Awaited directly insidewhen_anyit propagates that cancellation into the parent frame and skips everything after theco_await; wrap it inwith_token(which catches internally) or add.catch_cancel().
Logging
- Log through the
LOG_TRACE/LOG_DEBUG/LOG_INFO/LOG_WARN/LOG_ERRORmacros (std::formatsyntax, source location captured automatically);LOG_FATALlogs and aborts,LOG_ANOMALYandLOG_GUIDANCEare the bug and user-setup channels (all insupport/macros.h). Never write to the outputs ofsupport/loggingdirectly, never print diagnostics to stdout/stderr.
Naming Conventions
- Variables, member fields, function names:
snake_case. Class member fields do NOT use any special suffix/prefix (no trailing_, nom_prefix). - Class names, template parameter names, enum names:
PascalCase. Exception: some class names also usesnake_case— follow the existing style in the project. - Enum values:
PascalCase. - Acronyms stay uppercase in type names:
PCHState,LSPClient,TUIndex— neverPchState,LspClient. - A file-local type is named by its role in the file, without the
words of the file's own subject: in an anonymous namespace,
FoldingRangeCollectorinfolding_ranges.cppisBuilder,ScanOptionsinscan.ccisOptions. A type that is the file's subject itself keeps its name (Lexerinlexer.cpp). - Nothing is a
Collector: the type producing the file's answer is aBuilder; a visitor gathering nodes is a<What>Finder(PackForwardFinder), or is named by what it gathers (SpelledNames); a clang callback that only records what clang reports is a<What>Recorder(CompilationUnit::DiagnosticRecorder). - Overloads, not prefixes, tell one job's variants apart: every
conversion to the LSP form is a
to_protocoloverload (a second form of the same input is named by the form:to_symbol_information), and a feature's answer from the index is an overload of its answer from the AST (semantic_tokens(SourceText&, …)besidesemantic_tokens(CompilationUnit&, …)) — neversemantic_tokens_to_protocolorindex_semantic_tokens. - Doc comments on declarations use
///; the bar for when to write a comment at all is in CLAUDE.md.
Words to Use and Avoid
Identifiers and comments use plain words: the word a newcomer would guess for the thing, not a metaphor or a rare word that needs a glossary (verdict, envelope, ledger). One word means one thing across the codebase — before naming, grep whether the word already means something else nearby, and don't split one concept over two words. A name that its own doc comment has to correct is the wrong name.
These were renamed out of the codebase; don't bring them back:
| Avoid | Say instead |
|---|---|
| row / rows | an index record: occurrence / relation, else entry; a file's whole index data: index / shard / variant; a source line: line |
| verdict | result / decision, whichever the site is |
| claim | one word per site: take, acquire, reserve, cover, report |
| envelope | what it is: TUIndexBlob, the TU index, a container, a header |
| ledger | table / list |
| wire (the serialized form) | serialized / encoded; "in the protocol" for IPC |
| settle | finish / become idle |
| flavor | kind / variant |
| land / landing (a result arriving) | commit / merge / finish |
| sweep | pass / run / scan; for cleanup, remove |
| tier (an encoding choice) | encoding (MaskEncoding); the search ranking's NameRank::tier stays |
| surface (verb), vouch for | report / show, guarantee / confirm |
stamp outside vfs |
version / generation |
| collector | Builder / <What>Finder / <What>Recorder (see Naming Conventions) |
Features, Requests (the server's request entry points) |
the request's handler in LSPClient (server/client), routed by its served project (ServedProject::pick_route); the features are src/feature's |
| projection | what it is: a document's latest compile (DocumentCompile), a result computed from the index (the SourceText overloads of semantic_tokens), the LSP form; only the indexer's step from the semantic map to entries is a projection (project_node) |
switch_source_header (the feature) |
counterparts: the files a file pairs with; only the editor command keeps the action's name (clice.switchSourceHeader) |
| quarantine, barred | the document's crash records (Session's, one FailureRecord per request kind); a record stops its operation |
| strikes | the failure count (FailureRecord::count) |
| license (to retry) | the retry a change or a save allows; the Attempt holding it |
| overtaken (an attempt) | crashed_meanwhile; a task-graph attempt overtaken by new content keeps the word |
| parked (crash records) | nothing: the records live in the document's Session and end with it; a reply a test hook holds, or a waiter on an event, is still parked |
BlameBudget, blaming a PCH pair |
a failure of its consumption (pch_consume_failed), a record with a cooldown policy |
| evidence (kind) | feature::RequestKind |
| ticket (outside a request) | a sequence number (ReindexQueue's seq); Ticket is a request's hold on its buffer |
| superseded (a blob) | previous |
PCHFamily, PCMFamily, PCHBuilder, PCMBuilder, builder (of PCHs, modules) |
the PCH and PCM work (Project::pch_work, Project::module_work) in project/graph/pch, project/graph/pcm; what both share is project/graph/precompiled |
TURunFamily, TU run, unit run |
a unit's compile (Project::compile_unit, UnitRequest, UnitResult, NodeKind::Unit) |
round (of a graph node, of the background index), RoundContext, RoundOutcome |
a graph node's attempt (Attempt, AttemptState, Result); the background index's sweep over its queue is a pass (BackgroundIndex::run_pass); round-trip and rounding keep the word |
| outcome | result, or the more exact word (CompileStatus, CompilationUnit::status); kotatsu's outcome types keep their name |
| lend, lent, lender, lending (a command) | the borrowing side's words: the unit a command is borrowed from (Candidate::from, borrowed_command), the unit whose command a file borrows; a host's command a header compiles under is the host's |
Inferred (a command source) |
CommandSource::Borrowed |
IndexPump, pump, engine (of the index) |
the background index (BackgroundIndex, BackgroundIndex::run_pass) |
SchedulingStack, scheduling stack |
the project: it builds with the event loop and worker pool its owner gives it (Project::loop, Project::pool) |
CommandResolver |
command resolution, members of Project (project/command/resolve: resolve_command, command_identity) |
PendingTable, pending table |
ReindexQueue: the background index's queue with each unit's reason |
TakenDebt, debt (a reindex owed) |
TakenReindex, a reindex |
CommandSnapshot, command snapshot, BuildSnapshot, SavedListings, listings |
SavedMembers (blob kind Members): which databases listed each file, and the provisional members; a unit's command is its index's indexed_command |
ServingSide, serving side (the project asking the server) |
nothing: the project calls no server; it emits signals (BackgroundIndex::on_attempt_finished, Project::on_index_changed, Project::on_metadata_changed) and its owner hands it the open documents as data (OpenFile) |
LiveSources, OpenDocuments, PeerQueries, peers (other projects' queries) |
IndexQuery: the open documents its owner hands it (OpenBuffer, IndexQuery::reading), the other projects it asks (IndexQuery::asking, asked, gather) |
indexer/ (the background index's directory) |
project/background/; index/indexing is the indexer that builds a TU's index |
IncludeEdge (the index's), include_edges; IncludeTree (analysis') |
ResolvedInclude, resolved_includes; ScopedIncludeTree; syntax::IncludeEdge and index::IncludeTree keep theirs |
| family (of graph nodes) | kind (NodeKind) |
| artifact (a PCH or PCM) | precompiled (Precompiled, the PCH and PCM records; the blob key artifacts stays: persisted) |
superseded (a unit's index), mark_behind |
outdated, mark_outdated |
| sentinel (the graph's node for an import no module provides) | unresolved import (unresolved_import) |
| retire, retirement (a unit leaving the build) | dropped: the build stopped compiling it (Project::update_members) |
| enterings | include_chains: every place a unit's compile enters a header, by its include tree |
probe (a header compiled alone, a __has_include lookup) |
the self-containment check; a __has_include check |
header mode, HeaderMode |
the self-containment check's result (SelfContainment, Hosting::self_containment) |
| trial (a header compiled alone) | the self-containment check (SelfContainment); the precompiled records' trials key keeps its spelling |
HostTree |
PreprocessedTree (Hosting::trees) |
| lexical chain (the chain a header's context is cut along) | nothing: a chain comes from the host's include tree; the lexical scan only finds the candidate hosts |
| standalone header (indexed on its own) | nothing: only the project's units are indexed, a header through the units reading it |
| workspace (one project or its root) | project, project root (Settings::project_root); "workspace" is only what the editor opens, its folders; the --workspace flag and ${workspace} in clice.toml keep their spelling |
IndexReader, ProjectReader, bootstrap, loading a project |
Project::open with its OpenOptions; a Project owns the read side of its index (index_query, freshness), and a command-line run reads through the OpenedProject the driver opens |
| heir, retire (a project), weigh (the databases) | successor, stop, reload_databases |
Selection, pin, pinned (a context) |
ContextChoice, "choice" in prose; a choice that stops holding stays inactive (MasterServer::ChoiceEntry) |
ProjectServer |
ServedProject |
ASTFamily, Dispatcher, Invalidator, FileTracker, EditorContext |
what the served project does, in its files: compiling the open documents (compile.cpp), what their diagnostics show (diagnostics.cpp), answering and asking the workers (answer.cpp), handling changes (changes.cpp), their commands (served_project.cpp) |
Config, config (what clice.toml holds) |
Settings (load_settings, project.settings); "configuration" is only the build configuration a rule's tag names (debug, release), and "configuration file" the clice.toml that holds the settings |
epoch (a document's compile), mark_not_current |
the AST node's generation (TaskGraph::generation): a compile is current while the one it ran at (DocumentCompile::generation) is the node's (ServedProject::current) |
a worker's own LRU, max_documents, document_unloaded, EvictedParams; the pool's owner table |
the pool's residency (Residency: a document's assignment, its AST resident or not, WorkerPool::resident, max_resident); a worker evicts only when told (EvictParams) |
| hand over (an index) | send_index |
ServingMode (IndexOnly, Escalated), escalate, escalation, investment (in an open document) |
whether the document is compiled (ServedDocument::compiles), from now on (ServedProject::start_compiling); otherwise only the index answers it |
full_lex, await_cold_attempt (what a request asks of routing) |
RouteOptions::lexes_whole_buffer, waits_for_index; needs_ast |
MasterServer::route, reroute, routing a file to a project |
the project a file belongs to: MasterServer::pick_owner, update_owners; a route is only what answers a request (pick_route, Route) |
uri_to_path |
feature::to_path, beside feature::to_uri |
| retire, retiring (a worker slot) | a scale-down (scheduler::ScaleDown, WorkerPool::scale_down_worker, scaling_down, SlotState::ScaledDown) |
Dead (a worker slot) |
GivenUp: its crash budget ran out, and only its revival brings it back |
foreground_pulse |
WorkerPool::user_activity, as the scheduler's |
BuildProbe, probe (test instrumentation) |
the test hooks: TestHooks (worker/test_hooks), the server's test-only requests in server/test_hooks.cppm |
| culprit, blame (a worker death) | the requests a death names (WorkerDeath::named); a request charged with a death |
| preempted (a worker the pool killed) | killed_on_purpose; preempt is the scheduler asking a request to stop |
WorkerCrashInfo, on_crash (the pool's callback) |
nothing: tests read the slot, the user is told by guidance when every worker of a kind is given up; kotatsu's sys::on_crash keeps its name |
ScopedTimer, ms_f |
Timer, Timer::precise_ms |
print_argv, host_quoting |
join_command; sys::cmd::native_quoting |
Domain words with one meaning each — use them only in that meaning:
- spelling: how a path is written — the anchored absolute path,
..kept, handed only to the identity functions or to clang. - host: the source file whose compile context a header borrows.
- slot: one worker's place in the worker pool.
- stamp: in
vfsonly, a file's change fingerprint (size + mtime + ctime).
Templates, Type Traits & Concepts
std::remove_cvref_t<T>only whereTcan be deduced as a reference: a forwarding referenceT&&.T,T&andconst T¶meters, class template parameters and return types never deduce one.- A type trait is a variable template specialized directly
(
template<> inline constexpr bool is_my_type_v<MyType> = true;), never a variable template wrapping a class template's::value. - A concept over a trait uses
Tas is (concept MyTrait = is_my_type_v<T>;) unless it deliberately sees through references and cv-qualifiers; the caller passes the right type.
String Literals
- Prefer C++11 raw string literals
R"(...)"over escaped strings. Avoid\",\\,\nin string literals when a raw literal is cleaner.
Style
- Definitions stand one blank line apart, which clang-format leaves to the
writer, so that a
KANNOTATE_TYPEsits directly above the type it annotates, the type's doc comment above both. - Prefer
[[maybe_unused]]over(void)for intentionally unused variables or parameters. - Arithmetic counters step with
x += 1/x -= 1, inforheads too — never++/--; split fused forms likeif(++x > n)into two statements. Iterators and other types without+=are exempt. This applies to code you write or touch, not as a repository-wide sweep.
Modern C++ Usage
- Use C++20/23 APIs whenever possible. Do NOT use
<iostream>facilities (std::cout,std::cin,std::cerr, etc.). Also do NOT use C-style I/O (printf,fprintf, etc.). - Prefer
ranges/viewsAPIs over raw loops and traditional<algorithm>calls. - Range helpers come from the standard library, never LLVM's
STLExtras:ranges::containsnotllvm::is_contained(a set or map uses its own.contains()),ranges::any_of/find_if/sortnotllvm::'s,views::enumerate/zip/reverse/transformnotllvm::enumerate/zip/reverse/map_range. Onlyllvm::concat(C++26 in the standard) andllvm::interleavestay. - Prefer LLVM's efficient data structures (e.g.,
llvm::SmallVector,llvm::DenseMap,llvm::StringMap,llvm::StringRef) over theirstdcounterparts when appropriate. - The operating system is reached through kotatsu: paths, files and directories through
fs(kotatsu::fs), the environment, programs and processes throughsys(kotatsu::sys), neverllvm::sysorstd::filesystem. LLVM stays where an interface takes its types —llvm::vfsfor clang's file manager, allvm::MemoryBufferclang or the index adopts, filled from anfs::File. Crashes aresys::on_crash's and the tracesys::write_stacktrace's, never LLVM's signal handling: clang's outputs are not registered for removal on a crash, which would install LLVM's handlers over them. - Where a token is, which file holds it and what comes before it are the compile's questions: code holding a
CompilationUnitasks it (written_at/seen_at/produced_at, the ranges,before,expansion_level, the tokens), never clang'sSourceManager, whose answers miss what the unit maps (the PCH's copy of the focus file, the NUL at a completion's point). Each member's doc comment is its contract and a unit test holds it to it; a question the unit lacks is added there, with both. Helpers handed only aDeclor anASTContextkeep clang's.
Parameter Passing Preferences
- For string parameters, prefer
llvm::StringRef>std::string_view>const std::string&. - For array/span parameters, prefer
llvm::ArrayRef>std::span>const std::vector&.
04 同仓库更多 Skills
write-tests
Read before writing or modifying anything under tests/ or unittests/, or the tools' own tests — the case workflow, unit tests, hard rules and known pitfalls.
upgrade-llvm
Read before upgrading xclang/LLVM (arg = the xclang version, e.g. 23.1.2.1), touching MODULE.bazel, .bazelrc, BUILD.bazel or bazel/, or regenerating modules/ — after a library bump, or when clice needs a header or macro it does not export.
triage
Use when asked to triage issues — classifies untriaged issues with codex, validates the labels, and returns proposed labels plus an activity digest. Read-only; runs in a forked context.
translate-docs
Read before translating, reviewing or editing any docs/zh page — clice's glossary and page conventions, on top of the shared translation rules of clice-io/docs.
test
Run the clice test suites, on a box unless a local run is truly necessary; also how to test on Windows. Optional args = suite (unit | cases | all, default all) build type (Debug | RelWithDebInfo, default RelWithDebInfo) and the box id the build ran on. Runs in a forked context; only a digest returns.
resolve-comments
Use in each watch round of an open PR — pulls its unresolved review threads, fixes them in the worktree, resolves them, and returns a compact summary.