Skip to content

Commit a1af90a

Browse files
committed
feat(interop): D-526 drain 9 (final) — String char[] forms; campaign COMPLETE
Smell-audited: 1: the campaign's REMAINING note claimed (String. char-array) was missing — probing showed it already works (stale note, corrected in the discharge); the real gap was valueOf's char[] overloads silently rendering "#<array>" (a bug, not just a missing method). String/valueOf gained the char[] and char[]/offset/count overloads (building the string FROM the characters; bounds-checked window); String/copyValueOf (1+3 arg JVM alias) registered on the static surface. Corpus string_join_static +5 clj-byte-matched goldens. D-526 discharged — 9 drains total (java.lang scalars, Objects, Arrays, Collections, String); deliberate omissions doc'd at their surfaces per the dead-interop lesson.
1 parent 52a0929 commit a1af90a

4 files changed

Lines changed: 66 additions & 82 deletions

File tree

‎.dev/debt.yaml‎

Lines changed: 6 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -152,85 +152,6 @@ active:
152152
redundancy + public-readability; merge/prune. SSOT: ADR-0166 row 4.
153153
(.claude/ edits may hit the self-modification block — surface to user.)
154154
last_reviewed: "2026-06-24"
155-
- id: "D-526"
156-
status: |-
157-
open (2026-06-24, user-directed; ADR-0166). Java-interop static-member gap
158-
catalog + fill: 49 runtime/java/**/*.zig surfaces / 92 compat_tiers fqn.
159-
Some static members are missing. If not mechanically enumerable, FIRST
160-
catalogue them (extend compat_tiers.yaml / a coverage doc), then fill the
161-
"あと少し欠落" tail.
162-
DRAIN 1 (2026-06-25): a 63-expr clj-diff sweep over Math/Long/Integer/
163-
Character/Double/String statics found the surface near-complete — only
164-
`java.lang.String/join` (delim + Iterable form) was missing. Filled it in
165-
String.zig (realizes the seqable via clojure.core/vec, then index-walks —
166-
no in-loop GC alloc); non-string element raises a ClassCastException-class
167-
error like JVM; nil/empty → "". Corpus: test/diff/clj_corpus/string_join_static.txt
168-
(7 golden pairs). The 3+-arg varargs form is rejected like JVM Clojure.
169-
DRAIN 2 (2026-06-25): the unsigned-arithmetic + JDK-exact static cluster.
170-
Added Long/{parseUnsignedLong,sum,divideUnsigned,remainderUnsigned,
171-
compareUnsigned,toUnsignedString}, Integer/{parseUnsignedInt,sum,
172-
divideUnsigned,remainderUnsigned,compareUnsigned,toUnsignedLong,
173-
toUnsignedString}, Math/{absExact,multiplyHigh,clamp} (+ parse.zig
174-
parseUnsigned leaf). All happy + error paths match clj (ArithmeticException
175-
/ IllegalArgumentException / NumberFormatException classes align). Corpus:
176-
test/diff/clj_corpus/unsigned_int_statics.txt (24 golden pairs).
177-
DRAIN 3 (2026-06-25): the Character surrogate/codePoint cluster + Boolean.
178-
Added Character/{charCount,isSurrogate,isHighSurrogate,isLowSurrogate,
179-
isSurrogatePair,highSurrogate,lowSurrogate,toCodePoint,isBmpCodePoint,
180-
isValidCodePoint,isSupplementaryCodePoint,isISOControl} (pure Unicode-spec
181-
arithmetic, no Unicode-DB lookup) + Boolean/{compare,hashCode}. cljw chars
182-
ARE codepoints so surrogate values hold numerically (matches clj). Corpus:
183-
test/diff/clj_corpus/char_codepoint_bool_statics.txt (27 golden pairs).
184-
DEFERRED (Unicode-DB-heavy, low ROI): Character/{getType,isDefined,
185-
isMirrored,isJavaIdentifierStart,…} need the full Unicode database.
186-
DRAIN 4 (2026-06-25): Double IEEE-754 bit conversion. Added Double/
187-
{doubleToLongBits,doubleToRawLongBits,longBitsToDouble,hashCode} (reuses
188-
the existing private doubleToLongBits NaN-canonicalizer; bits box as BigInt
189-
per D-165, exact). NaN canonical 0x7ff8…/raw split matches clj. Corpus:
190-
test/diff/clj_corpus/double_bit_conversion.txt (14 golden pairs). NEXT:
191-
java.util.Objects/* is a whole new surface (separate unit); the i48→u64
192-
bitcast widening repeats across surfaces (a simplify candidate).
193-
DRAIN 5 (2026-06-25): NEW surface java.util.Objects (runtime/java/util/
194-
Objects.zig + _host_api + compat_tiers). isNull/nonNull/equals (valueEqual)/
195-
requireNonNull (1+2 arg, throws a REAL NullPointerException via the ex_info
196-
host-throw path)/requireNonNullElse/toString (1+2 arg). hash/hashCode
197-
deferred (Java-hashCode ≠ cljw value hash). Corpus: objects_static.txt
198-
(22 golden, incl. catch-by-NullPointerException error cases). REMAINING
199-
simplify candidate: the i48→u64 bitcast widening repeated across surfaces.
200-
DRAIN 6 (2026-06-25): CLOSE-OUT of the java.lang scalar static surface
201-
(Discipline 2 drive-to-zero). Long/Integer/Character hashCode (Java's
202-
bit formulas) + Math/{ceilDiv,ceilMod,divideExact,floorDivExact,fma}.
203-
Long/Integer divideExact were NOT added (they don't exist in Java — clj
204-
compile-errors them, so not a gap). Corpus: scalar_static_close.txt (18
205-
golden). The java.lang scalar static surface (Long/Integer/Double/Boolean/
206-
Character/Math) is now clj-parity COMPLETE for the common surface; the
207-
remaining interop gaps are other packages (java.util.Arrays/Collections,
208-
String static char[] forms) — separate units, lower value.
209-
DRAIN 7 (2026-07-07): NEW surface java.util.Arrays (runtime/java/util/
210-
Arrays.zig + _host_api + compat_tiers keyword `arrays`). asList (returns
211-
the D-294 ArrayList host surface — the JVM fixed-size-VIEW aliasing is
212-
deliberately not mirrored) / toString ("[e1, e2, …]"; nil arg -> "null") /
213-
copyOf (nil-padded like Java's null pad) / sort (in-place natural
214-
`compare`, stable) / equals (element value equality, nil-safe) / fill.
215-
binarySearch / hashCode / typed-primitive overloads omitted until a real
216-
consumer (dead-interop lesson). Corpus: objects_static.txt +9 goldens
217-
(clj-byte-matched).
218-
DRAIN 8 (2026-07-07): NEW surface java.util.Collections (runtime/java/
219-
util/Collections.zig, keyword `collections_util`). emptyList /
220-
singletonList (fresh MUTABLE ArrayList hosts — no unmodifiable wrapper
221-
in cljw; faking the mutation-throw would lie, noted in the surface doc) /
222-
sort + reverse (in-place on an ArrayList via the new pub
223-
ArrayList.isArrayList/itemsOf accessors) / max + min (natural compare
224-
over vector/array/ArrayList). unmodifiable*/shuffle/synchronized*
225-
omitted until a real consumer. Corpus: objects_static.txt +6 goldens.
226-
REMAINING: String static char[] forms — the last drain of this row.
227-
category: "interop"
228-
quality_floor: "public-ization: java-interop static-member gaps (ADR-0166)"
229-
barrier: |-
230-
Repeatable drain (ADR-0166). Code-touching → diff oracle + corpus apply.
231-
Per /continue: catalog or fill one interop surface's missing statics.
232-
SSOT: ADR-0166 row 5.
233-
last_reviewed: "2026-06-25"
234155
- id: "D-527"
235156
status: |-
236157
USER RECONFIRMED 2026-07-02: direction stands — Clojure stdlib near-total equivalence is the goal; NOT a priority raise (easiest-first order unchanged).
@@ -6546,6 +6467,12 @@ discharged:
65466467
barrier: "DISCHARGED (was: transient overflow panic, repro lost-then-found)."
65476468
last_reviewed: "2026-07-07"
65486469

