/
Search
EN

Modules, the standard library, and fn references

Modules

One file is one module, and a directory is a tree of modules. Only items marked pub are visible outside their module; fields carry no visibility of their own and are readable wherever their struct type is. An import names either a whole module or a single item:

use std::encoding::bytes;            // the module: call it as bytes::rd16(...)

fn parse_len[r](frame: &r [u8]) -> u16 !{}
    req frame.len >= 2
{
    bytes::rd16(frame, 0)
}

test wire_helpers {
    region r {
        let b = alloc in r [18u8, 52u8];   // 0x12, 0x34
        assert parse_len(b) == 4660;       // 0x1234
    }
}

use std::encoding::bytes::rd16; would import the one function instead, after which you call it as plain rd16(...). Only pub items can be imported.

A module import may take a local alias, use pb::mid as m; followed by m::six(). You need this when two dependencies expose modules with the same name. Only a module can be aliased. use shapes::box2d::Extent as E; is refused, and the diagnostic offers both repairs: drop the alias, or alias the module (use shapes::box2d as E;, then E::Extent).

impl Type { ... } is a namespace for a type's functions, nothing more. There is no inheritance, no virtual dispatch and no traits. Polymorphism comes from generics and from fn references, described below.

Types are module items, just like functions. A struct or enum declared in module M is used by its bare name inside M. Elsewhere you either import it (use M::T;, then T) or write the path M::T in a type position. There is no program-wide type namespace, so two modules can each declare an unrelated Extent:

// shapes/box2d.kl:  pub struct Extent { w: u64, h: u64 }     pub fn area(e: Extent) -> u64
// shapes/box3d.kl:  pub struct Extent { w: u64, h: u64, d: u64 }  pub fn volume(e: Extent) -> u64
use shapes::box2d;
use shapes::box2d::Extent;
use shapes::box3d;

test module_scoped_types {
    let flat: Extent = Extent { w: 3, h: 4 };                      // bare, imported
    let cube: box3d::Extent = box3d::Extent { w: 2, h: 2, d: 2 };  // qualified path
    assert box2d::area(flat) == 12;
    assert box3d::volume(cube) == 8;
}

The one exception is a short list of names the language itself defines and the compiler constructs directly, such as Trap, TrapKind, FsErr, NetErr, IoErr and ExitStatus. They are global, and declaring your own type with one of those names is refused.

A package of your own

Imports are not limited to std. Put a kola.pack manifest next to your files, and the package name becomes the import root for them. A directory below the root is a module path segment, so geo/line.kl is wire::geo::line. A minimal package with two modules:

wire/
├── kola.pack      # manifest: name, version, budgets
├── frame.kl       # module wire::frame
└── main.kl        # module wire::main
# wire/kola.pack
[pack]
name = "wire"
version = "0.1.0"

[caps]
allowed = []          # no capability at all may appear in this package's code
// wire/frame.kl
pub fn checksum(b: &[u8]) -> u64 !{} {
    let mut s: u64 = 0;
    for x in b {
        s = s +% widen[u64](x);
    }
    s
}
// wire/main.kl
use wire::frame;

test pack_import_works {
    let b = [1u8, 2u8, 3u8];
    assert frame::checksum(&b) == 6;
}

kola test wire/main.kl runs it. The [caps] allowed line is the package's capability budget, explained at the end of this page. (Outside the package, the compiler would answer that only std::... imports resolve.) A dependency on another package is declared in the manifest's [deps] section, and its name then becomes an import root as well. Modules reached through any root can be aliased: use wire::frame as fr; followed by fr::checksum(&b).

Packages are published and fetched with kola pack push and kola pack fetch; a fetch verifies the publisher's signature. While you develop against a sibling directory, the working tree resolves first. The details are in Packages (kola.pack).

The standard library

Most of std is written in kola, and every module carries its own tests. The compiler itself provides the core types (Option, Result, Vec, Pool), the opaque wrappers Secret and Entropy, and the capabilities. Two references tell you what is there. kola help std-index prints every pub item of the std embedded in your compiler, so it answers "does this function exist?". kola help std-card explains what the operations mean.

