Sized Types in Turmeric

Sized types track the size of data structures at compile time. This enables memory layout verification, stack allocation of fixed-size buffers, type-safe array operations, and FFI structs that carry size annotations through Turmeric wrappers.

Sized types are enabled by default; the layer is built on the GADT machinery.

Static checking. Size indices are lifted to the type level: a length-n vector's type mentions n, and a dimension mismatch whose sizes are statically known is a compile-time error (TUR-E0260), not just a runtime assertion. This covers the cases that matter in practice:

  • Cross-parameter unification. A signature that names the same size variable in two or more parameters -- e.g. (defn pairwise-sum [xs : (SizedVec n) ys : (SizedVec n)] ...) -- unifies n across all the slots. Passing a length-2 vector for xs and a length-3 vector for ys is rejected at compile time. Multi-index carriers ((LaMatN m n)) unify per-index position.
  • Propagation through wrappers. A defstruct/defopaque field whose type carries a size index keeps it: projecting (.fst p) and (.snd p) into a shared-n callee recovers each field's static size and rejects a mismatch.
  • Polymorphic helpers. A function over (SizedVec n) whose body re-wraps or threads the value preserves the index without per-call re-annotation.

Where both sides of a comparison fold to a known constant, the check fires statically. The runtime predicates/assertions (size-eq?, size-compat?, size-assert-eq!, sized-matrix-assert-shape!) remain the fall-through for the genuinely dynamic cases: a size that is only known at run time (read from input, computed by non-constant arithmetic) or an open size expression with free variables that cannot be folded. Size arithmetic on indices (+, * for concat-typed vectors) is also a runtime check today. The historical roll-out is recorded in the archived sized-types-completion-plan.md (phases SZ6–SZ9) and sized-types-cross-param-unification-plan.md.

Table of Contents

  1. Size Foundations
  2. StaticInt
  3. Size expressions
  4. SizedVec
  5. Size Predicates and Assertions
  6. Size Inference
  7. Flat Buffers and Memory Layout
  8. Heap allocation
  9. Stack allocation
  10. Allocation dispatch
  11. SizedMatrix
  12. SizedBitVec
  13. FFI Integration
  14. Error Messages
  15. API Reference

Size Foundations

StaticInt

StaticInt is a compile-time integer annotation. It carries an integer value intended for use as a type-level literal.

(import stdlib/sized)

(let [si (static-int 10)]
  (println (static-int-val si)))   ; => 10

Arithmetic on StaticInt values:

(let [a (static-int 3)
      b (static-int 4)]
  (println (static-int-val (static-int-add a b)))   ; => 7
  (println (static-int-val (static-int-mul a b))))  ; => 12

Size expressions

Size is a GADT that represents arithmetic expressions over compile-time sizes. Its constructors are:

Constructor Meaning
(Static n) Literal size n
(Add s1 s2) Sum of two sizes
(Mul s1 s2) Product of two sizes

Use size-static, size-add, and size-mul to build Size expressions, and size-eval to reduce them to a runtime integer:

(let [s (size-add (size-static 3) (size-mul (size-static 2) (size-static 5)))]
  (println (size-eval s)))   ; => 13

size-normalize flattens a Size expression to (Static n):

(let [s (size-normalize (size-add (size-static 4) (size-static 6)))]
  (println (size-eval s)))   ; => 10

size-simplify folds constant sub-expressions and collapses identities such as 0 + s = s and 1 * s = s:

(let [s (size-simplify (size-mul (size-static 1) (size-add (size-static 2) (size-static 3))))]
  (println (size-eval s)))   ; => 5

SizedVec

SizedVec is a linked-list collection that carries a Size annotation. It is primarily useful when you need a simple sized container and do not require flat memory layout (see Flat Buffers for that).

(let [v (sized-vec-of-3 10 20 30)]
  (println (sized-vec-len v))       ; => 3
  (println (sized-vec-get v 1)))    ; => 20

sized-vec-push prepends an element and returns a new SizedVec whose size is n + 1:

