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

Где кончается flang и начинается хозяин

Ни прерываний, ни ассемблерных вставок в языке нет, а цикл службы на flang не пишется. Эта страница называет поимённо, какой слой системы писать можно, какой нельзя, и как эти два слоя разговаривают.

Короткий ответ:

На flang пишется то, что РЕШАЕТ. Хозяину остаётся то, что ЖДЁТ и что держит управление между двумя решениями.

Два слоя и дверь между нимиоперационная системахозяин на C:цикл, ожидание, сокеты,файлы, экранпрограмма на flang:решения и состояниеновое состояниеи следующее поручениеотклик: что случилосьпоручение: описание действия
Два слоя и дверь между ними

Слово «хозяин» здесь значит программу на другом языке, которая принимает события мира и исполняет ответ, — обычно это C. Решение, которым эта граница проведена, записано целиком в docs/adr/0008-layer-boundary.md, вместе с прогонами, опровергнувшими прежнее объяснение.

Чего граница НЕ означает

Привычное объяснение — «на flang пишется то, что завершается» — неверно, и неверно проверяемо.

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

$ cat вечный.flang
модуль «Вечный цикл»

функция «Крутить»
  принимает н: число
  возвращает число
  «Крутить» от (н плюс 1)

$ flang check вечный.flang; echo $?
модуль «Вечный цикл»: функций 1, из них с доказанным завершением 0; типов 0
без доказанного завершения: «Крутить»
вечный.flang: проверено — разбор, типы, завершаемость, ядро и примеры; замечаний нет
0

Незавершение при этом не превращается в зависание: и вычислитель, и напечатанная программа считают шаги и переводят исчерпание в названный отказ FLANG_RECURSION_LIMIT. То есть язык запрещает не бесконечный цикл, а ложь о завершении.

Планировщик на flang уже написан. Его годами приводили как образец того, чего написать нельзя. Он лежит в дереве — flang/concurrency/scheduler.flang, — и завершение доказано у всех его функций до единой. «Есть планировщик» оказалось свойством не кода, а того, кто держит управление: уберите из планировщика ожидание и вызов обработчика по имени, и останутся решения.

Что на flang не пишется

Четыре вещи, и ни одна из них не про длину цикла.

1. Ожидание. accept, connect, чтение сокета, poll, чтение часов, приход прерывания. У языка нет ни одного слова, которым это выражается: ждёт хозяин, а программа получает результат ожидания доводом.

2. Управление между двумя решениями. Между двумя действиями управление обязано выйти наружу. Передать функцию туда, где она приезжает данными, язык не умеет, а звать хозяина по имени программа не может — доступа к миру у неё нет. Поэтому цикл принадлежит хозяину.

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

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

Что на flang пишется

Всё, что превращает вход в вывод и ничего не ждёт. Это не обещание, а опись уже написанного:

чтогдефункцийиз них с пометкой тотальная
разбор и печать HTTPflang/stdlib/http.flang6464
шифр AESflang/stdlib/aes.flang105105
разговор с PostgreSQLflang/stdlib/postgres.flang6767
хеш SHA-256flang/stdlib/sha256.flang3737
перевод base64flang/stdlib/base64.flang1919
решения планировщикаflang/concurrency/scheduler.flang5454

Счёт — по заголовкам функций в файле (grep -c '^функция «\|^тотальная функция «', дерево 11 сентября 2026, выпуск 0.7.17). У планировщика то же самое сказал и сам компилятор, вместе с ввезённым модулем:

$ flang check flang/concurrency/scheduler.flang; echo $?
модуль «Планировщик узла»: функций 73, из них с доказанным завершением 73; типов 17; файлов вместе с импортами 2
0

Разбор пакетов, кодеки, состояния протоколов, проверка прав — и решения планировщика в том же ряду.

Что из этого следует про операционную систему

Переписать ядро операционной системы на flang по-прежнему нельзя, но довод другой, и разница не словесная:

На flang нельзя написать ожидание. Кто-то всё равно должен дождаться прерывания, и этот кто-то — не программа на flang.

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

Вторым, независимым препятствием стоит то, что хозяин сам обязан быть программой под операционной системой: он зовёт open, read, write, fork. То есть в такой системе flang стоял бы над ядром, а не был бы им.

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

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

