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

Цена втаскивания цели печати в двоичный оказалась не в столкновениях имён, а в долге эталона, который прятала побайтовая сверка с вычитанием

Печать в Go втащена в замыкание двоичного (flang/self/bootstrap/compiler.flang, строка использует «Печать в Go»). Ожидание, поставленное предыдущим слоем (five-binary-commands-and-the-price-of-each), было «платишь за встречу с уже втащенными»: у факт-чекинга вышло 21 столкновение на 148 объявлений. Здесь замер дал 1 столкновение на 197 привезённых объявлений — функция «Слить просьбы», занятая в flang/self/link.flang; типов и вариантов не столкнулось ни одного, то есть беды, которую в прошлый раз нашёл не линковщик, а cc, не было вовсе. Развелось одним переименованием («Слить просьбы Go»), три вхождения.

А дорого вышло другое, и в оценке этого не значилось.

Долг эталона был назван честно и при этом прятал несобирающийся вывод. flang/self/emit-go.flang не печатал БЛОК ГРАНИЦЫ ВХОДА — таблицу объявленных типов параметров. Сверка со свидетелем (self-emit-go.test.mjs) от этого не краснела: она вычитала блок из вывода свидетеля и сверяла остаток. Вычитание было описано в тексте теста абзацем на четырнадцать строк, с указанием, что расхождение всегда одно и то же.

Чего абзац не говорил: в вычтенном блоке стоит func Entry(), а flang/src/emit/go/flang_cli.go зовёт flang.Entry(). То есть напечатанное эталоном не собиралось вовсе, пока печатался прогонщик, — а прогонщик печатается по умолчанию. Сверка была зелёной на 108 программах из 108.

Цена закрытия: 104 строки на flang (8 функций и четыре поля в «Настройках Go»), ноль строк на JavaScript в реализации. После этого вычитание из теста убрано, сверка снова побайтовая на целом файле — и снова 108 из 108.

Класс подтверждён вторым случаем в тот же день. Агент по Rust нашёл ровно то же у своей цели независимо и до меня: у него блок вычитался так же, прогонщик крейта звал program::entry() так же, и крейт без блока не собирался (error[E0425]: cannot find function 'entry'). Ему это стоило 128 строк на flang, мне — 104. Он же проверил остальные и говорит: слов «Граница входа» нет в эталонах python, java, csharp и elixir — то есть долг тот же и там. Разобрано в the-entry-boundary-was-a-debt-of-printing-not-a-subtraction-in-the-check.

Класс, а не случай. Признак, по которому это ищется в других целях заранее: взять побайтовую сверку эталона со свидетелем и найти в ней всякое место, где из одной стороны что-то ВЫЧИТАЕТСЯ или НОРМАЛИЗУЕТСЯ перед сравнением. Каждое такое место — незакрытый долг, и вопрос к нему один: соберётся ли то, что остаётся после вычитания. «Расхождение названо» не значит «расхождение безобидно».

Ещё один замер, попутный и важный: двоичный не отбрасывает недостижимое, а свидетель отбрасывает. flang emit --target c у двоичного зовёт «Напечатать связанное», а та идёт мимо «Отбросить у отмеченной». На examples/import-check.flang это 80 209 байт против 6 096 в файле модуля (и 18 340 против 3 931 в заголовке) — то есть на любой программе с использует вывод двоичного и вывод Node расходятся целыми модулями. Печать в Go проведена через «К печати» и отбрасывает как свидетель; у цели c это осталось как было — правка туда меняет то, что сверяет неподвижная точка, и делать её заодно значило бы смешать две работы.

Что мешает диффу против Node стать нулём — названо и померено. Ровно одно: таблицу границы входа строит таблицаВхода из flang/src/types.mjs (строка 1180), у которой на flang нет пары. Двоичный поэтому печатает ПУСТУЮ таблицу и говорит об этом на stderr — то же поведение, что у цели c. Работа не заблокирована, а не сделана: машинерия в flang/self/types.flang уже есть («Собрать сигнатуры», «Собрать параметры», «Таблицы программы», 576 функций), не хватает уплощения в четыре списка.