(let [v  (sized-vec-of-2 1 2)
      v2 (sized-vec-push v 0)]
  (println (sized-vec-len v2)))   ; => 3

Size Predicates and Assertions

Five predicates compare two Size expressions at runtime:

(size-eq? (size-static 4) (size-add (size-static 2) (size-static 2)))   ; => true
(size-lt? (size-static 3) (size-static 4))                               ; => true
(size-le? (size-static 4) (size-static 4))                               ; => true
(size-gt? (size-static 5) (size-static 3))                               ; => true
(size-ge? (size-static 4) (size-static 4))                               ; => true

size-compat? is the runtime analog of the subtyping rule -- two sizes are compatible when they evaluate to the same integer:

(size-compat? (size-static 8) (size-mul (size-static 2) (size-static 4)))
; => true

Two assertion functions panic with a descriptive message when the condition fails:

; passes silently
(size-assert-eq! (size-static 4) (size-add (size-static 2) (size-static 2)))

; passes silently (3 <= 10)
(size-assert-le! (size-static 3) (size-static 10))

; panics: "sized type mismatch"
(size-assert-eq! (size-static 4) (size-static 5))

Static checking (SZ7)

size-assert-eq! and size-assert-le! are checked statically when both of their size arguments reduce to compile-time constants -- the Size expression is folded at elaboration and compared:

; COMPILE-TIME error TUR-E0260 (no runtime check is emitted):
(size-assert-eq! (size-static 4) (size-static 5))

; accepted at compile time -- both sides fold to 4:
(size-assert-eq! (size-static 4) (size-add (size-static 2) (size-static 2)))

Run tur explain TUR-E0260 for the full diagnostic.

Runtime fallback (the boundary). When at least one size is not statically known -- for example a dimension derived from a runtime length or passed through a function parameter -- the checker cannot decide the equation and lowers to the existing runtime assertion shown above. It never silently accepts a possibly-wrong size:

(let [n (read-length)]              ; n is a runtime value
  ; not statically known -> runtime assertion (panics if n != 4)
  (size-assert-eq! (size-static n) (size-static 4)))

Folding covers Static/Add/Mul (and the size-static/size-add/ size-mul value forms) at the assertion call site itself. Sizes that arrive through a wrapper function still fall back to the runtime check for the assertion forms above; the type-index path (below) does propagate through wrappers, so the more common dimension checks are caught statically.

Cross-parameter unification and index propagation

Beyond the assertion forms, the elaborator unifies size indices carried in parameter and field types:

; both parameters share `n`; a 2-vs-3 call is a COMPILE-TIME TUR-E0260:
(defn pairwise-sum [xs : (SizedVec n) ys : (SizedVec n)] : int ...)

(pairwise-sum (SVCons 1 (SVCons 2 (SVNil)))            ; n := 2
              (SVCons 10 (SVCons 20 (SVCons 30 (SVNil)))))  ; n := 3 -> TUR-E0260

The same unification recovers indices across multi-index carriers ((LaMatN m n) -- the shared inner dimension of a matrix multiply), through defstruct/defopaque field projections ((.fst p) / (.snd p) into a shared-n callee), and through length-polymorphic helpers whose body re-wraps or threads the value. None of these require per-call re-annotation. The fixtures under tests/fixtures/sized-* exercise each case (accept + reject).

The boundary is the same as for the assertion forms: an index that is only known at run time, or an open size expression with free variables that cannot be folded to a constant, stays polymorphic and falls through to the runtime predicates.


Size Inference

When the number of elements is known at the call site, use the fixed-arity constructors to let the compiler infer the size:

(let [v1 (sized-vec-of-1 42)          ; size inferred as 1
      v2 (sized-vec-of-2 1 2)         ; size inferred as 2
      v3 (sized-vec-of-3 1 2 3)       ; size inferred as 3
      v4 (sized-vec-of-4 1 2 3 4)]    ; size inferred as 4
  (println (sized-vec-len v3)))       ; => 3

When the size is determined at runtime (e.g. from a list value), use sized-vec-from-list:

