Modules Guide

This guide covers the Sigil module system in depth.

Library Basics

Modules are defined with define-library:

(define-library (myapp utils)
  (export helper-function another-function)
  (begin
    (define (helper-function x) ...)
    (define (another-function y) ...)))

Note: Basic Scheme forms (define, if, lambda, arithmetic, etc.) are available without imports. Use import to bring in additional modules like (sigil io) or (sigil string).

Library Names

Library names are lists of symbols that form a hierarchical namespace:

(sigil io)           ; Standard library module
(myapp utils)         ; Your application module
(myapp data models)   ; Nested namespace

Exports

Only explicitly exported bindings are visible to importers:

(export
  public-procedure     ; A procedure
  *global-constant*    ; A value
  my-macro)            ; A macro

Private bindings remain internal:

(define-library (example)
  (export public-fn)
  (begin
    (define (private-helper x) (* x 2))
    (define (public-fn x) (private-helper (+ x 1)))))

Import Forms

Basic Import

(import (myapp utils))

All exports become available by their original names.

Selective Imports

Import only specific bindings:

(import (only (myapp utils)
              helper-function))

Import all except specific bindings:

(import (except (myapp utils)
                deprecated-function))

Renaming

Rename bindings to avoid conflicts or improve clarity:

(import (rename (myapp utils)
                (helper-function util-helper)
                (another-function util-other)))

(util-helper 42)  ; Use the renamed binding

Prefixes

Add a prefix to all imported bindings:

(import (prefix (myapp utils) utils:))

(utils:helper-function 42)

Combining Forms

Import forms can be combined:

(import (prefix
          (only (myapp utils) helper-function)
          u:))

(u:helper-function 42)

File Organization

Mapping Names to Files

Library names map to file paths:

Library NameFile Path
(myapp utils)myapp/utils.sgl
(myapp data models)myapp/data/models.sgl
(sigil io)sigil/io.sgl

Search Path

A library name is resolved by walking a single ordered list of directories and taking the first hit. Highest priority first:

#SourceSet by
1-L <dir>the sigil command line, leftmost -L wins (except the modules sigil is built from; see below)
2SIGIL_LIBenvironment, colon-separated. Note the RIGHTMOST entry wins, which is the opposite of PATH
3paths added while the program runsadd-library-path!, and the CLI's own setup for a project's src/ and its build output
4<binary>/../liba runtime that does not carry a bundle
5the bundle embedded in the binarysigil build, at bundle time

Rows 4 and 5 never both apply: <binary>/../lib is registered only when the binary carries no bundle, so a shipped sigil has row 5 and a dev build has row 4.

Within one directory, extensions are tried .sgb, .sgl, .sld, .scm — so a compiled module in a directory beats a source module in a lower-priority one.

The bottom entry is why sigil works on a machine with no project and no dependencies installed: (sigil string) resolves out of the binary itself. The top entry is how you override that. sigil -L ./src test inside a package that defines (sigil log) runs the checkout's source rather than the copy inside the runtime.

-L is an option of the sigil CLI, not of programs sigil builds. An application compiled with sigil build does not scan its own argv for -L, and honours SIGIL_LIB only when it carries no bundle — its modules are its own.

Modules compiled from source

