RFC-034: Унифицированный инструментарий отладки
Резюме
Внедрение в YaoXiang унифицированного инструментария отладки. Ключевая идея — один источник, три потребителя: компилятор-фронтенд встраивает позиции исходного кода, имена переменных и информацию о типах как полноправных граждан в YaoXiang IR, а три бэкенда — интерпретатор, JIT и LLVM — каждый потребляют один и тот же набор метаданных. Пользователь запускает DAP-сервер (Debug Adapter Protocol) командой yaoxiang run --debug, VS Code подключается через stdio, и пользователь получает единый опыт: точки останова, пошаговое выполнение, просмотр переменных, стек вызовов, вычисление выражений, отладка конкурентных вычислений — вне зависимости от того, какой движок выполнения используется под капотом.
Мотивация
Зачем нужна эта возможность?
Сейчас средства поиска ошибок в программах на YaoXiang крайне примитивны:
io.println("DEBUG: x = " + x.to_string())
io.println("DEBUG: entered branch A")Три критические проблемы:
- Самозагрузка компилятора затруднена: YaoXiang-компилятор пишется на самом YaoXiang, но человек, пишущий компилятор, не может отлаживать собственный код. Отсутствие интерактивной отладки на этапе самозагрузки — это тупик.
- Три движка, ноль отладки: интерпретатор, JIT и LLVM работают каждый по-своему; при сбое пользователь может лишь проверить, появилось ли
ALL TESTS PASSEDв stdout. Утверждение упало? Непонятно на какой строке и каково значение переменной. - Конкурентность — чёрный ящик:
spawnпорождает несколько задач — какая из них зависла? Кто переместил (move) переменную? Всё угадывается наугад.
Цели проектирования
- Единый опыт: точка останова, срабатывающая в интерпретаторе, должна срабатывать и в JIT, а в LLVM — иметь согласованное соответствие исходному коду. Пользователь не должен ощущать разницу между движками.
- Один источник: отладочные метаданные текут вместе с IR, не дублируются, не нужно сопровождать два набора отображений.
- Нулевая инвазивность: один флаг
yaoxiang run --debug; без него поведение компиляции и выполнения полностью не меняется. - Стандарт DAP: прямое подключение к экосистеме VS Code, без изобретения новых протоколов редактора.
Предложение
Ключевая идея
Обзор архитектуры:
┌──────────────────────────────────────────────────────┐
│ VS Code / редактор │
│ DAP-клиент (launch.json) │
└────────────────────────┬─────────────────────────────┘
│ stdio
┌────────────────────────▼─────────────────────────────┐
│ DAP-сервер (yx-core) │
│ ┌─────────┐ ┌──────────┐ ┌───────────────────┐ │
│ │ Сессии │ │ Точки │ │ Движок вычисления │ │
│ │ │ │ останова │ │ выражений │ │
│ └─────────┘ └──────────┘ └───────────────────┘ │
└────────────────────────┬─────────────────────────────┘
│ запросы / управление
┌────────────────────────▼─────────────────────────────┐
│ Интерфейс отладки среды выполнения (trait) │
│ pause / resume / step / get_frames / eval / ... │
└────┬──────────────────┬──────────────────┬──────────┘
│ │ │
┌────▼────┐ ┌───────▼───────┐ ┌─────▼──────┐
│ Интерпр.│ │ JIT (RFC-028)│ │ LLVM AOT │
│ напрямую│ │ формирует │ │ метаданные │
│ потребляет│ │ облегчённые │ │ IR → DWARF │
│ метаданные│ │ таблицы отладки│ │ │
│ IR │ └───────────────┘ └────────────┘
└─────────┘Ключевые проектные решения:
- DAP-сервер и среда выполнения разделены через trait. Серверу не важно, что внизу — интерпретатор или JIT: он отдаёт команды исключительно через trait
DebugEngine. Каждый движок самостоятельно реализует один и тот же trait. yaoxiang run --debugпринудительно использует интерпретатор. Отладка требует управляемости, а не производительности. В режиме LLVM лишь генерируется DWARF для посмертного анализа (core dump / crash report), интерактивная отладка не выполняется.- Точка входа обнаружения заимствуется у
yaoxiang run. Новые подкоманды не вводятся — модель мышления: «запустить мою программу в режиме отладки».
Метаданные отладки в IR
К существующему YaoXiang IR добавляются метаданные, новые виды IR не вводятся. Все метаданные порождаются в одном месте — фронтенде компилятора, бэкенды лишь читают:
| Метаданные | Точка прикрепления | Описание |
|---|---|---|
SourceLocation | каждый узел IR | файл:строка:столбец |
VarName | объявление/связывание | имя переменной в исходном коде |
TypeAnnotation | переменная/выражение | выведенный тип (включая компайл-тайм предикаты) |
ScopeBoundary | вход/выход блока/функции | жизненный цикл области видимости |
SpanInfo | узлы spawn | границы задач внутри блока spawn |
Последовательность запуска
yaoxiang run --debug file.yx
│
├── Фаза 1: компиляция (с отладочными метаданными)
│ ├── разбор → AST
│ ├── проверка типов + верификация компайл-тайм предикатов
│ └── понижение до IR (с прикреплением отладочных метаданных)
│
├── Фаза 2: использование движка-интерпретатора
│ └── в режиме --debug независимо от --release используется интерпретатор
│
├── Фаза 3: запуск DAP-сервера
│ ├── инициализация транспортного канала stdio
│ ├── ожидание подключения VS Code
│ ├── после успешного подключения — пауза на входе в программу
│ └── переход в интерактивный цикл отладки
│
└── Фаза 4: завершение программы / сеанса отладки → выходРазличия режимов
| Режим | Способ отладки |
|---|---|
yaoxiang run --debug | принудительно интерпретатор, полнофункциональная интерактивная DAP-отладка |
yaoxiang run --release | генерация DWARF для посмертного анализа (core dump / crash report), DAP не запускается |
yaoxiang run (обычный) | без отладочных метаданных, отладка не поддерживается |
Детальное проектирование
1. Точки останова
Типы точек останова:
├── по строке исходного кода → фронтенд генерирует метаданные позиции, бэкенд ищет совпадения
├── на входе в функцию → срабатывает при вызове функции (фаза 2)
├── условные → срабатывают, когда выражение истинно
└── по данным → срабатывают при изменении переменной (фаза 2)Ключевая логика точки останова по строке исходного кода:
VS Code отправляет: «Установить точку останова в file.yx:42»
│
DAP-сервер:
├── ищет в IR все узлы с SourceLocation == (file.yx, 42)
├── пересылает среде выполнения: «приостановить на этих IR-адресах»
└── среда выполнения возвращает: идентификатор точки останова
Программа достигает узла IR → среда выполнения проверяет: этот узел в списке точек останова?
├── обычная точка → приостановить, уведомить DAP-сервер
└── условная точка → вычислить условное выражение → true → приостановитьРеализация в трёх движках:
| Интерпретатор | JIT | LLVM | |
|---|---|---|---|
| Установка точки | проверка ID IR-узла в цикле выполнения | int3 в машинном коде из JIT | DWARF LLVM + аппаратные точки |
| Вычисление условия | прямая интерпретация выражения | временная JIT-компиляция выражения | стек DWARF-выражений + вычисление |
| Накладные расходы | один лишний поиск на IR-узел | только в точках останова | почти ноль (аппаратные точки) |
2. Пошаговое выполнение
Step Over → выполнить текущую строку, пропустить тело вызова, остановиться на следующей
Step Into → войти внутрь вызова функции на текущей строке
Step Out → выполнить до возврата из текущей функции
Continue → возобновить выполнение до следующей точки останова или завершенияЛогика реализации: пошаговые операции по сути являются временными точками останова. Они разделяют один механизм с явно установленными пользователем точками — это не две системы, а два способа использования одной.
Step Over:
текущий номер строки = query_line(frame)
→ установить временную точку останова на следующей строке
→ если текущая строка — вызов функции: временная точка после точки вызова
→ Continue → попадание во временную точку → удалить → пауза
Step Into:
первая исполнимая строка цели вызова
→ найти позицию исходного кода первого IR-узла тела функции
→ установить временную точку останова → Continue → попадание → пауза
Step Out:
адрес возврата текущего кадра
→ найти следующую строку вызывающего
→ установить временную точку останова → Continue → попадание → паузаОбработка четырёх граничных случаев временных точек останова:
- Принадлежность при конкурентности: временная точка останова привязана к идентификатору текущей задачи; попадания в других задачах игнорируются.
- Step Over блока
spawn: снаружиspawnStep Over означает прогон всего блокаspawnи переход дальше. Для входа внутрь отладкиspawnследует использовать Step Into. - Временная точка не сработала: сторожевой таймер (30 секунд без попадания) → принудительная пауза → уведомление VS Code. Одновременно слушаем событие выхода программы → немедленная очистка.
- Несколько IR-узлов на одной строке: временная точка Step Over помечается флагом
ignore_current_line; при попадании, если строка равна текущей → игнорировать и продолжить.
3. Просмотр переменных и областей видимости
Запрос VS Code: «Список переменных текущего кадра»
│
DAP-сервер:
├── найти IR-узел текущей точки паузы
├── обойти связывания переменных внутри текущей ScopeBoundary
│ └── каждое связывание возвращает: (имя, тип, ссылка на значение в среде выполнения)
└── собрать VariablesResponse → VS CodeСлои областей видимости:
┌─ Globals ───────────────────────────┐
│ связывания уровня модуля: константы, │
│ псевдонимы типов, глобальные │
├─ Locals ────────────────────────────┤
│ локальные переменные текущей функции│
│ ├── параметры (аргументы функции) │
│ └── локальные связывания (let / присваивание) │
├─ Captured ──────────────────────────┤
│ внешние переменные, захваченные │
│ блоком spawn / замыканием │
│ отображать состояние владения: │
│ перемещено (move) / разделяемая ссылка │
└─────────────────────────────────────┘Различия движков:
| Движок | Получение значения переменной |
|---|---|
| Интерпретатор | прямое чтение кадра и кучи VM. Каждое значение имеет явное представление в памяти |
| JIT | значения в регистрах и на стеке → требуется таблица отображения «переменная → регистр/слот стека», формируемая при JIT-компиляции |
| LLVM | секция .debug_info DWARF → DW_AT_location → нативная поддержка LLDB |
Отображение специальных типов: компайл-тайм предикаты уточняют тип, и эта информация полезна при отладке:
x: Positive(x) → отображать «Int (x > 0 = True)»
y: Sorted(y) → отображать «Array(Int) (гарантия сортировки)»
result: T → отображать конкретный тип среды выполнения4. Стек вызовов
Запрос DAP: StackTrace
│
Ответ:
┌──────────────────────────────────┐
│ #0 process_item() file.yx:42 │ ← текущая точка паузы
│ locals: item = "hello" │
│ spawn task ID: task-3 │
├──────────────────────────────────┤
│ #1 main() file.yx:67 │ ← caller
│ locals: data = ["hello", ...]│
├──────────────────────────────────┤
│ #2 <entry> file.yx:1 │ ← корень
└──────────────────────────────────┘Каждый кадр содержит: сигнатуру функции, позицию вызова (файл+строка), локальные переменные (ленивое вычисление), контекст spawn (идентификатор задачи).
Интерпретатор и JIT каждый поддерживают свой список кадров. Получение кадров не бесплатно, но в режиме отладки нулевая стоимость не требуется.
5. Вычисление выражений (Watch / REPL)
Пользователь вводит произвольное выражение YaoXiang в точке останова:
Watch: x + y → вернуть результат вычисления
Watch: items[2].name → доступ к сложной структуре
Watch: f(x) → вызов функции (риск побочных эффектов)Стратегия вычисления:
Пользователь вводит выражение
│
├── разбор выражения фронтендом компилятора
├── проверка типов в контексте текущего кадра
├── значения переменных берутся из текущего кадра (доступ только на чтение)
├── выражение выполняется как отдельная микропрограмма
│ └── изменение внешних переменных запрещено
│ └── spawn запрещён
│ └── IO запрещено (или опционально разрешено)
└── возврат значения → состояние исходного кадра полностью неизменноИнтерпретатор как естественная песочница: вычисление выражений — не создание новой песочницы, сам интерпретатор уже является песочницей. Вычисление лишь временно помещает кадр, который уничтожается после использования. Разделяет ту же VM с обычным выполнением, но не фиксирует никаких побочных эффектов.
Вычисление вызовов функций: разрешено по умолчанию, но пользователь предупреждается «это выражение может иметь побочные эффекты» и выполнение требует подтверждения.
Различия движков:
| Движок | Вычисление выражений |
|---|---|
| Интерпретатор | повторное использование существующего пути eval, инъекция окружения текущего кадра |
| JIT | временная компиляция выражения → линковка с текущим кадром → выполнение → выброс временного кода |
| LLVM | не поддерживается — режим LLVM не предоставляет интерактивной отладки |
6. Отладка конкурентности
Видимость модели задач:
Понятие DAP threads отображается на задачи spawn в YaoXiang. Каждая задача имеет собственный список кадров стека и состояние выполнения.
┌─ Threads ───────────────────────────┐
│ ● task-1 main() file.yx:10 │ ← текущий фокус
│ ▶ task-2 fetch() file.yx:34 │ ← в работе
│ ⏸ task-3 process() file.yx:56 │ ← пауза на точке останова
│ ◼ task-4 write() завершена │
└─────────────────────────────────────┘Точки останова в контексте конкурентности:
| Режим паузы | Поведение | Сценарий использования |
|---|---|---|
stop-all (по умолчанию) | одна задача попала → все задачи приостановлены | отладка гонок данных, глобального состояния |
stop-this-only | приостановить только попавшую задачу, остальные продолжают | отладка логики отдельной задачи |
Семантика пошагового выполнения для блоков spawn:
spawn { // Step Over → прогон всего блока spawn
task_a() // Step Into → войти в task_a
task_b() // работает параллельно, отдельный step не влияет
}7. Отображение протокола DAP
Фаза 1: базовые запросы
| Запрос DAP | Семантика YaoXiang |
|---|---|
initialize | согласование возможностей: точки останова, шаг, переменные, кадры |
launch / attach | запуск/присоединение к программе YaoXiang (--debug идёт по attach) |
setBreakpoints | установка точек останова по строкам исходного кода |
configurationDone | точки останова готовы, начать выполнение |
threads | вернуть список всех активных задач spawn |
stackTrace | вернуть список кадров указанной задачи |
scopes | вернуть области видимости переменных текущего кадра |
variables | вернуть список переменных указанной области видимости |
continue | возобновить выполнение |
next | Step Over |
stepIn | Step Into |
stepOut | Step Out |
pause | прервать все задачи |
evaluate | вычислить выражение в текущем кадре |
disconnect | завершить сеанс отладки |
Фаза 2: расширенные запросы
| Запрос DAP | Семантика YaoXiang |
|---|---|
setFunctionBreakpoints | точки останова по имени функции |
setExceptionBreakpoints | пауза при ошибке/panic |
dataBreakpointInfo | точки останова по данным (срабатывает при изменении переменной) |
Стратегия реализации
Фаза ноль: инфраструктура (предшествует всем фазам)
Цель: фронтенд компилятора прикрепляет отладочные метаданные к IR.
| Компонент | Изменения |
|---|---|
| Определение IR | новые поля метаданных SourceLocation, VarName, TypeAnnotation и др. |
| Parser | каждый узел AST записывает позицию в исходном коде |
| TypeChecker | информация о типах прикрепляется к IR-узлам |
| Тесты | проверить, что дамп IR содержит позиции и информацию о переменных |
Среда выполнения не затрагивается.
Ход реализации (2026-09-17):
| Поставка | Статус | Форма реализации |
|---|---|---|
| Позиции в исходном коде | Завершено | Все 76 вариантов Instruction несут поле span; метод span() намеренно не имеет ветки по умолчанию — добавление нового варианта без span приводит к ошибке компиляции. Покрытие позиций 40/41 инструкций |
| Имена переменных | Завершено | LocalSlot { name, ty, scope_depth } подвешен к FunctionBody::Code::locals; register_local записывает на месте в момент генерации. Отладочная секция .42 v2 несёт имена, продукты v1 обратно совместимы при чтении |
| Имена глобальных слотов | Завершено | Отладочная секция .42 v3 несёт таблицу «номер слота → имя связывания верхнего уровня». Связывания верхнего уровня идут через Operand::Global, которого нет в локальной таблице имён ни одной функции — без этой таблицы можно сообщить только число, но не имя. Для продуктов v1/v2 читается пустая таблица |
| Информация о типах | Частично | Слот уже содержит ty; отдельные метаданные TypeAnnotation не сделаны |
| Видимость в дампе | Завершено | dump для каждой инструкции выводит ; <file>:<line>:<col>, а также список locals: имя@слот |
Форма данных этой таблицы напрямую стыкуется с фазой 1: разбор точек останова DAP потребляет debug_map, панель переменных — local_names (имена) и locals (типы).
Фаза 1: DAP MVP для интерпретатора
Цель: yaoxiang run --debug file.yx позволяет устанавливать точки останова, выполнять пошагово и просматривать переменные.
| Компонент | Изменения |
|---|---|
| DAP-сервер (новый модуль yx-core) | транспорт stdio, обработка базовых запросов, менеджер точек останова (строка исходного кода → IR-узел) |
| Trait отладки среды выполнения (yx-core) | определение trait DebugEngine (pause, resume, step, get_frames, eval, get_variables) |
| Интерпретатор | проверка точек останова в цикле выполнения, механизм паузы/возобновления, ведение списка кадров, реализация InterpreterDebugEngine |
| CLI | параметр yaoxiang run --debug |
Критерий приёмки: для любого файла .yx в tests/yaoxiang/ можно в VS Code установить точку останова, выполнить Step Over и увидеть значения переменных.
Фаза 2: расширенные возможности отладки
Цель: вычисление выражений, точки останова по функциям, отладка конкурентности, точки останова по исключениям.
| Компонент | Изменения |
|---|---|
| Движок вычисления выражений | компайл-тайм-микропрограмма (повторное использование parser + typechecker), временный пуш кадра в VM, изоляция побочных эффектов |
| Отладка конкурентности | отображение задач spawn, привязка точек останова к идентификатору задачи, стратегии паузы stop-all / stop-this-only |
| Точки останова по функциям/исключениям | отображение setFunctionBreakpoints, setExceptionBreakpoints |
| Расширение VS Code | шаблон launch.json по умолчанию |
Фаза 3: отладка JIT и DWARF для LLVM
Цель: повторное использование DAP движком JIT, формирование DWARF LLVM для анализа сбоев.
| Компонент | Изменения |
|---|---|
| JIT | реализация trait DebugEngine, генерация таблицы отображения «переменная → регистр» во время компиляции, список кадров в среде выполнения, временная компиляция выражений |
| LLVM | отладочные метаданные IR → LLVM DILocation / DISubprogram → DWARF (без интерактивного DAP) |
Зависимости
Фаза 0 (метаданные IR)
↓
Фаза 1 (DAP MVP для интерпретатора) ← с этого момента уже пригодно к использованию
↓
Фаза 2 (расширенные возможности)
↓
Фаза 3 (JIT + DWARF для LLVM)Риски
| Риск | Смягчение |
|---|---|
| Сложность механизма паузы интерпретатора | простой канал/сигнал вместо сложного конечного автомата; пауза — это просто запрет на выборку следующей инструкции |
| Типобезопасность вычисления выражений | повторное использование существующего typechecker, доступ только на чтение, побочные эффекты не фиксируются |
| Детали протокола DAP | ссылаться на реализации debugpy / delve, протокол зрелый |
| Активная блокировка stop-all при конкурентности | механизм тайм-аута + принудительная пауза |
Компромиссы
Достоинства
- Один источник: отладочные метаданные генерируются один раз, используются тремя движками. Не возникнет ситуации «в интерпретаторе отладочная информация верна, а в LLVM — нет».
- Нулевая инвазивность: один параметр
--debug, без него поведение полностью не меняется. - Стандарт DAP: прямое подключение к экосистеме VS Code, не нужны ни пользовательский протокол редактора, ни собственный UI отладчика.
- Приоритет интерпретатора: отладка от природы подходит интерпретатору — гибкость, управляемость, простота вычисления выражений. Отсутствие интерактивной отладки в режиме LLVM — самый прагматичный выбор.
Недостатки
- Низкая производительность в режиме отладки: интерпретатор значительно медленнее JIT/LLVM. Но отладка не требует производительности — никто не ожидает запуска рабочей нагрузки в режиме отладки.
- Ограничения отладки LLVM: AOT-компиляция не допускает интерактивной отладки, доступны только GDB/LLDB + DWARF. Но это компромисс: в режиме LLVM не должно быть поведенческих различий в отладке.
- Сложность паузы при конкурентности: реализация семантики stop-all в интерпретаторе требует обхода всех активных задач.
Альтернативы
| Альтернатива | Почему не выбрана |
|---|---|
| Самостоятельная реализация DAP каждым из трёх движков | тройной объём работы, тройной набор багов. Нарушает «хороший вкус» |
| Использовать только DWARF, без собственного DAP | у интерпретатора и JIT нет концепции DWARF, LLDB не проникнет внутрь VM |
| Сделать консольный отладчик по образцу Python pdb | опыт VS Code радикально превосходит консольный отладчик |
| Встроить DAP в процесс LSP | жизненные циклы совершенно различны — LSP привязан к проекту, DAP — к сеансу отладки. Изоляция процессов — жёсткое требование |
Открытые вопросы
- [ ] Синтаксис выражений условных точек останова полностью совпадает с обычным YaoXiang? (Предложение: полностью совпадает, повторно используется parser)
- [ ] Поведение Step Into внутри блока
spawn: какую из нескольких параллельных задач следует показать, когда пользователь нажимает Step Into, входя в блокspawn? (Предложение: пауза на первой уже созданной задаче) - [ ] Расширение VS Code: конфигурация отладки размещается в существующей директории
vscode-extension/или в отдельном репозитории?
Ссылки
- RFC-024: Модель конкурентности на основе блоков spawn
- RFC-027: Компайл-тайм предикаты и унифицированная статическая верификация
- RFC-028: JIT-компилятор — многоуровневые движки выполнения в VM
- RFC-030: Механизм утверждений assert
- Спецификация протокола DAP
- debugpy — справочная реализация DAP для Python
- Delve — справочный отладчик для Go