В дереве есть отдельный пример, docs/examples/driver/uart.flang. Драйвер там — чистая функция «состояние и событие → новое состояние и список записей в регистры». Она ничего не пишет: она отвечает, что записать. Записывает хозяин, и вся его работа — цикл на шесть строк. Автомат при этом доказан целиком.

Отсюда правило, которое дороже всех частных: меряйте не файл, а строки. «Это трогает мир, значит на flang не пишется» почти всегда сказано о файле, в котором мир и решения о мире лежат вперемешку, и пропорция бывает какая угодно.

Как слои разговаривают

Способов два, и оба записаны данными, а не соглашением.

Первый: словарь поручений. Программа не выполняет действие — она возвращает описание действия, вариант суммы «Поручение». Хозяин исполняет и возвращает «Отклик» — тоже вариант суммы; отказ приезжает вариантом, а не исключением, исключений в языке нет. Вариантов поручения 23, и набор закрыт намеренно: позвать функцию из чужой библиотеки нельзя ни через разделяемый объект, ни системным вызовом.

Из этого следует свойство, которое иначе теряется: программа, ходящая в сеть, доказано завершается — потому что сеть в неё не входит. Завершается описание; ждёт хозяин.

Полномочия при этом остаются у хозяина, а не у программы: шесть запретов (--no-read, --no-write, --no-net, --no-clock, --no-random, --no-spawn), и запрещённое поручение возвращается отказом FLANG_IO_DENIED. Работает это ровно потому, что хозяин знает, что именно его просят сделать.

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

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

чтогдестрок
планировщик: цикл, потоки, ожидание сетиflang/src/emit/c/flang_conc.c4650
решения о процессах, надзоре и связиflang/concurrency/*.flang3350

Строки сняты 11 сентября 2026 (wc -l). Циклов без выхода в первом файле шесть (for (;;)), во втором ноль — и не потому, что они запрещены. Без ожидания такому циклу нечего делать, а ждать в языке нечем.

Дверь: одна функция, и типы сверяются до вызова

Печать в C кладёт рядом с кодом две двери. Разница между ними — не стиль.

/* вычисление: типы доводов НЕ сверяются */
fl_status privratnik_call (fl_ctx *ctx, const char *name, ...);
/* граница входа: объявленные типы сверяются ДО вызова */
fl_status privratnik_enter(fl_ctx *ctx, const char *name, ...);

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

Там же, на границе, стоят и предусловия. Строка требует «имя» условие — это договор с тем, кто зовёт; проверка на неё ставится в напечатанный код только там, куда значение приезжает извне. Внутренние вызовы за неё не платят.

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

Пример, который прогоняется

docs/examples/host-boundary/ — стык целиком, три файла:

файлчто это
docs/examples/host-boundary/gatekeeper.flangрешение: кого пропустить, кому отказать, с каким кодом. Модуль «Gatekeeper»
docs/examples/host-boundary/host.cисполнение: цикл, ввод-вывод, изменяемое состояние
docs/examples/host-boundary/run.shнапечатать в C, собрать системным cc, прогнать

Решения — разбор запроса, проверка прав, запас и его пополнение, переход состояния, коды ответа — все по эту сторону границы. Хозяин строит значение, зовёт одну функцию и исполняет то, что она вернула.

На стороне flang восемь функций, все тотальные. Ни одна из них ничего не делает — каждая возвращает значение:

тотальная функция «Шаг привратника»
  принимает врата: «Врата», событие: «Событие»
  возвращает «Решение»
  требует «запас не выше ёмкости» врата.«запас» не больше врата.«ёмкость»
  обеспечивает «ГЛАВНОЕ: из годных врат выходят годные» результат.«врата».«запас» не больше результат.«врата».«ёмкость»
  обеспечивает «ёмкость шагом не меняется» результат.«врата».«ёмкость» равен врата.«ёмкость»

На стороне C — то, чего на flang не написать:

for (;;) {
  if (fgets(line, sizeof line, stdin) == NULL) break;  /* мир решил, что событий больше нет */
  ...
  privratnik_enter(&ctx, "Шаг привратника", args, 2, &decision, &error);
  ...
}

