^fat)^fat is a one-token type annotation that controls how the compiler
represents a function value at a specific source position. It is
purely a representation hint — it does not change runtime semantics,
arity, or signature compatibility. But omitting it where it belongs
causes a SEGV at the call site, so it is worth understanding.
This guide explains the two closure representations Turmeric uses, why
the language can't always pick the right one automatically, what
^fat does at parameter and return positions, and when you need to
reach for it.
A function value in Turmeric compiles to one of two ABIs:
| Representation | Layout | Call site |
|---|---|---|
| Bare fn-pointer | int64_t (*)(int64_t...) — a raw C function pointer |
f(arg) |
| Fat closure | Heap int64_t[] — slot 0 is a thunk pointer, slots 1..N are captured free vars |
apply-fat(f, arg) / TUR_APPLY1(fat, arg) |
The compiler picks the bare representation whenever a (fn ...) body
captures no free variables, because there is nothing to put in
slots 1..N and a bare pointer is cheaper than a heap allocation. A
(fn ...) that captures one or more enclosing locals is always fat.
This is a real type-system distinction, tracked by arg_fat[] and
result_fat bits on the function Type. A captureless fn and a
captureful fn are not interchangeable.
apply-fat (and its TUR_APPLY1/TUR_APPLY2/... macros) load the
thunk from slot 0 of a fat closure and pass the closure itself as the
environment pointer. Feeding a bare fn-pointer to apply-fat
reads the first instruction byte of the function as a "thunk address"
and segfaults.
Any combinator that dispatches its callback through apply-fat is
therefore a fat-expecting sink:
(defn bind-parser [p f] : ptr<void>
;; ... eventually calls (apply-fat f x) ...
...)
If a caller passes a captureless lambda here:
(bind-parser p (fn [x] (mreturn (transform x)))) ;; captures nothing
…the lambda lowers to a bare fn-pointer, apply-fat reads it as a fat
closure, and the program crashes. Historically the workaround was to
force a capture by hand:
(bind-parser p
(let [sentinel 0]
(fn [x] (let [_ sentinel] ;; force a capture
(mreturn (transform x))))))
That workaround is obsolete. The replacement is ^fat.
^fat on a parameterAnnotating a function-typed parameter ^fat declares that the body
calls it through the fat-closure ABI. The elaborator then auto-shims
any captureless fn passed at the call site, boxing it into a
one-cell fat closure ({ __tur_fatshim<arity>, orig_fn }) before the
call.
(defn bind-parser [p ^fat f] : ptr<void>
;; (apply-fat f x) is now safe for any caller
...)
fn argument is already fat and passes through unchanged.extern-c C function flowing in is also auto-shimmed.:int/:ptr<void> arguments are unaffected — the marker only
fires on function-typed parameters.Reach for ^fat on a parameter whenever your function body invokes the
parameter via apply-fat, TUR_APPLY1, or any other fat-ABI form.
^fat parameters inside inline-CSince #286, a ^fat function-typed parameter is materialised in the
generated C signature as plain int64_t -- the unified closure handle
(a pointer to the fat box, or, for the bare-shimmed case, a one-cell box
whose slot 0 is the original fn-pointer). That means you can name it
directly inside an inline-C body and feed it back to the TUR_APPLY*
macros:
(defn run-once [^fat f x : int] : int
```c
/* f is int64_t at the C level; pass it through TUR_APPLY1
to invoke the closure under the unified ABI. */
return (int64_t)TUR_APPLY1(f, x);
```)
The same rule applies in either direction: a Turmeric global declared
^fat is callable from inline-C as int64_t, and an inline-C function
that wants to receive a closure handle from Turmeric should take it as
int64_t. Do not spell it as void * -- inline-C ascription is scanned
for captures (#264) and the carrier type is what the codegen agrees on.
^fat on a return typeThe parameter form only fires at call-argument sites. A different situation arises when a factory function returns a fat closure built from a captureless inner lambda:
(defn pfail [] ^fat :ptr<void> ;; <- ^fat on the return
(fn [inp] (pfail-impl inp))) ;; inner lambda captures nothing
Without ^fat on the return type, the inner (fn ...) lowers to a
bare pointer and any downstream apply-fat on the returned value
segfaults. ^fat on the return tells the compiler to box the
captureless tail lambda into a one-cell fat closure before returning.
A capturing tail lambda or a forwarded value that is already fat
passes through unchanged — the shim only fires when the tail is a
bare TY_FN.
Reach for ^fat on a return type whenever your function constructs a
function value that downstream code will fat-call. In a parser-
combinator library, that is essentially every public constructor
(pfail, item, pure, or-parser, the *-ref thunks for
mutually recursive non-terminals).
^fat is int-carrier onlyThere are two ways to spell a ^fat parameter, and they differ in what
they know about the closure's result type:
(defn run-with [^fat g x : int] : int (g x)) ;; bare
(defn run-with [^fat g :(fn [float] #{} float) x : float] : float (g x)) ;; annotated
Under the unified closure representation (#276 and the typed-invocation
refinements that followed: #283/#285/#287/#292/#293/9588cda7), every
closure value -- bare or fat, captureless or capturing -- is carried as a
single int64_t handle. The remaining distinction is what the call site
knows about the result type at the point where it dispatches the handle.
A bare ^fat g records no result signature, so a direct call (g x)
falls back to the int-carrier dispatch path: the result is read out of the
integer return register as an int64_t. That is correct for the int-carrier
generic combinators (>>>, option-map, and friends), where every value
is already an int64 at runtime.
But a closure that returns a non-int register class -- :float is the
one that bites -- cannot go through the bare form unless the tail-position
inference (below) can recover the type. A double is returned in a
floating-point register (xmm0), while the bare-^fat int-carrier dispatch
reads the integer register (rax). The bits do not line up, and you get
garbage. (A :cstr or :ptr<void> result happens to share the integer
register, so it round-trips -- but do not rely on that; annotate non-int
results. For the integer/pointer carrier specifically, the SF-application
call sites now bridge int <-> ptr<void> (9588cda7), so mixing the two
no longer requires a manual (::) cast at the boundary.)
For any non-int result, use the annotated form -- it carries the result type, and the compiler dispatches through a typed thunk that uses the right register:
(defn run-with [^fat g :(fn [float] #{} float) x : float] : float
(g x)) ;; returns a real double
As of the tail-position retype pass (shipped #208), a bare ^fat g in the
result tail of a :float function infers the result type from the
declared return -- no annotation required. The one case still left to the
annotated form is a bare-^fat non-int result consumed in a non-tail
position; that is tracked in
docs/upcoming/v1/bare-fat-result-monomorphization-plan.md (deferred until
a real consumer exists). The annotated form remains the checked path in
all positions.
^fatf(arg) syntax (direct
invocation, not apply-fat). The compiler dispatches against the
known representation.int64_t payload.extern-c declaration and never
flows through a fat-ABI sink.If unsure: try without ^fat first; if the program SEGVs at an
apply-fat/TUR_APPLY1 site, the missing annotation is almost
certainly the cause.
The auto-shim ships with __tur_fatshim0 through __tur_fatshim5,
covering 0–5 argument lambdas. Higher arities are rejected at
elaboration time. In practice every combinator in the stdlib that
uses ^fat is 1- or 2-ary, so this rarely binds.
^fat is independent of #fx{Unsafe}, :linear, and effect rows.
It is a representation marker, not a discipline or capability.^fat on a parameter and ^fat on the return type compose: a
factory that takes a captureless callback and returns a captureless
fat closure marks both positions.^fat only changes how the
compiler decides to produce a fat vs. bare value at the marked
position.| Situation | Annotation |
|---|---|
Your function calls a callback via apply-fat / TUR_APPLY1 |
[cb ^fat fn-type] on the parameter |
The callback returns a non-int type (:float, ...) |
[cb ^fat :(fn [argtypes] #fx{} :RetType)] -- annotate so the result type is threaded |
| The callback is int-carrier (generic combinator) | bare [cb ^fat] is fine |
Your function returns a (fn ...) that downstream code will fat-call |
^fat :ptr<void> on the return type |
| Both of the above | Mark both positions |
| Pure direct invocation only | No annotation needed |
For a worked example end-to-end, see
parser-combinators-tutorial.md
sections 4, 5, and 8. The historical design rationale lives in
docs/archive/history/captureless-lambda-abi-plan.md and
docs/archive/history/fat-closure-return-position-plan.md.