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

Втаскивание модуля в самоприменённый компилятор стоит не по размеру модуля, а по числу столкновений имён с уже втащенными

Связывание в flang сливает объявления всех модулей в ОДНО плоское пространство имён. Поэтому цена «добавить модуль в список использует» не читается по размеру модуля: платит не он, а его встреча с теми, кто уже там.

Чем подтверждено. Три замера подряд на одном и том же компиляторе (flang/self/bootstrap/compiler.flang):

Что втащилиОбъявлений у негоУже былоСтолкнулось
вычислитель (interpret.flang)29823603 (1 %)
факт-чекинг (factcheck.flang)148379821 (1,4 %)
языковой сервер (lsp.flang)314426447 (15 %)
печать в Rust (emit-rust.flang)23442642 (0,85 %)
печать в Java (emit-java.flang)37745367 (1,9 %)

Факт-чекинг ВДВОЕ МЕНЬШЕ вычислителя и стоил в СЕМЬ РАЗ больше переименований. Разгадка простая: имена он выбирает из того же словаря предметной области, что и разбор языка с проверкой типов, — «Отказ», «Сравнить», «Найти функцию», «Есть ключ», «Это истина». Модуль про другое (вычисление) столкнулся почти ни с чем; модуль про то же (разбор и сверка) столкнулся со всеми.

Признак для оценки заранее. Пересечь множество объявленных имён модуля с объединением имён уже втащенных — минуту работы, и это ЗАМЕР, а не оценка. Оценка «модуль маленький, значит дёшево» промахнулась вчетверо.

Второе место, где связывание молчит: ВАРИАНТЫ. Диагностику FLANG_DUPLICATE_NAME связывание выдаёт на типы и функции. Имена вариантов сумм оно НЕ сверяет — и правильно, они принадлежат своему типу. А генератор кода в C транслитерирует вариант в отдельную функцию верхнего уровня, и два варианта с одинаковым именем в разных типах дают error: redefinition of compiler_flang_variant_est_funkciya.

Поймалось это только компилятором C, то есть на четвёртой минуте сборки, уже после перепечатки точки раскрутки. Проверять надо тем же пересечением, что и имена, но по варианту — не считая тех, что приезжают общим импортом (у factcheck.flang и core/json.flang варианты «Значение записи», «Значение списка», «Значение скаляра» одни и те же, потому что это одно объявление).

Порядок в списке использует — тоже часть цены. Модуль, просящий импорт только, обязан стоять ПОСЛЕ тех, кто просит целиком: разрешение работает по первому обходу, и wanted для уже загруженного файла не расширяется. Отсюда factcheck.flang поставлен последним — его узкая просьба к core/json.flang не должна была стать просьбой всего замыкания.

Третий замер подтвердил признак и опроверг процент. Языковой сервер — 314 объявлений против 4264 — дал 47 столкновений, 15 %: вдесятеро большая доля, чем у двух предыдущих. Предметная область у него та же самая и в предельном виде: он рассказывает редактору про разбор, типы и завершаемость, то есть говорит словами всех пятнадцати слоёв сразу. Тридцать из сорока семи — один блок доступа к полям JSON («Поле узла», «Взять поле», «Это строка», «Элементы поля»), выписанный в сервере заново и слово в слово совпавший с тем, что уже выписан в генераторе кода C. Остальные — «Отказ», «Вызвать», «Описание», «Область», «Параметр», «Итог».

Четвёртый замер (19 августа 2026) уточняет признак, а не подтверждает его. Печать в Rust — РОДНОЙ БРАТ уже втащенной печати в C: та же предметная область, то же строение, те же слова. По признаку «модуль про то же стоит дороже» она обязана была стоить как факт-чекинг или дороже. Вышло 2 столкновения из 234 своих объявлений («Без ведущих пробелов» — с разбором языка, «Ключи полей» — с вычислителем) — дешевле вычислителя, который про совсем другое.

Разгадка ровно одна и она видна первой строкой файла: emit-rust.flang не объявляет общую машинерию заново, а ввозит её из emit-c.flang списком только на 162 имени — навигацию по AST, таблицы «имя → идентификатор», Тарьяна, транслитерацию. Одно объявление, ввезённое двумя модулями, — не столкновение. А то, что осталось своим, названо с именем цели («Состояние Rust», «Итог Rust», «Змейка Rust»), и столкнуться такому не с чем.

То есть признак «про ту же область — дороже» верен не про ОБЛАСТЬ, а про СПОСОБ: платит тот, кто пишет второй экземпляр общего, и не платит тот, кто его ввозит. Проверяется это тем же пересечением, только считать надо не все объявления модуля, а свои: у emit-rust.flang их 225 при 2573 строках, у emit-js.flang при 3896 строках своих 336 — и столкновений у него 139, потому что общее он объявляет заново.

