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

Замер цены доказательства, второй раз: те же двадцать функций, обе работы

Вопрос тот же, что и в первый раз: дешевле ли доказать функцию, чем написать на неё тесты? Первый замер (docs/benchmark-proof-cost.md, 15 августа) дал 0 из 20 и честно сказал, что делить не на что. Здесь то же самое измерено заново, на том же материале, чтобы было видно движение.

Движение есть, и оно не там, где его ждали.


Основание — без него числа не сравнить

ветка замераwork/zamer-tseny-2
основаниеorigin/main = 17d68539c866c319476c871dcf199902371896ba (16 августа 2026, 15:49 UTC)
прошлый замер стоял наorigin/main = 8203f39e… (14 августа)
инструментflang test (примеры) и flang check --proof (ведомость доказательства)
файлы работыdocs/benchmark2/01-…flangdocs/benchmark2/20-…flang, 20 штук, все в дереве
журнал с секундомеромbenchmarks/proof-cost/journal.md; счётчик строк — benchmarks/proof-cost/schitat.mjs
дата16 августа 2026

Ветки work/indukciya-vstroennyh и work/zamknutaya-cel в main НЕ влиты, и я их не вливал. Мерил на голом main. Это сказано первой строкой нарочно: их собственные отчёты обещают другое число, и складывать одно с другим нельзя.

Что из обещанного реально стоит на основании — снято прогоном, а не пересказом:

пробаответчем снято
индукция по встроенному спискуЕСТЬотказы называют список носителем принципа наравне с объявленной суммой
индукция по отрезку натЕСТЬто же, третий носитель
индукция по признакНЕТзамер 12: «у этого типа (признак) принципа индукции нет»
индукция по строкаНЕТзамеры 19 и 20, тот же отказ
вычисление замкнутой целиНЕТbenchmarks/proof-cost/probe-closed-goal.flang: `результат начинается с "\"` при теле-строке — «объявлено, не доказано»
предусловие требуетЕСТЬзамер 10: отказ говорит «известно: предусловие функции «Двоичный поиск»»
дыра «один пример доказывает обо ВСЕХ входах»ЗАКРЫТАbenchmarks/proof-cost/probe-forgery.flang — подделка прошлого замера теперь отвергается поимённо
дано <имя>: <тип>, где имя совпадает с именем типадыра открытапроба в docs/benchmark2/13-even.flang: «имя «число» не связано»

Ведомость всего дерева на этом основании (node flang/scripts/proof-ledger.mjs): высказано 1045 утверждений, доказано ядром 407; сетка 556; отвергнуто 0; законов на веру 0.

Числа эти сверяются с деревом сторожем (./ярлык подсчёты:проверка), поэтому здесь стоит СЕГОДНЯШНЯЯ ведомость, а не снимок дня замера. На день замера было 142 / 115; четыре утверждения прибавила ветка work/claims-core — три потолка римской цифры и потолок разности натуральных, — и ядра эта прибавка не касается ни строкой. Выборка двадцати функций ниже от неё не сдвинулась: ни одна из четырёх в неё не входит.


Про выборку: те же двадцать, и почему правило отбора больше не годится

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

Правило прошлого отбора («все функции flang/stdlib, порядок «файл, объявление», каждая девятая») сегодня даёт другой набор, и вот почему:

Оба списка привожу целиком, чтобы никто не считал их заново.

#прошлый набор (шаг 9 из 185) — взят СЕЙЧАСчто дало бы правило сегодня (шаг 10 из 201)
1«Ключ связи» (dictionary)«Ключ связи» (dictionary)
2«Ключ звена» (hashmap)«Значение звена» (hashmap)
3«Вписать» (hashmap)«Вложить» (hashmap)
4«Размер словаря» (hashmap)«Больше из двух» (hashmap)
5«Противоположное» (higher-order)«По возрастанию» (higher-order)
6«Приписать в начало» (higher-order)«Позиция где» (higher-order)
7«Вставить по» (higher-order)«Максимум» (higher-order)
8«Максимум» (higher-order)«Срез» (lists)
9«Взять первые» (lists)«Любой не меньше» (lists)
10«Двоичный поиск» (lists)«Сжать в пары» (lists)
11«Уникальные» (lists)«Максимум двух» (numbers)
12«Следует» (logic)«Добавить число» (numtree)
13«Чётное» (numbers)«Первый элемент или запасное» (optional)
14«Дерево из чисел» (numtree)«Значение или запасное» (result)
15«Первый элемент или запасное» (optional)«Убрать из множества» (sets)
16«Успешно» (result)«Заменить» (strings)
17«Есть в множестве» (sets)«Обратить строку» (strings)
18«Приписать строку в начало» (strings)«Это латинская буква» (strings)
19«Строчная буква» (strings)«Номер строки» (strlists)
20«Обрезать пробелы» (strings)«Приоритет» (tree)

