Skip to content

QLISP v2.8.3 — rc-контракт стабильных ячеек, отложенная смерть, live-глобалы

Latest

Choose a tag to compare

@FQingLars FQingLars released this 05 Oct 09:54
· 1 commit to main since this release

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 МБ на 20k setq; под 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 файлов без утечек.