Files
breakshaft/COMMUTATIVITY_DESIGN.md
Qwen Code Assistant ca605001b3 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>
2026-03-28 13:42:04 +00:00

738 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Масштабное проектирование: Решение проблемы некоммутативных преобразований в 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*