Проверено прогоном (LC_ALL=C.UTF-8 node benchmarks/proof-cost/otbor.mjs), что старая двадцатка воспроизводима и сегодня: в порядке «файл, объявление» их номера по-прежнему 0, 9, 18, …, 171 — то есть первые 172 позиции библиотеки не сдвинулись, а 23 новые функции приписаны позже. Та же команда печатает и правый столбец таблицы выше.

Одна оговорка против нас. У функции 13 («Чётное») в библиотеке за это время появилось постусловие автора. Я его убрал — ровно как убраны примеры автора, — и написал своё. По правилу отбора она сегодня в выборку бы не попала.

Тела и зависимости перенесены из библиотеки машиной (benchmarks/proof-cost/vydelit.mjs), примеры автора убраны. Копирование в измеренные строки не входит, как и в прошлый раз.


Ответы на четыре вопроса — сразу

1. Отношение цены: доказательство против тестов

мератестыдоказательствоотношение
строк написано394169доказательство в 2,3 раза короче
времени276 с (4 мин 36 с)589 с (9 мин 49 с)доказательство в 2,1 раза дольше
попыток2041доказательство в 2,05 раза больше
на функцию19,7 строки / 13,8 с8,4 строки / 29,4 с
что получено20 работающих наборов из 20, 110 примеров, 4 настоящие ошибки2 доказанных содержательных утверждения из 20 (+4 закрытых даром ослабленных)

Отношение цены за полученный результат — то, ради чего замер и делается:

тестыдоказательствоотношение
строк на единицу результата394 / 20 = 19,7169 / 2 = 84,5доказательство дороже в 4,3 раза
секунд на единицу результата276 / 20 = 13,8589 / 2 = 294доказательство дороже в 21 раз

Это по-прежнему хуже мирового ориентира (5–20×) по времени и лучше него по строкам. Но главное число не здесь, а в следующем абзаце, и оно новое.

Там, где ядро берёт, доказательство стоит одну строку.

функциятестыдоказательствоотношение
04 «Размер словаря»18 строк / 17 с1 строка / 11 св 18 раз короче, в 1,5 раза быстрее
13 «Чётное»18 строк / 17 с1 строка / 29 св 18 раз короче, в 1,7 раза дольше

(У 13 в файле лежат ещё 4 строки отвергнутой пробы — ею проверялось, жива ли дыра дано число: число прошлого замера. Жива. К доказательству она не нужна.)

Теорема в обоих случаях не понадобилась вовсе: постусловие свелось с телом функции. То же самое даром закрылось ещё у четырёх функций (01, 02, 05, 07) — там, правда, утверждение пришлось ослабить, об этом ниже.

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

2. Сколько из двадцати ядро закрыло само

2 из 20 — если считать утверждения, которые что-то говорят о функции («размер неотрицателен», «чётность есть делимость на два»).

6 из 20 — если считать всё, что ядро закрыло без теоремы, включая ослабленное и тавтологичное.

Разницу расписываю поимённо, чтобы её нельзя было потерять:

#что закрылосьэто говорит о функции?
04«размер словаря неотрицателен»да — слабо, но правда о поведении
13«чётность есть делимость на два»да — связывает две функции библиотеки
01«длина ключа неотрицательна»нет — заглушка вместо невыразимой правды
02«длина ключа неотрицательна»нет — то же
07«длина результата неотрицательна»нет — вместо «длина плюс один», которую ядро не взяло
05«результат равен (0 минус х)»нет, это тавтология: постусловие дословно переписывает тело

Пятую строку стоит прочитать вслух: ядро закрывает тавтологию бесплатно. результат равен (0 минус х) при теле 0 минус х сводится правилом тождества за один шаг. Содержательное утверждение о той же функции — (результат плюс х) равен 0 — ядро не взяло. То есть цифру «закрыто без теоремы» можно надуть до любого размера, просто переписав тело в постусловие, и ведомость напечатает «доказано обо ВСЕХ входах». Это не обвинение ядру — оно право, — а предупреждение о том, как эту метрику нельзя мерить.

Теорем написано 14, принято ядром 0. Ни одна теорема за весь замер не прошла. Всё, что доказано, доказано БЕЗ теоремы.

3. Что мешает чаще всего

