Skip to content

Commit 73800c8

Browse files
committed
feat(interop): D-563(a2) — Clojure 1.12 Class/.instanceMethod + Class/new method values
Smell-audited: 1: no wrapper fns minted — cljw's method ABI is already receiver-first, so the method_table entry's method_val IS the 1.12 qualified-instance-method fn; one shared helper serves both value and call positions (call heads fall through to analyzeCall). (String/.length "abc") → 3, (map String/.length ["a" "bb"]) → (1 2) (native-backed classes fall back to the tag-keyed descriptor), (Thread/new f) / (map java.util.Date/new ms) construct via the <init> entry, (StringBuilder/.toString (StringBuilder/new "hi")) → "hi" — all clj-1.12-oracle-matched. Unresolvable .member / ctor-less new keep the D3 member diagnostic. e2e: 6 new cases.
1 parent 99acdd2 commit 73800c8

3 files changed

Lines changed: 53 additions & 5 deletions

File tree

‎.dev/debt.yaml‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,15 @@ active:
6060
(type-hash XOR mapHasheq; corpus records_method_values.txt,
6161
AD-035 retired as parity). deftype `(class …)` naming stays simple
6262
(AD-003/044 scope — revisit only with a consumer).
63-
(a2) NEW: Clojure 1.12 METHOD VALUES — the static form
64-
(`(every? Character/isWhitespace s)`, brew-user report 2026-07-16)
65-
landed via the analyzer's Java-surface value-position branch; the
66-
`Class/.instanceMethod` and `Class/new` 1.12 forms are NOT yet
67-
carried (implement when a consumer appears; same branch extends).
63+
(a2) DISCHARGED 2026-07-17 — all three 1.12 method-value forms now
64+
carried: statics (2026-07-16), `Class/.instanceMethod` (instance
65+
method as receiver-first fn — the ADR-0174 merged tables made
66+
cljw's method ABI already receiver-first, so `method_val` IS the
67+
fn; native-backed classes fall back to the tag-keyed descriptor)
68+
and `Class/new` (the `<init>` entry as a fn). analyzeSymbol lifts
69+
the value; call heads fall through to analyzeCall. e2e: 6 cases on
70+
phase14_host_class_identity (map String/.length, Thread/new,
71+
StringBuilder chain, Date/new higher-order), clj-1.12-oracle-matched.
6872
(b) **Var source meta** — clj vars carry :line/:file/:column from
6973
the compiler; cljw def sites know their SourceLocation but drop it.
7074
Adding it converges `(meta #'x)` and clojure.test's

‎src/eval/analyzer/analyzer.zig‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,31 @@ fn classValueKeyFor(name: []const u8) ?[]const u8 {
615615
return null;
616616
}
617617

