flang компилятор доказывает, что программа не зациклится 0.7.22 GitHub English

Как это доказывается

Компилятор flang закрывает три обещания до того, как соберёт файл:

ОбещаниеСлово в кодеЧто значит
функция завершится на любом входетотальнаянет входа, на котором она зациклится
вызов законентребуетпроверяется в точке вызова, а не внутри функции
результат таков, как сказанообеспечиваетутверждение про все входы, а не про написанные примеры

Ниже — как каждое из трёх доказывается, что остаётся в собранном коде, а что из него исчезает. Всё, что напечатано в блоках вывода, снято прогонами flang 0.7.20 (сборка из ствола 19 сентября 2026); команда стоит рядом с выводом, любой блок повторяется за минуту.

Кто что проверяет

Путь от исходника до вердиктаисходник .flangкомпилятортипызавершаемостьобязательства: требует,обеспечиваетядро доказательствв собранный код проверкаНЕ идётпроверка во время работысертификат: весь ходвывода в файлсверщик — отдельнаяпрограмма на Cвердикт: переиграно илиподлогдоказаноне доказано
Путь от исходника до вердикта

Ядро доказательств — часть компилятора. Сверщик (flang/proof/checker/checker.c) — отдельная программа, которая не верит компилятору на слово: она берёт сертификат и переигрывает каждый шаг вывода заново. Это разделение и есть причина, по которой «доказано» здесь значит больше, чем «компилятор так сказал».

1. Завершаемость

Структурный спуск

Рекурсия идёт по части входного значения, и часть каждый раз меньше целого.

модуль «Спуск»

тотальная функция «Сумма списка»
  принимает ряд: список числа
  возвращает число
  разбор ряд
    случай пусто
      то 0
    случай голова первый и хвост остальные
      первый плюс («Сумма списка» от остальные)

Аргумент рекурсивного вызова — остальные, хвост разобранного списка. Список конечен, цепочка хвостов обрывается сама, доказывать нечего:

$ flang check --proof spusk.flang
  «Сумма списка»  доказано структурой: аргумент 1 («ряд») на каждом витке
  становится частью себя; цепочка частей конечного дерева обрывается сама,
  сторожа нет

Последние два слова отчёта — самые важные. Когда доказательство не сошлось до конца, компилятор дописывает в собранную программу проверку, которая ловит разницу уже во время работы. Здесь ловить нечего: обход конечного дерева кончается сам, и проверки в коде не будет.

Спуск по числу: тип решает, останется ли проверка

Та же функция, но спуск не по части значения, а по числу:

тотальная функция «Обратный отсчёт»
  принимает н: число
  возвращает число
  если н не больше 0
    то 0
    иначе 1 плюс («Обратный отсчёт» от (н минус 1))
$ flang check --proof bez-mery.flang
  «Обратный отсчёт»  доказано постоянным шагом: аргумент 1 («н») убывает на
  постоянный шаг и ограничен снизу; на IEEE-754 шаг не всегда меняет число,
  поэтому сторож, 1 место
  сторожей в рантайме: 1 место

Причина проверки названа прямо: число — это binary64, и при н больше 2⁵³ вычитание единицы не меняет число. Спуск встанет, функция зациклится. Компилятор это знает и вставляет проверку.

Меняем одно слово — тип параметра:

  принимает н: неотрицательное
$ flang check --proof tochnyy.flang
  «Обратный отсчёт»  доказано точным шагом: аргумент 1 («н») объявлен
  натуральным и убывает на 1; дно и потолок даёт тип, внутри потолка шаг точен
  — сторожа нет
  сторожей в рантайме: 0 мест

Тип неотрицательное — это отрезок [0, 2⁵³−1]. Внутри него шаг точен, дно есть, спуск конечен. Проверка из собранного кода ушла. Объявленный тип дешевле проверки во время работы — и это измеримо, а не на словах.

Объявленная мера

Когда убывает не сам аргумент, а что-то, посчитанное из аргументов, это пишется строкой убывает. Пример из стандартной библиотеки (flang/stdlib/bignum.flang:383):

  убывает (длина первые) плюс (длина вторые)

Компилятор проверяет две вещи: что названное выражение на каждом витке строго меньше прежнего и что снизу у него есть дно. Убывающая величина с дном не убывает вечно.

