Skip to content

Commit 9be73a3

Browse files
committed
refactor: Lombok @ToString/@EqualsAndHashCode across jllama production sources
Apply the BAF/plugin Lombok migration pattern to java-llama.cpp. Value-shaped types get @tostring + @EqualsAndHashCode; mutable lifecycle handles and operational classes get @tostring only (identity is the correct equals semantics); classes with semantically-meaningful handwritten toString implementations keep their custom toString and gain @EqualsAndHashCode only. Build wiring fixes (mirrors BAF/plugin): - pom.xml: add Lombok to <annotationProcessorPaths> AND extend the explicit -processor argument list to include both Lombok SPI processors (AnnotationProcessorHider$AnnotationProcessor and $ClaimingProcessor) alongside the existing Checker Framework NullnessChecker. - lombok.config: switch callSuper default from "call" to "skip" since most value classes extend Object directly. Value types (Lombok @tostring + @EqualsAndHashCode): - Pair, ChatChoice, ChatResponse, ContentPart, ToolDefinition Value types with derived/aggregated toString fields: - Usage (totalTokens via @ToString.Include on the getter) - TokenLogprob (topLogprobs rendered as size via @ToString.Include) - Timings (full field dump) Value types with semantically-meaningful handwritten toString preserved (@EqualsAndHashCode only, toString documented as intentional): - ChatMessage ("role: content" conversation-trace format) - ToolCall ("name(args)[id]" function-call syntax) - CompletionResult, LlamaOutput (return generated text verbatim — public API contract) - ModelMeta, ServerMetrics (re-serialise to compact JSON for assertEquals) - JsonParameters (emits actual JSON the native server consumes) - CliParameters (emits CLI argv-style string for the native binary) Subclasses of JsonParameters/CliParameters get @EqualsAndHashCode(callSuper=true): - InferenceParameters, ModelParameters Operational / lifecycle classes (Lombok @tostring only; identity-equals is correct; native handles and lambdas excluded from rendered output): - LlamaModel (parser collaborators rendered, native ctx handle visible) - LlamaIterator, LlamaIterable, LlamaPublisher (owning model excluded — recursive native-state dump) - Session (model + lambdas + lock excluded) - CancellationToken, ChatRequest (lambdas excluded) - LlamaLoader, LlamaSystemProperties, NativeLibraryPermissionSetter, ProcessRunner, Java8CompatibilityHelper Test update: - PairTest.testHashCodeMatchesObjectsHash renamed and rewritten to verify the hashCode contract (non-zero, varies by field) rather than pinning to Objects.hash. The PIT-mutation-killing intent is preserved but no longer assumes the specific implementation. Intentionally skipped (rationale documented inline): - OSInfo — vendored from sqlite-jdbc with explicit "only deviations" policy; adding Lombok would diverge further. Spotless line-wrapping is incidental. - LlamaException, ModelUnavailableException — extend Throwable, which already provides toString and identity-equals. - StopReason, LogLevel — enums, inherit toString from Enum. - LoadProgressCallback, ToolHandler — interfaces. - SkipDownloadFailureTranslator, TimingsLogger — non-instantiable utility classes (private constructor + all-static methods). All 888 runnable tests pass; the single RerankingModelTest error is a pre-existing UnsatisfiedLinkError on this sandbox (no native library built in restricted-network env, per CLAUDE.md). https://claude.ai/code/session_01LzoKmqzgtQsELS5tsH4Wog
1 parent baffa37 commit 9be73a3

40 files changed

Lines changed: 325 additions & 132 deletions

lombok.config

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ config.stopBubbling = true
1010
# from coverage requirements and bug detectors.
1111
lombok.addLombokGeneratedAnnotation = true
1212

13-
# Default to "call" on @EqualsAndHashCode / @ToString: when extending a
14-
# non-Object parent we want the parent's state included; failing loud
15-
# forces an explicit decision per class.
16-
lombok.equalsAndHashCode.callSuper = call
17-
lombok.toString.callSuper = call
13+
# Default to "skip" on @EqualsAndHashCode / @ToString: we inherit from
14+
# Object in almost all cases; "skip" is the right default for
15+
# Object-extending classes. Classes that extend a non-Object base override
16+
# per-annotation with @EqualsAndHashCode(callSuper = true) /
17+
# @ToString(callSuper = true).
18+
lombok.equalsAndHashCode.callSuper = skip
19+
lombok.toString.callSuper = skip
1820

1921
# Do NOT generate Spring-style @ConstructorProperties; java.beans is not
2022
# needed by this codebase and pulls in the desktop module on some JDKs.

