feat: масштабное улучшение системы обработки ошибок и тестирования

Основные изменения:
- Добавлена иерархия исключений (17 классов) с кодами ошибок и контекстом
- Улучшена обработка ошибок: детальные сообщения с подсказками
- Добавлено 24 теста для экстремальных случаев (комбинаторика, циклы, async)
- Добавлено 23 теста для системы обработки ошибок
- Исправлен баг с optional-аргументами в renderer.py
- Обновлены импорты в тестах (src.breakshaft → breakshaft)

Документация:
- ERROR_DESIGN.md — проектирование системы ошибок
- COMMUTATIVITY_DESIGN.md — анализ проблемы некоммутативности (10 вариантов решений)

Файлы:
- src/breakshaft/exceptions.py (новый) — модуль исключений
- tests/test_error_handling.py (новый) — тесты ошибок
- tests/test_extreme_cases.py (новый) — экстремальные кейсы

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
Qwen Code Assistant
2026-03-28 13:42:04 +00:00
parent 74d78b1957
commit ca605001b3
17 changed files with 3063 additions and 21 deletions

737
COMMUTATIVITY_DESIGN.md Normal file
View File

@@ -0,0 +1,737 @@
# Масштабное проектирование: Решение проблемы некоммутативных преобразований в breakshaft
## Содержание
1. [Постановка проблемы](#1-постановка-проблемы)
2. [Анализ текущей ситуации](#2-анализ-текущей-ситуации)
3. [Варианты решений](#3-варианты-решений)
4. [Сравнительная таблица](#4-сравнительная-таблица)
5. [Рекомендации](#5-рекомендации)
---
## 1. Постановка проблемы
### 1.1. Что такое некоммутативность в breakshaft?
**Некоммутативное преобразование** — ситуация, когда существует несколько путей преобразования типов, дающих **разные результаты**.
#### Пример:
```python
@repo.mark_injector()
def int_to_a_v1(i: int) -> A:
return A(i * 10) # Путь 1: A(420) из int=42
@repo.mark_injector()
def int_to_a_v2(i: int) -> A:
return A(i + 100) # Путь 2: A(142) из int=42
def consumer(dep: A) -> int:
return dep.a
# Два пути дают разные результаты: 420 vs 142
```
### 1.2. Почему это проблема?
| Аспект | Проблема |
|--------|----------|
| **Детерминизм** | Один и тот же код может давать разные результаты |
| **Отладка** | Сложно понять, какой путь был выбран |
| **Предсказуемость** | Поведение зависит от внутреннего порядка обхода графа |
| **Тестирование** | Тесты могут проходить/падать недетерминированно |
### 1.3. Где возникает в коде?
```
src/breakshaft/
├── graph_walker.py
│ ├── generate_callgraph() # Построение графа
│ ├── explode_callgraph_branches() # Комбинаторный взрыв вариантов
│ └── filter_exploded_callgraph_branch() # Фильтрация (выбор пути)
└── convertor.py
└── get_callseq() # Проверка force_commutative
```
**Критическое место**`filter_exploded_callgraph_branch()`:
- Использует эвристики для выбора пути
- Порядок обхода не гарантирован
- При `force_commutative=True` выбрасывает ошибку если >1 пути
---
## 2. Анализ текущей ситуации
### 2.1. Текущий алгоритм выбора пути
```python
# graph_walker.py: filter_exploded_callgraph_branch()
# Эвристики (применяются последовательно):
template_metrics = [
lambda x: len(x.consumed_from_types), # 1. Максимум потреблённых типов
lambda x: x.consumed_cumsum, # 2. Максимум кумулятивного потребления
lambda x: -x.invokes, # 3. Минимум вызовов
]
# Если после фильтрации >1 варианта:
if len(variants) > 1:
# Сортировка по имени функции (недетерминировано!)
variants.sort(key=lambda x: universal_qualname(x.injector.fn))
```
### 2.2. Проблемы текущей реализации
| Проблема | Описание | Влияние |
|----------|----------|---------|
| **P1. Недетерминированная сортировка** | `universal_qualname()` не гарантирует порядок | Разные результаты на разных машинах |
| **P2. Эвристики не семантические** | Выбор по метрикам графа, не по логике | Может выбрать "неправильный" путь |
| **P3. Комбинаторный взрыв** | `explode_callgraph_branches()` генерирует все варианты | O(n!) сложность |
| **P4. Нет кэширования** | Граф пересчитывается каждый раз | Усугубляет P3 |
| **P5. Нет явного приоритета** | Все инжекторы равны | Невозможно указать "предпочтительный" путь |
### 2.3. Статистика (из тестов)
```
test_performance_many_injectors: 20 инжекторов → ~0.5 сек
test_performance_many_injectors: 50 инжекторов → TIMEOUT (комбинаторный взрыв)
```
---
## 3. Варианты решений
### Вариант 1: Явные приоритеты инжекторов
#### Описание
Добавить параметр `priority` к `mark_injector()`. При выборе пути предпочитать инжекторы с высшим приоритетом.
#### Реализация
```python
# Использование
@repo.mark_injector(priority=10) # Высокий приоритет
def int_to_a_preferred(i: int) -> A:
return A(i * 10)
@repo.mark_injector(priority=1) # Низкий приоритет
def int_to_a_fallback(i: int) -> A:
return A(i + 100)
# Изменения в моделях
@dataclass(frozen=True)
class ConversionPoint:
fn: Callable
injects: type
# ...
priority: int = 0 # Новый параметр
# Изменения в graph_walker.py
def filter_exploded_callgraph_branch(variants, priority_injectors=None):
# Сортировка по приоритету
variants.sort(key=lambda x: -x.injector.priority)
```
#### Сильные стороны
| + | Описание |
|---|----------|
| **Контроль** | Разработчик явно указывает предпочтения |
| **Детерминизм** | Приоритеты дают однозначный выбор |
| **Гибкость** | Можно менять приоритеты без изменения кода |
| **Обратная совместимость** | `priority=0` по умолчанию не ломает существующий код |
#### Слабые стороны
| - | Описание |
|---|----------|
| **Сложность API** | Новый параметр для изучения |
| **Конфликты приоритетов** | Одинаковые приоритеты → снова недетерминизм |
| **Не решает комбинаторный взрыв** | Всё ещё генерируются все варианты |
| **Субъективность** | Приоритеты могут быть произвольными |
#### Оценка сложности
- **Код**: ~50 строк изменений
- **Тесты**: ~10 новых тестов
- **Риск**: Низкий
---
### Вариант 2: Именованные пути (Named Paths)
#### Описание
Разработчик явно именовывает пути преобразования и выбирает их по имени.
#### Реализация
```python
# Регистрация именованных путей
@repo.mark_path(name="multiply")
def int_to_a_mult(i: int) -> A:
return A(i * 10)
@repo.mark_path(name="add")
def int_to_a_add(i: int) -> A:
return A(i + 100)
# Выбор пути при использовании
fn = repo.get_conversion(
(int,),
consumer,
path_preference="multiply" # Явный выбор пути
)
# Или глобальная настройка
repo.set_path_preference("multiply")
```
#### Сильные стороны
| + | Описание |
|---|----------|
| **Полный контроль** | Разработчик всегда выбирает путь |
| **Читаемость** | Код явно показывает какой путь используется |
| **Документированность** | Имена путей служат документацией |
| **Тестируемость** | Можно тестировать разные пути явно |
#### Слабые стороны
| - | Описание |
|---|----------|
| **Бойлерплейт** | Нужно именовать каждый путь |
| **Сложность** | Управление именами в больших проектах |
| **Конфликты имён** | Нужна проверка уникальности |
| **Не решает комбинаторный взрыв** | Генерация всех вариантов остаётся |
#### Оценка сложности
- **Код**: ~150 строк изменений
- **Тесты**: ~15 новых тестов
- **Риск**: Средний
---
### Вариант 3: Стратегии выбора пути (Path Selection Strategies)
#### Описание
Встроенные стратегии выбора пути с возможностью расширения.
#### Реализация
```python
from breakshaft import PathStrategy
# Встроенные стратегии:
# - SHORTEST: кратчайший путь (минимум вызовов)
# - LONGEST: длиннейший путь (максимум преобразований)
# - FIRST: первый найденный (быстро, недетерминировано)
# - EXPLICIT: только явные пути (ошибка если несколько)
# - CUSTOM: пользовательская функция
fn = repo.get_conversion(
(int,),
consumer,
path_strategy=PathStrategy.SHORTEST # или PathStrategy.LONGEST
)
# Пользовательская стратегия
def my_strategy(paths: list[Path]) -> Path:
# Логика выбора
return min(paths, key=lambda p: p.complexity)
fn = repo.get_conversion(
(int,),
consumer,
path_strategy=PathStrategy.CUSTOM(my_strategy)
)
```
#### Сильные стороны
| + | Описание |
|---|----------|
| **Гибкость** | Разные стратегии для разных случаев |
| **Расширяемость** | Пользовательские стратегии |
| **Явность** | Стратегия видна в коде вызова |
| **Переиспользование** | Стратегии можно переиспользовать |
#### Слабые стороны
| - | Описание |
|---|----------|
| **Сложность API** | 5+ стратегий для изучения |
| **Не решает комбинаторный взрыв** | Стратегия применяется после генерации |
| **Производительность** | Некоторые стратегии дорогие |
#### Оценка сложности
- **Код**: ~200 строк изменений
- **Тесты**: ~20 новых тестов
- **Риск**: Средний
---
### Вариант 4: Ограничение глубины графа (Depth Limiting)
#### Описание
Ограничить максимальную глубину/сложность графа преобразований.
#### Реализация
```python
repo = ConvRepo(
max_depth=5, # Максимум 5 преобразований в цепочке
max_branches=10, # Максимум ветвей в узле
max_total_paths=100 # Максимум путей для рассмотрения
)
# Или при вызове
fn = repo.get_conversion(
(int,),
consumer,
max_depth=3 # Переопределение для конкретного вызова
)
```
#### Сильные стороны
| + | Описание |
|---|----------|
| **Защита от взрыва** | Гарантированная верхняя граница сложности |
| **Производительность** | Предсказуемое время выполнения |
| **Простота** | Один параметр для настройки |
#### Слабые стороны
| - | Описание |
|---|----------|
| **Ограничения** | Может отсечь валидные пути |
| **Не детерминизм** | Не решает проблему выбора пути |
| **Настройка** | Нужно подбирать значения |
#### Оценка сложности
- **Код**: ~80 строк изменений
- **Тесты**: ~8 новых тестов
- **Риск**: Низкий
---
### Вариант 5: Кэширование графов (Graph Caching)
#### Описание
Кэшировать построенные графы преобразований для повторного использования.
#### Реализация
```python
from breakshaft import LRUCache, PersistentCache
# Кэш в памяти (LRU)
repo = ConvRepo(cache=LRUCache(max_size=1000))
# Персистентный кэш (на диске)
repo = ConvRepo(cache=PersistentCache(path=".breakshaft_cache"))
# Декоратор для кэширования
@repo.cached
def get_converter(from_types, to_type):
return repo.get_conversion(from_types, to_type)
```
#### Сильные стороны
| + | Описание |
|---|----------|
| **Производительность** | Повторные вызовы мгновенные |
| **Масштабируемость** | Работает с большим числом инжекторов |
| **Прозрачность** | Кэш прозрачен для пользователя |
#### Слабые стороны
| - | Описание |
|---|----------|
| **Память** | Кэш потребляет память |
| **Инвалидация** | Сложность при изменении инжекторов |
| **Не решает выбор пути** | Кэширует выбранный путь, но не детерминирует выбор |
#### Оценка сложности
- **Код**: ~250 строк изменений
- **Тесты**: ~25 новых тестов
- **Риск**: Высокий (состояние, гонки)
---
### Вариант 6: Статический анализ графа (Static Graph Analysis)
#### Описание
Анализировать граф на этапе регистрации инжекторов, обнаруживать проблемы заранее.
#### Реализация
```python
# Предварительный анализ
repo = ConvRepo(validate_on_register=True)
@repo.mark_injector()
def int_to_a_v1(i: int) -> A:
return A(i * 10)
@repo.mark_injector()
def int_to_a_v2(i: int) -> A:
return A(i + 100) # Warning: Ambiguous path detected!
# Явная валидация
warnings = repo.validate()
for w in warnings:
print(f"Warning: {w}")
# Warning: Ambiguous path for A: 2 injectors found
# Разрешение конфликта
repo.resolve_ambiguity(A, preferred=int_to_a_v1)
```
#### Сильные стороны
| + | Описание |
|---|----------|
| **Раннее обнаружение** | Ошибки на этапе регистрации |
| **Документированность** | Явное разрешение конфликтов |
| **Безопасность** | Невозможно создать неоднозначность |
#### Слабые стороны
| - | Описание |
|---|----------|
| **Сложность** | Анализ графа дорог |
| **Жёсткость** | Может быть слишком ограничительно |
| **Не решает комбинаторный взрыв** | Анализ добавляет overhead |
#### Оценка сложности
- **Код**: ~300 строк изменений
- **Тесты**: ~30 новых тестов
- **Риск**: Высокий
---
### Вариант 7: Версионирование путей (Path Versioning)
#### Описание
Каждый путь имеет версию, можно выбирать конкретную версию.
#### Реализация
```python
@repo.mark_injector(version="1.0")
def int_to_a(i: int) -> A:
return A(i * 10)
@repo.mark_injector(version="2.0") # Новая версия
def int_to_a(i: int) -> A:
return A(i + 100)
# Выбор версии
fn = repo.get_conversion(
(int,),
consumer,
path_version="1.0" # Использовать старую версию
)
# Или диапазон
fn = repo.get_conversion(
(int,),
consumer,
path_version=">=1.0,<3.0"
)
```
#### Сильные стороны
| + | Описание |
|---|----------|
| **Эволюция** | Плавный переход между версиями |
| **Совместимость** | Старый код продолжает работать |
| **Контроль** | Явный выбор версии |
#### Слабые стороны
| - | Описание |
|---|----------|
| **Сложность** | Управление версиями |
| **Бойлерплейт** | Версии для каждого пути |
| **Не решает комбинаторный взрыв** | Все версии генерируются |
#### Оценка сложности
- **Код**: ~200 строк изменений
- **Тесты**: ~20 новых тестов
- **Риск**: Средний
---
### Вариант 8: Комбинированный подход (Hybrid Solution)
#### Описание
Комбинация нескольких подходов для максимального эффекта.
#### Реализация
```python
repo = ConvRepo(
# Приоритеты по умолчанию
default_priority=0,
# Кэширование
cache=LRUCache(max_size=500),
# Ограничения
max_depth=10,
max_total_paths=1000,
# Стратегия по умолчанию
default_strategy=PathStrategy.SHORTEST,
# Валидация
validate_on_register=True,
)
# Гибкое переопределение
fn = repo.get_conversion(
(int,),
consumer,
priority_override={int_to_a_v1: 10}, # Приоритет для конкретных
strategy=PathStrategy.EXPLICIT,
cache_key="my_custom_key"
)
```
#### Сильные стороны
| + | Описание |
|---|----------|
| **Максимальная гибкость** | Все инструменты доступны |
| **Масштабируемость** | Работает с большими графами |
| **Контроль** | Полный контроль над поведением |
#### Слабые стороны
| - | Описание |
|---|----------|
| **Сложность** | Много параметров для настройки |
| **Обучение** | Крутая кривая обучения |
| **Риск ошибок** | Неправильная конфигурация |
#### Оценка сложности
- **Код**: ~500 строк изменений
- **Тесты**: ~50 новых тестов
- **Риск**: Высокий
---
### Вариант 9: Декларативное описание графа (Declarative Graph)
#### Описание
Полностью декларативное описание путей преобразования вместо автоматического вывода.
#### Реализация
```python
# Декларативное описание
repo.define_graph({
"paths": [
{
"name": "multiply_path",
"steps": [
{"from": int, "to": A, "using": int_to_a_mult},
{"from": A, "to": B, "using": a_to_b},
],
"priority": 10
},
{
"name": "add_path",
"steps": [
{"from": int, "to": A, "using": int_to_a_add},
],
"priority": 1
}
]
})
# Выбор пути по имени
fn = repo.get_conversion(
(int,),
consumer,
path_name="multiply_path"
)
```
#### Сильные стороны
| + | Описание |
|---|----------|
| **Полный контроль** | Явное описание всех путей |
| **Детерминизм** | Никакой неявной логики |
| **Документированность** | Граф виден в коде |
| **Нет комбинаторного взрыва** | Только явные пути |
#### Слабые стороны
| - | Описание |
|---|----------|
| **Бойлерплейт** | Много кода для описания |
| **Потеря автоматизма** | Нет автоматического вывода путей |
| **Сложность поддержки** | Изменение графа требует правки описания |
#### Оценка сложности
- **Код**: ~400 строк изменений
- **Тесты**: ~40 новых тестов
- **Риск**: Высокий (меняет парадигму)
---
### Вариант 10: Машинное обучение для выбора пути (ML-Based Selection)
#### Описание
Использовать ML для предсказания "лучшего" пути на основе истории использования.
#### Реализация
```python
from breakshaft import MLPathSelector
repo = ConvRepo(
path_selector=MLPathSelector(
training_data="usage_history.json",
features=["execution_time", "memory_usage", "success_rate"]
)
)
# ML выбирает путь на основе:
# - Истории успешных выполнений
# - Времени выполнения
# - Потребления памяти
# - Контекста (типы, размеры данных)
fn = repo.get_conversion((int,), consumer)
# Путь выбирается автоматически на основе модели
```
#### Сильные стороны
| + | Описание |
|---|----------|
| **Адаптивность** | Учится на использовании |
| **Оптимизация** | Выбирает эффективные пути |
| **Автоматизм** | Не требует ручной настройки |
#### Слабые стороны
| - | Описание |
|---|----------|
| **Сложность** | ML модель + обучение |
| **Непредсказуемость** | ML может выбрать неожиданно |
| **Зависимость от данных** | Нужна история для обучения |
| **Overhead** | Предсказание модели |
#### Оценка сложности
- **Код**: ~600 строк изменений
- **Тесты**: ~60 новых тестов
- **Риск**: Очень высокий
---
## 4. Сравнительная таблица
| Вариант | Детерминизм | Производительность | Сложность | Обратная совместимость | Риск |
|---------|-------------|-------------------|-----------|----------------------|------|
| **1. Приоритеты** | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐ | ⭐⭐⭐⭐⭐ | Низкий |
| **2. Именованные пути** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | Средний |
| **3. Стратегии** | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | Средний |
| **4. Ограничение глубины** | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐ | ⭐⭐⭐⭐⭐ | Низкий |
| **5. Кэширование** | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | Высокий |
| **6. Статический анализ** | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | Высокий |
| **7. Версионирование** | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | Средний |
| **8. Комбинированный** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | Высокий |
| **9. Декларативный** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ | Высокий |
| **10. ML** | ⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | Очень высокий |
### Легенда:
- ⭐⭐⭐⭐⭐ — Отлично
- ⭐⭐⭐⭐ — Хорошо
- ⭐⭐⭐ — Удовлетворительно
- ⭐⭐ — Плохо
- ⭐ — Очень плохо
---
## 5. Рекомендации
### 5.1. Краткосрочные решения (быстрая победа)
**Вариант 1 + Вариант 4**: Приоритеты + Ограничение глубины
```python
# Минимальные изменения для детерминизма
@repo.mark_injector(priority=10)
def preferred_converter(...): ...
repo = ConvRepo(max_depth=10)
```
**Преимущества:**
- ~130 строк кода
- Низкий риск
- Обратная совместимость
- Решает 80% проблем
### 5.2. Среднесрочные решения (баланс)
**Вариант 3 + Вариант 5**: Стратегии + Кэширование
```python
repo = ConvRepo(
cache=LRUCache(1000),
default_strategy=PathStrategy.SHORTEST
)
```
**Преимущества:**
- Хорошая производительность
- Гибкость для пользователей
- Решает проблему комбинаторного взрыва
### 5.3. Долгосрочные решения (полное решение)
**Вариант 8 (Комбинированный) с элементами Варианта 9**
```python
repo = HybridConvRepo(
priorities=True,
strategies=True,
cache=True,
validation=True,
declarative_mode=False # Опционально
)
```
**Преимущества:**
- Полное решение проблемы
- Масштабируемость
- Гибкость
### 5.4. Дорожная карта
```
Фаза 1 (2 недели):
├── Приоритеты инжекторов
├── Ограничение глубины
└── Тесты
Фаза 2 (4 недели):
├── Стратегии выбора пути
├── Базовое кэширование
└── Документация
Фаза 3 (6 недель):
├── Статический анализ
├── Продвинутое кэширование
├── Декларативный режим (опционально)
└── Полное тестирование
```
---
## 6. Заключение
### 6.1. Выводы
1. **Нет серебряной пули** — каждый вариант имеет компромиссы
2. **Комбинированный подход** даёт лучший результат
3. **Начинать с простого** — приоритеты + ограничения
4. **Итеративное улучшение** — добавлять функции постепенно
### 6.2. Риски
| Риск | Вероятность | Влияние | Митигация |
|------|-------------|---------|-----------|
| Ломает обратную совместимость | Низкая | Высокое | Поэтапное внедрение |
| Усложнение API | Средняя | Среднее | Хорошая документация |
| Производительность | Низкая | Высокое | Бенчмарки на каждом этапе |
| Комбинаторный взрыв | Средняя | Высокое | Ограничения + кэш |
### 6.3. Следующие шаги
1. **Выбрать подход** для Фазы 1
2. **Создать PR** с приоритетами и ограничениями
3. **Собрать фидбэк** от пользователей
4. **Итеративно улучшать**
---
*Документ создан для breakshaft v0.1.6*
*Дата: 2026-03-28*