Когда не доказал

тотальная функция «До нуля»
  принимает н: число
  возвращает число
  если н равен 0
    то 0
    иначе «До нуля» от (н плюс 1)
$ flang check rastyot.flang; echo "код $?"
FLANG_NOT_TOTAL в файле rastyot.flang, строка 8, столбец 11: тотальная функция
«До нуля»: рекурсивный вызов «До нуля» не убывает — аргумент 1 («н» add 1)
увеличивает параметр «н». Передавайте часть аргумента: хвост списка из образца
«голова и хвост», поле варианта из образца, поле записи или элемент коллекции
rastyot.flang: не проверено — замечаний 1
код 1

Файл не собран. Не предупреждение, не линтер — отказ с кодом 1.

2. Предусловие проверяется в точке вызова

тотальная функция «Цена со скидкой»
  принимает цена: неотрицательное, скидка: неотрицательное
  требует «скидка не больше цены» скидка не больше цена
  возвращает число
  цена минус скидка

Теперь вызов с заведомо негодным доводом:

тотальная функция «Счёт»
  возвращает число
  «Цена со скидкой» от 100 и 150
$ flang check zakaz.flang; echo "код $?"
FLANG_PRECONDITION_CALL в файле zakaz.flang, строка 16, столбец 3: вызов «Цена
со скидкой» в функции «Счёт» не снимает предусловие «скидка не больше цены»
zakaz.flang: не проверено — замечаний 1
код 1
Кто снимает предусловиеaltчем снять — естьelse снять нечемВызывающая функцияКомпиляторЯдро«Цена со скидкой» отцена и скидкаснять «скидка не большецены»доказанофайл собран,проверки в коде нетне доказаноFLANG_PRECONDITION_CALL,файл не собран
Кто снимает предусловие

Отличие от assert в Python или Java: assert срабатывает у пользователя, через полгода после выкладки, на входе, которого никто не ждал. Здесь вызов разбирается в компиляторе, и снять предусловие обязан тот, кто вызывает: своим требует, объявленным типом довода или доказанным обещанием функции, которая этот довод посчитала. Ту же дисциплину даёт SPARK у Ada; разница в том, что здесь она в языке, а не в отдельном инструменте поверх него.

Проверка во время работы остаётся ровно в одном месте — на границе, где в программу входят чужие данные. Вот что печатается в C на ту же функцию:

