Structs in Turmeric

Structs are named, ordered collections of fields. They are the primary way to group related data under a single type name, and they integrate cleanly with typeclasses, ownership, and the effects system.


Defining a struct

(defstruct Name [field1 :type1 field2 :type2 ...])

Fields are listed as alternating name/type pairs inside the vector literal. All field names must be distinct.

(defstruct Point [x :int y :int])
(defstruct Pixel [r :uint8 g :uint8 b :uint8 a :uint8])
(defstruct Vec2  [x :float32 y :float32])

A defstruct at file scope registers the type immediately so that later definitions -- including self-referential or mutually-recursive structs -- can refer to it.


Ownership annotations

Every struct has a copy kind that governs how the value can be used:

Annotation Meaning
:copy Bitwise-copyable; can be freely duplicated (all fields must be copy types)
:move Move-only (default when no annotation is given)
:linear Exactly-once consumed; must be used exactly once
(defstruct Point   :copy   [x :int y :int])      ; freely copyable
(defstruct Packet  :move   [ptr :ptr<void>])      ; ownership transferred on use
(defstruct Socket  :linear [fd :int])             ; must be consumed exactly once

Omitting the annotation is the same as :move:

(defstruct Pair [first :int second :int])   ; move-only by default

The compiler enforces that a :copy struct's fields are all themselves copy-compatible types. Using a non-copy field (such as :ref<int>) in a :copy struct is a compile error.


Supported field types

Syntax Meaning
:int 64-bit signed integer
:bool Boolean
:float 64-bit float
:float32 32-bit float
:uint8, :int16, :int32, … Sized numeric types
:cstr C string pointer (const char *)
:ptr<void> Raw pointer
:rc<T> Reference-counted pointer to T
:ref<T> Owned reference to T (non-RC)
:lref<T> Linear reference to T (exactly-once)
:weak<T> Weak reference (non-owning, for rc<T> cycles)
:fn Function pointer
:fn #{Effect} Function pointer with an explicit effect-row annotation

Drop glue (automatic cleanup of rc<T>, ref<T>, and weak<T> fields) is generated by the compiler whenever any such field is present.


Creating instances

Use make-struct to construct a struct value. Fields are supplied positionally in the order they appear in defstruct.

(defstruct Point :copy [x :int y :int])

(let [p (make-struct Point 3 4)]
  ...)
(defstruct Pixel [r :uint8 g :uint8 b :uint8 a :uint8])

(let [px (make-struct Pixel 255u8 128u8 64u8 255u8)]
  ...)

The argument count must exactly match the field count; a mismatch is a compile-time error.


Accessing fields

Use .fieldname to read a field from a struct value:

(defstruct Point :copy [x :int y :int])

(let [p (make-struct Point 3 4)]
  (println (.x p))    ; 3
  (println (.y p)))   ; 4

The .fieldname form is also valid for arithmetic:

(defn distance-sq [p] :int
  (+ (* (.x p) (.x p))
     (* (.y p) (.y p))))

Borrowing fields

Prefix .fieldname with & to take an immutable borrow of a field without moving the struct:

(defstruct Point :copy [x :int y :int])

(let [p (make-struct Point 3 4)]
  (let [rx &(.x p)]
    (println @rx)))   ; prints 3

Linear fields (lref<T>)

A :move struct may contain :lref<T> fields. Extracting such a field transfers linear ownership out of the struct and marks the struct binding as moved. A second extraction from the same binding is a use-after-move error (TUR-E0005).

(defstruct PtrBox :move [ptr :lref<int>])

;; OK: single extraction
(let [b (make-struct PtrBox (lref/new 42))]
  (println (deref (.ptr b))))

;; ERROR TUR-E0005: second extraction of a moved struct
(defstruct Box :move [val :lref<int>])
(let [b (make-struct Box (lref/new 42))]
  (let [x (.val b)]
    (let [y (.val b)]   ; use-after-move
      (println (deref x)))))

Reference-counted structs

Wrap a struct in rc/of to heap-allocate it with reference counting. The RC wrapper manages drop glue automatically when the count reaches zero.

(defstruct Wrapper :move [val :rc<int>])

(let [inner (rc/of 10)]
  (let [w (rc/of (make-struct Wrapper inner))]
    (println (rc/strong-count w))))

If the struct itself contains :rc<T> fields, the compiler generates nested drop glue to decrement those inner counts:

(defstruct Node :move [val :rc<int>])

(let [inner (rc/of 42)
      outer (rc/of (make-struct Node inner))]
  (println (rc/strong-count outer)))

Structs and typeclasses

Implement typeclasses for your struct types with definstance. Inside inline-C methods, struct parameters arrive as C struct values, so field access uses dot notation.

Clone

(defstruct Pair [first :int second :int])

