Skip to content

Commit 0bb02e1

Browse files
committed
ci: enforce pinned clang-format (22.1.5) on C++ sources
Add .github/workflows/clang-format.yml: install clang-format 22.1.5 (pinned via pip for reproducibility across CI and local checkouts) and fail the build via `clang-format --dry-run --Werror` over all hand-written C++ (src/main/cpp + src/test/cpp). The generated JNI header src/main/cpp/jllama.h (from `javac -h`) is intentionally excluded. Reformat the whole C++ tree with that version so the check is green, and document the pinned version plus the bump procedure in CLAUDE.md. Whitespace-only — all 445 C++ tests still pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014L2dLbAtwdq7C6a2gFRsQQ
1 parent 7c5fd67 commit 0bb02e1

11 files changed

Lines changed: 768 additions & 785 deletions

File tree

.github/workflows/clang-format.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# SPDX-FileCopyrightText: 2026 Bernard Ladenthin <bernard.ladenthin@gmail.com>
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
name: clang-format
6+
on:
7+
push:
8+
pull_request:
9+
workflow_dispatch:
10+
11+
# Enforces a single, pinned clang-format across all C++ sources so formatting is
12+
# reproducible between contributors and CI. Bump CLANG_FORMAT_VERSION here and in
13+
# CLAUDE.md (Code Formatting) together, then reformat the tree with the same version.
14+
env:
15+
CLANG_FORMAT_VERSION: "22.1.5"
16+
17+
jobs:
18+
clang-format:
19+
runs-on: ubuntu-latest
20+
steps:
21+
- uses: actions/checkout@v6
22+
- uses: actions/setup-python@v5
23+
with:
24+
python-version: "3.x"
25+
- name: Install pinned clang-format
26+
run: pip install "clang-format==${CLANG_FORMAT_VERSION}"
27+
- name: Check C++ formatting
28+
run: |
29+
clang-format --version
30+
# All hand-written C++ sources; the generated JNI header (src/main/cpp/jllama.h,
31+
# produced by `javac -h`) is intentionally excluded.
32+
files=$(find src/main/cpp src/test/cpp -type f \( -name '*.cpp' -o -name '*.hpp' \) | sort)
33+
echo "Checking:"; echo "$files"
34+
clang-format --dry-run --Werror $files

CLAUDE.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,10 +392,21 @@ not track the loader's own Java package). This is the same
392392
`spotbugs-exclude.xml`, PIT `targetClasses`, and `CMakeLists.txt` OSInfo repairs.
393393

394394
### Code Formatting
395+
396+
C++ formatting is **enforced in CI** (`.github/workflows/clang-format.yml`) with a **pinned**
397+
clang-format — currently **22.1.5**, installed via `pip install clang-format==22.1.5`. Format with
398+
that exact version before committing; a different clang-format version reflows code differently and
399+
will fail the check.
400+
395401
```bash
396-
clang-format -i src/main/cpp/*.cpp src/main/cpp/*.hpp # Format C++ code
402+
pip install "clang-format==22.1.5"
403+
clang-format -i src/main/cpp/*.cpp src/main/cpp/*.hpp src/test/cpp/*.cpp # Format C++ code
397404
```
398405

406+
The generated JNI header `src/main/cpp/jllama.h` (produced by `javac -h`) is intentionally excluded.
407+
To bump the enforced version, update the pin in **both** the workflow (`CLANG_FORMAT_VERSION`) and
408+
this line, then reformat the whole tree with the new version in the same commit.
409+
399410
### Javadoc — must build cleanly before `mvn package`
400411

401412
The release packaging job runs `mvn package` with the `release` profile, which attaches

src/main/cpp/jllama.cpp

Lines changed: 169 additions & 201 deletions
Large diffs are not rendered by default.

src/main/cpp/jni_helpers.hpp

