Libraries¶
Sushi supports compiling code to reusable libraries and linking them into programs. This enables code sharing, modular architecture, and faster incremental builds.
A .slib is Sushi source plus an index. The consumer compiles that source as ordinary
compilation units and caches the object files, so one library file works on every platform:
text carries no target triple. Binary distribution stays available as an opt-in.
Contributor-level design: see design/libraries.md for how the
.slibcontainer, manifest, and export-closure machinery work internally.
Table of Contents¶
- Overview
- Creating Libraries
- Using Libraries
- Library Search Path
- Inspecting Libraries
- Library Format
- Versions and Compatibility
- Symbol Resolution
- Best Practices
Overview¶
The library system has two operations:
- Compile to a library: turn Sushi source files into one
.slibfile - Use a library: import it with
use <lib/...>
# Create a library
./sushic --lib --lib-version 1.0.0 mathutils.sushi -o mathutils.slib
# Use the library in a program (via use statement in source)
./sushic program.sushi -o program
The consumer does the compiling. A library's units enter the build as ordinary units, they
are type-checked and borrow-checked with everything else, and each one caches its own object
file in __sushi_cache__/. The first build against a library pays for it; later builds do
not.
Creating Libraries¶
The --lib Flag¶
Use --lib to compile source files into a library instead of an executable:
./sushic --lib --lib-version 1.0.0 mylib.sushi -o mylib.slib
This writes one .slib file containing:
- the complete source text of every unit in the library
- a MessagePack index of everything it declares, which --lib-info and the consumer read
Every library states its own version. The value comes from a nori.toml beside the sources
when there is one, and from --lib-version otherwise; neither is CE3505. See
Versions and Compatibility.
Library Kinds¶
--lib-kind chooses what the file carries. The default is source.
| Kind | Ships | Portable | Notes |
|---|---|---|---|
source |
unit source text | yes | the default; the consumer compiles it |
binary |
LLVM bitcode | no | platform-bound (CE3504 elsewhere) |
hybrid |
both | no | the bitcode still binds it to one platform |
# The default: one artifact for every platform
./sushic --lib --lib-version 1.0.0 mylib.sushi -o mylib.slib
# The opt-in: compiled bitcode, for this platform only
./sushic --lib --lib-kind binary --lib-version 1.0.0 mylib.sushi -o mylib.slib
Choose binary when you want to ship a library without shipping its source. Note what that
does not buy: a generic cannot be pre-compiled, because monomorphization needs the
consumer's concrete type arguments, so a binary library carries the source text of its
generics in the index regardless. Binary distribution hides concrete bodies only.
Every kind re-exports. A façade unit that says public use on each of the library's
other units is the way to give a multi-unit library one namespace, and it works whichever
kind you build: a source library ships the statement as text and the consumer's compiler
reads it, a compiled one ships a reexports record per statement and the consumer
composes the namespace from that. A public use <io/fs> hands the module on the same
way, and the consumer's build compiles the module on the strength of the record even
where no unit of its own wrote the import.
Public Declarations¶
Only declarations marked public are accessible from other compilation units. Five kinds
carry the marker -- fn, const, struct, enum and perk -- and private is the
default for all of them:
# mylib.sushi
# This function can be called from programs that use this library
public fn add(i32 a, i32 b) i32:
return Result.Ok(a + b)
# This function is internal to the library
fn helper(i32 x) i32:
return Result.Ok(x * 2)
public fn double_add(i32 a, i32 b) i32:
let i32 sum = add(a, b)??
return Result.Ok(helper(sum)??)
A generic is no exception. public fn pick@(T)(...) is part of the API; fn pick@(T)(...)
is internal, and a consumer that calls it hears CE3005 exactly as it does for a concrete
function. Only a public generic ships as a template, so on the binary path the symbol is
not in the consumer's tables at all -- but the manifest names what the library declares and
keeps, so the answer is CE3005 there too, naming the library instead of a unit. CE2008
is left for what it is for: a name that no unit and no linked library declares.
No main() Required¶
Libraries do not need a main() function. If you include one, compilation will fail.
Structs, Enums, Constants and Perks¶
A type, a constant and a perk are private to the library unless they say public. Only a
marked one is in the manifest, so only a marked one is API a consumer can name -- and only
a marked one is frozen: an unmarked declaration can change shape in the next version
without breaking anybody.
# shapes.sushi
public struct Point: # API: a consumer may name the type
i32 x
i32 y
public enum Color:
Red
Green
Blue
public const i32 MAX_SHAPES = 64
struct Cursor: # a decoder detail; not in the manifest
i32 at
public fn make_point(i32 x, i32 y) Point:
return Result.Ok(Point(x, y))
Two consequences worth knowing:
- A public signature may not name a private type (
CE3009), and a public constraint may not name a private perk (CE3010). Privacy is worth nothing if the signature hands the type out anyway, so mark what the API returns and takes. - A private declaration a template body needs still travels. A public generic's body may name a private type, constant or helper; the export closure ships them so the consumer can monomorphize, and the consumer still cannot name them itself.
- A public constant is API, on both library kinds. A constant has no body to link, so
the manifest carries the declaration's own source and the consumer registers it under
its own name. A consumer's own constant of that name is
CE0105, exactly as it is when the two declarations are two ordinary units.
A consumer that writes a library-private name hears CE3005 -- "private struct 'Cursor',
defined in that library" -- and not "unknown type".
Using Libraries¶
The use <lib/...> Statement¶
To use a library, add a use statement with the lib/ prefix:
# program.sushi
use <lib/mathutils>
fn main() i32:
let i32 result = add(10, 20).realise(0)
println("10 + 20 = {result}")
return Result.Ok(0)
The compiler will:
1. Search for mathutils.slib in the library search path
2. Read metadata and register all functions, structs, and enums
3. Link the bitcode into the final executable
Multiple Libraries¶
Use multiple use statements:
use <lib/math>
use <lib/utils>
fn main() i32:
# Functions from both libraries are available
return Result.Ok(0)
Library Search Path¶
Automatic Discovery via Nori¶
Libraries installed with the Nori package manager are found automatically by the compiler. No environment variable configuration is needed:
nori install math-utils from ./dist/
./sushic program.sushi # finds math-utils.slib automatically
SUSHI_LIB_PATH Environment Variable¶
For libraries not managed by Nori, the compiler searches directories specified by SUSHI_LIB_PATH:
export SUSHI_LIB_PATH=/usr/local/lib/sushi:./libs:~/mylibs
./sushic program.sushi
The path is colon-separated on Unix (semicolon on Windows).
Search Order¶
- Each directory in
SUSHI_LIB_PATH(in order) - Project-local Nori packages (
.sushi_bento/*/lib/) - Global Nori packages (
~/.sushi/bento/*/lib/) - Current working directory (always searched last)
Project-local packages take precedence over global ones, so a version pinned in .sushi_bento/ always wins. See Project Environments for details on how .sushi_bento/ is populated.
Hierarchical Namespaces¶
Libraries can be organized in subdirectories:
libs/
math/
vectors.slib
matrices.slib
utils/
strings.slib
Import with the path:
use <lib/math/vectors>
use <lib/utils/strings>
Inspecting Libraries¶
The --lib-info Flag¶
Use --lib-info to display metadata from a compiled library:
./sushic --lib-info mylib.slib
The plain report is the API surface: one line per symbol. Add --docs to print each
symbol's documentation block under its own line:
./sushic --lib-info mylib.slib --docs
Example output, with --docs:
Library: mylib
Version: 1.0.0
Kind: source
Compiler: 0.11.1
Requires compiler: ~0.11
Compiled: 2026-08-23T10:30:00+00:00
Protocol: 2.0
Units (1):
mylib
Arithmetic that reports its own failures.
Public Functions (3):
fn add(i32 a, i32 b) i32
Adds two numbers.
- Parameter a: The first addend.
- Parameter b: The second addend.
- Returns: The sum.
fn multiply(i32 a, i32 b) i32
fn shout(nom string s) string
Hands the string back, and takes it over.
Structs (1):
struct Point:
A point in the plane.
i32 x
The distance along x.
i32 y
The distance along y.
Enums (1):
enum Color:
Red
Green
Blue
Dependencies (1):
<io/fs>
Source: 1,204 bytes
A documented symbol prints its doc block, indented two spaces under its own line, with a
blank line closing the block and another between one claim and the next; multiply above
has no block, so it prints as a bare line and the run of bare lines stays dense. Without
--docs no block prints at all and the whole report is that dense -- prose is what makes
a report long, and a reader asking what a library exports usually does not want ten
screens of it. A nom parameter shows its mode, which is the one mode a type cannot
spell, and it prints either way. See
Documentation Blocks for the record and
for the few things that do not travel in it.
This is useful for:
- Checking what functions a library exports, and what each one is for
- Reading a library's contracts: every public perk prints with its method signatures, and
Perk Implementations lists which types satisfy each one -- a concrete implementation
and a generic-target template (extend Box@(T) with Show) alike
- Seeing what a unit hands on: Re-exports prints one line per public use, as the
statement was written, so a façade unit's whole surface reads off the report
- Verifying platform compatibility
- Understanding library dependencies
Library Format¶
The .slib Container¶
One file holds a MessagePack index next to a payload, framed by a fixed 52-byte header:
[Magic: 16 bytes] [Version: 4 bytes] [Flags: 4 bytes] [Kind: 4 bytes] [Reserved: 16 bytes]
[Metadata Length: 8 bytes] [Metadata: MessagePack]
[Source Length: 8 bytes] [Source: MessagePack map, unit name -> source text]
[Bitcode Length: 8 bytes] [Bitcode: LLVM]
The Kind field states which payload is present, so a reader can branch before it unpacks
anything. A source library has an empty bitcode section, a binary one an empty source
section, and a hybrid carries both. See Library Format for the full
specification.
Platform Compatibility¶
A source library is portable. It carries text, and text has no target triple, so the
same .slib builds on macOS and on Linux.
A binary library is not. LLVM bitcode looks target-neutral and is not: it carries a target triple and a data layout, and the C ABI is already lowered into it. Loading a binary library built for another platform is a hard error:
CE3504: platform mismatch: library compiled for 'linux', current platform is 'darwin'
The check is skipped entirely for a source library, and --lib-info prints no Platform
line for one, because the field means nothing there.
Portable as text is not the same as portable in behaviour. See Limitation #2 below.
Versions and Compatibility¶
A .slib records two versions, with two different jobs.
library_version — the library's own version¶
major.minor.patch, taken from the first of these that exists:
[package] versionin anori.tomlbeside the sources- the
--lib-version X.Y.Zflag
Neither present is CE3505, and so is a --lib-version that contradicts the
nori.toml — silently preferring one would let a package ship under a version it does not
claim. The packager stays the source of truth for a real package, without forcing a manifest
on a bare ./sushic --lib build.
requires_compiler — which compilers can build it¶
A source library is compiled by the consumer's compiler, not the author's. So a library that built cleanly under one compiler can fail under a later one. That is the standard cost of source distribution, and it is not fixable — only declarable.
Every build stamps a constraint. The default is ~<major>.<minor> of the building compiler,
so a compiler at 0.11.1 writes ~0.11: every 0.11.z is accepted and 0.12.0 is not.
Pre-1.0 semver makes the minor the breaking unit, which is how Sushi's 0.x releases already
behave.
A library the running compiler does not satisfy is a hard error:
CE3503: library 'mylib' accepts compiler ~0.11, this is 0.12.0
Not a warning. A real incompatibility that is only warned about surfaces later as a confusing error deep inside library source you never wrote.
The escape is --ignore-compiler-version, for an author testing a library forward against a
new compiler. It is build-wide and obviously temporary, on purpose.
Symbol Resolution¶
Two-Phase Linking¶
Sushi uses a two-phase linking process to handle symbol conflicts:
- Extract: Parse all modules and build symbol tables
- Resolve: Deduplicate symbols using priority rules
- Merge: Build final module with resolved symbols
Priority Rules¶
When the same symbol is defined in multiple places:
| Priority | Source | Description |
|---|---|---|
| 1 (highest) | Main program | Your program's definitions win |
| 2 | User library | Library definitions |
| 3 | Standard library | Stdlib definitions |
| 4 (lowest) | Runtime | Runtime helper functions |
This means you can override library functions in your main program.
A symbol carries the unit that declared it -- <unit>$<name>, with every / in the unit
name becoming $ -- so two units may each declare a private helper without colliding
(docs/design/unit-namespaces.md section 9). The priority table above is about a name two
units both offer, not about a symbol two units both take.
Which Symbol a Binary Library's Body Calls¶
A source library recompiles at the consumer, and its units are renamed to
lib/<library>/<unit> on the way in, so the consumer derives every symbol itself. A
binary library links: its bodies are in the shipped bitcode, and the symbols in them
were named by the PRODUCER's compiler. The manifest records those names in link_symbol,
one per record that has a symbol, and the binary path is the only reader. The manifest also
records the declaring unit on every record, which is a different question and is answered
for both kinds. docs/library-format.md carries the schema and the reasoning.
Dead Code Elimination¶
Only symbols reachable from main() are included in the final executable. Unused library functions are automatically removed, reducing binary size.
Best Practices¶
1. Use Public Sparingly¶
Mark a declaration public only if it is part of your library's API. Everything you mark
is API you have to keep; everything you leave unmarked you can change:
# Good: only the API is marked
public const i32 SCALE = 2
public fn calculate(i32 x) i32:
return Result.Ok(internal_helper(x)??)
struct Work:
i32 at
fn internal_helper(i32 x) i32:
return Result.Ok(x * SCALE)
zlib is the example the rule is for: 38 functions and 13 types and constants, of which 6
functions and one enum belong in the API. It exports 7 names instead of 19.
2. Document Your Library¶
Write a ##: ... :## doc block on every public symbol. A block is part of the declaration,
so the library carries it and --lib-info prints it; a # comment is dropped at the
boundary and reaches nobody.
An - Example: is worth writing on a public symbol: the code travels in the index, and
python tests/docs_sweep.py compiles and runs it against the library's own source, so an
example that drifts out of date says so. --lib-info does not print one -- a fenced program
inside a plain dump would bury the signature.
##:
Adds two integers.
- Parameter a: The first addend.
- Parameter b: The second addend.
- Returns: The sum.
- Errors: `MathError.Overflow` when the sum does not fit an `i32`.
:##
public fn safe_add(i32 a, i32 b) i32 | MathError:
return Result.Ok(a + b)
A block first in the file documents the unit itself, which is the right place for what the library as a whole is for. Documentation Blocks is the guide.
3. Organize with Namespaces¶
Use directory structure to organize related libraries:
myproject/
libs/
math/
basic.slib
advanced.slib
io/
network.slib
files.slib
4. State a Version¶
Every library records its own version, so it does not belong in the filename. Let a
nori.toml supply it for a real package, and pass --lib-version for a one-off build:
./sushic --lib --lib-version 1.0.0 mylib.sushi -o mylib.slib
5. Test Libraries Independently¶
Create test programs that exercise your library functions:
# test_mylib.sushi
use <lib/mylib>
fn main() i32:
# Test cases
let i32 r1 = add(1, 2).realise(-1)
if (r1 != 3):
println("FAIL: add(1, 2) = {r1}, expected 3")
return Result.Ok(1)
println("All tests passed")
return Result.Ok(0)
Limitations¶
Current limitations of the library system:
- No transitive dependencies: If library A depends on library B, you must import both
explicitly. A library's own
use <lib/...>is not followed, and a library exports nothing of the stdlib module or the library it imports — a consumer states each one for itself. - Portable as text, not automatically in behaviour: a source library compiles anywhere,
but Sushi has no conditional compilation — no
cfg, no build tags, no per-platform source files. A library that binds a platform-specific C function throughunsafe externalstill only builds where that function exists, and it cannot yet say so. - A binary library is platform-bound:
--lib-kind binaryorhybridships bitcode, which is bound to the platform that produced it (CE3504). - A public generic cannot reach FFI: a public generic whose body (transitively)
references an
unsafe externalnamespace, or a private helper whose signature exposes a foreignptr, cannot be exported (CE5006; see also CE5002). Wrap the foreign detail behind a private helper with a C-ABI-free signature. This applies to every kind, source included. - A public native variadic cannot be exported: a
...Tvariadic collects into a runtimeT[]inside one concrete function, so there is no template to monomorphize and public export is CE0116. A type pack (...Ts) is different: it exports as a template. This applies to every kind, source included. - Generic instantiation across a BINARY boundary: the notes below describe how generics
cross a
--lib-kind binarylibrary. A source library needs none of this machinery — its generics are ordinary source in ordinary units, so they monomorphize exactly as they would in a multi-file program. Regular generic functions, variadic-generic pack functions (...Ts), and generic structs/enums can be instantiated across.slibboundaries.
The library producer ships a re-parsable source template in the .slib templates
section (templates version 4); the consumer re-parses it, registers it alongside its own
definitions, and monomorphizes it at consumer call sites using the standard instantiate/monomorphize
machinery. A pack function carries type_params (the ...Ts is recorded with is_pack), so it
ships as a template and is monomorphized per call site exactly like a regular generic. Perk
definitions are also shipped so consumers do not need to redeclare a perk contract that
originates in the library. Constraint re-checking uses CE4006 against the consumer's
perk-impl table.
Perk implementations also ship (concrete impls only): a library's own
extend <ConcreteType> with <Perk>: block for a shipped perk crosses the boundary, so a
consumer can instantiate e.g. pick_bigger@(T: Doubler) at i32 without writing
extend i32 with Doubler itself. The impl's bodies are not re-compiled at the consumer - its
signatures register for constraint checking and dispatch, the method symbols are declared, and
the definitions link from the library bitcode (where they carry weak linkage). Precedence:
a consumer's own impl of the same (type, perk) always wins, both semantically and at link
time; across multiple libraries shipping the same impl, the first registered wins; if a local
extension method on the target type already uses one of the impl's method names, the library
impl is skipped entirely (write your own extend to opt in, which surfaces the normal
CE4007 conflict diagnostics). Only impls of perks referenced by an exported generic's
constraints ship; impls of library-internal perks stay internal.
Private helpers ship automatically (the export closure): a public generic whose body
references library-private symbols no longer fails to export - the producer walks the
transitive closure of everything the generic depends on and ships it: private generic
helpers as source templates (flagged private), private concrete helpers as signature
records (their definitions carry external linkage in the library bitcode and link at the
consumer), and constants with their source (the consumer needs the value for compile-time
evaluation). The manifest's templates.closure_summary lists what shipped, by kind. At the
consumer, a local symbol with the same name as a shipped private is an error (CE5007,
not local-wins): shadowing it would silently change what the library's monomorphized bodies
call. A shipped private helper is callable by the library's own bodies and by nothing
else: consumer code that names one is CE3005, like any other private function. The
exception is a constant, which has no private form yet (#466), so a shipped one is
readable. None of this can arise on the source path: library units are namespaced, so
there is no shared namespace to clash in, and nothing has to be shipped ahead of need.
A private the closure does not ship is named too. The closure only walks what a
public generic needs, so a private a concrete function calls -- or one nothing public
calls -- ships nowhere. The manifest's not_exported key carries those names and their
kind, and nothing else: no signature, no body, no source. It is what lets the consumer
hear CE3005 for them rather than CE2008 (#469). A name in that list is not shipped,
so it clashes with nothing: a consumer may declare a function of the same name and it is
the consumer's own.
Remaining restriction on the binary path:
- Generic-target perk impls do not ship: extend <Generic@(T)> with <Perk> is not supported
in-program, so only concrete-target impls cross the boundary.
These limitations may be addressed in future versions.
See Also¶
- Nori Package Manager - Packaging and distributing libraries
- Compiler Reference - All compiler options
- Getting Started - Introduction to Sushi
- Standard Library - Built-in library modules