/
Поиск
RU

Модули, стандартная библиотека и fn-ссылки

Модули

Один файл — один модуль, каталог — дерево модулей. Снаружи модуля видно только то, что помечено pub; у полей собственной видимости нет, и поле можно читать везде, где доступен тип его структуры. Импорт называет либо модуль целиком, либо один элемент:

use std::encoding::bytes;            // модуль: вызов выглядит как 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; импортировал бы одну функцию, и вызывать её можно было бы просто как rd16(...). Импортировать можно только элементы с pub.

Импорту модуля можно дать локальный псевдоним: use pb::mid as m;, после чего пишется m::six(). Это нужно, когда две зависимости предоставляют модули с одинаковыми именами. Псевдоним бывает только у модуля. use shapes::box2d::Extent as E; компилятор отвергнет и предложит оба исправления: убрать псевдоним или дать его модулю (use shapes::box2d as E;, затем E::Extent).

impl Type { ... } — это пространство имён для функций типа, и ничего больше. Наследования, виртуальных вызовов и трейтов нет. Полиморфизм дают дженерики и fn-ссылки, о которых ниже.

Типы — такие же элементы модуля, как функции. struct или enum, объявленный в модуле M, внутри M называют просто по имени. В других местах его либо импортируют (use M::T;, затем T), либо пишут путь M::T там, где ожидается тип. Общего для всей программы пространства имён типов нет, поэтому два модуля могут объявить каждый свой, никак не связанный 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 };                      // просто имя, импортировано
    let cube: box3d::Extent = box3d::Extent { w: 2, h: 2, d: 2 };  // полный путь
    assert box2d::area(flat) == 12;
    assert box3d::volume(cube) == 8;
}

Исключение одно: короткий список имён, которые определяет сам язык и которые компилятор создаёт напрямую, например Trap, TrapKind, FsErr, NetErr, IoErr и ExitStatus. Они глобальны, и объявить свой тип с таким именем нельзя.

Собственный пакет

Импорт не ограничен std. Положите рядом с файлами манифест kola.pack, и имя пакета станет для них корнем импорта. Подкаталог — это очередной сегмент пути модуля, так что geo/line.kl — это wire::geo::line. Минимальный пакет из двух модулей:

wire/
├── kola.pack      # манифест: имя, версия, бюджеты
├── frame.kl       # модуль wire::frame
└── main.kl        # модуль wire::main
# wire/kola.pack
[pack]
name = "wire"
version = "0.1.0"

[caps]
allowed = []          # в коде пакета не может появиться ни одна капабилити
// 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. Строка [caps] allowed — бюджет капабилити пакета; о нём в конце главы. (Вне пакета компилятор ответил бы, что разрешаются только импорты std::....) Зависимость от другого пакета объявляется в разделе [deps] манифеста, и тогда её имя тоже становится корнем импорта. Модулям, найденным через любой корень, можно давать псевдонимы: use wire::frame as fr;, затем fr::checksum(&b).

Пакеты публикуются и скачиваются командами kola pack push и kola pack fetch; при скачивании проверяется подпись издателя. Пока вы разрабатываете против соседнего каталога, первой разрешается рабочая копия. Подробности — в главе Пакеты (kola.pack).

Стандартная библиотека

Почти вся std написана на kola, и у каждого модуля есть свои тесты. Сам компилятор предоставляет базовые типы (Option, Result, Vec, Pool), непрозрачные обёртки Secret и Entropy и капабилити. Что в ней есть, подскажут два справочника. kola help std-index печатает все pub-элементы std, встроенной в ваш компилятор, и отвечает на вопрос «существует ли такая функция?». kola help std-card объясняет, что операции означают.

Модуль

Что в нём есть

std::encoding::bytes

Чтение и запись в порядке big-endian для сетевых и дисковых форматов (rd16, rd32, rd64, wr16 … wr64, push16 …), а также copy(dst, src) — копирование среза в срез, требующее равных длин. std::encoding::bytes_le — его little-endian-близнец.

std::encoding::*

Форматы: hex, base64, pem, der (ASN.1), json, toml, yaml, xml, url, http, http2, hpack, flate, tls13 и другие. DER — это кодировка, а не криптография, поэтому он живёт здесь, а не в crypto.

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

min_u64, max_u64, clamp_u64, насыщающие sat_add_u64 и sat_sub_u64; циклические сдвиги и ctz/clz; границы целых типов (U64_MAX, I32_MIN …).

std::text::*

Классы байтов ascii, проверка utf8 (to_str — единственный способ получить str из байтов), приведение регистра, ширина при отображении, разбор и форматирование десятичных чисел (text::num).

std::hash

FNV-1a, а также crc32 и crc32c. Ни один из них не криптографический.

std::collections::seq

sort, sort_scratch, lower_bound, contains, reverse; компараторы передаются как fn-ссылки. sort — устойчивая сортировка вставками, O(n²): хороша для маленьких или почти упорядоченных входов и опасна, когда размер входа решает кто-то другой. sort_scratch(xs, scratch, less) — устойчивая сортировка слиянием за O(n log n); буфер вы передаёте сами (req scratch.len >= xs.len), поэтому она остаётся !{}.

std::collections::map

