Skip to content

RFC-009a: Анализ времени жизни токенов — на основе конвейера доказательств Хора

Родительский RFC: RFC-009: Проектирование модели владения

Зависимость: RFC-027: Предикаты времени компиляции и унифицированная статическая верификация

Предварительное условие: RFC-027 уже принят. Все механизмы данного RFC (конвейер доказательств, SMT fallback, сборpath_condition) зависят от реализации RFC-027.

Данный RFC исправляет и заменяет RFC-009 §"Обнаружение конфликтов токенов: потоково-чувствительный анализ активности" (строки 663-684).

Аннотация

Строка 684 RFC-009 утверждает, что обнаружение конфликтов токенов "не требует... NLL". Вывод правильный, аргументация ошибочна.

Дело не в том, что "токены — это значения, достаточно линейного отслеживания". Дело в том: жизнеспособность токена — это пропозиция логики Хора, а не специальный потоково-чувствительный анализ.

{все_конфликтующие_токены_мертвы} op {WriteToken безопасно получен} — одна и та же форма {P} op {Q}, общая для проверки типов, верификации предикатов и использующая конвейер доказательств RFC-027. Без новой аналитической структуры. Один конвейер, множество пропозиций.


Мотивация

Путаница в RFC-009

RFC-009 смешивает две проблемы:

  1. Линейное отслеживание (после перемещения недоступно) — {v не перемещён} use(v) {тип совпадает}. Имеется в типоchecker.
  2. Взаимодействие времени жизни токенов (дочерний токен жив → родительский приостановлен → дочерний умер → родительский возрождён) — {все_конфликтующие_токены_мертвы} write(data) {безопасно}. Требуется анализ жизнеспособности, не линейное отслеживание.

Текущее состояние кодовой базы

КомпонентСостояние
BorrowCheckerЛинейное сканирование IR, пассивная реакция на явные инструкции Borrow/Release
ControlFlowAnalyzer::analyze_instructionПустая реализация (control_flow.rs:145-153)
liveness_analysisСуществует, но используется только для вставки Drop, не подключен к конфликтам токенов
Вставка ReleaseЖёстко закодирована после инструкции Call — чистая лексическая область видимости (ir_gen.rs:2734-2736)

Видимые пользователю последствия:

yaoxiang
data = vec![1, 2, 3]
view = &data              # Создание ReadToken
x = view.total_count      # Последнее использование view
data.push(4)              # ❌ Release(view) ещё не выполнен, ReadToken "жив"

Почему нужен пересмотр

Предыдущая версия (009a v1) использовала нарратив "DAG вместо NLL", что внесло ненужные новые концепции (консервативные правила ветвления, специальная обработка циклов). Основное противоречие не было чётко сформулировано: проверка заимствований — не独立ная система — это разновидность пропозиций Хора.


Основной дизайн

Всё есть Хор

Проверка типов:    { x: Int }        x + 1        { result: Int }
Проверка заимствований: { view мёртв } data.push(4) { WriteToken успешно получен }
Верификация предикатов: { y > 0 }   divide(x, y) { result: Int }
Разрыв обратных рёбер: { i == n }  следующий цикл  { cond == false }

Та же самая форма {P} op {Q}. Компилятор генерирует предусловие P для каждой операции и отправляет в конвейер доказательств.

Проверка заимствований и пользовательские предикаты используют один и тот же конвейер. Разница только в том, кто генерирует пропозиции и что делать при невозможности доказательства.

Два класса предикатов, один конвейер

Пользовательские предикатыСистемные предикаты (заимствования)
ГенерацияПрограммист (аннотации типов)Компилятор (дерево брендов + правила владения)
Предоставление доказательстваКомпилятор + программистПолностью автоматическое
Невозможность доказательстваПисать доказатель или рефакторитьРефакторинг кода (дверь открыта, но редко нужна)
ВидимостьВидна в сигнатуреНеявная, не загрязняет сигнатуру
Порог вхожденияИзучаешь по желаниюНоль