Вариантов у Rust не столкнулось ни одного, и это тоже замер, а не удача: emit-rust.flang объявляет 20 типов, и все двадцать — ЗАПИСИ. Сумм ноль, значит и вариантов ноль, значит и второму месту, где связывание молчит, взяться неоткуда. Проверено пересечением множеств имён вариантов до сборки; сборка cc -std=c99 -Wall -Wextra -Werror -pedantic -O2 это подтвердила — ни одного redefinition.

Что стоило втаскивание, кроме переименований — замер, а не оценка. Замыкание: типов 345 → 365, функций 3919 → 4144. compiler_flang.c 11 420 741 → 12 207 828 байт (+787 087, +6,9 %), compiler_flang.h 2 335 182 → 2 475 991 (+140 809), собранный двоичный 7 941 104 → 8 450 656 (+509 552, +6,4 %). Сборка make -j8 CFLAGS='-std=c99 -Wall -Wextra -Werror -pedantic -O2' в чистом каталоге: 78,4 → 87,5 с (+11,6 %), ни одного предупреждения. flang check самого компилятора двоичным: 358,5 → 390,8 с (+9,0 %) — то есть цена проверки растёт примерно как размер замыкания, а не быстрее. Неподвижная точка сошлась: self-bootstrap.test.mjs — 37 из 37, «7 файлов совпали побайтово у свидетеля, flang₁ и flang₂».

Чем ограничено. Числа сняты на одном компиляторе. Доля столкновений (1–1,4 %) — не закон, а два наблюдения; полезен здесь признак («модуль про ту же предметную область стоит дороже»), а не процент.

Вывод, который стоит принять до следующего втаскивания: доля растёт с числом уже втащенных, а не держится на процентах. Каждый новый модуль встречается со всё более полным словарём, и «1 %» из первых двух замеров был свойством маленького замыкания, а не свойством языка.

И ловушка при разведении: новое имя тоже бывает занято. Суффикс «сервера» для типа «Итог» дал «Итог сервера» — имя, уже занятое ФУНКЦИЕЙ в том же файле. Связывание держит типы и функции в разных таблицах и об этом молчит, а генератор кода в C — нет. Проверять переименование надо тем же пересечением, что и исходные имена: не «нет ли такого типа», а «нет ли такого имени вообще».

Чем ограничено. Числа сняты на одном компиляторе. Доля столкновений (1 % → 1,4 % → 15 %) — три наблюдения, а не закон; полезен здесь признак («модуль про ту же предметную область стоит дороже, и дороже тем сильнее, чем больше уже втащено»), а не процент.

Пятый замер: печать в Java, 20 августа 2026. Модуль привозит 43 типа, 324 функции и 10 имён вариантов — 377 объявлений против 4536 уже втащенных (366 типов, 3966 функций, 204 имени вариантов). Столкнулось 7 (1,9 %), и все семь — с печатью в C, ни одного с остальными четырнадцатью слоями. Разведены суффиксом « Java»: типы «Пара имён», «Может быть узел», «Может быть имя» и варианты «Есть узел», «Нет узла», «Есть имя», «Нет имени».

Разгадка та же, что у факт-чекинга, только с обратным знаком. emit-java.flang называет ЧУЖИМ словарём почти всё: 40 типов из 43 уже несут суффикс « Java» («Состояние Java», «Функция Java», «Общее Java»), и столкнуться им не с чем. Столкнулись ровно те три, которые автор суффиксом не пометил, — и все три пришли из общего строения братьев-эталонов печати. То есть суффикс имени цели в самом модуле работает как прививка, и цена втаскивания у go (2 из 189), rust (2 из 234) и java (7 из 377) на порядок ниже, чем у js, python, csharp и elixir, где имена взяты у печати в C дословно.

Числа роста, снятые тем же заходом: compiler_flang.c 11 420 741 → 12 455 003 байта (+1 034 262, +9,1 %), compiler_flang.h 2 335 182 → 2 541 786 (+206 604), собранный двоичный 7 941 104 → 8 559 048 (+617 944, +7,8 %). Связанных функций 3919 → 4257, типов 345 → 389.

Сборка make -j8 CFLAGS="-std=c99 -Wall -Wextra -Werror -pedantic -O2" в чистом каталоге: 84,8 → 95,1 с процессорного времени (+12,2 %), оба замера сняты на одной и той же машине под одной и той же нагрузкой — это важно, потому что первый замер ствола при пустой машине дал 82,3 с, и разница между «до» и «после» там читалась бы как +17 %. Мерить обе стороны подряд, а не одну сегодня и другую вчера — иначе в число попадает нагрузка соседей, а не цена правки.

flang check самого компилятора двоичным — те же две стороны подряд, на одном дереве и под одной нагрузкой: 352,7 → 455,9 с процессорного времени (+29,3 %), 363,1 → 469,0 с по часам. Порог владельца «больше чем вдвое» не задет. Замечание в обоих прогонах одно и то же и НЕ от втаскивания: примеры компилятора не укладываются в 40 000 000 шагов (FLANG_RECURSION_LIMIT, до правки на «Сторожа в списке», после — на «Сторожа в узле»).