Восемнадцать исходов «не вышло» разложены по названной причине отказа:

причинасколькофункции
тело написано не разбором — свёрткой, вызовом или арифметикой, и заключение посылки индукции строить не из чего703, 06, 10, 11, 14, 17, 18
у типа нет принципа индукциистрока (2), признак (1)312, 19, 20
на языке не выразить401, 02, 15, 16
у цели нет вида, к которому есть правило — «или», «не больше <выражение>»208, 09
в базе индукции остаётся свободное имя, а в шаге не сходится тождество107
цель не сводится ни одним из трёх правил105

Главная помеха — форма тела, и у неё есть измеренный знаменатель. Ядро строит заключение посылки индукции только с ветвей разбор по переменной индукции. Таких тел в flang/stdlib 58 из 208 (benchmarks/proof-cost/tela.mjs). Остальные 150 написаны свёрткой (45), условием (37), арифметикой (21), вызовом (18), встроенной формой (9), пусть (8), отображением и отбором (8), построением (3), применением (1).

То есть 72 % библиотеки написано формами, к которым индукция ядра не цепляется — и это ровно то, что видно на выборке: 7 отказов из 18. Индукция по встроенному списку появилась и работает, но она требует, чтобы автор написал функцию разбором. Обычный код пишут свёрткой.

Невыразимость: 4 случая из 20, и все четыре — дыры языка, а не ядра.

дырагдесколько
поле суммы из выражения не достать: связь.ключ → «доступ к полю требует записи», а разбор в постусловие не помещается (постусловие однострочное)01, 022
равенства на параметре типа нет: «результат равен запасному» → «сравнивать на равенство можно только скаляры, а не «А»»151
проверки варианта выражением нет вовсе: «результат да ровно тогда, когда итог это «Успех»» сказать нечем161

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

4. Как изменилось против прошлого замера

исходбыло (15 августа)стало (16 августа)
ядро закрыло само, теорема не понадобилась02 (и ещё 4 на ослабленном или тавтологичном утверждении)
теорема написана, ядро приняло00
не вышло2018
— из них «ядро не берёт»1514
— из них «на языке не выразить»54

По цене:

мерабылостало
строк тестов390394
времени тестов7 мин 49 с4 мин 36 с
настоящих ошибок тестами44 (те же самые, ни одной новой)
строк доказательства196169
времени доказательства9 мин 39 с9 мин 49 с
попыток доказательства3041
принято ядром02

Время тестовой стороны упало почти вдвое, и это не улучшение языка, а кривая обучения агента и разброс машины: сравнивать здесь можно только две стороны между собой внутри одного замера, о чём прошлый отчёт предупреждал.


Чего доказательства не нашли, и это важнее всех цифр

Тесты нашли четыре настоящие ошибки. Все четыре — те же, что нашёл первый замер, и ни одна не починена.

  1. «Чётное» от −4 отвечает «нет» (-4 остаток от 2 = -0, а равенство скаляров считает Object.is);
  2. «Уникальные» от [0, -0, 1] оставляет два нуля — тот же корень;
  3. «Строчная буква» от пустой строки отказывает, а функция объявлена тотальной;
  4. «Строчная буква» от "AB" молча портит вход в "a".

Новых ошибок тесты не нашли — потому что материал тот же и за сутки его не трогали. Так что вклад тестовой стороны за этот день по-новому знанию ноль, и называть его находкой было бы приписыванием.

Доказательства не нашли ни одной ошибки — ни новой, ни старой.

И вот случай, ради которого стоит писать этот раздел. Функция 13 «Чётное» — одна из двух, где доказательство сработало. Ведомость печатает:

постусловие «чётность есть делимость на два» функции «Чётное» —
доказано сведением цели с телом функции: правило «тождество после переписки
допущением» … утверждение обо ВСЕХ входах, а не о написанных

И та же функция на том же дереве отвечает нет на −4, что видно первым же примером. Противоречия нет: ядро право. «Чётное» и «Делится на» ошибаются ОДИНАКОВО, и «доказано» здесь означает ровно «две ошибки согласованы обо всех входах» — не больше. Это ровно тот пункт, который в передаче записан седьмым в списке запретов: «доказано» не равно «правильно», доказательство говорит только о соответствии спецификации, а не о том, выражает ли спецификация намерение. Здесь это не рассуждение, а наблюдение на живой функции.


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

Исход «ядро закрыло само» — цена одна строка

docs/benchmark2/04-dictionary-size.flang:

