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

Node ушёл с пути сборки: точку раскрутки печатает сам двоичный, и обе печати совпали байт в байт

До 20 августа 2026 перепечатать bootstrap/ — компилятор, напечатанный в C99, из которого он и собирается, — можно было ровно одним способом: node scripts/bootstrap-c.mjs. Значит Node лежал на пути СБОРКИ, а не только в проверках, и удалить реализацию на JavaScript было физически нельзя: без неё дерево не восстанавливается из своих же исходников flang/self/*.flang.

Теперь печатает сам двоичный: sh scripts/bootstrap-c.sh зовёт bootstrap/flang emit --target c. Курица и яйцо решается тем, что двоичный уже лежит в дереве собранным — make -C bootstrap даёт его из точки раскрутки, и он печатает следующую.

Главное число: две печати совпали БАЙТ В БАЙТ

Прогон 20 августа 2026, ветка vypusk/raskrutka-sama от c3bebbf0, окружение без Node (env -i PATH=/usr/bin:/bin, где node недостижим):

файлбайт
Makefile2 893
compiler_flang.c12 353 351
compiler_flang.h2 525 322
flang_cli.c49 020
flang_repl.c499 612
flang_runtime.c150 606
flang_runtime.h60 567
итого15 641 371

Все семь совпали с печатью реализации на JavaScript — сверено cmp пофайлово, не по сумме байт. Компилятор в этой печати: 4273 функции, 392 типа после связывания.

Это снято сейчас нарочно. Пока вторая реализация жива, сравнить есть с чем; после её удаления сравнивать будет не с чем вовсе, и утверждение «двоичный печатает то же самое» станет непроверяемым задним числом.

Повторено на восьмицелевой точке: снова СЕМЬ из семи

Ствол втащил в двоичный все восемь целей печати (9bc9da69) и следом удалил реализацию на JavaScript (fe8e8a37). Точка раскрутки выросла с 15 641 371 до 23 903 454 байт. Сверка повторена на этом дереве, в окружении без Node, той командой, что зовёт scripts/bootstrap-c.sh:

файлбайтсовпал с деревом
Makefile2 893да
compiler_flang.c19 291 878да
compiler_flang.h3 833 997да
flang_cli.c49 020да
flang_repl.c514 493да
flang_runtime.c150 606да
flang_runtime.h60 567да
итого23 903 4547 из 7

Печать стоила 11 м 44 с и 12,4 ГБ — против 5 м 37 с и 8,3 ГБ на одноцелевой. То есть цена растёт вместе с компилятором, и линейно по памяти она уже не помещается в обычный прогон CI.

ГРАБЛИ: без ключей пределов расходится flang_runtime.h, и это не беда печати

Замер той же командой БЕЗ --max-steps 40000000 --max-depth 20000 даёт шесть из семи: расходится flang_runtime.h. Причина не в печати, а в том, что пределы попадают прямо в напечатанный байт — #define FL_MAX_STEPS и #define FL_MAX_DEPTH стоят именно там. Умолчания бэкенда — 10⁶ и 10⁴; компилятор печатается с 40 000 000 и 20 000, иначе собранный из этой печати компилятор отвечает FLANG_RECURSION_LIMIT на любой настоящий вход (ровно так уехал v0.4.1).

Отсюда правило: команду печати не набирают по памяти. Она записана один раз в scripts/bootstrap-c.sh, и звать надо его, а не двоичный напрямую.

Цена: двоичный печатает в 99 раз дольше и берёт 8,3 ГБ

реализация на JavaScriptдвоичный
печать точки раскрутки3,4 с5 м 37 с
сверка --check целиком3,4 с4 м 57 с
памятьдоли гигабайта8,3 ГБ

Восемь гигабайт — не мелочь: обычный прогон CI даёт 7 ГБ, то есть там это отказ по памяти, а не медлительность. Поэтому проверка «печать двоичного совпадает с деревом» (flang/test/tochka-dvoichnym.test.mjs) по умолчанию говорит «НЕ ПРОВЕРЕНО» и включается явно — FLANG_TOCHKA_DVOICHNYM=1 или заранее собранным bootstrap/flang. Причина расхода известна и записана в bootstrap-circle-is-broken-but-the-binary-cannot-check-itself: вычислитель здесь написан на самом языке, а арена не отдаёт память до конца запроса.

Сколько JavaScript лежало на пути сборки — замерено загрузчиком, а не поиском

Мерить grep-ом по строкам import неверно: часть импортов динамические. Замер снят перехватчиком загрузки модулей (module.registerHooks) на настоящем прогоне печати:

былостало
файлов JavaScript, загруженных при печати точки раскрутки240
строк в них31 5970
байт2 110 4750
объявлений function5830

Крупнейшие из 24: flang/src/types.mjs (7 963 строки), flang/src/parser.mjs (4 739), flang/src/emit/c.mjs (2 342), flang/bin/flang.mjs (2 140).

Сборка релизного архива (scripts/build-release-c.mjs) грузила те же 24, потому что печатала архив сама. Теперь она берёт готовые семь файлов из bootstrap/ и грузит из дерева ОДИН файл — scripts/bootstrap-c.mjs, и то ради имён и пределов.

Проверено на дереве, где реализации на JavaScript нет вовсе

Выгрузка git archive HEAD в пустой каталог, затем удалены все 49 файлов .mjs из flang/src и flang/bin (56 256 строк). В таком дереве, без Node в PATH там, где он не нужен:

  1. make -C bootstrap → двоичный 8 544 752 байта, код 0;
  2. sh scripts/bootstrap-c.sh --check → 7 файлов, 15 641 371 байт, код 0;
  3. node scripts/build-release-c.mjs → «релиз готов»: архив собрался на PATH без Node, собранный компилятор ответил, установка проверена;
  4. node packaging/postinstall.mjs → двоичный собран за 34,2 с; node packaging/flang-launch.mjs --versionflang 0.5.1.

Ловушка: flang/src/emit/c/ — это C, а не JavaScript

В flang/src лежат не только 45 файлов .mjs. Там же — 20 673 строки на C (flang_runtime.[ch], flang_cli.c, flang_repl.c, flang_conc.[ch]), и печать читает их с диска дословно: они уезжают в вывод с приписанной сверху шапкой. Удали их вместе с JavaScript — и печать откажет словами «не найдены исходники рантайма C», а точка раскрутки перестанет перепечатываться совсем.

Отсюда же следует, что для восстановления компилятора нужен не каталог bootstrap/, а дерево целиком: bootstrap/ содержит только НАПЕЧАТАННЫЕ копии этих файлов, с шапкой, и печать их отвергает по делу — приписать шапку второй раз значило бы соврать о происхождении файла.

Рецепт печати переехал туда, где переживёт удаление

Пределы (--max-steps 40000000, --max-depth 20000) попадают прямо в напечатанный байт — #define FL_MAX_STEPS в bootstrap/flang_runtime.h. Единственной их записью был scripts/bootstrap-c.mjs; уйдёт JavaScript — уйдёт и рецепт, а собранный по памяти разойдётся молча. Теперь запись — в scripts/bootstrap-c.sh (FLANG_MAX_STEPS, FLANG_MAX_DEPTH), а ПРЕДЕЛЫ в .mjs читаются оттуда разбором ИМЯ='значение' у левого края. Это закрывает названное препятствие №1 из bootstrap-circle-is-broken-but-the-binary-cannot-check-itself.

Чем ограничено

Ступени не сняты. Печатает двоичный, собранный из того, что лежит в дереве СЕЙЧАС, а печатает он СЕГОДНЯШНИЕ исходники. Если правка тронула сам слой печати (flang/self/emit-c.flang), одного захода мало: первый даёт печать по старым правилам от новых исходников. Надо пересобрать и перепечатать, пока два захода подряд не дадут одно и то же. Скрипт называет число тронутых файлов сам.

Двоичный по-прежнему не проверяет то, что печатает: flang emit не гоняет ни типы, ни завершаемость, ни примеры, а flang check на самом компиляторе исчерпывает бюджет шагов. Это препятствие №3 из соседней заметки, и оно НЕ закрыто. То есть пересобрать компилятор без Node можно, а убедиться, что пересобираешь исправный, — нечем.

Всё измерено на одной машине: Linux 7.0.0, cc 15.2.0, 256 ядер, 499 ГБ памяти, версия 0.5.1. Секунды плавают с загрузкой машины; байты — нет.

Сверку двух печатей повторить уже нельзя: реализация на JavaScript удалена в тот же день. Число 15 641 371 снято нарочно в последний день, когда было с чем сверять. Дальше сверяется только «печать двоичного совпадает с тем, что лежит в bootstrap/», и это другое, более слабое утверждение: оно ловит устаревший артефакт, но не ловит расхождение двух пониманий языка — второго понимания больше нет.

Связано: bootstrap-circle-is-broken-but-the-binary-cannot-check-itself, four-pieces-of-javascript, javascript-stays-only-as-a-print-target, byte-for-byte-comparison