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.
A struct argument is copied into the callee, at every copy kind. A callee therefore mutates its own copy, and the caller's value is unchanged:
(defstruct Ctr [n : int])
(defn set-it! [^mut a : Ctr] : int
(set! (.n a) 3) ; mutates the CALLEE's copy
0)
(defn main [] : int
(let [^mut c (Ctr 0)]
(set-it! c)
(println (.n c))) ; => 0, not 3
0)
^mut on a parameter is not an out-parameter. It means "this binding is
mutable inside this body" -- useful for treating a parameter as a mutable local
seeded from the argument, which is observable within the function:
(defn bump-and-read [^mut a : Ctr] : int
(set! (.n a) 3)
(.n a)) ; => 3; the write is visible HERE, just not to the caller
Nesting does not change this: an Outer holding an Inner by value is one
flat value, so copying the outer copies the inner and
(set! (.n (.inner o)) v) is equally invisible to the caller.
To let a callee mutate something the caller can observe, reach for a type whose sharing is part of its meaning:
| you want | use |
|---|---|
| shared, reference-counted mutation | rc<T> -- see Reference-counted structs |
| interior mutability by declaration | a :heap struct |
| to return the new value instead | an ordinary return, which is usually clearest |
A &Struct receiver is rejected outright -- set! (.field s) requires a struct
or an rc<Struct> -- so the reference route is not available by accident.
| 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 flow through the closure ABI. 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.
name : cstrhere is fine -- it holds a string literal, whose bytes are static. But a struct field that must own a computed or stored string (form input, a decoded value, anything that outlives its source) should beString, notcstr, so the struct owns its bytes instead of borrowing a pointer that can dangle. See strings-guide.md.
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)))))
ShowThe stdlib Show renders to an OWNED String ((show x) : String): the caller
string/releases the result. Build a struct instance directly with a
StringBuilder over each field's own show, releasing every intermediate. Most
of the time you do not write this by hand -- derive-show (below)
generates exactly this.
(load "stdlib/typeclass.tur")
(defstruct Point :copy [x : int y : int])
(definstance Show [Point]
(show [__p]
(let [b (builder/new)]
(do
(builder/push-cstr! b "Point { x = ")
(let [sx (show (.x __p))] (do (builder/push-string! b sx) (string/release sx)))
(builder/push-cstr! b ", y = ")
(let [sy (show (.y __p))] (do (builder/push-string! b sy) (string/release sy)))
(builder/push-cstr! b " }")
(builder/finish b)))))
(let [p (make-struct Point 3 4)]
(show-line p)) ; Point { x = 3, y = 4 } (show + println + release)
show-line (and print-show) wrap show + print + release so a
render-and-print call site stays a one-liner despite the owned result. To keep
the String, hold it and release when done:
(let [s (show p)] (do ... (string/to-cstr s) ... (string/release s))).
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 = ... }"
derive-show (owned) and derive-show-cstr (local class)derive-show is the deriver for the stdlib Show. Since the stdlib Show
returns an owned String, derive-show generates the Show [T] instance shown
above -- a StringBuilder over each field's show, releasing every
intermediate, one owned String result, no per-field concat leak. It needs
String / StringBuilder in scope, i.e. a program that loaded
stdlib/typeclass.tur (or stdlib/string.tur). Field descriptors -- bare
symbols and the [label .accessor] alias form -- are the same for both derivers.
(load "stdlib/typeclass.tur")
(defstruct Point :copy [x : int y : int])
(derive-show Point x y)
(let [p (make-struct Point 3 4)]
(show-line p)) ; Point { x = 3, y = 4 }
The sibling derive-show-cstr emits a cstr-bodied Show instance joined
with str-concat. It is for programs that define their own minimal local
Show class and never load the String stack. The emitted body needs three
things in scope at the call site:
Show class (defclass Show [a] (show [x] : cstr));str-concat : (cstr cstr) -> cstr;Show instance for each field's type.stdlib/str-build.tur is the intended source of str-concat (and int->str):
a dependency-free leaf that pulls in no typeclass instances and carries
interpreter natives, so a program built on it runs on both the compiled and
--interpret paths with no inline-C.
(load "stdlib/str-build.tur") ; str-concat + int->str (both paths)
(defclass Show [a] (show [x] : cstr))
(definstance Show [int] (show [x] : cstr (int->str x)))
(defstruct Point :copy [x : int y : int])
(derive-show-cstr Point x y)
(let [p (make-struct Point 3 4)]
(println (.show p))) ; Point { x = 3, y = 4 }
A missing str-concat or field Show instance is a compile error attributed to
the macro, not the call site. Using derive-show-cstr against the owned stdlib
Show (or derive-show against a local cstr Show) is a type error -- match
the deriver to the Show class in scope.
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 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