Известные ограничения
Список ниже — то, что пока не работает или работает не полностью, на момент написания этого документа:
- ⚠️
параллельно детерминировано { ... }(детерминированный планировщик,
"эстафета", см. раздел Детерминированность
выше) — честные v1-ограничения: нет НАСТОЯЩЕЙ параллельности между
детерминировано-потоками одной партии (строго по очереди, один
физически работает в каждый момент); точки передачи хода — только
начало итерации пока, вывести(...), канал.отправить/получить —
для каждого НЕ является точкой передачи; статический подсчёт размера
партии (нужен для барьера регистрации, см. там же) работает только
для параллельно детерминировано-операторов, лежащих НЕПОСРЕДСТВЕННО
друг за другом в одном блоке (не внутри если/цикла) — иначе
действует честный, менее надёжный запасной вариант без барьера;
максимум 256 потоков на партию; главный поток в эстафете не участвует.
- ✅
результат из Список из Tирезультат из Словарь из К и З— раньше цепочечный
вызов метода прямо на поле .значение результата (р.значение.метод(...))
падал с ошибкой кодогенерации ("не является структурой — у неё нет
полей"), а объявление такой переменной через вывод типа падало с
ошибкой "нельзя объявить через вывод типа". Обе проблемы исправлены:
ДополнительнаяИнформация::Результат теперь хранит LLVM-тип элемента/
ключа/значения вложенного контейнера, что даёт кодогенератору
отдельный путь диспетчеризации для р.значение.метод(...), а для
объявлений через вывод типа добавлен отдельный, узко ограниченный путь,
не затрагивающий исходный принципиальный запрет на вывод типа для
голого Список из T/Словарь из К и З. Подтверждено тестами, в т.ч. со
структурными элементами (результат из Список из Точка) и мутирующими
методами (.добавить). Массив из Список из T отдельно этим исправлением
не тестировался.
- ✅ "для каждого" по коллекции, выраженной как цепочка доступа к полю
(для каждого х в к.элементы { ... } — поле структуры-Список, или
для каждого х в р.значение { ... } — .значение результата
результат из Список из T) — раньше падало с ошибкой "индексация
поддержана пока только для простых переменных", потому что внутренняя
кодогенерация цикла принимала коллекцию только как простую переменную-
идентификатор. Исправлено: добавлен метод, распознающий оба случая
(поле структуры — по уже существующей карте списочные_поля_структур,
и .значение результата — по уже существующей `ДополнительнаяИнформация::
Результат из исправления результат<Список<T>>` выше) — та же логика,
что уже применяется для объект.поле.метод(...), просто переиспользована
для получения указателя на список вместо диспетчеризации вызова.
Подтверждено тестами, в т.ч. с элементами-структурами. Обходной путь
(промежуточная переменная) по-прежнему работает, но больше не нужен.
- ✅ Индексация (
[...], включая "для каждого") настоящего фиксированного
массива (целое[N]), когда он — поле СТРУКТУРЫ, а не простая локальная
переменная (д.числа[0], для каждого х в д.числа) — раньше падала с
ошибкой "индексация поддержана пока только для простых переменных",
как для ЧТЕНИЯ, так и для ЗАПИСИ (д.числа[1] = 99;). Исправлено:
добавлена карта массивные_поля_структур (точное зеркало уже
существующей списочные_поля_структур, только для РешённыйТип::Массив,
без промежуточного build_load — фиксированный массив, в отличие от
Список из T, встроен прямо в память структуры, а не лежит за отдельным
heap-указателем), и оба соответствующих внутренних пути (`получить_
массив_из_выражения для чтения, получить_массив_из_места` для
записи) расширены случаем "поле структуры" — тем же паттерном
build_struct_gep, что уже использовался для Список/Словарь-полей.
Заодно, при тестировании этого исправления, найден и устранён смежный
ПАРСЕРНЫЙ пробел: объявление ЛОКАЛЬНОЙ переменной — фиксированного
массива ПОЛЬЗОВАТЕЛЬСКОГО (структурного) типа (Точка[2] точки = ...;)
— падало с ошибкой парсера, хотя то же самое объявление внутри ПОЛЯ
структуры уже работало (разобрать_тип уже умел строить Тип::Массив
для пользовательских типов) — диспетчер операторов просто никогда не
пробовал этот путь для "Идентификатор [ ... ]" на старте оператора,
путая его с индексацией переменной. Добавлена симметричная проверка
(похоже_на_объявление_массива_пользовательского_типа, по образцу уже
существующей проверки для generic-объявлений) с тем же приёмом отката
позиции парсера. Подтверждено тестами: чтение и запись поля-массива
(примитивные элементы), "для каждого" по полю-массиву, и объявление
локального массива пользовательского типа.
- ✅ Фиксированные массивы ЭЛЕМЕНТОВ-СТРУКТУР (
Точка[2]) — раньше не
работали вообще ни в каком контексте (не через поле структуры, не как
локальная переменная): ни индексация (точки[0].х), ни "для каждого"
по такому массиву не распознавали элемент как структуру — индексация
падала с "доступ к полю поддержан пока только для простых переменных",
а переменная цикла "для каждого" регистрировалась без метаданных о
структуре (ДополнительнаяИнформация::Нет безусловно, для любого типа
элемента) — обращение к полю переменной цикла падало с "не является
структурой — у неё нет полей". Исправлено: `ДополнительнаяИнформация::
Массив теперь хранит имя_структуры_элемента` (тот же принцип, что
уже был у Список.имя_структуры_элемента), вычисляемое на всех трёх
местах, где эта структура создаётся; получить_массив_из_выражения/
...из_места и массивные_поля_структур прокидывают его дальше;
точки[0].х работает через новую ветку Выражение::ДоступПоИндексу в
получить_структуру_из_выражения; "для каждого" регистрирует
переменную цикла как ДополнительнаяИнформация::Структура, когда
элемент — структура. ⚠️ **Тот же класс use-after-free, что уже был
найден и исправлен для Список из Структура.получить()**, был здесь
СРАЗУ учтён (не как отдельная находка, а как часть этого же
исправления, по аналогии): и при чтении по индексу
(сгенерировать_доступ_по_индексу), и в переменной цикла "для
каждого" элемент-структура теперь retain-ится (тот же принцип
"утечка предпочтительнее краха", что и у Список — retain без
соответствующего release, как и у Список.получить()). Подтверждено
тестами и valgrind (0 ошибок, "definitely lost: 0 bytes"). Осталось
честно НЕ исправлено — уже существовавшее, не новое ограничение:
запись в поле элемента массива через индекс (точки[0].х = 99;) по-прежнему
не поддержана (то же явное сообщение об ошибке, что было и раньше).
UPD: это ограничение снято — см. следующий пункт.
- ✅ Запись в поле элемента массива структур через индекс
(точки[0].х = 99;) — предыдущее ограничение выше. Исправлено:
получить_структуру_из_места (сторона ЗАПИСИ) получила новую ветку
МестоЗаписи::Индекс { .. } — точное зеркало уже работавшей на ЧТЕНИЕ
ветки Выражение::ДоступПоИндексу в получить_структуру_из_выражения,
только через получить_массив_из_места вместо ..._из_выражения.
Единственное содержательное отличие от чтения: retain (`self.
захватить(...)`) здесь не нужен — полученный указатель на структуру
используется сразу же, чтобы записать в его поле (build_struct_gep),
а не сохраняется как новая владеющая ссылка. Работает в цикле
(точки[i].х = точки[i].х + 1000;), любой индекс, любая глубина
вложенности массива (через уже существующую рекурсию `получить_
массив_из_места). Подтверждено тестами и valgrind`
("ERROR SUMMARY: 0 errors" — есть ожидаемая утечка в 96 байт при
выходе из главная(), та же, что и раньше для массивов структур:
локальный Точка[N] не освобождается при выходе из области видимости
— известное, отдельное, не новое ограничение ARC для фиксированных
массивов структур, не относящееся к записи через индекс как таковой).
- ✅ Раньше текст под ARC не покрывал результат, переданный напрямую в
вызов функции без промежуточной переменной (вывести(целое_в_текст(42));)
— теперь исправлено широко: вывести(...), обычные и квалифицированные
(модуль.функция(...)) вызовы функций, вызовы МЕТОДОВ (в т.ч.
динамические вызовы через контракт), и длина, содержит, найти,
подстрока. ✅ Отдельно устранено для Список из текстов.добавить(...):
раньше список для текстовых элементов хранил СЫРОЙ указатель как есть
— если аргумент был заимствованным (литерал, существующая переменная),
а не свежим буфером, Список из текстов.освободить() не мог его
освободить безопасно (не отличить литерал от malloc — краш или
двойное освобождение). Решение: добавить(...) теперь ВСЕГДА
сохраняет самостоятельно владеемую копию — strdup() заимствованного
значения (когда оно не является гарантированно свежим буфером) или
прямая передача владения (когда является) — тот же принцип, что уже
применяется для структурных элементов (захватить/retain), просто
ролью "увеличить счётчик" здесь играет "сделать независимую копию" (у
текста нет счётчика ссылок). После этого Список из текстов.освободить()
безопасно освобождает каждый элемент.
- ✅ То же самое перенесено на
Словарь из Ключ и Значение.положить(...): ключ и/или
значение типа текст теперь тоже всегда независимая, самостоятельно
владеемая копия (strdup() заимствованного значения). Ключ копируется
НЕ сразу, а непосредственно перед сохранением НОВОГО ключа (после
поиска существующего) — иначе при ОБНОВЛЕНИИ уже существующего ключа
(значение переписывается, ключ — нет) свежая копия ключа немедленно
терялась бы, утечка на каждое такое обновление; честно найдено и
устранено ДО поставки, при проектировании этого исправления. Заодно
устранена ОТДЕЛЬНАЯ, независимая от текста-под-ARC утечка: раньше
ОБНОВЛЕНИЕ значения по уже существующему ключу просто затирало старое
значение новым, без освобождения — при текстовом значении это была
тихая утечка на каждый повторный положить(...) с тем же ключом
(например, в цикле) — теперь старое текстовое значение освобождается
перед перезаписью. После этих изменений Словарь из Ключ и Значение.освободить()
безопасно освобождает каждый текстовый ключ/значение (раньше это было
сознательно не сделано — Словарь не мог отличить свежий буфер от
литерала).
- ✅ Исправлен настоящий use-after-free между потоками (не просто
утечка, как для Список/Словарь выше — это оказалось СЕРЬЁЗНЕЕ):
канал из текста.отправить(значение) копировал только 8 байт битового
представления указателя, без какого-либо копирования содержимого. Это
означало, что отправленный текст физически оставался ТЕМ ЖЕ буфером,
на который продолжает ссылаться локальная переменная отправляющего
потока (текст сообщение = "привет" + " из потока"; канал.отправить(сообщение);)
— а эта переменная освобождается как обычно при выходе из области
видимости потока, СОВЕРШЕННО НЕ ЗНАЯ, что её значение было отправлено
куда-то ещё. Получатель в другом потоке читает уже освобождённую
память — гонка данных, а не детерминированная утечка. Найдено сначала
РАССУЖДЕНИЕМ (до всякого запуска — сам факт отсутствия копирования при
пересечении границы потоков это гарантирует), затем ЭМПИРИЧЕСКИ
подтверждено специально написанным тестом
(тест_канал_текст.код) — под valgrind воспроизводились многократные
"Invalid read" в printf/strlen главного потока на памяти, которую
free()-нул именно поток-отправитель. Исправлено: отправить(...)
теперь ВСЕГДА (безусловно, не только для "заимствованных" значений)
делает независимую копию (strdup()) перед записью в канал — в
отличие от Список/Словарь, где "свежее" (заведомо только что
порождённое) значение можно передать во владение без копии, для
канала это небезопасно ДАЖЕ для свежего значения, потому что оно всё
равно может быть привязано к переменной, живущей в исходном потоке.
Для этого у ДополнительнаяИнформация::Канал появился статический
признак "элемент — именно текст" (аналог ключ/ключ_типа у
Список/Словарь), которого раньше не было — opaque-указатели LLVM не
позволяют отличить текстовый указатель от любого другого на уровне
самого LLVM-типа. Проверено под valgrind — краш исчез (10 повторных
запусков подряд, ноль "Invalid read"/"Invalid free"). ⚠️ Честно
остающиеся, уже НЕ краши, а обычные утечки (сознательно принятый
компромисс "утечка лучше краха", тот же принцип, что и у
Список из T.получить()/Словарь из Ключ и Значение.получить(), результат которых
тоже не отслеживается как владеющее значение):
- ✅ **Оба честно отложенных пункта из предыдущей версии этого списка
(см. историю) теперь тоже устранены**:
- Текст, полученный через канал из текста.получить(), теперь
распознаётся как владеющее значение: раз отправить(...) (см.
выше) ВСЕГДА кладёт в канал независимую strdup-копию, получатель
— единственный, кто на неё ссылается, и `является_текст_
порождающим теперь признаёт канал<текст>.получить()`
текст-порождающим вызовом — той же веткой, что уже применяется к
функциям и методам, — освобождается автоматически при сохранении
в переменную и выходе из области видимости, как и любой другой
свежий текст.
- У канал из T появился метод .освободить() — раньше не
существовало вообще никакого способа освободить память самого
объекта канала (создать_канал(), ~272 байта — мьютекс +
условная переменная + флаг + слот). Метод вызывает
pthread_mutex_destroy/pthread_cond_destroy (обратная операция
_init, освобождает не только память, но и ОС-ресурсы мьютекса/
условной переменной) перед free() самого буфера — та же
"парность", что у pthread_create/pthread_join.
- ⚠️ Честно осталось: если в канале лежит неполученный текст на
момент .освободить() (отправлено, но ни разу не вызвано
получить()), эта конкретная копия текста всё равно утечёт —
контракт "за временем жизни следит программист", тот же, что и у
Список/Словарь
- ✅ Отдельно, скорее всего самый часто задеваемый случай из всех:
результат ПОЛЬЗОВАТЕЛЬСКОЙ функции, возвращающей текст
(функция сгенерировать(...): текст { ... }), раньше вообще не
распознавался как владеющее значение — утекал и при `текст с =
сгенерировать(x);, и при прямой передаче (вывести(сгенерировать(x));`).
Теперь распознаётся и освобождается корректно, включая переприсваивание
внутри цикла (с = сгенерировать(i); на каждой итерации), а с недавних
пор — и для МЕТОДОВ структур, возвращающих текст (объект.метод()),
когда статический тип объект известен как конкретная структура.
⚠️ Важная честная поправка: ПЕРВАЯ версия этого распознавания (и
для функций, и для методов) полагалась только на ОБЪЯВЛЕННЫЙ тип
возврата (-> текст) — этого оказалось НЕДОСТАТОЧНО и приводило к
настоящему краху (free(): invalid pointer / "double free or
corruption"): функция или метод вида
функция сказать(): текст { вернуть "Мяу"; } возвращает СТРОКОВЫЙ
ЛИТЕРАЛ (статическая константа в .rodata, никогда не malloc'нутая),
но кодогенератор, поверив декларации типа, пытался освободить (free)
этот указатель при каждом использовании результата — разрушая
аллокатор. Обнаружено воспроизведённым крахом на одном из регрессионных
тестов (структура с методом, буквально возвращающим текстовый литерал).
Исправлено переходом на анализ ТЕЛА функции/метода: теперь функция или
метод считаются "гарантированно возвращающими новый буфер" только если
КАЖДЫЙ достижимый вернуть <выражение>; в её теле сам является
заведомо текст-порождающим выражением в узком смысле (конкатенация
+ или вызов одной из конкретных built-in функций, которые сами
гарантированно делают malloc) — декларации типа возврата теперь
недостаточно. Это осторожный анализ: он может НЕ распознать функцию,
которая на самом деле всегда возвращает новый буфер, но делает это
через вызов ДРУГОЙ пользовательской функции (`вернуть
другая_функция();`) — такой случай честно не покрывается (упущенная
утечка предпочтительнее краха). ✅ **Устранено и это, последнее в этой
категории**: метод, возвращающий текст, вызванный через ДИНАМИЧЕСКУЮ
диспетчеризацию контракта (объект.метод(), где объект статически
имеет тип контракта, а не конкретной структуры), теперь тоже
распознаётся как текст-порождающий и освобождается. Кодогенератор
по-прежнему не знает во время компиляции, КАКАЯ именно структура
выполнит вызов в рантайме — но это и не требуется: список ВСЕХ
структур, реализующих данный контракт, известен статически (все их
vtable уже сгенерированы к этому моменту компиляции). Если КАЖДАЯ из
них уже признана "гарантированно возвращающей новый буфер" для этого
метода (по анализу тела — см. выше), то и результат динамического
вызова гарантированно свежий буфер, НЕЗАВИСИМО от того, какая
конкретно реализация исполнится — освобождать безопасно. Если хотя бы
ОДНА реализация не проходит проверку (например, у неё метод
возвращает литерал), вызов честно НЕ считается текст-порождающим, как
и раньше (утечка вместо риска краха). Проверено: `тест_контракт_
список_освободить.код (уже существующий тест с .напечатать()` через
контракт) — утечка результата этого вызова (33 байта, 3 блока)
полностью исчезла; тест_контракт_возврат.код — та же утечка (11
байт × 3) тоже исчезла. Полная регрессия под valgrind и привет.код
— без единого нового падения или утечки (побайтово идентично, не
считая PID в заголовках valgrind)
- ✅
Список из Контрактраньше содержал настоящий use-after-free (не
просто утечку) — исправлено (см. подробности в разделе про контракты
выше). ✅ Устранена и честная утечка, появившаяся взамен: элементы
Список из Контракт теперь не только корректно захватываются
(захватить), но и корректно освобождаются — как через .удалить(...)
(удаляемый элемент), так и через .освободить() (все оставшиеся
элементы перед освобождением самого буфера). Решение — тот же принцип
рантайм-сравнения vtable, что уже применялся для освобождения
локальных переменных контрактного типа при выходе из области
видимости (см. чуть ниже): конкретная структура за толстым указателем
элемента неизвестна на этапе компиляции, поэтому она определяется в
рантайме сравнением адреса vtable элемента с адресами всех известных
vtable этого контракта. Код освобождения вынесен в отдельный
переиспользуемый метод (принимающий уже загруженное значение толстого
указателя, а не адрес ячейки — списку не нужна отдельная alloca для
каждого элемента). Проверено под valgrind (`тест_контракт_список_
освободить.код, с явным .удалить(...) и .освободить()`) — утечка
самих объектов-элементов исчезла полностью; на момент этого
исправления оставалась ещё одна, ОТДЕЛЬНАЯ утечка (33 байта, 3 блока)
— результат напечатать() (текст, возвращённый динамическим вызовом
через контракт), не имеющая отношения к самим элементам списка; она
описана и устранена отдельно чуть выше. Полная регрессия (весь набор
тестов сессии, под valgrind) и привет.код — без единого нового
падения или утечки (побайтово
идентичный вывод и профиль утечек, привет.код не использует
Список из Контракт, поэтому подтверждает только отсутствие постороннего
регресса)
- ✅ Контракт как тип возврата функции — проверено, оказался УЖЕ
безопасным (задокументированная ранее "утечка" для этого конкретного случая была неточной — реальной проблемы не было, см. раздел про контракты выше)
- ✅ Исправлена утечка локальных переменных КОНТРАКТНОГО типа
(например Печатаемый а = создать_питомца(1);) — раньше такая
переменная, хранящая структуру, обёрнутую в "толстый указатель"
{данные, vtable}, никогда не освобождалась (ARC release) при выходе
из области видимости, в отличие от локальной переменной КОНКРЕТНОГО
структурного типа, которая освобождается корректно. Причина: механизм
ARC-кадров регистрировал для освобождения только `РешённыйТип::
Структура, не РешённыйТип::Контракт`. Найдено при регрессионной
проверке под valgrind (тест_контракт_возврат.код) во время работы
над предыдущим пунктом этого списка. Исправлено отдельным,
параллельным стеком ARC-кадров для контрактных переменных: поскольку
КОНКРЕТНАЯ структура за конкретным значением неизвестна во время
компиляции (в этом и смысл динамической диспетчеризации), она
определяется в рантайме — сравнением адреса vtable из толстого
указателя с адресами ВСЕХ известных vtable этого контракта (список
кандидатов известен статически). ⚠️ Попутно, при добавлении этого
освобождения, был обнаружен и исправлен ДОПОЛНИТЕЛЬНЫЙ риск двойного
освобождения: копирование существующей переменной в контрактную
(Печатаемый а = кот;, где кот — уже существующая переменная-
структура, или Печатаемый б = а;, копирование одной контрактной
переменной в другую) заворачивало/копировало ТОТ ЖЕ указатель без
увеличения счётчика ссылок — раньше это было лишь “тихой” утечкой
(лишний невладеющий “хвост”), но после добавления освобождения стало
бы настоящим двойным освобождением; добавлен захватить() (retain)
для обоих случаев копирования. Проверено под valgrind (включая
отдельный тест на оба вида копирования) — ноль ошибок, ноль утечек;
полная регрессия и привет.код — без изменений (те же самые 8
записей об утечке, что и раньше, побайтово идентично)
- Каналы поддерживают только целочисленные/указательные типы, только
одностороннюю разовую передачу, без закрытия; .освободить() есть
(см. выше), но неполученный текст в канале на момент освобождения
всё равно утечёт
- ✅ Устранены оба честных, узких ограничения
дождаться_поток(номер):
раньше его нужно было вызывать СТРОГО до дождаться_все_потоки() для
той же партии потоков (иначе поток был бы pthread_join-нут дважды —
неопределённое поведение POSIX), и нельзя было вызвать дважды с одним
и тем же номером (та же причина). Решение — после успешного join
ячейка массива для этого номера обнуляется (маркер "уже
присоединён"), а перед ЛЮБЫМ join (в обеих функциях) сначала
проверяется, не NULL ли уже ячейка — если да, join просто
пропускается как безопасный no-op вместо передачи NULL в
pthread_join. Порядок вызовов дождаться_поток(...) и
дождаться_все_потоки() друг относительно друга, а также повторные
вызовы дождаться_поток(...) с одним и тем же номером, теперь
безопасны в любом сочетании. Проверено под valgrind (5 повторных
запусков) на двух новых тестах: дождаться_поток(...) дважды с одним
номером, и дождаться_поток(...) уже после дождаться_все_потоки()
для той же партии — ноль ошибок, ноль утечек в обоих случаях. ⚠️
Осталось не проверяемое во время выполнения, более узкое ограничение:
номер не должен ссылаться на поток из УЖЕ СБРОШЕННОЙ (после
дождаться_все_потоки()) партии, чья ячейка могла быть перезаписана
новым потоком следующей партии
- ✅ Исправлен настоящий use-after-free (не просто утечка):
переприсваивание УЖЕ СУЩЕСТВУЮЩЕЙ переменной структурного или
контрактного типа (Кот а = Кот(1); а = Кот(2); или `Печатаемый а =
кот1; Печатаемый б = кот2; а = б;` — в отличие от НОВОГО объявления
переменной, которое уже было исправлено раньше в этой сессии) вообще
не трогало ARC: старое значение не освобождалось (честная утечка сама
по себе — единственная ссылка на объект пропадала бесследно), а если
новое значение было КОПИЕЙ другой существующей переменной — не
захватывалось (retain), из-за чего обе переменные впоследствии
освобождали ОДИН И ТОТ ЖЕ объект при выходе из области видимости.
Найдено не багрепортом, а целенаправленной проверкой ("а если
попробовать вот так?") в рамках общего наведения языка на
стабильность — воспроизведено под valgrind как "Invalid read"/"Invalid
write" на уже освобождённой памяти (настоящий краш, не абстрактный
риск). Исправлено для ОБОИХ типов (структура и контракт) — порядок
действий: сначала вычислить новое значение, затем ЗАХВАТИТЬ его (если
оно оказалось копией существующей переменной) и только ПОТОМ
ОТПУСТИТЬ старое — такой порядок (retain-до-release) заодно делает
безопасным даже самоприсваивание (а = а;: счётчик временно
увеличивается на 1, тут же уменьшается обратно тем же вызовом — риска
обратиться к уже освобождённой памяти нет, даже если старое и новое
значение физически совпадают). Для контрактных переменных
переиспользован уже существующий механизм рантайм-сравнения vtable
(отпустить_контрактное_значение). Проверено под valgrind (отдельные
тесты на переприсваивание структуры, самоприсваивание структуры и
переприсваивание контрактной переменной другой контрактной
переменной) — ноль ошибок, ноль утечек во всех трёх; полная регрессия
и привет.код — без единого нового падения или утечки (побайтово
идентично, не считая PID в заголовках valgrind)
- Установщик среды разработки пока есть только для macOS
Полную, честную историю каждой находки, включая настоящие баги
(use-after-free, double-free) и то, как они были найдены и исправлены
— смотри в ИСТОРИЯ_РАЗРАБОТКИ.md.