Доказательство системных предикатов не открывает программисту возможность писать доказательства — компилятор полностью автоматичен. При невозможности доказательства пользователь делает рефакторинг кода.

Три режима отказа, один верификационный движок. Пропозиция типа не доказана → ошибка компиляции (необходимо исправить). Пропозиция заимствования не доказана → ошибка компиляции, рефакторинг кода (необходимо исправить). Пользовательский предикат не доказана → ошибка компиляции, можно писать доказательную функцию (можно обойти). Разные стратегии отказа, но один верификационный движок — SMT решатель + правила вывода ядра компилятора. Разница только в том, "кто отвечает за дополнение доказательства при невозможности доказательства" — компилятор отказывается писать доказательства заимствований за программиста (стратегия доказательства для пропозиций заимствований — структурный анализ + SMT, не требует вмешательства программиста), но принимает написанные программистом пользовательские доказательные функции. Это не несоответствие конвейера — это разные границы ответственности для разных классов пропозиций.

Это отличается от Rust 'a: 'a — обязательный курс, доказательные функции — факультатив — подавляющее большинство пользователей никогда не столкнутся с факультативом.

Пропозиции заимствований: автоматическая генерация компилятором

Пользователь пишет data.push(4). Компилятор автоматически генерирует пропозицию:

WriteToken(data, node) доступен
  ≡ forall t in conflicting_tokens(data): t мёртв в node
  ≡ forall t in brand_tree.children(data): forward_reachable(node) ∩ consumers(t) == ∅

Три правила, ноль исключений:

  1. Дерево брендов (RFC-009 §2.7) отвечает на вопрос "кто с кем конфликтует": префиксное сопоставление, O(depth), глубина ≤ 3
  2. Список потребителей (собирается автоматически при построении DAG) отвечает на вопрос "кто последним использовал токен"
  3. Прямая достижимость отвечает на вопрос "сможет ли потребитель выполниться": структурный разрыв + логический разрыв

Прямая достижимость: обратный обход от потребителей

Для каждого потребителя C токена T:

Из C, обратный BFS по DAG.
Ребро разрывается, если:
  1. Это break (структурный разрыв)
  2. path_condition ⇒ !loop_cond доказано СМТ истинным (логический разрыв, конвейер RFC-027)

Обратное распространение жизнеспособности по всем неразорванным рёбрам (включая обратные рёбра — они распространяют жизнеспособность в предыдущую итерацию).
Пометка всех достижимых узлов → unsafe.

Запрос: операция записи в узле W → W ∉ unsafe → безопасно.

Не нужно изобретать "консервативные правила ветвления". Не нужно "консервативную жизнеспособность в циклах". Один обратный BFS + два правила разрыва.

Стратегия доказательства: быстрый путь приоритетен, СМТ как fallback

Каждая операция записи, требующая токена

  ├→ Быстрый путь: структурный анализ DAG (покрывает 95%+ случаев)
  │     │
  │     ├→ Префиксное сопоставление в дереве брендов → найти конфликтующие токены (O(depth))
  │     ├→ Обратный BFS, break разрывает обратные рёбра
  │     └→ Нет проходимых обратных рёбер → прямое решение Proved / Disproved

  └→ Медленный путь: СМТ логический разрыв (только когда быстрый путь встречает проходимое обратное ребро)

        ├→ У обратного ребра есть path_condition → СМТ проверяет path_cond ⇒ !loop_cond
        │     ├→ Proved → логический разрыв → понижение до быстрого пути, продолжить
        │     └→ Disproved / Unproven → обратное ребро проходимо → пометка unsafe

        └→ У обратного ребра нет path_condition → обратное ребро прямо проходимо

Покрытие быстрого пути: линейный код, if/else, loop + break, while без path_condition. Покрытие медленного пути: тело цикла while, когда есть path_condition, подразумевающее выход из цикла. Не покрыто: условия времени выполнения, которые невозможно доказать статически → обратное ребро проходимо → unsafe → ошибка компиляции (рефакторинг пользователем).

