Turmeric's module system gives you namespaced, self-contained units of code with controlled visibility. Use it to organize larger programs, prevent name collisions, and keep the public surface of a library narrow.
This guide covers everything a working programmer needs to know:
A module is a single .tur file with a defmodule form at the top:
;; src/geom/vector.tur
(defmodule geom/vector
(export Point make-vector vector-x vector-y magnitude)
(defn make-vector [x :int y :int] :ptr
(...))
(defn vector-x [^Point p] :int (.x p))
(defn vector-y [^Point p] :int (.y p))
(defn magnitude [^Point p] :int
(...)))
Rules:
defmodule must be the first form in the file (after stdlib auto-loads).defmodule per file./ to express nesting
(geom/vector, db/postgres/conn).(defmodule geom/vector "2-D vector math" ...).Convention is that the module name matches the file path under the project root:
| Module name | File path |
|---|---|
geom/vector |
geom/vector.tur |
db/postgres/conn |
db/postgres/conn.tur |
app |
app.tur |
The compiler resolves imports by translating the module name to a path under
the importing file's directory. The convention is not strictly enforced (the
name in the defmodule form is the canonical identifier), but breaking it
will produce confusing "module not found" errors.
Use (import ...) inside defmodule to bring in another module:
(defmodule app
(import geom/vector :as v)
(import math :refer [sqrt abs])
(defn main [] :int
(let [p (v/make-vector 3 4)]
(println (v/magnitude p))
0)))
import| Form | Effect |
|---|---|
(import math) |
Names available as fully-qualified: math/sqrt. |
(import math :as m) |
Alias: m/sqrt. |
(import math :refer [sqrt abs]) |
Pull selected names directly into scope: sqrt, abs. |
Self-qualification works inside a module too:
(defmodule geom/vector ... (geom/vector/magnitude p)) resolves
to the local magnitude.
If module A imports B and B imports A, you get a clear error at the second import attempt:
error: circular import: module 'a' is already being loaded
There is no lazy resolution in v1 — break the cycle by factoring a shared helper module.
Everything declared inside a defmodule is private by default. Only names
listed in (export ...) are visible to other modules.
(defmodule list-utils
(export map filter)
(defn map [f xs] :ptr (...))
(defn filter [pred xs] :ptr (...))
;; private helper — only visible inside list-utils
(defn -fold [f acc xs] :ptr (...)))
:refer'd: only exported names are visible.tur/...) bindings: globally visible — see §6.(defmodule control-flow
(export when2)
(defmacro when2 [test a b]
(list if test (do a b))))
Imported macros expand correctly in the consumer module — recursive macro calls inside an exported macro can still reach private helpers of the defining module (the elaborator tracks the "expansion module" for this).
Top-level (defer ...) forms inside a module run at process exit via
atexit:
(defmodule logger
(export log)
(def file-handle ...)
(defer (println "shutting down logger"))
(defn log [msg] :int (...)))
Ordering: module defers fire in LIFO order (last-defined fires first), matching function-level defer semantics. Across modules they fire in reverse load order.
When a module exports a symbol, the emitted C name is prefixed with the mangled module name to prevent linker collisions:
| Source | Emitted C symbol |
|---|---|
geom/vector exports add2 |
geom__vector__add2 |
my-lib exports do-thing |
my_lib__do_thing |
tur/safe exports box |
box (stdlib promotion) |
/ in module name → __ (double underscore)-, ., and any other non-[A-Za-z0-9_] character → ___ and then the (sanitized) binding nameIf two exported symbols mangle to the same C name (e.g. module my-lib and
module my_lib both exporting foo), the compiler emits a hard error at
elaboration time and tells you which two definitions collide. Rename one or
use (export-as ...) to override the C name (see below).
(export-as "c_name") — explicit C name overrideFor FFI scenarios where you need a specific C symbol name (matching a C
header, registering with a runtime, etc.), use (export-as "...") before
the function name:
(defmodule plugin
(export init)
(defn (export-as "plugin_init_v2") init [] :int
(...)))
This bypasses module mangling: the emitted C function will be named exactly
plugin_init_v2. Useful for:
extern declarations in existing C headersThe argument must be a string literal.
Two stdlib files are auto-loaded into every program:
stdlib/macros.tur — (defmodule tur/macros ...): cond, when,
unless, must!, must-msg!, ignore!, do-m, for.stdlib/safe.tur — (defmodule tur/safe ...): array-get, array-set,
array-slice, with-c-string, from-c-string, box, unbox.After loading, the elaborator promotes every export from any module under
the tur/ namespace back to "stdlib pre-module" status (its
defining_module_name is reset to NULL). This means:
(when test body) from user code without writing an import.tur__macros__ prefix — when is when.The tur/ namespace is reserved for stdlib. User code should not declare
modules under tur/.
For a single-file program, ./build/tur build app.tur -o app works as before
— the elaborator finds and inlines any (import ...) targets.
For a multi-file build, place modules under the same directory tree as the main file:
src/
app.tur ;; (defmodule app (import geom/vector :as v) ...)
geom/
vector.tur ;; (defmodule geom/vector ...)
./build/tur build src/app.tur -o app recursively loads geom/vector from
src/geom/vector.tur (matching the module name to the file path under the
main file's directory).
.h / .c generationFor incremental builds or integrating into a C build system, you can emit each module as a separate header/implementation pair:
./build/tur emit-h src/geom/vector.tur > geom__vector.h
./build/tur emit-c src/geom/vector.tur > geom__vector.c
In separate-compilation mode, exported functions get extern linkage in the
header, private definitions stay static, and the header #includes its
own dependencies' headers.
defmodule must be the first form in the fileMove any other top-level forms inside the defmodule body, or below it (the
latter is also rejected — defmodule must wrap everything).
symbol 'foo' is private to module 'mymod'Either add foo to mymod's (export ...) list, or use mymod/foo
inside mymod itself (private names can be self-qualified).
exported symbol 'foo' is not defined in this moduleThe (export ...) list names a symbol that has no matching defn/def
inside the module body. Check for typos and verify the definition is
inside the defmodule form.
module 'foo/bar' not foundThe compiler couldn't locate foo/bar.tur relative to the importing file's
directory. Check the path or use an absolute path via the project's
module_base_dir.
circular import: module 'foo' is already being loadedModule A imports B which transitively imports A. Factor out the shared parts into a third module.
exported symbol 'foo' from module 'X' mangles to the same C name 'Y' as 'bar' from module 'Z'The mangling rules produced identical C names. Rename one of the symbols
or use (export-as "...") to override the C name explicitly.
;; src/math.tur
(defmodule math
(export sqrt square)
(defn sqrt [x :int] :int (...))
(defn square [x :int] :int (* x x)))
;; src/geom/vector.tur
(defmodule geom/vector
(import math :as m)
(export Point make-vector magnitude)
(defstruct Point [x : int y : int])
(defn make-vector [x :int y :int] :ptr (Point x y))
(defn magnitude [^Point p] :int
(m/sqrt (+ (m/square (.x p))
(m/square (.y p))))))
;; src/app.tur
(defmodule app
(import geom/vector :refer [make-vector magnitude])
(defn main [] :int
(let [p (make-vector 3 4)]
(println (magnitude p)) ;; 5
0)))
Build with ./build/tur build src/app.tur -o app.
(import ...) statements must be at the top of
a module body. No (require) at function scope.:refer :all: explicit symbol lists only.(export-from
other-module foo bar) form is planned.See docs/archive/module-system-plan.md for the full design history and
deferred work items.