Skip to content

RFC-034: Единый инструментарий отладки

Аннотация

Введение единого инструментария отладки для YaoXiang. Основной принцип проектирования — один источник, три варианта потребления: компиляторный фронтенд встраивает информацию о расположении в исходном коде, имена переменных и сведения о типах как объекты первого класса в YaoXiang IR, а интерпретатор, JIT и LLVM — три бэкенда —各自 потребляют одну и ту же метаинформацию. Пользователь запускает сервер DAP (Debug Adapter Protocol) через yaoxiang run --debug, VS Code подключается через stdio и получает унифицированный опыт работы с точками останова, пошаговым выполнением, просмотром переменных, стеком вызовов, вычислением выражений и отладкой параллельных задач — независимо от того, какой движок выполнения используется в основе.

Мотивация

Почему эта функция необходима?

Текущие средства поиска ошибок в программах YaoXiang крайне примитивны:

yaoxiang
io.println("DEBUG: x = " + x.to_string())
io.println("DEBUG: entered branch A")

Три критические проблемы:

  1. Самоприменение компилятора заблокирована: Компилятор YaoXiang пишется на YaoXiang, но разработчики компилятора не могут отлаживать собственный код. Отсутствие интерактивных средств отладки на этапе самоприменения — это тупик.
  2. Три движка, ноль отладки: Интерпретатор, JIT и LLVM работают независимо друг от друга; при возникновении проблем пользователь может лишь проверить, появилась ли строка ALL TESTS PASSED в stdout. Ошибка проверки? Неизвестно, в какой строке, неизвестно значение переменной.
  3. Параллелизм — чёрный ящик: 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   │    │              │    │            │
└─────────┘    └───────────────┘    └────────────┘

Ключевые проектные решения:

  1. DAP сервер и среда выполнения разделены через trait. Серверу не важно, что в основе — интерпретатор или JIT; он только отправляет команды через trait DebugEngine. Каждый движок независимо реализует один и тот же trait.
  2. yaoxiang run --debug принудительно использует интерпретатор. Для отладки нужна управляемость, а не производительность. В режиме LLVM генерируется только DWARF для посмертной трассировки (core dump / crash report), интерактивная отладка не выполняется.
  3. Повторное использование логики обнаружения точки входа из 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 приостановить

Реализация для трёх движков:

ИнтерпретаторJITLLVM
Метод вставкиПроверка в цикле выполнения по 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 → Попадание → Приостановить

Обработка четырёх граничных случаев временных точек останова:

  1. Принадлежность к параллелизму: Временная точка останова привязана к ID текущей задачи; попадание других задач игнорируется.
  2. Step Over блока spawn: Step Over вне spawn означает выполнение всего блока spawn с переходом к следующей строке. Для отладки внутри spawn следует использовать Step Into.
  3. Промах временной точки останова: Установить тайм-аут наблюдателя (30 секунд без попадания в любую точку останова) → Принудительная приостановка → Уведомление VS Code. Одновременно слушать события завершения программы → Немедленная очистка.
  4. Несколько 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Возобновление выполнения
nextStep Over
stepInStep Into
stepOutStep 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 pdbVS Code обеспечивает несравнимо лучший опыт, чем командная строка
Помещение DAP в процесс LSPЖизненные циклы полностью различаются — LSP привязан к проекту, DAP к сеансу отладки. Изоляция процессов — жёсткое требование

Открытые вопросы

  • [ ] Синтаксис выражений условных точек останова полностью совпадает с обычным YaoXiang? (Рекомендация: Полное совпадение, повторное использование парсера)
  • [ ] Поведение Step Into блока spawn: когда пользователь нажимает Step Into для входа в блок spawn, какая из параллельных задач должна отображаться? (Рекомендация: Приостановка на первой созданной задаче)
  • [ ] Расширение VS Code: следует ли размещать конфигурацию отладки в существующем каталоге vscode-extension/ или в отдельном репозитории?

Ссылки