Skip to content

Commit 29b90ac

Browse files
authored
Merge pull request #207 from ausimian/claude/nif-static-analysis-tools-t3rmhg
Add cppcheck static analysis for the NIF
2 parents ea03e3e + 752353b commit 29b90ac

4 files changed

Lines changed: 77 additions & 2 deletions

File tree

.github/workflows/ci.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,26 @@ jobs:
200200
- name: fast_kernels_full
201201
run: mix test --only fast_kernels_full
202202

203+
# C++ static analysis of the NIF sources with cppcheck. Kept separate
204+
# from the precommit lane on purpose: the `cppcheck` Makefile target
205+
# needs neither a built libmlx nor the BEAM toolchain (it analyses our
206+
# own c_src/ tree and suppresses the third-party headers it can't see),
207+
# so this runs on a bare checkout and finishes in seconds. Runs on
208+
# macOS to match the platform the NIF actually ships on, and invokes
209+
# the same `make cppcheck` a developer runs locally.
210+
cppcheck:
211+
name: cppcheck (NIF static analysis)
212+
if: github.event_name != 'push' || github.ref_type == 'branch'
213+
runs-on: macos-14
214+
steps:
215+
- uses: actions/checkout@v6
216+
217+
- name: Install cppcheck
218+
run: brew install cppcheck
219+
220+
- name: Run cppcheck
221+
run: make cppcheck
222+
203223
# ASan CI deferred: requires OTP built with --enable-sanitizers=address
204224
# (macOS SIP blocks DYLD_INSERT_LIBRARIES, and late-loaded libasan
205225
# fails). See Makefile and RELEASE.md for details.

MAINTAINING.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,20 @@ so changing the attribute is the entire pin.
191191

192192
## Local debugging
193193

194+
### Static analysis of the NIF (cppcheck)
195+
196+
```sh
197+
brew install cppcheck # one-time
198+
make cppcheck
199+
```
200+
201+
Runs cppcheck over `c_src/` and exits non-zero on any finding. It needs
202+
neither a built `libmlx` nor the BEAM toolchain, so it works on a bare
203+
checkout in seconds — the same target CI runs (`.github/workflows/ci.yml`,
204+
the `cppcheck` job). See the `cppcheck` target in the `Makefile` for the
205+
enabled checks and suppressions; use inline `// cppcheck-suppress <id>`
206+
for one-off false positives.
207+
194208
### Build MLX in isolation
195209

196210
```sh

Makefile

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,48 @@ ifeq ($(EMILY_ASAN),1)
4747
LDFLAGS += -fsanitize=address
4848
endif
4949

50-
.PHONY: all clean bench-native
50+
.PHONY: all clean bench-native cppcheck
5151

5252
all: $(NIF_SO) $(METALLIB)
5353

54+
# ------------------------------------------------------------------
55+
# cppcheck: static analysis of the first-party NIF sources.
56+
#
57+
# Deliberately self-contained: it does NOT need libmlx built or the
58+
# MLX_INCLUDE_DIR / FINE_INCLUDE_DIR / ERTS_INCLUDE_DIR env that the
59+
# real compile relies on, so a developer can just run `make cppcheck`
60+
# from the repo root without a full NIF build (and CI can run it on a
61+
# bare checkout). cppcheck degrades gracefully on the third-party
62+
# headers it can't see — we only feed it our own `-Ic_src` tree and
63+
# suppress the unavoidable missing-include notices for <mlx/...>,
64+
# <fine.hpp>, <erl_nif.h>, etc. Findings are therefore scoped to code
65+
# we actually own.
66+
#
67+
# `passedByValueCallback` is suppressed on purpose: every NIF entry
68+
# point takes its container/aggregate args (std::vector, std::string,
69+
# std::tuple) by value because Fine's FINE_NIF macro decodes each BEAM
70+
# term into a value and passes it in — the signature is dictated by the
71+
# binding, not a stray copy. (Plain helpers still use const& where they
72+
# should.) Use inline `// cppcheck-suppress <id>` for one-off cases.
73+
#
74+
# Install: `brew install cppcheck`.
75+
# ------------------------------------------------------------------
76+
CPPCHECK ?= cppcheck
77+
CPPCHECK_JOBS ?= $(shell sysctl -n hw.ncpu 2>/dev/null || echo 4)
78+
CPPCHECK_FLAGS := --enable=warning,performance,portability \
79+
--std=c++20 --language=c++ \
80+
--inline-suppr \
81+
--error-exitcode=1 \
82+
--quiet -j $(CPPCHECK_JOBS) \
83+
-Ic_src \
84+
--suppress=missingInclude \
85+
--suppress=missingIncludeSystem \
86+
--suppress=unmatchedSuppression \
87+
--suppress=passedByValueCallback
88+
89+
cppcheck:
90+
$(CPPCHECK) $(CPPCHECK_FLAGS) $(SOURCES)
91+
5492
# ------------------------------------------------------------------
5593
# bench-native: standalone C++ microbenchmarks under bench/native/.
5694
#

c_src/emily/program.hpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,10 @@ inline int64_t index_of(int64_t r) { return r & kIndexMask; }
5353
class Program;
5454

5555
struct CompiledInstr {
56-
Opcode opcode;
56+
// Value-initialized so a default-constructed instr has a defined opcode
57+
// (the other members are containers that self-initialize). Every real
58+
// construction aggregate-initializes all fields, overriding this.
59+
Opcode opcode{};
5760
std::vector<int64_t> operands; // packed refs
5861
std::vector<std::vector<int64_t>> iattrs; // integer attrs (shapes/axes/dtype codes)
5962
// Nested programs an instruction carries (empty for all but control

0 commit comments

Comments
 (0)