Skip to content

Commit 52a0929

Browse files
committed
feat(interop): D-526 drain 8 — java.util.Collections common statics
Smell-audited: 1: emptyList/singletonList return MUTABLE ArrayList hosts — cljw has no unmodifiable wrapper and faking the mutation-throw would lie; unmodifiable*/shuffle/synchronized* stay omitted until a real consumer (dead-interop lesson), all noted in the surface doc. New surface runtime/java/util/Collections.zig (keyword collections_util): emptyList / singletonList / sort + reverse (in-place on an ArrayList via new pub ArrayList.isArrayList/itemsOf accessors) / max + min (natural compare over vector/array/ArrayList). Corpus objects_static +6 clj-byte-matched goldens.
1 parent d424436 commit 52a0929

6 files changed

Lines changed: 214 additions & 2 deletions

File tree

‎.dev/debt.yaml‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,8 +214,16 @@ active:
214214
`compare`, stable) / equals (element value equality, nil-safe) / fill.
215215
binarySearch / hashCode / typed-primitive overloads omitted until a real
216216
consumer (dead-interop lesson). Corpus: objects_static.txt +9 goldens
217-
(clj-byte-matched). REMAINING: java.util.Collections statics + String
218-
static char[] forms — next drains of this row.
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.
219227
category: "interop"
220228
quality_floor: "public-ization: java-interop static-member gaps (ADR-0166)"
221229
barrier: |-

‎data/compat_tiers.yaml‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,19 @@ tiers:
285285
surface: runtime/java/util/Arrays.zig
286286
methods: [asList, toString, copyOf, sort, equals, fill]
287287
clojure_peer_vars: []
288+
# java.util.Collections — common statics (D-526). emptyList/singletonList
289+
# return MUTABLE ArrayList hosts (no unmodifiable wrapper in cljw —
290+
# faking immutability would lie); sort/reverse are in-place on an
291+
# ArrayList; max/min take vector/array/ArrayList.
292+
- fqn: java.util.Collections
293+
cljw_ns: cljw.java.util.Collections
294+
keyword: collections_util
295+
tier: A
296+
phase: 14
297+
files:
298+
surface: runtime/java/util/Collections.zig
299+
methods: [emptyList, singletonList, sort, reverse, max, min]
300+
clojure_peer_vars: []
288301
# java.util.HashMap — mutable key→value map (D-425, host_instance backing a
289302
# cljw map Value; one map Value GC-traced via host_trace, reuses cljw
290303
# hashing/equality). seq yields cljw MapEntry pairs (AD-032).