(definstance Clone [Pair]
  (clone [x] :int
    ```c
    struct { int64_t first; int64_t second; } *dst = malloc(sizeof(Pair));
    dst->first = x.first;
    dst->second = x.second;
    return (int64_t)(intptr_t)dst;
    ```))

Eq

(definstance Eq [Pair]
  (eq? [x y] (pair-eq? x y (fn [a b] (= a b)))))

Show

(defstruct Point :copy [x :int y :int])

(definstance Show [Point]
  (show [__p] :cstr
    ```c
    int nx = snprintf(NULL, 0, "%lld", (long long)__p.x);
    int ny = snprintf(NULL, 0, "%lld", (long long)__p.y);
    size_t len = 13 + (size_t)nx + 6 + (size_t)ny + 3 + 1;
    char *buf = (char *)malloc(len);
    snprintf(buf, len, "Point { x = %lld, y = %lld }",
             (long long)__p.x, (long long)__p.y);
    return (const char *)(intptr_t)buf;
    ```))

(let [p (make-struct Point 3 4)]
  (println (.show p)))   ; Point { x = 3, y = 4 }

Deriving Show, Debug, Display

Writing the inline C above for every struct is tedious. The derive-show, derive-debug, and derive-display macros in stdlib/macros.tur generate the instance from a struct name and a list of field descriptors.

Each field descriptor is either a bare symbol -- x becomes label "x" with accessor (.x s) -- or a [label .accessor] pair to use a different label or non-default accessor.

(defstruct Point :copy [x :int y :int])
(derive-show    Point x y)
(derive-debug   Point x y)
(derive-display Point x y)

(let [p (make-struct Point 3 4)]
  (println (.show    p))    ; Point { x = 3, y = 4 }
  (println (.debug   p))    ; (Point (x 3) (y 4))
  (println (.display p)))   ; Point { x = 3, y = 4 }

derive-show produces "TypeName { k = v, ... }". derive-debug produces the s-expression-flavoured "(TypeName (k v) ...)". derive-display matches derive-show but dispatches through the Display typeclass.

Each field must itself have an instance of the relevant typeclass -- the macro expansion calls .show (or .debug/.display) on each field value and joins the results with str-concat. Show, Debug, and Display instances exist in stdlib for primitives, Pair, Option, Result, List, and Vec, so nested structs and collections print recursively out of the box.

To alias a field name or access through a non-standard reader, use the [label .accessor] pair form:

(defstruct MyStruct [name :cstr internal-label :cstr count :int])
(derive-show MyStruct name [display-name .internal-label] count)
;; => "MyStruct { name = ..., display-name = ..., count = ... }"

REPL auto-show

The native REPL and web REPL automatically call show on the result of each top-level expression when an applicable Show instance exists, so:

> (make-struct Point 3 4)
Point { x = 3, y = 4 }

If no Show instance is registered for the result type, the REPL falls back to printing the raw value. Note: heap-allocated stdlib types returned through constructors that elaborate to :int (for example pair-new) are seen by typeclass dispatch as int and will print as a raw pointer; use (make-struct Pair 1 2) (or a typed alias) when you want Show-dispatch to fire on the constructed value.

Bifunctor

(definstance Bifunctor [Pair]
  (bimap [container fn-left fn-right]
    (__bifunctor_pair_bimap container fn-left fn-right)))

Function-pointer fields

A :fn field stores a function pointer. Annotate the field with #{Effect} to declare the effect row the stored function may perform. Calling the field propagates that effect row to the enclosing function.

(defeffect Emit [s :cstr] :nil)

(defstruct Emitter :copy [run :fn #{Emit}])

(defn main [] :int
  (let [em (make-struct Emitter (fn [s] (perform (Emit s))))]
    (handle
      (do (.run em "hello") 0)
      (Emit [s] k) (do (println s) (resume k nil)))))

Cross-module structs

A struct defined in one module can be imported and used in another. Construct it with make-struct and access its fields with .fieldname exactly as if it were locally defined.

;; geom module defines Point
(defmodule app
  (import geom :refer [Point])
  (defn main [] :int
    (let [p (make-struct Point 3 4)]
      (println (+ (.x p) (.y p)))
      0)))

Docstrings

Follow the ;;; convention immediately above the defstruct form:

;;; Point -- a 2D integer coordinate.
;;;
;;; Parameters:
;;;   x -- horizontal position
;;;   y -- vertical position
;;;
;;; Example:
;;;   (make-struct Point 3 4)  ; => Point with x=3, y=4
;;;
;;; Since: Phase B1
(defstruct Point :copy [x :int y :int])

See the docstring standard in CLAUDE.md for the full required-fields table.


Common errors

Error Cause
TUR-E0005 Use-after-move: reading a field from a struct that was already moved (or extracting a linear field twice)
Field count mismatch make-struct given fewer or more arguments than the struct has fields
:copy constraint violation A :copy struct contains a field type that is not copy-compatible (e.g. :ref<int>)

See also