Module

What it provides

std::encoding::bytes

Big-endian reads and writes for wire and disk formats (rd16, rd32, rd64, wr16 … wr64, push16 …), plus copy(dst, src), a slice-to-slice copy that requires equal lengths. std::encoding::bytes_le is the little-endian twin.

std::encoding::*

Formats: hex, base64, pem, der (ASN.1), json, toml, yaml, xml, url, http, http2, hpack, flate, tls13 and more. DER is an encoding, not cryptography, so it lives here and not under crypto.

std::num::math, std::num::bits, std::num::limits

min_u64, max_u64, clamp_u64, saturating sat_add_u64 and sat_sub_u64; rotations and ctz/clz; the integer limits (U64_MAX, I32_MIN …).

std::text::*

ascii byte classes, utf8 validation (to_str is the only way to make a str from bytes), case folding, display width, decimal parsing and formatting (text::num).

std::hash

FNV-1a, plus crc32 and crc32c. None of these is a cryptographic hash.

std::collections::seq

sort, sort_scratch, lower_bound, contains, reverse, with comparators passed as fn references. sort is a stable insertion sort, O(n²): fine for small or nearly sorted input, a real problem when the input size is up to someone else. sort_scratch(xs, scratch, less) is a stable O(n log n) merge sort; you supply the scratch buffer (req scratch.len >= xs.len), which is why it stays !{}.

std::collections::map

Map, a hash table that stores entries directly in one array (open addressing), whose hash and eq functions are fn references passed to Map::new. Its capacity is a hard bound: there is no rehashing, and inserting far more distinct keys than the construction hint (about four times) traps.

std::crypto::*

Over forty modules: SHA-1/2/3, BLAKE2, HMAC, HKDF, PBKDF2, Argon2, AES-CTR/GCM/XTS, ChaCha20-Poly1305, X25519, ECDH and ECDSA on P-256/P-384, Ed25519, RSA, ML-KEM, ML-DSA, X.509, the TLS 1.3 key schedule and record layer. Import as use std::crypto::sha256; and call sha256::digest(...). See Cryptography in std.

Numeric helpers are separate functions per width (min_u64, min_usize) rather than one generic min[T]. Without traits there is nothing for a numeric generic to hang its operations on.

Embedded files

A file that exists at build time can be compiled into the program. It then has the same status as a b"..." literal: fixed, static data that is never read from disk again. The file is listed in the manifest:

# kola.pack
[embed]
files = ["web/index.html"]
use std::embed;

// No capability and no effect: as pure as a b"..." literal.
fn page() -> &heap [u8] !{} {
    embed::bytes("web/index.html")
}

test the_page_is_compiled_in {
    let p = page();
    assert p.len == 45;
}

page takes no capability and its row is !{}, so "may this package read the filesystem?" is still answered no, and a package that only serves embedded assets can keep [caps] allowed = []. Reading the same file at run time is a different action: it needs an Fs capability, carries the io effect and can fail. Writing the bytes out still needs an output capability; only the embedding is free.

The argument must be a string literal, resolved when the program is checked. A path missing from the [embed] list is a compile error:

use std::embed;

fn main() {
    let _logo = embed::bytes("web/logo.png");   // never added to [embed] files
}
error[card §9]: "web/logo.png" is not listed in `[embed] files` of /path/to/site/kola.pack — embed::bytes resolves at check time against that list
  --> bad.kl:4:17
   |
 4 |     let _logo = embed::bytes("web/logo.png");   // never added to [embed] files
   |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   = help: add "web/logo.png" to the `[embed] files = [...]` list (the file must exist inside the pack root)

Because the manifest is where a reviewer looks, kola audit lists exactly what ships:

$ kola audit .
audit: embed — 1 file(s):
  web/index.html  45 bytes  sha256=b5cd3937e462466f25103c3c489b2333b5237e8484b4df594f2470a53875e1b4

The manifest's rules for embedded files (no .. or absolute paths, symlinks checked, a warning above 1 MiB) are covered in Packages (kola.pack).

Fn references: polymorphism without closures