flang check компилятора у двоичного КРАСЕН, и красен он был до этой работы. Замер обеими сторонами: старый двоичный на старом исходнике — FLANG_RECURSION_LIMIT: «Сторожа в списке» исчерпала лимит шагов (40000000) на глубине вызовов 35, код 1, 8 м 39,9 с. Новый двоичный на новом — то же самое, другой функцией того же обхода («Сторожа в узле», глубина 21), код 1, 8 м 48,4 с. Node на том же файле отвечает valid: true, ноль замечаний, 1 м 09,5 с. То есть предел витков прогона примеров (40 000 000, «Предел витков проверки» в compiler.flang) мал для примеров самого компилятора, и втаскивание цели этого не создало: рост времени 1,6 %, а не вдвое.

Чем подтверждено. Ветка rabota/vosem-go от ствола 061b09c9. Неподвижная точка — FTS_REQUIRE_TOOLCHAINS=c node --test flang/test/self-bootstrap.test.mjs: 37 из 37, красных ноль, «7 файлов совпали побайтово у свидетеля, flang₁ и flang₂». Столкновения — два независимых замера: свой обход замыкания и linkProgram, плюс scripts/stolknoveniya-celey.mjs восьмого агента (у него для go тоже 1 из 197). Сверка эталона — self-emit-go.test.mjs, 19 из 19, 108 программ из 108 побайтово. Сборка — -std=c99 -Wall -Wextra -Werror -pedantic -O2, ноль предупреждений. Напечатанный двоичным Go собран go1.26.5 и прошёл gofmt -l без правок.

Чем ограничено. Число 1 — про сегодняшнее замыкание (20 файлов, 345 типов, 3919 функций, 179 имён вариантов). Оно вырастет у того, кто втащит цель после меня: у js, python, csharp и elixir столкновений от 143 до 324, потому что они зовут свои части так же, как emit-c.flang.

Поправка к прозе, найденная замером. В flang/src/emit/c/flang_repl.c стояла таблица столкновений по всем семи целям, и для go там значилось 2 («Слить просьбы», «Только цифры»). Второго нет: compiler.flang ввозит stdlib/strings.flang через только из пяти имён, и «Только цифры» в замыкание не попадает вовсе. Число было снято по СЫРЫМ объявлениям, без правил видимости. Правило: столкновения считаются тем же обходом, каким их считает linkProgram, — иначе они завышены, и завышены незаметно.

Грабля машины, стоившая получаса. grep считает flang/self/link.flang двоичным файлом (строки до 683 символов) и молча не печатает НИЧЕГО — ни совпадений, ни предупреждения; grep -c отдаёт код 1. Из этого я сделал вывод, что «Слить просьбы» в этом файле нет, и он был неверен. Лечится ключом -a. Правило: по файлам flang/self/*.flang искать только grep -a.

Расхождение на не-латинских именах — не про Go и не про эту работу. На surfaces/factorial.eo.flang двоичный печатает AntaMeti, а Node Antaŭmeti: «Словарные символы» в emit-c.flang — таблица из 62 знаков латиницы и цифр, а words() свидетеля режет по юникодному \p{L}. Приезжает это импортом во все восемь эталонов, и у цели c на двоичном ДО этой правки видно ровно то же (faktorialo_anta_meti против faktorialo_antaŭmeti). Разобрано в word-splitting-is-ascii-in-flang-and-unicode-in-the-witness.

Связано: five-binary-commands-and-the-price-of-each, the-entry-boundary-was-a-debt-of-printing-not-a-subtraction-in-the-check, word-splitting-is-ascii-in-flang-and-unicode-in-the-witness, the-installed-binary-is-a-named-subset, emitter-twin-gap-is-only-caught-by-a-five-minute-build, checks-that-stopped-comparing