тотальная функция «Размер словаря»
  принимает словарь: «Узел хеша»
  возвращает число
  для всех словарь обеспечивает «размер неотрицателен» результат не меньше 0
  … 6 примеров, 18 строк …
  длина («Звенья словаря» от словарь)

Ведомость:

постусловие «размер неотрицателен» функции «Размер словаря» — доказано сведением
цели с телом функции: правило «неотрицательность по построению», объявленные типы
аргументов не понадобились — утверждение обо ВСЕХ входах, а не о написанных;
теоремы при нём нет и не нужно

Ядро подставило тело на место результат, увидело встроенную длина, у которой объявленный тип результата — нат, и закрыло цель. Ни теоремы, ни индукции, ни единого шага. Цена: 1 строка против 18 строк примеров.

Исход «ядро не берёт» — правда высказана, правил не хватает

docs/benchmark2/17-member-of-set.flang, утверждение ПОЛНОЕ — оно исчерпывает смысл функции:

  для всех множество обеспечивает «это и есть вхождение» результат равен (множество содержит искомое)
  …
  свёртка множество начиная с нет как найдено и эл → если эл равно искомое то да иначе найдено

теорема «это и есть вхождение»
  дано множество: список строки
  дано искомое: строка
  утверждаем результат равен (множество содержит искомое)
  индукция по множество
    случай пусто
      то по примеру «В пустом множестве нет ничего»
    случай голова и хвост
      то по предположению
  следовательно доказано

Отказ — и он не тот, что был в прошлый раз:

тело функции «Есть в множестве» не разбирает «множество» на верхнем уровне,
поэтому заключение посылки индукции построить не из чего: ядро берёт «результат»
случая с ветви `разбор` по той же переменной, а не угадывает его

Год назад — точнее, сутки назад — здесь стояло «а это не объявленная сумма». Принцип индукции у списка теперь есть. Упирается всё на шаг дальше: у свёртки нет ветвей, с которых читается заключение посылки. Разница существенная: раньше не хватало ПРИНЦИПА, теперь не хватает СПОСОБА ПРИЦЕПИТЬ его к телу.

Исход «на языке не выразить» — правду нечем записать

docs/benchmark2/16-success.flang. Смысл функции: «да ровно тогда, когда итог — вариант «Успех»». Попытка записать это выражением:

FLANG_PARSE: ожидался знак ')'

Проверки варианта выражением у языка нет вовсе — вариант различает только разбор, а он в постусловие не помещается: постусловие однострочное, у разбор обязателен блок с отступом. Записать можно было бы только тавтологию результат равен («Успешно» от итог), которая не утверждает ничего.


Что удешевило бы сильнее всего — по измеренной отдаче

По убыванию, с числом функций из двадцати, которые это открывает.

работаоткрываетпочему именно столько
1. Прицепить индукцию к телу-свёртке (заключение посылки читать с начала и шага свёртки, а не только с ветвей разбор)до 7 из 2003, 06, 11, 14, 17, 18 написаны свёрткой, 10 — вызовом. У правила неотрицательности такой ход в ядре УЖЕ есть (раздел 3б-секстэ в flang/proof/SPEC.md), но он живёт внутри правила, а не в построении посылки
2. Таблица переписок для встроенных формдлина [х] → 1, длина пусто → 0, (добавить х к с) содержит хда1 из 20 прямо (07), 03 в одиночку и без индукциибаза 07 — (длина [значение]) равен ((длина пусто) плюс 1); сегодня ядро видит там свободное имя значение и останавливается, а после переписки от имени не остаётся ничего
3. Правило для цели «не больше <выражение>» (сегодня потолок обязан быть конечным литералом)1 из 20 (09) прямо; у 19 и 20 снимает вторую помеху из двухэто самая частая форма утверждения о библиотеке — «результат не длиннее входа»
4. Индукция по строка2 из 20 (19, 20)в flang/proof/SPEC.md отложена намеренно и с доводом: у строки голова — тоже строка, значит мест X два, и принцип потребовал бы два допущения
5. Индукция по признак (ветка work/indukciya-vstroennyh, в main нет)1 из 20 (12)самый дешёвый из всех возможных случаев: два значения, четыре строки таблицы
6. Доступ к полю суммы из выражения2 из 20 (01, 02)дыра языка, не ядра
7. Правило для цели вида «или»1 из 20 (08)
8. Проверка варианта выражением; равенство на параметре типа2 из 20 (16, 15)дыры языка; вторая — решение владельца, это новое понятие в системе типов
9. Вычисление замкнутой цели (ветка work/zamknutaya-cel, в main нет)0 из 20 на этой выборкесказано прямо, потому что легко переоценить: все базы индукции в двадцатке либо уже закрыты подходящим примером, либо содержат свободное имя, и вычисление их не берёт. Отдача этой работы лежит в другом месте — в 50 леммах из 56 (docs/lemmas-report.md), а не здесь

