Шпаргалка: чего нет и частые ошибки
kola — небольшой язык, и большинство ошибок новичка — это привычки, перенесённые из C, C++, Rust или Python. На этой странице собрано то, чего в языке нарочно нет, чем это заменено и где чаще всего спотыкаются. Незнакомый термин ищите в словаре.
Чего нет и что вместо этого
Привычка | В kola |
|---|---|
|
|
Исключения, |
|
| Регионы. Регион — это блок кода со своей ареной, то есть куском памяти, который раздаётся по частям и освобождается целиком: память выделяется через |
Деструкторы, RAII, | Линейные ресурсы. Каждый расходуется ровно один раз на каждом пути, и компилятор это проверяет. |
Borrow checker, времена жизни | Живость региона проверяется лексически. Внутри региона разделяемое изменение разрешено. Правило для |
Трейты, интерфейсы, | Дженерики в квадратных скобках, для каждого типа компилируется отдельная копия, и fn-ссылки. Блоки |
Замыкания, лямбды, | Fn-ссылки, которые ничего не захватывают, и обычные циклы |
Макросы, | Нет. Весь смысл программы — в тексте, который читает рецензент. |
| Нет, и других способов выйти из проверяемого языка тоже нет. Вручную оптимизированный машинный код попадает в программу только как именованная ветка под конкретную архитектуру ( |
Глобальные переменные, | Глобального состояния нет. Любой доступ к миру идёт через капабилити, переданную параметром. |
Неявные преобразования, | Три явных: |
| kola никогда не вычисляет раскладку типа в памяти, а утверждает её. |
| Их нет. Есть |
Перегрузка, аргументы по умолчанию, | Ни перегрузки, ни значений по умолчанию. Дженерики и регионы пишутся в квадратных скобках: |
|
|
Что есть, хотя часто думают, что нет
Восьмеричные и двоичные литералы:
0o644,0b1010_1010— наряду с десятичными и шестнадцатеричными._между цифрами служит разделителем.Побитовое НЕ:
~x— ровноx ^со всеми единицами своей ширины, в том числе для знаковых; трапа не бывает.f64: IEEE 754 binary64, побитово одинаковый на всех бэкендах, только с явными преобразованиями (f64::of_i64,f64::to_i64, возвращающееOption). NaN в любой операции вызывает трап, внутриct fnтипf64запрещён, трансцендентных функций нет.Байтовые строки:
b"GET "имеет тип[u8; 4], а&b"GET "— это&heap [u8], который передают байтовому API. Сырые литералыr"...",r#"..."#иbr#"..."#хранят текст в точности как написано, что удобно для JSON и фрагментов shell.Владеющие векторы:
let mut v = Vec::new();— владеющийVec(растущий массив), хранилище которого лежит вheap, регионе, живущем столько же, сколько программа. Рост такого вектора — эффектalloc(heap). Если память должна вернуться на скобке, вектор выделяют в регионе.Домены отказа:
domainиrecover(t: Trap)— на всех исполнителях.Атомики — для памяти, общей с другим ядром или устройством. Атомарная операция читает или обновляет ячейку памяти за один неделимый шаг, так что другое ядро процессора никогда не увидит её выполненной наполовину.
Atomic::add64_relaxed(cells, i, 1u64)и родственные операции работают с разделяемым срезом и индексом, под эффектомhw. Два потока kola общаются не так: они перемещают владение.Окружение (переменные процесса вида
NAME=value):sys.env.get(b"HOME")возвращаетOption[&heap [u8]]. Ответ — байты, потому что окружение не обязано быть в UTF-8; подkola testоно пусто.Непредсказуемая случайность:
Entropy::draw[[u8; 32]](&mut rng)иRng::fill_osпри каждом вызове берут энтропию у ОС.Rng::weak_u64— быстрый поток от зерна, для джиттера и перемешивания. Его состояние восстанавливается по одному выходному значению, поэтому для ключей, токенов и nonce он не годится.Внешние программы:
sys.proc.spawn(...)запускает бинарник, который оператор выдал по имени (--proc-bin ffmpeg=/abs/path). ВозвращаемыйChildлинейный.Ослабленные капабилити:
dial.to_host(host, port),dir.ro(),w.ro(),w.atomic()иsplit()уClock,RngиDial. Капабилити перемещается один раз, поэтому её сужают, а не копируют.Сортировка и копирование:
seq::sort_scratch— устойчивая сортировка за O(n log n); обычныйseq::sort— сортировка вставками.bytes::copy(dst, src)копирует срез в срез.Пределы целых: в
std::num::limitsестьU8_MAX…U128_MAXиI8_MIN…I64_MAX.USIZE_MAXнет: на 32-битной платформеusizeимеет 32 бита, и такая константа значила бы разное в двух сборках одного исходника.Помощники для Option:
o.take()забирает значение из изменяемого места типаOption, оставляя тамNone;o.with_mut(f)вызывает именованную функцию над содержимым на месте.Маркеры доверия к данным:
untrustedу параметра (данные, которым нельзя доверять),Secret[T](данные, которые нельзя раскрыть) иct fn(код, время работы которого не должно зависеть от секретных значений: иначе атакующий, способный измерять время, может их восстановить).SIMD-дорожки: типы, которые держат несколько чисел и обрабатывают их все одной инструкцией (SIMD — single instruction, multiple data). Есть дорожки константного времени
u32x4/u32x8и не-ct медийные дорожкиi16x8/u8x16/i32x4с 256-битными двойниками.Отчёты: у
kola reportестьchecks,reachable,leaks,regions,effects,secrets,tainted-reqs,wipe-windows,stack-frames,deps,perfи другие; полный список даётkola help report.
Частые ошибки
Почти на каждую из них компилятор отвечает сообщением, в котором названы правило и способ исправления. Пример ниже собирает несколько исправлений в одном тесте:
use std::log;
fn count_spaces(line: &[u8]) -> u64 !{} {
let mut n: u64 = 0;
for b in line {
if b == 32u8 { n = n + 1; } // 32 — это ' '
}
n
}
test the_usual_repairs {
let mut v = Vec::new();
v.push(7u64);
v.push(9u64);
let small: Option[u8] = narrow[u8](v[1]);
assert small == Some(9u8);
let wide: u64 = widen[u64](3u8);
assert wide == 3;
assert count_spaces(&b"GET / HTTP/1.1") == 2;
region r {
let s = f"len={v.len}";
assert s.len == 5;
}
log::debug(f"total {v[0] + v[1]}"); // передан прямо в вызов: без выделения памяти
}Подстановка в обычной строке. Обычная
"..."— это текст, и фигурные скобки в ней остаются скобками. Для подстановки нужен префиксf:log::info(f"x={v}"). Если написатьlog::info("x={v}"), когдаvв области видимости, компилятор откажет и предложит добавитьf.Привязка f-строки без открытого региона.
f"...", привязанная к имени, выделяет память. Регион берётся из типа места назначения, а если там его нет — из ближайшего открытогоregion. Если нет ни того ни другого, компилятор откажет, а не положит строку молча вheap. Переданная прямо в параметр&strна время вызова, как вlog::info(f"n={n}"), строка ничего не выделяет. Если нужно время жизни программы, его пишут явно:let s: &heap str = f"...";.Не тот вид ссылки в параметре. По умолчанию берите
&T: такой параметр принимает и значения на стеке, и значения в регионе, и ничего не выделяет.&r Tнужен, только если функция сохраняет или возвращает ссылку.Ожидание, что
region— выражение.let x = region r { ... };компилятор отвергнет. Регион, стоящий последней инструкцией функции, отдаёт своё хвостовое значение как результат; в остальных местах внутри блока присваивают внешней переменнойlet mut.Забытый
mutу растущегоVec.let v = Vec::new(); v.push(x);даётerror[card §4]: `push` mutates an immutable receiver, и строка help подсказывает написатьlet mut v. Кроме того, рост владеющегоVecтребуетalloc(heap)в ряде эффектов.Чтение значения после передачи по значению. Владеющий
Vec,Mapи любое значение, которое их содержит, перемещаются в каждой позиции «по значению»: аргумент вызова,let, поле структуры,Some(v), сообщение в канал, возврат. Читать источник после этого — ошибка. Вместо этого заимствуйте (&v,&mut v) или больше его не читайте. Из-под ссылки (h: &H) поле переместить нельзя вовсе; заимствуйте&h.v.Поиск символьного литерала.
'v'не существует. Пишите число с комментарием,118u8 // 'v', или сравнивайте с байтовой строкой.strкак байтовый буфер.str— неизменяемое представление текста в UTF-8. У него есть.lenи.bytes(представление без копирования), а формы&mutнет. Данные с провода разбирают как&[u8]. Единственный способ получитьstrиз байтов —utf8::to_str, который проверяет корректность и возвращаетOption.Литерал без суффикса в сдвиге или счётчик, уходящий в минус. Литерал без суффикса берёт ширину из контекста, а если её ничто не задаёт, это
u64(илиi64, если туда когда-нибудь попадает отрицательное значение). Поэтому1 << nсдвигает в 64-битном пространстве; для 32 бит пишут1u32 << n. Аlet mut c = 0; c = c - 1;вызывает трап на нуле, потому что-для беззнаковой ширины проверяется. Где значение может стать отрицательным, укажитеi64.Изменение состояния внутри
assert. Доказанныйassertудаляется, поэтому его выражение ничего не должно менять.assert get(&mut n);компилятор отвергает сerror[card §8]: assert expression mutates state. Сначала привяжите результат:let ok = get(&mut n); assert ok;.Затенение живого ресурса. Обычное затенение через
letработает как в Rust, но спрятать так живой ресурс нельзя. Сначала его нужно израсходовать...в образце структуры. Каждое поле должно быть названо. Завершающие запятые разрешены в любом списке.Поиск
print. Компилятор отвечаетerror[card §7]: `print` no longer exists — and neither does the `Io` capability. kola разделяет то, чтоprintсмешивал. Диагностика уходит в журнал, которому капабилити не нужна:use std::log;, затемlog::info("..."), эффектlog. Вывод программы, то есть байты, которые читает другая программа, идёт в поток и требует капабилити:sys.stdout.write(&bytes), эффектio. Случайная диагностика в stdout испортила бы вывод — поэтому каналы разные.Контракт на байтах от атакующего. Помечайте параметр как
untrusted, если в нём данные, которые может контролировать атакующий: байты из сети, из файла, со стандартного ввода или от другого процесса. Тогдаreqна нём отвергается:error[card §8]: `req` depends on untrusted input `p`. Контракт — это утверждение о вызывающем, а у этих байтов вызывающего нет, так что нарушенный контракт обернулся бы удалённым отказом в обслуживании. Проверяйте значение во время выполнения и возвращайтеResultилиOption.fn main() -> i32.mainвозвращаетu8— восемь бит, которые видит родительский процесс, — или ничего. Его единственный необязательный параметр —sys: Sysпо значению. Вычисленный код выхода сужают явно:narrow[u8](n).{0}внутри f-литерала.f"{0}"подставил бы число 0, поэтому он запрещён. Для буквального заполнителя из каталога сообщений используйте обычную строку"{0}"или экранируйте скобки внутри f-литерала:f"{{0}}".Выдуманный синтаксис. Самая дорогая ошибка. Если конструкции нет в
SPEC-CARD.mdилиSTD-CARD.md, её нет и в языке, как бы естественно она ни выглядела.
Отброшенный Result
Одно правило ловит тела тестов чаще любого другого: Result нельзя просто выбросить.
pub fn drop_it(c: Conn) !{io} {
c.close();
}Компилятор отвечает:
error[card §3]: a `Result` cannot be discarded by an expression statement
= help: handle it — `match`, `?`, or `.unwrap()` — or discard it deliberately with `let _ = ...;`Четыре способа исправить не равноценны. match говорит, что делать в каждом исходе, ? передаёт ошибку выше, .unwrap() при неудаче вызывает трап (это утверждение, что неудачи быть не может), а let _ = ...; отбрасывает результат намеренно и на виду. Вот вариант с match:
use std::log;
pub fn hang_up(c: Conn) !{io, log} {
match c.close() {
Ok(_) => {},
Err((_, os)) => log::warn(f"close failed: {os}"),
}
}Правило действует на инструкцию-выражение и на хвост блока, значение которого никто не принимает: тело while, for, loop, region, domain, recover или test. Option под него не попадает: None — это ответ, а Err — проигнорированная неудача. let _ = не снимает обязанности израсходовать ресурс: это отдельное правило, и оно продолжает действовать. В тесте лучше писать match с Err(_) => { assert false; }: тест, который игнорирует отказ, проходит по неправильной причине.
Если компилятор ругается
Каждая диагностика ссылается на раздел карты, например error[card §5]: .... Откройте этот раздел, прежде чем перебирать варианты наугад; формат сообщений разобран в быстром старте. У большинства диагностик есть и строка help: со способом исправления. Если кажется, что вы нашли ошибку или дыру в языке, сведите её к минимальному файлу и загляните в docs/confusion-log.md в репозитории: такие находки записывают туда. Объяснение, придуманное по одному случаю, считайте догадкой, пока минимальный файл его не подтвердит.