СМТ не главный — это страховочная сеть. В отличие от пользовательских предикатов RFC-027, где СМТ — главный; в системе заимствований главный — структурный анализ, СМТ только для углов, недоступных структурному анализу.


Анализ вариантов использования

Линейный код

yaoxiang
data = vec![1, 2, 3]        # Узел 1
view = &data                # Узел 2: потребляет data, производит ReadToken(#1)
x = view.total_count        # Узел 3: потребляет view (последний потребитель #1)
data.push(4)                # Узел 4: требует WriteToken(data)

Обратный BFS от view.total_count (узел 3) → узел 3 — последний потребитель #1 → узел 4 > узел 3 → узел 4 не в unsafe → ✅

if/else: без специальных правил

yaoxiang
view = &data
if cond {
    use(view)               # then-ветвь потребляет view
} else {
    do_something_else()     # не трогает view
}
data.push(4)                # Последний потребитель view внутри if → после if потребителей нет → ✅

if/else — составной узел DAG. Внутреннее потребление атрибутируется этому узлу. Без слияния состояний ветвей. Без консервативного голосования. Есть ли потребители дальше — простое сравнение целых.

if/else с возвратом значения и экранированием

yaoxiang
view = &data
result = if cond {
    view                     # view экранируется в result
} else {
    something_else
}
use(result)                  # Косвенное потребление view
data.push(4)                 # view всё ещё имеет потребителя (use(result))
                             # → push в unsafe → ❌ Корректная ошибка

view экранируется через возвращаемое значение → use(result) — потребитель view → обратный обход от push достигает use(result) → unsafe.

Цикл: break разрывает обратное ребро

yaoxiang
view = &data
loop {
    use(view)                # Потребитель
    if is_last {
        data.push(4)         # Операция записи
        break                # ← Структурный разрыв
    }
}

Обратный BFS от use(view) → обратное ребро → вперёд до data.push(4) → встречает breakРазрывdata.push(4) не в unsafe → ✅

Без break:

yaoxiang
view = &data
loop {
    use(view)
    data.push(4)             # Нет break → обратное ребро проходимо → use(view) следующей итерации достижимо
                             # → push в unsafe → ❌ Корректная ошибка
}

while: СМТ логический разрыв

yaoxiang
view = &data
mut i: UpTo(n) = 0
while i < n {
    use(view)                # Потребитель
    i += 1
    if i == n {
        data.push(4)         # path_condition: i == n
    }
}

Обратный BFS от use(view) → обратное ребро → до data.push(4) → проверка path_condition i == n → СМТ запрос: i == n ⇒ !(i < n)? → Proved → Логический разрывdata.push(4) не в unsafe → ✅


Суть: Brand ID — это и есть 'a

Не говорим "нам не нужен 'a". Говорим "#42 — это и есть '42".

RustYaoXiangЭквивалентность
'a#42Времяной идентификатор времени жизни
'a: 'b ограничение lifespan#42 — префикс #42.field_xСравнение строковых префиксов = частичный порядок
NLL жизнеспособность (неподвижная точка CFG)Обратный BFS (DAG)Оба — вычисления достижимости
Факты PoloniusСМТ логический разрывОба — вывод по path_condition
Решение ограничений (неподвижная точка)Brand tree префикс + BFSРазличные кодировки, одна задача

Мы не изобрели новый анализ. Мы просто опустили 'a с уровня сигнатур типов на уровень доказательств. Brand ID делает ровно то же, что 'a — помечает идентичность заимствования, отслеживает производные отношения, определяет конфликты. Единственное отличие: 'a в пользовательских сигнатурах типов; #42 внутри компилятора.

Это не позор. Curry-Howard говорит: типы — это пропозиции, программы — это доказательства. 'a не часть пропозиции — это часть стратегии доказательства. Rust записал стратегию доказательства в сигнатуру пропозиции. Мы вернули её туда, где ей место.

