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

Круг раскрутки разорван — двоичный пересобирает себя без Node побайтово, но свои исходники проверить не может и считает медленнее Node

Разведка 19 августа 2026 отвечает на главный вопрос смены хозяина: если Node исчезнет совсем, компилятор пересобирается из исходников на flang, имея только cc и make. Круг «чтобы собрать двоичный, нужен двоичный» разорван закоммиченным каталогом bootstrap/ и замыкается обратно на себя побайтово.

Чем подтверждено. Ветка work/zelenyy-stvol, коммит 49d84ddc, прогон в чистом каталоге вне дерева:

  1. make -C bootstrap -j8 CFLAGS='-std=c99 -Wall -Wextra -Werror -pedantic -O2'flang 7 959 696 байт, ни одного предупреждения, код 0. Это flang₁.
  2. flang₁ печатает компилятор из исходников на flang в окружении без Node (env -i PATH=/usr/bin:/bin, где which node отвечает кодом 1):

`` flang emit flang/self/bootstrap/compiler.flang --target c --repl --cli \ --max-steps 40000000 --max-depth 20000 --runtime flang/src/emit/c --out КАТАЛОГ ``

→ 7 файлов, 14 585 529 байт, 3 м 43 с, код 0.

  1. diff -r -q bootstrap КАТАЛОГ → расходится только README.md (проза, в печать не входит). Остальные семь файлов совпали, sha256 сверен на трёх.
  2. Из этой печати собран flang₂ теми же ключами: sha256 f159584188…13e14106, 7 959 696 байт — тот же файл, что flang₁.

Итого неподвижная точка сходится и на двоичном тоже, а не только через реализацию на JavaScript.

Поправка от 20 августа 2026. Препятствия 1 и 2 из списка ниже закрыты, 3 — нет. Рецепт пересборки больше не записан только в JavaScript: он переехал в scripts/bootstrap-c.sh, откуда его читает и sh, и scripts/bootstrap-c.mjs. Печать двоичным стала РАБОЧИМ путём сборки, а не только предметом разведки, и её печать сверена с печатью реализации на JavaScript байт в байт — 7 файлов, 15 641 371 байт. Про то, что печать читает исходники рантайма C с диска (пункт 2), теперь сказано прямо там, где это важно: flang/src/emit/c/ — это 20 673 строки на C, и удалять их вместе с JavaScript нельзя. Подробности и числа — node-ushyol-s-puti-sborki.

Что этому мешает на практике — три названные вещи

1. Рецепт пересборки записан только в JavaScript. Ключи из пункта 2 не выдуманы: без --max-steps 40000000 --max-depth 20000 печать разойдётся, потому что пределы попадают прямо в напечатанный байт — bootstrap/flang_runtime.h, строки 10—11, против умолчаний рантайма FL_MAX_DEPTH 10000 и FL_MAX_STEPS 1000000. Единственное место, где эти числа записаны, — scripts/bootstrap-c.mjs (ПРЕДЕЛЫ). В bootstrap/README.md и в bootstrap/Makefile их нет вовсе (проверено grep). Уйдёт Node — уйдёт и рецепт, а собранный по памяти разойдётся молча.

2. Печать читает рантайм C с диска. --runtime / $FLANG_RUNTIME_DIR берёт шесть файлов flang/src/emit/c/, и в bootstrap/ их копий нет. Для пересборки нужен не каталог bootstrap/, а дерево целиком.

3. Двоичный не проверяет то, что печатает. flang check на flang/self/bootstrap/compiler.flang у двоичного падает: FLANG_RECURSION_LIMIT: функция «Сторожа в списке» исчерпала лимит шагов (40000000). С ключом --proof — то же самое на «Сторожа внутри узла». Причина измерена по коду: ctx->steps обнуляется в fl_ctx_init (bootstrap/flang_runtime.c:537), а зовётся он один раз на весь вызов (fl_human_main), то есть бюджет шагов один на всю команду. У реализации на JavaScript бюджет свой на каждый evaluate (умолчание 1 000 000 — flang/src/interpret.mjs:69), поэтому она проходит. Ключа, поднимающего предел, у check нет.

Печать при этом проходит — потому что flang emit у двоичного не проверяет программу вовсе. Программа с заведомо ложным примером печатается кодом 0, тогда как реализация на JavaScript отказывает кодом 1 и не пишет ни файла. То есть пересобрать компилятор без Node можно, а убедиться, что пересобираешь исправный, — нечем.

Двоичный медленнее Node на больших программах, а не быстрее

Это опровергает допущение, стоявшее в dropping-unreachable-code-in-flang-costs-180-times-more: «собранный двоичный компилятор считает то же самое машинным кодом, и к нему этот замер не относится вовсе». Относится. Машинный код здесь исполняет вычислитель, написанный на flang, и на больших входах он проигрывает прямой реализации на JavaScript под JIT-компиляцией V8.

предметNodeдвоичныйотношение
check examples/rosetta/quicksort.flang0,64 с0,10 сдвоичный быстрее в 6,4×
check self/lexer.flang, 1 917 строк1,17 с3,42 смедленнее в 2,9×
check self/types.flang, 6 876 строк2,90 с34,19 смедленнее в 11,8×
check компилятора, 46 880 строк59,7 / 58,1 / 60,4 с, код 0347,5 / 351,4 / 349,2 с, код 1медленнее в 5,8× и падает
check --proof компилятора68,2 / 67,4 / 65,2 с, код 0267,2 / 277,4 / 271,4 с, код 1медленнее в 4,0× и падает
печать компилятора в C3,4 с (bootstrap-c.mjs --check, вместе с побайтовой сверкой)223,0 смедленнее в 65×

