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 порождает несколько задач — какая из них зависла? Кто переместил (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       │    └───────────────┘    └────────────┘
└─────────┘

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

  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переменная/выражениевыведенный тип (включая компайл-тайм предикаты)
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 → приостановить

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

ИнтерпретаторJITLLVM
Установка точкипроверка ID IR-узла в цикле выполненияint3 в машинном коде из JITDWARF 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 → попадание → пауза

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

  1. Принадлежность при конкурентности: временная точка останова привязана к идентификатору текущей задачи; попадания в других задачах игнорируются.
  2. Step Over блока spawn: снаружи spawn Step Over означает прогон всего блока 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 / замыканием           │
│  отображать состояние владения:      │
│  перемещено (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возобновить выполнение
nextStep Over
stepInStep Into
stepOutStep 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/ или в отдельном репозитории?

Ссылки ​