Модули, стандартная библиотека и 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 объясняет, что операции означают.
Модуль | Что в нём есть |
|---|---|
| Чтение и запись в порядке big-endian для сетевых и дисковых форматов ( |
| Форматы: |
|
|
| Классы байтов |
| FNV-1a, а также |
|
|
|
|
| Больше сорока модулей: 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. Импорт — |
Числовые помощники — отдельные функции для каждой ширины (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 отличают два правила.
fn-значение ничего не захватывает. Функция видит только свои аргументы и никогда — область видимости, в которой её назвали.
Эффекты передаваемой функции должны быть подмножеством эффектов из типа параметра. Протащить функцию с
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.