Repository navigation
QLISP 2.8.3 — Release Notes
Тег: v2.8.3 · Платформы: Linux x86_64, Windows x86_64
Бинарники — сборка CI, коммит 1e79234 ветки main (зелёный прогон 37269734007). Состав: qlisp (REPL/интерпретатор + JIT), qlisp-lsp, qvalent.
| Файл | Платформа | Состав |
|---|---|---|
qlisp-linux-x86_64 |
Linux x86_64 | REPL/интерпретатор + JIT |
qlisp-lsp-linux-x86_64 |
Linux x86_64 | Language Server |
qvalent-linux-x86_64 |
Linux x86_64 | пакетный менеджер |
qlisp-windows-x86_64.zip |
Windows x86_64 | qlisp.exe, qlisp-lsp.exe, qvalent.exe |
SHA256SUMS — контрольные суммы всех файлов.
Что нового
1. rc-контракт стабильных ячеек (главное изменение)
Стабильная ячейка несёт refs_ (бит стабильности + счётчик, rc=1 при создании). Каждое место разделения берёт ссылку: захваты замыканий, O(1)-append списков, adopt_world, plist-таблица, fusion-реестры, слоты инстансов, back-refs декодера образов, примитивы-эмбеды. Смерть последнего владельца — итеративный worklist-каскад stable_release: строковые байты возвращаются в malloc, программы освобождают W^X-страницу и удаляются, тензоры снимаются с трекинга. nil бессмертен.
Вместе с этим удалены якоря-владельцы и аудитор контрактов смерти: QLISP_AUDIT больше не читается, реестр якорей и audit_death_contracts вычищены, CI-шаг leak_watch (QLISP_AUDIT=1) заменён на ASan-sweep с detect_leaks=0.
QA-хук: QLISP_RC_POISON=1 заполняет освобождённые ячейки и падает на use-after-release / double-release — контракт ссылок проверяется без ASan.
2. Доктрина отложенной смерти
Любая ЗАМЕНА стабильного значения посреди формы (adopt_world старого мира, swap setq, перепись put, переопределение defun/слота/макроса) больше не каскадит сразу: корень паркуется в defer_stable_release с меткой эпохи и умирает либо на следующем restore скраша (правило эпох), либо на границе формы (flush_form_end). Причина (доказано ASan): под live-резолвингом у исполняемого замыкания rc=1 — оно держит только пару спайна, и каскад посреди формы убивал ТЕЛО исполняемой функции (use-after-free в adopt_world). Явная смерть по запросу — (clear sym ...), (drop-model m), перепись биндинга — остаётся синхронной: пользователь попросил умереть сейчас. Парк константный (очередь хронологическая, слив с конца), иначе restore в горячем цикле сканировал бы всю очередь.
3. Live-резолвинг глобалов
«Острова циклов захвата» устранены: замыкание больше не захватывает глобальный кадр, глобалы резолвятся в момент вызова (семантика Common Lisp — переопределение defun видно живым замыканием). Захватываются только локальные кадры (let/let*/циклы), каждый со своей ссылкой. Разобраны три зависимости: образы — через отложенные каскады, import_from — через модульные лексические кадры, JIT — тест переписан на живой контракт. Контракт закрыт сюитом tests/live_globals.qlsp.
⚠️ Следствие: лисп-обёртка с именем примитива затеняет сам примитив (символы регистронезависимы) —(defun sample (logits) (SAMPLE logits))вызовет сам себя и уйдёт в бесконечный хвостовой цикл. В stdlib такие обёртки убраны (SLEEP→WAIT-MS,STRING-CONTAINS→STRING-INDEX).
4. Свип утечек
ASan + detect_leaks=1 по всему корпусу: было 55/87 файлов без утечек, стало 85/87. Закрыто две ошибки владения: кэш раскрытий макросов ронял свои root-клон'ы (кэш не может быть владельцем памяти, которую читатель берёт без ссылки — либо парк, либо клон-на-хит) и clone_to инстанса добавлял ссылку поверх счётчика создания слота.
5. Фикс &optional со значением по умолчанию
Регрессия 2.8.2 закрыта: (defun f (&optional (k 0) (s 1)) ...) больше не теряет default-формулы — новая clean_param_list нормализует сырой список параметров напрямую, &OPTIONAL/&REST и формы (имя default) проходят насквозь.
(defun addup (&optional (k 0) (s 1)) (print (list k s)))
(addup) ;; → (0 1) — в 2.8.2 было (nil 1)
(addup 5) ;; → (5 1)6. Телдаун мира
~Interpreter разбирает все владеющие реестры; добавлены ~MacroExpander (дренирует макро-таблицы и кэш раскрытия) и деструктор ClassRegistry. run-tests освобождает реестр *tests* в конце — цикл «пара ↔ захваты тест-замыканий» разорван до телдауна, LSan-чистый выход.
7. Починка Windows-сборки
Удалены мёртвые #include <execinfo.h> и <unistd.h> из src/eval/interpreter.cpp — glibc-заголовок в MinGW-w64 ломал сборку (вызовов backtrace в коде нет).
Изменение (mem-report)
Ключ rooted удалён: бессмертие теперь выражается ссылками. Добавлены счётчики смерти — stable-cells, stable-cells-released, buffers-released, progs-released. Реальный вывод на этом бинаре:
(print (mem-report))
;; → (("names" . 0) ("payloads" . 0) ("progs" . 0) ("tensors" . 0)
;; ("shared" . 0) ("stable-bytes" . 123664) ("scratch-bytes" . 3832)
;; ("scratch-cells" . 25249) ("stable-cells" . 25711)
;; ("stable-cells-released" . 22235) ("buffers-released" . 0)
;; ("progs-released" . 0))Известные ограничения
- Острова циклов через
set_cdrне собираются — доктрина no-GC: остров, удерживающий сам себя, неразрешим без GC. - Две остаточные утечки (обе стабильны, не растут с повторами):
tests/symbolic_neuro.qlsp(66 Б) иtests/tablet.qlsp(67 Б) — стабильный клон строки, последняя ссылка не снимается. - MSan неприменим для текущего GCC-тулчейна (нужен clang + инструментированная libc++).
- Класс C — многократный
setqодного тензора: ранее +12 МБ на 20ksetq; под rc-контрактом не перезамерялось. run-testsосвобождает реестр в конце, поэтому повторный вызов в том же скрипте даст0/0 passed— контракт фреймворка, не регресс.
Тесты: корпус зелёный, новые сюиты rc_contract.qlsp (9 сценариев) и live_globals.qlsp (9 deftest), tail_call.qlsp расширен &optional-тестами, plist_anchor.qlsp переведён на rc-метрики; ASan-свип 85/87 файлов без утечек.