(let [lst (list 10 20 30)]
  (let [v (unsafe (sized-vec-from-list lst))]
    (println (sized-vec-len v))))   ; => 3

sized-vec-from-list requires #fx{Unsafe} because it casts the cons cell layout directly.

Type-level index inference (SZ8)

When a sized GADT carries a type-level size index, the index of a constructed value is inferred automatically by threading operand indices through the constructors -- no annotation is needed:

(defgadt SizedVec [n]
  (SVNil : (SizedVec (Static 0)))
  (SVCons int (SizedVec n) : (SizedVec (Add (Static 1) n))))

; (SVCons _ (SVCons _ (SVNil))) infers (SizedVec 2)

Pass --dump-sizes to print the inferred index for each constructor application:

size: SVNil  : (SizedVec 0)
size: SVCons : (SizedVec 1)
size: SVCons : (SizedVec 2)

A SizedVec parameter written without an index (v : SizedVec) is length-polymorphic: it elaborates against a fresh size variable and accepts any length. Inference covers literal constructor chains and linear Add/Mul over Static and one variable; an index that depends on an unknown operand (a parameter, a runtime-built vector) is left polymorphic -- --dump-sizes shows it as (SizedVec ?) -- rather than being guessed. See sized-types-index-spec.md section 6 for the full inference boundary.


Flat Buffers and Memory Layout

SizedBuf stores elements in a contiguous int64_t *data array. This is more cache-friendly than SizedVec (which allocates one struct per element) and is the right choice for numerical work, bulk operations, or FFI.

C struct layout:

struct { int64_t len; int64_t *data; }

All SizedBuf operations require #fx{Unsafe} at the call site.

Heap allocation

(import stdlib/sized-buf)

(let [b (unsafe (sized-buf-new-zeroed 4))]
  (unsafe (sized-buf-set! b 0 10))
  (unsafe (sized-buf-set! b 1 20))
  (unsafe (sized-buf-set! b 2 30))
  (unsafe (sized-buf-set! b 3 40))
  (println (unsafe (sized-buf-get  b 2)))    ; => 30
  (println (unsafe (sized-buf-sum  b)))      ; => 100
  (println (unsafe (sized-buf-min  b)))      ; => 10
  (println (unsafe (sized-buf-max  b)))      ; => 40
  (unsafe (sized-buf-free b)))

sized-buf-fill! sets every element to the same value:

(let [b (unsafe (sized-buf-new 8))]
  (unsafe (sized-buf-fill! b 7))
  (println (unsafe (sized-buf-sum b)))   ; => 56
  (unsafe (sized-buf-free b)))

sized-buf-copy! copies all elements from one buffer into another (both must have the same length):

(let [src (unsafe (sized-buf-new-zeroed 3))
      dst (unsafe (sized-buf-new-zeroed 3))]
  (unsafe (sized-buf-set! src 0 1))
  (unsafe (sized-buf-set! src 1 2))
  (unsafe (sized-buf-set! src 2 3))
  (unsafe (sized-buf-copy! src dst))
  (println (unsafe (sized-buf-get dst 1)))   ; => 2
  (unsafe (sized-buf-free src))
  (unsafe (sized-buf-free dst)))

Stack allocation

sized-buf-with-stack allocates n elements on the stack via alloca and calls a non-capturing function with the buffer. No malloc/free overhead; the buffer is reclaimed automatically when the enclosing function returns.

(defn fill-and-sum [b] : int
  (unsafe (sized-buf-set! b 0 10))
  (unsafe (sized-buf-set! b 1 20))
  (unsafe (sized-buf-set! b 2 30))
  (unsafe (sized-buf-sum b)))

(println (unsafe (sized-buf-with-stack 3 fill-and-sum)))   ; => 60

The callback must be a named function (not an anonymous lambda) because alloca memory must not escape the frame.

Allocation dispatch

sized-buf-compute demonstrates the dispatch strategy that your own code can mirror: use the stack when the size is small and known, fall back to the heap otherwise.

