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 namespaceExports
Only explicitly exported bindings are visible to importers:
(export
public-procedure ; A procedure
*global-constant* ; A value
my-macro) ; A macroPrivate 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 bindingPrefixes
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 Name | File 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:
| # | Source | Set by |
|---|---|---|
| 1 | -L <dir> | the sigil command line, leftmost -L wins |
| 2 | SIGIL_LIB | environment, colon-separated. Note the RIGHTMOST entry wins, which is the opposite of PATH |
| 3 | paths added while the program runs | add-library-path!, and the CLI's own setup for a project's src/, its build output and the auto-compile cache |
| 4 | <binary>/../lib | a runtime that does not carry a bundle |
| 5 | the bundle embedded in the binary | sigil 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.
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 adev-redirects.sglpath are both registered aftersigil's own modules have been imported.sigil -L src testis 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. Redefiningstring-upcasein your ownsigil/string.sglwill not change whatstring-upcasedoes.
Together those explain the one constraint worth knowing: -L is applied before the runtime loads its own modules, so a -L directory holding a broken or incompatible copy of a module the runtime itself uses will stop it from starting. That is what -L is for, and in the ordinary case the error names the file and line:
Error loading (sigil cli): /home/you/pkg/src/sigil/log.sgl:412:1: 3 unclosed delimiters:
One case gives a much worse error: a -L directory containing sigil/core.sgl. The runtime would have to compile the core macros using the core macros, and what you get is:
Error in macro 'let': unbound variable 'let'
which names neither the file nor the line. Do not put a tree containing sigil/core.sgl on -L; point -L at the package you are working on.
Recommended Structure
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.sglMultiple 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:
- Extract shared code: Move common definitions to a third module
- Dependency injection: Pass procedures as parameters
- 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 everything2. 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) ; Unclear4. 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.sglCompilation
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.