Map — хеш-таблица, хранящая записи прямо в одном массиве (открытая адресация); функции hash и eq передаются в Map::new как fn-ссылки. Ёмкость — жёсткая граница: перехеширования нет, и вставка намного большего числа различных ключей, чем указано при создании (примерно вчетверо), срабатывает трапом.

std::crypto::*

Больше сорока модулей: SHA-1/2/3, BLAKE2, HMAC, HKDF, PBKDF2, Argon2, AES-CTR/GCM/XTS, ChaCha20-Poly1305, X25519, ECDH и ECDSA на P-256/P-384, Ed25519, RSA, ML-KEM, ML-DSA, X.509, расписание ключей и уровень записей TLS 1.3. Импорт — use std::crypto::sha256;, вызов — sha256::digest(...). См. Криптография в std.

Числовые помощники — отдельные функции для каждой ширины (min_u64, min_usize), а не один обобщённый min[T]. Без трейтов числовому дженерику не на что опереться.

Встроенные файлы

Файл, существующий на момент сборки, можно вкомпилировать в программу. Тогда у него тот же статус, что у литерала b"...": неизменные статические данные, которые больше никогда не читаются с диска. Файл указывается в манифесте:

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

// Ни капабилити, ни эффекта: так же чисто, как литерал b"...".
fn page() -> &heap [u8] !{} {
    embed::bytes("web/index.html")
}

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

page не принимает капабилити, а её ряд эффектов — !{}. Значит, на вопрос «может ли этот пакет читать файловую систему?» ответ по-прежнему «нет», и пакет, который только раздаёт встроенные ресурсы, может оставить [caps] allowed = []. Прочитать тот же файл во время выполнения — совсем другое действие: для этого нужна капабилити Fs, эффект io, и чтение может не удаться. Вывод байтов наружу тоже требует капабилити вывода; бесплатно только встраивание.

Аргумент должен быть строковым литералом: он разрешается при проверке программы. Путь, которого нет в списке [embed], — ошибка компиляции:

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)

Рецензент смотрит в манифест, поэтому kola audit перечисляет ровно то, что попадает в программу:

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

Правила манифеста для встроенных файлов (никаких .. и абсолютных путей, проверка символических ссылок, предупреждение для файлов больше 1 МиБ) описаны в главе Пакеты (kola.pack).

fn-ссылки: полиморфизм без замыканий

Имя функции верхнего уровня — это значение. Его можно связать с переменной, передать аргументом или сохранить в поле. Тип такого значения перечисляет параметры, результат и — обязательно — ряд эффектов:

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;      // имя функции передано как значение
    let f: fn(u64) -> u64 !{} = double;  // и сохранено в переменной
    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);   // по возрастанию
        assert v[0] == 1;
        seq::sort(v, ge_u64);          // свой компаратор: по убыванию
        assert v[0] == 3;
    }
}

Этого хватает для сортировок, хеш-таблиц и таблиц диспетчеризации. От языков с замыканиями kola отличают два правила.

  1. fn-значение ничего не захватывает. Функция видит только свои аргументы и никогда — область видимости, в которой её назвали.

  2. Эффекты передаваемой функции должны быть подмножеством эффектов из типа параметра. Протащить функцию с io в чистый код через параметр fn(...) !{} нельзя, поэтому сортировка с чужим компаратором остаётся доказуемо чистой.

Правило подмножества действует в одну сторону. Параметр типа fn(u64) -> u64 !{io, log} примет double с пустым рядом эффектов. Не пройдёт обратное — функция с бо́льшим набором эффектов, чем разрешает параметр: apply(noisy, 2) с функцией !{log} компилятор отвергнет с сообщением «the fn value performs log, which its fn type does not allow».

fn-значения можно хранить и в данных. Так получается таблица диспетчеризации — массив, сопоставляющий числу (например, коду операции) функцию, которая его обрабатывает:

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;
}

Ссылаться можно только на свободные функции, не на функции внутри impl. У fn-значения нет ==, поэтому структуру, которая его содержит, тоже нельзя сравнить через ==; сравнивайте данные рядом с ним или метку, обозначающую функцию.

Если поле структуры имеет функциональный тип, h.hash(42) не вызывает функцию через поле. Вызов через точку ищет метод в блоке impl:

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

Достаточно сначала связать поле с именем или взять обращение к полю в скобки:

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;           // взять значение поля, потом вызвать
    assert hf(21) == 42;
    assert (h.hash)(5) == 10;  // или скобки: сначала поле, потом вызов
}

Пакеты и бюджеты доверия

[caps] allowed перечисляет типы капабилити, которые вообще могут встретиться в коде пакета, а kola audit сверяет этот список с тем, что код действительно использует. Лишняя запись — предупреждение: бюджет шире кода вводит рецензента в заблуждение. Имя, которое не является капабилити, — ошибка (log допускается как эффект с бюджетом). Если публичная функция принимает параметром уже созданный дескриптор, например File, аудит выводит его отдельной строкой reaches:: одного allowed недостаточно, чтобы увидеть полномочия, потраченные вызывающим.

Единица доверия — одна строка. Библиотека разбора с allowed = [], как wire выше, не получит ни одной капабилити, что бы ни было в её коде, и проверку такой зависимости начинают с манифеста и вердикта аудита.

Нормативный источник — SPEC-CARD §1, §2 и §9 (kola help spec-card), для библиотеки — STD-CARD.