Monomorphization ABI Guide

This guide explains the codegen ABI that Turmeric settled on after the end-to-end monomorphization migration (concluded 2026-06-19). It is aimed at two audiences:

If you only ever read Turmeric source and never write inline-C, you can get away without this guide -- the language surface hides the ABI. You'll start needing it the first time you see a symbol like option_map__spec__int_Option__cstr in a link error.

TL;DR

Why monomorphization, not a uniform carrier

The pre-2026 ABI threaded every polymorphic value as int64_t -- a "carrier" wide enough to hold any pointer or primitive, with implicit boxing/unboxing at type boundaries. The carrier worked, but each type-system feature shipped that year (HKTs, sized types, substructural caps, refinement types, associated types) ended in an ABI-patch bug report:

The pattern: a value would be classified as concrete (Pos, double, Option<int>) in one phase and as a carrier (int64_t) in the next, and a cast was missing at the boundary. Patches stacked up in elaborator and codegen until the cumulative cost motivated picking one ABI: by-value end-to-end.

The win:

The cost: more code generated. A (option-map f xs) used five places in your project, each with different element types, lowers to five specs (option_map__spec__int, option_map__spec__float, option_map__spec__Pos, ...). This is the same trade Rust makes; in practice the cost is fine because (a) most call sites share types, (b) dead-spec elimination prunes unused ones, and (c) the binary stays small because each spec is tiny.

What gets monomorphized

Construct Spec naming Example
Polymorphic defn <defn>__spec__<arg-types> option_map__spec__int_int
Typeclass method via dict <class>_<method>__spec__<dict> Functor_fmap__spec__Option
Constrained-polymorphic HOF <defn>__spec__<arg-types>_<dict> bind__spec__int_Option_Result
#fx{Construct} polymorphic ctor <ctor>__spec__<carrier-args> some__spec__int
HKT instance method (by-value) <class>_<method>__spec__<F>__<element-types> Functor_fmap__spec__Option__int

The spec name is deterministic: same call-site types → same spec name → no symbol collisions across separate compilation units. If two modules instantiate option-map with int they refer to the same emitted symbol; only one TU actually emits the body (the rest declare extern).

How to read a spec symbol in an error message

Most ABI-flavored errors look like one of:

undefined reference to `option_map__spec__int_Option__cstr'
multiple definition of `some___spec__bool_Option__opaque'

Parse it:

  1. The first segment up to __spec__ is the originating defn/method/ctor.
  2. Everything after is the argument-type signature, in elaboration order, with double-underscore separators. Type applications use F__A -- Option__cstr means (Option cstr).

some___spec__bool_Option__opaque = the some constructor specialized for the type (Option (Option <opaque>)) (note the triple underscore distinguishing constructor-name from method-name).

If you see one of these in a link error, the usual causes are:

The residual carrier bridge -- what it does, why it stays

Phase 5 of the migration set out to delete the carrier bridge entirely. As of 0.22.0 (#444), two classes of crossings remain -- either intentional, or load-bearing in a way that no method-level fix can address:

  1. Consumer-side bridges, by design. Some inline-C extractors take an int64_t argument and reinterpret. The audit fixture coverage requires this to stay working so the bridge is regression- tested. Removing it removes the test, not a bug.
  2. HOF return-type closures. Monad bind / Applicative ap take a continuation (fn [A] (m B)) whose result is an HKT-applied type. Threading the result by-value end-to-end requires retyping the parser/combinator library (Parser A and its continuations) plus propagating generic-dispatch type information through the entire call graph. That's a parser-rewrite unit of work, not a localized ABI fix. Until it lands, the closure's return value rides the carrier and a tiny spill shim boxes it. 0.22.0 #438 narrowed this considerably -- Applicative ap now preserves fn type through polymorphic constructors -- but the residual continuation case still rides the bridge.

The Vec and MutableMap element bridges that previously sat in this section are gone: 0.21.0 #377 migrated Vec to the :heap typed-pointer ABI (retiring carrier-based Eq[Vec]); 0.22.0 #396 retyped MutableMap to the honest (MutableMap K V) and retired its carrier bridge; 0.22.0 #391/#393 monomorphized the Vec inline-C producers to typed pointers.

The bridge is now a documented small surface, not an open migration. Look for tur_box_T / tur_unbox_T helpers in the runtime and the ensure_aggregate_spill_shim codegen helper for the spill path.

Practical impact on day-to-day Turmeric code

You should mostly not notice the ABI. The places it surfaces:

Inline-C blocks see real C types

A defn with #fx{Unsafe} and an inline-C body sees the parameter types as their natural C layout:

(defn add-pos [a : Pos b : Pos] #{Unsafe} : Pos
  ```c
  Pos out;
  out.x = a.x + b.x;
  out.y = a.y + b.y;
  return out;
  ```)

Before monomorphization landed, you would have had to cast a and b from int64_t back to Pos* and dereference. That style is no longer needed (and is wrong now -- a and b are passed by value).

The carrier-bridge corners (HKT continuation results, Vec/Map element reinterprets) are the only places you still write (int64_t)(intptr_t) casts. If you find yourself reaching for one outside those cases, the typechecker probably wants a real type.

FFI / C-ABI callbacks

When you hand a Turmeric function pointer to a C library (qsort comparators, Arrow release callbacks, signal handlers, raylib draw callbacks), the mangled spec symbol is what the C side takes. Mark the defn #[used] (shipped 0.22.0, #467) so the compiler keeps external linkage and doesn't DCE the body:

(defn #[used] compare-ints [a : ptr<void> b : ptr<void>] : int
  ```c
  int ai = *(const int*)a;
  int bi = *(const int*)b;
  return (ai > bi) - (ai < bi);
  ```)

Without #[used], the symbol is static in the emitted C and the extern C reference fails at link. With it, external linkage is preserved and every project module compiles and links.

Separate compilation

Each module compiles independently. Spec emission rules:

If you're maintaining the codegen, the rule of thumb is: the owning module is whoever declared the originating defn/instance. If that module is the prelude, fall back to static.

Compiler-contributor cheat sheet

Where to look next