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создаёт несколько задач; какая задача завершилась с ошибкой? Какая переменная была перемещена? Всё решается угадыванием.
Цели проектирования
- Единый опыт: Точка останова, работающая в интерпретаторе, работает в JIT и LLVM с согласованным отображением исходного кода. Пользователь не замечает различий между движками.
- Один источник: Отладочные метаданные перемещаются вместе с IR, без дублирования и поддержки двух наборов отображений.
- Нулевое влияние: Один параметр
yaoxiang run --debug; без него компиляция и выполнение полностью идентичны. - Стандарт DAP: Прямое взаимодействие с экосистемой VS Code, без изобретения собственных протоколов редактора.
Предложение
Основное проектирование
Общая архитектура:
┌──────────────────────────────────────────────────────┐
│ VS Code / Редактор │
│ DAP Client (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 | Узлы переменных/выражений | Выведенный тип (с compile-time предикатами) |
ScopeBoundary | Входы/выходы блоков/функций | Жизненный цикл области видимости переменных |
SpanInfo | Узлы spawn | Границы задач внутри блока spawn |
Процесс запуска
yaoxiang run --debug file.yx
│
├── Фаза 1: Компиляция (с отладочными метаданными)
│ ├── Парсинг → AST
│ ├── Проверка типов + верификация compile-time предикатов
│ └── Понижение до IR (с附加 отладочные метаданные)
│
├── Фаза 2: Использование интерпретаторного движка
│ └── В режиме --debug независимо от --release используется интерпретатор
│
├── Фаза 3: Запуск DAP сервера
│ ├── Инициализация канала stdio
│ ├── Ожидание подключения VS Code
│ ├── После attach пауза на точке входа программы
│ └── Переход в интерактивный цикл отладки
│
└── Фаза 4: Завершение программы / сеанса отладки → ВыходРазличия режимов
| Режим | Способ отладки |
|---|---|
yaoxiang run --debug | Принудительный интерпретатор, полнофункциональная DAP интерактивная отладка |
yaoxiang run --release | Генерация DWARF для посмертной трассировки (core dump / crash report), DAP не запускается |
yaoxiang run (обычный) | Без отладочных метаданных, без поддержки отладки |
Детальное проектирование
1. Точки останова
Типы точек останова:
├── Точка останова на строке исходника → Фронтенд генерирует метаданные позиции, бэкенд запрашивает совпадение
├── Точка останова на входе в функцию → Срабатывает при вызове функции (вторая фаза)
├── Условная точка останова → Срабатывает когда выражение вычисляется в true
└── Точка останова на данных → Срабатывает при изменении переменной (вторая фаза)Базовая логика точек останова на строках исходника:
VS Code отправляет: "Установить точку останова на file.yx:42"
│
DAP сервер:
├── Запросить в IR все узлы с SourceLocation == (file.yx, 42)
├── Переслать среде выполнения: "Приостановить на этих IR-адресах"
└── Среда выполнения возвращает: ID точки останова
Программа выполняется до IR-узла → Среда выполнения проверяет: этот узел в списке точек останова?
├── Обычная точка останова → Приостановить, уведомить DAP сервер
└── Условная точка останова → Вычислить условное выражение → true приостановитьРеализация для трёх движков:
| Интерпретатор | JIT | LLVM | |
|---|---|---|---|
| Метод вставки | Проверка в цикле выполнения по ID IR-узла | JIT вставляет int3 в машинный код | Использование LLVM DWARF + аппаратные точки останова |
| Вычисление условной точки | Интерпретация выражения напрямую | Временная JIT-компиляция условного выражения | DWARF стек выражений + вычисление |
| Накладные расходы | Дополнительный поиск по таблице на каждом IR-узле | Накладные расходы только в точках останова | Почти нулевые накладные расходы (аппаратные точки останова) |
2. Пошаговое выполнение
Step Over → Выполнить текущую строку, пропустить внутренности вызова функции, остановиться на следующей строке
Step Into → Войти внутрь вызова функции на текущей строке
Step Out → Выполнить до возврата из текущей функции
Continue → Продолжить выполнение до следующей точки останова или конца программыРеализация: Пошаговые операции по сути являются временными точками останова. Они используют тот же механизм, что и явно заданные пользователем точки останова — не две разные системы, а две разные формы использования одной системы.
Step Over:
Текущий номер строки исходника = query_line(frame)
→ Установить временную точку останова на следующей строке
→ Если текущая строка содержит вызов функции: установить временную точку останова после точки вызова
→ Continue → Попадание во временную точку останова → Удалить → Приостановить
Step Into:
Первая исполняемая позиция текущей цели вызова
→ Найти исходную позицию первого IR-узла тела функции
→ Установить временную точку останова → Continue → Попадание → Приостановить
Step Out:
Адрес возврата текущего стекового кадра
→ Найти следующую строку вызывающего
→ Установить временную точку останова → Continue → Попадание → ПриостановитьОбработка четырёх граничных случаев временных точек останова:
- Принадлежность к параллелизму: Временная точка останова привязана к ID текущей задачи; попадание других задач игнорируется.
- Step Over блока spawn: Step Over вне spawn означает выполнение всего блока 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 / замыканием │
│ Отображать состояние владения: │
│ перемещена / общий ref │
└─────────────────────────────────────┘Различия между движками:
| Движок | Получение значений переменных |
|---|---|
| Интерпретатор | Прямое чтение кадров стека VM и кучи. Каждое значение имеет чёткое представление в памяти |
| JIT | Значения в регистрах и на стеке → Требуется таблица соответствия «переменная → регистр/слот стека», генерируемая при JIT-компиляции |
| LLVM | Секция .debug_info DWARF → DW_AT_location → Нативная поддержка LLDB |
Отображение специальных типов: Уточнение типов compile-time предикатами предоставляет полезную отладочную информацию:
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" │
│ ID spawn-задачи: task-3 │
├──────────────────────────────────┤
│ #1 main() file.yx:67 │ ← Вызывающий
│ locals: data = ["hello", ...]│
├──────────────────────────────────┤
│ #2 <entry> file.yx:1 │ ← Корневой
└──────────────────────────────────┘Каждый кадр содержит: сигнатуру функции, позицию вызова (файл + строка), локальные переменные (отложенное вычисление), контекст spawn (ID задачи).
Интерпретатор и JIT各自 поддерживают связный список кадров. Получение кадров не является бесплатным — но в режиме отладки нулевая стоимость не требуется.
5. Вычисление выражений (Watch / REPL)
Пользователь вводит произвольное выражение YaoXiang в точке останова:
Watch: x + y → Возвращает результат вычисления
Watch: items[2].name → Доступ к сложной структуре
Watch: f(x) → Вызов функции (риск побочных эффектов)Стратегия вычисления:
Ввод пользовательского выражения
│
├── Компиляторный фронтенд парсирует выражение
├── Проверка типов в контексте текущего кадра
├── Значения переменных получены из текущего кадра (только для чтения)
├── Выражение выполняется как независимая микропрограмма
│ └── Изменение внешних переменных запрещено
│ └── spawn запрещён
│ └── IO запрещён (или опционально разрешён)
└── Возврат результата → Исходное состояние кадра полностью неизменноИнтерпретатор как естественная песочница: Вычисление выражений — это не создание новой песочницы; интерпретатор сам является песочницей. Вычисление выражений лишь временно помещает кадр в стек VM, который уничтожается после использования. Используется та же VM, что и при обычном выполнении, но побочные эффекты не фиксируются.
Вычисление вызовов функций: Разрешено по умолчанию, но с предупреждением пользователю «это выражение может иметь побочные эффекты» — требуется подтверждение перед выполнением.
Различия между движками:
| Движок | Вычисление выражений |
|---|---|
| Интерпретатор | Повторное использование существующего пути eval, с подстановкой окружения текущего кадра |
| JIT | Временная компиляция выражения → Связывание с текущим кадром → Выполнение → Удаление временного кода |
| LLVM | Не поддерживается — режим LLVM не обеспечивает интерактивную отладку |
6. Параллельная отладка
Видимость модели задач:
Концепция threads в DAP映射到 YaoXiang 的 spawn 任务. Каждая задача имеет собственный связный список стековых кадров и состояние выполнения.
┌─ 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
Первая фаза: Основные запросы
| 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 | Завершение сеанса отладки |
Вторая фаза: Расширенные запросы
| DAP запрос | Семантика YaoXiang |
|---|---|
setFunctionBreakpoints | Точки останова по имени функции |
setExceptionBreakpoints | Приостановка при ошибке/panic |
dataBreakpointInfo | Точки останова на данных (триггер модификации переменной) |
Стратегия реализации
Фаза ноль: Инфраструктура (перед всеми фазами)
Цель: Компиляторный фронтенд附加 отладочные метаданные к IR.
| Компонент | Изменения |
|---|---|
| Определение IR | Новые поля метаданных: SourceLocation, VarName, TypeAnnotation и др. |
| Парсер | Каждый узел AST записывает позицию в исходном коде |
| TypeChecker | Информация о типах附加 к узлам IR |
| Тестирование | Проверка того, что IR dump содержит позиции и информацию о переменных |
Без участия среды выполнения.
Первая фаза: Интерпретаторный 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 и просмотреть значения переменных.
Вторая фаза: Продвинутые возможности отладки
Цель: Вычисление выражений, точки останова на функциях, параллельная отладка, точки останова на исключениях.
| Компонент | Изменения |
|---|---|
| Движок вычисления выражений | Микропрограммная компиляция (повторное использование парсера + typechecker), временное помещение кадра в VM, изоляция побочных эффектов |
| Параллельная отладка | Маппинг списка spawn-задач, привязка ID задачи к точке останова, стратегия приостановки stop-all / stop-this-only |
| Точки останова на функциях/исключениях | Маппинг setFunctionBreakpoints, setExceptionBreakpoints |
| Расширение VS Code | Предоставление шаблона launch.json по умолчанию |
Третья фаза: JIT-отладка и LLVM DWARF
Цель: JIT-движок использует DAP повторно, LLVM генерирует DWARF для трассировки после сбоев.
| Компонент | Изменения |
|---|---|
| JIT | Реализация trait DebugEngine, генерация таблицы соответствия «переменная → регистр/слот» при компиляции, связный список кадров времени выполнения, временная компиляция выражений |
| LLVM | Отладочные метаданные IR → LLVM DILocation / DISubprogram → DWARF (без DAP-взаимодействия) |
Зависимости
Фаза ноль (метаданные IR)
↓
Фаза один (интерпретаторный DAP MVP) ← С этого момента уже работает
↓
Фаза два (продвинутые возможности)
↓
Фаза три (JIT + LLVM DWARF)Риски
| Риск | Смягчение |
|---|---|
| Сложность механизма приостановки интерпретатора | Использование простого channel/signal вместо сложного автомата; приостановка — это просто запрет на fetch следующей инструкции |
| Типобезопасность вычисления выражений | Повторное использование существующего typechecker, только чтение, без фиксации побочных эффектов |
| Детали протокола DAP | Ориентир на реализации debugpy / delve; протокол зрелый |
| Взаимоблокировка stop-all при параллельной отладке | Тайм-аут механизма + принудительная приостановка |
Компромиссы
Преимущества
- Один источник: Отладочные метаданные генерируются один раз, три движка используют совместно. Не возникает ситуации «в интерпретаторе отладочная информация корректна, а в LLVM — нет»
- Нулевое влияние: Один параметр
--debug; поведение без него абсолютно идентично - Стандарт DAP: Прямое подключение к экосистеме VS Code без собственных протоколов редактора или UI отладчика
- Приоритет интерпретатора: Отладка естественно подходит интерпретатору — гибкость, управляемость, простое вычисление выражений. Отказ от интерактивной отладки в режиме LLVM — наиболее прагматичный выбор
Недостатки
- Низкая производительность режима отладки: Интерпретатор значительно медленнее JIT/LLVM. Но для отладки производительность не нужна — никто не ожидает, что debug-режим будет работать с производственной нагрузкой
- Ограниченная отладка LLVM: AOT-компиляция не позволяет интерактивную отладку, только GDB/LLDB + DWARF. Но это компромисс: в режиме LLVM и не должно быть различий в поведении отладки
- Сложность приостановки параллелизма: Семантика stop-all требует обхода всех активных задач в интерпретаторе
Альтернативные решения
| Решение | Причина отклонения |
|---|---|
| Три движка各自 реализуют DAP | Тройная работа, три набора ошибок. Нарушение «хорошего вкуса» |
| Только DWARF, без собственного DAP | У интерпретатора и JIT нет концепции DWARF; LLDB не может проникнуть внутрь VM |
| Командный отладчик в стиле Python pdb | VS Code обеспечивает несравнимо лучший опыт, чем командная строка |
| Помещение DAP в процесс LSP | Жизненные циклы полностью различаются — LSP привязан к проекту, DAP к сеансу отладки. Изоляция процессов — жёсткое требование |
Открытые вопросы
- [ ] Синтаксис выражений условных точек останова полностью совпадает с обычным YaoXiang? (Рекомендация: Полное совпадение, повторное использование парсера)
- [ ] Поведение Step Into блока
spawn: когда пользователь нажимает Step Into для входа в блок spawn, какая из параллельных задач должна отображаться? (Рекомендация: Приостановка на первой созданной задаче) - [ ] Расширение VS Code: следует ли размещать конфигурацию отладки в существующем каталоге
vscode-extension/или в отдельном репозитории?