6470+
- id: "D-526"
6471+
status: "DISCHARGED 2026-07-07 — the Java-interop static-member catalog+fill campaign is COMPLETE across 9 drains. DRAIN 9 (final): String char[] forms — `String/valueOf` gained the char[] and char[]/offset/count overloads (was falling into the generic \"#<array>\" render — a real bug), `String/copyValueOf` (1+3 arg, the JVM alias) registered, `(String. (char-array …))` verified already working (the campaign's stale REMAINING note). Corpus string_join_static +5 clj-byte-matched goldens. FULL CAMPAIGN LEDGER: D1 java.lang scalar sweep (String/join was the only gap — filled); D2 unsigned-arithmetic + JDK-edge Long/Integer statics; D3-D4 (see history); D5 java.util.Objects (new surface); D6 java.lang scalar close-out (hashCode formulas + Math ceilDiv family); D7 java.util.Arrays (new surface: asList/toString/copyOf/sort/equals/fill); D8 java.util.Collections (new surface: emptyList/singletonList/sort/reverse/max/min); D9 String char[] forms. Deliberate omissions (all doc'd at their surfaces, dead-interop lesson): Arrays binarySearch/hashCode/typed-primitive overloads; Collections unmodifiable*/shuffle/synchronized*; Objects hash/hashCode (Java hashCode contract ≠ cljw value hash). Any future static gap is a NEW discovery against a then-current consumer, not this row."
6472+
category: "interop"
6473+
barrier: "DISCHARGED (was: static-member gap catalog+fill, ADR-0166 row 5)."
6474+
last_reviewed: "2026-07-07"
6475+
65496476
conventions: |-
65506477
- Structure: two top-level lists — `active:` and `discharged:`. Active entries carry id / status / category / barrier (+ optional quality_floor / last_reviewed); discharged entries carry id / discharged_at / resolution. Edit as YAML (no Markdown table to align).
65516478
- New debt: grep `.dev/debt.yaml` for class-overlap before adding (per `.claude/rules/debt_dedup.md`).

