sigildocs

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
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/, its build output and the auto-compile cache
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.

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.

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