When sigil <file>, sigil eval, sigil test or the REPL imports a library that exists only as source (a project's src/, a dependency in .sigil/deps, a .sgl on the search path), the CLI compiles it once and reuses the result from $XDG_CACHE_HOME/sigil/compiled/ (~/.cache/sigil/compiled/ by default). A compiled entry is named by the hash of everything that shaped its bytes: the compiler, the source file's content and absolute path, the identity of every library it imports, the compile settings (SIGIL_FEATURES, SIGIL_CHECKS, SIGIL_OPT_LEVEL, SIGIL_PATH_PREFIX_MAP, the SIGIL_CPS_* switches) and the host. The cache is not on the search path: the CLI works out which entry this process needs and loads that file directly, so an entry compiled for one project is never used by another project that locks a different version, and an edit is seen even when it keeps the file's modification time.

A library whose compile reads files the key cannot name (an include, or a cond-expand on a library's presence) is compiled fresh on every run, and so is every library that imports it. SIGIL_DEBUG_LOAD=1 prints one [auto-compile] compiled|reused|uncached|artifact line per library.

Two things sit outside the list:

  • A module that is already loaded is never re-resolved. The first import wins for the life of the process, whichever entry served it. This is why entry 3 cannot displace anything the runtime loaded during startup: a project's src/ and a dev-redirects.sgl path are both registered after sigil's own modules have been imported. sigil -L src test is the form that works today.
  • Modules implemented in C — (sigil io), (sigil fs), (sigil string) and their siblings — register their bindings when the VM starts. A load path can supply a replacement for such a module's Scheme half, and its new definitions do take effect; what it cannot do is override a binding the C side already installed. Redefining string-upcase in your own sigil/string.sgl will not change what string-upcase does.

Which modules -L can replace in sigil itself

-L is applied before sigil loads its own modules, so it can replace them, but only the ones that come from separately released packages. The modules sigil is built from (the ones in the Sigil repository that it loads at startup: (sigil core), (sigil args), (sigil build), (sigil cli) and about a hundred more) always come from sigil's own copy, ahead of -L and SIGIL_LIB. A project's build output on -L, holding an old compiled (sigil args), therefore no longer stops sigil from starting.

Modules from extracted packages, the ones sigil.lock pins such as (sigil json), (sigil log), (sigil mcp) and (sigil http), stay replaceable. That is how such a package is tested from its own checkout: sigil -L ./src test.

The list is packages/sigil-lib/src/cli-owned-modules.h, generated by scripts/derive-cli-owned-modules from what a dev and a release sigil actually load at startup, never written by hand. To try a change to one of those modules, rebuild sigil (sigil build sigil-cli in the Sigil repository); -L will not pick it up. The rule is about the sigil command only: programs built with sigil do not read -L at all.

A replaceable module on -L can still stop sigil from starting if it is broken or incompatible. In the ordinary case the error names the file and line:

Error loading (sigil cli): /home/you/sigil-log/src/sigil/log.sgl:412:1: 3 unclosed delimiters:

An error raised while such a module runs (a stale compiled .sgb from an older sigil, or a struct whose fields changed) carries no file name of its own, so the runtime adds one. When a module found on a load path fails and the program has its own copy further down the path, the report says which copy failed and what it replaced. For the native sigil binary, whose own modules are built in (@bundle/lib):

Error importing entry module: error: <whatever the module raised>
  while loading (sigil json) from /home/you/other-project/build/dev/lib/sigil/json.sgb
  -L /home/you/other-project/build/dev/lib is searched before this program's own modules, so its (sigil json)
  replaced the program's copy (@bundle/lib/sigil/json.sgb), which the program itself imports.
  Remove -L /home/you/other-project/build/dev/lib, or rebuild what it holds with this version of sigil.

Pointing -L at another project's build output (build/dev/lib) is the usual way to hit this: that directory holds compiled copies of every library the project depends on, at whatever versions it was last built with.

my-project/
├── package.sgl          ; Package metadata
├── src/
│   └── myapp/
│       ├── main.sgl     ; Entry point
│       ├── core.sgl     ; Core functionality
│       ├── utils.sgl    ; Utilities
│       └── data/
│           ├── models.sgl   ; Data models
│           └── storage.sgl  ; Persistence
└── test/
    ├── test-core.sgl
    └── test-utils.sgl

Multiple begin Blocks

A library can have multiple begin blocks for organization:

(define-library (myapp utils)
  (export string-helpers list-helpers)

  ;; String utilities
  (begin
    (define (string-helpers ...) ...))

  ;; List utilities
  (begin
    (define (list-helpers ...) ...)))

Conditional Code

Use cond-expand for platform-specific code:

(define-library (myapp platform)
  (export get-home-directory)
  (cond-expand
    (linux
      (begin
        (define (get-home-directory)
          (getenv "HOME"))))
    (windows
      (begin
        (define (get-home-directory)
          (getenv "USERPROFILE"))))
    (else
      (begin
        (define (get-home-directory) #f)))))

Re-exporting

Create facade modules that re-export from multiple sources:

(define-library (myapp)
  (import (myapp core)
          (myapp utils)
          (myapp data))
  (export
    ;; From core
    initialize shutdown
    ;; From utils
    helper-function
    ;; From data
    save-data load-data))

Users can now import just (myapp) to get the public API.

Circular Dependencies

Sigil does not support circular imports. If module A imports module B, then B cannot import A.

Solutions:

  1. Extract shared code: Move common definitions to a third module
  2. Dependency injection: Pass procedures as parameters
  3. Restructure: Rethink module boundaries

Best Practices

1. Single Responsibility

Each module should have one clear purpose:

;; Good: focused modules
(myapp config)     ; Configuration handling
(myapp logging)    ; Logging utilities
(myapp http)       ; HTTP client

;; Avoid: kitchen-sink modules
(myapp utils)      ; Contains everything

2. Minimal Exports

Export only what users need:

;; Good: clean interface
(export
  connect
  disconnect
  send-message)

;; Avoid: exposing internals
(export
  connect disconnect send-message
  internal-buffer retry-count socket-handle)

3. Clear Naming

Use descriptive names that indicate the module's purpose:

(myapp user-authentication)  ; Clear
(myapp ua)                   ; Unclear

4. Documentation

Document public APIs:

(define-library (myapp http)
  (export
   ;; Connect to a URL, returns a connection or #f on failure
   http-connect

   ;; Send a GET request, returns response body as string
   http-get

   ;; Close a connection
   http-close)
  ...)

5. Test Modules Separately

Create corresponding test modules:

myapp/
├── http.sgl
└── test/
    └── test-http.sgl

Compilation

When you use sigil build, modules are automatically compiled to bytecode (.sgb files) which load faster than source.

For standalone applications created with sigil bundle, all modules are bundled into the executable.