‎data/compat_tiers.yaml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,8 @@ tiers:
209209
clojure_peer_vars: []
210210
# java.lang.StringBuilder — mutable growable buffer (ADR-0114, host_instance).
211211
# hiccup's compiler builds HTML with it. java.lang.String also has the
212-
# static surface `valueOf` + `join` (delim+Iterable) in String.zig
212+
# static surface `valueOf` (incl. the char[] + char[]/offset/count
213+
# overloads) + `copyValueOf` + `join` (delim+Iterable) in String.zig
213214
# (instances stay installNativeMethods).
214215
- fqn: java.lang.StringBuilder
215216
cljw_ns: cljw.java.lang.StringBuilder

‎src/runtime/java/lang/String.zig‎

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -594,14 +594,59 @@ const print_mod = @import("../../print.zig");
594594
/// `(String/valueOf x)` — Java `String.valueOf(Object)`: the str-rendering of
595595
/// `x`, with `nil` → `"null"` (JVM valueOf(null) is the literal "null").
596596
fn valueOf(rt: *Runtime, env: *Env, args: []const Value, loc: SourceLocation) anyerror!Value {
597-
try error_catalog.checkArity("String/valueOf", args, 1, loc);
597+
if (args.len != 1 and args.len != 3)
598+
return error_catalog.raise(.arity_not_expected, loc, .{ .fn_name = "String/valueOf", .expected = 1, .got = args.len });
598599
if (args[0].tag() == .nil) return string_collection.alloc(rt, "null");
600+
// Java String.valueOf(char[]) / (char[], offset, count) — the char-array
601+
// overloads build the string FROM the characters, not a render of the
602+
// array object (D-526; was falling into the generic "#<array>" arm).
603+
if (java_array.isArray(args[0])) return charsToString(rt, args, "String/valueOf", loc);
604+
try error_catalog.checkArity("String/valueOf", args, 1, loc);
599605
var aw: std.Io.Writer.Allocating = .init(rt.gpa);
600606
defer aw.deinit();
601607
try print_mod.writeStrValue(rt, env, &aw.writer, args[0]);
602608
return string_collection.alloc(rt, aw.writer.buffered());
603609
}
604610