Lines changed: 25 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -50,23 +50,23 @@ struct server_response_reader;
5050
// worker thread. Stored as the Java-side `ctx` (jlong) pointer.
5151
// ---------------------------------------------------------------------------
5252
struct jllama_context {
53-
server_context server; // value member (pimpl inside)
54-
std::thread worker;
55-
bool vocab_only = false;
53+
server_context server; // value member (pimpl inside)
54+
std::thread worker;
55+
bool vocab_only = false;
5656
std::atomic<bool> worker_ready{false};
5757

5858
// Cached after load_model() — valid for the lifetime of this context.
59-
const llama_vocab *vocab = nullptr;
59+
const llama_vocab *vocab = nullptr;
6060
// Non-null only in vocab-only mode (bypasses server_context entirely).
61-
llama_model *vocab_only_model = nullptr;
61+
llama_model *vocab_only_model = nullptr;
6262

6363
// Saved copy of common_params used to load the model.
6464
// Required by server_task::params_from_json_cmpl which takes common_params&.
65-
common_params params;
65+
common_params params;
6666

6767
// Per-streaming-task response readers, keyed by task id.
6868
// Guarded by readers_mutex.
69-
std::mutex readers_mutex;
69+
std::mutex readers_mutex;
7070
std::map<int, std::unique_ptr<server_response_reader>> readers;
7171
};
7272

