From dca86f0bf3b2bf69b696e94c6a7d62d05e8e06ef Mon Sep 17 00:00:00 2001 From: Caitlin Ross Date: Tue, 14 Jul 2026 18:09:40 -0500 Subject: [PATCH 1/2] doc: add CLAUDE.md guidance for Claude Code --- CLAUDE.md | 164 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..5eb5a3a8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,164 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +CODES is a parallel discrete-event simulation framework for HPC networks, workloads, and +storage, built on ROSS (a PDES engine with optimistic synchronization). ROSS is an +**external dependency**, not vendored: CMake finds it via `find_package(ROSS CONFIG)`, so +an installed ROSS must be reachable through `ROSS_ROOT` or `CMAKE_PREFIX_PATH`. CI pins a +specific ROSS commit (`ROSS_REF`) reciprocally with a CODES pin in ROSS — see +`doc/dev/ci.md` before bumping either. + +## Build + +Presets (CMake ≥ 3.21, Ninja) are the standard path; build trees land in +`build//`: + +```bash +export ROSS_ROOT=$HOME/ross # or CMAKE_PREFIX_PATH +cmake --preset debug +cmake --build --preset debug +ctest --preset debug +``` + +Committed presets: `debug`, `release`, `relwithdebinfo`, `coverage`, `asan`, `ubsan`, +`full-ci`, `full-local-{debug,release}` (the `full-*` ones force all heavy optional deps — +SWM/UNION/DUMPI/Torch/ZeroMQ — on; plain `debug`/`release` auto-detect and skip missing +ones). A git-ignored `CMakeUserPresets.json` may add user presets inheriting these (e.g. +ones pointing `CMAKE_PREFIX_PATH` at a sibling ROSS checkout's install tree). + +Do not set `CC=mpicc` — MPI is found via `find_package(MPI)`. + +## Tests + +```bash +ctest --preset debug # whole suite (what CI runs) +ctest --test-dir build/debug -R # subset by name, e.g. -R example-ping-pong +ctest -N # list without running +DONT_DELETE_TEST_DIR=1 ctest -R # keep the staged run dir for inspection +./build/debug/tests/codes-config-compiler-test --gtest_filter=ConfigCompiler.* # gtest direct +``` + +Two kinds of test live in `tests/` (see `tests/README.md`, which is authoritative): + +- **Unit tests**: GoogleTest (vendored in `thirdparty/googletest/`), serial, no MPI, + `-test.cxx` + 3-line CMake registration. +- **Model/integration tests**: run a model under `mpiexec` via helper functions in + `tests/CMakeLists.txt` — `codes_add_run_test` (smoke: clean exit + marker line), + `codes_add_equivalence_test` (determinism: identical `Net Events Processed` across + runs), `codes_add_lpio_equivalence_test` (strongest: per-LP `lp-io` diff between two + configs; proves `.conf`-vs-`.yaml` twins), `codes_add_config_equivalence_test` + (marker-based fallback when a model emits no reproducible lp-io). Prefer the helpers; + only custom-assertion tests go in the `test-shell-files` shell-script list (those + scripts get `$bindir` and generate configs from `*.template.conf.in` via `envsubst`). +- Long UNION workload tests carry the `nightly` label; PR CI excludes them. + +## Formatting (CI-enforced) + +CI runs **clang-format 20** with `--dry-run --Werror` over every `*.c/*.h/*.cpp/*.hpp/*.cxx` +under `src codes doc tests` — whole files, not just diffs. Different clang-format major +versions produce subtly different output, so use major version 20 (check +`clang-format --version`; stop and say so if it isn't 20 rather than formatting anyway). + +**Commit protocol — apply this to every commit that touches C/C++ files:** + +1. Before committing, run `clang-format -i` on the touched `.c/.h/.cpp/.hpp/.cxx` files + and verify they pass the CI check: + ```bash + clang-format --dry-run --Werror + ``` +2. Include the formatting in the same commit as the change. Never create a separate + "apply clang-format" / "fix formatting" commit — every commit must pass the format + check on its own. +3. Never hand-format to satisfy the check; the formatter is the source of truth. + +An opt-in repo hook runs the same check at commit time — enable once per clone with +`git config core.hooksPath scripts/git-hooks`. + +Other conventions (`doc/dev/conventions.md`): `snake_case` everywhere, `.cxx` for C++ +(never `.C`), `#ifndef CODES_..._H` guards (no `#pragma once`), Doxygen `/** */` on public +headers added incrementally (a header needs a `/** @file */` block or its symbol comments +are silently dropped). Commit messages use a lowercase area prefix (`tests:`, `fix:`, +`dragonfly-dally:`, `doc:`). + +## Running a model + +Model binaries take ROSS options, then `--`, then the config file: + +```bash +mpirun -np 3 build/debug/doc/example/tutorial-synthetic-ping-pong \ + --sync=3 --num_messages=100 -- build/debug/doc/example/tutorial-ping-pong.conf +``` + +`--sync=1` sequential, `--sync=2` conservative, `--sync=3` optimistic, `--sync=6` reverse- +handler checks (see Determinism below). Configs come in two equivalent formats chosen by +extension: legacy `.conf` (LPGROUPS/PARAMS) and YAML/JSON (`doc/dev/yaml-config.md`); the +YAML front-end (`src/modelconfig/`, ryml vendored in `thirdparty/rapidyaml/`) compiles to +the same internal representation, and equivalence tests hold the twins together — **edit +both when changing a config**. + +## Architecture + +Everything is shaped by ROSS's optimistic PDES model. Models are LPs (logical processes) +defined by a `tw_lptype` of callbacks: `init`, forward event handler, **reverse event +handler**, optional `commit`, `final`, and a mapping function. Under `--sync=3` events +speculatively execute and may roll back; every state mutation in a forward handler must +have an exact inverse in the reverse handler (RNG calls reversed with +`tw_rand_reverse_unif`, values needed for reversal stashed in the message, conditional +work flagged via `tw_bf` bitfields). Getting this wrong is the classic CODES bug: it shows +up as non-determinism, not a crash. + +**Determinism is a hard project guarantee**: two runs with the same config must process +the same number of net events. The equivalence tests enforce it; `--synch=6` + +`crv_checkpointer` state save/compare functions (e.g. `save_terminal_state` / +`check_terminal_state` in dragonfly models) exist to localize reverse-handler bugs. New +state fields must be threaded through those save/check functions or listed in their +exclusion comments. + +Layout: `codes/` is the installed public header surface; `src/` builds the single +`libcodes` library; model drivers (executables) live in `src/network-workloads/` +(synthetic traffic + MPI trace replay via DUMPI/SWM/UNION) and `doc/example/` (tutorial +models, notably `tutorial-synthetic-ping-pong.c` — the commented teaching example). + +A model driver's `main()` follows a fixed sequence: `tw_opt_add`/`tw_init` → +`configuration_load` → `model_net_register()` + `lp_type_register()` for the driver's own +LPs (+ `st_model_type_register()` for instrumentation callbacks) → `codes_mapping_setup()` +(instantiates LPs from the config's group/count layout — `src/util/codes_mapping.c` owns +the name→type→global-id mapping) → `model_net_configure` → `tw_run()` → finalize/report. + +**model-net** (`src/networks/model-net/`) is the network abstraction. Each network model +(dragonfly.c, dragonfly-custom/-plus/-dally.cxx, fattree, slimfly, torus, express-mesh, +simplenet, simplep2p, loggp) implements a `model_net_method` vtable +(`codes/model-net-method.h`). Terminal/NIC LPs are wrapped by a base LP +(`core/model-net-lp.c`) that handles scheduling, queuing, and sampling hooks before +delegating to the method; router LPs are separate methods with their own LP types. +Application LPs inject traffic with `model_net_event()` and reverse it with +`model_net_event_rc2()`. LP counts and parameters (radix, num_rails, num_qos_levels…) +come from the config, so payload/state sizes are often only known at LP-init time. +dragonfly-dally is the most maintained variant. + +**LP state rules** (root cause of past segfaults, see `doc/dev/conventions.md`): ROSS +hands LPs a zeroed blob and never runs C++ constructors — C++/STL members in state are +allowed only if placement-new'd in `init` and destructed in `final`; never assign a +container into never-constructed memory. Event/message structs must stay POD (ROSS +memcpys them). Growing a driver's message struct can overflow the config's +`message_size` PARAM — the run aborts with an explicit size error; bump the configs. + +**Instrumentation** is two separate mechanisms: (1) CODES-native sampling +(`model_net_enable_sampling`, per-model `*_sample_file` PARAMS, model-owned output +formats), and (2) the ROSS instrumentation layer (`--model-stats=1..4` for GVT/real-time/ +virtual-time sampling + `--event-trace`), driven by per-LP `st_model_types` callbacks. +The binary output format of (2) and a reference parser live in +`doc/model-stats-binary-format.md` and `scripts/parse-ross-model-stats.py`; registration +mechanics in `doc/codes-vis-readme.md`. Keep payload layouts, `mstat_sz`/ +`sample_struct_sz` sizing, the format doc, and the parser in sync when touching any of +them. + +**Surrogate subsystem** (`src/surrogate/`): multi-fidelity simulation that can freeze the +network model mid-run (`NETWORK_SURROGATE` config section) and replace packet latencies +with predictions (ML integration via Torch/ZeroMQ). The freeze path memsets terminal +state and selectively restores fields — any new terminal-state field must be considered +there (`dragonfly_dally_terminal_highdef_to_surrogate` and its unfreeze twin), or it +silently zeroes (or crashes, for pointers) while frozen. From be350dcd3247d1ef09d4a9da80fd34493db16a7f Mon Sep 17 00:00:00 2001 From: Caitlin Ross Date: Tue, 14 Jul 2026 18:10:32 -0500 Subject: [PATCH 2/2] scripts: add opt-in pre-commit hook enforcing clang-format Runs the same clang-format --dry-run --Werror check as CI (.github/workflows/format.yml) on the staged content of C/C++ files under src/, codes/, doc/, and tests/, rejecting the commit with a fix-it command on drift so formatting fixups never need their own commit. Checks the index (git show :file), not the worktree, so partially staged files are judged on what would actually land; warns when the local clang-format major differs from CI's pinned 20. Opt-in per clone via 'git config core.hooksPath scripts/git-hooks' (documented in the README); bypass a single commit with --no-verify. --- README.md | 12 ++++++++ scripts/git-hooks/pre-commit | 54 ++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100755 scripts/git-hooks/pre-commit diff --git a/README.md b/README.md index 28cddcf7..ec6b2e6a 100644 --- a/README.md +++ b/README.md @@ -268,6 +268,18 @@ This repo uses [clang-format](https://clang.llvm.org/docs/ClangFormat.html) to k To reformat a file manually: `clang-format -i path/to/file.c`. CI runs `clang-format --dry-run --Werror` on every PR and rejects any drift, so PRs with unformatted code don't merge. Note: The CI uses clang-format major release version 20, so you should format your files with that version. +To catch drift at commit time instead of in CI, enable the repo's pre-commit hook +(one-time, per clone): + +```bash +git config core.hooksPath scripts/git-hooks +``` + +The hook runs the same `clang-format --dry-run --Werror` check CI does, on the *staged* +content of C/C++ files under `src/`, `codes/`, `doc/`, and `tests/`, and rejects the +commit with a fix-it command if anything drifts. Bypass a single commit with +`git commit --no-verify`. + ### Determinism Before contributing please run the full test suite. Some tests verify our determinism guarantees (every simulation should be reproducible), i.e, the number of net events processed between two runs in parallel mode should be the same. We want to keep our determinism guarantees forever. Non-deterministic simulations are often the result of faulty reverse handlers, which have caused serious bug failures and hundreds of hours of debugging. diff --git a/scripts/git-hooks/pre-commit b/scripts/git-hooks/pre-commit new file mode 100755 index 00000000..c184938f --- /dev/null +++ b/scripts/git-hooks/pre-commit @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Pre-commit hook: reject commits whose staged C/C++ files fail clang-format. +# +# Enable once per clone: git config core.hooksPath scripts/git-hooks +# Bypass a single commit: git commit --no-verify +# +# Mirrors CI (.github/workflows/format.yml): clang-format --dry-run --Werror +# over *.c *.h *.cpp *.hpp *.cxx beneath src/ codes/ doc/ tests/. The staged +# (index) content is checked, not the working tree, so partially staged files +# are judged on exactly what would be committed. + +set -u + +files=() +while IFS= read -r -d '' f; do + case "$f" in + src/* | codes/* | doc/* | tests/*) ;; + *) continue ;; + esac + case "$f" in + *.c | *.h | *.cpp | *.hpp | *.cxx) files+=("$f") ;; + esac +done < <(git diff --cached --name-only --diff-filter=ACMR -z) + +[[ ${#files[@]} -eq 0 ]] && exit 0 + +if ! command -v clang-format > /dev/null 2>&1; then + echo "pre-commit: clang-format not found, but these staged C/C++ files need checking:" >&2 + printf ' %s\n' "${files[@]}" >&2 + echo "pre-commit: install clang-format 20 (the version CI pins) or bypass once with --no-verify." >&2 + exit 1 +fi + +version=$(clang-format --version) +if [[ $version != *"version 20."* ]]; then + echo "pre-commit: warning: CI pins clang-format 20; you have: $version" >&2 + echo "pre-commit: output can differ subtly between major versions, so CI may disagree." >&2 +fi + +bad=() +for f in "${files[@]}"; do + if ! git show ":$f" | clang-format --dry-run --Werror --assume-filename="$f" > /dev/null 2>&1; then + bad+=("$f") + fi +done + +if [[ ${#bad[@]} -ne 0 ]]; then + echo "pre-commit: staged files are not clang-format clean:" >&2 + printf ' %s\n' "${bad[@]}" >&2 + echo "pre-commit: fix with: clang-format -i ${bad[*]}" >&2 + echo "pre-commit: then re-stage (git add) and commit again. Bypass once with --no-verify." >&2 + exit 1 +fi +exit 0