Круг раскрутки разорван — двоичный пересобирает себя без Node побайтово, но свои исходники проверить не может и считает медленнее Node
Разведка 19 августа 2026 отвечает на главный вопрос смены хозяина: если Node исчезнет совсем, компилятор пересобирается из исходников на flang, имея только cc и make. Круг «чтобы собрать двоичный, нужен двоичный» разорван закоммиченным каталогом bootstrap/ и замыкается обратно на себя побайтово.
Чем подтверждено. Ветка work/zelenyy-stvol, коммит 49d84ddc, прогон в чистом каталоге вне дерева:
make -C bootstrap -j8 CFLAGS='-std=c99 -Wall -Wextra -Werror -pedantic -O2'→flang7 959 696 байт, ни одного предупреждения, код 0. Это flang₁.- 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.
diff -r -q bootstrap КАТАЛОГ→ расходится толькоREADME.md(проза, в печать не входит). Остальные семь файлов совпали, sha256 сверен на трёх.- Из этой печати собран 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.flang | 0,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 с, код 0 | 347,5 / 351,4 / 349,2 с, код 1 | медленнее в 5,8× и падает |
check --proof компилятора | 68,2 / 67,4 / 65,2 с, код 0 | 267,2 / 277,4 / 271,4 с, код 1 | медленнее в 4,0× и падает |
| печать компилятора в C | 3,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. Расхождения не случайны, у каждого своя причина, и три из них — дыры в проверке, а не разница вывода:
- законы не считаются. Сломанный моноид (
единица 0→единица 7вexamples/cat/monoid-and-monad.flang): Node даётFLANG_MONOID_IDENTITYиFLANG_GROUP_INVERSE, код 1; двоичный — «проверено, замечаний нет», код 0. С ключом--proofон тот же файл честно отвергает кодом 2 и называет препятствие. То есть отказ есть у ведомости и нет у проверки; - запас витков у обработчика не требуется.
examples/web/shortener/handler-without-budget.flang: Node —FLANG_HANDLER_NOT_TOTAL, код 1; двоичный — код 0; - примеры при морфизмах не гоняются.
examples/cat/order-shipment.flang: Node прогоняет 4 примера, двоичный — 0. Так жеmodules/orders.flang6 против 4,modules/reconciliation.flang14 против 12,shortener/server.flang243 против 240; - и обратно, вывод параметров типа слабее:
examples/monad/order-total.flangдвоичный отвергает четырьмяFLANG_TYPE_PARAM, Node принимает.
Первые три опаснее последнего: ложное «нет» видно сразу, ложное «да» — нет.
Что совпадает байт в байт
Обе стороны знают ровно 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) |
|---|---|---|
| печать компилятора в C | 3 м 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