Этот цикл на C не потому, что он бесконечный, — нетотальные функции язык принимает. Он на C из-за одной строки внутри: fgets ждёт.

Решение проверяется обычными командами:

bootstrap/flang check docs/examples/host-boundary/gatekeeper.flang --proof
bootstrap/flang test  docs/examples/host-boundary/gatekeeper.flang

Отчёт --proof говорит: все функции тотальны, завершение каждой доказано композицией; утверждений вида «объявлено, не доказано» нет; часть постусловий доказана обо всех входах сведением с телом функции, часть — на сетке примеров, и в их числе «ГЛАВНОЕ: из годных врат выходят годные» у «Шаг привратника» — неравенство над полями записи ядро не берёт. Сколько именно, печатает последняя строка отчёта: на 0.7.17 (11 сентября 2026) — «утверждений 31: доказано 22, сетка 9, объявлено, не доказано 0», код возврата 0; примеров 15, прошло 15. Доказанные постусловия в напечатанный код проверками не едут — печать называет это первой строкой («проверок при работе снято …»); недоказанное обещание ехало бы проверкой на каждом возврате.

Прогон сегодня не собирается. С 29 августа 2026 модуль называется «Gatekeeper» — все модули примеров названы по-английски, чтобы напечатанный файл не был транслитом, — и печать даёт файлы и функции с этим именем, а хозяин и скрипт прогона ждут прежние:

печать:  gatekeeper.c  gatekeeper.h      gatekeeper_enter(…)  gatekeeper_sozdat_vrata(…)
host.c:  #include "privratnik.h"         privratnik_enter(…)  privratnik_sozdat_vrata(…)
run.sh:  cc … privratnik.c …

Первый шаг прогона (печать) и обе команды выше проходят; второй шаг — сборка — отказывает (перепроверено 11 сентября 2026 на 0.7.17: bash docs/examples/host-boundary/run.sh → «privratnik.h: No such file or directory», «сборка отказала, код 1»). Вывод ниже снят до переименования; чтобы повторить его, имена в docs/examples/host-boundary/host.c и docs/examples/host-boundary/run.sh надо привести к новым.

$ bash docs/examples/host-boundary/run.sh
…
== 3. прогон: девять событий на стандартный ввод
хозяин: врата открыты. ёмкость 3, уровень ключа 2, запас 0
хозяин: жду событий на стандартном вводе: «такт» или «запрос N»
запрос 1 → код 429, ОТКАЗАНО, запас 0, пропущено 0, отказано 1
такт     → запас 1 из 3
такт     → запас 2 из 3
такт     → запас 3 из 3
такт     → запас 3 из 3
запрос 1 → код 200, пропущен, запас 2, пропущено 1, отказано 1
запрос 5 → код 403, ОТКАЗАНО, запас 2, пропущено 1, отказано 2
запрос 1 → код 200, пропущен, запас 1, пропущено 2, отказано 2
запрос 1 → код 200, пропущен, запас 0, пропущено 3, отказано 2
хозяин: событий 9, пропущено 3, отказано 2
хозяин: нарочно порчу довод — запас 4 при ёмкости 3
граница отвергла довод: FLANG_PRECONDITION — не выполнено требование «запас не выше ёмкости» функции «Шаг привратника»
код возврата: 0

Код возврата 0. Виден каждый исход: отказ по исчерпанному запасу (429), накопление запаса тактами, потолок ёмкости, пропуск (200), отказ по правам (403). Последней строкой хозяин НАРОЧНО подаёт порченый довод — запас выше ёмкости, — и предусловие границы его отбивает. Отказ приезжает статусом, а не падением: цикл хозяина продолжается.

Три подробности стыка, которые видны только в исходниках:

Рядом в дереве лежит второй стык, сделанный первым способом — через словарь поручений: docs/examples/io/фильтр-пакетов.flang разбирает заголовок дейтаграммы IPv4 вместе с портом TCP и решает, пропускать её или отбросить. Семнадцать функций, все тотальные; октеты уходят в операционную систему и возвращаются через write и read. Та же граница на других задачах: docs/examples/driver/ — на железе (драйвер MSI), docs/examples/web/orders-api.flang — на службе REST, docs/examples/io/link-report.flang — на вводе-выводе.

Что этим НЕ решено

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

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

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

Дальше