По три прогона на строку там, где строка решает. Перелом лежит между 1 917 и 6 876 строками входа: на коротких программах выигрывает отсутствие запуска Node (~0,5 с), на длинных проигрывает вычислитель.

Что двоичный проверяет молча мимо

Свод по корпусу (167 файлов examples, flang/stdlib, flang/core): check — вердикт совпал на 165 из 167, test — число сошедшихся примеров совпало на 161 из 167. Расхождения не случайны, у каждого своя причина, и три из них — дыры в проверке, а не разница вывода:

Первые три опаснее последнего: ложное «нет» видно сразу, ложное «да» — нет.

Что совпадает байт в байт

Обе стороны знают ровно 10 команд (check, ast, run, test, facts, emit, io, lock, package, оболочка) — прежняя запись «6 против 12» в the-installed-binary-is-a-named-subset устарела. Побайтово совпал вывод check --proof, ast, facts, lock, package, io на настоящих файлах дерева. Расходится вид, а не вердикт, у check, test и run: Node печатает JSON, двоичный — человеческую прозу; у run двоичный печатает голое значение. Отдельно: run --args у двоичного берёт только плоский объект скаляров{"элементы":[2,3,4]} отвергается кодом 2.

Чем ограничено. Всё измерено на одной машине (Linux 7.0.0, cc 15.2.0, 256 ядер) и на версии 0.5.0. Отношения скорости — про эту пару реализаций, а не про «C медленнее JavaScript»: сравниваются вычислитель на flang, собранный в машинный код, и прямая реализация на JavaScript, а не два одинаковых алгоритма. io на examples/web/shortener/plan-durable.flang не сверен: он не завершается за 120 с ни на одной стороне и требует среды, которой в замере не было.

Дополнение 20 августа 2026: Node ушёл, рецепт остался, сверка подорожала в 200 раз

Реализация на JavaScript удалена (fe8e8a37, 48 файлов, 56 072 строки), и три помехи выше разрешились по-разному.

Помеха 1 снята. Рецепт больше не записан в JavaScript: ключи печати живут в scripts/raskrutka.sh (MAX_STEPS=40000000, MAX_DEPTH=20000, --cli, --repl) и больше нигде. Печатает сам двоичный, Node не участвует ни на одном шаге. Проверено прогоном: env -i PATH=/usr/bin:/bin (node на этом PATH не находится) проходит весь круг.

Помеха 2 уточнилась и оказалась хуже, чем записано. Печать ищет рантайм не просто «в дереве», а рядом с самим исполняемым файлом; подробности и замер — printing-to-c-looks-for-runtime-sources-next-to-the-binary. Лечится тем, что рецепт называет --runtime явно.

Помеха 3 осталась целиком. flang check на собственных исходниках по-прежнему отвечает FLANG_RECURSION_LIMIT, а flang emit не проверяет программу вовсе. Пересобрать компилятор без Node можно; убедиться, что пересобираешь исправный, — по-прежнему нечем. Второго мнения больше нет вообще: раньше расхождение ловила вторая реализация, теперь такой второй нет.

Круг сходится дважды, а не один раз. Замер на svod/vosem поверх fe8e8a37, версия 0.5.1, та же машина:

шагбыло (19 авг, 0.5.0)стало (20 авг, 0.5.1)
печать компилятора в C3 м 43 с, 14 585 529 байт12 м 03 с, 23 903 454 байта
сборка семени, -Werror -pedanticне засечена, 7 959 696 байт55 с, 12 694 664 байта
сверка печати с деревом3,4 с (через реализацию на Node)та же печать, 11 м 52 с

Совпало не только напечатанное, но и собранное: двоичный, собранный из семи файлов в каталоге вне дерева, — побайтово тот же файл, что закоммиченный bootstrap/flang, все 12 694 664 байта.

Цена, которую заплатили за удаление JavaScript. Сверка «точка раскрутки совпадает с печатью исходников» стоила 3,4 секунды, шла на любой машине и не требовала компилятора C. Теперь она стоит 12 минут, требует cc, make и около 19 ГБ оперативной памяти (пик RSS 20 072 180 КБ по /usr/bin/time -v). Это примерно двухсоткратное подорожание самой сильной проверки репозитория, и оно не обходится: единственный способ вернуть дешёвую сверку — вторая реализация печати, ровно то, что решено не держать.

Отдельное следствие: на обычном раннере GitHub ubuntu-latest (16 ГБ) сверка не поместится — не хватает примерно четырёх гигабайт. Поэтому в CI она не поставлена — названа как то, что зовут руками перед слиянием правки в flang/self/ или flang/src/emit/c/.

Связано: dropping-unreachable-code-in-flang-costs-180-times-more, the-installed-binary-is-a-named-subset, vedomost-dvoichnogo-byvaet-slabee-i-nikogda-ne-silnee, javascript-stays-only-as-a-print-target, four-pieces-of-javascript, byte-for-byte-comparison, printing-to-c-looks-for-runtime-sources-next-to-the-binary