Turmeric Module System Guide

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:

  1. Declaring a module
  2. Importing other modules
  3. Exports and visibility
  4. Module-level defer
  5. C symbol mangling and FFI naming
  6. Auto-loaded stdlib modules
  7. Common errors and how to fix them

1. Declaring a Module

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:

Name → file-path convention

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.


2. Importing Other Modules

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)))

Forms of 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.

Circular imports

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.


3. Exports and Visibility

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 (...)))

Visibility rules

Macros are exported the same way

(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).


4. Module-Level Defer

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.


5. C Symbol Mangling and FFI Naming

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)

Mangling rules

Collision detection

If 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 override

For 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:

The argument must be a string literal.


6. Auto-Loaded Stdlib Modules

Two stdlib files are auto-loaded into every program:

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:

The tur/ namespace is reserved for stdlib. User code should not declare modules under tur/.


7. Building Multi-Module Programs

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).

Separate .h / .c generation

For 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.


8. Common Errors

defmodule must be the first form in the file

Move 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 module

The (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 found

The 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 loaded

Module 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.


9. A Complete Example

;; 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.


10. Limitations and v1 Caveats

See docs/archive/module-system-plan.md for the full design history and deferred work items.