Date: 2026-07-03
ADR 39 carved a hot path for scalar-only signatures: no confined arena,
no per-arg marshalling map, just a thread-local carrier array filled with
to-carrier and a from-return read-back. A signature with any non-scalar
param or return -- including an enum or a [:handle Type] return -- took
the general arena-backed path, which allocates a mapv of marshalled
maps, a mapcat :carriers lazy seq, a copy-back! closure, and a
concat+object-array per invoke-* helper.
But an enum crosses the C ABI as its backing scalar (i32 by default),
and a handle return is a pointer with no out-seg. A signature like
[s Suit :ret Suit] or [v :i64 :ret [:handle Box]] lowers to the same
(int|long) -> (int|long|ptr) ABI the scalar hot path already serves. The
bench measured the cost of sending them through the general path:
:enum ran at ~965 ns per call against a ~55 ns floor (a ~17x overhead),
and :handle ran at ~1716 ns against a ~120 ns floor. The scalar
hot path (:scalar-passthrough) ran at ~199 ns. The gap was the
general-path bookkeeping applied to shapes that did not need it.
The keyword-to-backing-int and int-to-keyword translations an enum adds
are O(1) map lookups through the cached enum-index. The Handle record
wrap a handle return adds is one record allocation. Both are cheap to
fold into the scalar hot path's carrier-array choreography.
bind detects an enum-aware signature -- every param a plain scalar or
a named enum, the return a plain scalar, a named enum, or a [:handle Type], and the signature not pure-scalar -- and returns a third invoker
alongside the scalar and general ones. The enum-aware invoker:
arity, one slot per
param's backing scalar.to-carrier for a scalar, the cached enum-member->value lookup plus
backing-scalar to-carrier for an enum.from-return for a
scalar, enum-value->member for an enum, the ->Handle wrap for a
[:handle Type] (nil for a null pointer).The predicate enum-aware-scalar? is true only when scalar-only? is
false, so the scalar hot path stays single-shape and the enum-aware path
is reached only for sigs that mix in an enum or carry a handle return.
An enum-only, enum-and-scalar, or scalar-args/handle-return signature
called in a loop no longer allocates and closes a confined arena per
call, nor builds the per-arg marshalling maps. The bench measures
:enum at ~185 ns per call against a ~60 ns floor (parity with the
scalar hot path's shape), and :handle at ~330 ns against a ~120 ns
floor (the residual is the body's c_allocator.create plus the Handle
record allocation).
The hot path reuses the existing enum cache, to-carrier, and
from-return, so its results are identical to the general path by
construction; the existing enum round-trip, handle round-trip, and
volume suites cover it, plus an enum-aware-selects-the-enum-path arm
and a thread-safety arm mirroring the scalar hot path's.
Leave enum and handle on the general path. Rejected: it is measurable, avoidable overhead on signature shapes that are, at the C ABI, indistinguishable from a scalar (enum) or a plain pointer (handle).
Extend the scalar hot path to inline enum and handle coercion. Rejected: keeping the scalar hot path single-shape preserves its single-type fast path and avoids per-arg type dispatch in the tightest loop. The enum-aware invoker carries its own per-arg closures, dispatch-free per call.
ADR 39's "Any signature with a slice, pointer, array, struct, enum,
handle, optional, or an error-union/owned/struct return keeps the
general arena-backed path unchanged" no longer holds for enum (args or
return) or for a [:handle Type] return with scalar/enum args: those
signatures now take this hot path. ADR 39's scalar-only predicate and
invoker are unchanged; this ADR adds a sibling alongside.
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 |