Какие сложности устранены языковыми ограничениями

Источник сложностиУстранено?Причина
Затенение переменныхЯзык запрещает — имя всегда ссылается на одно и то же
Заимствование в for через итерацииКаждая итерация — новая привязка — изоляция между итерациями
Аннотации времени жизни 'aBrand path = #42.field_x, выводится компилятором
Именованное время жизни + распространение ограниченийСравнение префиксов brand path заменяет явные ограничения
Решение ограничений borrow-графа (Polonius)Префиксное сопоставление в дереве брендов + запросы потребителей DAG
Жизнеспособность заимствований в теле циклаКак в Rust — требует обработки через обратный BFS + логический разрыв
Консервативность условных ветвейКак в Rust — СМТ покрывает доказуемое, остальное консервативно отклоняется

Почему DAG возможен

Три языковых ограничения YaoXiang делают анализ DAG возможным:

  • Нет затенения переменных — имя всегда ссылается на одно и то же, не нужно отслеживать через перепривязки
  • for создаёт новую привязку каждую итерацию — естественная изоляция между итерациями, нет межитерационных заимствований
  • Структурированный параллелизм — чёткие границы задач, не нужно распространять жизнеспособность между задачами

Эти ограничения устраняют основной источник сложности итерации неподвижной точки CFG в Rust. DAG не "лучше" CFG — более простой дизайн языка позволяет более простой анализ.


Детальный дизайн

Каталог системных предикатов

Компилятор автоматически генерирует следующие пропозиции и отправляет в конвейер доказательств RFC-027:

Системный предикатТриггерФорма пропозиции
borrow_conflictТребуется WriteToken(v)forall t ∈ conflicting(v): dead_at(t, node)
use_after_moveИспользование переменной v¬moved(v)
use_after_dropИспользование переменной v¬dropped(v)
double_dropDrop(v)¬dropped(v)
mut_violationЗапись в неизменяемую переменную vis_mut(v)

Существующие BorrowChecker, MoveChecker, DropChecker, MutChecker становятся генераторами пропозиций — не исчезают, меняют роль. Они генерируют пропозиции, конвейер верифицирует.

Дерево брендов

Механизм брендов из RFC-009 §2.7 формализован как дерево брендов.

Семантика токенов — заморозка приоритетна, не копирование:

Суть различия &T и &mut T не в "можно ли копировать", а в "допускается ли одновременная запись":

ReadToken(T): Предоставляет права только на чтение, одновременно замораживая данные T — любой WriteToken(T) в этот период
              недоступен. Заморозка — первичная семантика ReadToken. Dup (копируемость) — следствие заморозки:
              поскольку данные заморожены (мутация невозможна), несколько представлений только для чтения естественно безопасны.

WriteToken(T): Предоставляет эксклюзивные права на чтение/запись. Поскольку возможна запись, никакой другой токен
              (чтение или запись) не может сосуществовать.
              Не реализуемый Dup (линейный тип) — следствие эксклюзивности.

Причинно-следственная связь:

ReadToken существует → Данные заморожены → Несколько представлений чтения безопасны → Dup

              WriteToken отклонён (системный предикат borrow_conflict принудительно)

Не:

У ReadToken есть Dup → Может быть несколько → Проверка конфликтов "между делом"  ← Причинность перевёрнута
BrandTree:
  nodes: Map<BrandId, BrandNode>

BrandNode:
  id: BrandId               # "#42", "#42.field_x"
  kind: ReadToken | WriteToken
  source_var: Operand
  parent: Option<BrandId>   # Родительский узел производного отношения
  children: Set<BrandId>    # Производные токены
  consumers: Set<NodeId>    # Узлы DAG, потребляющие этот токен
  ref_count: usize          # Счётчик безопасных копий во время заморозки ReadToken

Определение конфликта — механизм обеспечения заморозки:

