Снятая проверка типа даёт не отказ, а неверный ответ — и это другой класс риска
Обычная снятая проверка портит диагностику: программа падает позже и хуже объясняет причину. Снятая проверка тега в напечатанном C не портит диагностику — она отменяет её. Значение flang в C это тег плюс объединение; .as.number на значении со строковым тегом читает соседнее поле объединения и возвращает ЧИСЛО. Никакого падения, никакого кода отказа — просто другой ответ.
Это не рассуждение, а найденная ошибка. Первая версия правки скорости («не печатать проверку там, где тип доказан») снимала проверку тега совсем, опираясь на дверь входа: значения снаружи сверяются с объявленными типами до вызова (fl_check_entry). Прогон корпуса через напечатанный C показал ровно две расходящиеся точки из 9139:
| точка | было | стало без проверки |
|---|---|---|
«Отобразить» от Удвоить и [1, "два"] | FLANG_TYPE: «add» допустима только для чисел | [2, 1.32e-309] |
«Отфильтровать» от Чётное и [1, "два"] | FLANG_TYPE: «mod» допустима только для чисел | [] |
1.32e-309 — это биты указателя на строку, прочитанные как double.
Почему дверь входа не помогла, хотя она есть. Она сверяет НЕ ВСЁ: вид неизвестно — параметр полиморфизма и значение-функцию — она пропускает, потому что сверять его не с чем. «Отобразить» от «А» и «Б» объявлена принимает ф: функция из «А» в «Б», элементы: список «А», и список [1, "два"] дверь пропускает законно. А внутри «Удвоить» параметр объявлен число, и проверка типов честно доказывает, что х плюс х — арифметика над числами. Оба рассуждения верны по отдельности; ложна связка между ними.
Как починено. Снят не тег, а МЕЖМОДУЛЬНЫЙ ВЫЗОВ. Сверка тега печатается одним if в теле вызывающего, а арифметика считается выражением C:
if (n.tag != FL_NUMBER) FL_TRY(fl_not_numbers(ctx, "div", n, fl_number(2.0), error));
const fl_value fl_t11 = fl_number(n.as.number / 2.0);
Выигрыш, как оказалось, был почти весь не в самой проверке, а в том, что вокруг неё: вызов в другую единицу трансляции, два значения по 32 байта туда, результат через указатель обратно и ветвь отказа посреди горячего кода. Цена честности измерена: коллатц 132,5 мс вместо 121,6 без сверки — 9 % на самой арифметической задаче и 2 % по геометрическому среднему.
Правило, которое из этого следует. Всякая оптимизация, снимающая проверку в напечатанном коде, обязана СНАЧАЛА назвать место, где значение чужого типа перехватывается, и проверить это прогоном. «Типы доказаны на исходнике» — не ответ: доказательство верно при условии, которое кто-то должен обеспечить, и дверь входа обеспечивает его не полностью.
Чем подтверждено. flang/test/proven-number.test.mjs — 15 проверок, из них три прогоном собранного C, включая ту самую точку с параметром полиморфизма. Ответы корпуса через напечатанный C: 95 программ, 9139 точек сетки, побайтово те же, что до всей работы (один и тот же md5). Ветка work/skorost.
Чем ограничено. Проверка осталась только там, где тег неизвестен при печати. У числового литерала и у уже посчитанного выражения тег известен, и сверка им не печатается — это не послабление, а факт про напечатанный код.
Связано: type-inference-answers-with-a-node-mark, byte-for-byte-comparison, a-removal-must-turn-a-test-red, checks-that-stopped-comparing