Минус ноль всплыл четыре раза — это класс, а не отдельные ошибки
Четыре независимых места, найденные разными работами. Первые три — за одни сутки 15 августа 2026:
- Разбор отрицательных чисел ломался:
−3 остаток от 1даёт−0, а сравнение — этоObject.is. Обычные примеры («42», «12.5») не показывали, поймала строка"-3". - «Чётное» от любого отрицательного даёт «нет» — той же ошибкой сломаны «Делится на» и её копия.
- «Уникальные» оставляет два нуля —
0и−0считаются разными.
Корень один: в IEEE-754 −0 и +0 равны по сравнению, но различимы по Object.is, и код, написанный через строгое равенство, ведёт себя не так, как ожидает автор.
Почему это класс. Три раза за сутки, в трёх не связанных между собой местах, разными людьми. Значит ловить надо не по одному, а правилом: любая функция, сравнивающая числа на равенство или берущая остаток, должна быть проверена на −0 до того, как попадёт в библиотеку.
Четвёртый случай, 16 августа 2026 — и он не в функции языка, а в оснастке. Ключом таблицы замороженных ответов свидетеля стоял JSON.stringify входа. Он превращает -0 в "0", значит входы {сумма: -0} и {сумма: 0} получили ОДИН ключ: вторая запись затёрла первую, и проверка «ноль не равен минус нулю» покраснела на подменённом ответе. Правило, записанное здесь, шире, чем было сказано: на −0 проверяется не только функция, сравнивающая числа, но и любой код, который делает из значения ключ, имя или подпись. JSON.stringify в этой роли лжёт трижды — -0 становится 0, NaN и обе бесконечности становятся null. Подробности — a-frozen-reference-changes-the-check.
Практическое следствие для замеров. Все три нашлись тестами, а не доказательствами — и именно они составляют большую часть четырёх настоящих ошибок из proof-cost-0-of-20. То есть тесты сегодня ловят класс, который доказательства не ловят вовсе, потому что до этих функций ядро не достаёт.
Проверено ещё раз 16 августа: все три живы, ни одна не починена. И к ним добавилось четвёртое следствие, которого раньше не было: ядро теперь доказывает «чётность есть делимость на два» обо ВСЕХ входах — потому что «Чётное» и «Делится на» ошибаются на −0 одинаково. То есть класс −0 умеет прятаться не только от примеров автора, но и от доказательства, если спецификация написана через вторую функцию с тем же корнем. Подробности в proven-is-not-correct.
Связано: nan-is-reachable, proof-cost-0-of-20, double-has-no-laws, the-core-accepts-falsehood-a-class, proven-is-not-correct, a-frozen-reference-changes-the-check
Поправка 18 августа 2026: корень не в IEEE-754, а в ДВУХ равенствах
Здесь стояло «корень один: в IEEE-754 −0 и +0 равны по сравнению, но различимы по Object.is». Это верно, но называет не ту половину, и из-за этого класс выглядит неизбежной платой за плавающую точку. Он ею не является.
Замер: равен языка — это Object.is, а === JavaScript — нет, и расходятся они ровно на минус нуле.
-0 === 0 в JavaScript → true
«-0 равен 0» во flang → нет
(0 делить на 0) равен (0 делить на 0) → да (то есть NaN равен себе)
Отсюда следствие, которое и объясняет, почему класс всплывает снова и снова: одна и та же наивная запись работает на JavaScript и врёт на flang. x % 2 === 0 в JavaScript верно для −4, потому что −4 % 2 даёт −0, а −0 === 0 истинно. Дословный перевод (н остаток от 2) равен 0 во flang ложен, потому что равен минус ноль от нуля отличает. Автор переносит привычную строку, она проходит проверку типов, доказывается тотальной — и врёт.
Верная запись сравнивает не с нулём, а с нулём ЛЮБОГО знака:
пусть «остаток» равно («делимое» остаток от «делитель»)
(«остаток» равен 0) или («Это минус ноль» от «остаток»)
Чем подтверждено. Прогоном flang/test/self-builtins.test.mjs на сетке из 15 чисел (ноль, минус ноль, ±2⁵³, «не число», обе бесконечности), 225 пар: минус ноль вышел из остатка 33 раза. Сама сверка сперва покраснела именно на этом — в ней стояло ожидание остаток === 0, то есть равенство JavaScript, там, где считается Object.is. Ветка work/vstroennye, коммит «Форма диагностики, границы чисел и кодовые точки».
Пятый и шестой случаи: минус ноль приезжает из ТЕКСТА, а печать его теряет
Оба измерены тем же прогоном, и оба — не арифметика.
Пятый. к числу "-0" даёт именно МИНУС ноль (Object.is с −0 истинно). То есть минус ноль попадает в программу не только из вычисления, но и из разбора строки — из данных пользователя, где его никто не ждёт и где «проверю входные числа» его не ловит: строка выглядит безобидно.
Шестой. к строке на числах НЕ взаимно однозначна: к строке -0 и к строке 0 дают одну строку «0». Значит круг «к строке, потом к числу» теряет знак нуля, и значение, равное себе до круга, после круга себе не равно. Это тот же корень, что у четвёртого случая (ключ таблицы через JSON.stringify), но теперь он назван на поверхности языка, а не в оснастке: всякий раз, когда число уезжает в строку и возвращается, оно может вернуться другим значением.
Правило класса от этого расширяется ещё раз: на −0 проверяется не только функция, сравнивающая числа или берущая остаток, и не только код, делающий из значения ключ, но и любой круг «число → строка → число».
Седьмой случай, и с ним — дешёвое лекарство: х плюс 0
19 августа 2026, работа над байтами. остаток от на отрицательном входе даёт МИНУС ноль там, где делится нацело, и дальше он расползается: «Свести к байту» и «Свести к слову» в flang/stdlib/sha256.flang обязаны отдавать настоящий ноль, иначе постусловие «результат не меньше 0» держится, а сравнение на равенство с нулём — уже нет (равен в языке это Object.is).
Лекарство — одно действие: х плюс 0 превращает минус ноль в ноль и не трогает ничего другого. В IEEE-754 при округлении к ближайшему (−0) + (+0) = +0, а для любого другого конечного х прибавление нуля — тождество. Проверено прогоном: Object.is(-0 + 0, 0) истинно.
Правило класса от этого дополняется практикой: там, где значение уходит в сравнение на равенство или в ключ, сведение к каноническому нулю стоит одного плюс 0, и это дешевле, чем ветка если х равен 0 то 0 иначе х — та на минус нуле как раз и не сработает, потому что −0 равен 0 ложно.
Пятый случай, 20 августа 2026: високосность до нашей эры
«Високосный год» из flang/stdlib/datetime.flang отвечает «нет» на годах −4, −100 и −400, хотя −4 и −400 високосные. Причина ровно та же: −4 остаток от 4 даёт минус ноль, а равен 0 на нём ложно. Лечится тем же плюс 0.
«Безопасное деление» от 10 и (0 умножить на (0 минус 1))
→ вариант «Успех» с значение равным −Infinity
То есть функция отдаёт «успех» с бесконечностью — ровно то значение, от которого она защищает. Лекарство то же, что в седьмом случае: `(делитель плюс
- равен 0`.
Следствие для утверждений. Сильное постусловие «у успеха частное конечно» написать НЕЛЬЗЯ — оно на −0 ложно. Правдой остаётся только слабое «успех ровно при ненулевом делителе», где «ненулевой» понимается тем же Object.is, что и в теле. То есть спецификация повторяет ошибку тела — тот же приём, каким класс спрятался в паре «Чётное» / «Делится на».
Чем подтверждено. Ветка vypusk/utv-derevya на основании github/main df055a6b, прогон bootstrap/flang test на пробе из трёх строк.
Девятый заход, 20 августа 2026: противоречие названо целиком — порядок говорит «равны», равенство говорит «нет»
Предыдущие восемь случаев называли ПОСЛЕДСТВИЯ. Замер на настоящем пути показывает причину одной картинкой (двоичный bootstrap/flang 0.5.1, вход --args '{"х":-0}', файл из четырёх однострочных функций):
−0 равен 0 → нет
−0 не меньше 0 → да
−0 не больше 0 → да
к строке −0 → "0"
Порядок языка говорит «равны», равенство языка говорит «не равны», печать показывает одно и то же. Значит это не восемь ошибок в восьми местах, а одно противоречие внутри типа число, и лекарство х плюс 0 из седьмого случая — припарка на каждое место, а не лечение.
Ход, закрывающий его целиком, ровно один и лежит в одном месте на цель: равенство чисел обязано согласоваться с порядком, который у языка УЖЕ есть, — а равен б значит не (а меньше б) и не (б меньше а). В рантаймах это одна строка: Object.is(a, b) в JavaScript и fl_same_number в C дополняются случаем нуля. Арифметику это не трогает вовсе, а NaN равен NaN (тоже Object.is) остаётся отдельным решением и этой правкой не задевается.
Почему это НЕ тот же ход, что у строк. У строки было два значения, которые представление РАЗЛИЧИТЬ НЕ МОЖЕТ, и лечится это отказом там, где неразличимое возникает (dve-mery-stroki-delyat-vstroennye-formy). У числа — два значения, которые представление различает, а смысл не различает, и лечится это согласованием сравнения. Одним ходом обе болезни не закрываются; общее у них правило, а не правка: у типа не должно быть двух значений, которые его собственные действия различают несогласованно.
Ход здесь НЕ сделан: он меняет ответы программ дерева и требует своей сверки до и после. Смешивать его с правкой строковых мер значило бы сделать непроверяемыми обе. Важно не это, а то, ЧЕМ поймано. Все прежние четыре случая нашли тесты. Этот нашло постусловие — «високосный год тот, в котором 366 дней», прогнанное по сетке годов. То есть класс −0 ловится доказательным слоем ровно тогда, когда утверждение написано ЧЕРЕЗ ДРУГОЙ СЧЁТ, а не через ту же арифметику: прежнее постусловие этой функции дословно повторяло её тело и, разумеется, молчало. Подробности — a-correction-written-for-truncation-fires-twice-under-floor-division.