pom.xml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,14 +377,23 @@ SPDX-License-Identifier: MIT
377377
so it acts as a second-opinion verifier on the same JSpecify
378378
annotations.
379379
-->
380+
<!-- Explicit processor list: Lombok (two SPI classes) + Checker Framework's
381+
Nullness Checker as a 2nd nullness pass alongside NullAway. The explicit
382+
-processor argument overrides SPI discovery, so every processor we want
383+
to run must be listed here. -->
380384
<arg>-processor</arg>
381-
<arg>org.checkerframework.checker.nullness.NullnessChecker</arg>
385+
<arg>lombok.launch.AnnotationProcessorHider$AnnotationProcessor,lombok.launch.AnnotationProcessorHider$ClaimingProcessor,org.checkerframework.checker.nullness.NullnessChecker</arg>
382386
<arg>-XDaddTypeAnnotationsToSymbol=true</arg>
383387
<arg>-XDcompilePolicy=simple</arg>
384388
<arg>--should-stop=ifError=FLOW</arg>
385389
<arg>-Xplugin:ErrorProne -Xep:NullAway:ERROR -XepOpt:NullAway:OnlyNullMarked=true -XepOpt:NullAway:JSpecifyMode=true -XepOpt:NullAway:CheckOptionalEmptiness=true -XepOpt:NullAway:AcknowledgeRestrictiveAnnotations=true -XepOpt:NullAway:AcknowledgeAndroidRecent=true -XepOpt:NullAway:AssertsEnabled=true -Xep:BoxedPrimitiveEquality:ERROR -Xep:EqualsHashCode:ERROR -Xep:EqualsIncompatibleType:ERROR -Xep:IdentityBinaryExpression:ERROR -Xep:SelfAssignment:ERROR -Xep:SelfComparison:ERROR -Xep:SelfEquals:ERROR -Xep:DeadException:ERROR -Xep:FormatString:ERROR -Xep:InvalidPatternSyntax:ERROR -Xep:OptionalEquality:ERROR -Xep:ImpossibleNullComparison:ERROR</arg>
386390
</compilerArgs>
387391
<annotationProcessorPaths>
392+
<path>
393+
<groupId>org.projectlombok</groupId>
394+
<artifactId>lombok</artifactId>
395+
<version>${lombok.version}</version>
396+
</path>
388397
<path>
389398
<groupId>com.google.errorprone</groupId>
390399
<artifactId>error_prone_core</artifactId>

src/main/java/net/ladenthin/llama/CancellationToken.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
package net.ladenthin.llama;
66