; n <= 64: uses alloca (stack)
(println (unsafe (sized-buf-compute 10)))    ; => 45  (0+1+...+9)

; n > 64: uses malloc (heap)
(println (unsafe (sized-buf-compute 100)))   ; => 4950

sized-buf-size returns the element count as a Size expression so you can pass it to size predicates and assertions:

(let [b (unsafe (sized-buf-new 6))]
  (println (size-eval (unsafe (sized-buf-size b))))   ; => 6
  (unsafe (sized-buf-free b)))

Converting a SizedVec to a flat SizedBuf:

(let [v (sized-vec-of-3 1 2 3)
      b (unsafe (sized-buf-from-sized-vec v))]
  (println (unsafe (sized-buf-sum b)))   ; => 6
  (unsafe (sized-buf-free b)))

SizedMatrix

SizedMatrix is a 2-D flat row-major matrix.

C struct layout:

struct { int64_t rows; int64_t cols; int64_t *data; }

All operations require #fx{Unsafe}.

(import stdlib/sized-matrix)

(let [m (unsafe (sized-matrix-new-zeroed 3 4))]
  ; Shape
  (println (unsafe (sized-matrix-rows m)))    ; => 3
  (println (unsafe (sized-matrix-cols m)))    ; => 4
  (println (size-eval (unsafe (sized-matrix-size m))))   ; => 12

  ; Element access
  (unsafe (sized-matrix-set! m 0 0 1))
  (unsafe (sized-matrix-set! m 0 1 2))
  (unsafe (sized-matrix-set! m 1 0 5))
  (println (unsafe (sized-matrix-get m 0 1)))   ; => 2

  ; Bulk operations
  (println (unsafe (sized-matrix-row-sum   m 0)))   ; => 3  (1+2+0+0)
  (println (unsafe (sized-matrix-col-sum   m 0)))   ; => 6  (1+5+0)
  (println (unsafe (sized-matrix-total-sum m)))     ; => 8

  ; Shape assertion (panics if shape does not match)
  (unsafe (sized-matrix-assert-shape! m (Static 3) (Static 4)))

  (unsafe (sized-matrix-free m)))

sized-matrix-fill! sets every element to the same value:

(let [m (unsafe (sized-matrix-new 2 2))]
  (unsafe (sized-matrix-fill! m 9))
  (println (unsafe (sized-matrix-total-sum m)))   ; => 36
  (unsafe (sized-matrix-free m)))

sized-matrix-row-size returns the column count as a Size expression, useful for checking row-vector widths:

(let [m (unsafe (sized-matrix-new 3 4))]
  (println (size-eval (unsafe (sized-matrix-row-size m))))   ; => 4
  (unsafe (sized-matrix-free m)))

SizedBitVec

SizedBitVec packs bits tightly: n bits occupy ceil(n / 8) bytes.

C struct layout:

struct { int64_t len; uint8_t *bits; }

Bit i is stored at byte i/8, at position i%8 within that byte (LSB = bit 0). All operations require #fx{Unsafe}.

(import stdlib/sized-bits)

(let [bv (unsafe (sized-bitvec-new 16))]
  ; Set some bits
  (unsafe (sized-bitvec-set!    bv 0))
  (unsafe (sized-bitvec-set!    bv 3))
  (unsafe (sized-bitvec-set!    bv 7))

  ; Read bits
  (println (unsafe (sized-bitvec-get bv 0)))   ; => 1
  (println (unsafe (sized-bitvec-get bv 1)))   ; => 0

  ; Popcount
  (println (unsafe (sized-bitvec-count bv)))   ; => 3

  ; Clear and toggle
  (unsafe (sized-bitvec-clear!  bv 0))
  (unsafe (sized-bitvec-toggle! bv 1))
  (println (unsafe (sized-bitvec-count bv)))   ; => 2

  ; Fill all bits
  (unsafe (sized-bitvec-fill! bv 1))
  (println (unsafe (sized-bitvec-count bv)))   ; => 16

  ; Length and size
  (println (unsafe (sized-bitvec-len  bv)))                      ; => 16
  (println (size-eval (unsafe (sized-bitvec-size bv))))          ; => 16

  ; Length assertion (panics if length does not match)
  (unsafe (sized-bitvec-assert-len! bv (Static 16)))

  (unsafe (sized-bitvec-free bv)))

