/
Поиск
RU

Шпаргалка: чего нет и частые ошибки

kola — небольшой язык, и большинство ошибок новичка — это привычки, перенесённые из C, C++, Rust или Python. На этой странице собрано то, чего в языке нарочно нет, чем это заменено и где чаще всего спотыкаются. Незнакомый термин ищите в словаре.

Чего нет и что вместо этого

Привычка

В kola

null, нулевые указатели

Option[T]. Отсутствие значения видно по типу.

Исключения, panic!, раскрутка стека

Result[T, E] и ? для ожидаемых ошибок. Нарушенное допущение вызывает трап — детерминированную остановку. Блок domain d { … } recover(t: Trap) { … } сужает то, что трап убивает: не весь процесс, а только этот блок. Трап при этом остаётся ошибкой, и kola audit считает каждый домен.

malloc/free, new/delete, GC

Регионы. Регион — это блок кода со своей ареной, то есть куском памяти, который раздаётся по частям и освобождается целиком: память выделяется через alloc in r, а на закрывающей скобке освобождается всё сразу. Долгоживущие данные со структурой графа хранят в Pool[T].

Деструкторы, RAII, Drop (код очистки, который сам запускается, когда значение выходит из области видимости)

Линейные ресурсы. Каждый расходуется ровно один раз на каждом пути, и компилятор это проверяет.

Borrow checker, времена жизни 'a (механизм Rust, доказывающий, что ссылки остаются действительными)

Живость региона проверяется лексически. Внутри региона разделяемое изменение разрешено. Правило для &mut — исключительность на месте вызова плюс одно правило представлений: пока представление места ещё используется, это место нельзя менять ни под каким другим именем.

Трейты, интерфейсы, dyn, наследование

Дженерики в квадратных скобках, для каждого типа компилируется отдельная копия, и fn-ссылки. Блоки impl — только пространства имён.

Замыкания, лямбды, .map().filter()

Fn-ссылки, которые ничего не захватывают, и обычные циклы for. Итераторов нет: for обходит диапазон a..b или срез, и больше ничего.

Макросы, #[derive], атрибуты

Нет. Весь смысл программы — в тексте, который читает рецензент.

unsafe, FFI, extern "C", сырые указатели, встроенный ассемблер

Нет, и других способов выйти из проверяемого языка тоже нет. Вручную оптимизированный машинный код попадает в программу только как именованная ветка под конкретную архитектуру (hw x86_64(...) = "symbol"): определением остаётся тело на kola, и ветку проверяют на совпадение с ним. Чтобы использовать программу на C, её запускают как процесс.

Глобальные переменные, static, синглтоны

Глобального состояния нет. Любой доступ к миру идёт через капабилити, переданную параметром. const — данные времени компиляции. Единственное исключение — эффект log: он работает только на запись, и капабилити ему не нужна.

Неявные преобразования, as

Три явных: widen[u64](x) (без потерь), narrow[u8](x), возвращающее Option[u8], и wrap[i16](x) (младшие биты в новом типе; трапа не бывает). as — ошибка компиляции.

size_of, alignof

kola никогда не вычисляет раскладку типа в памяти, а утверждает её. layout_assert[T](SIZE, ALIGN); — элемент уровня модуля, и C-бэкенд превращает его в _Static_assert.

Vec::remove(i), reserve, retain, dedup

Их нет. Есть push и insert(i, x) (выделяют память), pop, truncate(n), clear() и swap(i, j) (не выделяют), массовые методы extend_from_slice, resize и copy_from_slice, .len и индексация. Чтобы удалить элемент из середины, его меняют местами с последним и вызывают truncate.

Перегрузка, аргументы по умолчанию, <T>

Ни перегрузки, ни значений по умолчанию. Дженерики и регионы пишутся в квадратных скобках: [T], [r].

async/await, Mutex

async нет. Потоки и каналы есть (sys.spawn, Thread[R], sys.chan, Tx[T], Rx[T]), и это настоящие потоки ОС; см. Потоки и каналы. Mutex нет потому, что ни одна ссылка не пересекает границу потока: вместо этого перемещается владение, и охранять общее нечего.