618+
/// Clojure 1.12 qualified method values (D-563 a2): `Class/.member` is the
619+
/// INSTANCE method as a receiver-first fn (`(map String/.length xs)`), and
620+
/// `Class/new` is the constructor as a fn (`(map java.util.Date/new ms)`).
621+
/// cljw's method ABI already takes the receiver as args[0], so the entry's
622+
/// `method_val` IS the fn value — no wrapper. For a native-backed class
623+
/// (String/Long/… — instance methods live on the tag-keyed native
624+
/// descriptor, not the static surface) fall back there. Returns null when
625+
/// the name is not one of the two forms or the member is absent (the
626+
/// caller's member-miss diagnostic then names the dotted spelling).
627+
fn qualifiedMemberValue(rt: *Runtime, td: *const type_descriptor.TypeDescriptor, class_head: []const u8, name: []const u8) ?Value {
628+
if (std.mem.eql(u8, name, "new")) {
629+
if (td.lookupMethod(null, "<init>")) |me| return me.method_val;
630+
return null;
631+
}
632+
if (name.len > 1 and name[0] == '.') {
633+
const mname = name[1..];
634+
if (td.lookupMethod(null, mname)) |me| return me.method_val;
635+
if (class_name.nativeTagFor(class_head)) |tag| {
636+
const ntd = rt.nativeDescriptor(tag) catch return null;
637+
if (ntd.lookupMethod(null, mname)) |me| return me.method_val;
638+
}
639+
}
640+
return null;
641+
}
642+
618643
/// Resolve a bare (dot-free) class name through the current ns's `(:import …)`
619644
/// map to its FQCN; qualified + unimported names pass through unchanged. Reuses
620645
/// `ns.imports` (D-235) — the same map `(Class. …)` consults — so an imported
@@ -743,6 +768,11 @@ fn analyzeSymbol(
743768
// the descriptor carries a static field of this name, the symbol
744769
// IS that constant — emit it directly (no Var resolution follows).
745770
if (special_forms.resolveJavaSurface(env.rt, env, ns_name)) |td| {
771+
// 1.12 qualified method values: `Class/.member` (instance method
772+
// as receiver-first fn) + `Class/new` (ctor as fn) — D-563 a2.
773+
if (qualifiedMemberValue(env.rt, td, ns_name, sym.name)) |mv| {
774+
return try makeConstant(arena, mv, form);
775+
}
746776
if (td.lookupStaticField(sym.name)) |sf| {
747777
// ADR-0087: a heap-singleton static field resolves to the live
748778
// per-Runtime value at analyze time (handled inside the helper).
@@ -914,6 +944,11 @@ fn analyzeList(
914944
if (td.lookupStaticField(head.name)) |sf| {
915945
if (items.len == 1)
916946
return try makeConstant(arena, try staticFieldValue(rt, sf), form);
947+
} else if (qualifiedMemberValue(rt, td, ns_head, head.name) != null) {
948+
// 1.12 `(Class/.member recv …)` / `(Class/new …)` call
949+
// head: resolvable as a method value — fall through to
950+
// analyzeCall, whose head analysis (analyzeSymbol) lifts
951+
// the fn constant and the call applies it (D-563 a2).
917952
} else if (!isDeferredHostNs(ns_head) and env.findNsOrAlias(ns_head) == null) {
918953
// ADR-0174 D3: the class RESOLVED but has neither a method
919954
// nor a static field of this name — raise the call-position

‎test/e2e/phase14_host_class_identity.sh‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,15 @@ assert_eq 'def_shadows_class' "$(run '(def System 42) System' | tail -1)" '42'
8181
# --- user code sees a clean class value through higher-order use ---
8282
assert_eq 'group_by_class' "$(run '(contains? (group-by class [(java.util.Date. 0) (java.util.Date. 1)]) java.util.Date)')" 'true'
8383

84+
# --- Clojure 1.12 qualified method values (D-563 a2): Class/.instanceMethod
85+
# is the instance method as a receiver-first fn; Class/new is the ctor fn ---
86+
assert_eq 'qual_instance_call' "$(run '(String/.length "abc")')" '3'
87+
assert_eq 'qual_instance_value' "$(run '(map String/.length ["a" "bb"])')" '(1 2)'
88+
assert_eq 'qual_instance_merged' "$(run '(java.util.Date/.getTime (java.util.Date. 42))')" '42'
89+
assert_eq 'ctor_value_class' "$(run '(class (Thread/new (fn [])))')" 'java.lang.Thread'
90+
assert_eq 'ctor_value_chain' "$(run '(StringBuilder/.toString (StringBuilder/new "hi"))')" '"hi"'
91+
assert_eq 'ctor_value_higher_order' "$(run '(map java.util.Date/new [1 2])')" '(#inst "1970-01-01T00:00:00.001-00:00" #inst "1970-01-01T00:00:00.002-00:00")'
92+
8493
# --- ADR-0174 D4: Class is a first-class marker (clj: java.lang.Class) ---
8594
assert_eq 'class_of_class' "$(run '(class Long)')" 'Class'
8695
assert_eq 'class_of_host_class' "$(run '(class (class (java.util.Date. 0)))')" 'Class'

0 commit comments

Comments
 (0)