Skip to content
Saule
Docs

Imports and File Structure

An import names either a single .sau file or a folder module (a directory with an init.sau — see Folder Modules), and the path is relative to the importing file’s directory, then the project’s src_dirs. The module comes first, so an editor knows what it can offer before you type a name.

-- one name, several, or one under another name
from "entities/Player" import Player
from "utils/Math" import clamp, lerp
from "data/PlayerRepository" import PlayerRepository as PlayerRepo
-- every exported name
from "entities/Player" import *

Or import the module as a whole. Everything it exports comes in, as with from shine import *, and is also reachable through its name:

-- `shine` exports Window, Graphics, Keyboard, Mouse and Timer
import shine -- all five, and shine.Window, shine.Graphics, …
import shine as sh -- all five, and sh.Window, sh.Graphics, …
import "uikit" -- everything uikit exports, and uikit.WindowGroup, …
local a: Window = Window("Demo")
local b: shine.Window = shine.Window("Demo")

An import brings in only what that module exports.

The qualified name is there for when two modules export the same one. A namespace is named after the last part of its path. A path whose last part is not a name, like "my-lib", needs as. shine.Window works anywhere a name or a type does, and a name the module does not export is reported as one: "`shine` has no export named `Windw`".

The path may be written with or without quotes. Unquoted, . separates folders — the two lines below mean exactly the same thing:

from "some/folder/module" import *
from some.folder.module import *

A project is one of two shapes, declared by kind: in saule.config:

kind Has entry: saule run Purpose
"app" (default) yes runs it a program
"library" no refuses, and says why imported by other projects

Scaffold either with saule init:

Terminal window
saule init myapp # an app, with src/main.sau
saule init mylib --lib # a library, with src/init.sau

A library’s src/init.sau is its public surface — whatever that file exports is what importers see. Running one is a category error and reports as such rather than failing on a missing entry file.

A project listed in dependencies: is reachable by its name:. Naming the dependency on its own imports the package itself:

from "json" import Json -- the `json` package
from "json/lexer" import Parser -- a specific module inside it

A package exposes itself through an init.sau in one of its src_dirs — the same folder module rule that applies anywhere else, so there is one convention to learn rather than a special case for dependencies. A package without one can still have its modules imported by path, but its name alone won’t resolve.

json/
├── saule.config name: "json"
└── src/
└── init.sau ← what `import ... from "json"` gets

A folder becomes a single importable module by giving it an init.sau. Importing the folder imports that file, so whatever it exports is the folder’s public surface.

export from passes names on: they are imported here and exported again, so a folder of files can be consumed as one unit.

-- some/folder/module/init.sau
-- Paths are relative to this file.
export from view import *
export from button import Button, Toggle as Switch

Consumers then import the folder itself and get everything it passes on:

from some.folder.module import View, Button
import some.folder.module as ui
local view: View = View("Name")
local s: ui.Switch = ui.Switch()

Only what is written with export from is passed on. A plain from … import keeps its names private to the file, in an init.sau too, so a file’s imports stay an implementation detail. Any file can re-export, and a re-export of a re-export works. A namespace cannot be re-exported; name what to pass on instead.

Add export before a class, interface, enum, function, or variable to make it accessible from other files:

export class Player
-- ...
end
export fn clamp(value: integer, min: integer, max: integer) -> integer
if value < min then return min end
if value > max then return max end
return value
end
export maxPlayers: integer = 8

An export name: T = value is a module variable (see Variables) — a single value shared by every file that imports it, not a copy per importer.

A file without export is private to itself — even sibling files in the same folder can’t see its declarations. The only way to share code across files is to export it and import it explicitly.

Not everything needs a class. Export standalone functions from a utility file and import them by name — a file is not a namespace, so the functions arrive as themselves rather than under the file’s name:

-- utils/Math.sau
export fn clamp(value: integer, min: integer, max: integer) -> integer
if value < min then return min end
if value > max then return max end
return value
end
export fn lerp(a: float, b: float, t: float) -> float
return a + (b - a) * t
end
from "utils/Math" import clamp, lerp
local clamped: integer = clamp(150, 0, 100) -- 100
local smooth: float = lerp(0.0, 1.0, 0.5) -- 0.5
Situation Accessible from
export class Foo anywhere that imports it
class Foo without export only inside the same file
local field or method only within the class
static field or method via ClassName.x anywhere

Saule forbids circular imports at compile time:

ERROR: Circular import detected
Player.sau → Inventory.sau → Player.sau
Hint: Extract shared types into a separate file