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.
(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.
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.
| 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 #fx{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.
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.
Every defstruct also binds the struct name as a constructor function in
the value namespace, so you can drop the make-struct keyword entirely:
(defstruct Point :copy [x : int y : int])
(let [p (Point 3 4)] ; identical to (make-struct Point 3 4)
...)
The struct name lives in the type namespace and the constructor in the
value namespace, so Point still works as a type annotation (p : Point)
with no conflict. If the struct name is already bound as a value elsewhere,
opt out with :no-auto-ctor; construction then goes only through
make-struct:
(defstruct Point :copy :no-auto-ctor [x : int y : int])
;; (Point 3 4) is now an error; use (make-struct Point 3 4)
Note: an under-applied positional constructor curries.
(Point 3)on a two-field struct returns a closure expecting the remaining field, and((Point 3) 4)-- or(let [mk (Point 3)] (mk 4))-- yields thePoint, with field access working on the result. By-value struct results now flow through the closure ABI (this was previously a known limitation). The keyword form does not curry: a keyword call must supply every field.
Both make-struct and the auto-bound constructor accept :field value pairs.
Keyword order is free; the elaborator reorders into declared-field order:
(defstruct Person :copy [name : cstr age : int])
(Person :name "Bob" :age 40) ; auto-bound ctor, keyword form
(make-struct Person :age 40 :name "Bob") ; make-struct, reversed order
Keyword construction is checked strictly: every field must be supplied
(TUR-E0292), an unknown field (TUR-E0294) or a duplicate field
(TUR-E0293) is an error, and positional and keyword forms cannot be mixed in
one call (TUR-E0299). Use one form or the other.
withwith returns a new struct value with some fields overridden and the rest
copied from a source value. It is only valid on :copy structs (copying the
unchanged fields out of a move-only source would consume it):
(defstruct Person :copy [name : cstr age : int active : int])
(let [p (Person "Bob" 40 1)
q (with p [name "Alice"]) ; => Person "Alice" 40 1
r (with p [active 0 age 41])] ; override order is free
...)
with lowers to a constructor call that takes the listed fields from the
override list and every other field from the source, so override values are
type-checked exactly as the constructor would check them. Applying with to a
non-:copy struct is rejected (TUR-E0296), as are unknown (TUR-E0297) and
duplicate (TUR-E0298) override 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))))
matchA struct is the single-variant case of a tagged union, so match takes one
apart too -- the sole pattern names the struct and binds its fields. A
single-variant match is trivially exhaustive (no catch-all needed):
(defstruct Person :copy [name : cstr age : int])
(let [p (make-struct Person "Ada" 36)]
(match p
(Person name age) ; positional binding
(println name)))
(match p
(Person :age a :name n) ; by-name binding, order free
(println a))
(match p
(Person _ age) ; `_` ignores a field
(println age))
This is the same surface as matching a record-style defdata variant; see
Sum Types. Use
.fieldname for a single read and match when you want several fields bound
at once.
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
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)))))
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)))
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 }
Show, Debug, DisplayWriting 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 = ... }"
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)))
A :fn field stores a function pointer. Annotate the field with #fx{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)))))
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)))
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.
:int-pointer struct codeOlder C-backed spice code predates typed structs. It stored a struct as an
:int pointer, sized allocations with (sizeof T), read fields through
generated (Struct-field obj) accessor functions, and indexed heap float
buffers with a (float64* ptr i) intrinsic. None of these are Turmeric
language forms -- sizeof is valid only inside an inline-C block, and the
Struct-field accessor and float64*/float32* indexing forms never existed
at the language level. Code using them fails with unknown function or
operator '<name>' (the compiler now attaches a migration hint pointing here).
Translate each form to the supported equivalent:
| Legacy form | Supported equivalent |
|---|---|
(malloc (sizeof T)) -> struct as :int |
(make-struct T ...); let the type system own the layout |
(Struct-field obj) accessor function |
(.field obj) read |
(set! (Struct-field obj) v) over an :int pointer |
give the field a real type and mutate the backing value (e.g. (vec-set! (.data obj) i v)) |
heap float buffer as :int + (float64* ptr i) |
a stdlib (Vec float) field with (vec-get v i) / (vec-set! v i x) |
(declare malloc free) |
(extern-c malloc [size : int] : ptr<void>) (only when you genuinely need raw C malloc) |
A matrix wrapper, before and after:
;; Before -- struct pointer stored as :int, data malloc'd, fields via accessors
(defstruct mat [rows : int cols : int data : int])
(defn mat-get [m i] : float (float64* (mat-data m) i))
;; After -- typed struct over a heap (Vec float)
(defstruct mat [rows : int cols : int data : (Vec float)])
(defn mat-get [^borrow m : mat i : int] : float
(vec-get (.data m) i))
A struct that owns a (Vec float) (or any non-:copy field) is move-only, so a
plain (defn mat-get [m : mat ...] ...) consumes m and a second call trips
TUR-E0005 use-after-move. Take the receiver by borrow with the ^borrow
parameter annotation -- not a &mat / ^&mat type spelling -- and .field
reads (and vec-set! writes through (.data m)) work without moving:
(defn mat-get [^borrow m : mat i : int] : float
(vec-get (.data m) i))
(defn mat-set! [^borrow m : mat i : int v : float] : void
(vec-set! (.data m) i v))
The call site passes the struct directly -- (mat-get m 0) -- and keeps
ownership, so the same m can be read and mutated repeatedly.
| Error | Cause |
|---|---|
TUR-E0005 |
Use-after-move: reading a field from a struct that was already moved (or extracting a linear field twice). For a move-only struct, take the receiver ^borrow (see Migrating legacy :int-pointer struct code) |
unknown function or operator 'sizeof' / 'float64*' / '<Struct>-<field>' |
A legacy :int-pointer form; see the migration table above |
| 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>) |
^linear, ^affine, ^relevant