rust
fn conflicts(a: &BrandId, b: &BrandId) -> bool {
    // Условие конфликта: общий источник + хотя бы один записывающий + brand path пересекаются
    // Это означает:
    //   1. ReadToken vs ReadToken → Нет конфликта (оба только читают, мутации нет)
    //   2. WriteToken vs ReadToken → Конфликт (запись нарушает гарантию заморозки чтения)
    //   3. WriteToken vs WriteToken → Конфликт (две записи не могут сосуществовать)
    a.source() == b.source()
        && (a.is_write() || b.is_write())
        && (a.is_prefix_of(b) || b.is_prefix_of(a))
}

Сравнение строковых префиксов O(depth), глубина ≤ 3. Константный уровень.

Анализ жизнеспособности обратным BFS

Алгоритм: check_borrow(token, node, dag, brand_tree)

Вход:
  token: WriteToken для проверки
  node:  Узел DAG, где находится операция записи

Выход: Proved | Disproved

Алгоритм:
  # Быстрый путь: обратный BFS
  unsafe = empty_set
  queue = brand_tree.consumers(token)

  while queue not empty:
    cur = queue.pop()
    unsafe.add(cur)

    for each pred in dag.predecessors(cur):
      # Структурный разрыв: break непроходим
      if pred — ребро break:
        continue

      # Обратное ребро → проверка необходимости SMT fallback
      if pred — обратное ребро:
        path_cond = path_condition в pred
        loop_cond = условие цикла
        # Сначала структурная возможность разрыва (соответствующий break уже разорвал путь → сюда не дойдёт)
        # Затем path_condition
        if path_cond не пусто:
          result = smt_fallback(path_cond, loop_cond)   # ← Медленный путь
          if result == Proved:
            continue                    # Логический разрыв
        # Нет path_condition или SMT не доказало → обратное ребро проходимо
        # fall through

      if pred ∉ unsafe:
        queue.push(pred)

  # Решение
  if node ∈ unsafe:
    return Disproved
  else:
    return Proved


smt_fallback(path_cond, loop_cond):
  # Вызывается только при обратном ребре + есть path_condition
  # Использует конвейер доказательств RFC-027, общий SMT решатель, общий бюджет
  return smt.prove(path_cond ⇒ !loop_cond)
  # Proved → Логический разрыв
  # Disproved / Unproven → Не разрывать, обратное ребро проходимо

O(N), где количество вызовов СМТ = количество обратных рёбер × доля обратных рёбер с path_condition. В реальном коде вызовы СМТ крайне редки — только в теле цикла while с уточнёнными типами переменных.

Сбор path_condition

Обеспечивается существующими механизмами RFC-027 §3.2-3.3:

  • if guard: if y > 0 → в true-ветвь добавляется y > 0
  • match pattern: if let Some(v) = opt → в ветви добавляется opt == Some(v)
  • Присваивание: i += 1, компилятор поддерживает информацию о домене переменной
  • while cond: в теле цикла добавляется cond == true

Каждый узел DAG несёт набор path_condition. При обратном BFS через обратное ребро берётся path_condition начала обратного ребра, СМТ определяет, исключает ли оно условие входа в следующую итерацию.

Интерфейс с RFC-027

Системные предикаты заимствований и пользовательские предикаты используют один конвейер доказательств — разница в главной стратегии доказательства:

Тип запросаИсточник пропозицииГлавная стратегияFallback
Типовое равенствоТипоcheckerСтруктурная эквивалентность
Пользовательские предикатыАннотации типов программистаСМТДоказательная функция программиста
Конфликт заимствованийАвтогенерация компиляторомСтруктурный анализ DAG (быстрый путь)СМТ логический разрыв

Роль SMT решателя в проверке заимствований: не главная, страховочная сеть. Вызывается только когда нужно логически разорвать обратное ребро while. Подавляющее большинство проверок заимствований завершается на быстром пути — O(N) обратный BFS, нулевые накладные расходы СМТ.

Отношение с существующим кодом

