A library is a .loft file you can share across projects. Place it in a lib/ directory and import it at the top of your file. use name; gives you the name:: prefix, which reaches every pub item the library defines. use name::*; also brings every pub name into your own namespace, so you can write them bare; use name::(a, b); brings in just those. A definition without pub is the library's own: neither the bare name nor name::thing reaches it from outside. pub is the library's surface. A name you import is yours to use, not to hand on: a file that imports YOURS does not receive it. Write pub use name::*; to pass the names on as well. use statements must come before any fn or struct definition in your file. Putting a use after a definition is a syntax error.
use testlib::*;
Constants and Enums
A library constant reads like one of your own, and so does an enum variant. You can compare, order, and convert enum values exactly as you would with enums defined in the same file. The libname:: prefix is always available and is what disambiguates a variant name two enums share.
Struct Construction and Field Access
A library struct is constructed like any other. For a pub type the prefix is optional — testlib::Point {…} and Point {…} build the same value — and it is what you reach for when a name of your own would otherwise win. Field access never takes a prefix.
Calling Library Methods
Methods defined in the library are called on the value directly — no prefix on the call site. A pub free function is callable bare as well; the prefix is how you say which library you meant.
Extending a Library Type
You can add new methods to a library type from your own file. Write the self parameter type with the :: separator and the compiler treats the function as a method on that type.
fn shifted(self: testlib::Point, dx: float, dy: float) -> testlib::Point {
testlib::Point {x: self.x + dx, y: self.y + dy }
}
fn main() {
A library constant, both ways: the prefix always works, and the bare name works because use testlib::*; brought every pub name into this file.
assert(testlib::MAX_SIZE == 100, "library constant MAX_SIZE");
assert(MIN_SIZE == 1, "the same constant, written bare");
Library enum variants: comparison and ordering work as normal.
s = testlib::Ok;
assert(s == testlib::Ok, "enum equality");
assert(s < testlib::Error, "enum ordering");
assert("{s}" == "Ok", "enum to text: {s}");
assert("Warning" as testlib::Status == testlib::Warning, "text to enum");
Library enum values can be passed to library free functions.
assert(testlib::status_text(testlib::Error) == "err", "free fn with enum arg");
A struct built through the prefix, and read by plain field access.
p = testlib::Point {x: 3.0, y: 4.0 };
assert(p.x == 3.0, "field access: x={p.x}");
assert(p.y == 4.0, "field access: y={p.y}");
Methods defined in the library are called without a prefix.
dist = p.distance();
assert(round(dist) == 5.0, "method call: distance={dist}");
User-defined method on a library type (self: testlib::Point) works.
assert(p.shifted(1.0, 2.0).x == 4.0, "shifted x");
assert(p.shifted(0.0, 2.0).y == 6.0, "shifted y");
Scalar fields can be mutated directly.
b = testlib::Bag {label: "test", count: 0 };
b.label = "updated";
assert(b.label == "updated", "direct scalar field mutation");
Methods that mutate scalar fields work.
b.bump();
assert(b.count == 1, "method scalar mutation: count={b.count}");
b.bump();
assert(b.count == 2, "method scalar mutation: count={b.count}");
Free functions: a pub one answers to either spelling.
assert(testlib::add(3, 4) == 7, "free function add");
assert(add(0, -5) == -5, "the same free function, written bare");
A definition the library did NOT mark pub is not reachable from outside at all — neither internal_add(3, 4) nor testlib::internal_add(3, 4) compiles here; the second answers "internal_add is not pub in testlib". A library's surface is its pub items, by either spelling. The library's own pub functions still use them:
assert(testlib::add(3, 4) == 7, "the pub surface answers");
Library types work as function parameter types when written with their full namespace prefix in the function signature. A struct with a vector field can be initialised via a struct literal, including elements that are themselves library structs.
bag2 = testlib::Bag {label: "full", count: 3, items: [
testlib::Point {x: 1.0, y: 0.0 },
testlib::Point {x: 0.0, y: 1.0 }
] };
assert(len(bag2.items) == 2, "initialized vector field: {len(bag2.items)}");
assert(bag2.items[0].x == 1.0, "vector field element access");
Appending a struct variable with += [var] and a whole vector with += vec both work.
extra = testlib::Point {x: 7.0, y: 8.0 };
bag2.items +=[extra];
assert(len(bag2.items) == 3, "append via var: len={len(bag2.items)}");
assert(bag2.items[2].x == 7.0, "append via var: x={bag2.items[2].x}");
more_pts =[testlib::Point {x: 9.0, y: 10.0 }, testlib::Point {x: 11.0, y: 12.0 }];
bag2.items += more_pts;
assert(len(bag2.items) == 5, "append vector: len={len(bag2.items)}");
assert(bag2.items[3].x == 9.0, "append vector[3].x={bag2.items[3].x}");
Importing the same library twice is silently ignored. A library can itself import other libraries using use, so dependency chains work.
Package Layout and loft.toml
A library can be distributed as a directory package instead of a single flat file. The packaged directory layout is:
mylib/
loft.toml optional manifest
src/
mylib.loft library source (default entry)
The directory form is found in any search directory you NAMED — --lib <dir>, a path in LOFT_LIB, ~/.loft/lib/ — and in your own project's lib/ once your program is itself a package, with a loft.toml of its own at the top. In all of those, <dir>/mylib/ resolves to <dir>/mylib/src/mylib.loft on its own.
A lone script with no manifest above it reads its lib/ flat, so there a library must be the single file lib/mylib.loft — a lib/mylib/ directory beside such a script answers "Library mylib not found". Giving the script a loft.toml is what turns the directory form on.
The loft.toml manifest carries the whole package declaration — name, version, dependencies, native crates, build targets. Two of its settings decide how the library above is LOADED:
[package]
loft = ">=2026.10" the oldest loft release that can load it
[library]
entry = "src/mylib.loft" override the default entry path
If the running loft is older than the stated minimum, loading the library produces a fatal compile error describing the version mismatch. If no manifest is present, the default entry src/<name>.loft is used.
Wildcard and Selective Imports
A bare use lib; brings in only the lib:: prefix, and use lib::* brings in every pub name. The selective forms give you a say in WHICH names arrive: use lib::Name takes one, and use lib::(Name, Other) takes a group. Multiple names must be parenthesised — the flat use lib::Name, Other is refused ("import multiple names with parentheses"). Only pub definitions can be named this way; the rest are not reachable at all.
Taking only what you name is the stronger position, because it makes you immune to the library GROWING a name. A wildcard import and your own definition of a name the library exports are a conflict, and it is refused naming both sites; under a selective import every name you did not ask for stays yours.
use lib as short; gives you the qualifier under another name — short::name — and, like a bare use lib;, brings nothing in bare. Single names alias too: use lib::Name as N, or use lib::(Name as N, other).
Limitations
`use` must appear before all definitions. If you write a function first and then a use, the compiler reports a syntax error.
`pub` decides the surface. Only pub definitions come in bare, can be named by an import, or answer to lib::name; a library's non-pub items are its own.
Native extensions. A library can be pure .loft, or ship a Rust crate beside it: declare [library] native = "..." in loft.toml and loft builds and links it for you. See doc/claude/PACKAGES.md in the loft repository for the build + binding model.
}
Run it yourself
This page is a loft program. It runs here, in your browser — press Run, then ask it something.
Stop on a line
// Copyright (c) 2024-2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
// @NAME: Libraries
// @TITLE: Library system — importing and using external loft modules.
// A library is a `.loft` file you can share across projects. Place it in a
// `lib/` directory and import it at the top of your file. `use name;` gives you
// the `name::` prefix, which reaches every `pub` item the library defines. `use name::*;`
// also brings every `pub` name into your own namespace, so you can write them bare;
// `use name::(a, b);` brings in just those.
// A definition without `pub` is the library's own: neither the bare name nor
// `name::thing` reaches it from outside. `pub` is the library's surface.
// A name you import is yours to use, not to hand on: a file that imports YOURS does
// not receive it. Write `pub use name::*;` to pass the names on as well.
// `use` statements must come before any `fn` or `struct` definition in your
// file. Putting a `use` after a definition is a syntax error.
use testlib::*;
// ## Constants and Enums
// A library constant reads like one of your own, and so does an enum variant.
// You can compare, order, and convert enum values exactly as you would with
// enums defined in the same file. The `libname::` prefix is always available and
// is what disambiguates a variant name two enums share.
// ## Struct Construction and Field Access
// A library struct is constructed like any other. For a `pub` type the prefix is
// optional — `testlib::Point {…}` and `Point {…}` build the same value — and it
// is what you reach for when a name of your own would otherwise win. Field
// access never takes a prefix.
// ## Calling Library Methods
// Methods defined in the library are called on the value directly — no prefix
// on the call site. A `pub` free function is callable bare as well; the prefix is
// how you say which library you meant.
// ## Extending a Library Type
// You can add new methods to a library type from your own file. Write the
// `self` parameter type with the `::` separator and the compiler treats the
// function as a method on that type.
fn shifted(self: testlib::Point, dx: float, dy: float) -> testlib::Point {
testlib::Point {x: self.x + dx, y: self.y + dy }
}
fn main() {
// A library constant, both ways: the prefix always works, and the bare name
// works because `use testlib::*;` brought every `pub` name into this file.
assert(testlib::MAX_SIZE == 100, "library constant MAX_SIZE");
assert(MIN_SIZE == 1, "the same constant, written bare");
// Library enum variants: comparison and ordering work as normal.
s = testlib::Ok;
assert(s == testlib::Ok, "enum equality");
assert(s < testlib::Error, "enum ordering");
assert("{s}" == "Ok", "enum to text: {s}");
assert("Warning" as testlib::Status == testlib::Warning, "text to enum");
// Library enum values can be passed to library free functions.
assert(testlib::status_text(testlib::Error) == "err", "free fn with enum arg");
// A struct built through the prefix, and read by plain field access.
p = testlib::Point {x: 3.0, y: 4.0 };
assert(p.x == 3.0, "field access: x={p.x}");
assert(p.y == 4.0, "field access: y={p.y}");
// Methods defined in the library are called without a prefix.
dist = p.distance();
assert(round(dist) == 5.0, "method call: distance={dist}");
// User-defined method on a library type (self: testlib::Point) works.
assert(p.shifted(1.0, 2.0).x == 4.0, "shifted x");
assert(p.shifted(0.0, 2.0).y == 6.0, "shifted y");
// Scalar fields can be mutated directly.
b = testlib::Bag {label: "test", count: 0 };
b.label = "updated";
assert(b.label == "updated", "direct scalar field mutation");
// Methods that mutate scalar fields work.
b.bump();
assert(b.count == 1, "method scalar mutation: count={b.count}");
b.bump();
assert(b.count == 2, "method scalar mutation: count={b.count}");
// Free functions: a `pub` one answers to either spelling.
assert(testlib::add(3, 4) == 7, "free function add");
assert(add(0, -5) == -5, "the same free function, written bare");
// A definition the library did NOT mark `pub` is not reachable from outside at all —
// neither `internal_add(3, 4)` nor `testlib::internal_add(3, 4)` compiles here; the
// second answers "`internal_add` is not `pub` in `testlib`". A library's surface is its
// `pub` items, by either spelling. The library's own `pub` functions still use them:
assert(testlib::add(3, 4) == 7, "the pub surface answers");
// Library types work as function parameter types when written with their
// full namespace prefix in the function signature.
// A struct with a vector field can be initialised via a struct literal,
// including elements that are themselves library structs.
bag2 = testlib::Bag {label: "full", count: 3, items: [
testlib::Point {x: 1.0, y: 0.0 },
testlib::Point {x: 0.0, y: 1.0 }
] };
assert(len(bag2.items) == 2, "initialized vector field: {len(bag2.items)}");
assert(bag2.items[0].x == 1.0, "vector field element access");
// Appending a struct variable with `+= [var]` and a whole vector with `+= vec` both work.
extra = testlib::Point {x: 7.0, y: 8.0 };
bag2.items +=[extra];
assert(len(bag2.items) == 3, "append via var: len={len(bag2.items)}");
assert(bag2.items[2].x == 7.0, "append via var: x={bag2.items[2].x}");
more_pts =[testlib::Point {x: 9.0, y: 10.0 }, testlib::Point {x: 11.0, y: 12.0 }];
bag2.items += more_pts;
assert(len(bag2.items) == 5, "append vector: len={len(bag2.items)}");
assert(bag2.items[3].x == 9.0, "append vector[3].x={bag2.items[3].x}");
// Importing the same library twice is silently ignored. A library can
// itself import other libraries using `use`, so dependency chains work.
// ## Package Layout and loft.toml
//
// A library can be distributed as a directory package instead of a single
// flat file. The packaged directory layout is:
//
// mylib/
// loft.toml optional manifest
// src/
// mylib.loft library source (default entry)
//
// The directory form is found in any search directory you NAMED — `--lib
// <dir>`, a path in `LOFT_LIB`, `~/.loft/lib/` — and in your own project's
// `lib/` once your program is itself a package, with a `loft.toml` of its own
// at the top. In all of those, `<dir>/mylib/` resolves to
// `<dir>/mylib/src/mylib.loft` on its own.
//
// A lone script with no manifest above it reads its `lib/` flat, so there a
// library must be the single file `lib/mylib.loft` — a `lib/mylib/` directory
// beside such a script answers "Library 'mylib' not found". Giving the script a
// `loft.toml` is what turns the directory form on.
//
// The `loft.toml` manifest carries the whole package declaration — name,
// version, dependencies, native crates, build targets. Two of its settings
// decide how the library above is LOADED:
//
// [package]
// loft = ">=2026.10" the oldest loft release that can load it
//
// [library]
// entry = "src/mylib.loft" override the default entry path
//
// If the running loft is older than the stated minimum, loading the
// library produces a fatal compile error describing the version mismatch.
// If no manifest is present, the default entry `src/<name>.loft` is used.
// ## Wildcard and Selective Imports
// A bare `use lib;` brings in only the `lib::` prefix, and `use lib::*` brings
// in every `pub` name. The selective forms give you a say in WHICH names
// arrive: `use lib::Name` takes one, and `use lib::(Name, Other)` takes
// a group. Multiple names must be parenthesised — the flat `use lib::Name,
// Other` is refused ("import multiple names with parentheses"). Only `pub`
// definitions can be named this way; the rest are not reachable at all.
//
// Taking only what you name is the stronger position, because it makes you
// immune to the library GROWING a name. A wildcard import and your own definition
// of a name the library exports are a conflict, and it is refused naming both
// sites; under a selective import every name you did not ask for stays yours.
//
// `use lib as short;` gives you the qualifier under another name — `short::name` —
// and, like a bare `use lib;`, brings nothing in bare.
// Single names alias too: `use lib::Name as N`, or `use lib::(Name as N, other)`.
// ## Limitations
//
// **`use` must appear before all definitions.** If you write a function
// first and then a `use`, the compiler reports a syntax error.
//
// **`pub` decides the surface.** Only `pub` definitions come in bare, can be named
// by an import, or answer to `lib::name`; a library's non-`pub` items are its own.
//
// **Native extensions.** A library can be pure `.loft`, or ship a Rust crate
// beside it: declare `[library] native = "..."` in `loft.toml` and loft builds
// and links it for you. See doc/claude/PACKAGES.md in the loft repository for the build + binding model.
}