From afd81f62ff3f32ad877b816e1734fdca267471a3 Mon Sep 17 00:00:00 2001
From: Pedro Gomes Branquinho
Date: Wed, 14 Oct 2026 19:09:18 -0300
Subject: [PATCH 01/17] test(class): defmethod dispatch on the five inspect*
seq class symbols (ADR-0194)
tools.reader's inspect* dispatches a multimethod on LazySeq, Cons and the
three JVM inner seq classes; a defmethod on a class symbol resolves it at
analyze time. Pins that LazySeq and Cons are native exact-tag classes whose
methods are reached, and that PersistentVector$ChunkedSeq,
PersistentArrayMap$Seq and PersistentHashMap$NodeSeq resolve as opaque
class values that no cljw value is an instance of (AD-066 / AD-067).
Supersedes the 2026-09-09 WIP b3b332fd, whose Cons-as-opaque assertions
predate f8837bcc. Qualified-name assertions checked against JVM Clojure.
---
test/clj/suites/class_type_test.clj | 52 +++++++++++++++++++++++++++++
1 file changed, 52 insertions(+)
diff --git a/test/clj/suites/class_type_test.clj b/test/clj/suites/class_type_test.clj
index 885e603e3..c8cb9aa9c 100644
--- a/test/clj/suites/class_type_test.clj
+++ b/test/clj/suites/class_type_test.clj
@@ -135,3 +135,55 @@
(is (= "TransientVector" (pr-str (class (transient []))))))
(testing "class-level isa? still holds after the rename (isCallableClassName)"
(is (= true (isa? (class +) clojure.lang.IFn)))))
+
+;; --- ADR-0194: the five seq class symbols tools.reader's `inspect*`
+;; dispatches on. A defmethod on a class symbol resolves it at ANALYZE time,
+;; so these forms loading at all is the first assertion. LazySeq and Cons
+;; name types cljw has, so they are native exact-tag classes and their
+;; methods are reached. The three JVM inner seq classes name types cljw has
+;; no values of (its vector seq is ArraySeq, its map seq is PersistentList),
+;; so they resolve as OPAQUE class values whose `instance?` is uniformly
+;; false and whose methods are never reached (AD-066 / AD-067).
+(defmulti inspect-shape
+ (fn [x] (cond (vector? x) :vector (map? x) :map :else (class x))))
+(defmethod inspect-shape clojure.lang.PersistentVector$ChunkedSeq [_] :vec-seq)
+(defmethod inspect-shape clojure.lang.PersistentArrayMap$Seq [_] :map-seq)
+(defmethod inspect-shape clojure.lang.PersistentHashMap$NodeSeq [_] :map-seq)
+(defmethod inspect-shape clojure.lang.Cons [_] :cons)
+(defmethod inspect-shape clojure.lang.LazySeq [_] :lazy-seq)
+(defmethod inspect-shape :default [_] :other)
+
+(deftest native-seq-classes
+ (testing "LazySeq resolves and is the class of a lazy seq"
+ (is (= true (class? clojure.lang.LazySeq)))
+ (is (= true (= clojure.lang.LazySeq (class (map inc [1])))))
+ (is (= true (instance? clojure.lang.LazySeq (map inc [1]))))
+ (is (= false (instance? clojure.lang.LazySeq [1])))
+ (is (= false (instance? clojure.lang.LazySeq (range 3)))))
+ (testing "Cons is a native exact-tag class, as on the JVM"
+ (is (= true (class? clojure.lang.Cons)))
+ (is (= true (= clojure.lang.Cons (class (cons 1 [2 3])))))
+ (is (= true (instance? clojure.lang.Cons (cons 1 [2 3]))))))
+
+(deftest opaque-jvm-seq-classes
+ (testing "each resolves as a class value distinct from the others"
+ (is (= true (class? clojure.lang.PersistentVector$ChunkedSeq)))
+ (is (= true (class? clojure.lang.PersistentArrayMap$Seq)))
+ (is (= true (class? clojure.lang.PersistentHashMap$NodeSeq)))
+ (is (= false (= clojure.lang.Cons clojure.lang.PersistentVector$ChunkedSeq)))
+ (is (= false (= clojure.lang.PersistentArrayMap$Seq
+ clojure.lang.PersistentHashMap$NodeSeq))))
+ (testing "no cljw value is an instance of one (AD-066 / AD-067)"
+ (is (= false (instance? clojure.lang.PersistentVector$ChunkedSeq (seq [1 2 3]))))
+ (is (= false (instance? clojure.lang.PersistentArrayMap$Seq (seq {:a 1}))))
+ (is (= false (instance? clojure.lang.PersistentHashMap$NodeSeq
+ (seq (zipmap (range 20) (range 20))))))))
+
+(deftest inspect-multimethod-dispatches
+ (testing "the native methods are reached"
+ (is (= :lazy-seq (inspect-shape (map inc [1 2]))))
+ (is (= :cons (inspect-shape (cons 1 [2 3])))))
+ (testing "the opaque methods are dead: cljw seqs fall to :default (AD-066 / AD-067)"
+ (is (= :other (inspect-shape (seq [1 2 3]))))
+ (is (= :other (inspect-shape (seq (range 3)))))
+ (is (= :other (inspect-shape (seq {:a 1}))))))
From f60306adc4200d40587b7783d24b8405e54df52b Mon Sep 17 00:00:00 2001
From: Pedro Gomes Branquinho
Date: Wed, 14 Oct 2026 20:35:14 -0300
Subject: [PATCH 02/17] fix(vm): raise interop, set! and ctor errors at their
form, and check error locations in the diff oracle (ADR-0036)
The ADR-0036 dual-backend oracle compared only the Zig error tag for a
failing case. It never checked WHERE the error was raised, so the VM could
raise at 0:0 while TreeWalk reported the right line:column, and nothing
caught it.
evaluator.compare keeps the error-identity `equal` (6e1c72fb) and adds
tree_walk_loc / vm_loc / loc_equal. Each position is sampled with
error_info.peekLastError() right after its own backend run. runOnce does
not reset the threadlocal slot, so compare clears it before each run;
otherwise a stale payload would be read as this run's position. A run
that fails without publishing a payload reports 0:0.
Every diff fixture check (check, checkEqual, checkRaises) now asserts
loc_equal. The new "error source location survives BOTH backends" test
requires a real (non-zero) and equal position for set!, op_method_call,
a constructor arity failure, and multi-line forms. The staging binary
(b59d3363) raises the set!, .member and ctor cases with no location on
the VM, so this test fails without the VM changes below.
VM: every error_catalog.raise that passed an empty SourceLocation and has
a located TreeWalk twin now uses instr_loc (as D-326 did for callFn):
op_set_var, op_set_field, op_method_call, op_static_method_call (and its
callFn), op_ctor_call (constructInstance), op_ns_import, the require/refer
path, op_invoke_builtin, and the slot_out_of_range guards. The compiler
stamps the form's loc before op_set_var, op_set_field, op_method_call,
op_static_method_call and op_ctor_call, so the instruction reports the
form, not its last argument. The stack_overflow raises keep `.{}`
because their TreeWalk twins are unlocated too.
Port of 04b0d0bb (worktree-agent-a96eb3d50b10efd45), never landed.
Smell-audited: 2: Changes the locations passed to existing raise sites and adds three CompareResult fields plus one assertion helper; no new branching. vm.zig, compiler.zig and diff_test.zig are past the size ceiling; this grows them by a few lines with no new concept.
---
src/eval/backend/vm.zig | 42 +++++++++---------
src/eval/backend/vm/compiler.zig | 11 +++++
src/eval/evaluator.zig | 55 ++++++++++++++++++++++-
src/lang/diff_test.zig | 76 ++++++++++++++++++++++++++++++--
4 files changed, 159 insertions(+), 25 deletions(-)
diff --git a/src/eval/backend/vm.zig b/src/eval/backend/vm.zig
index b8a0551f0..d098f2a00 100644
--- a/src/eval/backend/vm.zig
+++ b/src/eval/backend/vm.zig
@@ -527,7 +527,7 @@ inline fn stepOnce(
},
.op_load_local => {
if (instr.operand >= locals.len)
- return error_catalog.raise(.slot_out_of_range, .{}, .{ .form = "Local", .index = instr.operand, .max = locals.len });
+ return error_catalog.raise(.slot_out_of_range, instr_loc, .{ .form = "Local", .index = instr.operand, .max = locals.len });
if (sp >= stack.len)
return operandOverflow(instr_loc);
stack[sp] = locals[instr.operand];
@@ -535,7 +535,7 @@ inline fn stepOnce(
},
.op_store_local => {
if (instr.operand >= locals.len)
- return error_catalog.raise(.slot_out_of_range, .{}, .{ .form = "let*", .index = instr.operand, .max = locals.len });
+ return error_catalog.raise(.slot_out_of_range, instr_loc, .{ .form = "let*", .index = instr.operand, .max = locals.len });
if (sp == 0) return raiseInternal("vm: op_store_local on empty stack");
sp -= 1;
locals[instr.operand] = stack[sp];
@@ -600,7 +600,7 @@ inline fn stepOnce(
return raiseInternal("vm: op_ns_import site index out of range");
const imp = chunk.import_sites[instr.operand];
const here = env.current_ns orelse
- return error_catalog.raise(.current_namespace_missing, .{}, .{ .sym = imp.simple });
+ return error_catalog.raise(.current_namespace_missing, instr_loc, .{ .sym = imp.simple });
try here.addImport(env.alloc, imp.simple, imp.fqcn);
if (sp >= stack.len)
return operandOverflow(instr_loc);
@@ -618,7 +618,7 @@ inline fn stepOnce(
if (!env_mod.setBinding(var_ptr, val)) {
const full = try std.fmt.allocPrint(rt.gpa, "{s}/{s}", .{ var_ptr.ns.name, var_ptr.name });
defer rt.gpa.free(full);
- return error_catalog.raise(.var_set_not_bound, .{}, .{ .@"var" = full });
+ return error_catalog.raise(.var_set_not_bound, instr_loc, .{ .@"var" = full });
}
},
.op_set_field => {
@@ -644,7 +644,7 @@ inline fn stepOnce(
break;
}
}
- if (!wrote) return error_catalog.raise(.symbol_unresolved, .{}, .{ .sym = field_name });
+ if (!wrote) return error_catalog.raise(.symbol_unresolved, instr_loc, .{ .sym = field_name });
sp -= 2;
stack[sp] = value;
sp += 1;
@@ -860,7 +860,7 @@ inline fn stepOnce(
const lslot: usize = instr.operand >> 8;
const cidx: usize = instr.operand & 0xFF;
if (lslot >= locals.len)
- return error_catalog.raise(.slot_out_of_range, .{}, .{ .form = "Local", .index = @as(u16, @intCast(lslot)), .max = locals.len });
+ return error_catalog.raise(.slot_out_of_range, instr_loc, .{ .form = "Local", .index = @as(u16, @intCast(lslot)), .max = locals.len });
if (cidx >= chunk.constants.len)
return raiseInternal("vm: local_const constant index out of range");
const a = locals[lslot];
@@ -893,7 +893,7 @@ inline fn stepOnce(
const sa: usize = instr.operand >> 8;
const sb: usize = instr.operand & 0xFF;
if (sa >= locals.len or sb >= locals.len)
- return error_catalog.raise(.slot_out_of_range, .{}, .{ .form = "Local", .index = @as(u16, @intCast(if (sa >= locals.len) sa else sb)), .max = locals.len });
+ return error_catalog.raise(.slot_out_of_range, instr_loc, .{ .form = "Local", .index = @as(u16, @intCast(if (sa >= locals.len) sa else sb)), .max = locals.len });
const a = locals[sa];
const b = locals[sb];
const aop = intrinsic.fromLocalsOpcode(instr_op).?;
@@ -931,7 +931,7 @@ inline fn stepOnce(
const offset: i16 = @bitCast(chunk.instructions[ip].operand);
ip += 1;
if (sa >= locals.len)
- return error_catalog.raise(.slot_out_of_range, .{}, .{ .form = "Local", .index = @as(u16, @intCast(sa)), .max = locals.len });
+ return error_catalog.raise(.slot_out_of_range, instr_loc, .{ .form = "Local", .index = @as(u16, @intCast(sa)), .max = locals.len });
const a = locals[sa];
const b = if (info.b_is_const) bblk: {
if (sb >= chunk.constants.len)
@@ -939,7 +939,7 @@ inline fn stepOnce(
break :bblk chunk.constants[sb];
} else bblk: {
if (sb >= locals.len)
- return error_catalog.raise(.slot_out_of_range, .{}, .{ .form = "Local", .index = @as(u16, @intCast(sb)), .max = locals.len });
+ return error_catalog.raise(.slot_out_of_range, instr_loc, .{ .form = "Local", .index = @as(u16, @intCast(sb)), .max = locals.len });
break :bblk locals[sb];
};
const fast: ?Value = if (rt.core_arith_pristine)
@@ -975,7 +975,7 @@ inline fn stepOnce(
ip += 1;
if (sp < nb) return raiseInternal("vm: op_recur_loop underflow");
if (base + nb > locals.len)
- return error_catalog.raise(.slot_out_of_range, .{}, .{ .form = "loop*", .index = @as(u16, @intCast(base + nb)), .max = locals.len });
+ return error_catalog.raise(.slot_out_of_range, instr_loc, .{ .form = "loop*", .index = @as(u16, @intCast(base + nb)), .max = locals.len });
const top: u16 = sp - @as(u16, @intCast(nb));
var k: usize = 0;
while (k < nb) : (k += 1) {
@@ -1096,7 +1096,7 @@ inline fn stepOnce(
// through `op_call` + vtable). Per
// `no_op_stub_forbidden.md`, raise rather than fall
// through silently.
- return error_catalog.raise(.feature_not_supported, .{}, .{ .name = "op_invoke_builtin" });
+ return error_catalog.raise(.feature_not_supported, instr_loc, .{ .name = "op_invoke_builtin" });
},
.op_push_handler => {
const offset: i16 = @bitCast(instr.operand);
@@ -1303,7 +1303,7 @@ inline fn stepOnce(
// lib_not_found renders the caret (tree_walk parity).
const target_ns = try loader.loadOrFindNs(rt, env, spec.ns_name, instr_loc);
const here = env.current_ns orelse
- return error_catalog.raise(.current_namespace_missing, .{}, .{ .sym = spec.ns_name });
+ return error_catalog.raise(.current_namespace_missing, instr_loc, .{ .sym = spec.ns_name });
if (spec.alias) |alias_name| {
try env.setAlias(here, alias_name, target_ns);
}
@@ -1319,7 +1319,7 @@ inline fn stepOnce(
.private_blocked => {
const full = try std.fmt.allocPrint(rt.gpa, "{s}/{s}", .{ spec.ns_name, refer_name });
defer rt.gpa.free(full);
- return error_catalog.raise(.private_access_error, .{}, .{
+ return error_catalog.raise(.private_access_error, instr_loc, .{
.sym = full,
.ns = spec.ns_name,
});
@@ -1327,7 +1327,7 @@ inline fn stepOnce(
.not_found => {
const full = try std.fmt.allocPrint(rt.gpa, "{s}/{s}", .{ spec.ns_name, refer_name });
defer rt.gpa.free(full);
- return error_catalog.raise(.symbol_unresolved, .{}, .{ .sym = full });
+ return error_catalog.raise(.symbol_unresolved, instr_loc, .{ .sym = full });
},
}
}
@@ -1373,7 +1373,7 @@ inline fn stepOnce(
// Shared resolver/dispatcher (deftype/record + java-surface
// ``) — identical to TreeWalk's evalConstructorCall so a
// `(java.io.File. …)` ctor works on both backends.
- const new_val = try special_forms.constructInstance(rt, env, type_name, args_slice, .{});
+ const new_val = try special_forms.constructInstance(rt, env, type_name, args_slice, instr_loc);
sp -= arg_count;
stack[sp] = new_val;
sp += 1;
@@ -1419,11 +1419,11 @@ inline fn stepOnce(
stack[sp] = fv;
sp += 1;
} else if (cs_entry.field_only) {
- return error_catalog.raise(.symbol_unresolved, .{}, .{ .sym = cs_entry.method_name });
+ return error_catalog.raise(.symbol_unresolved, instr_loc, .{ .sym = cs_entry.method_name });
} else {
if (cs_entry.cache.lookupWithCache(td, null, cs_entry.method_name, rt.protocol_generation)) |me| {
if (me.method_val.tag() == .nil)
- return error_catalog.raise(.feature_not_supported, .{}, .{ .name = "method declared but not implemented" });
+ return error_catalog.raise(.feature_not_supported, instr_loc, .{ .name = "method declared but not implemented" });
const args_slice = stack[sp - arg_count .. sp];
const vt = rt.vtable orelse return error.NoVTable;
// Frame the user `/` so the interop form
@@ -1448,7 +1448,7 @@ inline fn stepOnce(
stack[sp] = r;
sp += 1;
} else {
- return error_catalog.raise(.protocol_no_satisfies, .{}, .{
+ return error_catalog.raise(.protocol_no_satisfies, instr_loc, .{
.protocol = "<.member>",
.method = cs_entry.method_name,
.type_name = td.fqcn orelse "",
@@ -1470,17 +1470,17 @@ inline fn stepOnce(
const td = cs_entry.descriptor orelse
return raiseInternal("vm: op_static_method_call missing descriptor (compiler bug)");
const me = td.lookupMethod(null, cs_entry.method_name) orelse {
- return error_catalog.raise(.protocol_no_satisfies, .{}, .{
+ return error_catalog.raise(.protocol_no_satisfies, instr_loc, .{
.protocol = "",
.method = cs_entry.method_name,
.type_name = td.fqcn orelse "",
});
};
if (me.method_val.tag() == .nil)
- return error_catalog.raise(.feature_not_supported, .{}, .{ .name = "static method declared but not implemented" });
+ return error_catalog.raise(.feature_not_supported, instr_loc, .{ .name = "static method declared but not implemented" });
const args_slice = stack[sp - arg_count .. sp];
const vt = rt.vtable orelse return error.NoVTable;
- const result = try vt.callFn(rt, env, me.method_val, args_slice, .{});
+ const result = try vt.callFn(rt, env, me.method_val, args_slice, instr_loc);
sp -= arg_count;
stack[sp] = result;
// Result loc = this call form (pops below sp_entry, so the sweep misses it).
diff --git a/src/eval/backend/vm/compiler.zig b/src/eval/backend/vm/compiler.zig
index d9f2dda8f..7584fe2bd 100644
--- a/src/eval/backend/vm/compiler.zig
+++ b/src/eval/backend/vm/compiler.zig
@@ -193,6 +193,9 @@ const Compiler = struct {
fn compileSet(self: *Compiler, n: node_mod.SetNode) Error!void {
try self.compileNode(n.value_expr);
const idx = try self.addConstant(Value.encodeHeapPtr(.var_ref, n.var_ptr));
+ // The value expr re-stamped `current_*`; op_set_var raises at the
+ // set! form itself (TreeWalk evalSet's n.loc, ADR-0036 error half).
+ self.stampLoc(n.loc);
try self.emit(.op_set_var, idx);
}
@@ -205,6 +208,7 @@ const Compiler = struct {
try self.compileNode(n.value_expr);
const name_val = try string_mod.alloc(self.rt, n.field_name);
const idx = try self.addConstant(name_val);
+ self.stampLoc(n.loc);
try self.emit(.op_set_field, idx);
}
@@ -740,6 +744,9 @@ const Compiler = struct {
.type_name = name_dup,
.arg_count = @intCast(n.args.len),
});
+ // Raise at the ctor form, not its last argument
+ // (TreeWalk evalConstructorCall's n.loc, ADR-0036 error half).
+ self.stampLoc(n.loc);
try self.emit(.op_ctor_call, ctor_idx);
},
.instance_member => {
@@ -757,6 +764,9 @@ const Compiler = struct {
.arg_count = total_args,
.field_only = n.field_only,
});
+ // Raise at the interop form, not its last argument
+ // (TreeWalk evalInstanceMember's n.loc, ADR-0036 error half).
+ self.stampLoc(n.loc);
try self.emit(.op_method_call, cs_idx);
},
.static_method => {
@@ -773,6 +783,7 @@ const Compiler = struct {
.arg_count = arg_count,
.descriptor = td,
});
+ self.stampLoc(n.loc);
try self.emit(.op_static_method_call, cs_idx);
},
}
diff --git a/src/eval/evaluator.zig b/src/eval/evaluator.zig
index 98f07e7a8..d2ef7609f 100644
--- a/src/eval/evaluator.zig
+++ b/src/eval/evaluator.zig
@@ -32,6 +32,7 @@ const tree_walk = @import("backend/tree_walk.zig");
const vm = @import("backend/vm.zig");
const vm_compiler = @import("backend/vm/compiler.zig");
const driver = @import("driver.zig");
+const error_info = @import("../runtime/error/info.zig");
pub const CompareResult = struct {
tree_walk: anyerror!Value,
@@ -39,8 +40,40 @@ pub const CompareResult = struct {
/// `true` when both backends produced the same NaN-boxed bit pattern, or
/// failed with the same Zig error.
equal: bool,
+ /// Source position of the error each backend raised; null when that
+ /// backend returned a value. Only `line`/`column` are captured — the
+ /// `file` field of a fixture-analysed form is "unknown" on both
+ /// backends and carries no signal.
+ tree_walk_loc: ?ErrorPos = null,
+ vm_loc: ?ErrorPos = null,
+ /// `true` when both backends agree on WHERE the failure is: neither
+ /// raised, or both raised at the same line:column. The error half of
+ /// the dual-backend contract (ADR-0036) — a backend that raises the
+ /// same Kind at 0:0 while the other reports 3:11 is a parity break,
+ /// invisible to `equal` (which only compares returned Values).
+ loc_equal: bool = true,
};
+/// The comparable part of a raised error's `SourceLocation`.
+pub const ErrorPos = struct {
+ line: u32,
+ column: u16,
+};
+
+/// The position of the error a just-finished backend run raised, or null
+/// when the run returned a value. Reads the threadlocal payload without
+/// clearing it, so the caller's own error handling still sees it; must be
+/// called before the next run overwrites the slot. A run that failed
+/// without publishing a payload (a user throw, OOM) reports 0:0, the same
+/// position a dropped location has, rather than null ("did not raise").
+fn errorPos(result: anyerror!Value) ?ErrorPos {
+ _ = result catch {
+ const info = error_info.peekLastError() orelse return .{ .line = 0, .column = 0 };
+ return .{ .line = info.location.line, .column = info.location.column };
+ };
+ return null;
+}
+
pub fn compare(
rt: *Runtime,
env: *Env,
@@ -48,11 +81,20 @@ pub fn compare(
arena: std.mem.Allocator,
source: []const u8,
) CompareResult {
+ // runOnce does not reset the threadlocal error slot, so clear it before
+ // each run: a stale payload from an earlier raise must not be read as
+ // this run's position.
+ error_info.clearLastError();
tree_walk.installVTable(rt);
const tw_value = runOnce(rt, env, table, arena, source, .tree_walk);
+ // Read the raise position NOW: the threadlocal payload is a single slot
+ // that the VM run below overwrites.
+ const tw_loc = errorPos(tw_value);
+ error_info.clearLastError();
vm.installVTable(rt);
const vm_value = runOnce(rt, env, table, arena, source, .vm);
+ const vm_loc = errorPos(vm_value);
const equal = if (tw_value) |tw| blk: {
const vm_v = vm_value catch break :blk false;
@@ -61,7 +103,18 @@ pub fn compare(
_ = vm_value catch |vm_err| break :blk tw_err == vm_err;
break :blk false;
};
- return .{ .tree_walk = tw_value, .vm = vm_value, .equal = equal };
+ const loc_equal = if (tw_loc) |a|
+ if (vm_loc) |b| a.line == b.line and a.column == b.column else false
+ else
+ vm_loc == null;
+ return .{
+ .tree_walk = tw_value,
+ .vm = vm_value,
+ .equal = equal,
+ .tree_walk_loc = tw_loc,
+ .vm_loc = vm_loc,
+ .loc_equal = loc_equal,
+ };
}
pub const BackendChoice = enum { tree_walk, vm };
diff --git a/src/lang/diff_test.zig b/src/lang/diff_test.zig
index 1af1c9fea..824992841 100644
--- a/src/lang/diff_test.zig
+++ b/src/lang/diff_test.zig
@@ -74,20 +74,47 @@ const Fixture = struct {
fn check(self: *Fixture, source: []const u8, expected: i64) !void {
const r = evaluator.compare(&self.rt, &self.env, &self.table, self.arena.allocator(), source);
+ try expectLocParity(r, source);
try testing.expect(r.equal);
try testing.expectEqual(expected, (try r.tree_walk).asInteger());
}
+ /// Both backends raise the same error at the same source position
+ /// (ADR-0036 error half). For cases whose contract is the failure itself.
+ fn checkRaises(self: *Fixture, source: []const u8, expected: anyerror) !void {
+ const r = evaluator.compare(&self.rt, &self.env, &self.table, self.arena.allocator(), source);
+ try testing.expectError(expected, r.tree_walk);
+ try testing.expectError(expected, r.vm);
+ try expectLocParity(r, source);
+ }
+
/// Assert only that both backends agree (VM == TreeWalk) — for results whose
/// type is not an inline integer (bigint / float / string), where
/// `asInteger()` cannot be used. `r.equal` is the dual-backend parity oracle.
fn checkEqual(self: *Fixture, source: []const u8) !void {
const r = evaluator.compare(&self.rt, &self.env, &self.table, self.arena.allocator(), source);
+ try expectLocParity(r, source);
_ = try r.tree_walk; // surface an eval error rather than a silent mismatch
try testing.expect(r.equal);
}
};
+/// The shared error-location assertion every fixture check runs (ADR-0036
+/// error half): whenever either backend raises, both must report the same
+/// line:column. `loc_equal` is true when neither raised, so value cases pass
+/// through; a raise on one backend only, or at a different position, fails
+/// here with both positions printed.
+fn expectLocParity(r: evaluator.CompareResult, source: []const u8) !void {
+ if (r.loc_equal) return;
+ const tw = r.tree_walk_loc orelse evaluator.ErrorPos{ .line = 0, .column = 0 };
+ const v = r.vm_loc orelse evaluator.ErrorPos{ .line = 0, .column = 0 };
+ std.debug.print(
+ "error-location parity broken for: {s}\n tree_walk {d}:{d} (raised: {})\n vm {d}:{d} (raised: {})\n",
+ .{ source, tw.line, tw.column, r.tree_walk_loc != null, v.line, v.column, r.vm_loc != null },
+ );
+ return error.TestUnexpectedResult;
+}
+
test "diff: D-346 chunked collection literals preserve order and nested evaluation" {
var f: Fixture = undefined;
try Fixture.init(&f, testing.allocator);
@@ -120,9 +147,7 @@ test "diff: D-413 unresolved symbol → clean symbol_unresolved, not a panic" {
var f: Fixture = undefined;
try Fixture.init(&f, testing.allocator);
defer f.deinit();
- const r = evaluator.compare(&f.rt, &f.env, &f.table, f.arena.allocator(), "(re-matcher 1)");
- try testing.expectError(error.NameError, r.tree_walk);
- try testing.expectError(error.NameError, r.vm);
+ try f.checkRaises("(re-matcher 1)", error.NameError);
}
test "diff: D-563b def meta — user ^meta + compiler :line land on the Var in BOTH backends (op_var_meta ≡ evalDef meta_expr)" {
@@ -1800,3 +1825,48 @@ test "diff: op_get / op_nth collection intrinsics (O-043, both backends)" {
// fixture's literal-arg quirk in TreeWalk; the value path is what matters).
try f.check("(let [v [[1 2] [3 4]]] (count (nth v 0 nil)))", 2);
}
+
+/// Assert the error-location half of the dual-backend contract (ADR-0036):
+/// both backends must raise, and both must report the SAME source position.
+/// A raise at line 0 is rejected on either side — "both agree on nothing" is
+/// vacuous parity, and 0:0 is exactly what a dropped location looks like.
+fn checkErrorLoc(f: *Fixture, source: []const u8) !void {
+ const r = evaluator.compare(&f.rt, &f.env, &f.table, f.arena.allocator(), source);
+ const tw = r.tree_walk_loc orelse {
+ std.debug.print("tree_walk did not raise for: {s}\n", .{source});
+ return error.TestExpectedRaise;
+ };
+ const vm_pos = r.vm_loc orelse {
+ std.debug.print("vm did not raise for: {s}\n", .{source});
+ return error.TestExpectedRaise;
+ };
+ if (tw.line == 0 or vm_pos.line == 0 or !r.loc_equal) {
+ std.debug.print(
+ "error-location parity broken for: {s}\n tree_walk {d}:{d}\n vm {d}:{d}\n",
+ .{ source, tw.line, tw.column, vm_pos.line, vm_pos.column },
+ );
+ return error.TestUnexpectedResult;
+ }
+}
+
+test "diff: error source location survives BOTH backends (ADR-0036 error half)" {
+ var f: Fixture = undefined;
+ try Fixture.init(&f, testing.allocator);
+ defer f.deinit();
+ // Control arm: a builtin raising through op_call already threads the call
+ // form's loc into `vt.callFn`, so this one held before the fix.
+ try checkErrorLoc(&f, "(/ 1 0)");
+ // `set!` on a var with no active thread binding → var_set_not_bound, raised
+ // by the VM's op_set_var directly rather than through `vt.callFn`.
+ try checkErrorLoc(&f, "(do (def svl 1) (set! svl 2))");
+ // Interop member on a receiver whose type has no such method → raised by
+ // the VM's op_method_call directly.
+ try checkErrorLoc(&f, "(.noSuchMember \"s\")");
+ // Constructor arity failure → raised inside constructInstance with the
+ // loc the backend hands it (TreeWalk n.loc, VM op_ctor_call instr_loc).
+ try checkErrorLoc(&f, "(MapEntry. 1)");
+ // Multi-line source: the position must be the inner form's own line and
+ // column, not the enclosing form's and not the last argument's.
+ try checkErrorLoc(&f, "(do\n (def svl2 1)\n (set! svl2\n 2))");
+ try checkErrorLoc(&f, "(let [x 1]\n (MapEntry. x))");
+}
From 75d98e34adf3889df7199dfbc3e086dbf83adc78 Mon Sep 17 00:00:00 2001
From: Pedro Gomes Branquinho
Date: Wed, 14 Oct 2026 20:52:51 -0300
Subject: [PATCH 03/17] style(diff): drop em-dashes from the error-location
oracle comments
Smell-audited: 0: comment punctuation only, no code change.
---
src/eval/evaluator.zig | 4 ++--
src/lang/diff_test.zig | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/eval/evaluator.zig b/src/eval/evaluator.zig
index d2ef7609f..58c84cfeb 100644
--- a/src/eval/evaluator.zig
+++ b/src/eval/evaluator.zig
@@ -41,14 +41,14 @@ pub const CompareResult = struct {
/// failed with the same Zig error.
equal: bool,
/// Source position of the error each backend raised; null when that
- /// backend returned a value. Only `line`/`column` are captured — the
+ /// backend returned a value. Only `line`/`column` are captured: the
/// `file` field of a fixture-analysed form is "unknown" on both
/// backends and carries no signal.
tree_walk_loc: ?ErrorPos = null,
vm_loc: ?ErrorPos = null,
/// `true` when both backends agree on WHERE the failure is: neither
/// raised, or both raised at the same line:column. The error half of
- /// the dual-backend contract (ADR-0036) — a backend that raises the
+ /// the dual-backend contract (ADR-0036). A backend that raises the
/// same Kind at 0:0 while the other reports 3:11 is a parity break,
/// invisible to `equal` (which only compares returned Values).
loc_equal: bool = true,
diff --git a/src/lang/diff_test.zig b/src/lang/diff_test.zig
index 824992841..9906b16db 100644
--- a/src/lang/diff_test.zig
+++ b/src/lang/diff_test.zig
@@ -1828,7 +1828,7 @@ test "diff: op_get / op_nth collection intrinsics (O-043, both backends)" {
/// Assert the error-location half of the dual-backend contract (ADR-0036):
/// both backends must raise, and both must report the SAME source position.
-/// A raise at line 0 is rejected on either side — "both agree on nothing" is
+/// A raise at line 0 is rejected on either side: "both agree on nothing" is
/// vacuous parity, and 0:0 is exactly what a dropped location looks like.
fn checkErrorLoc(f: *Fixture, source: []const u8) !void {
const r = evaluator.compare(&f.rt, &f.env, &f.table, f.arena.allocator(), source);
From 012f8ee0be8a925243fba8ab6d158fa26376bdc6 Mon Sep 17 00:00:00 2001
From: Pedro Gomes Branquinho
Date: Wed, 14 Oct 2026 20:55:21 -0300
Subject: [PATCH 04/17] fix(runtime): a user class is matched by descriptor
identity, never by its simple name (ADR-0201)
A user class IS its TypeDescriptor. instance?, .isInstance, class isa?
and multimethod dispatch now answer by descriptor identity
(class_name.isInstanceOfClass / classIsaOf / descriptorIsa), so they agree
with (= (class x) C) for a redefined record, a namesake defined in another
namespace and a reify class. The record-interface, IFn and clojure.lang.Fn
arms route through descriptorIsa instead of bypassing identity.
multimethod: an instance dispatch value relates to a Named value only by
derive; the D-082 instance-vs-Named simple-name arm is gone.
The type_redefinition diff corpus pins every redefinition scenario on both
backends. Ported from swarm/td-uaf (94a2ed45, d4d028a3, c0932e99).
Smell-audited: 2: replaces name-keyed membership checks with one descriptor-identity predicate and deletes the D-082 name arm; no new branching surface.
---
src/lang/primitive/core.zig | 51 +++--
src/runtime/class_name.zig | 167 ++++++++++++--
src/runtime/java/lang/Class.zig | 8 +-
src/runtime/multimethod.zig | 129 +++--------
src/runtime/type_descriptor.zig | 33 ++-
test/diff/clj_corpus/type_redefinition.txt | 254 +++++++++++++++++++++
6 files changed, 480 insertions(+), 162 deletions(-)
create mode 100644 test/diff/clj_corpus/type_redefinition.txt
diff --git a/src/lang/primitive/core.zig b/src/lang/primitive/core.zig
index 5c3954a68..96f2a0c7a 100644
--- a/src/lang/primitive/core.zig
+++ b/src/lang/primitive/core.zig
@@ -51,36 +51,38 @@ const big_int = @import("../../runtime/numeric/big_int.zig");
/// `(map (partial instance? C) …)`, works exactly as in clj). Returns true iff
/// `x` is an instance of `c`.
///
-/// `runtime/class_name.zig::isInstance` is the COMPLETE membership oracle
-/// (native exact / interface multi-tag / Throwable hierarchy / Object-universal /
-/// user TypeDescriptor walk; an opaque collapsed class — Integer / BigInteger —
-/// is in no table so it returns false, clj-faithful). So this primitive is just
-/// `fqcn → isInstance` with no opaque/Object/Number special-case (those dissolved
-/// into the oracle per ADR-0128). A non-class `c` raises (clj: ClassCastException).
+/// `runtime/class_name.zig` is the COMPLETE membership oracle (native exact /
+/// interface multi-tag / Throwable hierarchy / Object-universal / descriptor
+/// identity for a user class; an opaque collapsed class, Integer / BigInteger,
+/// is in no table so it returns false, clj-faithful). So this primitive only
+/// picks the entry point, with no opaque/Object/Number special-case (those
+/// dissolved into the oracle per ADR-0128). A non-class `c` raises (clj:
+/// ClassCastException).
pub fn instanceOf(rt: *Runtime, env: *Env, args: []const Value, loc: SourceLocation) anyerror!Value {
_ = rt;
_ = env;
try error_catalog.checkArity("instance?", args, 2, loc);
- // The class arg is either a class VALUE (`.type_descriptor` — String / Number /
- // Object / a user deftype / an exception class / `clojure.lang.IFn` FQCN) OR a
- // cljw interface-marker PROTOCOL (`.protocol` — bare `IFn`/`ISeq`/`IPersistentMap`/
- // `IDeref`/… resolve to the defprotocol Var, the cljw representation of the clj
- // interface). Both name a class in the membership oracle, so dispatch by fqcn.
- const fqcn = switch (args[0].tag()) {
- .type_descriptor => td_mod.asTypeDescriptorRef(args[0]).fqcn orelse return .false_val,
- .protocol => protocol_mod.asProtocol(args[0]).fqcn(),
+ // The class arg is either a class VALUE (`.type_descriptor`: String / Number /
+ // Object / a user deftype / an exception class / a `clojure.lang.IFn` FQCN),
+ // answered through its descriptor so a user class is matched by identity
+ // (ADR-0201), OR a cljw interface-marker PROTOCOL (`.protocol`: bare `IFn` /
+ // `ISeq` / `IPersistentMap` / `IDeref` resolve to the defprotocol Var, the
+ // cljw representation of the clj interface), which names an interface.
+ const member = switch (args[0].tag()) {
+ .type_descriptor => class_name.isInstanceOfClass(args[1], td_mod.asTypeDescriptorRef(args[0])),
+ .protocol => class_name.isInstance(args[1], protocol_mod.asProtocol(args[0]).fqcn()),
else => return error_catalog.raise(.type_arg_invalid, loc, .{
.fn_name = "instance?",
.expected = "class",
.actual = @tagName(args[0].tag()),
}),
};
- return if (class_name.isInstance(args[1], fqcn)) .true_val else .false_val;
+ return Value.initBoolean(member);
}
/// `(-class-isa? child parent)` — true iff both args are class values
/// (TypeDescriptor) and `child`'s class is `parent` or a subclass of it, via the
-/// shared `class_name.classIsa` (Object/Number/IFn + interface markers + the host
+/// shared `class_name.classIsaOf` (Object/Number/IFn + interface markers + the host
/// exception/parent hierarchy — shared with multimethod dispatch so the two
/// cannot drift, D-464). The class-hierarchy arm of `isa?` (clj's
/// `Class.isAssignableFrom` step); equality + the ad-hoc `derive` hierarchy stay
@@ -91,13 +93,16 @@ pub fn classIsaPrim(rt: *Runtime, env: *Env, args: []const Value, loc: SourceLoc
try error_catalog.checkArity("__class-isa?", args, 2, loc);
if (args[0].tag() != .type_descriptor or args[1].tag() != .type_descriptor)
return .false_val;
- const child = td_mod.asTypeDescriptorRef(args[0]).fqcn orelse return .false_val;
- const parent = td_mod.asTypeDescriptorRef(args[1]).fqcn orelse return .false_val;
- // The class-vs-class hierarchy step (ADR-0109 Object/Number/IFn + D-293
- // interface markers + the exception/parent hierarchy) lives in the shared
- // `class_name.classIsa` so `isa?` and multimethod dispatch cannot drift
- // (D-464). The ad-hoc `derive` hierarchy + equality stay in the `.clj` isa?.
- return if (class_name.classIsa(child, parent)) .true_val else .false_val;
+ // The class-vs-class hierarchy step lives in the shared
+ // `class_name.classIsaOf` so `isa?` and multimethod dispatch cannot drift
+ // (D-464): a user class by descriptor identity and by what it declares,
+ // never by its name (ADR-0201); a native class by name (ADR-0109
+ // Object/Number/IFn, D-293 interface markers, the exception/parent
+ // hierarchy). The ad-hoc `derive` hierarchy + equality stay in the `.clj`
+ // isa?.
+ const child = td_mod.asTypeDescriptorRef(args[0]);
+ const parent = td_mod.asTypeDescriptorRef(args[1]);
+ return Value.initBoolean(class_name.classIsaOf(child, parent));
}
/// Java String's immediate interfaces plus Object. JVM's `Class.getInterfaces`
diff --git a/src/runtime/class_name.zig b/src/runtime/class_name.zig
index bbb736102..30f5df0f3 100644
--- a/src/runtime/class_name.zig
+++ b/src/runtime/class_name.zig
@@ -28,9 +28,13 @@
//! - **Throwable hierarchy**: delegates to `host_class.matches`
//! (covers `Throwable` / `Exception` / `RuntimeException` /
//! `ExceptionInfo` etc.).
-//! - **User types** (`defrecord` / `deftype` / `reify`): walks the
-//! `TypeDescriptor.parent` chain, comparing `fqcn` against the
-//! class name.
+//! - **User classes** (`defrecord` / `deftype` / `reify`) are NOT matched
+//! by name (ADR-0201). A user class is its TypeDescriptor, so membership
+//! in one is descriptor identity: `isInstanceOfClass` / `classIsaOf`,
+//! which take the class VALUE's descriptor. The name oracle below only
+//! answers which host interfaces a user type declares (its
+//! `protocol_impls` / `method_table`), and matches a descriptor's own
+//! fqcn only for a native host-surface descriptor.
//!
//! ## DIVERGENCE from cw v0
//!
@@ -308,13 +312,14 @@ fn isInterfaceName(simple: []const u8) bool {
/// 1. Throwable hierarchy (host_class.matches walks the parent chain).
/// 2. Native exact-tag table (`String`, `Long`, etc.).
/// 3. Interface-shaped multi-tag set (`IFn`, `Number`, `IPersistent*`).
-/// 4. User TypeDescriptor walk for `.typed_instance` / `.reified_instance`
-/// receivers — comparing the simple name against each ancestor's
-/// `fqcn`.
+/// 4. Descriptor walk for `.typed_instance` / `.reified_instance` /
+/// `.host_instance` receivers: the interfaces a type declares, plus the
+/// own fqcn of a native host-surface descriptor (see `matchUserType`).
///
-/// Unknown class names return false here; callers (= the
-/// `__instance?` primitive) should pre-check with `isKnown` and raise
-/// loud rather than relying on silent false.
+/// This is the NAME oracle, for host / native classes. A user class is
+/// never answered by name; callers holding a class VALUE go through
+/// `isInstanceOfClass`, which routes a user class to descriptor identity.
+/// Unknown class names return false here.
pub fn isInstance(v: Value, class_name: []const u8) bool {
const simple = normalizeClassName(class_name);
@@ -339,6 +344,59 @@ pub fn isInstance(v: Value, class_name: []const u8) bool {
return false;
}
+/// `(instance? C v)` for a class VALUE, where `class_td` is the descriptor `C`
+/// evaluates to. The entry point for every caller that holds a class rather
+/// than a name (`instance?`, `Class.isInstance`). A user class is answered by
+/// identity (ADR-0201): `v` is a member iff `class_td` is `v`'s descriptor or on
+/// its parent chain, so a redefined `R` and its previous vintage, or `aaa.Point`
+/// and `bbb.Point`, never answer for each other. A host / native class has no
+/// user descriptor and goes to the name oracle `isInstance`.
+pub fn isInstanceOfClass(v: Value, class_td: *const TypeDescriptor) bool {
+ if (class_td.isUserClass()) {
+ const own = userInstanceDescriptor(v) orelse return false;
+ return descendsFrom(own, class_td);
+ }
+ const name = class_td.fqcn orelse return false;
+ return isInstance(v, name);
+}
+
+/// Class-vs-class `isa?` over class VALUES, shared by the `isa?` primitive and
+/// multimethod dispatch so the two cannot drift (D-464). Identity first. A
+/// user-class PARENT is reached only along `child`'s parent chain. A user-class
+/// CHILD under a host parent is a subclass of Object and of what it declares
+/// (`descriptorIsa`, the relation `instance?` reads), never of a host class that
+/// merely shares its simple name: a `(deftype BigInt [])` is not a Number. Only
+/// a native child keeps the name relation `classIsa` (ADR-0201).
+pub fn classIsaOf(child: *const TypeDescriptor, parent: *const TypeDescriptor) bool {
+ if (child == parent) return true;
+ if (parent.isUserClass()) return descendsFrom(child, parent);
+ const parent_name = parent.fqcn orelse return false;
+ if (child.isUserClass()) {
+ const simple = normalizeClassName(parent_name);
+ return host_class.isUniversalClass(simple) or descriptorIsa(child, simple);
+ }
+ return classIsa(child.fqcn orelse "", parent_name);
+}
+
+/// The descriptor a user-class instance carries, or null for any other value
+/// (a host_instance's descriptor is a native surface, never a user class).
+fn userInstanceDescriptor(v: Value) ?*const TypeDescriptor {
+ return switch (v.tag()) {
+ .typed_instance => v.decodePtr(*const TypedInstance).descriptor,
+ .reified_instance => v.decodePtr(*const ReifiedInstance).descriptor,
+ else => null,
+ };
+}
+
+/// True iff `ancestor` is `td` itself or on `td`'s parent chain, by pointer.
+fn descendsFrom(td: *const TypeDescriptor, ancestor: *const TypeDescriptor) bool {
+ var cursor: ?*const TypeDescriptor = td;
+ while (cursor) |t| : (cursor = t.parent) {
+ if (t == ancestor) return true;
+ }
+ return false;
+}
+
fn isThrowableTag(t: Tag) bool {
return switch (t) {
.ex_info => true,
@@ -408,15 +466,15 @@ fn displayClassTag(name: []const u8) ?Tag {
return M.get(name);
}
-/// Class-vs-class `isa?` (the `(isa? )` hierarchy step), by fqcn
-/// name. SHARED by `core.classIsaPrim` (the `isa?` primitive) and
-/// `multimethod.isaCheck` (method dispatch) so the two cannot drift (D-464 — a
-/// `(defmethod f java.lang.Number)` must be found for a `Long` dispatch value
-/// exactly as `(isa? Long Number)` is true). Order mirrors clj's isa? class
-/// step: identity, universal Object, narrow Number, narrow IFn, interface-marker
-/// membership, then the host exception/parent hierarchy. The ad-hoc `derive`
-/// hierarchy and non-class isa? stay with the callers.
-pub fn classIsa(child: []const u8, parent: []const u8) bool {
+/// Class-vs-class `isa?` BY NAME, for a host / native parent: the name half of
+/// `classIsaOf`, which is the entry both `core.classIsaPrim` (the `isa?`
+/// primitive) and `multimethod.isaCheck` (method dispatch) call so the two
+/// cannot drift (D-464: a `(defmethod f java.lang.Number)` must be found for a
+/// `Long` dispatch value exactly as `(isa? Long Number)` is true). Order mirrors
+/// clj's isa? class step: identity, universal Object, narrow Number, narrow IFn,
+/// interface-marker membership, then the host exception/parent hierarchy. The
+/// ad-hoc `derive` hierarchy and non-class isa? stay with the callers.
+fn classIsa(child: []const u8, parent: []const u8) bool {
if (std.mem.eql(u8, child, parent)) return true;
if (host_class.isUniversalClass(parent)) return true;
if (host_class.isNumberClass(parent)) return host_class.isNumericClass(child);
@@ -442,11 +500,27 @@ fn matchUserType(v: Value, simple: []const u8) bool {
.host_instance => @import("host_instance.zig").asHostInstance(v).descriptor,
else => return false,
};
+ return descriptorIsa(td, simple);
+}
+
+/// True iff a value whose descriptor is `td` is an instance of the host class
+/// named `simple` (normalised). The descriptor-level half of `matchUserType`,
+/// shared with `classIsaOf` so `(instance? C x)` and `(isa? (class x) C)` read
+/// ONE relation. A user class answers only from what it DECLARES (its
+/// interfaces), never from its own name; a native host-surface descriptor also
+/// answers to its own fqcn (ADR-0201).
+fn descriptorIsa(td: *const TypeDescriptor, simple: []const u8) bool {
var cursor: ?*const TypeDescriptor = td;
while (cursor) |t| {
- if (t.fqcn) |fqcn| {
- if (std.mem.eql(u8, fqcn, simple)) return true;
- if (std.mem.eql(u8, normalizeClassName(fqcn), simple)) return true;
+ // Only a native host-surface descriptor is its NAME. A user class's
+ // fqcn is a display string it may share with a host interface (a
+ // `(deftype Sorted [])` is not `clojure.lang.Sorted`) or with another
+ // user class, so it never answers here (ADR-0201).
+ if (!t.isUserClass()) {
+ if (t.fqcn) |fqcn| {
+ if (std.mem.eql(u8, fqcn, simple)) return true;
+ if (std.mem.eql(u8, normalizeClassName(fqcn), simple)) return true;
+ }
}
// The declared interfaces (ADR-0116 Decision B). `protocol_impls` holds
// what the type DECLARED; the method table is not consulted, because a
@@ -676,3 +750,54 @@ test "isCallableClassName derives from the IFn tag set" {
try testing.expect(isClassInterfaceMember("TransientArrayMap", "Counted"));
try testing.expect(!isClassInterfaceMember("Cons", "Counted"));
}
+
+test "ADR-0201: a user class is matched by descriptor identity, never by its simple name" {
+ const Runtime = @import("runtime.zig").Runtime;
+ const td_mod = @import("type_descriptor.zig");
+ var threaded = std.Io.Threaded.init(testing.allocator, .{});
+ defer threaded.deinit();
+ var rt = Runtime.init(threaded.io(), testing.allocator);
+ defer rt.deinit();
+
+ // Two vintages of user.R, plus a namesake in another namespace.
+ const old = try td_mod.registerType(&rt, "R", "user", &.{"a"}, &.{}, .defrecord);
+ const old_inst = try td_mod.allocInstance(&rt, old, &.{Value.initInteger(1)});
+ const new = try td_mod.registerType(&rt, "R", "user", &.{ "a", "b" }, &.{}, .defrecord);
+ const namesake = try td_mod.registerType(&rt, "R", "other", &.{"a"}, &.{}, .defrecord);
+ try testing.expect(old != new);
+
+ try testing.expect(isInstanceOfClass(old_inst, old));
+ try testing.expect(!isInstanceOfClass(old_inst, new));
+ try testing.expect(!isInstanceOfClass(old_inst, namesake));
+ try testing.expect(!isInstanceOfClass(Value.initInteger(1), old));
+ try testing.expect(classIsaOf(old, old));
+ try testing.expect(!classIsaOf(old, new));
+ try testing.expect(!classIsaOf(new, old));
+ try testing.expect(!classIsaOf(namesake, old));
+ // The name oracle no longer answers for a user class by its display name.
+ try testing.expect(!isInstance(old_inst, "R"));
+
+ // Under a host parent a user class is an Object, and is NOT the subclass of
+ // a host class that merely shares its simple name.
+ const object = try rt.classDescriptor("Object");
+ try testing.expect(classIsaOf(old, object));
+ try testing.expect(isInstanceOfClass(old_inst, object));
+ const user_big_int = try td_mod.registerType(&rt, "BigInt", "user", &.{}, &.{}, .deftype);
+ try testing.expect(!classIsaOf(user_big_int, try rt.classDescriptor("Number")));
+ try testing.expect(classIsaOf(user_big_int, object));
+
+ // A reify descriptor has no name at all and is still its own class.
+ const reify_td: TypeDescriptor = .{
+ .fqcn = null,
+ .kind = .reify_anon,
+ .field_layout = null,
+ .protocol_impls = &.{},
+ .method_table = &.{},
+ .parent = null,
+ .meta = .nil_val,
+ };
+ const reified = try td_mod.allocReifiedInstance(&rt, &reify_td);
+ try testing.expect(isInstanceOfClass(reified, &reify_td));
+ try testing.expect(!isInstanceOfClass(reified, old));
+ try testing.expect(classIsaOf(&reify_td, object));
+}
diff --git a/src/runtime/java/lang/Class.zig b/src/runtime/java/lang/Class.zig
index 8279e9ee2..b064def83 100644
--- a/src/runtime/java/lang/Class.zig
+++ b/src/runtime/java/lang/Class.zig
@@ -60,16 +60,16 @@ fn getSimpleName(rt: *Runtime, env: *Env, args: []const Value, loc: SourceLocati
}
/// `(.isInstance klass obj)` — true iff `obj` is an instance of `klass`. Routes
-/// through the shared `class_name.isInstance` predicate (native tags + interface
-/// sets + Throwable hierarchy + user TypeDescriptor walk). JVM reference:
+/// through the same `class_name.isInstanceOfClass` entry `instance?` uses
+/// (native tags + interface sets + Throwable hierarchy by name, descriptor
+/// identity for a user class per ADR-0201). JVM reference:
/// java.lang.Class#isInstance.
fn isInstance(rt: *Runtime, env: *Env, args: []const Value, loc: SourceLocation) anyerror!Value {
_ = rt;
_ = env;
try error_catalog.checkArity(".isInstance", args, 2, loc);
const td = type_descriptor.asTypeDescriptorRef(args[0]);
- const name = td.fqcn orelse return .false_val;
- return Value.initBoolean(class_name.isInstance(args[1], name));
+ return Value.initBoolean(class_name.isInstanceOfClass(args[1], td));
}
/// Populate the per-Runtime `.type_descriptor` native descriptor's method table
diff --git a/src/runtime/multimethod.zig b/src/runtime/multimethod.zig
index 70dd1d373..1c6a7daf2 100644
--- a/src/runtime/multimethod.zig
+++ b/src/runtime/multimethod.zig
@@ -153,13 +153,11 @@ pub fn isPreferred(prefer_table: Value, x: Value, y: Value) !bool {
return try set_mod.contains(preferred_over, y);
}
-/// cw v1 `isa?` check: equality + hierarchy ancestors-map lookup +
-/// TypeDescriptor parent walk. Mirrors `clojure.core/isa?` steps
-/// 1 + 3. Step 2 (`Class.isAssignableFrom`) and step 4 (`supers`
-/// walk) have no JVM-direct equivalent in cw v1 per ADR-0007
-/// Option β + `.claude/rules/no_jvm_specific_assumption.md` — the
-/// `typeDescriptorIsa` walk is the cw-side replacement (DIVERGENCE
-/// from JVM class hierarchy).
+/// cw v1 `isa?` check for multimethod dispatch, mirroring `clojure.core/isa?`:
+/// equality, element-wise vectors, the class-vs-class step through the shared
+/// `class_name.classIsaOf` (clj's `Class.isAssignableFrom` step, the same
+/// function the `isa?` primitive calls), then the `derive` hierarchy's
+/// ancestors map.
///
/// `hierarchy_ancestors` is the `:ancestors` sub-map of a
/// hierarchy struct: `{child #{ancestor1 ancestor2 ...}, ...}`.
@@ -186,38 +184,22 @@ pub fn isaCheck(hierarchy_ancestors: Value, child: Value, parent: Value) !bool {
// D-464: a class VALUE child isa? a class VALUE parent via the class hierarchy
// (Long isa? Number, vector-class isa? Sequential), through the SAME shared
- // `class_name.classIsa` the `isa?` primitive uses. Only short-circuits on a
- // true match; a class can ALSO sit in an ad-hoc `derive` hierarchy, so a
+ // `class_name.classIsaOf` the `isa?` primitive uses, so a method keyed by a
+ // user class catches only that class, never a same-named redefinition
+ // vintage or another namespace's namesake (ADR-0201). Only short-circuits on
+ // a true match; a class can ALSO sit in an ad-hoc `derive` hierarchy, so a
// non-match falls through to the ancestors lookup below.
if (child.tag() == .type_descriptor and parent.tag() == .type_descriptor) {
- const child_fqcn = td_mod.asTypeDescriptorRef(child).fqcn orelse "";
- const parent_fqcn = td_mod.asTypeDescriptorRef(parent).fqcn orelse "";
- if (class_name_mod.classIsa(child_fqcn, parent_fqcn)) return true;
- }
-
- // Row 7.5 cycle 6 — D-082 discharge: typed_instance + reified_instance
- // children walk their descriptor chain. JVM `clojure.core/isa?` step 2
- // (`Class.isAssignableFrom`) + step 4 (`supers` walk) map onto:
- // - `descriptor.fqcn` ≡ JVM Class.name
- // - `descriptor.protocol_impls` ≡ JVM implementedInterfaces walk
- // - `descriptor.parent` ≡ JVM superclass chain
- // Parent symbol/keyword name is matched against each. Triggered only
- // when child is a TypedInstance / ReifiedInstance and parent is
- // a Named (symbol or keyword) — the JVM analogue's input shape.
- if ((child.tag() == .typed_instance or child.tag() == .reified_instance) and
- (parent.tag() == .symbol or parent.tag() == .keyword))
- {
- const parent_name: []const u8 = if (parent.tag() == .symbol)
- symbol.asSymbol(parent).name
- else
- keyword.asKeyword(parent).name;
- const child_desc: *const td_mod.TypeDescriptor = if (child.tag() == .typed_instance)
- child.decodePtr(*const td_mod.TypedInstance).descriptor
- else
- child.decodePtr(*const td_mod.ReifiedInstance).descriptor;
- if (typeDescriptorIsa(child_desc, parent_name)) return true;
+ const child_td = td_mod.asTypeDescriptorRef(child);
+ const parent_td = td_mod.asTypeDescriptorRef(parent);
+ if (class_name_mod.classIsaOf(child_td, parent_td)) return true;
}
+ // An INSTANCE child (a `(defmulti m identity)` dispatch value) is related
+ // to a symbol or keyword parent only through the `derive` hierarchy below,
+ // never by its class's simple name or a protocol its type declares:
+ // clj's isa? has no such step, and `(isa? (->R 1) 'R)` is false here too
+ // (ADR-0201).
if (hierarchy_ancestors.tag() == .nil) return false;
const child_ancestors = try map_mod.get(hierarchy_ancestors, child);
@@ -225,22 +207,6 @@ pub fn isaCheck(hierarchy_ancestors: Value, child: Value, parent: Value) !bool {
return try set_mod.contains(child_ancestors, parent);
}
-/// Walk a descriptor's `fqcn` + `protocol_impls` + `parent` chain
-/// looking for a match against `parent_name`. Row 7.5 cycle 6 D-082
-/// discharge — replaces JVM `Class.isAssignableFrom` + `supers` walk
-/// per `.claude/rules/no_jvm_specific_assumption.md` (cw-side
-/// equivalent of class hierarchy via TypeDescriptor).
-fn typeDescriptorIsa(td: *const td_mod.TypeDescriptor, parent_name: []const u8) bool {
- if (td.fqcn) |fqcn| {
- if (std.mem.eql(u8, fqcn, parent_name)) return true;
- }
- for (td.protocol_impls) |impl| {
- if (std.mem.eql(u8, impl, parent_name)) return true;
- }
- if (td.parent) |p| return typeDescriptorIsa(p, parent_name);
- return false;
-}
-
/// Dereference an IRef-shape hierarchy reference to its held map.
/// `defmulti` threads the `-global-hierarchy` atom (D-161), so an
/// atom ref is deref'd to its current value — and crucially each
@@ -545,74 +511,31 @@ test "isaCheck: nil hierarchy falls back to equality only" {
try testing.expect(!(try isaCheck(Value.nil_val, a, b)));
}
-test "isaCheck: typed_instance child matches descriptor.fqcn against Symbol parent (D-082)" {
+test "isaCheck: an instance child is not isa? a Named parent spelled like its class (ADR-0201)" {
var fix: TestFixture = undefined;
fix.init(testing.allocator);
defer fix.deinit();
- // Synthetic descriptor with fqcn "Foo".
+ // clj's isa? relates an INSTANCE only by equality (derive rejects one as a
+ // child), so neither its class's simple name nor a protocol its type
+ // declares makes `(defmulti m identity)` catch it under that name.
+ const impls = [_][]const u8{"Drawable"};
const td = try testing.allocator.create(td_mod.TypeDescriptor);
defer testing.allocator.destroy(td);
td.* = .{
.fqcn = "Foo",
.kind = .defrecord,
.field_layout = null,
- .protocol_impls = &.{},
- .method_table = &.{},
- .parent = null,
- .meta = Value.nil_val,
- };
- const inst_val = try td_mod.allocInstance(&fix.rt, td, &.{});
- const foo_sym = try symbol.intern(&fix.rt, null, "Foo");
- const bar_sym = try symbol.intern(&fix.rt, null, "Bar");
- try testing.expect(try isaCheck(Value.nil_val, inst_val, foo_sym));
- try testing.expect(!(try isaCheck(Value.nil_val, inst_val, bar_sym)));
-}
-
-test "isaCheck: typed_instance child walks protocol_impls list (D-082)" {
- var fix: TestFixture = undefined;
- fix.init(testing.allocator);
- defer fix.deinit();
-
- const impls = [_][]const u8{ "Drawable", "Serializable" };
- const td = try testing.allocator.create(td_mod.TypeDescriptor);
- defer testing.allocator.destroy(td);
- td.* = .{
- .fqcn = "Shape",
- .kind = .defrecord,
- .field_layout = null,
.protocol_impls = &impls,
.method_table = &.{},
.parent = null,
.meta = Value.nil_val,
};
const inst_val = try td_mod.allocInstance(&fix.rt, td, &.{});
- const drawable_sym = try symbol.intern(&fix.rt, null, "Drawable");
- const unknown_sym = try symbol.intern(&fix.rt, null, "Unknown");
- try testing.expect(try isaCheck(Value.nil_val, inst_val, drawable_sym));
- try testing.expect(!(try isaCheck(Value.nil_val, inst_val, unknown_sym)));
-}
-
-test "isaCheck: reified_instance child walks descriptor like typed_instance (D-082)" {
- var fix: TestFixture = undefined;
- fix.init(testing.allocator);
- defer fix.deinit();
-
- const impls = [_][]const u8{"IProto"};
- const td = try testing.allocator.create(td_mod.TypeDescriptor);
- defer testing.allocator.destroy(td);
- td.* = .{
- .fqcn = null,
- .kind = .reify_anon,
- .field_layout = null,
- .protocol_impls = &impls,
- .method_table = &.{},
- .parent = null,
- .meta = Value.nil_val,
- };
- const inst_val = try td_mod.allocReifiedInstance(&fix.rt, td);
- const iproto_sym = try symbol.intern(&fix.rt, null, "IProto");
- try testing.expect(try isaCheck(Value.nil_val, inst_val, iproto_sym));
+ try testing.expect(!(try isaCheck(Value.nil_val, inst_val, try symbol.intern(&fix.rt, null, "Foo"))));
+ try testing.expect(!(try isaCheck(Value.nil_val, inst_val, try keyword.intern(&fix.rt, null, "Foo"))));
+ try testing.expect(!(try isaCheck(Value.nil_val, inst_val, try symbol.intern(&fix.rt, null, "Drawable"))));
+ try testing.expect(try isaCheck(Value.nil_val, inst_val, inst_val));
}
test "getMethod: hierarchy walk returns ancestor's method on isa? match" {
diff --git a/src/runtime/type_descriptor.zig b/src/runtime/type_descriptor.zig
index 8fc5b5389..de2e8a663 100644
--- a/src/runtime/type_descriptor.zig
+++ b/src/runtime/type_descriptor.zig
@@ -262,6 +262,15 @@ pub const TypeDescriptor = struct {
big_decimal: i64,
};
+ /// True iff this descriptor is a class the USER defined (`deftype`,
+ /// `defrecord`, `reify`), as opposed to a native / host-surface class.
+ /// A user class is identified by the descriptor itself, never by its
+ /// display name (ADR-0201): two vintages of a redefined `R`, or `aaa.Point`
+ /// and `bbb.Point`, share a simple name and are still different classes.
+ pub fn isUserClass(self: *const TypeDescriptor) bool {
+ return self.kind != .native;
+ }
+
/// Find a static field by name (ADR-0061). Linear — field tables are
/// tiny (≤ 2 today). Parallel to `lookupMethod`; walks the parent
/// chain for symmetry though host surfaces have no parent.
@@ -662,17 +671,6 @@ pub fn finaliseReifiedInstance(gc_ptr: *anyopaque, header: *HeapHeader) void {
_ = header;
}
-/// Allocate a TypeDescriptor on `rt.gpa` with the declared field
-/// layout and register it in `rt.types` under `name`. Re-registering
-/// the same name frees the previous descriptor (keeps the REPL
-/// re-`def` path clean). `field_names` is borrowed; each entry is
-/// `dupe`'d into the descriptor.
-///
-/// Shared by the `__deftype!` and `__defrecord!` Layer-2 primitives —
-/// both lower to the same Layer-0 surface per F-009 (feature-impl
-/// neutrality), differing only in the `kind` argument. Since ADR-0066
-/// both `deftype` and `defrecord` are macros emitting their respective
-/// `rt/__…!` registration call (no analyzer Node fork, no backend op).
/// Build a defrecord instance from map `m` — clj's `Record/create`:
/// declared fields pulled by keyword (nil when absent), every remaining
/// key held in the extmap (nil when empty, so the result `=`/hashes as a
@@ -700,6 +698,19 @@ pub fn recordFromMap(rt: *Runtime, td: *const TypeDescriptor, m: Value) !Value {
return allocInstanceFull(rt, td, fields, Value.nil_val, ext);
}
+/// Allocate a TypeDescriptor on `rt.gpa` with the declared field layout and
+/// register it in `rt.types` under its qualified key `.`
+/// (ADR-0198). Re-registering the same key RETIRES the previous descriptor to
+/// `rt.retired_types` rather than freeing it: instances, boxed class values
+/// and CallSite caches still hold it, and the retired descriptor stays a
+/// distinct class from its replacement (ADR-0201). `field_names` is borrowed;
+/// each entry is `dupe`'d into the descriptor.
+///
+/// Shared by the `__deftype!` and `__defrecord!` Layer-2 primitives, which
+/// lower to the same Layer-0 surface per F-009 (feature-impl neutrality) and
+/// differ only in the `kind` argument. Since ADR-0066 both `deftype` and
+/// `defrecord` are macros emitting their `__deftype!` / `__defrecord!`
+/// registration call (no analyzer Node fork, no backend op).
pub fn registerType(
rt: *Runtime,
name: []const u8,
diff --git a/test/diff/clj_corpus/type_redefinition.txt b/test/diff/clj_corpus/type_redefinition.txt
new file mode 100644
index 000000000..cb7d2cedf
--- /dev/null
+++ b/test/diff/clj_corpus/type_redefinition.txt
@@ -0,0 +1,254 @@
+(do (defprotocol Area (area [s])) :ok)
+;;=> :ok
+(do (defrecord R [a] Area (area [_] :old-area)) :ok)
+;;=> :ok
+(do (def r (->R 1)) (def rc (class r)) :ok)
+;;=> :ok
+(do (defrecord R [a b] Area (area [_] :new-area)) :ok)
+;;=> :ok
+(:a r)
+;;=> 1
+(.-a r)
+;;=> 1
+(get r :b :missing)
+;;=> :missing
+(pr-str r)
+;;=> "#user.R{:a 1}"
+(pr-str (->R 1 2))
+;;=> "#user.R{:a 1, :b 2}"
+(instance? R r)
+;;=> false
+(instance? rc r)
+;;=> true
+(instance? user.R r)
+;;=> false
+(instance? R (->R 1 2))
+;;=> true
+(instance? rc (->R 1 2))
+;;=> false
+(.isInstance R r)
+;;=> false
+(.isInstance rc r)
+;;=> true
+(= rc R)
+;;=> false
+(identical? rc (class r))
+;;=> true
+(= r r)
+;;=> true
+(= r (->R 1 nil))
+;;=> false
+(= (->R 1 2) (->R 1 2))
+;;=> true
+(= (hash r) (hash r))
+;;=> true
+(integer? (hash r))
+;;=> true
+(area r)
+;;=> :old-area
+(area (->R 1 2))
+;;=> :new-area
+(satisfies? Area r)
+;;=> true
+(extends? Area rc)
+;;=> true
+(map? r)
+;;=> true
+(record? r)
+;;=> true
+(pr-str (assoc r :c 3))
+;;=> "#user.R{:a 1, :c 3}"
+(pr-str (assoc r :a 9))
+;;=> "#user.R{:a 9}"
+(pr-str (dissoc r :a))
+;;=> "{}"
+(keys r)
+;;=> (:a)
+(vals r)
+;;=> (1)
+(count r)
+;;=> 1
+(isa? rc R)
+;;=> false
+(isa? R rc)
+;;=> false
+(isa? rc rc)
+;;=> true
+(isa? rc Object)
+;;=> true
+(instance? Object r)
+;;=> true
+(do (defmulti f class) (defmethod f :default [_] :default) (defmethod f R [_] :new) :ok)
+;;=> :ok
+(f r)
+;;=> :default
+(f (->R 1 2))
+;;=> :new
+(do (defmethod f rc [_] :old) :ok)
+;;=> :ok
+(f r)
+;;=> :old
+(f (->R 1 2))
+;;=> :new
+(do (defprotocol P2 (p2 [x])) (extend-protocol P2 R (p2 [_] :new-p2)) :ok)
+;;=> :ok
+(satisfies? P2 r)
+;;=> false
+(satisfies? P2 (->R 1 2))
+;;=> true
+(extends? P2 rc)
+;;=> false
+(p2 (->R 1 2))
+;;=> :new-p2
+(do (derive rc ::old-kind) :ok)
+;;=> :ok
+(isa? rc ::old-kind)
+;;=> true
+(isa? R ::old-kind)
+;;=> false
+(do (defrecord S [a]) :ok)
+;;=> :ok
+(do (def s1 (->S 1)) :ok)
+;;=> :ok
+(do (defrecord S [a]) :ok)
+;;=> :ok
+(= s1 (->S 1))
+;;=> false
+(= (hash s1) (hash (->S 1)))
+;;=> true
+(instance? S s1)
+;;=> false
+(pr-str s1)
+;;=> "#user.S{:a 1}"
+(do (deftype T [x] Area (area [_] :old-t)) :ok)
+;;=> :ok
+(do (def t (T. 1)) (def tc (class t)) :ok)
+;;=> :ok
+(do (deftype T [x y]) :ok)
+;;=> :ok
+(.-x t)
+;;=> 1
+(.-x (T. 1 2))
+;;=> 1
+(.-y (T. 1 2))
+;;=> 2
+(instance? T t)
+;;=> false
+(instance? tc t)
+;;=> true
+(instance? T (T. 1 2))
+;;=> true
+(.isInstance T t)
+;;=> false
+(= tc T)
+;;=> false
+(= t t)
+;;=> true
+(= t (T. 1 2))
+;;=> false
+(= (hash t) (hash t))
+;;=> true
+(area t)
+;;=> :old-t
+(satisfies? Area t)
+;;=> true
+(satisfies? Area (T. 1 2))
+;;=> false
+(isa? tc T)
+;;=> false
+(isa? tc Object)
+;;=> true
+(f t)
+;;=> :default
+(do (defn mk [i] (reify Area (area [_] i))) :ok)
+;;=> :ok
+(reduce + (map area (map mk (range 1000))))
+;;=> 499500
+(let [xs (doall (map mk (range 200)))] (System/gc) (reduce + (map area xs)))
+;;=> 19900
+(let [a (mk 1)] [(instance? (class a) a) (.isInstance (class a) a) (isa? (class a) (class a)) (instance? Object a) (isa? (class a) Object)])
+;;=> [true true true true true]
+(do (dotimes [_ 300] (eval '(defrecord Churn [v]))) :ok)
+;;=> :ok
+(pr-str (->Churn 7))
+;;=> "#user.Churn{:v 7}"
+(let [olds (doall (for [i (range 50)] (do (eval '(defrecord Churn2 [v])) (eval (list '->Churn2 i)))))] (System/gc) [(reduce + (map :v olds)) (pr-str (first olds)) (pr-str (last olds)) (count (set (map class olds)))])
+;;=> [1225 "#user.Churn2{:v 0}" "#user.Churn2{:v 49}" 50]
+(let [olds (doall (for [i (range 20)] (do (eval '(defrecord Churn3 [v])) (eval (list '->Churn3 i)))))] (count (filter #(= (class %) (class (last olds))) olds)))
+;;=> 1
+(do (deftype Sorted []) :ok)
+;;=> :ok
+(sorted? (Sorted.))
+;;=> false
+(instance? clojure.lang.Sorted (Sorted.))
+;;=> false
+(isa? Sorted clojure.lang.Sorted)
+;;=> false
+(do (deftype Keyword []) :ok)
+;;=> :ok
+(ifn? (Keyword.))
+;;=> false
+(instance? clojure.lang.IFn (Keyword.))
+;;=> false
+(isa? Keyword clojure.lang.IFn)
+;;=> false
+(keyword? (Keyword.))
+;;=> false
+(do (deftype PersistentVector []) :ok)
+;;=> :ok
+(vector? (PersistentVector.))
+;;=> false
+(isa? PersistentVector clojure.lang.IFn)
+;;=> false
+(isa? PersistentVector clojure.lang.IPersistentVector)
+;;=> false
+(do (deftype BigInt []) :ok)
+;;=> :ok
+(number? (BigInt.))
+;;=> false
+(instance? Number (BigInt.))
+;;=> false
+(isa? BigInt Number)
+;;=> false
+(do (deftype Ratio []) :ok)
+;;=> :ok
+(isa? Ratio Number)
+;;=> false
+(rational? (Ratio.))
+;;=> false
+(do (deftype ExceptionInfo []) :ok)
+;;=> :ok
+(isa? ExceptionInfo Exception)
+;;=> false
+(instance? Throwable (ExceptionInfo.))
+;;=> false
+(do (deftype LazySeq []) :ok)
+;;=> :ok
+(isa? LazySeq clojure.lang.Seqable)
+;;=> false
+(seq? (LazySeq.))
+;;=> false
+(do (defmulti g class) (defmethod g Number [_] :number) (defmethod g clojure.lang.IFn [_] :ifn) (defmethod g :default [_] :default) :ok)
+;;=> :ok
+(g (BigInt.))
+;;=> :default
+(g (Keyword.))
+;;=> :default
+(g 1)
+;;=> :number
+(g :k)
+;;=> :ifn
+(do (defmulti mi identity) (defmethod mi 'R [_] :sym) (defmethod mi :R [_] :kw) (defmethod mi 'Area [_] :protocol) (defmethod mi :default [_] :default) :ok)
+;;=> :ok
+(mi (->R 1 2))
+;;=> :default
+(mi (mk 1))
+;;=> :default
+(mi r)
+;;=> :default
+(isa? (->R 1 2) 'R)
+;;=> false
+(isa? (->R 1 2) :R)
+;;=> false
+(isa? (mk 1) 'Area)
+;;=> false
From ea75de74ec81d9d00f7b590844d2eaf5570dccc0 Mon Sep 17 00:00:00 2001
From: Pedro Gomes Branquinho
Date: Wed, 14 Oct 2026 20:55:34 -0300
Subject: [PATCH 05/17] fix(analyzer): a bare constructor binds the class of
the namespace it was written in ([CLJW-CTOR-ANALYZE-TIME])
analyzeCtorCall records definer_ns, the namespace a bare, non-imported
(Name. args) form is analyzed in. Both backends carry it to
constructInstance (tree_walk evalConstructorCall, vm op_ctor_call through
CtorEntry.definer_ns), which looks up . before any other
reading. So aaa/->Point and a (Point. x y) inside aaa build aaa.Point even
after bbb registered a namesake of a different shape (ADR-0198, ADR-0201).
The ctor site's definer_ns is serialized: bytecode VERSION 12 -> 13
(docs/spec/formats/chunk-v13.edn). A unit test pins the definer_ns-first
lookup.
Tests: user_class_identity_test.clj (new) covers instance?, isa?, class
equality and dispatch across redefinition and namesakes. The six scenarios
of the former type_descriptor_redefinition e2e (never landed) live in
type_redefine_test.clj, so the e2e count does not grow.
Smell-audited: 2: threads one namespace field from the analyzer to the existing constructor lookup and bumps the chunk format once; no new dispatch path.
---
docs/spec/formats/chunk-v13.edn | 17 ++
src/eval/analyzer/analyzer.zig | 21 +++
src/eval/analyzer/special_forms.zig | 46 ++++-
src/eval/backend/tree_walk.zig | 2 +-
src/eval/backend/vm.zig | 2 +-
src/eval/backend/vm/compiler.zig | 1 +
src/eval/backend/vm/opcode.zig | 5 +-
src/eval/bytecode/serialize.zig | 28 ++-
src/eval/node.zig | 6 +
test/clj/suites/type_redefine_test.clj | 133 +++++++++++++++
test/clj/suites/user_class_identity_test.clj | 169 +++++++++++++++++++
11 files changed, 415 insertions(+), 15 deletions(-)
create mode 100644 docs/spec/formats/chunk-v13.edn
create mode 100644 test/clj/suites/user_class_identity_test.clj
diff --git a/docs/spec/formats/chunk-v13.edn b/docs/spec/formats/chunk-v13.edn
new file mode 100644
index 000000000..e19d50431
--- /dev/null
+++ b/docs/spec/formats/chunk-v13.edn
@@ -0,0 +1,17 @@
+;; Add-only bytecode archive amendment, 2026-10-14 (ADR-0034 D11, ADR-0198,
+;; ADR-0201, [CLJW-CTOR-ANALYZE-TIME]). Earlier records remain unchanged.
+;; v13 keeps the chunk-body framing of chunk-v12.edn except for one field
+;; added to each ctor_sites entry, so a decoder of an older version must
+;; reject the chunk.
+{:chunk-format
+ {:version 13
+ :magic "CLJW"
+ :base-layout "chunk-v12.edn"
+ :semantic-history
+ [{:version 13
+ :change "A ctor site carries definer_ns, the namespace its `(Name. args)` form was analyzed in, so a bare user-type name resolves to . before any reading the calling namespace could give it (ADR-0198, ADR-0201)."}]
+ :ctor-site
+ {:fields [{:name :type_name :body "u32 length, then bytes"}
+ {:name :definer_ns :body "u32 length, then bytes; empty when the name was qualified at analysis or no namespace was current"}
+ {:name :arg_count :body "u16"}]}
+ :compatibility "The runtime rejects a chunk version unequal to 13; rebuild older payloads."}}
diff --git a/src/eval/analyzer/analyzer.zig b/src/eval/analyzer/analyzer.zig
index 6a29ac4b3..5723828b2 100644
--- a/src/eval/analyzer/analyzer.zig
+++ b/src/eval/analyzer/analyzer.zig
@@ -2444,6 +2444,27 @@ test "ctor call (Foo. ...) analyses into interop_call_node .constructor" {
try testing.expect(n.interop_call_node.kind == .constructor);
try testing.expectEqualStrings("Foo", n.interop_call_node.type_name);
try testing.expectEqual(@as(usize, 2), n.interop_call_node.args.len);
+ // [CLJW-CTOR-ANALYZE-TIME]: a bare type name carries the namespace it was
+ // analyzed in, so eval-time lookup qualifies it lexically.
+ try testing.expectEqualStrings(fix.env.current_ns.?.name, n.interop_call_node.definer_ns);
+}
+
+test "constructInstance resolves . before the last-wins simple name" {
+ const td_mod = @import("../../runtime/type_descriptor.zig");
+ var fix: TestFixture = undefined;
+ try fix.init(testing.allocator);
+ defer fix.deinit();
+ // aaa.Point has two fields; bbb.Point, registered later, has one, so a
+ // simple-name lookup would pick bbb's and reject two arguments.
+ const aaa = try td_mod.registerType(&fix.rt, "Point", "aaa", &.{ "x", "y" }, &.{}, .defrecord);
+ const bbb = try td_mod.registerType(&fix.rt, "Point", "bbb", &.{"q"}, &.{}, .defrecord);
+ const two = [_]Value{ Value.initInteger(1), Value.initInteger(2) };
+ const a = try special_forms.constructInstance(&fix.rt, &fix.env, "Point", "aaa", &two, .{});
+ try testing.expect(a.tag() == .typed_instance);
+ try testing.expect(a.decodePtr(*const td_mod.TypedInstance).descriptor == aaa);
+ const one = [_]Value{Value.initInteger(9)};
+ const b = try special_forms.constructInstance(&fix.rt, &fix.env, "Point", "bbb", &one, .{});
+ try testing.expect(b.decodePtr(*const td_mod.TypedInstance).descriptor == bbb);
}
test "member access (.x inst) analyses into interop_call_node .instance_member" {
diff --git a/src/eval/analyzer/special_forms.zig b/src/eval/analyzer/special_forms.zig
index ffccaa65c..27681b7d3 100644
--- a/src/eval/analyzer/special_forms.zig
+++ b/src/eval/analyzer/special_forms.zig
@@ -104,9 +104,22 @@ pub fn constructInstance(
rt: *Runtime,
env: *Env,
type_name: []const u8,
+ /// The namespace the ctor form was analyzed in ("" when none). A user
+ /// type registered as `.` wins over every other
+ /// reading of the bare name, as a class name clj compiled there would.
+ definer_ns: []const u8,
args: []const Value,
loc: error_catalog.SourceLocation,
) anyerror!Value {
+ if (definer_ns.len != 0) {
+ var qbuf: [512]u8 = undefined;
+ if (std.fmt.bufPrint(&qbuf, "{s}.{s}", .{ definer_ns, type_name })) |qualified| {
+ if (rt.types.get(qualified)) |td| return constructFromDescriptor(rt, env, td, type_name, args, loc);
+ } else |_| {
+ // Namespace + name longer than the buffer: no registered key
+ // could match it either, so fall through to the steps below.
+ }
+ }
// D-284: `(MapEntry. k v)` / `(new clojure.lang.MapEntry k v)` constructs an
// entry, so collection libs that build entries explicitly (priority-map,
// ordered, …) work. It used to return a plain 2-vector, from when a map entry
@@ -151,9 +164,23 @@ pub fn constructInstance(
}
const td = resolveJavaSurface(rt, env, type_name) orelse
return error_catalog.raise(.symbol_unresolved, loc, .{ .sym = type_name });
+ return constructFromDescriptor(rt, env, td, type_name, args, loc);
+}
+
+/// The descriptor-driven half of `constructInstance`: a field layout
+/// (deftype / record) allocates an instance, a `` entry (java surface)
+/// is called through the runtime vtable, anything else takes no constructor.
+fn constructFromDescriptor(
+ rt: *Runtime,
+ env: *Env,
+ td: *const type_descriptor_mod.TypeDescriptor,
+ type_name: []const u8,
+ args: []const Value,
+ loc: error_catalog.SourceLocation,
+) anyerror!Value {
if (td.field_layout) |fl| {
if (args.len != fl.len)
- return error_catalog.raise(.arity_not_expected, loc, .{ .got = args.len, .fn_name = type_name, .expected = fl.len });
+ return error_catalog.raise(.arity_not_expected, loc, .{ .got = args.len, .fn_name = td.fqcn orelse type_name, .expected = fl.len });
return type_descriptor_mod.allocInstance(rt, td, args);
}
if (td.lookupMethod(null, "")) |me| {
@@ -261,14 +288,20 @@ pub fn analyzeCtorCall(
// lexically correct one. Eval-time resolution would consult the CALLER's
// ns and miss it (hiccup.util's `(URI. s)` inside to-uri, called from
// user code). Guarded on the FQCN actually resolving as a registered
- // surface. A USER deftype keeps the bare spelling here and resolves on
- // the eval-time path (which also allows forward refs to later deftypes);
- // since ADR-0198 that path finds it by qualifying the bare name with the
- // current ns inside `host_class_resolve.resolve`, rather than by the
- // simple-name registry hit this comment used to describe.
+ // surface.
+ //
+ // A USER type keeps the bare spelling and resolves at eval time, which is
+ // what lets a `(Name. …)` inside the defrecord's own expansion refer to a
+ // type registered a moment later. The namespace of THIS analysis rides
+ // along as `definer_ns` so that eval-time lookup qualifies the name
+ // lexically (ADR-0198): the current ns at eval time is the CALLER's, and
+ // a bare `Point` there fell to the last-wins simple-name index, so
+ // `aaa/->Point` built whichever `Point` any namespace registered last.
var resolved_name = type_name;
+ var definer_ns: []const u8 = "";
if (std.mem.findScalar(u8, type_name, '.') == null) {
if (env.current_ns) |cur_ns| {
+ definer_ns = try arena.dupe(u8, cur_ns.name);
if (cur_ns.imports.get(type_name)) |fqcn| {
if (rt.types.get(fqcn) != null) {
resolved_name = fqcn;
@@ -288,6 +321,7 @@ pub fn analyzeCtorCall(
n.* = .{ .interop_call_node = .{
.kind = .constructor,
.type_name = resolved_name,
+ .definer_ns = if (resolved_name.ptr == type_name.ptr) definer_ns else "",
.args = args,
.loc = form.location,
} };
diff --git a/src/eval/backend/tree_walk.zig b/src/eval/backend/tree_walk.zig
index ba3632d82..809fad70e 100644
--- a/src/eval/backend/tree_walk.zig
+++ b/src/eval/backend/tree_walk.zig
@@ -722,7 +722,7 @@ fn evalConstructorCall(rt: *Runtime, env: *Env, locals: []Value, n: node_mod.Int
buf[i] = try eval(rt, env, locals, &arg_node);
af.publish(i + 1);
}
- return special_forms.constructInstance(rt, env, n.type_name, buf[0..n.args.len], n.loc);
+ return special_forms.constructInstance(rt, env, n.type_name, n.definer_ns, buf[0..n.args.len], n.loc);
}
/// `(.member recv args...)` / `(.-field recv)` unified instance member
diff --git a/src/eval/backend/vm.zig b/src/eval/backend/vm.zig
index b8a0551f0..975f5279a 100644
--- a/src/eval/backend/vm.zig
+++ b/src/eval/backend/vm.zig
@@ -1373,7 +1373,7 @@ inline fn stepOnce(
// Shared resolver/dispatcher (deftype/record + java-surface
// ``) — identical to TreeWalk's evalConstructorCall so a
// `(java.io.File. …)` ctor works on both backends.
- const new_val = try special_forms.constructInstance(rt, env, type_name, args_slice, .{});
+ const new_val = try special_forms.constructInstance(rt, env, type_name, ctor.definer_ns, args_slice, .{});
sp -= arg_count;
stack[sp] = new_val;
sp += 1;
diff --git a/src/eval/backend/vm/compiler.zig b/src/eval/backend/vm/compiler.zig
index d9f2dda8f..50fa32b0a 100644
--- a/src/eval/backend/vm/compiler.zig
+++ b/src/eval/backend/vm/compiler.zig
@@ -738,6 +738,7 @@ const Compiler = struct {
const name_dup = try self.arena.dupe(u8, n.type_name);
try self.ctor_sites.append(self.arena, .{
.type_name = name_dup,
+ .definer_ns = try self.arena.dupe(u8, n.definer_ns),
.arg_count = @intCast(n.args.len),
});
try self.emit(.op_ctor_call, ctor_idx);
diff --git a/src/eval/backend/vm/opcode.zig b/src/eval/backend/vm/opcode.zig
index 88d7337d1..a1526d73a 100644
--- a/src/eval/backend/vm/opcode.zig
+++ b/src/eval/backend/vm/opcode.zig
@@ -682,9 +682,12 @@ pub const LibspecEntry = struct {
/// here (not packed into the operand) so it carries full width — the old
/// `(name_idx << 8) | arg_count` packing truncated name_idx to 8 bits and
/// corrupted ctor calls in chunks with > 255 constants. `type_name` is
-/// analyzer-arena-owned, chunk lifetime.
+/// analyzer-arena-owned, chunk lifetime. `definer_ns` is the namespace the
+/// form was analyzed in (empty when none), which qualifies a bare user-type
+/// name at run time, where the current namespace is the caller's.
pub const CtorEntry = struct {
type_name: []const u8,
+ definer_ns: []const u8 = "",
arg_count: u16,
};
diff --git a/src/eval/bytecode/serialize.zig b/src/eval/bytecode/serialize.zig
index 336c1c476..7e607f8da 100644
--- a/src/eval/bytecode/serialize.zig
+++ b/src/eval/bytecode/serialize.zig
@@ -29,7 +29,8 @@
//! + has_only:u8 + (only_count:u32
//! + each: only_len:u32 + bytes)?
//! [...] ctor_sites_count (u32 LE)
-//! for each entry: type_name_len:u32 + bytes + arg_count:u16
+//! for each entry: type_name_len:u32 + bytes
+//! + definer_ns_len:u32 + bytes + arg_count:u16
//! [...] import_sites_count (u32 LE)
//! for each entry: simple_len:u32 + bytes + fqcn_len:u32 + bytes
//!
@@ -78,6 +79,10 @@ const tree_walk = @import("../backend/tree_walk.zig");
const Function = tree_walk.Function;
pub const MAGIC: [4]u8 = .{ 'C', 'L', 'J', 'W' };
+// v13: a ctor site carries `definer_ns`, the namespace its form was analyzed
+// in, so a bare user-type name is qualified lexically instead of by the
+// caller's namespace (ADR-0198, ADR-0201). A v12 ctor site has no such field,
+// so v12 is rejected.
// v12 (D-346): the op_*_extend opcodes build a large literal in bounded steps,
// and a folded constant literal may be a hash_map (new wire tag) or a set past
// the array-map threshold. A v11 decoder cannot read either, so v11 is rejected.
@@ -89,7 +94,7 @@ pub const MAGIC: [4]u8 = .{ 'C', 'L', 'J', 'W' };
// v10: def operands capture Var constants instead of caller-relative names.
// v9 added op_var_meta; v8 unified host-class FQCNs; v7 compacted WireInstr
// and interned the name pool; v6 merged rt into clojure.core.
-pub const VERSION: u16 = 12;
+pub const VERSION: u16 = 13;
pub const SerializeError = error{
OutOfMemory,
@@ -852,6 +857,7 @@ fn writeChunkBody(ctx: *WriteCtx, w: *std.Io.Writer, chunk: BytecodeChunk) Seria
try writeU32(w, @intCast(chunk.ctor_sites.len));
for (chunk.ctor_sites) |ct| {
try writeLenPrefixed(w, ct.type_name);
+ try writeLenPrefixed(w, ct.definer_ns);
try writeU16(w, ct.arg_count);
}
try writeU32(w, @intCast(chunk.import_sites.len));
@@ -1039,8 +1045,10 @@ fn readChunkBody(ctx: *ReadCtx, r: *ByteReader) DeserializeError!BytecodeChunk {
while (i < ctor_count) : (i += 1) {
const tn_bytes = try r.readLenPrefixed();
const tn_dup = try allocator.dupe(u8, tn_bytes);
+ const dn_bytes = try r.readLenPrefixed();
+ const dn_dup = try allocator.dupe(u8, dn_bytes);
const arg_count = try r.readU16();
- ctor_sites[i] = .{ .type_name = tn_dup, .arg_count = arg_count };
+ ctor_sites[i] = .{ .type_name = tn_dup, .definer_ns = dn_dup, .arg_count = arg_count };
}
const import_count = try r.readU32();
@@ -1132,7 +1140,10 @@ pub fn freeChunk(allocator: std.mem.Allocator, chunk: BytecodeChunk) void {
if (nf.doc) |d| allocator.free(d);
}
allocator.free(chunk.ns_filters);
- for (chunk.ctor_sites) |ct| allocator.free(ct.type_name);
+ for (chunk.ctor_sites) |ct| {
+ allocator.free(ct.type_name);
+ allocator.free(ct.definer_ns);
+ }
allocator.free(chunk.ctor_sites);
for (chunk.import_sites) |ip| {
allocator.free(ip.simple);
@@ -2077,7 +2088,7 @@ test "chunk completeness gate: every side-table + entry field round-trips (D-365
}
inline for (std.meta.fields(CtorEntry)) |f| {
const classified: bool = switch (@field(std.meta.FieldEnum(CtorEntry), f.name)) {
- .type_name, .arg_count => true,
+ .type_name, .definer_ns, .arg_count => true,
};
try testing.expect(classified);
}
@@ -2119,6 +2130,7 @@ test "chunk completeness gate: every side-table + entry field round-trips (D-365
};
const ctor_sites = [_]CtorEntry{
.{ .type_name = "java.io.File", .arg_count = 1 },
+ .{ .type_name = "Point", .definer_ns = "app.core", .arg_count = 2 },
};
const import_sites = [_]ImportPair{
.{ .simple = "File", .fqcn = "java.io.File" },
@@ -2179,9 +2191,13 @@ test "chunk completeness gate: every side-table + entry field round-trips (D-365
try testing.expectEqual(@as(u32, 7), round.ns_filters[0].attr_const);
// ctor_sites
- try testing.expectEqual(@as(usize, 1), round.ctor_sites.len);
+ try testing.expectEqual(@as(usize, 2), round.ctor_sites.len);
try testing.expectEqualStrings("java.io.File", round.ctor_sites[0].type_name);
+ try testing.expectEqualStrings("", round.ctor_sites[0].definer_ns);
try testing.expectEqual(@as(u16, 1), round.ctor_sites[0].arg_count);
+ try testing.expectEqualStrings("Point", round.ctor_sites[1].type_name);
+ try testing.expectEqualStrings("app.core", round.ctor_sites[1].definer_ns);
+ try testing.expectEqual(@as(u16, 2), round.ctor_sites[1].arg_count);
// import_sites
try testing.expectEqual(@as(usize, 1), round.import_sites.len);
diff --git a/src/eval/node.zig b/src/eval/node.zig
index a3c8d5c0a..b9a63bd61 100644
--- a/src/eval/node.zig
+++ b/src/eval/node.zig
@@ -425,6 +425,12 @@ pub const InteropCallNode = struct {
/// `.constructor`: type name string for eval-time `resolveJavaSurface`
/// lookup. Other kinds: empty string.
type_name: []const u8 = "",
+ /// `.constructor` with a dot-free `type_name`: the namespace the form was
+ /// ANALYZED in. A user type is keyed `.` (ADR-0198) and clj
+ /// binds a class name lexically, so the eval-time lookup tries
+ /// `.` before anything the caller's namespace
+ /// could answer. Empty when there is none.
+ definer_ns: []const u8 = "",
/// `.static_method` / `.instance_member`: method or field name.
/// `.constructor`: ignored.
name: []const u8 = "",
diff --git a/test/clj/suites/type_redefine_test.clj b/test/clj/suites/type_redefine_test.clj
index 8fd4b771c..28b75b858 100644
--- a/test/clj/suites/type_redefine_test.clj
+++ b/test/clj/suites/type_redefine_test.clj
@@ -19,6 +19,26 @@
(ns suites.type-redefine-test
(:require [clojure.test :refer [deftest is]]))
+;; --- two namespaces defining the SAME simple name must retain distinct
+;; descriptors. Unlike re-evaluation, this is not a redefine: the registry
+;; key must include the namespace. The CLI reproduction is the
+;; `ns-collision-then-narrowing-redefine` row at the end of this file.
+(ns suites.tduafa)
+(defrecord Point [x y])
+(def a (->Point 1 2))
+
+(ns suites.tduafb)
+(defrecord Point [x y])
+(def b (->Point 3 4))
+
+(ns suites.type-redefine-test
+ (:require [clojure.test :refer [deftest is]]))
+
+(deftest equal-simple-names-in-distinct-namespaces
+ (is (= "#suites.tduafa.Point{:x 1, :y 2}" (pr-str suites.tduafa/a)))
+ (is (= "#suites.tduafb.Point{:x 3, :y 4}" (pr-str suites.tduafb/b)))
+ (is (not (= (class suites.tduafa/a) (class suites.tduafb/b)))))
+
;; --- same name, same shape, redefined in one namespace (the REPL re-eval
;; and `require :reload` path) ---
@@ -115,3 +135,116 @@
(dotimes [_ 200] (vec (range 64)))
(is (some? captured-class))
(is (string? (pr-str captured-class))))
+
+;; --- scenarios migrated from the former test/e2e/type_descriptor_redefinition.sh.
+;; Every expected value is the JVM's (clojure -M). The printed forms use
+;; hyphen-free namespaces because clj munges `-` to `_` in a record class name
+;; and cljw does not yet (ADR-0201 Consequences). ---
+
+;; Same simple name in two namespaces, then a narrowing redefine in the first.
+(ns suites.tdra)
+(defrecord Point [x y])
+(def a (->Point 1 2))
+
+(ns suites.tdrb)
+(defrecord Point [x y])
+(def b (->Point 3 4))
+
+(ns suites.tdra)
+(def a-repr-after-namesake (pr-str a))
+(defrecord Point [x])
+(def a-after (->Point 5))
+(deftype Box [v])
+(def old-box (Box. 7))
+(deftype Box [v])
+
+;; defrecord R [a] -> R [a b], each vintage extended separately; a deftype
+;; with an inline impl redefined without it; then registration churn under GC.
+(ns suites.tdrc)
+(defprotocol Area (area [s]))
+(defrecord R [a])
+(extend-protocol Area R (area [s] (:a s)))
+(def r (->R 1))
+(def rc (class r))
+(defrecord R [a b])
+(extend-protocol Area R (area [s] (+ (:a s) (:b s))))
+(def r-new (->R 2 3))
+(def r-nil (->R 1 nil))
+
+(deftype T [x] Area (area [_] x))
+(def t (T. 5))
+(deftype T [x y])
+
+(def last-area (atom nil))
+(dotimes [i 3000]
+ (eval '(defrecord Churn [v]))
+ (reset! last-area (area (reify Area (area [_] i)))))
+(def cs (vec (for [i (range 50)]
+ (do (eval '(defrecord Churn [v w]))
+ ((ns-resolve 'suites.tdrc '->Churn) i i)))))
+(System/gc)
+
+;; A constructor form binds the class of the namespace it was WRITTEN in
+;; ([CLJW-CTOR-ANALYZE-TIME]). The factories, a `(Point. x y)` in a later fn
+;; and one inside the record's own method body all build suites.tdrd.Point when
+;; called from another namespace after suites.tdre registered a namesake with a
+;; different shape.
+(ns suites.tdrd)
+(defprotocol Grow (grow [p]))
+(defrecord Point [x y] Grow (grow [_] (Point. (inc x) y)))
+(deftype Box [v])
+(defn mk [] (Point. 1 2))
+
+(ns suites.tdre)
+(defrecord Point [q])
+(deftype Box [v w])
+
+(ns suites.type-redefine-test
+ (:require [clojure.test :refer [deftest is]]))
+
+(deftest ns-collision-then-narrowing-redefine
+ (is (= "#suites.tdra.Point{:x 1, :y 2}" (pr-str suites.tdra/a)))
+ (is (= "#suites.tdra.Point{:x 1, :y 2}" suites.tdra/a-repr-after-namesake))
+ (is (= "#suites.tdrb.Point{:x 3, :y 4}" (pr-str suites.tdrb/b)))
+ (is (= "#suites.tdra.Point{:x 5}" (pr-str suites.tdra/a-after)))
+ (is (= 7 (.-v suites.tdra/old-box))))
+
+(deftest widening-redefine-keeps-the-old-vintage-whole
+ (let [r suites.tdrc/r rc suites.tdrc/rc]
+ (is (= "#suites.tdrc.R{:a 1}" (pr-str r)))
+ (is (= 1 (:a r)))
+ (is (= 1 (.-a r)))
+ (is (true? (instance? rc r)))
+ (is (true? (= rc (class r))))
+ (is (false? (= (class r) suites.tdrc.R)))
+ (is (true? (= r r)))
+ (is (false? (= r suites.tdrc/r-nil)))
+ (is (= (hash r) (hash r)))
+ (is (true? (map? r)))
+ (is (= 1 (suites.tdrc/area r)))
+ (is (= 5 (suites.tdrc/area suites.tdrc/r-new)))))
+
+(deftest deftype-redefine-keeps-the-old-method-table
+ (let [t suites.tdrc/t]
+ (is (= 5 (.-x t)))
+ (is (= 5 (suites.tdrc/area t)))
+ (is (= t t))
+ (is (integer? (hash t)))))
+
+(deftest redefine-churn-under-gc
+ (let [cs suites.tdrc/cs]
+ (is (= 1225 (reduce + (map :v cs))))
+ (is (= "#suites.tdrc.Churn{:v 0, :w 0}" (pr-str (first cs))))
+ (is (= "#suites.tdrc.Churn{:v 49, :w 49}" (pr-str (last cs))))
+ (is (= 2999 @suites.tdrc/last-area))))
+
+(deftest ctor-binds-its-defining-namespace
+ (is (= "#suites.tdrd.Point{:x 1, :y 2}" (pr-str (suites.tdrd/->Point 1 2))))
+ (is (= "#suites.tdrd.Point{:x 1, :y 2}" (pr-str (suites.tdrd/mk))))
+ (is (= "#suites.tdrd.Point{:x 2, :y 2}" (pr-str (suites.tdrd/grow (suites.tdrd/->Point 1 2)))))
+ (is (= "#suites.tdre.Point{:q 9}" (pr-str (suites.tdre/->Point 9))))
+ (is (= "#suites.tdrd.Point{:x 1, :y 2}" (pr-str (suites.tdrd/map->Point {:x 1 :y 2}))))
+ (is (true? (instance? suites.tdrd.Point (suites.tdrd/mk))))
+ (is (false? (instance? suites.tdre.Point (suites.tdrd/mk))))
+ (is (= 7 (.-v (suites.tdrd/->Box 7))))
+ (is (= 8 (.-w (suites.tdre/->Box 7 8)))))
diff --git a/test/clj/suites/user_class_identity_test.clj b/test/clj/suites/user_class_identity_test.clj
new file mode 100644
index 000000000..fb4b61a21
--- /dev/null
+++ b/test/clj/suites/user_class_identity_test.clj
@@ -0,0 +1,169 @@
+;; ADR-0201: a user-defined class (deftype / defrecord / reify) IS its
+;; TypeDescriptor, so membership in it is decided by descriptor identity,
+;; never by comparing simple-name strings.
+;;
+;; The simple-name oracle survived ADR-0198 (which gave every other question,
+;; `(class x)`, `=` on classes, printing, the registry key, a per-descriptor
+;; answer) and so answered `instance?` and `isa?` differently from `=`:
+;; after `(defrecord R [a b])` replaced `(defrecord R [a])`, an OLD instance
+;; was still `(instance? R old)`, a record from another namespace with the same
+;; simple name was an instance of this one, and a deftype named `Sorted` was
+;; `sorted?`. Every expected value below is the JVM's (`clojure -M`).
+(ns suites.user-class-identity-test
+ (:require [clojure.test :refer [deftest is testing]]))
+
+(defprotocol Area (area [s]))
+
+;; --- same-namespace redefinition: the new class is a DIFFERENT class ---
+
+(defrecord R [a])
+(def ^:private old-r (->R 1))
+(def ^:private old-r-class (class old-r))
+(defrecord R [a b])
+(def ^:private new-r (->R 1 2))
+
+(deftest instance-of-redefined-record
+ (is (false? (instance? R old-r)))
+ (is (true? (instance? old-r-class old-r)))
+ (is (true? (instance? R new-r)))
+ (is (false? (instance? old-r-class new-r))))
+
+(deftest class-isInstance-agrees-with-instance?
+ (is (false? (.isInstance R old-r)))
+ (is (true? (.isInstance old-r-class old-r))))
+
+(deftest isa?-between-record-vintages
+ (is (false? (isa? old-r-class R)))
+ (is (false? (isa? R old-r-class)))
+ (is (true? (isa? old-r-class old-r-class)))
+ (is (true? (isa? R R)))
+ (testing "a user class still sits under Object"
+ (is (true? (isa? old-r-class Object)))
+ (is (true? (instance? Object old-r)))))
+
+(defmulti ^:private by-class class)
+(defmethod by-class :default [_] :default)
+(defmethod by-class R [_] :new-r)
+
+(defmulti ^:private by-old-class class)
+(defmethod by-old-class old-r-class [_] :old-r)
+(defmethod by-old-class :default [_] :default)
+
+(deftest multimethod-dispatch-on-class-vintage
+ (testing "a method keyed by the NEW class does not catch an old instance"
+ (is (= :default (by-class old-r)))
+ (is (= :new-r (by-class new-r))))
+ (testing "a method keyed by the OLD class catches only the old instance"
+ (is (= :old-r (by-old-class old-r)))
+ (is (= :default (by-old-class new-r)))))
+
+(deftype T [x])
+(def ^:private old-t (T. 1))
+(def ^:private old-t-class (class old-t))
+(deftype T [x])
+
+(deftest instance-of-redefined-deftype
+ (is (false? (instance? T old-t)))
+ (is (true? (instance? old-t-class old-t)))
+ (is (true? (instance? T (T. 2))))
+ (is (false? (= old-t-class T))))
+
+;; --- reify: its class value is a class like any other ---
+
+(defn- mk-area [i] (reify Area (area [_] i)))
+
+(deftest reify-instance-of-its-own-class
+ (let [a (mk-area 1)]
+ (is (true? (instance? (class a) a)))
+ (is (true? (.isInstance (class a) a)))
+ (is (true? (isa? (class a) (class a))))
+ (is (true? (instance? Object a)))
+ (is (= 1 (area a)))))
+
+;; --- a user type named like a host interface is not that interface ---
+
+(deftype Sorted [])
+
+(deftest user-type-named-like-an-interface
+ (is (false? (sorted? (Sorted.))))
+ (is (true? (instance? Sorted (Sorted.))))
+ (is (false? (instance? clojure.lang.Sorted (Sorted.)))))
+
+;; --- class-level isa? of a user class against a host class: what the user
+;; class DECLARES decides, never its simple name. These names are each a host
+;; class's simple name, which the name oracle used to match. ---
+
+(deftype BigInt [])
+(deftype Keyword [])
+(deftype PersistentVector [])
+(deftype LazySeq [])
+(deftype ExceptionInfo [])
+(deftype Ratio [])
+(deftype Deref [] clojure.lang.IDeref (deref [_] :derefed))
+
+(deftest user-class-named-like-a-host-class-is-not-a-subclass-of-it
+ (is (false? (isa? BigInt Number)))
+ (is (false? (isa? Ratio Number)))
+ (is (false? (isa? Keyword clojure.lang.IFn)))
+ (is (false? (isa? PersistentVector clojure.lang.IFn)))
+ (is (false? (isa? PersistentVector clojure.lang.IPersistentVector)))
+ (is (false? (isa? LazySeq clojure.lang.Seqable)))
+ (is (false? (isa? ExceptionInfo Exception)))
+ (is (false? (isa? Sorted clojure.lang.Sorted)))
+ (is (false? (number? (BigInt.))))
+ (is (false? (ifn? (Keyword.))))
+ (testing "Object and the declared interfaces still answer"
+ (is (true? (isa? BigInt Object)))
+ (is (true? (isa? Deref clojure.lang.IDeref)))
+ (is (= :derefed @(Deref.)))))
+
+(defmulti ^:private by-host-class class)
+(defmethod by-host-class Number [_] :number)
+(defmethod by-host-class clojure.lang.IFn [_] :ifn)
+(defmethod by-host-class :default [_] :default)
+
+(deftest multimethod-dispatch-on-a-user-class-named-like-a-host-class
+ (is (= :default (by-host-class (BigInt.))))
+ (is (= :default (by-host-class (Keyword.))))
+ (is (= :number (by-host-class 1)))
+ (is (= :ifn (by-host-class :k))))
+
+;; --- two namespaces, one simple name: two classes ---
+
+(ns suites.ucia)
+(defrecord Point [x y])
+(def a (->Point 1 2))
+
+(ns suites.ucib)
+(defrecord Point [q])
+(def b (->Point 9))
+
+(ns suites.user-class-identity-test)
+
+(deftest same-simple-name-in-two-namespaces
+ (is (false? (instance? suites.ucia.Point suites.ucib/b)))
+ (is (true? (instance? suites.ucib.Point suites.ucib/b)))
+ (is (true? (instance? suites.ucia.Point suites.ucia/a)))
+ (is (false? (= (class suites.ucia/a) (class suites.ucib/b))))
+ (is (false? (isa? (class suites.ucib/b) suites.ucia.Point)))
+ ;; a factory builds ITS namespace's class, also when called from another
+ ;; namespace after a namesake was registered later
+ (is (= [5 6] ((juxt :x :y) (suites.ucia/->Point 5 6))))
+ (is (true? (instance? suites.ucia.Point (suites.ucia/->Point 5 6))))
+ (is (= {:x 7 :y 8} (into {} (suites.ucia/map->Point {:x 7 :y 8}))))
+ (is (true? (instance? suites.ucib.Point (suites.ucib/->Point 1)))))
+
+;; --- an INSTANCE dispatch value is not isa? its class's name, nor a protocol
+;; its type declares: clj relates an instance only by equality or derive ---
+
+(defmulti ^:private by-identity identity)
+(defmethod by-identity 'R [_] :sym)
+(defmethod by-identity :R [_] :kw)
+(defmethod by-identity 'Area [_] :protocol)
+(defmethod by-identity :default [_] :default)
+
+(deftest instance-dispatch-value-is-not-its-class-name
+ (is (= :default (by-identity new-r)))
+ (is (= :default (by-identity (mk-area 1))))
+ (is (false? (isa? new-r 'R)))
+ (is (false? (isa? new-r :R))))
From f15ad170afe03304a61c4ef6c390b66f6d558a4d Mon Sep 17 00:00:00 2001
From: Pedro Gomes Branquinho
Date: Wed, 14 Oct 2026 20:55:41 -0300
Subject: [PATCH 06/17] docs(debt): discharge D-587 (ADR-0198, ADR-0201)
Redefining a deftype/defrecord no longer frees a descriptor live
instances still use, namesakes in different namespaces are different
classes, and a bare constructor binds its defining namespace. Pinned by
type_redefine_test.clj, user_class_identity_test.clj and the
type_redefinition corpus. rt.types_by_simple stays for the bare-name paths
that are not ctor sites (card 20260909231443-1ba4303c).
---
.dev/debt.yaml | 55 ++++++++++----------------------------------------
1 file changed, 11 insertions(+), 44 deletions(-)
diff --git a/.dev/debt.yaml b/.dev/debt.yaml
index 547c36fbf..17decb763 100644
--- a/.dev/debt.yaml
+++ b/.dev/debt.yaml
@@ -24,50 +24,6 @@
# md-table-align no longer pads it to ~800 KB of whitespace.
active:
- - id: "D-587"
- status: |-
- open (2026-09-09). USE-AFTER-FREE, user-reachable panic. Re-registering a
- deftype/defrecord name frees a TypeDescriptor that live instances still
- point at. `type_descriptor.zig registerType` keys `rt.types` by the SIMPLE
- name, and on re-registration does `fetchRemove(name)` then frees the old
- descriptor (each field_layout entry name, the layout slice, fqcn,
- defining_ns, method_table entries, protocol_impls, then
- `gpa.destroy(old.value)`). Every constructed instance holds
- `inst.descriptor` as a raw pointer to it, so `print.zig
- printTypedInstance` then reads `field_layout` out of freed memory: the
- symptom is `thread panic: integer overflow` (a garbage field index or
- layout length), the defect is the free.
- Repro (ReleaseSafe v1.14.3): `(ns aaa) (defrecord Point [x y]) (def a
- (->Point 1 2)) (ns bbb) (defrecord Point [x y]) (def b (->Point 1 2))
- (prn (pr-str aaa/a))` panics; printing `bbb/b` is fine. Variants: two ns
- with DIFFERENT names is fine; two ns SAME name panics; SAME ns redefining
- the same name panics (this is the ordinary REPL / `require :reload` path
- `dev/user.clj` depends on); same name with a different field count panics;
- the deftype form raises `WriteFailed` instead, same root.
- TWO separable defects: (1) the registry is keyed by simple name, so
- `aaa/Point` and `bbb/Point` collide with no redefinition at all (clj gives
- each ns its own class); (2) re-registration frees a descriptor live
- instances reference.
- ADR-0019 forbids a panic on a user-reachable path, and this one is reached
- by re-evaluating a defrecord form. It also caps the native-suite migration:
- `test/clj/suites/` shares ONE image, so no two suites may define the same
- type name (worked around 2026-09-09 by renaming defrecord_test's record to
- DPoint; 45 type definitions across 103 suites, no other duplicate today).
- Found while migrating phase7_defrecord.sh to a native suite.
- Card `[CLJW-TD-UAF]` 20260909211032-6471bc49.
- category: "correctness"
- quality_floor: "correctness: a user-reachable panic from an ordinary redefine"
- barrier: |-
- Needs an ADR: the free is deliberate (comments cite row 7.7 cycle 5 and
- D-190 / ADR-0068) and exists to avoid leaking on redefine, so removing it
- trades the leak back. Candidate shapes: key `rt.types` by the qualified
- name when `defining_ns` is present and RETIRE rather than free a replaced
- descriptor (a leak bounded by redefinition count, which is what clj does
- with the old class), or make descriptors GC-managed so a descriptor dies
- only when no instance references it. Devil's-advocate fork is mandatory at
- this depth. Not blocked on anything external: the repro is one file and the
- owning function is ~70 lines.
- last_reviewed: "2026-09-09"
- id: "D-586"
status: |-
open (2026-09-05, ADR-0195 trigger). UPSTREAM: zwasm/D-584. zwasm's JIT
@@ -7583,6 +7539,17 @@ discharged:
refs: |-
src/runtime/collection/literal.zig (the one literal construction); src/eval/analyzer/analyzer.zig (the constant fold); src/eval/backend/vm/compiler.zig and vm.zig (the extend opcodes); docs/spec/formats/chunk-v12.edn (bytecode format v12).
discharged_at: "2026-10-02"
+ - id: "D-587"
+ status: |-
+ DISCHARGED 2026-10-14 by ADR-0198 plus ADR-0201. Both defects the row named are fixed. (1) rt.types is keyed by ., so aaa.Point and bbb.Point are two classes (ADR-0198), and a bare constructor binds the class of the namespace its form was analyzed in ([CLJW-CTOR-ANALYZE-TIME]: a ctor site carries definer_ns, bytecode v13), so `aaa/->Point` builds aaa.Point even after bbb registered a namesake. (2) A displaced descriptor is retired, never freed, until Runtime.deinit (ADR-0198; ADR-0201 Part B makes that the one lifetime rule for every user descriptor). ADR-0201 also made membership descriptor identity: instance?, .isInstance, isa? and multimethod dispatch agree with (= (class x) C) for a redefined record, a namesake in another namespace and a reify class. The original repro prints both records and the narrowed redefine on both backends.
+ Was: open (2026-09-09), USE-AFTER-FREE, user-reachable panic. Re-registering a deftype/defrecord name freed a TypeDescriptor that live instances still pointed at, and the registry was keyed by the simple name, so two namespaces defining Point collided with no redefinition at all. Symptom `thread panic: integer overflow` from printTypedInstance, or `WriteFailed` for a deftype.
+ category: "correctness"
+ barrier: |-
+ none, discharged. Pinned by test/clj/suites/type_redefine_test.clj (the six scenarios of the former type_descriptor_redefinition e2e, incl. 3000 redefinitions interleaved with reify under a forced collection and the defining-namespace ctor binding), test/clj/suites/user_class_identity_test.clj, the type_redefinition diff corpus, and the class_name.zig / analyzer.zig unit tests. Still open and separate: rt.types_by_simple still exists for the bare-name paths that are not ctor sites (card 20260909231443-1ba4303c), and D-574 (reify mints a descriptor per evaluation).
+ last_reviewed: "2026-10-14"
+ refs: |-
+ ADR-0198, ADR-0201; src/runtime/type_descriptor.zig (registerType retires), src/runtime/class_name.zig (isInstanceOfClass, classIsaOf, descriptorIsa), src/eval/analyzer/special_forms.zig (constructInstance definer_ns lookup), docs/spec/formats/chunk-v13.edn.
+ discharged_at: "2026-10-14"
conventions: |-
- Category vocabulary is a closed set (guarded by scripts/check_debt_category.sh; synonyms were normalized 2026-10-08: structural->structure, tests/test->testing, numerics->numeric, performance->perf). Allowed: app backend bench bootstrap build clj-parity concurrency correctness debt-hygiene demo discipline distribution docs docs-onboarding ecosystem error-system eval eval-semantics feature gc governance host-surface infra interop isolation license macro meta-reorg missing-ns namespace numeric perf polymorphism process project-flow reader refactor regex robustness runtime scope security structure testing tooling wasm. A new category is added here first, in the same commit as the row that needs it.
- 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).
From 39f70b9ebeeea34d806338bd913051c8d4bb75cb Mon Sep 17 00:00:00 2001
From: Pedro Gomes Branquinho
Date: Wed, 14 Oct 2026 22:14:57 -0300
Subject: [PATCH 07/17] docs(changelog): record round 7 (type redefinition and
identity, VM error locations) under [Unreleased]
Smell-audited: 0: release notes only, no source change
---
CHANGELOG.md | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d5bc55917..2fa657829 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,25 @@ first stable `1.0.0` tag; pre-1.0 `alpha` / `rc` tags may still change surfaces.
## [Unreleased]
+### Fixed
+
+- **Redefining a deftype or defrecord is safe, and a user class is matched by
+ identity, not by name.** Re-registering a type no longer frees a descriptor
+ live instances still use (the `thread panic: integer overflow` on printing
+ an old instance is gone). `aaa.Point` and `bbb.Point` are two classes, and
+ `instance?`, `.isInstance`, class `isa?` and multimethod dispatch agree with
+ `(= (class x) C)` for a redefined record, a namesake in another namespace
+ and a reify class. A bare `(Point. x y)` or `->Point` builds the class of the
+ namespace the form was written in, even after another namespace registered a
+ namesake of a different shape. Compiled chunks move to format v13.
+
+- **The VM reports errors at their form.** `set!` on an unbound var, an
+ unresolved or unsupported `.member` call, a static method call, a
+ constructor arity failure, `import` and `require`/`refer` failures raised on
+ the VM with no source position (`0:0`) while TreeWalk reported line and
+ column. Both backends now report the same position, and the dual-backend
+ test oracle checks it for every failing case.
+
## [1.15.0] - 2026-10-13
### Added
From 5dadcd16f803de119f71b944e03b01c5b4be3114 Mon Sep 17 00:00:00 2001
From: Pedro Gomes Branquinho
Date: Wed, 21 Oct 2026 13:44:27 -0300
Subject: [PATCH 08/17] feat(wasm): wasm/exports lists a core module's exports
(ADR-0203)
(wasm/exports h) returns a vector of maps in export-section order. A
function answers {:name :kind :func :params [...] :results [...]}, any
other export {:name :kind :memory|:global|:table}. Signatures come from
Loaded.exportSig, the probe wasm/call marshals through, so what is listed
is callable at exactly the listed arity.
The export list is decoded at load, while the module bytes are alive:
zwasm's section view borrows them, and wasm/load frees them right after.
Tests: exports.zig unit tests (interp + jit, bytes freed before use) and
test/e2e/phase16_wasm_exports.sh (add.wasm, the C kernel, a bad handle).
Smell-audited: 1: new builtin in its own file, one registration line, no change to existing call paths
---
README.md | 3 +
src/runtime/cljw/wasm/engine.zig | 9 ++
src/runtime/cljw/wasm/exports.zig | 155 ++++++++++++++++++++++++++++++
src/runtime/cljw/wasm/surface.zig | 3 +-
test/e2e/phase16_wasm_exports.sh | 65 +++++++++++++
test/run_all.sh | 1 +
6 files changed, 235 insertions(+), 1 deletion(-)
create mode 100644 src/runtime/cljw/wasm/exports.zig
create mode 100755 test/e2e/phase16_wasm_exports.sh
diff --git a/README.md b/README.md
index 144c7d29e..cc0a7478d 100644
--- a/README.md
+++ b/README.md
@@ -191,6 +191,9 @@ cljw build script.clj -o app # one self-contained native binary
;; A core module (docs/examples/polyglot/c/kernel.c): numbers in, numbers
;; out, arrays in a buffer the guest owns and hands out by address.
(def m (wasm/load "docs/examples/polyglot/c/kernel.wasm" {:fuel 1000000 :max-memory-pages 16}))
+(wasm/exports m) ;=> [{:name "memory", :kind :memory}
+ ; {:name "buf_addr", :kind :func, :params [], :results [:i32]}
+ ; ... {:name "sum_f64", :kind :func, :params [:i32 :i32], :results [:f64]} ...]
(def buf (wasm/call m "buf_addr"))
(wasm/mem-write! m :f64 buf [1.0 2.0 3.0 4.0])
(wasm/call m "sum_f64" buf 4) ;=> 10.0
diff --git a/src/runtime/cljw/wasm/engine.zig b/src/runtime/cljw/wasm/engine.zig
index 84cc0e593..29b52429e 100644
--- a/src/runtime/cljw/wasm/engine.zig
+++ b/src/runtime/cljw/wasm/engine.zig
@@ -25,6 +25,8 @@ pub const Value = zwasm.Value;
pub const FuncType = zwasm.ir.zir.FuncType;
pub const ValType = zwasm.ir.zir.ValType;
pub const Memory = zwasm.Memory;
+/// Owned export-descriptor list (zwasm `Module.exports`), ADR-0203.
+pub const ModuleExports = @typeInfo(@typeInfo(@TypeOf(zwasm.Module.exports)).@"fn".return_type.?).error_union.payload;
/// Per-axis runtime budget for a loaded module (re-exported from zwasm's
/// `Module.Budget`, ADR-0179). `.unmetered` lifts the cap (trusted modules
@@ -80,6 +82,10 @@ pub const Loaded = struct {
/// Keys are owned copies; values are slices into the instance's own
/// signature storage, valid for the instance's lifetime.
sig_cache: std.StringHashMapUnmanaged(FuncType),
+ /// Export descriptors (name + kind, export order) decoded at `load`, while
+ /// the module bytes are still alive: zwasm's native section view borrows
+ /// them, so a later `Module.exports` would decode freed memory (ADR-0203).
+ export_list: ModuleExports,
/// Runtime signature of an exported function (`null` if absent / not a
/// func). Drives the marshal: caller sizes its `[]Value` buffers from
@@ -120,6 +126,7 @@ pub const Loaded = struct {
var keys = self.sig_cache.keyIterator();
while (keys.next()) |k| self.alloc.free(k.*);
self.sig_cache.deinit(self.alloc);
+ self.export_list.deinit();
self.instance.deinit();
self.module.deinit();
self.engine.deinit();
@@ -295,6 +302,8 @@ pub fn load(alloc: std.mem.Allocator, bytes: []const u8, opts: LoadOpts) !*Loade
if (opts.fuel) |b| inst_opts.fuel = b;
if (opts.max_memory_pages) |b| inst_opts.max_memory_pages = b;
inst_opts.engine = opts.engine;
+ self.export_list = try self.module.exports(alloc);
+ errdefer self.export_list.deinit();
self.instance = try self.module.instantiate(inst_opts);
self.engine_kind = opts.engine;
self.alloc = alloc;
diff --git a/src/runtime/cljw/wasm/exports.zig b/src/runtime/cljw/wasm/exports.zig
new file mode 100644
index 000000000..975eb3c42
--- /dev/null
+++ b/src/runtime/cljw/wasm/exports.zig
@@ -0,0 +1,155 @@
+// SPDX-License-Identifier: EPL-2.0
+//! `(wasm/exports h)` — list a core module's exports in export-section order
+//! (ADR-0203 decision 1). A function answers
+//! `{:name "add" :kind :func :params [:i32 :i32] :results [:i32]}`; a memory,
+//! global or table answers `{:name "memory" :kind :memory}`. The function
+//! signature comes from `Loaded.exportSig`, the same runtime probe `wasm/call`
+//! marshals through, so nothing here is declared by hand and a listed function
+//! is callable at exactly the listed arity. Compiled ONLY under `-Dwasm`.
+//!
+//! Backend: impl-only
+//! Impl deps: none
+//! Clojure peer: wasm/exports
+const std = @import("std");
+const zwasm = @import("zwasm");
+const engine = @import("engine.zig");
+const wasm_handle = @import("wasm_handle.zig");
+const Runtime = @import("../../runtime.zig").Runtime;
+const Env = @import("../../env.zig").Env;
+const Value = @import("../../value/value.zig").Value;
+const error_catalog = @import("../../error/catalog.zig");
+const SourceLocation = @import("../../error/info.zig").SourceLocation;
+const string_mod = @import("../../collection/string.zig");
+const vector_mod = @import("../../collection/vector.zig");
+const map_mod = @import("../../collection/map.zig");
+const keyword_mod = @import("../../keyword.zig");
+
+/// `(wasm/exports handle)` — a vector of export maps, in export order.
+pub fn wasmExportsFn(rt: *Runtime, env: *Env, args: []const Value, loc: SourceLocation) anyerror!Value {
+ _ = env;
+ try error_catalog.checkArity("wasm/exports", args, 1, loc);
+ if (!wasm_handle.isHandle(args[0]))
+ return error_catalog.raise(.wasm_handle_invalid, loc, .{ .fn_name = "wasm/exports" });
+ const loaded = wasm_handle.unwrap(args[0]);
+
+ const name_kw = try keyword_mod.intern(rt, null, "name");
+ const kind_kw = try keyword_mod.intern(rt, null, "kind");
+ const params_kw = try keyword_mod.intern(rt, null, "params");
+ const results_kw = try keyword_mod.intern(rt, null, "results");
+
+ var out = vector_mod.empty();
+ for (loaded.export_list.items) |item| {
+ var m = map_mod.empty();
+ m = try map_mod.assoc(rt, m, name_kw, try string_mod.alloc(rt, item.name));
+ m = try map_mod.assoc(rt, m, kind_kw, try keyword_mod.intern(rt, null, kindName(item.kind)));
+ if (item.kind == .func) {
+ // A func export always has a signature; a miss would mean the
+ // instance and the export section disagree, so the map simply
+ // carries no signature rather than inventing one.
+ if (loaded.exportSig(item.name)) |sig| {
+ m = try map_mod.assoc(rt, m, params_kw, try typeVector(rt, sig.params));
+ m = try map_mod.assoc(rt, m, results_kw, try typeVector(rt, sig.results));
+ }
+ }
+ out = try vector_mod.conj(rt, out, m);
+ }
+ return out;
+}
+
+fn typeVector(rt: *Runtime, types: []const engine.ValType) anyerror!Value {
+ var v = vector_mod.empty();
+ for (types) |t| v = try vector_mod.conj(rt, v, try keyword_mod.intern(rt, null, valTypeName(t)));
+ return v;
+}
+
+/// The `:kind` keyword name of an export.
+pub fn kindName(kind: zwasm.ExternKind) []const u8 {
+ return switch (kind) {
+ .func => "func",
+ .table => "table",
+ .memory => "memory",
+ .global => "global",
+ // The binary format has no tag export; zwasm's enum shares the import side.
+ .tag => "tag",
+ };
+}
+
+/// The keyword name of a value type: `i32 i64 f32 f64 v128`, and for a
+/// reference its abstract heap type's text-format shorthand (`funcref`,
+/// `externref`, ...). A concrete typed reference `(ref $t)` names as `ref`.
+pub fn valTypeName(t: engine.ValType) []const u8 {
+ return switch (t) {
+ .i32 => "i32",
+ .i64 => "i64",
+ .f32 => "f32",
+ .f64 => "f64",
+ .v128 => "v128",
+ .ref => |r| switch (r.heap_type) {
+ .concrete => "ref",
+ .abstract => |a| switch (a) {
+ .func => "funcref",
+ .extern_ => "externref",
+ .any => "anyref",
+ .eq => "eqref",
+ .i31 => "i31ref",
+ .struct_ => "structref",
+ .array => "arrayref",
+ .none => "nullref",
+ .noextern => "nullexternref",
+ .nofunc => "nullfuncref",
+ .exn => "exnref",
+ .noexn => "nullexnref",
+ },
+ },
+ };
+}
+
+test "valTypeName names scalars and the abstract references" {
+ const VT = engine.ValType;
+ try std.testing.expectEqualStrings("i32", valTypeName(.i32));
+ try std.testing.expectEqualStrings("f64", valTypeName(.f64));
+ try std.testing.expectEqualStrings("v128", valTypeName(.v128));
+ try std.testing.expectEqualStrings("funcref", valTypeName(VT.funcref));
+ try std.testing.expectEqualStrings("externref", valTypeName(VT.externref));
+}
+
+test "kindName names every export kind" {
+ try std.testing.expectEqualStrings("func", kindName(.func));
+ try std.testing.expectEqualStrings("memory", kindName(.memory));
+ try std.testing.expectEqualStrings("global", kindName(.global));
+ try std.testing.expectEqualStrings("table", kindName(.table));
+}
+
+// (module (func (export "add") (param i32 i32) (result i32)
+// local.get 0 local.get 1 i32.add) (memory (export "memory") 1))
+const add_mem_wasm = [_]u8{
+ 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x07, 0x01, 0x60,
+ 0x02, 0x7f, 0x7f, 0x01, 0x7f, 0x03, 0x02, 0x01, 0x00, 0x05, 0x03, 0x01,
+ 0x00, 0x01, 0x07, 0x10, 0x02, 0x03, 0x61, 0x64, 0x64, 0x00, 0x00, 0x06,
+ 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02, 0x00, 0x0a, 0x09, 0x01, 0x07,
+ 0x00, 0x20, 0x00, 0x20, 0x01, 0x6a, 0x0b,
+};
+
+test "the export list outlives the module bytes and pairs with exportSig" {
+ const alloc = std.testing.allocator;
+ inline for (.{ engine.EngineKind.interp, engine.EngineKind.jit }) |kind| {
+ // `wasm/load` frees the file bytes right after `load`; mirror that.
+ const bytes = try alloc.dupe(u8, &add_mem_wasm);
+ const loaded = try engine.load(alloc, bytes, .{ .engine = kind });
+ @memset(bytes, 0);
+ alloc.free(bytes);
+ defer {
+ loaded.deinit();
+ alloc.destroy(loaded);
+ }
+ const items = loaded.export_list.items;
+ try std.testing.expectEqual(@as(usize, 2), items.len);
+ try std.testing.expectEqualStrings("add", items[0].name);
+ try std.testing.expectEqualStrings("func", kindName(items[0].kind));
+ try std.testing.expectEqualStrings("memory", items[1].name);
+ try std.testing.expectEqualStrings("memory", kindName(items[1].kind));
+ const sig = loaded.exportSig("add").?;
+ try std.testing.expectEqual(@as(usize, 2), sig.params.len);
+ try std.testing.expectEqualStrings("i32", valTypeName(sig.results[0]));
+ }
+}
diff --git a/src/runtime/cljw/wasm/surface.zig b/src/runtime/cljw/wasm/surface.zig
index 58c1afc30..c6eb94931 100644
--- a/src/runtime/cljw/wasm/surface.zig
+++ b/src/runtime/cljw/wasm/surface.zig
@@ -12,7 +12,7 @@
//!
//! Backend: impl-only
//! Impl deps: none
-//! Clojure peer: wasm/load, wasm/call, wasm/engine, wasm/mem-size, wasm/mem-read, wasm/mem-write!
+//! Clojure peer: wasm/load, wasm/call, wasm/exports, wasm/engine, wasm/mem-size, wasm/mem-read, wasm/mem-write!
const std = @import("std");
const engine = @import("engine.zig");
const run_cache = @import("run_cache.zig");
@@ -604,6 +604,7 @@ pub fn register(env: *Env) !void {
_ = try env.intern(ns, "load", Value.initBuiltinFn(&wasmLoadFn), null);
_ = try env.intern(ns, "call", Value.initBuiltinFn(&wasmCallFn), null);
_ = try env.intern(ns, "engine", Value.initBuiltinFn(&wasmEngineFn), null);
+ _ = try env.intern(ns, "exports", Value.initBuiltinFn(&@import("exports.zig").wasmExportsFn), null);
_ = try env.intern(ns, "run", Value.initBuiltinFn(&wasmRunFn), null);
_ = try env.intern(ns, "clear-cache!", Value.initBuiltinFn(&wasmClearCacheFn), null);
// ADR-0192: the (ptr,len) half of the FFI — `wasm/call` passes the pointer,
diff --git a/test/e2e/phase16_wasm_exports.sh b/test/e2e/phase16_wasm_exports.sh
new file mode 100755
index 000000000..f01aa632d
--- /dev/null
+++ b/test/e2e/phase16_wasm_exports.sh
@@ -0,0 +1,65 @@
+#!/usr/bin/env bash
+# test/e2e/phase16_wasm_exports.sh — `(wasm/exports h)` lists a core module's
+# exports in export order with runtime-probed signatures (ADR-0203 decision 1).
+#
+# Functions answer {:name :kind :func :params [...] :results [...]}, every other
+# export {:name :kind :memory|:global|:table}. The signature comes from the same
+# probe `wasm/call` uses, so a function `wasm/exports` describes is callable with
+# exactly that arity. A non-handle argument raises the catalogued
+# wasm_handle_invalid error, catchable like every other wasm-surface error.
+#
+# Reuses the shared -Dwasm binary under CLJW_SKIP_BUILD, like the other phase16
+# wasm e2es; standalone it builds -Dwasm ReleaseSafe itself.
+set -euo pipefail
+cd "$(dirname "$0")/../.."
+BIN="zig-out/bin/cljw"
+fail() { echo "FAIL $1" >&2; exit 1; }
+
+if [ -z "${CLJW_SKIP_BUILD:-}" ] && ! zig build -Dwasm -Doptimize="${CLJW_OPT:-ReleaseSafe}" >/dev/null 2>&1; then
+ fail "zig build -Dwasm failed (zwasm dep unresolved?)"
+fi
+"$BIN" --version | grep -q wasm || fail "cljw is not wasm-enabled ($("$BIN" --version))"
+
+check() { # name expected expr
+ local got
+ got="$("$BIN" -e "$3" 2>&1)" || fail "$1: cljw exited non-zero:
+$got"
+ got="$(printf '%s\n' "$got" | sed '${/^nil$/d}')" # -e echoes the prn/println result
+ [ "$got" = "$2" ] || fail "$1: expected
+ $2
+got
+ $got"
+ echo "PASS $1"
+}
+
+# (1) The single-function module: the acceptance shape, verbatim.
+check wasm-exports-add \
+ '[{:name "add", :kind :func, :params [:i32 :i32], :results [:i32]}]' \
+ '(prn (wasm/exports (wasm/load "docs/examples/wasm/add.wasm")))'
+
+# (2) A C-compiled module: the linear memory and four functions, in export order.
+check wasm-exports-c-kernel-names \
+ '[["memory" :memory] ["buf_addr" :func] ["buf_cap" :func] ["sum_f64" :func] ["scale_f64" :func]]' \
+ '(prn (mapv (juxt :name :kind) (wasm/exports (wasm/load "docs/examples/polyglot/c/kernel.wasm"))))'
+
+check wasm-exports-c-kernel-sigs \
+ '[["buf_addr" [] [:i32]] ["buf_cap" [] [:i32]] ["sum_f64" [:i32 :i32] [:f64]] ["scale_f64" [:i32 :i32 :f64] [:i32]]]' \
+ '(prn (vec (for [e (wasm/exports (wasm/load "docs/examples/polyglot/c/kernel.wasm")) :when (= :func (:kind e))] [(:name e) (:params e) (:results e)])))'
+
+# (3) A non-function export carries no :params / :results.
+check wasm-exports-memory-has-no-sig \
+ '{:name "memory", :kind :memory}' \
+ '(prn (first (filter #(= :memory (:kind %)) (wasm/exports (wasm/load "docs/examples/polyglot/c/kernel.wasm")))))'
+
+# (4) What exports describes is callable at exactly that arity.
+check wasm-exports-drives-call \
+ '42' \
+ '(let [h (wasm/load "docs/examples/wasm/add.wasm") e (first (wasm/exports h))] (prn (apply wasm/call h (:name e) (take (count (:params e)) [40 2 99]))))'
+
+# (5) A non-handle is the catalogued wasm_handle_invalid error, catchable.
+check wasm-exports-bad-handle \
+ 'CAUGHT wasm/exports: the first argument must be a loaded wasm module' \
+ '(try (wasm/exports 42) (catch Exception e (println "CAUGHT" (ex-message e))))'
+
+echo
+echo "Phase 16 / wasm/exports introspection: all green."
diff --git a/test/run_all.sh b/test/run_all.sh
index aa3ff5b00..a2455775d 100644
--- a/test/run_all.sh
+++ b/test/run_all.sh
@@ -715,6 +715,7 @@ run_step "e2e_bulk_literal" "bash test/e2e/bulk_literal.sh"
# cannot resolve zwasm, build_cljw fails first — wasm is a gate prerequisite now.
run_step "e2e_phase16_wasm_ffi" "bash test/e2e/phase16_wasm_ffi.sh"
run_step "e2e_phase16_wasm_memory" "bash test/e2e/phase16_wasm_memory.sh"
+run_step "e2e_phase16_wasm_exports" "bash test/e2e/phase16_wasm_exports.sh"
run_step "e2e_phase16_wasm_engine_select" "bash test/e2e/phase16_wasm_engine_select.sh"
run_step "e2e_phase16_wasm_run" "bash test/e2e/phase16_wasm_run.sh"
# D-349: the third budget axis. Fuel bounds instructions; how many bytes an
From d4c0363dffb2eb3fced2eb02c7ad08cea44de95e Mon Sep 17 00:00:00 2001
From: Pedro Gomes Branquinho
Date: Wed, 21 Oct 2026 14:00:08 -0300
Subject: [PATCH 09/17] feat(wasm): dot-interop on a core module handle is
wasm/call (ADR-0203)
(.add h 40 2), (. h add 40 2) and (. h (add 40 2)) on a wasm_module value
call the export named by the method symbol, verbatim, so C names such as
sum_f64 work as written.
Runtime.wasm_member_call is a hook both backends consult on a method-table
miss (VM op_method_call, TreeWalk evalInstanceMember). The wasm surface
installs it at register, so it exists only under -Dwasm (F-001) and the
eval zone never imports a surface. wasm/call and the hook share one
callExport path: signature probe, arity check, marshalling, fuel and trap
mapping are not duplicated, and errors are identical.
Test: test/e2e/phase16_wasm_dot_interop.sh with its fixture (the three
spellings, the C kernel, missing export / arity / trap parity with
wasm/call, non-wasm receivers unchanged).
Smell-audited: 2: one runtime hook field instead of a surface import into eval; shared callExport extracted from wasmCallFn
---
docs/examples/wasm/README.md | 16 +++++
src/eval/backend/tree_walk.zig | 3 +
src/eval/backend/vm.zig | 7 ++
src/runtime/cljw/wasm/surface.zig | 17 ++++-
src/runtime/runtime.zig | 7 ++
test/e2e/fixtures/wasm_dot_interop_probe.clj | 33 ++++++++++
test/e2e/phase16_wasm_dot_interop.sh | 69 ++++++++++++++++++++
test/run_all.sh | 2 +
8 files changed, 151 insertions(+), 3 deletions(-)
create mode 100644 test/e2e/fixtures/wasm_dot_interop_probe.clj
create mode 100755 test/e2e/phase16_wasm_dot_interop.sh
diff --git a/docs/examples/wasm/README.md b/docs/examples/wasm/README.md
index 4cd63e458..6d8caa80d 100644
--- a/docs/examples/wasm/README.md
+++ b/docs/examples/wasm/README.md
@@ -14,6 +14,22 @@ sandboxed `.wasm` and invokes its exports.
name, marshalling arguments and results from the export's signature
(`i32`/`i64`/`f32`/`f64` today).
+A module handle also answers Clojure's host-interop method syntax (ADR-0203):
+the method name is the export name, verbatim, and the call is `wasm/call`'s
+own path, so results, fuel limits and traps behave the same.
+
+```clojure
+(.add m 40 2) ;=> 42
+(. m add 40 2) ;=> 42
+(. m (add 40 2)) ;=> 42
+
+;; A C kernel (../polyglot/c/kernel.c): C names work as written.
+(def k (wasm/load "docs/examples/polyglot/c/kernel.wasm"))
+(def buf (.buf_addr k))
+(wasm/mem-write! k :f64 buf [1.0 2.0 3.0 4.0])
+(.sum_f64 k buf 4) ;=> 10.0
+```
+
## Run it
The **released binaries** (GitHub releases, `brew install
diff --git a/src/eval/backend/tree_walk.zig b/src/eval/backend/tree_walk.zig
index ba3632d82..af437c45a 100644
--- a/src/eval/backend/tree_walk.zig
+++ b/src/eval/backend/tree_walk.zig
@@ -777,6 +777,9 @@ fn evalInstanceMember(rt: *Runtime, env: *Env, locals: []Value, n: node_mod.Inte
}
const me = td.lookupMethod(null, n.name) orelse {
+ // ADR-0203: a core wasm module handle answers `(.export h …)` through
+ // wasm/call's path (hook installed only under -Dwasm).
+ if (receiver.tag() == .wasm_module) if (rt.wasm_member_call) |call| return call(rt, receiver, n.name, args_buf[1..], n.loc);
// Universal java.lang.Object method fallback (D-207): str/=/hash/class.
if (try object_method.tryObjectMethod(rt, env, receiver, td, n.name, args_buf[1..])) |r| return r;
// clojure.lang read/op methods on a NATIVE collection (D-371): `.valAt`/
diff --git a/src/eval/backend/vm.zig b/src/eval/backend/vm.zig
index b8a0551f0..6b4729e17 100644
--- a/src/eval/backend/vm.zig
+++ b/src/eval/backend/vm.zig
@@ -1435,6 +1435,13 @@ inline fn stepOnce(
sp -= arg_count;
stack[sp] = result;
sp += 1;
+ } else if (receiver.tag() == .wasm_module and rt.wasm_member_call != null) {
+ // ADR-0203: `(.export h …)` on a core wasm module handle is
+ // wasm/call's path — mirrors TreeWalk's evalInstanceMember.
+ const result = try rt.wasm_member_call.?(rt, receiver, cs_entry.method_name, stack[sp - arg_count + 1 .. sp], instr_loc);
+ sp -= arg_count;
+ stack[sp] = result;
+ sp += 1;
} else if (try object_method.tryObjectMethod(rt, env, receiver, td, cs_entry.method_name, stack[sp - arg_count + 1 .. sp])) |r| {
// Universal java.lang.Object method fallback:
// str/=/hash/class — mirrors TreeWalk's evalInstanceMember.
diff --git a/src/runtime/cljw/wasm/surface.zig b/src/runtime/cljw/wasm/surface.zig
index 58c1afc30..b2806c5ed 100644
--- a/src/runtime/cljw/wasm/surface.zig
+++ b/src/runtime/cljw/wasm/surface.zig
@@ -138,10 +138,19 @@ pub fn wasmCallFn(rt: *Runtime, env: *Env, args: []const Value, loc: SourceLocat
if (!args[1].isString())
return error_catalog.raise(.wasm_export_name_invalid, loc, .{});
- const loaded = wasm_handle.unwrap(args[0]);
- const name = string_mod.asString(args[1]);
- const call_args = args[2..];
+ return callExport(rt, wasm_handle.unwrap(args[0]), string_mod.asString(args[1]), args[2..], loc);
+}
+
+/// `(.export h args…)` on a `.wasm_module` receiver (ADR-0203 decision 2):
+/// the method name is the export name verbatim, and the call is `wasm/call`'s
+/// own path. Installed on `rt.wasm_member_call` by `register`.
+fn memberCallFn(rt: *Runtime, receiver: Value, name: []const u8, args: []const Value, loc: SourceLocation) anyerror!Value {
+ return callExport(rt, wasm_handle.unwrap(receiver), name, args, loc);
+}
+/// The one export-invoke path shared by `wasm/call` and dot-interop: signature
+/// probe, arity check, marshalling, fuel and trap mapping.
+fn callExport(rt: *Runtime, loaded: *engine.Loaded, name: []const u8, call_args: []const Value, loc: SourceLocation) anyerror!Value {
const sig = loaded.exportSig(name) orelse
return error_catalog.raise(.wasm_export_not_found, loc, .{ .name = name });
if (call_args.len != sig.params.len)
@@ -600,6 +609,8 @@ pub fn register(env: *Env) !void {
// Register the `.wasm_module` GC finaliser so a swept `(wasm/load …)` handle
// tears down its zwasm triple instead of leaking (D-259 (b)).
wasm_handle.registerGcHooks();
+ // ADR-0203: `.member` dispatch on a handle routes to `callExport`.
+ env.rt.wasm_member_call = &memberCallFn;
const ns = try env.findOrCreateNs("wasm");
_ = try env.intern(ns, "load", Value.initBuiltinFn(&wasmLoadFn), null);
_ = try env.intern(ns, "call", Value.initBuiltinFn(&wasmCallFn), null);
diff --git a/src/runtime/runtime.zig b/src/runtime/runtime.zig
index 32aa1aa67..a508c7c0e 100644
--- a/src/runtime/runtime.zig
+++ b/src/runtime/runtime.zig
@@ -393,6 +393,13 @@ pub const Runtime = struct {
/// the zone direction legal (lang installs into runtime).
source_resolver: ?*const fn (*Runtime, []const u8) ?SourceContext = null,
+ /// ADR-0203 decision 2: `(.export h args…)` on a `.wasm_module` receiver.
+ /// Both backends' `.member` dispatch call this on a method-table miss; the
+ /// wasm surface (`runtime/cljw/wasm/surface.zig`) installs it at `register`,
+ /// so it is set only under `-Dwasm` (F-001) and the eval zone never imports
+ /// a surface. `args` EXCLUDES the receiver; `name` is the export, verbatim.
+ wasm_member_call: ?*const fn (rt: *Runtime, receiver: @import("value/value.zig").Value, name: []const u8, args: []const @import("value/value.zig").Value, loc: @import("error/info.zig").SourceLocation) anyerror!@import("value/value.zig").Value = null,
+
/// Swappable resolver fn for `require`. ADR-0035 D8. `null` at
/// `init`; bootstrap installs the embedded resolver. Phase 12+
/// can swap for classpath / build-artifact resolvers; Phase 16+
diff --git a/test/e2e/fixtures/wasm_dot_interop_probe.clj b/test/e2e/fixtures/wasm_dot_interop_probe.clj
new file mode 100644
index 000000000..d38d6e06a
--- /dev/null
+++ b/test/e2e/fixtures/wasm_dot_interop_probe.clj
@@ -0,0 +1,33 @@
+;; Dot-interop on a core wasm module handle (ADR-0203 decision 2). Every
+;; method-call form on a `wasm_module` value is `wasm/call` with the method
+;; symbol's name as the export name, verbatim. Asserted by
+;; test/e2e/phase16_wasm_dot_interop.sh.
+(def h (wasm/load "docs/examples/wasm/add.wasm"))
+(println "dot-method" (.add h 40 2))
+(println "dot-form" (. h add 40 2))
+(println "dot-list" (. h (add 40 2)))
+(println "same-as-call" (= (.add h 40 2) (wasm/call h "add" 40 2)))
+
+;; A C kernel: the export name `sum_f64` works as written (no munging), and the
+;; buffer comes from the guest's own allocator.
+(def m (wasm/load "docs/examples/polyglot/c/kernel.wasm"))
+(def buf (wasm/call m "buf_addr"))
+(wasm/mem-write! m :f64 buf [1.0 2.0 3.0 4.0])
+(println "c-sum" (.sum_f64 m buf 4))
+(println "c-scale" (.scale_f64 m buf 4 2.0) (.sum_f64 m buf 4))
+
+(defn msg [f]
+ (try (f) "NOT-RAISED" (catch Throwable e (.getMessage e))))
+
+;; A missing export and a trap raise exactly what wasm/call raises.
+(println "missing-same" (= (msg #(.nope h 1)) (msg #(wasm/call h "nope" 1))))
+(println "missing" (msg #(.nope h 1)))
+(println "arity" (msg #(.add h 1)))
+(def t (wasm/load "docs/examples/wasm/trap.wasm"))
+(println "trap-same" (= (msg #(.boom t)) (msg #(wasm/call t "boom"))))
+(println "trap" (msg #(.boom t)))
+
+;; Non-wasm receivers are unchanged.
+(println "string" (.toUpperCase "abc"))
+(println "nonwasm" (msg #(.add 42 1)))
+(println "DONE")
diff --git a/test/e2e/phase16_wasm_dot_interop.sh b/test/e2e/phase16_wasm_dot_interop.sh
new file mode 100755
index 000000000..3bf47d25e
--- /dev/null
+++ b/test/e2e/phase16_wasm_dot_interop.sh
@@ -0,0 +1,69 @@
+#!/usr/bin/env bash
+# test/e2e/phase16_wasm_dot_interop.sh — method-call forms on a core wasm
+# module handle (ADR-0203 decision 2). `(.add h 40 2)`, `(. h add 40 2)` and
+# `(. h (add 40 2))` are `(wasm/call h "add" 40 2)`: the method symbol's name is
+# the export name verbatim, and the call takes the SAME path as wasm/call, so a
+# missing export, an arity mismatch and a trap raise the same catalogued errors.
+#
+# OPT-IN for the same reason as phase16_wasm_ffi.sh: it builds `-Dwasm`, so it
+# resolves zwasm, which the default gate never does (F-001).
+set -euo pipefail
+cd "$(dirname "$0")/../.."
+BIN="zig-out/bin/cljw"
+fail() { echo "FAIL $1" >&2; exit 1; }
+
+if [ -z "${CLJW_SKIP_BUILD:-}" ] && ! zig build -Dwasm -Doptimize="${CLJW_OPT:-ReleaseSafe}" >/dev/null 2>&1; then
+ fail "zig build -Dwasm failed (zwasm dep unresolved?)"
+fi
+"$BIN" --version | grep -q wasm || fail "cljw is not wasm-enabled ($("$BIN" --version)) — zwasm did not resolve"
+
+FIX="test/e2e/fixtures/wasm_dot_interop_probe.clj"
+
+run_probe() {
+ local label="$1"; shift
+ out="$("$@" "$FIX" 2>&1)" || fail "[$label] cljw exited non-zero running $FIX:
+$out"
+ want() {
+ grep -qF "$1" <<<"$out" || fail "[$label] expected '$1' in the probe output, got:
+$out"
+ }
+
+ # (1) The three method-call spellings all reach the export.
+ want "dot-method 42"
+ want "dot-form 42"
+ want "dot-list 42"
+ want "same-as-call true"
+ echo "PASS wasm-dot-interop-forms [$label] -> (.add h 40 2) / (. h add 40 2) / (. h (add 40 2)) = 42"
+
+ # (2) A C export name is used verbatim; memory written by the host is read.
+ want "c-sum 10.0"
+ want "c-scale 4 20.0"
+ echo "PASS wasm-dot-interop-c-kernel [$label] -> (.sum_f64 m buf 4) = 10.0"
+
+ # (3) Errors are wasm/call's, not a '<.member>' miss.
+ grep -q "NOT-RAISED" <<<"$out" && fail "[$label] a wasm dot-call error was not raised:
+$out"
+ grep -q "<.member>.*wasm_module" <<<"$out" && fail "[$label] wasm receiver fell through to <.member> dispatch:
+$out"
+ want "missing-same true"
+ want "missing wasm/call: no exported function named 'nope'"
+ want "arity wasm/call:"
+ want "trap-same true"
+ want "trap wasm/call: 'boom' () -> (i32) trapped"
+ echo "PASS wasm-dot-interop-errors [$label] -> missing export / arity / trap raise wasm/call's errors"
+
+ # (4) Non-wasm receivers are unchanged.
+ want "string ABC"
+ want "nonwasm No implementation of method 'add' on protocol '<.member>'"
+ echo "PASS wasm-dot-interop-nonwasm [$label] -> other receivers keep <.member> dispatch"
+
+ grep -q "^DONE$" <<<"$out" || fail "[$label] the probe did not run to completion:
+$out"
+}
+
+# The backend is a build option (-Dbackend, vm by default); both arms
+# (VM op_method_call, TreeWalk evalInstanceMember) carry the wasm case.
+run_probe "${CLJW_BACKEND:-vm}" "$BIN"
+
+echo
+echo "phase16_wasm_dot_interop: all PASS"
diff --git a/test/run_all.sh b/test/run_all.sh
index aa3ff5b00..762cff1d4 100644
--- a/test/run_all.sh
+++ b/test/run_all.sh
@@ -730,6 +730,8 @@ run_step "e2e_phase16_wasm_component_budget" "bash test/e2e/phase16_wasm_compone
# as committed AND rebuilt from their source headers (each toolchain on PATH),
# plus hosts.cljc on every Clojure host present. A README claim with a gate.
run_step "e2e_phase16_wasm_polyglot" "bash test/e2e/phase16_wasm_polyglot.sh"
+# ADR-0203: `(.export h …)` on a core module handle is wasm/call's path.
+run_step "e2e_phase16_wasm_dot_interop" "bash test/e2e/phase16_wasm_dot_interop.sh"
# The reswap coverage moved to suites/wasm-require-component-reswap-test: it
# asserts values, not the process boundary, so it runs in-process now.
run_step "e2e_phase15_ns_import" "bash test/e2e/phase15_ns_import.sh"
From 8239bc2030f17ce44fc7be5e2620674064fb1169 Mon Sep 17 00:00:00 2001
From: Pedro Gomes Branquinho
Date: Wed, 21 Oct 2026 14:49:00 -0300
Subject: [PATCH 10/17] test(wasm): move the ADR-0203 tests into in-process
suites
wasm/exports and dot-interop are value assertions, not process-boundary
behaviour, so per decision 20260911144411-22d9cd94 they are deftests in
test/clj/suites/ (wasm_exports_test, wasm_dot_interop_test) instead of two
new e2e scripts. That keeps check_e2e_count at its 322 baseline.
data/placement.yaml is regenerated for the new wasm/exports var.
---
data/placement.yaml | 5 +-
test/clj/suites/wasm_dot_interop_test.clj | 48 ++++++++++++++
test/clj/suites/wasm_exports_test.clj | 42 ++++++++++++
test/e2e/fixtures/wasm_dot_interop_probe.clj | 33 ----------
test/e2e/phase16_wasm_dot_interop.sh | 69 --------------------
test/e2e/phase16_wasm_exports.sh | 65 ------------------
test/run_all.sh | 3 -
7 files changed, 93 insertions(+), 172 deletions(-)
create mode 100644 test/clj/suites/wasm_dot_interop_test.clj
create mode 100644 test/clj/suites/wasm_exports_test.clj
delete mode 100644 test/e2e/fixtures/wasm_dot_interop_probe.clj
delete mode 100755 test/e2e/phase16_wasm_dot_interop.sh
delete mode 100755 test/e2e/phase16_wasm_exports.sh
diff --git a/data/placement.yaml b/data/placement.yaml
index baf8a5848..04d946bb4 100644
--- a/data/placement.yaml
+++ b/data/placement.yaml
@@ -24,8 +24,8 @@
schema_version: "2.0"
summary:
namespaces_required: 36
- total: 1455
- zig: 423
+ total: 1456
+ zig: 424
clj: 806
value: 220
unbound: 6
@@ -1478,6 +1478,7 @@ vars:
"wasm/component-exports": { impl: zig, flags: "-" }
"wasm/component-invoke": { impl: zig, flags: "-" }
"wasm/engine": { impl: zig, flags: "-" }
+ "wasm/exports": { impl: zig, flags: "-" }
"wasm/load": { impl: zig, flags: "-" }
"wasm/load-component": { impl: zig, flags: "-" }
"wasm/mem-read": { impl: zig, flags: "-" }
diff --git a/test/clj/suites/wasm_dot_interop_test.clj b/test/clj/suites/wasm_dot_interop_test.clj
new file mode 100644
index 000000000..964de3867
--- /dev/null
+++ b/test/clj/suites/wasm_dot_interop_test.clj
@@ -0,0 +1,48 @@
+;; Dot-interop on a core wasm module handle (ADR-0203 decision 2).
+;; `(.add h 40 2)`, `(. h add 40 2)` and `(. h (add 40 2))` are
+;; `(wasm/call h "add" 40 2)`: the method symbol's name is the export name
+;; verbatim, and the call takes the same path as wasm/call, so a missing
+;; export, an arity mismatch and a trap raise the same catalogued errors.
+;; Both backends carry the case (VM op_method_call, TreeWalk
+;; evalInstanceMember). Needs the -Dwasm binary (the gate configuration).
+(ns suites.wasm-dot-interop-test
+ (:require [clojure.test :refer [deftest is testing]]))
+
+(def ^:private add-wasm "docs/examples/wasm/add.wasm")
+(def ^:private kernel-wasm "docs/examples/polyglot/c/kernel.wasm")
+(def ^:private trap-wasm "docs/examples/wasm/trap.wasm")
+
+(defn- msg [f]
+ (try (f) :not-raised (catch Throwable e (.getMessage e))))
+
+(deftest three-spellings
+ (let [h (wasm/load add-wasm)]
+ (is (= 42 (.add h 40 2)))
+ (is (= 42 (. h add 40 2)))
+ (is (= 42 (. h (add 40 2))))
+ (is (= (wasm/call h "add" 40 2) (.add h 40 2)))))
+
+(deftest c-export-names-verbatim
+ (let [m (wasm/load kernel-wasm)
+ buf (.buf_addr m)]
+ (wasm/mem-write! m :f64 buf [1.0 2.0 3.0 4.0])
+ (is (= 10.0 (.sum_f64 m buf 4)))
+ (is (= 4 (.scale_f64 m buf 4 2.0)))
+ (is (= 20.0 (.sum_f64 m buf 4)))))
+
+(deftest errors-are-wasm-call-errors
+ (let [h (wasm/load add-wasm)
+ t (wasm/load trap-wasm)]
+ (testing "missing export"
+ (is (= (msg #(wasm/call h "nope" 1)) (msg #(.nope h 1))))
+ (is (= "wasm/call: no exported function named 'nope'" (msg #(.nope h 1)))))
+ (testing "arity"
+ (is (= "wasm/call: 'add' expects 2 argument(s), got 1" (msg #(.add h 1)))))
+ (testing "trap"
+ (is (= (msg #(wasm/call t "boom")) (msg #(.boom t))))
+ (is (re-find #"^wasm/call: 'boom' \(\) -> \(i32\) trapped" (msg #(.boom t)))))))
+
+(deftest non-wasm-receivers-unchanged
+ (is (= "ABC" (.toUpperCase "abc")))
+ (is (re-find #"No implementation of method 'add' on protocol '<\.member>'"
+ (str (msg #(.add 42 1))))))
diff --git a/test/clj/suites/wasm_exports_test.clj b/test/clj/suites/wasm_exports_test.clj
new file mode 100644
index 000000000..8cdfb6b8a
--- /dev/null
+++ b/test/clj/suites/wasm_exports_test.clj
@@ -0,0 +1,42 @@
+;; `(wasm/exports h)` lists a core module's exports in export order with
+;; runtime-probed signatures (ADR-0203 decision 1). A function answers
+;; {:name :kind :func :params [...] :results [...]}, any other export
+;; {:name :kind :memory|:global|:table}. The signature comes from the probe
+;; `wasm/call` marshals through, so a listed function is callable at exactly
+;; the listed arity. Needs the -Dwasm binary (the gate configuration).
+(ns suites.wasm-exports-test
+ (:require [clojure.test :refer [deftest is testing]]))
+
+(def ^:private add-wasm "docs/examples/wasm/add.wasm")
+(def ^:private kernel-wasm "docs/examples/polyglot/c/kernel.wasm")
+
+(deftest single-function-module
+ (is (= [{:name "add" :kind :func :params [:i32 :i32] :results [:i32]}]
+ (wasm/exports (wasm/load add-wasm)))))
+
+(deftest c-kernel-exports
+ (let [es (wasm/exports (wasm/load kernel-wasm))]
+ (testing "the linear memory and four functions, in export order"
+ (is (= [["memory" :memory] ["buf_addr" :func] ["buf_cap" :func]
+ ["sum_f64" :func] ["scale_f64" :func]]
+ (mapv (juxt :name :kind) es))))
+ (testing "function signatures"
+ (is (= [["buf_addr" [] [:i32]]
+ ["buf_cap" [] [:i32]]
+ ["sum_f64" [:i32 :i32] [:f64]]
+ ["scale_f64" [:i32 :i32 :f64] [:i32]]]
+ (vec (for [e es :when (= :func (:kind e))]
+ [(:name e) (:params e) (:results e)])))))
+ (testing "a non-function export carries no :params / :results"
+ (is (= {:name "memory" :kind :memory}
+ (first (filter #(= :memory (:kind %)) es)))))))
+
+(deftest exports-drive-call
+ (let [h (wasm/load add-wasm)
+ e (first (wasm/exports h))]
+ (is (= 42 (apply wasm/call h (:name e) (take (count (:params e)) [40 2 99]))))))
+
+(deftest bad-handle-is-catalogued
+ (is (= "wasm/exports: the first argument must be a loaded wasm module"
+ (try (wasm/exports 42) nil
+ (catch Exception e (ex-message e))))))
diff --git a/test/e2e/fixtures/wasm_dot_interop_probe.clj b/test/e2e/fixtures/wasm_dot_interop_probe.clj
deleted file mode 100644
index d38d6e06a..000000000
--- a/test/e2e/fixtures/wasm_dot_interop_probe.clj
+++ /dev/null
@@ -1,33 +0,0 @@
-;; Dot-interop on a core wasm module handle (ADR-0203 decision 2). Every
-;; method-call form on a `wasm_module` value is `wasm/call` with the method
-;; symbol's name as the export name, verbatim. Asserted by
-;; test/e2e/phase16_wasm_dot_interop.sh.
-(def h (wasm/load "docs/examples/wasm/add.wasm"))
-(println "dot-method" (.add h 40 2))
-(println "dot-form" (. h add 40 2))
-(println "dot-list" (. h (add 40 2)))
-(println "same-as-call" (= (.add h 40 2) (wasm/call h "add" 40 2)))
-
-;; A C kernel: the export name `sum_f64` works as written (no munging), and the
-;; buffer comes from the guest's own allocator.
-(def m (wasm/load "docs/examples/polyglot/c/kernel.wasm"))
-(def buf (wasm/call m "buf_addr"))
-(wasm/mem-write! m :f64 buf [1.0 2.0 3.0 4.0])
-(println "c-sum" (.sum_f64 m buf 4))
-(println "c-scale" (.scale_f64 m buf 4 2.0) (.sum_f64 m buf 4))
-
-(defn msg [f]
- (try (f) "NOT-RAISED" (catch Throwable e (.getMessage e))))
-
-;; A missing export and a trap raise exactly what wasm/call raises.
-(println "missing-same" (= (msg #(.nope h 1)) (msg #(wasm/call h "nope" 1))))
-(println "missing" (msg #(.nope h 1)))
-(println "arity" (msg #(.add h 1)))
-(def t (wasm/load "docs/examples/wasm/trap.wasm"))
-(println "trap-same" (= (msg #(.boom t)) (msg #(wasm/call t "boom"))))
-(println "trap" (msg #(.boom t)))
-
-;; Non-wasm receivers are unchanged.
-(println "string" (.toUpperCase "abc"))
-(println "nonwasm" (msg #(.add 42 1)))
-(println "DONE")
diff --git a/test/e2e/phase16_wasm_dot_interop.sh b/test/e2e/phase16_wasm_dot_interop.sh
deleted file mode 100755
index 3bf47d25e..000000000
--- a/test/e2e/phase16_wasm_dot_interop.sh
+++ /dev/null
@@ -1,69 +0,0 @@
-#!/usr/bin/env bash
-# test/e2e/phase16_wasm_dot_interop.sh — method-call forms on a core wasm
-# module handle (ADR-0203 decision 2). `(.add h 40 2)`, `(. h add 40 2)` and
-# `(. h (add 40 2))` are `(wasm/call h "add" 40 2)`: the method symbol's name is
-# the export name verbatim, and the call takes the SAME path as wasm/call, so a
-# missing export, an arity mismatch and a trap raise the same catalogued errors.
-#
-# OPT-IN for the same reason as phase16_wasm_ffi.sh: it builds `-Dwasm`, so it
-# resolves zwasm, which the default gate never does (F-001).
-set -euo pipefail
-cd "$(dirname "$0")/../.."
-BIN="zig-out/bin/cljw"
-fail() { echo "FAIL $1" >&2; exit 1; }
-
-if [ -z "${CLJW_SKIP_BUILD:-}" ] && ! zig build -Dwasm -Doptimize="${CLJW_OPT:-ReleaseSafe}" >/dev/null 2>&1; then
- fail "zig build -Dwasm failed (zwasm dep unresolved?)"
-fi
-"$BIN" --version | grep -q wasm || fail "cljw is not wasm-enabled ($("$BIN" --version)) — zwasm did not resolve"
-
-FIX="test/e2e/fixtures/wasm_dot_interop_probe.clj"
-
-run_probe() {
- local label="$1"; shift
- out="$("$@" "$FIX" 2>&1)" || fail "[$label] cljw exited non-zero running $FIX:
-$out"
- want() {
- grep -qF "$1" <<<"$out" || fail "[$label] expected '$1' in the probe output, got:
-$out"
- }
-
- # (1) The three method-call spellings all reach the export.
- want "dot-method 42"
- want "dot-form 42"
- want "dot-list 42"
- want "same-as-call true"
- echo "PASS wasm-dot-interop-forms [$label] -> (.add h 40 2) / (. h add 40 2) / (. h (add 40 2)) = 42"
-
- # (2) A C export name is used verbatim; memory written by the host is read.
- want "c-sum 10.0"
- want "c-scale 4 20.0"
- echo "PASS wasm-dot-interop-c-kernel [$label] -> (.sum_f64 m buf 4) = 10.0"
-
- # (3) Errors are wasm/call's, not a '<.member>' miss.
- grep -q "NOT-RAISED" <<<"$out" && fail "[$label] a wasm dot-call error was not raised:
-$out"
- grep -q "<.member>.*wasm_module" <<<"$out" && fail "[$label] wasm receiver fell through to <.member> dispatch:
-$out"
- want "missing-same true"
- want "missing wasm/call: no exported function named 'nope'"
- want "arity wasm/call:"
- want "trap-same true"
- want "trap wasm/call: 'boom' () -> (i32) trapped"
- echo "PASS wasm-dot-interop-errors [$label] -> missing export / arity / trap raise wasm/call's errors"
-
- # (4) Non-wasm receivers are unchanged.
- want "string ABC"
- want "nonwasm No implementation of method 'add' on protocol '<.member>'"
- echo "PASS wasm-dot-interop-nonwasm [$label] -> other receivers keep <.member> dispatch"
-
- grep -q "^DONE$" <<<"$out" || fail "[$label] the probe did not run to completion:
-$out"
-}
-
-# The backend is a build option (-Dbackend, vm by default); both arms
-# (VM op_method_call, TreeWalk evalInstanceMember) carry the wasm case.
-run_probe "${CLJW_BACKEND:-vm}" "$BIN"
-
-echo
-echo "phase16_wasm_dot_interop: all PASS"
diff --git a/test/e2e/phase16_wasm_exports.sh b/test/e2e/phase16_wasm_exports.sh
deleted file mode 100755
index f01aa632d..000000000
--- a/test/e2e/phase16_wasm_exports.sh
+++ /dev/null
@@ -1,65 +0,0 @@
-#!/usr/bin/env bash
-# test/e2e/phase16_wasm_exports.sh — `(wasm/exports h)` lists a core module's
-# exports in export order with runtime-probed signatures (ADR-0203 decision 1).
-#
-# Functions answer {:name :kind :func :params [...] :results [...]}, every other
-# export {:name :kind :memory|:global|:table}. The signature comes from the same
-# probe `wasm/call` uses, so a function `wasm/exports` describes is callable with
-# exactly that arity. A non-handle argument raises the catalogued
-# wasm_handle_invalid error, catchable like every other wasm-surface error.
-#
-# Reuses the shared -Dwasm binary under CLJW_SKIP_BUILD, like the other phase16
-# wasm e2es; standalone it builds -Dwasm ReleaseSafe itself.
-set -euo pipefail
-cd "$(dirname "$0")/../.."
-BIN="zig-out/bin/cljw"
-fail() { echo "FAIL $1" >&2; exit 1; }
-
-if [ -z "${CLJW_SKIP_BUILD:-}" ] && ! zig build -Dwasm -Doptimize="${CLJW_OPT:-ReleaseSafe}" >/dev/null 2>&1; then
- fail "zig build -Dwasm failed (zwasm dep unresolved?)"
-fi
-"$BIN" --version | grep -q wasm || fail "cljw is not wasm-enabled ($("$BIN" --version))"
-
-check() { # name expected expr
- local got
- got="$("$BIN" -e "$3" 2>&1)" || fail "$1: cljw exited non-zero:
-$got"
- got="$(printf '%s\n' "$got" | sed '${/^nil$/d}')" # -e echoes the prn/println result
- [ "$got" = "$2" ] || fail "$1: expected
- $2
-got
- $got"
- echo "PASS $1"
-}
-
-# (1) The single-function module: the acceptance shape, verbatim.
-check wasm-exports-add \
- '[{:name "add", :kind :func, :params [:i32 :i32], :results [:i32]}]' \
- '(prn (wasm/exports (wasm/load "docs/examples/wasm/add.wasm")))'
-
-# (2) A C-compiled module: the linear memory and four functions, in export order.
-check wasm-exports-c-kernel-names \
- '[["memory" :memory] ["buf_addr" :func] ["buf_cap" :func] ["sum_f64" :func] ["scale_f64" :func]]' \
- '(prn (mapv (juxt :name :kind) (wasm/exports (wasm/load "docs/examples/polyglot/c/kernel.wasm"))))'
-
-check wasm-exports-c-kernel-sigs \
- '[["buf_addr" [] [:i32]] ["buf_cap" [] [:i32]] ["sum_f64" [:i32 :i32] [:f64]] ["scale_f64" [:i32 :i32 :f64] [:i32]]]' \
- '(prn (vec (for [e (wasm/exports (wasm/load "docs/examples/polyglot/c/kernel.wasm")) :when (= :func (:kind e))] [(:name e) (:params e) (:results e)])))'
-
-# (3) A non-function export carries no :params / :results.
-check wasm-exports-memory-has-no-sig \
- '{:name "memory", :kind :memory}' \
- '(prn (first (filter #(= :memory (:kind %)) (wasm/exports (wasm/load "docs/examples/polyglot/c/kernel.wasm")))))'
-
-# (4) What exports describes is callable at exactly that arity.
-check wasm-exports-drives-call \
- '42' \
- '(let [h (wasm/load "docs/examples/wasm/add.wasm") e (first (wasm/exports h))] (prn (apply wasm/call h (:name e) (take (count (:params e)) [40 2 99]))))'
-
-# (5) A non-handle is the catalogued wasm_handle_invalid error, catchable.
-check wasm-exports-bad-handle \
- 'CAUGHT wasm/exports: the first argument must be a loaded wasm module' \
- '(try (wasm/exports 42) (catch Exception e (println "CAUGHT" (ex-message e))))'
-
-echo
-echo "Phase 16 / wasm/exports introspection: all green."
diff --git a/test/run_all.sh b/test/run_all.sh
index 46861060b..aa3ff5b00 100644
--- a/test/run_all.sh
+++ b/test/run_all.sh
@@ -715,7 +715,6 @@ run_step "e2e_bulk_literal" "bash test/e2e/bulk_literal.sh"
# cannot resolve zwasm, build_cljw fails first — wasm is a gate prerequisite now.
run_step "e2e_phase16_wasm_ffi" "bash test/e2e/phase16_wasm_ffi.sh"
run_step "e2e_phase16_wasm_memory" "bash test/e2e/phase16_wasm_memory.sh"
-run_step "e2e_phase16_wasm_exports" "bash test/e2e/phase16_wasm_exports.sh"
run_step "e2e_phase16_wasm_engine_select" "bash test/e2e/phase16_wasm_engine_select.sh"
run_step "e2e_phase16_wasm_run" "bash test/e2e/phase16_wasm_run.sh"
# D-349: the third budget axis. Fuel bounds instructions; how many bytes an
@@ -731,8 +730,6 @@ run_step "e2e_phase16_wasm_component_budget" "bash test/e2e/phase16_wasm_compone
# as committed AND rebuilt from their source headers (each toolchain on PATH),
# plus hosts.cljc on every Clojure host present. A README claim with a gate.
run_step "e2e_phase16_wasm_polyglot" "bash test/e2e/phase16_wasm_polyglot.sh"
-# ADR-0203: `(.export h …)` on a core module handle is wasm/call's path.
-run_step "e2e_phase16_wasm_dot_interop" "bash test/e2e/phase16_wasm_dot_interop.sh"
# The reswap coverage moved to suites/wasm-require-component-reswap-test: it
# asserts values, not the process boundary, so it runs in-process now.
run_step "e2e_phase15_ns_import" "bash test/e2e/phase15_ns_import.sh"
From 7dc4fd143cc2d0df8156ac4847b9f93ab56ae397 Mon Sep 17 00:00:00 2001
From: Pedro Gomes Branquinho
Date: Wed, 21 Oct 2026 14:25:05 -0300
Subject: [PATCH 11/17] docs(readme): apply the agora d666cca9 review
Six changes agreed by the newcomer, clojurist and editor reviewers in
agora d666cca9:
1. Order and navigation: Install and Quickstart follow the intro, a
one-line table of contents is added, News shrinks to one paragraph,
and License and credit carries the full handover account.
2. Quickstart keeps the wasm/call line and adds the ADR-0203 dot form
(.add m 40 2), marked as arriving in the next release.
3. Wasm FFI section: wasm/exports as the discovery step and
(.sum_f64 m buf 4), both marked next release, with the dot-interop
semantics (ADR-0203); every elided output replaced by the real one.
4. Accuracy: startup figures only under Numbers; string-path :require
marked cljw-only; "What does not" names core.async, java.time.format,
the permissive clojure.edn/read-string and (clojure-version) "1.12.0".
5. Where it shines trimmed: the sandbox example runs a shipped module
(kernel.wasm with a cap, trap.wasm caught); Native tools shows a real
sh result; padding closer dropped.
6. What runs trimmed: the Maven resolver paragraph is two sentences
plus a CHANGELOG link; "test.check, ..." becomes "test.check and more".
---
README.md | 190 ++++++++++++++++++++++++++++--------------------------
1 file changed, 99 insertions(+), 91 deletions(-)
diff --git a/README.md b/README.md
index cc0a7478d..0cdbd7f6b 100644
--- a/README.md
+++ b/README.md
@@ -23,21 +23,50 @@ WebAssembly engine so a `.wasm` compiled from Rust, Go, C or Zig can be
required like a namespace and called like a function, inside a sandbox whose
fuel and memory the caller sets.
-## News
+[Install](#install) · [Quickstart](#quickstart) · [News](#news) ·
+[Where it shines](#where-it-shines) · [How it is put together](#how-it-is-put-together) ·
+[Wasm FFI](#the-wasm-ffi-in-one-screen) · [Numbers](#numbers) ·
+[What runs](#what-runs) · [Documentation](#documentation) ·
+[License](#license-and-credit)
-**2026-10: the project has a new maintainer.**
-[@chaploud](https://github.com/chaploud), who created ClojureWasm and released
-it through v1.10.1, has handed the [clojurewasm](https://github.com/clojurewasm)
-organization over to [@BuddhiLW](https://github.com/BuddhiLW). The fork's work
-(v1.11 through v1.15.0) is merged back into
-[clojurewasm/ClojureWasm](https://github.com/clojurewasm/ClojureWasm), so the
-original repository is active again. Releases and the Homebrew tap keep
-publishing from [BuddhiLW/ClojureWasm](https://github.com/BuddhiLW/ClojureWasm)
-for now; existing install commands and version pins keep working.
+## Install
+
+```sh
+brew install buddhilw/tap/cljw # macOS arm64, Linux x86_64
+```
+
+Binaries are also on the [Releases](https://github.com/BuddhiLW/ClojureWasm/releases)
+page. The binary is not code-signed; if macOS blocks it once, clear the
+quarantine flag: `xattr -d com.apple.quarantine "$(which cljw)"`.
+
+From source (Zig 0.16; `direnv allow` or `nix develop` provides it):
+
+```sh
+zig build -Dwasm -Doptimize=ReleaseSafe # → ./zig-out/bin/cljw
+```
+
+## Quickstart
+
+```sh
+cljw -e '(->> (range) (filter even?) (take 5))' # (0 2 4 6 8)
+cljw -e '(wasm/call (wasm/load "docs/examples/wasm/add.wasm") "add" 40 2)' # 42
+cljw -e '(.add (wasm/load "docs/examples/wasm/add.wasm") 40 2)' # 42, next release
+cljw script.clj # run a file
+cljw # REPL
+cljw nrepl --port 7888 # nREPL, CIDER-compatible
+cljw build script.clj -o app # one self-contained native binary
+```
-[Issues](https://github.com/BuddhiLW/ClojureWasm/issues) and PRs are open, and
-the most useful report is Clojure code that behaves differently here than on
-the JVM (there is an issue template for it).
+## News
+
+**2026-10:** [@chaploud](https://github.com/chaploud) handed the project to
+[@BuddhiLW](https://github.com/BuddhiLW); the fork's work is merged back into
+[clojurewasm/ClojureWasm](https://github.com/clojurewasm/ClojureWasm), releases
+and the Homebrew tap still publish from
+[BuddhiLW/ClojureWasm](https://github.com/BuddhiLW/ClojureWasm), and install
+commands and version pins are unchanged.
+[Issues](https://github.com/BuddhiLW/ClojureWasm/issues) and PRs are open; the
+most useful report is Clojure code that behaves differently here than on the JVM.
## Where it shines
@@ -49,9 +78,10 @@ runs in-process: plugin systems, MCP tool servers, request handlers that
evaluate user code.
```clojure
-(def zk (wasm/load-component "zk_groth16.wasm" {:fuel 5000000 :max-memory-pages 64}))
-(wasm/component-call zk "verify" proof 35) ;=> [:ok true]
-(wasm/component-call zk "spin") ; throws: exhausted its fuel budget
+(def k (wasm/load "docs/examples/polyglot/c/kernel.wasm" {:fuel 1000000 :max-memory-pages 16}))
+(wasm/call k "buf_cap") ;=> 256
+(try (wasm/call (wasm/load "docs/examples/wasm/trap.wasm") "boom")
+ (catch Exception e (re-find #"trapped" (ex-message e)))) ;=> "trapped"
```
**Other languages' libraries, from Clojure.** JVM Clojure calls Java;
@@ -68,6 +98,9 @@ vectors, `result` as `[:ok v]` / `[:err e]`.
(:arglists (meta #'tp/process)) ;; => ([input])
```
+A string path in `:require` is cljw-only; in a `.cljc`, write it as
+`#?(:cljw ["./typed_payload.wasm" :as tp])`.
+
Each guest language has a natural shape, and cljw has an entry point for each:
| guest | built with | crosses the boundary as | entry point |
@@ -82,30 +115,26 @@ All five run in [`docs/examples/polyglot`](./docs/examples/polyglot/README.md),
sources and build lines included; CI rebuilds them from source and runs them on
every push.
-**Native tools, unchanged.** When the tool already exists as a native binary,
-nothing needs recompiling to Wasm and nothing needs bindings: its seam is its
-command line. `clojure.java.shell/sh` runs any program by argv (never through a
-shell) and returns `{:exit :out :err}`, with a non-zero exit as data. The same
-call runs on the JVM and on clojurust, so the client that wraps the tool is one
-`.cljc`.
+**Native tools, unchanged.** A tool that already exists as a native binary
+needs no Wasm build and no bindings. `clojure.java.shell/sh`, the same API the
+JVM and babashka ship, runs it by argv (never through a shell) and returns
+`{:exit :out :err}`, with a non-zero exit as data.
```clojure
(require '[clojure.java.shell :refer [sh]])
-(sh "autopdf" "build" "invoice.tex" "invoice.json" "clean" :dir "examples")
-;; => {:exit 0, :out "...", :err "..."} ; a Go binary, driven as is
+(sh "echo" "hi") ;=> {:exit 0, :out "hi\n", :err ""}
```
[AutoPDF's Clojure client](https://github.com/BuddhiLW/AutoPDF/tree/main/clients/clojure)
-drives the unmodified Go `autopdf` CLI this way and renders the same PDF,
-byte for byte, on cljw, clojurust and the JVM. A child process escapes every
-in-process containment, so `sh` is refused under a filesystem root
-(`CLJW_FS_ROOT`) or inside `cljw.eval/with-budget`: pick Wasm when the guest
-must be confined, the process seam when the tool is trusted.
+drives the unmodified Go `autopdf` CLI this way and renders the same PDF on
+cljw, clojurust and the JVM. A child process escapes in-process containment, so
+`sh` is refused under a filesystem root (`CLJW_FS_ROOT`) or inside
+`cljw.eval/with-budget`.
-**Short-lived processes.** 8.1 MB on disk, about 6 ms from process start to
-first eval, and `cljw build app.clj -o app` produces a self-contained
-executable. CLI tools, serverless handlers, scripts that run a thousand times
-a day.
+**Short-lived processes.** One 8.1 MB binary that starts in milliseconds
+([Numbers](#numbers)), and `cljw build app.clj -o app` produces a
+self-contained executable. CLI tools, serverless handlers, scripts that run a
+thousand times a day.
**One program, two runtimes.** `cljw` reads `.cljc` under the feature set
`{:cljw :clj :default}`, and its sibling [clojurust](https://github.com/BuddhiLW/clojurust)
@@ -136,9 +165,6 @@ crate and feature set on both sides):
| file read | granted | denied |
| wall clock | granted | trap |
-Same proof bytes; the only thing that differs is how much authority the prover
-holds while it runs.
-
## How it is put together
```mermaid
@@ -158,58 +184,43 @@ from-scratch runtime keeps Clojure semantics honest without a reference
implementation in the process. [`docs/architecture.md`](./docs/architecture.md)
has the map.
-## Install
-
-```sh
-brew install buddhilw/tap/cljw # macOS arm64, Linux x86_64
-```
-
-Binaries are also on the [Releases](https://github.com/BuddhiLW/ClojureWasm/releases)
-page. The binary is not code-signed; if macOS blocks it once, clear the
-quarantine flag: `xattr -d com.apple.quarantine "$(which cljw)"`.
-
-From source (Zig 0.16; `direnv allow` or `nix develop` provides it):
-
-```sh
-zig build -Dwasm -Doptimize=ReleaseSafe # → ./zig-out/bin/cljw
-```
-
-## Quickstart
-
-```sh
-cljw -e '(->> (range) (filter even?) (take 5))' # (0 2 4 6 8)
-cljw -e '(wasm/call (wasm/load "docs/examples/wasm/add.wasm") "add" 40 2)' # 42
-cljw script.clj # run a file
-cljw # REPL
-cljw nrepl --port 7888 # nREPL, CIDER-compatible
-cljw build script.clj -o app # one self-contained native binary
-```
-
## The Wasm FFI in one screen
```clojure
;; A core module (docs/examples/polyglot/c/kernel.c): numbers in, numbers
;; out, arrays in a buffer the guest owns and hands out by address.
(def m (wasm/load "docs/examples/polyglot/c/kernel.wasm" {:fuel 1000000 :max-memory-pages 16}))
-(wasm/exports m) ;=> [{:name "memory", :kind :memory}
- ; {:name "buf_addr", :kind :func, :params [], :results [:i32]}
- ; ... {:name "sum_f64", :kind :func, :params [:i32 :i32], :results [:f64]} ...]
+
+;; Next release: list the exports before calling any.
+(mapv (juxt :name :kind) (wasm/exports m))
+;=> [["memory" :memory] ["buf_addr" :func] ["buf_cap" :func] ["sum_f64" :func] ["scale_f64" :func]]
+
(def buf (wasm/call m "buf_addr"))
(wasm/mem-write! m :f64 buf [1.0 2.0 3.0 4.0])
(wasm/call m "sum_f64" buf 4) ;=> 10.0
+(.sum_f64 m buf 4) ;=> 10.0, next release
(wasm/mem-read m :f64 buf 4) ;=> [1.0 2.0 3.0 4.0]
;; A WIT component: typed, so arguments and results are plain Clojure data.
(def g "test/e2e/fixtures/wasm/greet_component.wasm")
-(wasm/component-exports g) ;=> ({:name "greet", ...})
+(wasm/component-exports g) ;=> [{:name "greet", :params [["name" "string"]], :result "string"}]
(def c (wasm/load-component g {:fuel 100000}))
(wasm/component-call c "greet" "zwasm") ;=> "Hello, zwasm!"
;; A fault in the guest is a catchable Clojure exception, never a host crash.
(try (wasm/call (wasm/load "docs/examples/wasm/trap.wasm") "boom")
- (catch Exception e (ex-message e))) ;=> "wasm/call: 'boom' ... trapped ..."
+ (catch Exception e (ex-message e)))
+;=> "wasm/call: 'boom' () -> (i32) trapped on the auto (JIT-first) engine
+; (divide-by-zero, out-of-bounds, unreachable, or a call shape the engine
+; cannot dispatch)"
```
+Dot interop (ADR-0203, next release): on a module handle, `(.sum_f64 m buf 4)`,
+`(. m sum_f64 buf 4)` and `(. m (sum_f64 buf 4))` are `wasm/call` with the
+method name as the export name, verbatim, so results, fuel and errors are the
+same. It is cljw-only (use a `:cljw` reader conditional in a `.cljc`), and an
+export whose name is not a valid symbol still goes through `wasm/call`.
+
Budgets: a missing axis keeps the engine's finite default (1e9 fuel, 4096
pages), `0` removes a cap for a trusted guest, and fuel is per instance, so an
exhausted handle stays exhausted until you load again. The engine is JIT by
@@ -264,32 +275,26 @@ multimethods, `deftype` / `reify`, the full numeric tower (`Ratio`, `BigInt`,
coordinates, and a CIDER-compatible nREPL.
Real libraries that load and run today (medley, tools.cli, data.json,
-core.cache, malli, hiccup, honeysql, test.check, ...) are tracked in
+core.cache, malli, hiccup, honeysql, test.check and more) are tracked in
[`docs/works/ladder.md`](./docs/works/ladder.md).
-Maven `:mvn/version` coordinates resolve Clojure source entries in cached JARs
-from `$M2_REPO` (default `~/.m2/repository`). Missing artifacts are downloaded
-from Clojars, then Maven Central via HTTPS (`curl`); extracted source roots are
-cached under `$CLJW_HOME/mvn` (default `~/.cljw/mvn`). Compile/runtime POM
-coordinates are traversed; `org.clojure/clojure` and JARs with no Clojure
-sources are omitted. Remote JARs and POMs must match their repository's SHA-1
-sidecar before installation; a missing or mismatched checksum fails closed.
-`CLJW_MVN_REPOS` overrides the ordered repository bases as comma-separated URLs
-(default Clojars, then Maven Central; `file://` is useful for hermetic tests).
-Artifacts already in `$M2_REPO` are trusted. `curl` and `unzip` are required on the host. POM traversal
-supports direct compile/runtime dependencies with literal versions or local
-properties; it does not implement BOM imports, inherited dependencyManagement,
-classifiers, exclusions, or Maven version mediation. JVM-only jars provide no
-source path (native Java classes remain unavailable). For example:
+`:mvn/version` coordinates load Clojure sources from JARs in
+`~/.m2/repository`, fetching missing ones from Clojars, then Maven Central;
+every fetched JAR and POM must match its SHA-1 sidecar or the load fails
+closed. `curl` and `unzip` must be on the host, and the
+[CHANGELOG](./CHANGELOG.md) entry for 1.15.0 lists the limits.
```clojure
{:deps {metosin/malli {:mvn/version "0.20.2"}}}
```
-What does not: `core.async` is not bundled, and JVM
-interop that exists only to reach the JVM (`gen-class`, `proxy` over arbitrary
-classes, reflection, `import` of arbitrary Java classes) is out of scope by
-design. The full picture, including the intentional divergences, is
+What does not: `core.async` is not bundled, `java.time.format` is not
+implemented, and `clojure.edn/read-string` reuses the permissive Clojure
+reader, so some EDN-invalid input (such as `#(inc %)`) is accepted.
+`(clojure-version)` reports `"1.12.0"`, the semantics target. JVM interop that
+exists only to reach the JVM (`gen-class`, `proxy` over arbitrary classes,
+reflection, `import` of arbitrary Java classes) is out of scope by design. The
+full picture, including the intentional divergences, is
[`docs/clojure_vs_clojurewasm.md`](./docs/clojure_vs_clojurewasm.md).
## Documentation
@@ -315,7 +320,10 @@ Eclipse Public License 2.0, see [LICENSE](./LICENSE) and
ClojureWasm was created by [Shota Kudo (@chaploud)](https://github.com/chaploud),
who took it from nothing to a working JVM-free Clojure runtime with a Wasm FFI
-through v1.10, and in 2026-10 passed the project on to
-[@BuddhiLW](https://github.com/BuddhiLW). Everything shipped since is built on
-that work. The embedded
-engine is [zwasm](https://github.com/zwasm/zwasm).
+through v1.10.1, and in 2026-10 handed the
+[clojurewasm](https://github.com/clojurewasm) organization over to
+[@BuddhiLW](https://github.com/BuddhiLW). The fork's work (v1.11 through
+v1.15.0) is merged back into
+[clojurewasm/ClojureWasm](https://github.com/clojurewasm/ClojureWasm), so the
+original repository is active again. Everything shipped since is built on
+that work. The embedded engine is [zwasm](https://github.com/zwasm/zwasm).
From 088264967254f24f72b8936d06a7600316016c9e Mon Sep 17 00:00:00 2001
From: Pedro Gomes Branquinho
Date: Wed, 21 Oct 2026 16:04:03 -0300
Subject: [PATCH 12/17] docs(readme): a shorter README led by C-to-Clojure and
the graphs
329 lines to 219. The page now opens with install, then the C function
compiled to Wasm and called with dot interop, then the architecture
diagram, the size and startup charts, and the one-program-two-runtimes
diagram with the Groth16 table compressed to three rows. Use cases are
five bullets; what runs, docs and credit are each one short block.
Every figure and link carried over unchanged; nav anchors match headings.
---
README.md | 364 +++++++++++++++++++-----------------------------------
1 file changed, 127 insertions(+), 237 deletions(-)
diff --git a/README.md b/README.md
index 0cdbd7f6b..255dc6e3d 100644
--- a/README.md
+++ b/README.md
@@ -13,159 +13,97 @@
-
-ClojureWasm (`cljw`) runs Clojure without a JVM. It is written in Zig and
-Clojure, ships as one static binary, starts in milliseconds, and embeds a
-WebAssembly engine so a `.wasm` compiled from Rust, Go, C or Zig can be
-required like a namespace and called like a function, inside a sandbox whose
-fuel and memory the caller sets.
+
+ Install ·
+ Call C from Clojure ·
+ How it works ·
+ Numbers ·
+ Where it fits ·
+ What runs ·
+ Docs
+
-[Install](#install) · [Quickstart](#quickstart) · [News](#news) ·
-[Where it shines](#where-it-shines) · [How it is put together](#how-it-is-put-together) ·
-[Wasm FFI](#the-wasm-ffi-in-one-screen) · [Numbers](#numbers) ·
-[What runs](#what-runs) · [Documentation](#documentation) ·
-[License](#license-and-credit)
+`cljw` runs Clojure without a JVM. It ships as one static binary, starts in
+milliseconds, and embeds a WebAssembly engine: a `.wasm` built from C, Rust,
+Go or Zig loads like a library and runs inside a sandbox whose fuel and memory
+you set.
## Install
```sh
-brew install buddhilw/tap/cljw # macOS arm64, Linux x86_64
-```
-
-Binaries are also on the [Releases](https://github.com/BuddhiLW/ClojureWasm/releases)
-page. The binary is not code-signed; if macOS blocks it once, clear the
-quarantine flag: `xattr -d com.apple.quarantine "$(which cljw)"`.
-
-From source (Zig 0.16; `direnv allow` or `nix develop` provides it):
-
-```sh
-zig build -Dwasm -Doptimize=ReleaseSafe # → ./zig-out/bin/cljw
+brew install buddhilw/tap/cljw # macOS arm64, Linux x86_64
+zig build -Dwasm -Doptimize=ReleaseSafe # or from source (Zig 0.16)
```
-## Quickstart
+Prebuilt binaries are on [Releases](https://github.com/BuddhiLW/ClojureWasm/releases).
+They are not code-signed; on macOS, `xattr -d com.apple.quarantine "$(which cljw)"`
+once.
```sh
-cljw -e '(->> (range) (filter even?) (take 5))' # (0 2 4 6 8)
-cljw -e '(wasm/call (wasm/load "docs/examples/wasm/add.wasm") "add" 40 2)' # 42
-cljw -e '(.add (wasm/load "docs/examples/wasm/add.wasm") 40 2)' # 42, next release
-cljw script.clj # run a file
-cljw # REPL
-cljw nrepl --port 7888 # nREPL, CIDER-compatible
-cljw build script.clj -o app # one self-contained native binary
+cljw -e '(->> (range) (filter even?) (take 5))' # (0 2 4 6 8)
+cljw script.clj # run a file
+cljw # REPL
+cljw nrepl --port 7888 # nREPL, CIDER-compatible
+cljw build script.clj -o app # self-contained native binary
```
-## News
+## Call C from Clojure
-**2026-10:** [@chaploud](https://github.com/chaploud) handed the project to
-[@BuddhiLW](https://github.com/BuddhiLW); the fork's work is merged back into
-[clojurewasm/ClojureWasm](https://github.com/clojurewasm/ClojureWasm), releases
-and the Homebrew tap still publish from
-[BuddhiLW/ClojureWasm](https://github.com/BuddhiLW/ClojureWasm), and install
-commands and version pins are unchanged.
-[Issues](https://github.com/BuddhiLW/ClojureWasm/issues) and PRs are open; the
-most useful report is Clojure code that behaves differently here than on the JVM.
+A C function, compiled to Wasm, called with ordinary Clojure interop.
-## Where it shines
+```c
+// docs/examples/polyglot/c/kernel.c
+EXPORT("sum_f64") double sum_f64(int off, int n) {
+ const double *p = (const double *)(long)off;
+ double s = 0.0;
+ for (int i = 0; i < n; i++) s += p[i];
+ return s;
+}
+```
-**Sandboxed plugins and tools.** A module instantiates with an empty import
-object, so a guest reaches nothing the host did not grant, and every load takes
-a budget. A tool that spins burns its fuel and returns an error; the host
-process keeps running. That makes `cljw` a fit for hosts where untrusted code
-runs in-process: plugin systems, MCP tool servers, request handlers that
-evaluate user code.
+```sh
+zig cc -target wasm32-freestanding -O2 -nostdlib \
+ -Wl,--no-entry -Wl,--export-memory -Wl,-z,stack-size=65536 kernel.c -o kernel.wasm
+```
```clojure
(def k (wasm/load "docs/examples/polyglot/c/kernel.wasm" {:fuel 1000000 :max-memory-pages 16}))
-(wasm/call k "buf_cap") ;=> 256
-(try (wasm/call (wasm/load "docs/examples/wasm/trap.wasm") "boom")
- (catch Exception e (re-find #"trapped" (ex-message e)))) ;=> "trapped"
-```
-
-**Other languages' libraries, from Clojure.** JVM Clojure calls Java;
-ClojureScript calls JavaScript; here the host is a language-neutral `.wasm`.
-A WIT-typed component is a namespace: records arrive as maps, lists as
-vectors, `result` as `[:ok v]` / `[:err e]`.
-```clojure
-(ns my.app
- (:require ["./typed_payload.wasm" :as tp])) ; Rust + wit-bindgen, 51 KB
+(mapv :name (wasm/exports k)) ;=> ["memory" "buf_addr" "buf_cap" "sum_f64" "scale_f64"]
-(tp/process {:xs [3 4 5] :label "data"}) ;; => [:ok {:xs [3 4 5 12], :label "data!"}]
-(tp/process {:xs [] :label "fail"}) ;; => [:err "boom: fail"]
-(:arglists (meta #'tp/process)) ;; => ([input])
+(def buf (.buf_addr k))
+(wasm/mem-write! k :f64 buf [1.0 2.0 3.0 4.0])
+(.sum_f64 k buf 4) ;=> 10.0
```
-A string path in `:require` is cljw-only; in a `.cljc`, write it as
-`#?(:cljw ["./typed_payload.wasm" :as tp])`.
+`wasm/exports` and the dot form arrive in the next release; on the current
+one, write `(wasm/call k "sum_f64" buf 4)`. A guest fault is a catchable
+exception, never a host crash, and a guest that runs out of fuel stops with an
+error while your process keeps going.
-Each guest language has a natural shape, and cljw has an entry point for each:
-
-| guest | built with | crosses the boundary as | entry point |
-|---|---|---|---|
-| C | `zig cc -target wasm32-freestanding` | scalars, arrays in a guest-owned buffer | `wasm/load`, `wasm/call`, `wasm/mem-*` |
-| Zig | `zig build-exe -target wasm32-freestanding` | scalars | `wasm/load`, `wasm/call` |
-| Rust, `no_std` | `rustc --target wasm32-unknown-unknown` | scalars | `wasm/load`, `wasm/call` |
-| Rust, wit-bindgen | `cargo build --target wasm32-wasip2` | records, lists, strings, results | `(:require ["x.wasm" :as x])` |
-| Go | `GOOS=wasip1 GOARCH=wasm go build` | argv, env, stdin, stdout, exit code | `wasm/run` |
-
-All five run in [`docs/examples/polyglot`](./docs/examples/polyglot/README.md),
-sources and build lines included; CI rebuilds them from source and runs them on
-every push.
-
-**Native tools, unchanged.** A tool that already exists as a native binary
-needs no Wasm build and no bindings. `clojure.java.shell/sh`, the same API the
-JVM and babashka ship, runs it by argv (never through a shell) and returns
-`{:exit :out :err}`, with a non-zero exit as data.
+A WIT component goes further: it is required like a namespace, and records,
+lists and `result` cross as plain Clojure data.
```clojure
-(require '[clojure.java.shell :refer [sh]])
-(sh "echo" "hi") ;=> {:exit 0, :out "hi\n", :err ""}
-```
-
-[AutoPDF's Clojure client](https://github.com/BuddhiLW/AutoPDF/tree/main/clients/clojure)
-drives the unmodified Go `autopdf` CLI this way and renders the same PDF on
-cljw, clojurust and the JVM. A child process escapes in-process containment, so
-`sh` is refused under a filesystem root (`CLJW_FS_ROOT`) or inside
-`cljw.eval/with-budget`.
-
-**Short-lived processes.** One 8.1 MB binary that starts in milliseconds
-([Numbers](#numbers)), and `cljw build app.clj -o app` produces a
-self-contained executable. CLI tools, serverless handlers, scripts that run a
-thousand times a day.
-
-**One program, two runtimes.** `cljw` reads `.cljc` under the feature set
-`{:cljw :clj :default}`, and its sibling [clojurust](https://github.com/BuddhiLW/clojurust)
-(`cljrs`, Rust-hosted, Cranelift JIT) reads `{:rust}`. A host-free `.cljc`
-runs unchanged on the JVM, on `cljw` and on `cljrs`
-([`hosts.cljc`](./docs/examples/polyglot/hosts.cljc) prints the same map on
-all three, ratios and bigints included, and CI checks that), and the same Rust
-crate can serve both: as a zero-import Wasm component on `cljw`, as a native
-`cdylib` on `cljrs`. The program picks confinement or speed without a rewrite.
+(ns my.app (:require ["./typed_payload.wasm" :as tp])) ; Rust + wit-bindgen
-```mermaid
-flowchart LR
- P["demo.cljc
one Clojure program"] --> A["cljw
Wasm component, zero imports"]
- P --> B["cljrs
native cdylib"]
- A --> L[("arkworks Groth16
the same Rust crate")]
- B --> L
+(tp/process {:xs [3 4 5] :label "data"}) ;=> [:ok {:xs [3 4 5 12], :label "data!"}]
+(tp/process {:xs [] :label "fail"}) ;=> [:err "boom: fail"]
```
-Measured on that exact shape (a Groth16 prover over BN254, 2026-09-12, same
-crate and feature set on both sides):
-
-| | cljrs (native) | cljw (Wasm) |
+| guest | crosses the boundary as | entry point |
|---|---|---|
-| proof | 128 bytes | 128 bytes |
-| verify, correct / wrong statement | true / false | true / false |
-| wall time | 1.29 s (debug build) | 9.17 s |
-| env vars visible to the prover | 212 | 0 |
-| file read | granted | denied |
-| wall clock | granted | trap |
+| C, Zig, Rust `no_std` | scalars, arrays in guest memory | `wasm/load`, `wasm/call`, `(.f m args)`, `wasm/mem-*` |
+| Rust + wit-bindgen | records, lists, strings, results | `(:require ["x.wasm" :as x])` |
+| Go (`wasip1`) | argv, env, stdin, stdout, exit code | `wasm/run` |
+
+All of them build and run in [`docs/examples/polyglot`](./docs/examples/polyglot/README.md),
+rebuilt from source by CI on every push.
-## How it is put together
+## How it works
```mermaid
flowchart LR
@@ -175,65 +113,15 @@ flowchart LR
TW --> RT["runtime: NaN-boxed values, mark-sweep GC,
persistent collections, STM, agents, nREPL"]
VM --> RT
RT --> Z["zwasm engine
interpreter + JIT, fuel and memory budgets"]
- Z --> W[".wasm modules and WIT components
from Rust, Go, C, Zig"]
+ Z --> W[".wasm modules and WIT components
from C, Rust, Go, Zig"]
```
-Both backends evaluate the same analyzer output, and the test suite runs every
-case on both and fails on a mismatch. That differential oracle is how a
-from-scratch runtime keeps Clojure semantics honest without a reference
-implementation in the process. [`docs/architecture.md`](./docs/architecture.md)
+Two backends evaluate the same analyzer output, and every test runs on both:
+a mismatch fails the build. [`docs/architecture.md`](./docs/architecture.md)
has the map.
-## The Wasm FFI in one screen
-
-```clojure
-;; A core module (docs/examples/polyglot/c/kernel.c): numbers in, numbers
-;; out, arrays in a buffer the guest owns and hands out by address.
-(def m (wasm/load "docs/examples/polyglot/c/kernel.wasm" {:fuel 1000000 :max-memory-pages 16}))
-
-;; Next release: list the exports before calling any.
-(mapv (juxt :name :kind) (wasm/exports m))
-;=> [["memory" :memory] ["buf_addr" :func] ["buf_cap" :func] ["sum_f64" :func] ["scale_f64" :func]]
-
-(def buf (wasm/call m "buf_addr"))
-(wasm/mem-write! m :f64 buf [1.0 2.0 3.0 4.0])
-(wasm/call m "sum_f64" buf 4) ;=> 10.0
-(.sum_f64 m buf 4) ;=> 10.0, next release
-(wasm/mem-read m :f64 buf 4) ;=> [1.0 2.0 3.0 4.0]
-
-;; A WIT component: typed, so arguments and results are plain Clojure data.
-(def g "test/e2e/fixtures/wasm/greet_component.wasm")
-(wasm/component-exports g) ;=> [{:name "greet", :params [["name" "string"]], :result "string"}]
-(def c (wasm/load-component g {:fuel 100000}))
-(wasm/component-call c "greet" "zwasm") ;=> "Hello, zwasm!"
-
-;; A fault in the guest is a catchable Clojure exception, never a host crash.
-(try (wasm/call (wasm/load "docs/examples/wasm/trap.wasm") "boom")
- (catch Exception e (ex-message e)))
-;=> "wasm/call: 'boom' () -> (i32) trapped on the auto (JIT-first) engine
-; (divide-by-zero, out-of-bounds, unreachable, or a call shape the engine
-; cannot dispatch)"
-```
-
-Dot interop (ADR-0203, next release): on a module handle, `(.sum_f64 m buf 4)`,
-`(. m sum_f64 buf 4)` and `(. m (sum_f64 buf 4))` are `wasm/call` with the
-method name as the export name, verbatim, so results, fuel and errors are the
-same. It is cljw-only (use a `:cljw` reader conditional in a `.cljc`), and an
-export whose name is not a valid symbol still goes through `wasm/call`.
-
-Budgets: a missing axis keeps the engine's finite default (1e9 fuel, 4096
-pages), `0` removes a cap for a trusted guest, and fuel is per instance, so an
-exhausted handle stays exhausted until you load again. The engine is JIT by
-default for modules (`:engine :interp` to force the interpreter); components
-run on the interpreter. Element types for memory access are the guest's
-layout: `:i8 :u8 :i16 :u16 :i32 :u32 :i64 :f32 :f64`.
-
## Numbers
-Every figure below is generated from a committed measurement file under
-[`bench/`](./bench/README.md), not typed by hand, and the release size is
-checked against the built binary by a gate on every push.
-
```mermaid
xychart-beta
title "Binary size, MB (cljw 8.14 MB, others measured 2026-07-16)"
@@ -250,80 +138,82 @@ xychart-beta
bar [1.4, 3.2, 3.2, 10.0, 13.6, 18.8, 28.0, 33.7, 46.4, 231.9]
```
-Persistent collections trade wins with JVM Clojure op by op (20 000 ops at
-n = 16 000, Clojure 1.12.4): `assoc` on a map is 3.9× faster on cljw, `conj`
-on a vector 4.4× slower; `seq` / `rest` / `next` over a vector are O(1) views
-on both and cljw's are about 2× faster. The interpreter is where the JVM wins:
-an empty loop iteration costs ~45 ns here against ~2 ns under the JVM JIT, so
-tight numeric Clojure loops belong in a Wasm guest or on the JVM.
-[Measured, both sides.](./docs/works/collection_performance.md)
+- Persistent collections trade wins with JVM Clojure op by op: map `assoc`
+ 3.9× faster, vector `conj` 4.4× slower ([measured](./docs/works/collection_performance.md)).
+- Tight numeric loops are the JVM's: about 45 ns per empty iteration here
+ against 2 ns under its JIT. Put a hot kernel in a Wasm guest.
+- In a guest, the engine runs 2× to 3× behind wasmtime, and one call costs
+ about 1 µs ([bench](./bench/README.md)).
+
+The release size is checked against the built binary on every push.
+
+## Where it fits
+
+- **Untrusted code in-process.** Plugins, MCP tool servers, handlers that run
+ user code: a module sees only what you pass it, under a fuel and memory budget.
+- **Other languages' libraries.** Reach Rust, C, Go or Zig code through Wasm
+ instead of a JVM.
+- **Existing native tools.** `clojure.java.shell/sh` runs a binary by argv and
+ returns `{:exit :out :err}`, as on the JVM and babashka.
+- **Short-lived processes.** CLIs, scripts and serverless handlers that pay
+ for startup thousands of times a day.
+- **One program, two runtimes.** A host-free `.cljc` runs unchanged on the
+ JVM, on `cljw` and on its Rust-hosted sibling
+ [clojurust](https://github.com/BuddhiLW/clojurust) (`cljrs`).
-Inside a Wasm guest the embedded engine runs 2× to 3× behind wasmtime on
-compute-bound loops (fib: 1.29 s vs 0.49 s, same module, 2026-09-04) and one
-`wasm/call` crossing costs about 1 µs. The FFI is for keeping a hot kernel
-native, not for beating a standalone runtime;
-[the comparison is published as measured](./bench/README.md).
-
-## What runs
+```mermaid
+flowchart LR
+ P["demo.cljc
one Clojure program"] --> A["cljw
Wasm component, zero imports"]
+ P --> B["cljrs
native cdylib"]
+ A --> L[("arkworks Groth16
the same Rust crate")]
+ B --> L
+```
-`clojure.core` and the everyday standard library: `string`, `set`, `walk`,
-`zip`, `edn`, `data.json`, `data.csv`, `math`, `pprint`, `test`, `repl`,
-`template`, `tools.cli`, `spec`. STM, atoms, agents, futures, promises,
-watches, real threads. Lazy and chunked seqs, transducers, protocols, records,
-multimethods, `deftype` / `reify`, the full numeric tower (`Ratio`, `BigInt`,
-`BigDecimal`). A `deps.edn`-aware classpath with `:local/root` and `:git`
-coordinates, and a CIDER-compatible nREPL.
+| Groth16 over BN254, same crate | cljrs (native) | cljw (Wasm) |
+|---|---|---|
+| proof, verify | 128 bytes, true / false | 128 bytes, true / false |
+| wall time | 1.29 s (debug build) | 9.17 s |
+| env vars / file read / clock | 212 / granted / granted | 0 / denied / trap |
-Real libraries that load and run today (medley, tools.cli, data.json,
-core.cache, malli, hiccup, honeysql, test.check and more) are tracked in
-[`docs/works/ladder.md`](./docs/works/ladder.md).
+Confinement or speed, chosen per deployment, without a rewrite.
-`:mvn/version` coordinates load Clojure sources from JARs in
-`~/.m2/repository`, fetching missing ones from Clojars, then Maven Central;
-every fetched JAR and POM must match its SHA-1 sidecar or the load fails
-closed. `curl` and `unzip` must be on the host, and the
-[CHANGELOG](./CHANGELOG.md) entry for 1.15.0 lists the limits.
+## What runs
-```clojure
-{:deps {metosin/malli {:mvn/version "0.20.2"}}}
-```
+`clojure.core` and the everyday library (`string`, `set`, `walk`, `zip`,
+`edn`, `data.json`, `data.csv`, `pprint`, `test`, `spec`, `tools.cli` and more),
+STM, agents, futures, real threads, transducers, protocols, records,
+multimethods, `deftype` / `reify` and the full numeric tower. `deps.edn` with
+`:local/root`, `:git` and `:mvn/version` coordinates; libraries such as malli,
+hiccup, honeysql and test.check load today ([ladder](./docs/works/ladder.md)).
-What does not: `core.async` is not bundled, `java.time.format` is not
-implemented, and `clojure.edn/read-string` reuses the permissive Clojure
-reader, so some EDN-invalid input (such as `#(inc %)`) is accepted.
-`(clojure-version)` reports `"1.12.0"`, the semantics target. JVM interop that
-exists only to reach the JVM (`gen-class`, `proxy` over arbitrary classes,
-reflection, `import` of arbitrary Java classes) is out of scope by design. The
-full picture, including the intentional divergences, is
+Not yet: `core.async`, `java.time.format`, and a strict `clojure.edn` reader.
+JVM-only interop (`gen-class`, `proxy` over arbitrary classes, reflection) is
+out of scope by design. Details:
[`docs/clojure_vs_clojurewasm.md`](./docs/clojure_vs_clojurewasm.md).
## Documentation
-[`docs/`](./docs/README.md) is the index. The pieces most people want:
+[`docs/`](./docs/README.md) ·
+[CHANGELOG](./CHANGELOG.md) ·
+[vs JVM Clojure](./docs/clojure_vs_clojurewasm.md) ·
+[landscape](./docs/landscape.md) ·
+[testing](./docs/testing.md) ·
+[demos](./docs/works/demos.md) ·
+[roadmap](./.dev/ROADMAP.md)
-- [`CHANGELOG.md`](./CHANGELOG.md), every release, newest first.
-- [`docs/clojure_vs_clojurewasm.md`](./docs/clojure_vs_clojurewasm.md), what
- matches JVM Clojure, what deliberately does not, what is missing.
-- [`docs/landscape.md`](./docs/landscape.md), where cljw sits among Clojure
- runtimes, `cljrs` included.
-- [`docs/testing.md`](./docs/testing.md), the eight test layers and where a
- new test belongs. Read it before sending a patch.
-- [`docs/works/demos.md`](./docs/works/demos.md), two real applications
- (a browser playground and a bookshelf with SQLite as `sqlite3.wasm`) whose
- source still builds.
-- [`.dev/ROADMAP.md`](./.dev/ROADMAP.md), the plan.
+Issues and PRs are welcome at
+[BuddhiLW/ClojureWasm](https://github.com/BuddhiLW/ClojureWasm/issues); the most
+useful report is Clojure code that behaves differently here than on the JVM.
## License and credit
-Eclipse Public License 2.0, see [LICENSE](./LICENSE) and
-[NOTICE](./legal/NOTICE).
-
-ClojureWasm was created by [Shota Kudo (@chaploud)](https://github.com/chaploud),
-who took it from nothing to a working JVM-free Clojure runtime with a Wasm FFI
-through v1.10.1, and in 2026-10 handed the
-[clojurewasm](https://github.com/clojurewasm) organization over to
-[@BuddhiLW](https://github.com/BuddhiLW). The fork's work (v1.11 through
-v1.15.0) is merged back into
-[clojurewasm/ClojureWasm](https://github.com/clojurewasm/ClojureWasm), so the
-original repository is active again. Everything shipped since is built on
-that work. The embedded engine is [zwasm](https://github.com/zwasm/zwasm).
+Eclipse Public License 2.0 ([LICENSE](./LICENSE), [NOTICE](./legal/NOTICE)).
+
+Created by [Shota Kudo (@chaploud)](https://github.com/chaploud), who built it
+from nothing to a working JVM-free Clojure with a Wasm FFI through v1.10.1. In
+2026-10 the project passed to [@BuddhiLW](https://github.com/BuddhiLW); the
+fork's work (v1.11 to v1.15.0) is merged back into
+[clojurewasm/ClojureWasm](https://github.com/clojurewasm/ClojureWasm), while
+releases and the Homebrew tap publish from
+[BuddhiLW/ClojureWasm](https://github.com/BuddhiLW/ClojureWasm). The engine is
+[zwasm](https://github.com/zwasm/zwasm).
From 6b7f50dd83b307677c19a2379c15644605f4dd0d Mon Sep 17 00:00:00 2001
From: Pedro Gomes Branquinho
Date: Wed, 21 Oct 2026 16:07:41 -0300
Subject: [PATCH 13/17] fix(wasm): wasm/exports builds its result inside a
fabrication bracket
The partial result vector and each export map lived only in Zig locals
while the builder allocated, so a collection mid-build swept them: under
CLJW_GC_TORTURE_ALLOC=1 the call panicked with "@memcpy arguments alias",
and in a long suite run the vector came back as one map plus nil entries.
rt.gc.enterFabrication defers collection until the builder returns, the
same bracket every other multi-alloc builder uses (ADR-0150).
Smell-audited: 1: missing GC bracket in a new builtin, fixed with the existing idiom
---
src/runtime/cljw/wasm/exports.zig | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/src/runtime/cljw/wasm/exports.zig b/src/runtime/cljw/wasm/exports.zig
index 975eb3c42..322061da5 100644
--- a/src/runtime/cljw/wasm/exports.zig
+++ b/src/runtime/cljw/wasm/exports.zig
@@ -37,6 +37,11 @@ pub fn wasmExportsFn(rt: *Runtime, env: *Env, args: []const Value, loc: SourceLo
const params_kw = try keyword_mod.intern(rt, null, "params");
const results_kw = try keyword_mod.intern(rt, null, "results");
+ // The partial `out` vector and each `m` live only in Zig locals until
+ // return, so no collection may run while they are built.
+ rt.gc.enterFabrication();
+ defer rt.gc.exitFabrication();
+
var out = vector_mod.empty();
for (loaded.export_list.items) |item| {
var m = map_mod.empty();
From fd388b28d4e75e7ce385caae6a6c6dbccbb57da4 Mon Sep 17 00:00:00 2001
From: Pedro Gomes Branquinho
Date: Wed, 21 Oct 2026 16:07:41 -0300
Subject: [PATCH 14/17] chore(deps): pin zwasm to fork main d74e63b4 (upstream
main, 2026-10-21)
Fork main is fast-forwarded to upstream zwasm/zwasm main. Upstream now
carries all three fixes the fork's `cljw` branch existed for (851cd0e7
component re-export, f3067d03 wasip2 metadata-hash, 767b808a
PreparedWasi), so the pin leaves `cljw` for main. The version string is
still 2.7.0 and no newer tag exists; the pin returns to a tag when one is
cut.
Smell-audited: 0: dependency pin bump, no source change
---
build.zig.zon | 14 +++++++++++---
1 file changed, 11 insertions(+), 3 deletions(-)
diff --git a/build.zig.zon b/build.zig.zon
index 9b041366a..a05c8ca4b 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -14,7 +14,15 @@
// zwasm v2 embedding API (F-001 / D-037). `lazy` so the DEFAULT build +
// gate never resolve it — only `-Dwasm` (b.lazyDependency) does.
//
- // PINNED to the `cljw` branch of THIS FORK, BuddhiLW/zwasm
+ // 2026-10-21: PINNED to `main` of THIS FORK, BuddhiLW/zwasm, at
+ // d74e63b4 (fork main fast-forwarded to upstream zwasm/zwasm main,
+ // version string still 2.7.0, no newer tag). Upstream now carries all
+ // three cljw fixes listed below (851cd0e7 component re-export,
+ // f3067d03 metadata-hash, 767b808a PreparedWasi), so the `cljw`
+ // branch's patches are no longer needed. The pin returns to a
+ // zwasm/zwasm tag when the next one is cut.
+ //
+ // Before 2026-10-21: PINNED to the `cljw` branch of THIS FORK, BuddhiLW/zwasm
// (2026-09-14, user direction: "use our zwasm for now"). That branch
// is upstream v2.7.0 (d09d9248) plus two fixes found from cljw that
// upstream does not carry yet: every exported interface of a
@@ -60,8 +68,8 @@
// moment anything else claims that name. The `.hash` is
// content-addressed, so the host of the URL never changes it.
.zwasm = .{
- .url = "git+https://github.com/BuddhiLW/zwasm.git?ref=cljw#70c8b7ee60e66115e3bf1030e1d657f772e7b99f",
- .hash = "zwasm-2.7.0-FT1FvzKOnADpIAKlkOhKgEpJcG6LFoKrWsUAjd-jSCid",
+ .url = "git+https://github.com/BuddhiLW/zwasm.git?ref=main#d74e63b4aac1c7d392fc7e562467232945788dfe",
+ .hash = "zwasm-2.7.0-FT1Fv-zingBTL0it1Jtud_E9LvRZ6jkeZWH66zgwYpsw",
.lazy = true,
},
},
From cfa627094b98e20a3239130889c4f69349d1b53f Mon Sep 17 00:00:00 2001
From: Pedro Gomes Branquinho
Date: Wed, 21 Oct 2026 16:20:27 -0300
Subject: [PATCH 15/17] docs: BuddhiLW logo in the README header, changelog for
the next release
The header shows the ClojureWasm logo beside the maintainer's (linked to
github.com/BuddhiLW). [Unreleased] records wasm/exports and dot interop
(ADR-0203), the zwasm pin move to fork main d74e63b4, and the shorter
README; the README now says those features need v1.15.1, the patch the
release preflight cuts from 1.15.0.
---
CHANGELOG.md | 24 ++++++++++++++++++++++++
README.md | 8 +++++---
docs/assets/buddhilw_logo.png | Bin 0 -> 79816 bytes
3 files changed, 29 insertions(+), 3 deletions(-)
create mode 100644 docs/assets/buddhilw_logo.png
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d5bc55917..00746db19 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,30 @@ first stable `1.0.0` tag; pre-1.0 `alpha` / `rc` tags may still change surfaces.
## [Unreleased]
+### Added
+
+- **`wasm/exports` lists a core module's exports.** `(wasm/exports m)` returns
+ a vector in export order: a function as `{:name "sum_f64" :kind :func
+ :params [:i32 :i32] :results [:f64]}`, a memory, global or table as
+ `{:name "memory" :kind :memory}`. Signatures come from the same runtime probe
+ `wasm/call` uses (ADR-0203).
+
+- **Dot interop on a Wasm module handle.** `(.sum_f64 m buf 4)`,
+ `(. m sum_f64 buf 4)` and `(. m (sum_f64 buf 4))` call the export named by
+ the method symbol, verbatim, through `wasm/call`'s own path, so results,
+ fuel and errors are identical. A C function compiled to Wasm is called like a
+ host method (ADR-0203).
+
+### Changed
+
+- **zwasm pinned to the fork's `main` (d74e63b4, upstream main of
+ 2026-10-21).** Upstream now carries the component re-export, wasip2
+ `metadata-hash` and `PreparedWasi` fixes the fork's `cljw` branch existed
+ for.
+
+- **A shorter README** led by calling C from Clojure, with the architecture,
+ size and startup charts kept.
+
## [1.15.0] - 2026-10-13
### Added
diff --git a/README.md b/README.md
index 255dc6e3d..dd7e248a4 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
-
+
+
+
ClojureWasm
@@ -79,8 +81,8 @@ zig cc -target wasm32-freestanding -O2 -nostdlib \
(.sum_f64 k buf 4) ;=> 10.0
```
-`wasm/exports` and the dot form arrive in the next release; on the current
-one, write `(wasm/call k "sum_f64" buf 4)`. A guest fault is a catchable
+`wasm/exports` and the dot form need v1.15.1 or later; `(wasm/call k "sum_f64"
+buf 4)` works on every version. A guest fault is a catchable
exception, never a host crash, and a guest that runs out of fuel stops with an
error while your process keeps going.
diff --git a/docs/assets/buddhilw_logo.png b/docs/assets/buddhilw_logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..3c9be0bc83057669b1c4a4b1ebec11349de9ac5f
GIT binary patch
literal 79816
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