Существующий компонентОбработка
BorrowCheckerСтановится BorrowPredicateEmitter — генерирует пропозиции заимствований по Хору
MoveCheckerСтановится MovePredicateEmitter — генерирует пропозиции ¬moved(v)
DropCheckerАналогично — генерирует пропозиции Drop
MutCheckerАналогично — генерирует пропозиции is_mut(v)
ControlFlowAnalyzerБольше не нужен — конвейер унифицирован
liveness_analysisСохраняется — для вставки Drop нужна информация о переменных
Жёстко закодированный Release в ir_gen.rsУдаляется — позиция Release определяется анализом потребителей DAG

NLL и границы итераций

Момент смерти токена = точка последнего использования (NLL), не конец лексической области.

Это естественное следствие анализа потребителей: позиция потребителя определяет последнее использование токена. use(v) — потребитель vv умирает сразу после use(v). Не нужны дополнительные {} или drop() для досрочного завершения жизни токена.

Граница итерации цикла — линия смерти копии токена. Три правила:

Правило 1: Переменные, объявленные внутри цикла, автоматически умирают в конце каждой итерации.
           Каждая итерация for — новая привязка (гарантия дизайна языка), аналогично для loop.

Правило 2: ref_count в дереве брендов на заголовке цикла учитывает только копии, созданные вне цикла.
           Новые копии от Dup внутри цикла, ref_count обнуляется на границе итерации.

Правило 3: При обратном BFS через обратное ребро не переносится жизнеспособность текущей итерации.
           Переносится только ref_count в точке заголовка цикла (т.е. копии вне цикла).

Пример:

yaoxiang
view = &data                          # Заголовок цикла: ref_count = 1, consumer = use(view)
loop {
    v2: &Point = view                 # Dup внутри цикла → ref_count = 2
    use(v2)                           # Потребитель: последнее использование v2 → v2 умирает → ref_count = 1
    data.push(4)                      # ✅ Безопасно! v2 мёртв, остался только view (ref_count = 1, не конфликт записи)
    # Граница итерации: Правило 3 — не переносить v2 в следующий раунд. В начале следующей итерации v2 recreируется новой привязкой.
}

Этот дизайн не требует дополнительного "консервативного правила жизнеспособности в циклах". Обратный BFS от потребителя: потребитель в теле цикла → жизнеспособность ограничена текущей итерацией → обратное ребро не проходимо. Полностью согласуется с §Анализ вариантов использования, цикл.

? Распространение ошибок и Release, управляемый областями видимости

? — ранний возврат — дополнительный путь выхода за пределы обычного выхода области видимости. Токен должен быть освобождён на этом пути, неправильный порядок освобождения — UB.

Инструкции Release генерируются анализом области видимости, не жёстко закодированы после Call.

Компилятор поддерживает для каждой области видимости список точек выхода:

  • } (нормальное завершение области видимости)
  • ? (распространение ошибки, ранний return)
  • Явный return

В каждой точке выхода вставляются инструкции Release для всех активных токенов этой области в обратном порядке объявления (LIFO). Иерархия родитель-потомок в дереве брендов автоматически обрабатывает каскадное освобождение производных токенов:

yaoxiang
Point.get_x: (self: &Point) -> (&Float, &Point) = {
    return (&self.x, self)    # Возврат дочернего токена &Float + родительского &Point
}

fn use_case(p: Point) -> Result<(), Error> = {
    (x_ref, p_ref) = p.get_x()?   # При распространении ?:
    # Дерево брендов знает, что x_ref — производная p_ref (#42.field_x — префикс #42)
    # Порядок освобождения: x_ref (потомок) → p_ref (родитель) → LIFO автоматически обеспечен
    p.modify()                     # WriteToken — все ReadToken освобождены
    Ok(())
}

Место реализации: сохраняется в ir_gen.rs, переделывается на управление областями видимости — без нового прохода компилятора.

ОперацияСложностьЧастота триггера
Определение конфликта в дереве брендовO(1)При каждом требовании токена
Запрос потребителей DAGO(1)При каждом требовании токена
Обратный BFS (быстрый путь)O(N)При каждом требовании токена, N = количество узлов в блоке
СМТ логический разрыв (fallback)~1мсКрайне редко — только while + path_condition

