Reference / Modules
Modules
A program can be more than one file. Each file is a module, and its names are private to it unless it marks them pub. Other modules use them through import.
The examples on this page have more than one file. Each block is one file, and its first line names the file. sisu run main.sisu runs the program.
// report/table.sisu
pub class Table {
pub title: string
mut rows: [string]
pub fn add(self, row: string) {
self.rows.push(row)
}
pub fn size(self) -> i64 {
self.rows.len()
}
}
pub fn titled(title: string) -> Table {
Table(title: title, rows: [])
}
// main.sisu
import report.table
fn main() {
let t = table.titled("Saunas")
t.add("Löyly")
print(t.title) // Saunas
print(t.size()) // 1
}
Modules and packages In development
A module's path from the root names it. The file report/table.sisu is the module report.table. No declaration lists the modules: the file tree is the module tree.
A folder is a package. Its front door is the file pkg.sisu in that folder: cli/pkg.sisu is the module cli, and cli/flags.sisu is the module cli.flags.
// cli/pkg.sisu
pub fn usage() -> string {
"usage: tally [--verbose] FILE"
}
// cli/flags.sisu
pub fn is_verbose(arg: string) -> bool {
arg == "--verbose" || arg == "-v"
}
// main.sisu
import cli
import cli.flags
fn main() {
print(cli.usage()) // usage: tally [--verbose] FILE
print(flags.is_verbose("-v")) // true
}
The rules:
- A module name is lowercase ASCII: a letter or
_, then letters, digits and_. It is not a keyword. A file with another name, such asMy-Tool.sisu, can be an entry file, but never a module. - An import must match the file name byte for byte, also on a file system that ignores case.
import reportdoes not findReport.sisu. Two names in one folder that differ only in case are an error when an import names either one. - A folder without
pkg.sisuis not a module, but its files are:import cli.flagsworks withoutcli/pkg.sisu, andimport cliis an error that lists the modules incli/. import cli.pkgis an error, becausecli/pkg.sisuis the modulecli. Only the file namepkg.sisuis reserved: a folder namedpkgis an ordinary package, socli/pkg/pkg.sisuis the modulecli.pkg. When bothcli/pkg.sisuandcli/pkg/exist,import cli.pkgis an error that names both.cli.sisubesidecli/pkg.sisumakesimport clian error that names both files.- A
pkg.sisuin the root has no module name, and no import reaches it.
The root and the program
The root is the folder of the entry file, the file that sisu run, sisu build or sisu check gets. Import paths start from the root. sisu build dir/ builds dir/main.sisu, and with -e the root is the current folder.
The program is the entry file and each file that its imports reach. sisu reads no other file, so a half-written file next to a script does not break it.
- An import cannot go above the root. Import paths have no
... sisureplaces a symbolic link with its real path. A link whose real path is outside the root is an error. When the entry file is a link, the root is the real file's folder.- One file that two import paths reach is one module.
- The first segment
stdalways names the standard library, never a file or folder in the root. Until the standard library comes,import std.osis an error: "the standard library has no moduleos".
Only the entry file's main runs. A main in another module is an ordinary function, so one file can be a script and a module. Another module can import the entry file.
Imports In development
An import line names a module by its path and binds names in this file.
// main.sisu
import report.table // binds `table`
import report.table.{titled, Table as Grid} // binds `titled` and `Grid`
fn main() {
let a: Grid = titled("Saunas")
let b = table.titled("Lakes")
print("{a.title} and {b.title}") // Saunas and Lakes
}
import a.bbinds the last segment,b. Code then writesb.Xfor an itemXofa.b.import a.b.{X, Y}binds thepubitemsXandYofa.bwithout a prefix. The braces name items only, never a module:import report.{table}is an error whenreport/pkg.sisuhas no item namedtable, and the fix writesimport report.table.asrenames a module or an item:import report.table as tablesbindstables.- A submodule needs its own import. After
import cli, the namecli.flags.is_verboseis an error; writeimport cli.flags, thenflags.is_verbose(arg). - Imports come before the first item of a file. A module cannot import itself, but modules can import each other in a cycle.
- At the top level of a module, a name binds one thing. Two imports that bind one name, an import that binds a name that the module declares, and an import that binds a prelude name such as
printare errors (E0034).asgives an import another name. - A local, parameter or loop variable cannot take the name of an imported module. It can take the name of an imported item, as it can take the name of a function (Shadowing).
- An import that nothing uses gets a warning, and its fix removes it. An import counts as used when the code names it, or when the module it imports reaches, through its own imports, a module with a conformance that the program uses.
- The prelude is visible in every file without an import (Option and Result).
pub import,pub(...)and a---fence at the top of a file are reserved. Each is an error that says the feature is not in Sisu yet.
Visibility In development
Each top-level item is private to its module unless it is pub: a function, class, enum, constant, interface, type alias or record. In its own module, every name is visible.
Each field and each method needs its own pub. A pub field can be read in other modules. A pub mut field can also be assigned there.
// bank.sisu
pub class Account {
pub owner: string
pub mut note: string
mut balance: i64
pub fn deposit(self, amount: i64) {
self.balance += amount
}
pub fn balance(self) -> i64 {
self.balance
}
}
pub fn open(owner: string) -> Account {
Account(owner: owner, note: "", balance: 0)
}
// main.sisu
import bank
fn main() {
let a = bank.open("Eero")
a.deposit(10)
a.note = "savings"
print("{a.owner}: {a.note}") // Eero: savings
print(a.balance()) // 10
}
In main.sisu, a.balance = 5 is an error, because the field balance is private. a.owner = "Aino" is an error too: owner is pub, not pub mut. bank.Account(owner: "Aino", note: "", balance: 0) is also an error, because Account has a private field. Other modules make an Account through bank.open.
For a field that other modules read but only its module changes, write a private mut field and a pub method that returns it, as balance does above.
The rules:
- Enum variants and their payloads are as visible as their enum. Interface requirements are as visible as their interface.
pubon one of them is an error. - A conformance has no
pub. Its methods are as visible as the interface. - A class or record with a private field cannot be built in another module. Another module also cannot change a copy of it with a record update, or take it apart in a pattern. A pattern that binds the whole value works. So the module decides which values exist: an opaque type.
- A
pubitem cannot name a private type in its signature: a parameter, a result, apubfield, a payload of apubenum, a type alias, a bound or an associated type. A private field can have a private type. The arguments of a conformance of apubtype to apubinterface follow the same rule. - A method can have the name of a private field when the field's type is not a function type, as
balancedoes above. - A
pubitem that nothing in the program uses gets the unused-name warning (W0012), as a private one does.
Names reached through a type In development
A value's fields and methods are found through its type, with no import of the type's module.
// teller.sisu
import bank
pub fn open_savings(owner: string) -> bank.Account {
let a = bank.open(owner)
a.note = "savings"
a
}
// main.sisu
import teller
fn main() {
let a = teller.open_savings("Eero")
a.deposit(25)
print(a.balance()) // 25
}
main.sisu does not import bank, but it calls the pub methods of bank.Account.
- A generic's body keeps the privacy of its own module, in every module that uses it (Generics).
- A
pub constcan use apub constof another module. In one module, a constant still uses only the constants above it. Constants of several modules that use each other are an error (E0087).
Conformances and extend In development
A conformance extend T: I<A> is allowed in the project that declares T, I or a type in A. So two projects can never both conform one type to one interface with the same arguments (Generics).
// csv/pkg.sisu
pub interface CsvRow {
fn csv_row(self) -> string
}
// model.sisu
pub class User {
pub name: string
pub email: string
}
// output/user_csv.sisu
import csv.{CsvRow}
import model.{User}
extend User: CsvRow {
fn csv_row(self) -> string {
"{self.name},{self.email}"
}
}
// main.sisu
import model.{User}
import output.user_csv // used: it adds User: CsvRow
fn main() {
let u = User(name: "Aino", email: "aino@example.com")
print(u.csv_row()) // Aino,aino@example.com
}
- The rule looks only at the outermost type of
T, ofIand of each argument inA.[User]andUser?do not count as types of the program, and neither do the type arguments ofT:extend Result<User, MyErr>: Displayis an error. - In a program without a
sisu.toml, every file is in one project. So the rule rejects only a conformance where the type, the interface and every argument come from the prelude or the standard library:extend IoError: Displayis an error, andextend IoError: Convert<TotalsError>is allowed. - A conformance can be in any module of its project. A call finds its methods through the value's type when the conformance's file is part of the program, with no import of the interface. Above,
main.sisuimportsoutput.user_csvonly to make that file part of the program. - A plain
extend T { ... }, with methods and no interface, must be inT's module, so a type's own methods are always found with the type. - A conformance that replaces a generated one, such as a hand-written
DisplayorEq, must be in its type's module. Otherwisea == bcould give another answer when the file is not part of the program.
Names in messages
A message from sisu writes a type's short name, such as Account. When two types of one name meet in one message, both get their module path: a.Account and b.Account. A panic message writes the file's path from the root: report/table.sisu:3:5.
A class's generated Display prints only its short name, so moving a file never changes what a program prints. A record's generated Display prints every field, private fields too. To hide a field, write the record's own Display.
Sisu