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

Каталог спек: стенд, проверка и слепок

Соседняя страница — Спеки: доказанное правило — показывает, ЧТО такое спека-программа. Эта отвечает на следующий вопрос: как из спек собирается каталог, который не расползается, и чем он проверяется.

Каталог живёт в дереве и называется fspec/. Это не спецификация языка — спецификация языка лежит в docs/flang/SPEC.md. Это образец пакета и одновременно стенд: сорок с лишним правил предметной области, записанных программами, правило их приёмки, проверяющая программа, память о том, что стояло вчера, набор нарочно испорченных случаев и набор программ, показывающих границу.

Что где лежит

файлчто делаетзамер
spec/*.flangсами спеки: правила предметной области43 файла , 140 своих примеров
policy.flangправило приёмки на самом языке: что считать доказанным, что значит «согласна с предшественницей», что значит «содержание не переписано»250 строк
guard.flangплан: прочитать слепок, найти спеки, спросить компилятор, назвать беду, поставить код возврата1252 строки , 138 примеров
snapshot.flangоснастка: переписать слепок по нынешним спекам121 строка
snapshot.txtсам слепок: строка на обещание — файл, функция, имя, цель102 строк
forgery.flangподлог: нарочно испорченные каталоги, на каждом проверка обязана покраснеть279 строк
clarifications.flangмастер уточнений: из неудачи доказательства делает вопрос к автору требования397 строк
experience/грубое требование и два ответа на него — стенд для мастера уточнений3 файла
experiments/программы, показывающие границу; их не надо чинить22 файла
settings.txtдве строки: путь к компилятору и каталог спек

Разбор устройства мастера уточнений — отдельный документ, docs/fspec/clarifications.md.

Стенд написан на flang целиком: ни одного файла на JavaScript в каталоге нет. Это оказалось возможно потому, что проверка не разбирает .flang сама — она запускает компилятор процессом и читает ответ, а на это у языка есть поручения «Перечислить каталог», «Прочитать файл» и «Запустить процесс».

Как выглядит спека

Спека — не markdown, а программа. Вот первая спека каталога целиком, fspec/spec/01-discount-cap.flang, без комментариев:

модуль «Спека 1: потолок скидки»

тотальная функция «Потолок скидки»
  принимает сумма: число
  возвращает число
  обеспечивает «скидка не больше 30» результат не больше 30
  обеспечивает «zh: 折扣不超过 30» результат не больше 30
  обеспечивает «en: discount is at most 30» результат не больше 30
  пример «потолок»
    дано сумма равно 1000
    ожидается 30
  30

Имя утверждения — опознавательный знак. По паре «функция плюс имя» спеки сверяются между собой и со слепком. Переименовали утверждение — связь порвалась, и это видно прогоном, а не чтением.

Связь со спекой, написанной раньше, — строка использует:

модуль «Спека 3: наценка за срочность»
  использует «Спека 2: промо» из "./02-promo.flang"

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

Правило приёмки

Записано на самом языке, в policy.flang. Спека принимается, только если каждое её утверждение доказано, и каждое утверждение предшественницы осталось в отчёте наследницы доказанным.

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

Что отвергается:

что нашлосьпочему это не годится
«объявлено, не доказано»утверждение может оказаться ложным, и узнают об этом на боевом запуске
«сетка» — посчитано на значениях автораэто не утверждение обо всех входах, и складывать его с другим нельзя
«НАРУШЕНО на примере»ложь, уже найденная примером автора
спека без единого обеспечиваетпроверять нечего, а зеленеет она так же, как сошедшаяся
утверждение предшественницы пропало или ослабленоновое правило отменило старое молча
утверждение из слепка пропало из спекправило, однажды доказанное, снято молча
у утверждения из слепка переписана цельимя прежнее, обещание другое
утверждение есть, а в слепке его нетпока оно не записано, переписать его можно молча
каталог спек пустпроверка, которой не на чем разойтись, зеленеет всегда
одно имя носит два разных понятиячитатель считает, что речь об одном, а речь о двух
строка принимает разобрана не целикомпока вход не разобран, подмену понятия под этим именем не увидеть

Цена названа честно. Истинное, но невыводимое утверждение тоже будет отвергнуто: ядро неполно, и образец неполноты лежит в дереве — flang/proof/examples/precondition.flang. Спека, упёршаяся в неполноту, — это заявка на новое правило ядра, а не повод ослабить приёмку порогом. Какие записи ядро берёт сегодня, разобрано на странице Какие обещания ядро берёт.

Что считается настоящим утверждением

Утверждение может быть доказано и при этом не говорить ничего. Ослабленное утверждение — то, которое переживает подмену тела заглушкой той же подписи: 0, "", нет, пустой список. Если заглушка тоже проходит, утверждение верно про ЛЮБУЮ функцию такой подписи, а про эту не говорит ничего.

Проверяется это прогоном, а не спором: скопировать функцию в отдельный файл, оставив подпись и обеспечивает, заменить тело заглушкой и позвать check.

модуль «Заглушка»

тотальная функция «Потолок скидки»
  принимает сумма: число
  возвращает число
  обеспечивает «скидка не больше 30» результат не больше 30
  0
$ flang check zaglushka.flang --proof
zaglushka.flang: ПРОВЕРЕНО САМОСТОЯТЕЛЬНО — утверждений 1: доказано 1 … код возврата 0

Заглушка прошла — значит утверждение первой спеки ослаблено, и это не оговорка, а часть замысла: у функции с телом-константой содержательного обещания нет вовсе. Первые две спеки показывают, как выглядит СВЯЗЬ двух спек, а не как выглядит сильное утверждение.

Отсюда и выбор формы обещаний дальше по каталогу. Заглушку не переживают утверждения о длине ((длина результат) равен ((длина х) плюс 1)), о вхождении (результат содержит "…"), о булевой формуле, о полях записи и о границе, где границей стоит сам результат (цена не больше результат). Переживают — односторонние верхние границы (результат не больше 30) и неотрицательность (результат не меньше 0).

Слепок: память о том, что стояло вчера

Правило приёмки смотрит на спеки, КАКИЕ ОНИ СЕЙЧАС. Отсюда дыра: переписать цель утверждения, не тронув его имени, — и приёмка не заметит.

Дыру закрывает snapshot.txt — строка на каждое обещание, и в ней, кроме файла, функции и имени, стоит САМА ЦЕЛЬ дословно:

01-discount-cap.flang :: Потолок скидки :: скидка не больше 30 :: результат не больше 30

Проверка читает слепок первым делом и сверяет его со спеками в обе стороны:

Слепок пишется отдельной командой, и это тоже правило:

flang io fspec/snapshot.flang

Слепок, обновляющийся молча вместе со спеками, не защищал бы ни от чего: он записал бы ослабление вместе с ним. Правка цели — это ДВА действия, и второе видно в разборе изменений отдельной строкой.

Откуда берётся цель. Из исходника спеки, а не из отчёта компилятора: в отчёте при утверждении стоит проза («доказано сведением цели с телом функции…»), и сверять по ней содержание значит сверять по формулировке доказательства. Разобранная цель в отчёте есть, но она втрое больше остального отчёта, и разбор такого JSON на flang стоит гигабайтов памяти. Поэтому цель читается из первоисточника — строки обеспечивает самой спеки, знак в знак.

Соглашение, на которое это опирается: обещание пишется одной строкой — обеспечивает «имя» цель или под квантором, для всех н обеспечивает «имя» цель. Квантор входит в записанную цель, потому что «для всех н» и «для всех м» — разные утверждения. На соглашение поставлен замок: правило «исходник разобран не целиком» сверяет число вычитанных строк обеспечивает с числом утверждений отчёта. Разойдутся — проверка краснеет и говорит, что сверять содержание нельзя.

Чего слепок НЕ делает: он не судит, слабее ли новая цель прежней. «Не больше 1000» и «не больше 30» для него просто РАЗНЫЕ. Он говорит другое и меньшее: содержание доказанного обещания не меняется молча.

Требование на другом языке

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

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

Переводится ИМЯ обещания. Имена функций, параметров и переменных — нет: тело у видов общее, и переведённое имя функции проверка отвергает отдельной бедой.

Метки языков — список ЗАКРЫТЫЙ, и перечислен он в функции «Метки языков» самой проверки. Будь он открыт, опечатка zn: перестала бы быть видом и стала бы ОТДЕЛЬНЫМ обещанием, которое ни с чем не сверяется, — тихое расхождение вместо красного. Отсюда цена, названная прямо: разделитель ": " в имени обещания означает метку языка и ничего другого.

Чего сверка НЕ умеет: прочитать перевод. Если китайский вид назван неверно, а цель при нём стоит верная, проверка промолчит — судить о СЛОВАХ ей нечем. Она судит о том, что формально: два вида одного обещания обязаны обещать одно и то же.

Подлог: проверка обязана краснеть

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

flang io fspec/forgery.flang --timeout 600000

Ловятся, в числе прочего: ослабление правила под тем же именем; спека без предшественницы; вид на другом языке, обещающий не то, что основной; опечатка в метке языка; переведённое имя функции; одно имя правила с двумя разными подписями; замкнутая ложь у функции без входов.

Сегодня стенд красен, и вот чем

Так выглядит прогон на 11 сентября 2026, двоичным 0.7.17 (коммит 2c40752d0; впервые снято 8 сентября на 0.7.14, с тех пор не изменилось):

$ flang io fspec/guard.flang
бед в системе спек: 79
03-urgency-markup.flang: утверждение «скидка с промо не больше 30» функции
«Скидка с промо» доказано у 02-promo.flang, а здесь его нет или оно ослаблено
…
код 1

Все 79 бед одной породы, и причина у них одна: отчёт check --proof больше не содержит утверждений подключённых модулей. Наследница получает отчёт «ПРОВЕРЕНО САМОСТОЯТЕЛЬНО», где стоят только её собственные утверждения, — а второе правило приёмки требует найти там утверждение предшественницы.

$ flang check fspec/spec/03-urgency-markup.flang --proof
  утверждений 1: доказано 1 (из них без теоремы 1, объявленным типом 1), сетка 0

Подлог тем же прогоном отвечает «расхождений 7 (подложенных случаев 19)», и шесть из семи — следствие той же перемены: случаи, ждавшие слов «не доказано — declared» и «ЗАМКНУТА», получают вместо них отказ на самом языке.

Красноту стенд не прячет: она записана в описи проверок дерева (docs/ci-inventory.md) словами «КРАСЕН по делу», а починка ведётся задачей задачника. Пишете свои спеки — держите в уме, что проверка в этом состоянии краснеет на каждой наследнице, а не только на вашей ошибке.

Границы: чего одна flang check не видит

Программы в experiments/ показывают, где компилятор говорит, а где молчит. Коды сняты 8 сентября 2026 двоичным 0.7.14 и перепроверены 11 сентября двоичным 0.7.17 (коммит 2c40752d0) командой flang check <файл> --proof:

программачто в ней написанокод
contradiction-without-example.flangдва невыполнимых вместе обеспечивает у одной функции3
contradiction-one-function.flangто же противоречие плюс пример автора1
contradiction-in-requirements.flangдва невыполнимых вместе требует и вызывающий1
contradiction-delivery-window.flang«не дольше трёх дней» и «не быстрее семи», с примером1
contradiction-money-rounding.flang«итог кратен рублю» и «в итоге пятьдесят копеек», с примером1
contradiction-return-defect.flangдва правила возврата в одной функции, с примером1
contradiction-tax-theorem.flang«ставка не выше 20» и «не ниже 27», при каждом теорема1
contradiction-weight-requirements.flangтребует «тяжелее десяти» и «легче пяти» плюс вызывающий1
theorem-on-false.flangтеорема, доказывающая ложь1
false-claim-strict-discount.flang«скидка строго уменьшает цену» — ложно на нулевом заказе1
false-claim-not-a-number.flangто же, но цена объявлена число: ложно на «не число»1
false-claim-negative-zero.flang«сторно нулевого платежа есть ноль» — ложно на минус нуле1
spec-legacy.flang + spec-new-feature.flangпротиворечие ЧЕРЕЗ ФАЙЛ: наследница обещает обратное0 и 3
conflict-cap-legacy.flang + conflict-cap-heir.flangнаценка: новая спека требует «не меньше 50» и зовёт прежнюю0 и 3
conflict-rights-legacy.flang + conflict-rights-heir.flangдоступ: новая обещает, что чтения у гостя не осталось0 и 3
silent-drop-legacy.flang + silent-drop-heir.flangсклад: наследница подключает предшественницу отбором (только) и роняет одно её правило0 и 0
superseded-cap-30.flang + replacement-cap-45.flangзамена правила: вторая редакция потолка, без наследования0 и 0

Читается таблица так.

Три строки false-claim-* — не про доказуемость, а про истинность. Доказано ≠ правильно: ядро выводит то, что написано, и если написана ложь, поймает её пример, а не вывод. Ноль, пустое, край отрезка, минус ноль и «не число» стоят одной строки примера каждый — и дешевле боевого запуска. У «не числа» ядро с некоторых пор отвечает прямо, отдельной диагностикой FLANG_BOUND_ON_NAN, и называет контрпример словами.

Код 3 — это «НЕ ПРОВЕРЕНО». Утверждение высказано, доказательства при нём нет, ядро откладывает его на рантайм. Раньше такой файл получал код 0, и на этом стояла половина довода «одной flang check мало»; сегодня он получает свой код, и половина довода закрыта самим языком.

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

Ядро тут не виновато, и чинить его не надо. Непротиворечивость набора спек — это правило приёмки поверх отчёта о доказательствах, а не новое правило внутри ядра. Ровно оно и написано в policy.flang.

Как добавить спеку

  1. Положите файл в fspec/spec/ с номером в начале имени: 43-….flang. Номер задаёт порядок, в котором спеки читает человек.
  2. Первой строкой — модуль «Спека 43: …». Опирается на написанную раньше — второй строкой использует «Спека 42: …» из "./42-….flang".
  3. Напишите функцию с обеспечивает «имя» …. Имя пишите так, чтобы оно читалось в отчёте о беде: оно туда и попадёт.
  4. Прогоните проверку: flang io fspec/guard.flang. Получило утверждение «объявлено, не доказано» — либо цель сформулирована слишком широко, либо телу нечем её нести; ослаблять приёмку нельзя, а переписать цель или тело можно.
  5. Проверьте утверждение подменой тела на заглушку. Пережило — переписывайте: доказано оно про подпись, а не про вашу функцию.
  6. Впишите обещание в слепок: flang io fspec/snapshot.flang. Пока его там нет, проверка красит спеку словами «не записано в слепке».

Имена файлов — английскими словами, содержимое — русское. Транслит не годится ни в ту, ни в другую сторону.

Чего здесь нет

работапочему нужна
проверка совместности, а не выводимости«не доказано» ≠ «ложно». Чтобы отличать, нужен поиск противоречащего примера на конечной сетке — тогда найденное будет ложью, предъявленной значением, а не молчанием
спека до реализацииобеспечивает вешается только на функцию с телом, и утверждение выводится ИЗ ТЕЛА. Требование, у которого реализации ещё нет, высказать нечем
суждение о том, СЛАБЕЕ ли новая цель прежнейслепок видит, что цель другая, и красит любое расхождение. Чтобы отличить ослабление от усиления, нужна импликация между двумя целями, а её ядро не выводит
прогон примеров самой проверки138 примеров guard.flang написаны и ни одним ярлыком не гоняются: прогон упирается в предел шагов. Проверка, о которой неизвестно, работает ли она, ничем не лучше отсутствующей

Вторая строка — главная незакрытая. Спецификация без тела в языке сегодня не высказывается вовсе, а с телом она уже наполовину реализация.

Полный решатель выполнимости (Z3 и подобные) сюда не годится и не будет: он отвечает «невыполнимо» без предъявления значения, на котором это видно. Принять такой ответ значило бы завести аксиому «внешний решатель не врёт», а аксиом в языке ноль. Подсказчиком он допустим: ищет он, а решает ядро по тому, что он предъявил.

Что почитать дальше