Что есть, хотя часто думают, что нет

  • Восьмеричные и двоичные литералы: 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]}");   // передан прямо в вызов: без выделения памяти
}
  1. Подстановка в обычной строке. Обычная "..." — это текст, и фигурные скобки в ней остаются скобками. Для подстановки нужен префикс f: log::info(f"x={v}"). Если написать log::info("x={v}"), когда v в области видимости, компилятор откажет и предложит добавить f.

  2. Привязка f-строки без открытого региона. f"...", привязанная к имени, выделяет память. Регион берётся из типа места назначения, а если там его нет — из ближайшего открытого region. Если нет ни того ни другого, компилятор откажет, а не положит строку молча в heap. Переданная прямо в параметр &str на время вызова, как в log::info(f"n={n}"), строка ничего не выделяет. Если нужно время жизни программы, его пишут явно: let s: &heap str = f"...";.

  3. Не тот вид ссылки в параметре. По умолчанию берите &T: такой параметр принимает и значения на стеке, и значения в регионе, и ничего не выделяет. &r T нужен, только если функция сохраняет или возвращает ссылку.

  4. Ожидание, что region — выражение. let x = region r { ... }; компилятор отвергнет. Регион, стоящий последней инструкцией функции, отдаёт своё хвостовое значение как результат; в остальных местах внутри блока присваивают внешней переменной let mut.

  5. Забытый mut у растущего Vec. let v = Vec::new(); v.push(x); даёт error[card §4]: `push` mutates an immutable receiver, и строка help подсказывает написать let mut v. Кроме того, рост владеющего Vec требует alloc(heap) в ряде эффектов.

  6. Чтение значения после передачи по значению. Владеющий Vec, Map и любое значение, которое их содержит, перемещаются в каждой позиции «по значению»: аргумент вызова, let, поле структуры, Some(v), сообщение в канал, возврат. Читать источник после этого — ошибка. Вместо этого заимствуйте (&v, &mut v) или больше его не читайте. Из-под ссылки (h: &H) поле переместить нельзя вовсе; заимствуйте &h.v.

  7. Поиск символьного литерала. 'v' не существует. Пишите число с комментарием, 118u8 // 'v', или сравнивайте с байтовой строкой.

  8. str как байтовый буфер. str — неизменяемое представление текста в UTF-8. У него есть .len и .bytes (представление без копирования), а формы &mut нет. Данные с провода разбирают как &[u8]. Единственный способ получить str из байтов — utf8::to_str, который проверяет корректность и возвращает Option.

  9. Литерал без суффикса в сдвиге или счётчик, уходящий в минус. Литерал без суффикса берёт ширину из контекста, а если её ничто не задаёт, это u64 (или i64, если туда когда-нибудь попадает отрицательное значение). Поэтому 1 << n сдвигает в 64-битном пространстве; для 32 бит пишут 1u32 << n. А let mut c = 0; c = c - 1; вызывает трап на нуле, потому что - для беззнаковой ширины проверяется. Где значение может стать отрицательным, укажите i64.

  10. Изменение состояния внутри assert. Доказанный assert удаляется, поэтому его выражение ничего не должно менять. assert get(&mut n); компилятор отвергает с error[card §8]: assert expression mutates state. Сначала привяжите результат: let ok = get(&mut n); assert ok;.

  11. Затенение живого ресурса. Обычное затенение через let работает как в Rust, но спрятать так живой ресурс нельзя. Сначала его нужно израсходовать.

  12. .. в образце структуры. Каждое поле должно быть названо. Завершающие запятые разрешены в любом списке.

  13. Поиск 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 испортила бы вывод — поэтому каналы разные.

  14. Контракт на байтах от атакующего. Помечайте параметр как untrusted, если в нём данные, которые может контролировать атакующий: байты из сети, из файла, со стандартного ввода или от другого процесса. Тогда req на нём отвергается: error[card §8]: `req` depends on untrusted input `p`. Контракт — это утверждение о вызывающем, а у этих байтов вызывающего нет, так что нарушенный контракт обернулся бы удалённым отказом в обслуживании. Проверяйте значение во время выполнения и возвращайте Result или Option.

  15. fn main() -> i32. main возвращает u8 — восемь бит, которые видит родительский процесс, — или ничего. Его единственный необязательный параметр — sys: Sys по значению. Вычисленный код выхода сужают явно: narrow[u8](n).

  16. {0} внутри f-литерала. f"{0}" подставил бы число 0, поэтому он запрещён. Для буквального заполнителя из каталога сообщений используйте обычную строку "{0}" или экранируйте скобки внутри f-литерала: f"{{0}}".

  17. Выдуманный синтаксис. Самая дорогая ошибка. Если конструкции нет в 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 в репозитории: такие находки записывают туда. Объяснение, придуманное по одному случаю, считайте догадкой, пока минимальный файл его не подтвердит.