@@ -80,9 +80,7 @@ inline void erase_reader(jllama_context *jctx, int id_task) {
8080

8181
// Guard: throw and return false if the model was loaded without embedding
8282
// support enabled. Used by every JNI entry point that produces embeddings.
83-
[[nodiscard]] inline bool require_embedding_support(JNIEnv *env,
84-
bool embedding_enabled,
85-
jclass error_class) {
83+
[[nodiscard]] inline bool require_embedding_support(JNIEnv *env, bool embedding_enabled, jclass error_class) {
8684
if (embedding_enabled) {
8785
return true;
8886
}
@@ -101,9 +99,7 @@ inline void erase_reader(jllama_context *jctx, int id_task) {
10199
// already deleted (or never fully initialised), which is a valid no-op for
102100
// a destructor-style call.
103101
// ---------------------------------------------------------------------------
104-
[[nodiscard]] inline jllama_context *get_jllama_context_impl(JNIEnv *env,
105-
jobject obj,
106-
jfieldID field_id) {
102+
[[nodiscard]] inline jllama_context *get_jllama_context_impl(JNIEnv *env, jobject obj, jfieldID field_id) {
107103
const jlong handle = env->GetLongField(obj, field_id);
108104
if (handle == 0) {
109105
return nullptr;
@@ -117,10 +113,8 @@ inline void erase_reader(jllama_context *jctx, int id_task) {
117113
// Checks that `data` contains the given key. Returns true if present.
118114
// On missing key: throws "<field> is required" via JNI and returns false.
119115
// ---------------------------------------------------------------------------
120-
[[nodiscard]] inline bool require_json_field_impl(JNIEnv *env,
121-
const nlohmann::json &data,
122-
const char *field,
123-
jclass error_class) {
116+
[[nodiscard]] inline bool require_json_field_impl(JNIEnv *env, const nlohmann::json &data, const char *field,
117+
jclass error_class) {
124118
if (data.contains(field)) {
125119
return true;
126120
}
@@ -135,10 +129,9 @@ inline void erase_reader(jllama_context *jctx, int id_task) {
135129
// Reads a Java int array into a std::vector<int32_t> and releases the JNI
136130
// array elements with JNI_ABORT (read-only — no writeback needed).
137131
// ---------------------------------------------------------------------------
138-
[[nodiscard]] inline std::vector<int32_t> jint_array_to_tokens_impl(
139-
JNIEnv *env, jintArray array) {
132+
[[nodiscard]] inline std::vector<int32_t> jint_array_to_tokens_impl(JNIEnv *env, jintArray array) {
140133
const jsize length = env->GetArrayLength(array);
141-
jint *elements = env->GetIntArrayElements(array, nullptr);
134+
jint *elements = env->GetIntArrayElements(array, nullptr);
142135
std::vector<int32_t> tokens(elements, elements + length);
143136
env->ReleaseIntArrayElements(array, elements, JNI_ABORT);
144137
return tokens;
@@ -170,9 +163,7 @@ inline void erase_reader(jllama_context *jctx, int id_task) {
170163
// construction to results_to_json (json_helpers.hpp) and serialisation to
171164
// json_to_jstring_impl.
172165
// ---------------------------------------------------------------------------
173-
[[nodiscard]] inline jstring results_to_jstring_impl(
174-
JNIEnv *env,
175-
const std::vector<server_task_result_ptr> &results) {
166+
[[nodiscard]] inline jstring results_to_jstring_impl(JNIEnv *env, const std::vector<server_task_result_ptr> &results) {
176167
return json_to_jstring_impl(env, results_to_json(results));
177168
}
178169

@@ -184,13 +175,9 @@ inline void erase_reader(jllama_context *jctx, int id_task) {
184175
// On allocation failure: throws via JNI with oom_class and returns nullptr.
185176
// ---------------------------------------------------------------------------
186177
template <typename JArray, typename JElem, typename CppElem>
187-
[[nodiscard]] inline JArray vec_to_jarray_impl(
188-
JNIEnv *env,
189-
const std::vector<CppElem> &values,
190-
jclass oom_class,
191-
const char *oom_msg,
192-
JArray (JNIEnv_::*alloc)(jsize),
193-
void (JNIEnv_::*copy)(JArray, jsize, jsize, const JElem *)) {
178+
[[nodiscard]] inline JArray vec_to_jarray_impl(JNIEnv *env, const std::vector<CppElem> &values, jclass oom_class,
179+
const char *oom_msg, JArray (JNIEnv_::*alloc)(jsize),
180+
void (JNIEnv_::*copy)(JArray, jsize, jsize, const JElem *)) {
194181
const jsize len = static_cast<jsize>(values.size());
195182
JArray arr = (env->*alloc)(len);
196183
if (arr == nullptr) {
@@ -202,21 +189,15 @@ template <typename JArray, typename JElem, typename CppElem>
202189
}
203190

204191
// Converts a float vector to a Java jfloatArray.
205-
[[nodiscard]] inline jfloatArray embedding_to_jfloat_array_impl(
206-
JNIEnv *env,
207-
const std::vector<float> &values,
208-
jclass oom_class) {
209-
return vec_to_jarray_impl<jfloatArray, jfloat>(
210-
env, values, oom_class, "could not allocate embedding",
211-
&JNIEnv_::NewFloatArray, &JNIEnv_::SetFloatArrayRegion);
192+
[[nodiscard]] inline jfloatArray embedding_to_jfloat_array_impl(JNIEnv *env, const std::vector<float> &values,
193+
jclass oom_class) {
194+
return vec_to_jarray_impl<jfloatArray, jfloat>(env, values, oom_class, "could not allocate embedding",
195+
&JNIEnv_::NewFloatArray, &JNIEnv_::SetFloatArrayRegion);
212196
}
213197

214198
// Converts a token vector to a Java jintArray.
215-
[[nodiscard]] inline jintArray tokens_to_jint_array_impl(
216-
JNIEnv *env,
217-
const std::vector<int32_t> &tokens,
218-
jclass oom_class) {
219-
return vec_to_jarray_impl<jintArray, jint>(
220-
env, tokens, oom_class, "could not allocate token memory",
221-
&JNIEnv_::NewIntArray, &JNIEnv_::SetIntArrayRegion);
199+
[[nodiscard]] inline jintArray tokens_to_jint_array_impl(JNIEnv *env, const std::vector<int32_t> &tokens,
200+
jclass oom_class) {
201+
return vec_to_jarray_impl<jintArray, jint>(env, tokens, oom_class, "could not allocate token memory",
202+
&JNIEnv_::NewIntArray, &JNIEnv_::SetIntArrayRegion);
222203
}

src/main/cpp/json_helpers.hpp

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,7 @@
5151
// jni_helpers.hpp, and directly in receiveCompletionJson, embed, and
5252
// handleRerank in jllama.cpp.
5353
// ---------------------------------------------------------------------------
54-
[[nodiscard]] inline std::string get_result_error_message(
55-
const server_task_result_ptr &result) {
54+
[[nodiscard]] inline std::string get_result_error_message(const server_task_result_ptr &result) {
5655
return result->to_json()["message"].get<std::string>();
5756
}
5857

@@ -68,8 +67,7 @@
6867
// This mirrors the OpenAI API convention used by handleCompletions,
6968
// handleCompletionsOai, handleChatCompletions, and handleInfill.
7069
// ---------------------------------------------------------------------------
71-
[[nodiscard]] inline json results_to_json(
72-
const std::vector<server_task_result_ptr> &results) {
70+
[[nodiscard]] inline json results_to_json(const std::vector<server_task_result_ptr> &results) {
7371
if (results.size() == 1) {
7472
return results[0]->to_json();
7573
}
@@ -87,19 +85,14 @@
8785
// Each element contains the original document text (looked up via the
8886
// result's "index" field), the index, and the relevance score.
8987
// ---------------------------------------------------------------------------
90-
[[nodiscard]] inline json rerank_results_to_json(
91-
const std::vector<server_task_result_ptr> &results,
92-
const std::vector<std::string> &documents) {
88+
[[nodiscard]] inline json rerank_results_to_json(const std::vector<server_task_result_ptr> &results,
89+
const std::vector<std::string> &documents) {
9390
json arr = json::array();
9491
for (const auto &result : results) {
9592
const auto out = result->to_json();
96-
int index = out["index"].get<int>();
93+
int index = out["index"].get<int>();
9794
float score = out["score"].get<float>();
98-
arr.push_back({
99-
{"document", documents[index]},
100-
{"index", index},
101-
{"score", score}
102-
});
95+
arr.push_back({{"document", documents[index]}, {"index", index}, {"score", score}});
10396
}
10497
return arr;
10598
}
@@ -119,8 +112,12 @@
119112
return false;
120113
}
121114
const std::string format = body.at("encoding_format").get<std::string>();
122-
if (format == "base64") { return true; }
123-
if (format == "float") { return false; }
115+
if (format == "base64") {
116+
return true;
117+
}
118+
if (format == "float") {
119+
return false;
120+
}
124121
throw std::invalid_argument("encoding_format must be \"float\" or \"base64\"");
125122
}
126123

@@ -135,8 +132,7 @@
135132
// when "content" was used — the caller must downgrade oaicompat to NONE.
136133
// Throws std::invalid_argument if neither "input" nor "content" is present.
137134
// ---------------------------------------------------------------------------
138-
[[nodiscard]] inline json extract_embedding_prompt(const json &body,
139-
bool &force_no_oaicompat) {
135+
[[nodiscard]] inline json extract_embedding_prompt(const json &body, bool &force_no_oaicompat) {
140136
force_no_oaicompat = false;
141137
if (body.count("input") != 0) {
142138
return body.at("input");
@@ -168,8 +164,7 @@
168164
// Returns float — validated value in [0.0, 1.0].
169165
// Throws std::invalid_argument — present but outside [0.0, 1.0].
170166
// ---------------------------------------------------------------------------
171-
[[nodiscard]] inline std::optional<float>
172-
parse_slot_prompt_similarity(const json &config) {
167+
[[nodiscard]] inline std::optional<float> parse_slot_prompt_similarity(const json &config) {
173168
if (!config.contains("slot_prompt_similarity")) {
174169
return std::nullopt;
175170
}
@@ -189,8 +184,7 @@ parse_slot_prompt_similarity(const json &config) {
189184
// Returns int — validated value > 0.
190185
// Throws std::invalid_argument("<key> must be greater than 0") — present but ≤ 0.
191186
// ---------------------------------------------------------------------------
192-
[[nodiscard]] inline std::optional<int>
193-
parse_positive_int_config(const json &config, const char *key) {
187+
[[nodiscard]] inline std::optional<int> parse_positive_int_config(const json &config, const char *key) {
194188
if (!config.contains(key)) {
195189
return std::nullopt;
196190
}

src/main/cpp/log_helpers.hpp

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,24 @@
2020
// fall-through to mirror llama.cpp's own log routing.
2121
[[nodiscard]] inline const char *log_level_name(ggml_log_level level) {
2222
switch (level) {
23-
case GGML_LOG_LEVEL_ERROR: return "ERROR";
24-
case GGML_LOG_LEVEL_WARN: return "WARN";
25-
case GGML_LOG_LEVEL_DEBUG: return "DEBUG";
23+
case GGML_LOG_LEVEL_ERROR:
24+
return "ERROR";
25+
case GGML_LOG_LEVEL_WARN:
26+
return "WARN";
27+
case GGML_LOG_LEVEL_DEBUG:
28+
return "DEBUG";
2629
case GGML_LOG_LEVEL_INFO:
27-
default: return "INFO";
30+
default:
31+
return "INFO";
2832
}
2933
}
3034

3135
// Pure variant taking an explicit timestamp so tests are deterministic.
32-
[[nodiscard]] inline std::string format_log_as_json(
33-
ggml_log_level level, const char *text, std::time_t timestamp) {
36+
[[nodiscard]] inline std::string format_log_as_json(ggml_log_level level, const char *text, std::time_t timestamp) {
3437
nlohmann::json log_obj = {
3538
{"timestamp", timestamp},
36-
{"level", log_level_name(level)},
37-
{"message", text ? text : ""},
39+
{"level", log_level_name(level)},
40+
{"message", text ? text : ""},
3841
};
3942
return log_obj.dump();
4043
}

0 commit comments

Comments
 (0)