if (strcmp(name, "Цена со скидкой") == 0) {
  FL_TRY(fl_pre(ctx, fl_flag(args[1].as.number <= args[0].as.number),
                "скидка не больше цены", "Цена со скидкой", &fl_t1, error));
  if (!fl_t1) return fl_fail(ctx, error, "FLANG_PRECONDITION", ...);

Это разбор вызова по имени — вход из JSON, из командной строки, из чужой программы. Внутри самой программы такой проверки нет ни одной: там все вызовы уже разобраны компилятором.

3. Постусловие: «доказано» значит «проверки в собранном коде нет»

Берём ту же функцию без требует и с обещанием:

тотальная функция «Цена со скидкой»
  принимает цена: неотрицательное, скидка: неотрицательное
  возвращает число
  обеспечивает «в минус не уходим» результат не меньше 0
  пример «Сто минус десять»
    дано цена равно 100
    дано скидка равно 10
    ожидается 90
  цена минус скидка

Обещание ложно: скидка бывает больше цены. Вот что говорит ядро:

$ flang check --proof skidka.flang
  постусловие «в минус не уходим» функции «Цена со скидкой» — сетка 1 значение
  (примеры функции): нарушений НЕ ИСКАЛИ — прогона примеров не было, посчитано
  только их число. Это не доказательство — теоремы при утверждении нет
  утверждений 1: доказано 0, сетка 1, объявлено, не доказано 0

Запустить такое с 0.7.21 нельзя без явного согласия:

$ flang run skidka.flang --function 'Цена со скидкой' --args '{"цена": 100, "скидка": 150}'
не доказано: утверждений 1: доказано 0, сетка 1, на веру 0 — запуск только по
явному согласию: --на-веру
код 3

А в напечатанном C обещание превращается в проверку перед возвратом:

fl_status skidka_cena_so_skidkoy(fl_ctx *ctx, fl_value cena, fl_value skidka,
                                 fl_value *result, fl_error *error) {
  if (cena.tag != FL_NUMBER || skidka.tag != FL_NUMBER) FL_TRY(fl_not_numbers(...));
  const fl_value fl_t1 = fl_number(cena.as.number - skidka.as.number);
  /* постусловие «в минус не уходим» */
  bool fl_t2 = false;
  FL_TRY(fl_post(ctx, fl_flag(fl_t1.as.number >= 0.0), "в минус не уходим",
                 "Цена со скидкой", &fl_t2, error));
  if (!fl_t2) return fl_fail(ctx, error, "FLANG_PROPERTY", "%s",
      "нарушено свойство «в минус не уходим» функции «Цена со скидкой»");
  *result = fl_t1;
  return FL_OK;
}

Теперь добавляем одну строку — предусловие:

  требует «скидка не больше цены» скидка не больше цена
$ flang check --proof skidka-trebuet.flang
  постусловие «в минус не уходим» функции «Цена со скидкой» — доказано по
  объявленным типам аргументов: цель сведена правилом «неотрицательность по
  построению» — утверждение обо ВСЕХ входах, а не о написанных; теоремы при
  нём нет и не нужно
  утверждений 1: доказано 1 (из них без теоремы 1, объявленным типом 1), сетка 0

И та же функция в напечатанном C целиком:

fl_status skidka_s_usloviem_cena_so_skidkoy(fl_ctx *ctx, fl_value cena, fl_value skidka,
                                            fl_value *result, fl_error *error) {
  if (cena.tag != FL_NUMBER || skidka.tag != FL_NUMBER) FL_TRY(fl_not_numbers(...));
  *result = fl_number(cena.as.number - skidka.as.number);
  return FL_OK;
}

Проверки нет. Не выключена ключом — её не из чего печатать: утверждение закрыто про все входы, проверять во время работы нечего.

Это и есть практическая разница с контрактами в Eiffel, Java (@ensures) или Python (assert): там контракт всегда стоит в коде и всегда стоит времени. Здесь доказанный контракт из кода уходит, а недоказанный остаётся — и отчёт говорит вслух, который из двух перед вами.

4. Сетка: что это и зачем она, если это не доказательство

Сетка — проверка утверждения на конечном наборе значений: значениях из пример, значениях из объявленного закона. В отчёте она печатается словами «сетка N значений» и заканчивается фразой «Это не доказательство» — намеренно.

Что сетка даёт:

Чего сетка не даёт: ничего про входы, которых в наборе не было. Перебор конечного набора значений — это тест, записанный в другом месте, а не доказательство.

5. Сколько получается на настоящем модуле

Модуль списков стандартной библиотеки, flang/stdlib/lists.flang:

$ flang check --proof flang/stdlib/lists.flang
  функций 38: тотальных 38, обычных 0
  обещание несёт: композиция 28, структура 8, точный шаг 1, постоянный шаг 1,
                  объявленная мера 0
  сторожей в рантайме: 1 место
  утверждений 66: доказано 42 (из них индукцией 5) (из них без теоремы 37),
                  сетка 24, объявлено, не доказано 0

Читается так:

И то же самое в напечатанном C:

$ flang emit flang/stdlib/lists.flang --target c --out /tmp/out
$ grep -c 'fl_post(' /tmp/out/lists.c
25

25 проверок во время работы = 24 недоказанных постусловия + 1 проверка завершаемости (та самая, из «постоянного шага»). 42 проверки, которые в Python или Go пришлось бы либо писать руками, либо не иметь вовсе, в собранной программе отсутствуют — потому что доказаны.

6. Чего это не значит

Как повторить

flang check <файл>                       # типы, завершаемость, ядро; код 1 — отказ
flang check --proof <файл>               # отчёт: чем несётся каждое обещание
flang check --proof --строго <файл>      # то же, но опора обязана быть судимой
flang check --proof --записать <файл>    # сертификат в файл — для сверщика
flang emit <файл> --target c --out <кат> # посмотреть, какие проверки остались
flang test <файл>                        # прогон примеров

Разбор всего дерева языка — Отчёт о доказательствах по дереву; зачем это вообще нужно — Зачем и как.