Условия триггера SMT fallback крайне строгие: одновременно должны выполняться (1) цикл while (2) операция записи в теле цикла (3) после операции записи есть path_condition, определяющее выход из цикла (4) компилятору нужно это условие для разрыва обратного ребра. В реальном коде доля менее 1%. Остальные проверки заимствований полностью на быстром пути.

Отношение с пользовательскими предикатами RFC-027: пользовательские предикаты используют СМТ как главный инструмент, системные предикаты заимствований используют структурный анализ как главный. Оба используют общий SMT решатель и общий бюджет (RFC-027 §8), но системные предикаты заимствований почти не потребляют бюджет СМТ.

Линейный код → без обратных рёбер → быстрый путь O(N), мгновенно. Цикл + path_condition → вызов СМТ, линейная арифметика миллисекундного уровня (бюджет RFC-027 100мс). Результат одного BFS может кэшироваться для повторных запросов того же токена.

Дизайн сообщений об ошибках

Основной принцип: в сообщении об ошибке встречаются только символы, написанные пользователем.

Ошибки, связанные с Rust и заимствованиями, делятся на два типа:

Ошибки уровня переменной: E0597 (недостаточно долго живёт), E0502 (изменяемое + неизменяемое заимствование одновременно), E0499 (множественные изменяемые заимствования). Rust уже эталон — имя переменной + номер строки, без 'a. Точность YaoXiang на том же уровне. Информация вся в дереве брендов: точка создания токена, позиция потребителя, точка запроса.

Ошибки уровня сигнатуры: E0623 (несовпадение времени жизни), E0106 (отсутствует спецификатор времени жизни), E0477 (не удовлетворяет требуемому времени жизни). Вокруг 'a. В YaoXiang таких ошибок нет — в сигнатуре нет 'a. Не "невозможно сообщить", а "то, что пользователь не писал, не нужно сообщать".

Пример конфликта внутри функции:

Ошибка: `data` заморожен, невозможно получить изменяемые права
 --> src/main.yx:5:9
2 |     view = &data
  |            ----- `data` заморожен (токен чтения создан здесь)
4 |         use(view)
  |             ---- `view` всё ещё используется здесь, заморозка не снята
5 |         data.push(4)
  |         ^^^^ Здесь требуются изменяемые права

(Точность на уровне Rust E0499 — имя переменной + номер строки, без Brand ID.)

Пример межфункционального экранирования:

Ошибка: данные, удерживаемые `num` (строка 4), имеют один из источников `default_str` (строка 3),
но `default_str` становится недействительным в строке 6, а `num` всё ещё используется в строке 5.

Рассмотрите: перенести объявление `default_str` вызывающей стороне или использовать `ref default_str` для совместного удержания.

(Точность на уровне Rust E0597. Brand summary знает, что num имеет два пути источника — уже есть в компиляторе, формулировку ошибки можно адаптировать.)


Исправления текста RFC-009