7+
import lombok.ToString;
8+
79
/**
810
* Cancellation handle for a blocking {@link LlamaModel} call. Pass an instance to
911
* {@link LlamaModel#complete(InferenceParameters, CancellationToken)} and invoke
@@ -31,7 +33,13 @@
3133
* A token may be reused across calls. {@link #cancel()} and {@link #isCancelled()} are
3234
* safe to invoke concurrently with the inference loop.
3335
* </p>
36+
*
37+
* <p>{@code toString} is generated by Lombok over the {@code cancelled} flag.
38+
* {@code equals}/{@code hashCode} are intentionally NOT generated: a token is a
39+
* lifecycle handle managed by identity (the calling thread keeps a reference and
40+
* the inference loop observes that same instance), not a value object.</p>
3441
*/
42+
@ToString
3543
public final class CancellationToken {
3644

3745
private volatile boolean cancelled;

src/main/java/net/ladenthin/llama/ChatChoice.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,15 @@
44

55
package net.ladenthin.llama;
66

7+
import lombok.EqualsAndHashCode;
8+
import lombok.ToString;
9+
710
/**
811
* One choice in a chat completion response: the assistant message and the finish reason.
912
* Mirrors the OpenAI {@code choices[i]} object.
1013
*/
14+
@ToString
15+
@EqualsAndHashCode
1116
public final class ChatChoice {
1217

1318
private final int index;

src/main/java/net/ladenthin/llama/ChatMessage.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.util.Collections;
99
import java.util.List;
1010
import java.util.Optional;
11+
import lombok.EqualsAndHashCode;
1112
import org.jspecify.annotations.Nullable;
1213

1314
/**
@@ -27,7 +28,13 @@
2728
* (see {@link InferenceParameters#setMessages(java.util.List)}) emits an array-form
2829
* {@code content} field that the compiled-in {@code mtmd} pipeline understands.
2930
* </p>
31+
*
32+
* <p>{@code equals}/{@code hashCode} are generated by Lombok over all fields.
33+
* {@code toString} is intentionally handwritten (not Lombok-generated) so that
34+
* conversation traces in logs render as "{@code role: content}" or
35+
* "{@code role (tool_calls=N): content}" instead of a verbose field dump.</p>
3036
*/
37+
@EqualsAndHashCode
3138
public final class ChatMessage {
3239

3340
private final String role;

src/main/java/net/ladenthin/llama/ChatRequest.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import java.util.List;
1313
import java.util.Optional;
1414
import java.util.function.Consumer;
15+
import lombok.ToString;
1516
import org.jspecify.annotations.Nullable;
1617

1718
/**
@@ -23,7 +24,15 @@
2324
* setters; consumed by {@link LlamaModel#chat(ChatRequest)} and
2425
* {@link LlamaModel#chatWithTools(ChatRequest, java.util.Map)}.
2526
* </p>
27+
*
28+
* <p>{@code toString} is generated by Lombok over the request state fields. The
29+
* {@code paramsCustomizer} {@link Consumer} is excluded because lambda equality is
30+
* implementation-defined (compiler-synthesized class identity), not value-shaped,
31+
* and the rendered identity hash is noise in a request dump. {@code equals}/
32+
* {@code hashCode} are intentionally NOT generated: this is a mutable builder, not
33+
* a value object.
2634
*/
35+
@ToString
2736
public final class ChatRequest {
2837

2938
private static final ObjectMapper MAPPER = new ObjectMapper();
@@ -32,6 +41,10 @@ public final class ChatRequest {
3241
private final List<ToolDefinition> tools = new ArrayList<ToolDefinition>();
3342
private @Nullable String toolChoice;
3443
private int maxToolRounds = 8;
44+
45+
// Lambda Consumer — toString is the implementation hash, not useful in logs;
46+
// equality is compiler-synthesized class identity, not value-shaped.
47+
@ToString.Exclude
3548
private @Nullable Consumer<InferenceParameters> paramsCustomizer;
3649

3750
/** Construct an empty request; populate via the setters. */

src/main/java/net/ladenthin/llama/ChatResponse.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import java.util.Collections;
88
import java.util.List;
99
import java.util.Optional;
10+
import lombok.EqualsAndHashCode;
11+
import lombok.ToString;
1012

1113
/**
1214
* Typed result of {@link LlamaModel#chat(ChatRequest)} and
@@ -17,6 +19,8 @@
1719
* raw OAI JSON for fields not yet typed.
1820
* </p>
1921
*/
22+
@ToString
23+
@EqualsAndHashCode
2024
public final class ChatResponse {
2125

2226
private final String id;

src/main/java/net/ladenthin/llama/CliParameters.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,20 @@
99
import java.util.HashMap;
1010
import java.util.List;
1111
import java.util.Map;
12+
import lombok.EqualsAndHashCode;
1213
import net.ladenthin.llama.args.CliArg;
1314
import org.jspecify.annotations.Nullable;
1415

16+
/**
17+
* Base class for CLI-style parameter builders.
18+
*
19+
* <p>{@code equals}/{@code hashCode} are generated by Lombok over the parameters map.
20+
* {@code toString} is intentionally handwritten (not Lombok-generated): it emits the
21+
* accumulated parameters as a space-separated CLI argv-style string that callers can
22+
* forward to the native CLI. Replacing it with a Lombok field dump would break that
23+
* consumer contract.
24+
*/
25+
@EqualsAndHashCode
1526
abstract class CliParameters {
1627

1728
final Map<String, @Nullable String> parameters = new HashMap<>();

src/main/java/net/ladenthin/llama/CompletionResult.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import java.util.Collections;
88
import java.util.List;
9+
import lombok.EqualsAndHashCode;
910

1011
/**
1112
* Typed result of {@link LlamaModel#completeWithStats(InferenceParameters)}.
@@ -15,7 +16,15 @@
1516
* {@link InferenceParameters#setNProbs(int)} &gt; 0), and the {@link StopReason}.
1617
* The raw native JSON is exposed via {@link #getRawJson()} as an escape hatch.
1718
* </p>
19+
*
20+
* <p>{@code equals}/{@code hashCode} are generated by Lombok over all fields.
21+
* {@code toString} is intentionally handwritten (not Lombok-generated): it
22+
* returns the generated text verbatim so that {@code result + ""} or
23+
* {@code String.valueOf(result)} produce the completion text rather than a
24+
* verbose field dump. This is a public-API contract preserved from the
25+
* pre-Lombok shape.</p>
1826
*/
27+
@EqualsAndHashCode
1928
public final class CompletionResult {
2029

2130
private final String text;

src/main/java/net/ladenthin/llama/ContentPart.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import java.util.Base64;
1111
import java.util.Locale;
1212
import java.util.Objects;
13+
import lombok.EqualsAndHashCode;
14+
import lombok.ToString;
1315
import org.jspecify.annotations.Nullable;
1416

1517
/**
@@ -32,6 +34,8 @@
3234
* factories &#x2014; the constructor is private.
3335
* </p>
3436
*/
37+
@ToString
38+
@EqualsAndHashCode
3539
public final class ContentPart {
3640

3741
/** Discriminator for the two part kinds the OAI multipart schema supports. */

0 commit comments

Comments
 (0)