FFI Integration

Size annotations can be carried through Turmeric wrappers over opaque C pointers, providing type-safe size information without any runtime cost.

; Wrapper that reports the byte size of a point struct via an inline-C accessor.
(defn ffi-point-size [] #{Unsafe} : Size
  ```c
  return ctor_Static(sizeof(struct { int64_t x; int64_t y; }));
  ```)

; Wrapper that reports the element count for a fixed-size C array.
(defn ffi-array-size [] #{Unsafe} : Size
  ```c
  return ctor_Static(16);
  ```)

; Wrapper that reports the field count of a struct.
(defn ffi-struct-field-count [] #{Unsafe} : Size
  ```c
  return ctor_Static(3);
  ```)

(println (size-eval (unsafe (ffi-point-size))))         ; => 16
(println (size-eval (unsafe (ffi-array-size))))         ; => 16
(println (size-eval (unsafe (ffi-struct-field-count)))) ; => 3

The pattern is: return ctor_Static(n) from inline C to inject a sized annotation into Turmeric's type system.


Error Messages

Passing an int where a Size is expected is a compile-time error:

; ERROR: TUR-E0001: expected <adt>, got int
(size-add 3 4)

Shape or length assertions produce descriptive runtime messages:

; Panics: "row mismatch" (or "col mismatch")
(unsafe (sized-matrix-assert-shape! m (Static 2) (Static 2)))

; Panics: "sized type mismatch"
(unsafe (sized-bitvec-assert-len! bv (Static 8)))

; Panics: "sized type mismatch"
(size-assert-eq! (size-static 4) (size-static 5))

API Reference

stdlib/sized.tur

StaticInt

Function Signature Description
static-int (-> int StaticInt) Wrap an integer as a StaticInt
static-int-val (-> StaticInt int) Unwrap to a plain integer
static-int-add (-> StaticInt StaticInt StaticInt) Add two StaticInt values
static-int-mul (-> StaticInt StaticInt StaticInt) Multiply two StaticInt values

Size

Function Signature Description
size-static (-> int Size) Literal size from an integer
size-add (-> Size Size Size) Sum of two sizes
size-mul (-> Size Size Size) Product of two sizes
size-eval (-> Size int) Evaluate to a runtime integer
size-normalize (-> Size Size) Flatten to (Static n)
size-simplify (-> Size Size) Algebraic simplification

Predicates and assertions

Function Signature Description
size-eq? (-> Size Size bool) True if sizes evaluate equal
size-lt? (-> Size Size bool) True if s1 < s2
size-le? (-> Size Size bool) True if s1 <= s2
size-gt? (-> Size Size bool) True if s1 > s2
size-ge? (-> Size Size bool) True if s1 >= s2
size-compat? (-> Size Size bool) True if sizes are equal (runtime subtyping)
size-assert-eq! (-> Size Size nil) Panic if s1 != s2
size-assert-le! (-> Size Size nil) Panic if s1 > s2

SizedVec

Function Signature Description
sized-vec-of-1 (-> int SizedVec) 1-element vec; size inferred as 1
sized-vec-of-2 (-> int int SizedVec) 2-element vec; size inferred as 2
sized-vec-of-3 (-> int int int SizedVec) 3-element vec; size inferred as 3
sized-vec-of-4 (-> int int int int SizedVec) 4-element vec; size inferred as 4
sized-vec-from-list #fx{Unsafe} (-> int SizedVec) Convert a cons list; size inferred at runtime
sized-vec-push (-> SizedVec int SizedVec) Prepend an element; size becomes n+1
sized-vec-len (-> SizedVec int) Element count
sized-vec-get (-> SizedVec int int) Bounds-checked element read
sized-vec-size (-> SizedVec Size) Size as a Size expression