611+
/// Shared body of the char-array string builders: `(f arr)` uses the whole
612+
/// array, `(f arr offset count)` a validated window (JVM
613+
/// StringIndexOutOfBounds → cljw bounds error). Non-char elements raise a
614+
/// type error (a Java char[] cannot hold anything else).
615+
fn charsToString(rt: *Runtime, args: []const Value, comptime fn_name: []const u8, loc: SourceLocation) anyerror!Value {
616+
const items = java_array.asArray(args[0]).items();
617+
var start: usize = 0;
618+
var count: usize = items.len;
619+
if (args.len == 3) {
620+
const off = try error_catalog.expectInteger(args[1], fn_name, loc);
621+
const cnt = try error_catalog.expectInteger(args[2], fn_name, loc);
622+
if (off < 0 or cnt < 0 or off + cnt > items.len)
623+
return error_catalog.raise(.index_out_of_range, loc, .{ .fn_name = fn_name });
624+
start = @intCast(off);
625+
count = @intCast(cnt);
626+
}
627+
var aw: std.Io.Writer.Allocating = .init(rt.gpa);
628+
defer aw.deinit();
629+
for (items[start .. start + count]) |e| {
630+
if (e.tag() != .char)
631+
return error_catalog.raise(.type_arg_invalid, loc, .{ .fn_name = fn_name, .expected = "a char array", .actual = @tagName(e.tag()) });
632+
var buf: [4]u8 = undefined;
633+
const n = std.unicode.utf8Encode(e.asChar(), &buf) catch 0;
634+
try aw.writer.writeAll(buf[0..n]);
635+
}
636+
return string_collection.alloc(rt, aw.writer.buffered());
637+
}
638+
639+
/// `(String/copyValueOf chars)` / `(String/copyValueOf chars offset count)` —
640+
/// JVM aliases of the char-array valueOf overloads.
641+
fn copyValueOf(rt: *Runtime, env: *Env, args: []const Value, loc: SourceLocation) anyerror!Value {
642+
_ = env;
643+
if (args.len != 1 and args.len != 3)
644+
return error_catalog.raise(.arity_not_expected, loc, .{ .fn_name = "String/copyValueOf", .expected = 1, .got = args.len });
645+
if (!java_array.isArray(args[0]))
646+
return error_catalog.raise(.type_arg_invalid, loc, .{ .fn_name = "String/copyValueOf", .expected = "a char array", .actual = @tagName(args[0].tag()) });
647+
return charsToString(rt, args, "String/copyValueOf", loc);
648+
}
649+
605650
/// `(String/join delim coll)` — Java `String.join(CharSequence, Iterable)`: the
606651
/// elements of `coll` concatenated with `delim` between adjacent pairs. Each
607652
/// element must be a string; JVM requires `CharSequence` and throws
@@ -685,10 +730,11 @@ fn stringCtor(rt: *Runtime, env: *Env, args: []const Value, loc: SourceLocation)
685730

686731
fn initStringStatics(td: *type_descriptor.TypeDescriptor, gpa: std.mem.Allocator) anyerror!void {
687732
if (td.method_table.len != 0) return; // idempotent
688-
const entries = try gpa.alloc(type_descriptor.TypeDescriptor.MethodEntry, 3);
733+
const entries = try gpa.alloc(type_descriptor.TypeDescriptor.MethodEntry, 4);
689734
entries[0] = .{ .protocol_name = "", .method_name = try gpa.dupe(u8, "valueOf"), .method_val = Value.initBuiltinFn(&valueOf) };
690735
entries[1] = .{ .protocol_name = "", .method_name = try gpa.dupe(u8, "<init>"), .method_val = Value.initBuiltinFn(&stringCtor) };
691736
entries[2] = .{ .protocol_name = "", .method_name = try gpa.dupe(u8, "join"), .method_val = Value.initBuiltinFn(&join) };
737+
entries[3] = .{ .protocol_name = "", .method_name = try gpa.dupe(u8, "copyValueOf"), .method_val = Value.initBuiltinFn(&copyValueOf) };
692738
td.method_table = entries;
693739
}
694740

‎test/diff/clj_corpus/string_join_static.txt‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,13 @@
1212
;;=> "z"
1313
(String/join ", " ["x" "y" "z"])
1414
;;=> "x, y, z"
15+
(String/valueOf (char-array [\a \b \c]))
16+
;;=> "abc"
17+
(String/valueOf (char-array [\a \b \c]) 1 2)
18+
;;=> "bc"
19+
(String/copyValueOf (char-array [\x \y]))
20+
;;=> "xy"
21+
(String/copyValueOf (char-array [\p \q \r]) 0 2)
22+
;;=> "pq"
23+
(String. (char-array [\a \b]))
24+
;;=> "ab"

0 commit comments

Comments
 (0)