Date: 2026-06-16
The interface design names defenumz and shows
(defenumz ParseStatus [ok 0 invalid 1 eof 2]), but it leaves the
Clojure representation, the backing integer type, and the handling of an
unmapped value open. An enum bridge has to fix all three before it can
carry a value across the edge.
(defenumz Name [member value ...]) declares a named boundary type, a
Zig enum(i32) with the given members and values, registered in the
namespace's named-type registry beside deftypez and defrecordz. The
generated Zig declares const Name = enum(i32) { ok = 0, ... }; in the
preamble, and the user body uses the enum directly, both switch (s) { .ok => ... } and return .eof;.
An enum value is a Clojure keyword named for the member, :ok,
:invalid, :eof, not a raw integer. It crosses the C ABI as its
backing i32, which a raw FFM call confirms passes as a plain JAVA_INT
in both argument and return position; the export fn carries the enum
type and Zig lowers it to the int, so no inner function or out-parameter
is needed.
On the Clojure side an argument keyword maps to its integer value through
the member table, and a keyword that names no member is rejected at call
time with :clj-zig/unknown-enum-member. A return integer maps back to its
member keyword; an integer with no matching member returns as the raw
integer, total and lossless. Both positions are supported because an enum
is scalar-sized.
An enum reads as an ordinary Clojure keyword on both sides, and the Zig
body keeps the enum's exhaustiveness and switch checking. The bridge is
simpler than the struct path: a scalar carrier, no out-parameter. The
cost is that the keyword set lives in the defenumz form rather than in
the type system, so a member rename is a source change in two languages.
The backing is configurable (amendment, 2026-07-01): an optional options
map may carry :backing to widen or narrow the tag, defaulting to
:i32. A C library's u8, u16, or u32 tags now declare directly
({:backing :u8}). The carrier dispatch reads the backing width, so the
FFM layout and the argument coercion follow: a :u8-backed enum crosses
as JAVA_BYTE. A member value that does not fit the backing's range is
rejected at declaration time with :clj-zig/enum-value-overflow; a
non-integer or carrierless backing with :clj-zig/bad-enum-backing. The
backing enters the layout descriptor and therefore the content hash, so
redefining an enum with a different backing recompiles its callers.
Carrying values as raw integers was rejected: the keyword preserves the
member name, which is the point of the bridge. Symbols were rejected
because a keyword is the Clojure idiom for an enumerated tag. Throwing on
an unmapped return value was rejected in favor of returning the raw
integer, which keeps the return total; an unknown argument keyword still
throws, because that is a caller error like a wrong arity or a missing
struct field. Lowering the enum to its backing int in the export
signature with @enumFromInt/@intFromEnum, mirroring the
struct-by-pointer scheme, was also considered. Raw FFM showed enum(i32)
is already ABI-stable as a plain int, so the indirection is unnecessary.
A slice or array of enums crosses as a slab of backing integers
(amendment, 2026-07-02). The element gate accepts a named-enum element
for both argument and return positions. The marshaller maps each keyword
to its backing int via enum-member->value and writes the int slab; the
reader bulk-copies the backing ints and maps each through
enum-value->member. A :const qualifier is required for an
enum-element slice argument (same discipline as a struct-element slice:
the caller's immutable keyword sequence has no mutable container for
copy-back). An owned enum-slice return uses the simple slab free (ints,
no buffers).
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |