SagaSaga
Guide

Project & Modules

So far, every example has been a single-file script with a main function. Modules let you split code across files with namespaces and visibility control.

Creating a Project

To use modules, you need a project. Create one with:

saga new my-app

This creates a directory with a project.toml and a Main.saga file. From here you can add more .saga files, each declaring its own module.

Declaring a Module

A module file declares its name at the top:

module Math

Module names do not need to match the file path. A file at lib/utils/helpers.saga can declare module Helpers or module Utils.Helpers or anything else. The compiler discovers all .saga files in your project and resolves imports by declared module name.

Nested modules use dots:

module Data.Collections

Script files (single files with just a main function) do not declare a module name. Modules are only used in projects.

Imports

Import a module to use its public definitions:

# Import a module (access via Math.abs, Math.max)
import Math

# Import with an alias (access via M.abs, M.max)
import Math as M

# Import specific names into scope (access as bare abs, max)
import Math (abs, max)

# Both alias and specific names
import Math as M (abs, max)

A plain import Math gives you qualified access only: Math.abs, Math.max. To use names without a qualifier, import them explicitly with import Math (abs, max).

When two modules export the same name, use qualifiers to disambiguate:

import Data.List
import Data.Set

let xs = List.map f items
let ys = Set.map f items

Aliases let you choose a different qualifier:

import Data.List as L

L.map f items

Visibility

By default, definitions are private to their module. Use pub to export them:

pub fun add : Int -> Int -> Int       # function
pub type Shape = Circle Float | Rect Float Float   # ADT
pub record User { name: String, age: Int }         # record
pub handler console for Log { ... }   # handler

Public functions require a type annotation. This means making something public forces you to document its type, which serves as both a stable API contract and inline documentation for anyone reading the module's exports. Private functions can be fully inferred:

# Public: annotation required
pub fun abs : Int -> Int
abs n when n < 0 = -n
abs n = n

# Private: annotation optional, type is inferred
double x = x * 2

Opaque Types

An opaque type exports the type name but hides its constructors. Other modules can pattern match on it but cannot construct values directly:

module Auth

opaque type Token = Token String

pub fun make_token : String -> Token
make_token secret = Token (hash secret)
module App

import Auth (Token, make_token)

# Pattern matching is fine
case token {
  Token _ -> "has a token"
}

# Type error: Token constructor is not visible outside Auth
let bad = Token "forged"

This enforces invariants: values of the type can only be created through the module's public API.

Project Structure

A Saga project uses a project.toml file at its root. The simplest form declares an executable:

[project]
name = "my-app"

[bin]
main = "src/Main.saga"

[bin].main defaults to Main.saga. The main file must define a main function.

The compiler scans all .saga files in the project directory to build the module map. Running saga build, saga run, or saga test starts from the project root (the directory containing project.toml).

Test files live in a tests/ directory by default and are discovered automatically by saga test.

Libraries

A project can be a library that other projects depend on. Add a [library] section to declare what's exposed:

[project]
name = "mathlib"

[library]
module = "Math"                                  # root namespace, required
expose = ["Math", "Math.Vector", "Math.Matrix"]  # public modules, required
  • [library].module is the root namespace. Every module listed in expose must be prefixed by it.
  • [library].expose lists the modules consumers can import. Unlisted modules are still compiled (they're needed at runtime) but are invisible to the type system, so consumers can't import them. This lets you keep internal helpers private while still using them inside the library.

A project can have [library], [bin], or both. A combined project ships an executable while also being usable as a dependency:

[project]
name = "my-app"

[library]
module = "MyApp"
expose = ["MyApp.Client", "MyApp.Types"]

[bin]
main = "Main.saga"

For consuming dependencies (Hex, git, path), see Ecosystem. For calling Erlang or Elixir libraries directly, see Interop.