Skip to content

Commit d33e687

Browse files
committed
feat(array): multi-dim aget/aset land, closing D-446
`(aget a i j)` and `(aset a i j v)` raised arity errors where clj accepts the variadic multidim forms — and nested arrays exist (`(make-array Long 2 3)`), so the gap was reachable. `aget`/`aset` are now variadic in the builtin itself: each extra index walks into the nested array, the final aset pair is the ordinary store, exactly the recursion clj's `(apply aget (aget a i) idxs)` composes. The D-446 barrier sketched a core.clj-wrapper shape instead; the builtin recursion is smaller and leaves the hot 2-arg/3-arg path untouched, so the sketch was not obeyed (rows are memos, not contracts). The 8 typed `aset-*` gained clj's multidim arm in core.clj: `([a i j & more] (apply aset-X (aget a i) j more))`. Mechanised per the row's own anti-D-177 mandate: 5 clj-byte-matched golden pairs in the arity_envelopes corpus (multidim get/set + catchable out-of-bounds), 6 checks in phase14_arity_parity.sh (44/44) including AD-019 pins — the typed aset-* store without element-type checks because cljw arrays are type-erased, where clj on a boxed array throws IllegalArgumentException. The sweep surfaced those three DIFFs and AD-019 already covers them verbatim ("a typed-array constructor / aset does not coerce or type-check"). D-446 discharged. Smell-audited: 1: the barrier's sketched shape was re-derived rather than followed — Reservation-as-bias check came out the other way this time (the smaller shape is also the finished form, because the variadic IS the builtin's natural signature).
1 parent 59c11e6 commit d33e687

5 files changed

Lines changed: 77 additions & 22 deletions

File tree

‎.dev/debt.yaml‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1354,12 +1354,6 @@ active:
13541354
category: "perf"
13551355
barrier: "none hard — O-023 already ships the effective form. The 正しい姿 unification is a deliberate refactor that touches the gate-green O-023 path; a transducer stack (map's fn) ≠ a pred vector, so the unified representation needs design. Take up alongside a reduce-heavy bench that the current `[xform coll]` descriptor under-serves, or when the lazy-walk collapse is revisited with a real workload. Not blocking; the existing fused-reduce is correct + fast."
13561356
last_reviewed: "2026-06-15"
1357-
- id: "D-446"
1358-
status: "PARTIAL (2026-06-18 — MID/UNDER/over-arity sweep COMPLETE; only multidim residual open). An empirical both-sides arity probe (193 common clojure.core fns × arities 0-7, BOTH runtimes catch clojure.lang.ArityException; harness + data in private/arity_sweep/) found 22 over-strict fns (cljw REJECTS an arity clj accepts; ZERO over-lenient — the 0-arg class from the 2026-06-16 pass below stays clean). 14 fixed bug→fix (F-011): n-ary map/mapv/mapcat/list* (declare-d top-level -map-n-step + -spread, D-147 no-named-local-fn), bit-and-not variadic fold, resolve 2-arg env form (a local named in env → nil) [commit 982890aa]; the 8 typed array ctors gained the 2-arg (X-array size init-or-seq) form (uniform -array-from2 — value init fills, sequential init copies; the byte/short/char NUMBER-init divergence vs clj IS recorded as AD-036 deriving from AD-019, Devil-advocate-forked) [array commit]. Mechanised: test/diff/clj_corpus/arity_envelopes.txt (22 cases, all clj-byte-matched) + e2e phase14_arity_parity.sh (35/0). REMAINING OPEN SCOPE OF THIS ROW = multidim array indexing: (aget a i j …) and (aset a i j … v) + the typed aset-* multidim forms diverge at arity 3+/4+ (8 aset-* fns; aget is a builtin the ns-publics fn? sweep did not enumerate) — see barrier. EARLIER 0-ARG PASS (2026-06-16, kept for history): a fresh-context subagent probed the 0-ARG boundary (the not= precedent) + 22 over-arity + 20 extra-0-arg; found 10 divergences all aligned to clj — cljw-LENIENT = < > <= >= distinct? every-pred some-fn now throw; cljw-STRICT into + conj! now return clj values. ORIGINAL (2026-06-15, user-directed): arity-divergence audit — enumerate cljw fn arities, diff vs clj, classify each bug→fix or accepted→AD-NNN; big-bang-then-closed (clj_diff_sweep Discipline 2)."
1359-
category: "clj-parity"
1360-
quality_floor: "clj-parity: fn arity envelopes match clj (no silent over-lenient / missing arities)"
1361-
barrier: "none for the common surface (DONE). Remaining = multidim array indexing, a distinct rare feature: cljw aget/aset are fixed 2-arg/3-arg Zig builtins, so (aget a i j)/(aset-int a i j v) raise an arity error where clj accepts the variadic multidim form. Nested arrays DO exist ((make-array nil 2 2) → (aget a 0) is a nested array), so it is reachable. array.zig docstring states the F-009 intent: multi-dim aget+aset are a core.clj variadic over the minimal builtins — but that .clj layer was never built. The clean form renames the builtins rt/__aget/rt/__aset + adds core.clj aget/aset variadic wrappers + aset-* multidim arms, but routes the HOT 2-arg/3-arg path through a .clj fn (perf-vs-F-009-layering call for the array/perf owner). Rare in real Clojure → low priority; mechanise via the arity_envelopes corpus when landed (anti-D-177)."
1362-
last_reviewed: "2026-06-18"
13631357
- id: "D-447"
13641358
status: "open (2026-06-15, regex equivalence audit / ADR-0147). Regex parity gaps cljw's Pike-NFA matcher does NOT support — all currently RAISE NotImplemented (never silently wrong; the 48-golden `test/diff/clj_corpus/regex_equivalence.txt` deliberately excludes them; reluctant quantifiers `*? +? ?? {n,m}?` LANDED 2026-06-16 — split-priority swap pairing with the cut-on-match leftmost-first fix + a dedicated matchFull loop for the re-matches full-span case, corpus regex_equivalence): lookbehind `(?<=)`/`(?<!)`, named groups `(?<name>)`, backreferences `\\1`, `\\A \\z \\Z`, POSIX `[[:class:]]`. (Lookahead `(?=)`/`(?!)` and all four flags `(?imsx)` DO work.) NOT campaign-blocking — addressed only when a real workload needs them OR opportunistically alongside the ADR-0147 regex-perf engine work (reluctant quantifiers are a cheap split-priority swap that pairs naturally with the matcher changes; backrefs would need a backtracking fallback cljw rejects per ADR-0031, so likely stay a documented gap / RE2 posture). When a gap closes, MOVE its goldens into the equivalence corpus in the same cycle (corpus-backed discharge, anti-D-177). Also a depth-1 doc fix: `compile.zig:22-23` claims 'lookaround' unsupported but lookahead works."
13651359
category: "clj-parity"
@@ -2702,6 +2696,12 @@ standing:
27022696
barrier: "open after the D-440 arc completes (R4 + R5 must first rewrite the bulk of 'Phase N' citations to gap-area terms). Premature adoption = ~70 dangling citations with no 1:1 redirect. Trigger: D-440 R4/R5 done + a user nod to re-architect the planning axis (it is depth-4, the project's organizing metaphor)."
27032697
last_reviewed: "2026-06-15"
27042698
discharged:
2699+
- id: "D-446"
2700+
status: "PARTIAL (2026-06-18 — MID/UNDER/over-arity sweep COMPLETE; only multidim residual open). An empirical both-sides arity probe (193 common clojure.core fns × arities 0-7, BOTH runtimes catch clojure.lang.ArityException; harness + data in private/arity_sweep/) found 22 over-strict fns (cljw REJECTS an arity clj accepts; ZERO over-lenient — the 0-arg class from the 2026-06-16 pass below stays clean). 14 fixed bug→fix (F-011): n-ary map/mapv/mapcat/list* (declare-d top-level -map-n-step + -spread, D-147 no-named-local-fn), bit-and-not variadic fold, resolve 2-arg env form (a local named in env → nil) [commit 982890aa]; the 8 typed array ctors gained the 2-arg (X-array size init-or-seq) form (uniform -array-from2 — value init fills, sequential init copies; the byte/short/char NUMBER-init divergence vs clj IS recorded as AD-036 deriving from AD-019, Devil-advocate-forked) [array commit]. Mechanised: test/diff/clj_corpus/arity_envelopes.txt (22 cases, all clj-byte-matched) + e2e phase14_arity_parity.sh (35/0). REMAINING OPEN SCOPE OF THIS ROW = multidim array indexing: (aget a i j …) and (aset a i j … v) + the typed aset-* multidim forms diverge at arity 3+/4+ (8 aset-* fns; aget is a builtin the ns-publics fn? sweep did not enumerate) — see barrier. EARLIER 0-ARG PASS (2026-06-16, kept for history): a fresh-context subagent probed the 0-ARG boundary (the not= precedent) + 22 over-arity + 20 extra-0-arg; found 10 divergences all aligned to clj — cljw-LENIENT = < > <= >= distinct? every-pred some-fn now throw; cljw-STRICT into + conj! now return clj values. ORIGINAL (2026-06-15, user-directed): arity-divergence audit — enumerate cljw fn arities, diff vs clj, classify each bug→fix or accepted→AD-NNN; big-bang-then-closed (clj_diff_sweep Discipline 2)."
2701+
category: "clj-parity"
2702+
quality_floor: "clj-parity: fn arity envelopes match clj (no silent over-lenient / missing arities)"
2703+
barrier: none — discharged
2704+
last_reviewed: "2026-08-05"
27052705
- id: "D-571"
27062706
status: |-
27072707
DISCHARGED 2026-08-05 — the budget follows the work (thread-ownership

‎src/lang/clj/clojure/core.clj‎

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2573,14 +2573,30 @@
25732573

25742574
;; aset-* typed setters. clj coerces the value to the element type; cljw applies
25752575
;; only the value-changing modular wraps (byte/short/char), the rest are aset.
2576-
(defn aset-int [a i v] (aset a i v))
2577-
(defn aset-long [a i v] (aset a i v))
2578-
(defn aset-float [a i v] (aset a i v))
2579-
(defn aset-double [a i v] (aset a i v))
2580-
(defn aset-boolean [a i v] (aset a i (boolean v)))
2581-
(defn aset-byte [a i v] (aset a i (-to-byte v)))
2582-
(defn aset-short [a i v] (aset a i (-to-short v)))
2583-
(defn aset-char [a i v] (aset a i (char v)))
2576+
(defn aset-int
2577+
([a i v] (aset a i v))
2578+
([a i j & more] (apply aset-int (aget a i) j more)))
2579+
(defn aset-long
2580+
([a i v] (aset a i v))
2581+
([a i j & more] (apply aset-long (aget a i) j more)))
2582+
(defn aset-float
2583+
([a i v] (aset a i v))
2584+
([a i j & more] (apply aset-float (aget a i) j more)))
2585+
(defn aset-double
2586+
([a i v] (aset a i v))
2587+
([a i j & more] (apply aset-double (aget a i) j more)))
2588+
(defn aset-boolean
2589+
([a i v] (aset a i (boolean v)))
2590+
([a i j & more] (apply aset-boolean (aget a i) j more)))
2591+
(defn aset-byte
2592+
([a i v] (aset a i (-to-byte v)))
2593+
([a i j & more] (apply aset-byte (aget a i) j more)))
2594+
(defn aset-short
2595+
([a i v] (aset a i (-to-short v)))
2596+
([a i j & more] (apply aset-short (aget a i) j more)))
2597+
(defn aset-char
2598+
([a i v] (aset a i (char v)))
2599+
([a i j & more] (apply aset-char (aget a i) j more)))
25842600

25852601
(defmacro amap
25862602
"Like clojure.core/amap: map `expr` over array `a`, binding each index to

‎src/lang/primitive/array.zig‎

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,23 +50,38 @@ fn arrayMakeFn(rt: *Runtime, env: *Env, args: []const Value, loc: SourceLocation
5050
return java_array.make(rt, @intCast(n), args[1]);
5151
}
5252

53-
/// `(aget array idx)` — 2-arg element read. Multi-dim `(aget a i j)` is a
54-
/// core.clj variadic over this.
53+
/// `(aget array idx & idxs)` — element read, multi-dim by recursion exactly
54+
/// as clj's variadic `aget` composes: `(aget a i j)` reads the nested array
55+
/// at `i`, then `j` within it. Implemented directly in the builtin rather
56+
/// than as a core.clj variadic (the D-446 barrier sketched that shape) so the
57+
/// hot 2-arg path stays a single builtin call.
5558
fn agetFn(rt: *Runtime, env: *Env, args: []const Value, loc: SourceLocation) anyerror!Value {
5659
_ = rt;
5760
_ = env;
58-
try error_catalog.checkArity("aget", args, 2, loc);
59-
try requireArray(args[0], "aget", loc);
60-
return java_array.aget(args[0], try asIndex(args[1], "aget", loc), "aget", loc);
61+
try error_catalog.checkArityMin("aget", args, 2, loc);
62+
var v = args[0];
63+
for (args[1..]) |idx| {
64+
try requireArray(v, "aget", loc);
65+
v = try java_array.aget(v, try asIndex(idx, "aget", loc), "aget", loc);
66+
}
67+
return v;
6168
}
6269

6370
/// `(aset array idx val)` — 3-arg in-place write, returns `val`.
6471
fn asetFn(rt: *Runtime, env: *Env, args: []const Value, loc: SourceLocation) anyerror!Value {
6572
_ = rt;
6673
_ = env;
67-
try error_catalog.checkArity("aset", args, 3, loc);
68-
try requireArray(args[0], "aset", loc);
69-
return java_array.aset(args[0], try asIndex(args[1], "aset", loc), args[2], "aset", loc);
74+
try error_catalog.checkArityMin("aset", args, 3, loc);
75+
// `(aset a i j … v)`: every arg but the last two walks into a nested
76+
// array; the final pair is the ordinary element store, which returns the
77+
// stored value as clj does.
78+
var v = args[0];
79+
for (args[1 .. args.len - 2]) |idx| {
80+
try requireArray(v, "aset", loc);
81+
v = try java_array.aget(v, try asIndex(idx, "aset", loc), "aset", loc);
82+
}
83+
try requireArray(v, "aset", loc);
84+
return java_array.aset(v, try asIndex(args[args.len - 2], "aset", loc), args[args.len - 1], "aset", loc);
7085
}
7186

7287
fn alengthFn(rt: *Runtime, env: *Env, args: []const Value, loc: SourceLocation) anyerror!Value {

‎test/diff/clj_corpus/arity_envelopes.txt‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,13 @@
4242
;;=> [10 20 0]
4343
(vec (int-array 4))
4444
;;=> [0 0 0 0]
45+
(let [a (make-array Long 2 3)] (aset a 0 1 42) (aget a 0 1))
46+
;;=> 42
47+
(let [a (make-array Long 2 3)] (aset a 1 2 7) [(aget a 1 2) (aget a 0 0)])
48+
;;=> [7 nil]
49+
(let [b (make-array Long 2 2 2)] (aset b 1 0 1 9) (aget b 1 0 1))
50+
;;=> 9
51+
(let [a (make-array Long 2 3)] (try (aget a 9 0) (catch Throwable t :oob)))
52+
;;=> :oob
53+
(let [a (make-array Long 2 3)] (try (aset a 0 9 1) (catch Throwable t :oob)))
54+
;;=> :oob

‎test/e2e/phase14_arity_parity.sh‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,5 +91,19 @@ check "AD-051 (bytes? byte-array)" '(bytes? (byte-array 3))' 'true'
9191
check "AD-051 (bytes? int-array)" '(bytes? (int-array 3))' 'true'
9292
check "AD-051 (bytes? object-array)" '(bytes? (object-array 0))' 'true'
9393

94+
95+
# --- multi-dim aget/aset (D-446 close-out): the variadic forms walk nested
96+
# arrays exactly as clj's `(apply aget (aget a i) idxs)` recursion. The
97+
# generic aget/aset cases are clj-byte-matched (arity_envelopes corpus). ---
98+
check "(aget a i j)" '(let [a (make-array Long 2 3)] (aset a 0 1 42) (aget a 0 1))' '42'
99+
check "(aset a i j k v)" '(let [b (make-array Long 2 2 2)] (aset b 1 0 1 9) (aget b 1 0 1))' '9'
100+
check "(aget oob catchable)" '(let [a (make-array Long 2 3)] (try (aget a 9 0) (catch Throwable t :oob)))' ':oob'
101+
# AD-019 pin: the typed aset-* multidim arms store WITHOUT element-type
102+
# checking (arrays are type-erased; clj throws IllegalArgumentException when
103+
# the array is boxed, and cljw has no boxed/primitive distinction to key on).
104+
check "AD-019 (aset-long a i j v)" '(let [b (make-array Long 2 2 2)] (aset-long b 0 1 1 5) (aget b 0 1 1))' '5'
105+
check "AD-019 (aset-double a i j v)" '(let [c (make-array Double 2 2)] (aset-double c 1 1 2.5) (aget c 1 1))' '2.5'
106+
check "AD-019 (aset-int a i j v)" '(let [c (make-array Long 2 2)] (aset-int c 0 0 3) (aget c 0 0))' '3'
107+
94108
echo "pass=$pass fail=$fail"
95109
if [[ $fail -gt 0 ]]; then exit 1; fi

0 commit comments

Comments
 (0)