Известные ограничения

Список ниже — то, что пока не работает или работает не полностью, на момент написания этого документа:

  • ⚠️ параллельно детерминировано { ... } (детерминированный планировщик,

"эстафета", см. раздел Детерминированность выше) — честные 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.