stdlib/sized-buf.tur

Function Signature Description
sized-buf-new #fx{Unsafe} (-> int int) Heap-allocate n elements (uninitialized)
sized-buf-new-zeroed #fx{Unsafe} (-> int int) Heap-allocate n elements, zeroed
sized-buf-free #fx{Unsafe} (-> int nil) Free a heap-allocated buffer
sized-buf-len #fx{Unsafe} (-> int int) Element count
sized-buf-get #fx{Unsafe} (-> int int int) Bounds-checked element read
sized-buf-set! #fx{Unsafe} (-> int int int int) Bounds-checked element write; returns buf
sized-buf-fill! #fx{Unsafe} (-> int int int) Set all elements to a value
sized-buf-copy! #fx{Unsafe} (-> int int nil) Copy src into dst (equal lengths required)
sized-buf-sum #fx{Unsafe} (-> int int) Sum of all elements
sized-buf-min #fx{Unsafe} (-> int int) Minimum element
sized-buf-max #fx{Unsafe} (-> int int) Maximum element
sized-buf-size #fx{Unsafe} (-> int Size) Element count as a Size expression
sized-buf-with-stack #fx{Unsafe} (-> int fn int) Stack-allocate n elements; call f with buffer
sized-buf-compute #fx{Unsafe} (-> int int) Stack when n<=64, heap otherwise (demo)
sized-buf-from-sized-vec #fx{Unsafe} (-> SizedVec int) Convert a SizedVec to a flat heap buffer

stdlib/sized-matrix.tur

Function Signature Description
sized-matrix-new #fx{Unsafe} (-> int int int) Allocate rows x cols (uninitialized)
sized-matrix-new-zeroed #fx{Unsafe} (-> int int int) Allocate rows x cols, zeroed
sized-matrix-free #fx{Unsafe} (-> int nil) Free the matrix
sized-matrix-rows #fx{Unsafe} (-> int int) Row count
sized-matrix-cols #fx{Unsafe} (-> int int) Column count
sized-matrix-size #fx{Unsafe} (-> int Size) Total element count as Size
sized-matrix-row-size #fx{Unsafe} (-> int Size) Column count as Size
sized-matrix-get #fx{Unsafe} (-> int int int int) Bounds-checked element read at (r, c)
sized-matrix-set! #fx{Unsafe} (-> int int int int int) Bounds-checked element write at (r, c)
sized-matrix-fill! #fx{Unsafe} (-> int int int) Set all elements to a value
sized-matrix-row-sum #fx{Unsafe} (-> int int int) Sum of row r
sized-matrix-col-sum #fx{Unsafe} (-> int int int) Sum of column c
sized-matrix-total-sum #fx{Unsafe} (-> int int) Sum of all elements
sized-matrix-assert-shape! #fx{Unsafe} (-> int Size Size nil) Panic on shape mismatch

stdlib/sized-bits.tur

Function Signature Description
sized-bitvec-new #fx{Unsafe} (-> int int) Allocate n bits, all zero
sized-bitvec-free #fx{Unsafe} (-> int nil) Free the bit vector
sized-bitvec-len #fx{Unsafe} (-> int int) Bit count
sized-bitvec-size #fx{Unsafe} (-> int Size) Bit count as a Size expression
sized-bitvec-get #fx{Unsafe} (-> int int int) Read bit i; returns 0 or 1
sized-bitvec-set! #fx{Unsafe} (-> int int int) Set bit i to 1; returns bv
sized-bitvec-clear! #fx{Unsafe} (-> int int int) Clear bit i to 0; returns bv
sized-bitvec-toggle! #fx{Unsafe} (-> int int int) Flip bit i; returns bv
sized-bitvec-count #fx{Unsafe} (-> int int) Popcount (number of set bits)
sized-bitvec-fill! #fx{Unsafe} (-> int int int) Set all bits to v (0 or 1); returns bv
sized-bitvec-assert-len! #fx{Unsafe} (-> int Size nil) Panic if bit count does not match expected