Раздел RFC-009 §"Обнаружение конфликтов токенов: потоково-чувствительный анализ активности" обновлён:

  1. Удалено "не требуемое: ... NLL" — не потому что вывод неправильный, а потому что обоснование неверное ("токены — значения, достаточно линейного отслеживания")
  2. Стратегия перехода уровень 1/уровень 2 сохранена, полная стратегия ссылается на данный RFC
  3. Чётко указано: Brand ID (#42) — это и есть 'a — информация идентична, кодировка разная. Не новое изобретение — опущение времени жизни с уровня типов на уровень доказательств

Компромиссы

Преимущества

  1. Типовые сигнатуры без времени жизни: #42 — это '42 — та же информация, закодирована в дереве брендов, не в типовой сигнатуре. Невозможно опровергнуть: посчитайте, сколько параметров 'a нужно для generic типа с 3 ссылочными параметрами в Rust, и сколько в YaoXiang. Ответ: 3 против 0.

  2. Концептуальное объединение: проверка заимствований и пользовательские предикаты используют один конвейер доказательств — {P} op {Q}, конвейер верифицирует P. Согласованность Curry-Howard.

  3. Ноль новых аналитических фреймворков: не вводится новый аналитический фреймворк. Пользователь не осознаёт существование "проверки заимствований" — как пользователь не осознаёт детали реализации "типоchecker".

  4. Сообщения об ошибках содержат только написанные пользователем символы: на одно измерение ошибок меньше (E0623, E0106, E0477 — все围绕 'a). Ошибки уровня переменной на точности Rust.

  5. Алгоритм не консервативен: обратный BFS + разрыв break + СМТ логический разрыв. Не нужна "консервативная жизнеспособность в циклах". Не нужна "консервативное слияние ветвей".

Недостатки

  1. Не новое изобретение: Brand ID делает ровно то же, что 'a — сложность решения ограничений внутри компилятора не исчезла, просто кодировка изменилась с "имя переменной + набор ограничений" на "brand path + префиксное сопоставление". Для конечного пользователя разница только в том, что в сигнатуре не пишется 'a.

  2. Полностью новая реализация: дерево брендов существует только как концепция в коде, требуется реализация с нуля. BorrowChecker, ControlFlowAnalyzer заменяются.

  3. Зависимость от СМТ: логический разрыв зависит от Z3 (RFC-027 уже ввёл эту зависимость, не новая). Но проверка заимствований почти не триггерит — только while + path_condition.

  4. Очень редкие паттерны требуют рефакторинга: паттерны, которые компилятор не может автоматически доказать, требуют рефакторинга кода пользователем. В отличие от Rust 'a: у Rust есть 'a как запасной вариант (аннотация = прохождение); запасной вариант YaoXiang (доказательные функции) не в MVP.


Альтернативные решения

РешениеПочему не выбрано
Полная реализация Rust NLLОграничения дизайна YaoXiang (без затенения, for新绑定) уже устранили основную сложность NLL, CFG не требуется
Сохранить текущее (жёсткий Release)Недостаточно — пользователь должен вручную управлять областями видимости токенов
Анализ только в spawn блокахНедостаточно — использование токенов в не-spawn коде — большинство
GC вместо проверки заимствованийПротиворечит принципам дизайна языка — в YaoXiang нет GC

Этапы реализации

ЭтапСодержаниеЗависимость
Phase 1Реализация структуры данных дерева брендов
Phase 2Генераторы системных предикатов (Borrow/Move/Drop/Mut → пропозиции)Phase 1
Phase 3Анализ жизнеспособности обратным BFS + подключение конвейера (уровень 1)Phase 2
Phase 4Сбор path_condition + СМТ логический разрыв (уровень 2)Phase 3 + RFC-027 Phase 2
Phase 5Перевод инструкций Release на управление DAG-потребителямиPhase 3
Phase 6Удаление ControlFlowAnalyzer, рефакторинг BorrowCheckerPhase 4

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

  • [x] Семантика ref_count в дереве брендов при развёртывании цикла — следуем NLL: токен умирает после последнего использования. Копии, созданные в теле цикла, умирают на границе итерации, обратный BFS не переносит межитерационную жизнеспособность. См. §NLL и границы итераций.
  • [x] Порядок освобождения токенов на пути распространения ? — Release управляются анализом области видимости (сохраняется в ir_gen.rs). Каждая точка выхода области (}, ?, явный return) освобождает активные токены в порядке LIFO. Отношения родитель-потомок в дереве брендов автоматически обеспечивают каскадное освобождение. См. §? Распространение ошибок и Release, управляемый областями видимости.
  • [ ] Синтаксис доказательных функций (дальнесрочное, не MVP — не блокирует ни один этап)

Ссылки


Жизненный цикл и судьба

СостояниеРасположениеПримечание
Принятоdocs/design/rfc/accepted/Стало официальным дизайн-документом