A top-level function name is a value. You can bind it to a variable, pass it as an argument or store it in a field. Its type spells the parameters, the result and, always, the effect row:

use std::collections::seq;

fn double(x: u64) -> u64 !{} {
    x *% 2
}

fn apply(f: fn(u64) -> u64 !{}, x: u64) -> u64 !{} {
    f(x)
}

fn ge_u64(a: u64, b: u64) -> bool !{} {
    a >= b
}

test fn_references {
    assert apply(double, 21) == 42;      // a function name passed as a value
    let f: fn(u64) -> u64 !{} = double;  // and stored in a variable
    assert f(1) == 2;
}

test sort_with_comparator {
    region r {
        let v: &r mut Vec[u64] = alloc in r mut Vec::new();
        v.push(3u64);
        v.push(1u64);
        v.push(2u64);
        seq::sort(v, seq::less_u64);   // ascending
        assert v[0] == 1;
        seq::sort(v, ge_u64);          // your own comparator: descending
        assert v[0] == 3;
    }
}

That is enough for sorts, hash tables and dispatch tables. Two rules separate it from a language with closures.

  1. A fn value captures nothing. A function sees only its arguments, never the scope it was named in.

  2. The effects of the function you pass must be a subset of the effects in the parameter's type. You cannot slip an io function into pure code through a fn(...) !{} slot, so a sort with someone else's comparator stays provably pure.

The subset rule works in one direction only. A slot typed fn(u64) -> u64 !{io, log} accepts double, whose row is empty. Passing a function with more effects than the slot allows is what fails: apply(noisy, 2) with a !{log} function is refused with "the fn value performs log, which its fn type does not allow".

Fn values can be stored in data too. That gives you a dispatch table, an array that maps a number such as an opcode to the function that handles it:

fn op_add(a: u64, b: u64) -> u64 !{} { a +% b }
fn op_mul(a: u64, b: u64) -> u64 !{} { a *% b }

const OPS: [fn(u64, u64) -> u64 !{}; 2] = [op_add, op_mul];

test dispatch {
    let f = OPS[1];
    assert f(6, 7) == 42;
}

Only free functions can be referenced, not functions inside an impl. A fn value has no ==, so a struct that carries one cannot be compared with == either; compare the data next to it, or a tag that names the function.

When a struct field has a fn type, h.hash(42) does not call through the field. Dot-call syntax looks for a method in an impl block:

fn double(x: u64) -> u64 !{} { x *% 2 }

struct Hasher { hash: fn(u64) -> u64 !{} }

test looks_like_a_method {
    let h = Hasher { hash: double };
    let v = h.hash(42);        // refused: this looks for a method named `hash`
    assert v == 84;
}
error[card §5]: no method `hash` on `Hasher`
  --> h.kl:7:13
   |
 7 |     let v = h.hash(42);        // refused: this looks for a method named `hash`
   |             ^^^^^^^^^^
   = help: dot-call binds fns from `impl` blocks; free fns are called free

Bind the field first, or put the field access in parentheses:

fn double(x: u64) -> u64 !{} { x *% 2 }

struct Hasher { hash: fn(u64) -> u64 !{} }

test fn_field_call {
    let h = Hasher { hash: double };
    let hf = h.hash;           // bind the field's value, then call it
    assert hf(21) == 42;
    assert (h.hash)(5) == 10;  // or parenthesize: field first, then call
}

Packages and trust budgets

[caps] allowed lists the capability types that may appear in the package's code at all, and kola audit compares it with what the code uses. An entry nothing needs is a warning, since a budget wider than the code misleads the reviewer; a name that is not a capability is an error (log is accepted as a budgeted effect). When a public function takes an already-minted handle such as a File as a parameter, the audit prints it on a separate reaches: line, because allowed alone does not show authority the caller spent.

The unit of trust is one line. A parsing library with allowed = [], like wire above, cannot obtain a single capability whatever its code does, so reviewing it as a dependency starts with the manifest and the audit verdict.

The normative reference is SPEC-CARD §1, §2 and §9 (kola help spec-card), and STD-CARD for the library.