Неподвижная точка на дереве с втащенной печатью в Java сошлась: FTS_REQUIRE_TOOLCHAINS=c node --test flang/test/self-bootstrap.test.mjs — 37 проверок из 37, красных ноль, 27 мин 15 с под нагрузкой; «7 файлов совпали побайтово у свидетеля, flang₁ и flang₂». Аксиом по-прежнему ноль, точка раскрутки bootstrap/ совпадает с печатью текущих исходников (7 файлов, 15 700 163 байта).

Шестой замер: печать в Python, 20 августа 2026 — доля 79 %. Втащен flang/self/emit-python.flang: 381 своё объявление против 4331 уже втащенного, столкнулось 302 — 262 функции и 40 типов. Диагностику даёт связывание, число сошлось с независимым замером по исходникам до единицы.

Что втащилиОбъявлений у негоУже былоСтолкнулось
вычислитель29823603 (1 %)
факт-чекинг148379821 (1,4 %)
печать в Rust23442642 (0,9 %)
печать в Python3814331302 (79 %)

Признак «модуль про ту же предметную область стоит дороже» подтвердился и заострился: эталоны печати — братья одного строения, и 92 % столкновений пришлись на один модуль, emit-c.flang. Разброс между целями объясняется не размером, а тем, РАЗВЁЛ ЛИ АВТОР ИМЕНА ЗАРАНЕЕ: emit-go.flang и emit-rust.flang называют свои части с именем цели («Модуль Go», «Змейка Rust») и стоили 1 и 2 столкновений; emit-python.flang зовёт их так же, как emit-c.flang («Печать функции», «Общее», «Настройки»), и столкнулся сплошь.

Столкновения по вариантам РОЖДАЮТСЯ ПЕРЕИМЕНОВАНИЕМ, а не находятся до него. Здесь выше сказано «проверять пересечением по варианту». Верно, но неполно: у Python пересечение множеств вариантов до правки было ПУСТО в том смысле, что одноимённые варианты сидели в одноимённых типах, то есть в одном объявлении с точки зрения будущего связывания. Восемь столкновений («Есть узел»/«Нет узла», «Есть имя»/«Нет имени», «Есть функция»/«Нет функции», «Есть число»/«Нет числа») появились бы ровно от развода ТИПОВ: два разных типа с вариантами одного имени дают одну функцию C. Искать их надо не «до всякой правки», а среди вариантов тех сумм, чьи имена попали в список разводимых типов.

Дешёвая половина этой цены необязательна, и это измерено. Из 381 объявления emit-python.flang 286 имён есть и в emit-c.flang, и у 182 из них тело совпадает С ТОЧНОСТЬЮ ДО СИМВОЛА — это копии, а не однофамильцы. Их можно не разводить, а ВВЕЗТИ через использует … только, как уже делают emit-go.flang и emit-rust.flang. Тогда переименований осталось бы около 130 вместо 310, и две копии одной функции перестали бы расходиться при первой же правке. Здесь выбран развод: карта переименований на все семь целей (docs/eight-targets-renames.tsv) назначает Python именно его, и сводить семь веток проще по одному правилу, чем по двум. Дедупликация — отдельная работа по одному разу на всех, а не часть втаскивания.

Правило имён, сошедшееся у трёх работ независимо. Приписать латинское имя цели: «Печать программы» → «Печать программы Python». Если имя уже кончается меткой своей цели — заменить, а не удваивать («Строка Python», а не «Строка Python Python»). Проверено прогоном: из 1042 предложенных идентификаторов ноль занято базой и ноль совпало между целями.

Цена печати в Python числами, снятая прогоном 20 августа 2026. Замыкание двоичного 3919 → 4269 функций и 345 → 395 типов. compiler_flang.c 11 420 741 → 12 494 563 байт (+1 073 822, +9,4 %), compiler_flang.h 2 335 182 → 2 552 855 (+217 673), собранный двоичный 7 941 104 → 8 565 152 (+624 048, +7,9 %). flang check компилятора 64,2 с против 73,3—84,2 с, то есть примерно +15…30 % — разброс здесь от нагрузки машины, а не от правки. Сборку make мерить в этот день было нечем: на машине шло восемь таких же работ разом, и средняя нагрузка гуляла от 37 до 255; пара «до/после», снятая подряд под нагрузкой 250, дала 134,3 против 126,8 с, то есть «после» БЫСТРЕЕ «до», чего быть не может. Числа сборки из такого дня в базу знаний не годятся, и это тоже факт: замер времени на общей машине — не замер.

Связано: five-binary-commands-and-the-price-of-each, measuring-a-stale-artifact, the-byte-check-subtracted-the-entry-boundary-and-went-green-for-nothing

Связано: five-binary-commands-and-the-price-of-each, measuring-a-stale-artifact, a-file-name-is-lost-between-the-parser-and-the-failure