‎src/runtime/java/_host_api.zig‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ const java_surfaces = [_]type{
120120
@import("util/UUID.zig"),
121121
@import("util/Objects.zig"),
122122
@import("util/Arrays.zig"),
123+
@import("util/Collections.zig"),
123124
// Matcher's backing impl is shipped by Pattern's regex/match.zig.
124125
@import("util/regex/Matcher.zig"),
125126
@import("util/regex/Pattern.zig"),

‎src/runtime/java/util/ArrayList.zig‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,20 @@ fn listOf(recv: Value) *ValueList {
4646
return @ptrFromInt(host_instance.asHostInstance(recv).state[0]);
4747
}
4848

49+
/// Descriptor-identity check for a java.util.ArrayList host instance
50+
/// (the uriQ pattern — no cross-surface import needed by callers).
51+
pub fn isArrayList(v: Value) bool {
52+
return v.tag() == .host_instance and al_descriptor != null and
53+
host_instance.asHostInstance(v).descriptor == al_descriptor.?;
54+
}
55+
56+
/// The mutable element slice of an ArrayList host instance — the
57+
/// java.util.Collections statics (sort/reverse, D-526) mutate through
58+
/// this. Caller must have verified `isArrayList`.
59+
pub fn itemsOf(recv: Value) []Value {
60+
return listOf(recv).items;
61+
}
62+
4963
/// Print hook (D-468): a java.util.ArrayList prints by content like clj
5064
/// (`[1 2]`). The backing is a raw `*std.ArrayList(Value)`, so rebuild a native
5165
/// vector from its elements for the printer.
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// SPDX-License-Identifier: EPL-2.0
2+
//! Java surface for `java.util.Collections` static methods (D-526).
3+
//!
4+
//! Backend: impl-only
5+
//! Impl deps: none
6+
//! Clojure peer: clojure.core/sort, clojure.core/reverse, clojure.core/max, clojure.core/min
7+
//!
8+
//! The common static surface: `emptyList` / `singletonList` (fresh
9+
//! java.util.ArrayList host instances — the JVM's IMMUTABLE list types are
10+
//! deliberately not mirrored; cljw has no unmodifiable wrapper, and no cljw
11+
//! consumer depends on the mutation-throw) / `sort` + `reverse` (in-place on
12+
//! an ArrayList) / `max` + `min` (natural `compare` over a vector / Java
13+
//! array / ArrayList). `unmodifiable*` / `shuffle` / `synchronized*` are
14+
//! intentionally omitted until a real consumer needs them (dead-interop
15+
//! lesson) — an unmodifiable view faked as a mutable list would be a lie.
16+
17+
const std = @import("std");
18+
const host_api = @import("../_host_api.zig");
19+
const type_descriptor = @import("../../type_descriptor.zig");
20+
const Value = @import("../../value/value.zig").Value;
21+
const Runtime = @import("../../runtime.zig").Runtime;
22+
const Env = @import("../../env.zig").Env;
23+
const SourceLocation = @import("../../error/info.zig").SourceLocation;
24+
const error_catalog = @import("../../error/catalog.zig");
25+
const array_list_surface = @import("ArrayList.zig");
26+
const java_array = @import("../../collection/java_array.zig");
27+
const vector_mod = @import("../../collection/vector.zig");
28+
const compare_mod = @import("../../compare.zig");
29+
30+
/// `(java.util.Collections/emptyList)` — a fresh empty ArrayList (see the
31+
/// module doc's immutability note). JVM ref: Collections#emptyList.
32+
fn emptyList(rt: *Runtime, env: *Env, args: []const Value, loc: SourceLocation) anyerror!Value {
33+
try error_catalog.checkArity("java.util.Collections/emptyList", args, 0, loc);
34+
return array_list_surface.initArrayList(rt, env, &.{}, loc);
35+
}
36+
37+
/// `(java.util.Collections/singletonList x)` — a fresh 1-element ArrayList.
38+
fn singletonList(rt: *Runtime, env: *Env, args: []const Value, loc: SourceLocation) anyerror!Value {
39+
try error_catalog.checkArity("java.util.Collections/singletonList", args, 1, loc);
40+
const vec = try vector_mod.fromSlice(rt, args[0..1]);
41+
return array_list_surface.initArrayList(rt, env, &.{vec}, loc);
42+
}
43+
44+
fn expectArrayList(v: Value, fn_name: []const u8, loc: SourceLocation) !void {
45+
if (!array_list_surface.isArrayList(v))
46+
return error_catalog.raise(.type_arg_invalid, loc, .{ .fn_name = fn_name, .expected = "a java.util.ArrayList", .actual = @tagName(v.tag()) });
47+
}
48+
49+
const SortCtx = struct {
50+
rt: *Runtime,
51+
loc: SourceLocation,
52+
err: ?anyerror = null,
53+
};
54+
55+
fn lessThan(ctx: *SortCtx, a: Value, b: Value) bool {
56+
if (ctx.err != null) return false;
57+
const ord = compare_mod.valueCompare(ctx.rt, a, b, ctx.loc) catch |e| {
58+
ctx.err = e;
59+
return false;
60+
};
61+
return ord == .lt;
62+
}
63+
64+
/// `(java.util.Collections/sort list)` — IN-PLACE natural-order stable sort
65+
/// of an ArrayList; returns nil (void in Java).
66+
fn sortFn(rt: *Runtime, env: *Env, args: []const Value, loc: SourceLocation) anyerror!Value {
67+
_ = env;
68+
try error_catalog.checkArity("java.util.Collections/sort", args, 1, loc);
69+
try expectArrayList(args[0], "java.util.Collections/sort", loc);
70+
var ctx: SortCtx = .{ .rt = rt, .loc = loc };
71+
std.mem.sort(Value, array_list_surface.itemsOf(args[0]), &ctx, lessThan);
72+
if (ctx.err) |e| return e;
73+
return .nil_val;
74+
}
75+
76+
/// `(java.util.Collections/reverse list)` — IN-PLACE reversal of an
77+
/// ArrayList; returns nil (void in Java).
78+
fn reverseFn(rt: *Runtime, env: *Env, args: []const Value, loc: SourceLocation) anyerror!Value {
79+
_ = rt;
80+
_ = env;
81+
try error_catalog.checkArity("java.util.Collections/reverse", args, 1, loc);
82+
try expectArrayList(args[0], "java.util.Collections/reverse", loc);
83+
std.mem.reverse(Value, array_list_surface.itemsOf(args[0]));
84+
return .nil_val;
85+
}
86+
87+
/// The realized element slice of a vector / Java array / ArrayList arg, or
88+
/// a type error. Vectors need a scratch copy (trie, not flat); the caller
89+
/// frees it via `owned`.
90+
const Elems = struct { items: []const Value, owned: ?[]Value = null };
91+
fn elemsOf(rt: *Runtime, v: Value, fn_name: []const u8, loc: SourceLocation) !Elems {
92+
if (v.tag() == .vector) {
93+
const n = vector_mod.count(v);
94+
const buf = try rt.gpa.alloc(Value, n);
95+
var i: u32 = 0;
96+
while (i < n) : (i += 1) buf[i] = vector_mod.nth(v, i);
97+
return .{ .items = buf, .owned = buf };
98+
}
99+
if (java_array.isArray(v)) return .{ .items = java_array.asArray(v).items() };
100+
if (array_list_surface.isArrayList(v)) return .{ .items = array_list_surface.itemsOf(v) };
101+
return error_catalog.raise(.type_arg_invalid, loc, .{ .fn_name = fn_name, .expected = "a vector, Java array, or ArrayList", .actual = @tagName(v.tag()) });
102+
}
103+
104+
fn extremum(rt: *Runtime, args: []const Value, comptime fn_name: []const u8, comptime want: std.math.Order, loc: SourceLocation) anyerror!Value {
105+
try error_catalog.checkArity(fn_name, args, 1, loc);
106+
const es = try elemsOf(rt, args[0], fn_name, loc);
107+
defer if (es.owned) |b| rt.gpa.free(b);
108+
if (es.items.len == 0)
109+
return error_catalog.raise(.type_arg_invalid, loc, .{ .fn_name = fn_name, .expected = "a non-empty collection", .actual = "empty collection" });
110+
var best = es.items[0];
111+
for (es.items[1..]) |e| {
112+
if (try compare_mod.valueCompare(rt, e, best, loc) == want) best = e;
113+
}
114+
return best;
115+
}
116+
117+
/// `(java.util.Collections/max coll)` — natural-order maximum.
118+
fn maxFn(rt: *Runtime, env: *Env, args: []const Value, loc: SourceLocation) anyerror!Value {
119+
_ = env;
120+
return extremum(rt, args, "java.util.Collections/max", .gt, loc);
121+
}
122+
123+
/// `(java.util.Collections/min coll)` — natural-order minimum.
124+
fn minFn(rt: *Runtime, env: *Env, args: []const Value, loc: SourceLocation) anyerror!Value {
125+
_ = env;
126+
return extremum(rt, args, "java.util.Collections/min", .lt, loc);
127+
}
128+
129+
fn initCollections(td: *type_descriptor.TypeDescriptor, gpa: std.mem.Allocator) anyerror!void {
130+
if (td.method_table.len != 0) return; // idempotent re-run
131+
const specs = .{
132+
.{ "emptyList", &emptyList },
133+
.{ "singletonList", &singletonList },
134+
.{ "sort", &sortFn },
135+
.{ "reverse", &reverseFn },
136+
.{ "max", &maxFn },
137+
.{ "min", &minFn },
138+
};
139+
const entries = try gpa.alloc(type_descriptor.TypeDescriptor.MethodEntry, specs.len);
140+
inline for (specs, 0..) |spec, i| {
141+
entries[i] = .{
142+
.protocol_name = "",
143+
.method_name = try gpa.dupe(u8, spec[0]),
144+
.method_val = Value.initBuiltinFn(spec[1]),
145+
};
146+
}
147+
td.method_table = entries;
148+
}
149+
150+
pub const ___HOST_EXTENSION: host_api.Extension = .{
151+
.cljw_ns = "cljw.java.util.Collections",
152+
.descriptor = &descriptor,
153+
.init = &initCollections,
154+
};
155+
156+
var descriptor: type_descriptor.TypeDescriptor = .{
157+
.fqcn = "cljw.java.util.Collections",
158+
.kind = .native,
159+
.field_layout = null,
160+
.protocol_impls = &.{},
161+
.method_table = &.{},
162+
.parent = null,
163+
.meta = .nil_val,
164+
};

‎test/diff/clj_corpus/objects_static.txt‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,15 @@
6060
;;=> false
6161
(let [a (to-array [1 2 3])] (java.util.Arrays/fill a :x) (vec a))
6262
;;=> [:x :x :x]
63+
(vec (java.util.Collections/emptyList))
64+
;;=> []
65+
(vec (java.util.Collections/singletonList :x))
66+
;;=> [:x]
67+
(let [l (java.util.ArrayList. [3 1 2])] (java.util.Collections/sort l) (vec l))
68+
;;=> [1 2 3]
69+
(let [l (java.util.ArrayList. [1 2 3])] (java.util.Collections/reverse l) (vec l))
70+
;;=> [3 2 1]
71+
(java.util.Collections/max [3 1 2])
72+
;;=> 3
73+
(java.util.Collections/min ["b" "a" "c"])
74+
;;=> "a"

0 commit comments

Comments
 (0)