Первая строка стоит первой не по вкусу, а по счёту: свёрток в библиотеке 45 из 208, разборов — 58. Пока индукция цепляется только за вторые, доказуема меньшая половина библиотеки, и никакое расширение списка правил этого не меняет.


Про требует: слово появилось, цена измерена, отдача пока ноль

Прошлый замер писал условие импликацией в постусловии и платил 11 строк за предикат. Теперь есть требует, и на функции 10 («Двоичный поиск») это выглядит так:

  требует «список отсортирован» «Отсортирован» от элементы

Слово работает: типы проходят, отказ ядра прямо говорит «известно: предусловие функции «Двоичный поиск»» — то есть предусловие действительно приезжает в доказательство фактом. Цена — 15 строк предиката «Отсортирован», потому что сказать «отсортирован» одним выражением по-прежнему нечем. Закрыло оно ноль целей: тело функции — вызов, посылку индукции строить не из чего, и до места, где факт пригодился бы, доказательство не доходит.

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


Чего этот замер не мерил

Оговорки, работающие против доказательств, — назову сам

  1. Утверждения, которые я писал, слабее полного смысла функции. Полностью смысл выразили немногие; чаще писалось «длина не больше», «результат неотрицателен». Именно слабые ядро и брало.
  2. Одиннадцать из двадцати файлов не проходят flang check нарочно: отвергнутая теорема оставлена в файле уликой, а не убрана. Это не поломка дерева — файлы лежат в docs/ и в корпус (flang/**/*.flang) не входят, ведомость по корпусу их не видит.
  3. У всех двадцати функций уже стояли примеры автора. Замер их убрал и писал свои, но знание того, что функция проверена, писать примеры помогает: реальная цена тестов «с чистого листа» чуть выше измеренной.
  4. Тесты нашли те же четыре ошибки, что и в прошлый раз. Считать их находкой ЭТОГО дня нельзя.

Чем это проверить — четыре команды и четыре числа

LC_ALL=C.UTF-8 bash benchmarks/proof-cost/all-tests.sh

→ 110 примеров, 4 красных, и все четыре — ошибки библиотеки, а не примеров: 11 («Уникальные» на -0), 13 («Чётное» на -4), 19 два («Строчная буква» на "AB" и на пустой строке).

LC_ALL=C.UTF-8 node benchmarks/proof-cost/schitat.mjs

→ строк тестов 394, строк доказательства 169, секунд 276 / 589, попыток 20 / 41.

LC_ALL=C.UTF-8 node benchmarks/proof-cost/tela.mjs

→ тел вида «разбор по параметру» в flang/stdlib58 из 208.

LC_ALL=C.UTF-8 bootstrap/flang check docs/benchmark2/04-dictionary-size.flang --proof
LC_ALL=C.UTF-8 bootstrap/flang check docs/benchmark2/13-even.flang --proof

→ два «доказано … теоремы при нём нет и не нужно». Это и есть 2 из 20.


Вывод одним абзацем

Год назад — сутки назад — доказательство обычной функции в flang было невозможно: 20 из 20 не закрылись, и тринадцать упирались в отсутствие принципа индукции у встроенных типов. Принцип появился (список, отрезок нат), шесть дыр состоятельности закрыты, требует заведено — и число сдвинулось с нуля до двух из двадцати, причём оба доказательства обошлись в одну строку и ни одной теоремы, то есть в восемнадцать раз дешевле тестов на тех же функциях. Теорем при этом написано двенадцать и принято ноль: всё, что доказано, доказано без теоремы вовсе. Узкое место переехало и называется теперь точно: ядро цепляет индукцию только к телу-разбору, а 72 % библиотеки написано свёрткой, условием и вызовом. Тесты за то же время дали 20 работающих наборов из 20 и показали четыре старые ошибки, ни одна из которых не починена; доказательства не показали ни одной — и на функции «Чётное» напечатали «доказано обо ВСЕХ входах» про утверждение, истинное ровно потому, что две функции ошибаются одинаково. Отношение цены за результат сегодня 4,3 раза по строкам и 21 раз по времени не в пользу доказательств; отношение цены за результат ТАМ, ГДЕ ЯДРО БЕРЁТ, — в 18 раз в пользу доказательств. Дальше двигать надо не цену, а охват.