RFC-009: Проектирование модели владения
Аннотация
В данном документе определяется модель владения (Ownership Model) языка программирования YaoXiang.
Основной дизайн — пять концепций, один градиент:
Взглянуть/Изменить Забрать Общее владение Скопировать Системный уровень
│ │ │ │ │
&T Move ref clone() unsafe
&mut T Нулевое Компилятор Явное *T
Токен копирование автоматически глубокое Пользователь
нулевого По умолчанию выбирает копирование ответственен
размера С RAII Rc/Arc
Типовая
атрибутика
естественно
выводит права- Move (по умолчанию): присваивание/передача параметров/возврат = передача владения, нулевое копирование, автоматическое освобождение через RAII
&T/&mut T(токены заимствования): токены типов времени компиляции нулевого размера.&Tможет копироваться (общее чтение),&mut Tлинеен (монопольная запись). Права естественно выводятся из атрибутов типа, без специальных правил. Можно возвращать, можно хранить в структурах.refключевое слово: обмен данными между областями видимости. Компилятор автоматически выбирает Rc (не между задачами) или Arc (между задачами)clone(): явное глубокое копированиеunsafe+*T: голые указатели, аварийный выход на системный уровень
Устранённая сложность:
- ❌ Нет времени жизни
'a - ❌ Нет отдельной системы проверки заимствований (конфликт заимствований сводится к пропозиции Хоара, совместно с конвейером доказательств проверки типов)
- ❌ Нет GC
- ❌ Нет специальных правил типа «запрет на побег» (токены — обычные типы, области видимости обрабатываются единой системой типов)
- ❌ Пользователю не нужно знать разницу между Rc и Arc (компилятор выбирает автоматически)
Программная нагрузка:
&Tможет копироваться,&mut Tне может копироваться — два атрибута типа, ноль специальных правил, полная автоматизация от компилятора. Гарантия производительности: Move с нулевыми накладными расходами, токены с нулевыми накладными расходами (типы нулевого размера, исчезают после компиляции), ref по требованию, нет пауз GC.
Мотивация
Зачем нужна модель владения?
| Язык | Управление памятью | Проблемы |
|---|---|---|
| C/C++ | Ручное управление | Утечки памяти, висячие указатели, двойное освобождение |
| Java/Python | GC | Задержки, накладные расходы памяти, непредсказуемые паузы |
| Rust | Владение + проверка заимствований | Крутая кривая обучения времени жизни 'a |
| YaoXiang | Move + Token + ref | Просто, детерминировано, без GC |
Цели проектирования
# 1. Move по умолчанию (нулевое копирование)
p = Point(1.0, 2.0)
p2 = p # Move, p больше нельзя читать
# 2. &T / &mut T токены заимствования (нулевые накладные расходы, атрибуты типа естественно выводят права)
print_info(p2) # Компилятор автоматически создаёт токен &Point, освобождает после использования
shift(p2, 1.0, 1.0) # Компилятор автоматически создаёт токен &mut Point
# 3. ref = обмен (компилятор автоматически выбирает Rc/Arc)
shared = ref p2 # Владение между областями видимости
spawn { use(shared) } # Компилятор: между задачами → Arc
# 4. clone() = явное копирование
backup = p2.clone() # Глубокое копирование, независимая копия
# 5. unsafe + *T = системный уровень
unsafe {
ptr: *Point = &p
(*ptr).x = 0.0
}Ключевые отличия от Rust
| Характеристика | Rust | YaoXiang |
|---|---|---|
| Семантика по умолчанию | Заимствование &T (нужно явное .clone()) | Move (передача значений, нулевое копирование) |
| Заимствование | &T/&mut T, можно возвращать, нужны времена жизни | **&T/&mut T токены нулевого размера, атрибуты типа Dup/Linear естественно выводят права |
| Механизм обмена | Arc::new() + ручной Weak | ref ключевое слово (компилятор автоматически выбирает Rc/Arc) |
| Копирование | clone() | clone() |
| Голые указатели | *T | *T |
| Время жизни | 'a | ❌ Нет |
| Проверка заимствований | Глобальный вывод | Генерация пропозиций заимствования автоматическим проверщиком типов, унифицированный конвейер проверки |
| Циклические ссылки | Ручной Weak | Освобождение в конце задачи / lint для межзадачных / стандартный Weak |
Предложение
1. Move (передача владения по умолчанию)
# Правило: присваивание / передача параметров / возврат = Move, нулевое копирование
p: Point = Point(1.0, 2.0)
p2 = p # Move, p больше нельзя читать
# Переменной можно переприсвоить (стиль Python, без затенения)
p = Point(3.0, 4.0) # Перепривязка p, тип должен совпадать
# Параметры функции: Move
process: (p: Point) -> Point = {
p.transform()
p # Move возврат
}
# Возврат из функции: Move
create: () -> Point = {
p = Point(1.0, 2.0)
p # Move возврат, нулевое копирование
}Характеристики:
- Нулевое копирование (компилятор перемещает указатель)
- После перемещения исходная привязка недоступна для чтения (ошибка компиляции)
- RAII: автоматическое освобождение при выходе из области видимости
- Сигнатура функции
(T) -> Tсама по себе является документацией — потребляет T, возвращает T
2. &T / &mut T (токены заимствования)
Основной принцип: &T и &mut T — это токены типов времени компиляции нулевого размера. Это не «ссылки», а «типизированные доказательства прав доступа».
2.1 Два атрибута типа
&T → Нулевой размер, замораживает данные источника (запрет WriteToken при живом ReadToken),
безопасность множественных представлений только для чтения при заморозке → Можно копировать (Dup)
&mut T → Нулевой размер, монопольный доступ для чтения/записи (запрет любых других токенов при живом WriteToken),
копирование бессмысленно при монопольном доступе → Линеен (не Dup)Причинно-следственная связь не может быть обратной: заморозка — причина, Dup — следствие. Не потому что &T реализует Dup, можно сосуществовать — а потому что данные заморожены (нет возможности мутации), множественные представления только для чтения безопасны, Dup может быть реализован. Если принять Dup за определение, а проверку конфликтов за «дополнительный патч» — дизайн неправильный.
2.2 Базовое использование
# На стороне метода: объявление типа параметра, определяющего необходимые права
Point.print: (self: &Point) -> Void = {
print(self.x) # Токен &Point даёт права на чтение
print(self.y)
}
Point.shift: (self: &mut Point, dx: Float, dy: Float) -> Void = {
self.x = self.x + dx # Токен &mut Point даёт права на запись
self.y = self.y + dy
}
# На стороне вызова: компилятор автоматически выбирает заимствование или Move
p = Point(1.0, 2.0)
p.print() # Компилятор автоматически создаёт токен &Point
p.shift(1.0, 1.0) # Компилятор автоматически создаёт токен &mut Point
p.print() # OK, предыдущий токен освобождён после завершения вызова shift
# Для свободных функций аналогично
distance: (a: &Point, b: &Point) -> Float = {
sqrt((a.x - b.x)**2 + (a.y - b.y)**2) # Два токена &Point сосуществуют — тип Dup
}
d = distance(p, p2)2.3 Почему не нужно «запрет на побег»
В RFC-009 v8 для &T/&mut T применялись три специальных правила — только параметры, нельзя возвращать, нельзя хранить в структурах. Это патчи для концепции «заимствования».
Токенная система не нуждается в этих правилах. Токены — обычные типы, подчиняющиеся тем же правилам области видимости, что и все остальные типы.
Возврат ссылок — естественно поддерживается:
# ✅ Токен распространяется вместе с возвращаемым значением
Point.get_x: (self: &Point) -> (&Float, &Point) = {
return (&self.x, self) # Субтокен и родительский токен возвращаются вместе
}
# Использование
p = Point(1.0, 2.0)
(px_ref, p) = p.get_x() # Токен возвращается вызывающему
print(px_ref) # OK, токен всё ещё в области видимостиХранение в структурах — естественно поддерживается:
# ✅ Структура содержит токен как поле
Window: Type = {
target: Point,
view: &Point, # Поле токена — держит представление только для чтения для target
}
# Токен view происходит от target, Window владеет обоими
# Пока Window существует, токен view валиден2.3 Замыкания и явные параметры Lambda
Lambda — это значение-функция — может быть возвращено, сохранено, передано за пределы текущей области видимости. Поэтому Lambda не неявно захватывает внешние локальные переменные. Когда нужны внешние данные, используются явные параметры:
# ✅ Lambda использует явные параметры
double: (x: Int) -> Int = (x) => x * 2
filter_by: (items: List(Int), f: (Int) -> Bool) -> List(Int) = { ... }
# ✅ spawn { } не подчиняется этому правилу — spawn это немедленно выполняемый параллельный блок, родительская задача блокируется в ожидании
shared = ref data
spawn { use(shared) }
# ❌ Lambda не может неявно захватывать внешние переменные
x = 42
f = () => { x + 1 } # Ошибка компиляции: x не в области видимости
# ✅ Правильный способ: явная передача параметра
f = (x) => { x + 1 }
f(x)spawn { } — это не значение-функция. Блок, отмеченный spawn, как и тело if/while — выполняется немедленно, завершается пока жив стековый кадр родителя. Тело spawn может нормально обращаться к внешним переменным.
Между задачами — токены не могут проходить через потоки:
# ❌ Токены не могут пересекать границы задач
bad_task: (p: &Point) -> Void = {
spawn { print(p.x) } # ❌ Ошибка компиляции: токены нельзя передавать между задачами
}
# Это не специальное правило — токены это доказательства прав доступа времени компиляции,
# для обмена между задачами используйте refТокены нельзя оборачивать в ref:
# ❌ Токены это доказательства прав, не владение
bad_ref: (p: &Point) -> Void = {
shared = ref p # ❌ Ошибка компиляции: &T не является владеющим типом
}2.4 Жизненный цикл токенов
Жизненный цикл токенов определяется обычными правилами области видимости, параметры времени жизни не нужны:
- Токены в параметрах функции: живут во время вызова, освобождаются после завершения вызова
- Возвращаемые токены: владение передаётся вызывающему
- Токены, хранящиеся в структурах: живут вместе со структурой
Компилятору не нужны аннотации 'a, потому что токены — значения, жизненный цикл значений управляется системой владения (Move/RAII). Сведение проблемы заимствования к проблеме владения.
2.5 Обнаружение конфликтов токенов
Обнаружение конфликтов токенов — это пропозиции логики Хоара, а не независимый потоково-чувствительный анализ.
{все конфликтующие ReadToken мертвы} write(data) {WriteToken безопасно получен}Разделяет конвейер доказательств RFC-027 с проверкой типов и пользовательскими предикатами. Компилятор автоматически генерирует пропозиции заимствования (borrow_conflict, use_after_move, use_after_drop, mut_violation), передаёт в конвейер. Конвейер возвращает Proved / Disproved / Unproven.
# ❌ Токен &mut линеен, нельзя копировать
bad_dup: (p: &mut Point) -> Void = {
p2: &mut Point = p # Move, p больше нельзя читать
p.x = 10.0 # ❌ Ошибка компиляции: WriteToken уже перемещён
}
# ✅ Токен &T имеет тип Dup, можно свободно копировать
good_dup: (p: &Point) -> Void = {
p2: &Point = p # OK, &T имеет тип Dup
print(p.x) # OK
print(p2.x) # OK, два токена только для чтения сосуществуют
}Проверка заимствований не исчезла — она свелась. Существующий BorrowChecker становится BorrowPredicateEmitter (генератором пропозиций), генерируемые пропозиции заимствования и другие типовые пропозиции разделяют один конвейер доказательств. Это полностью параллельно концепции проверщика типов: проверщик типов генерирует типовые уравнения, генератор пропозиций заимствования генерирует пропозиции заимствования — один конвейер проверки. Детальный дизайн см. в RFC-009a.
2.7 Внутреннее устройство компилятора: механизм брендов
Пользователь никогда не сталкивается с брендами. Компилятор внутренне назначает каждому токену уникальный идентификатор времени компиляции:
Что видит пользователь Внутреннее представление компилятора
────────────────────────────────────────
&Point → ReadToken(Point, #N) // #N — уникальное целое число времени компиляции
&mut Point → WriteToken(Point, #M) // #M — уникальное целое число времени компиляцииНазначение брендов:
- Защита от подделки: токены можно получить только от владеющего владельца, нельзя создать из воздуха
- Отслеживание происхождения: при получении
&Floatиз&Point(доступ к полю),&Floatнесёт производный бренд (#N.field_x), компилятор может отследить до родительского токена - Обнаружение конфликтов: одноисходный
WriteTokenи производныйReadTokenне могут быть живы одновременно
Бренды полностью исчезают после мономорфизации и инлайнинга, в генерируемом машинном коде их нет. Нулевые накладные расходы во время выполнения.
2.8 Правила автоматического выбора заимствования
Компилятор на стороне вызова автоматически выбирает по следующему приоритету:
1. Если фактический аргумент ещё используется в дальнейшем → приоритет создания токена (&T или &mut T, согласно сигнатуре метода)
2. Если фактический аргумент больше не используется → Move
3. Приоритет соответствия: &T < &mut T < Move# Пример: автоматический выбор
p = Point(1.0, 2.0)
p.print() # print объявляет &self → компилятор создаёт токен &Point
p.shift(1.0, 1.0) # shift объявляет &mut self → компилятор создаёт токен &mut Point
p2 = p # Move, p больше не используется2.9 Сравнение с упрощённым заимствованием из RFC-009 v8
| Характеристика | Упрощённое заимствование (v8) | Токены заимствования (v9) |
|---|---|---|
| Возврат ссылок | ❌ Жёсткий запрет | ✅ Токен распространяется с возвращаемым значением |
| Хранение в структурах | ❌ Жёсткий запрет | ✅ Токен как поле структуры |
| Явные параметры Lambda | ❌ Жёсткий запрет | ✅ Lambda использует явные параметры |
| Специальные правила | 3 (только параметр/нельзя возвращать/нельзя хранить) | 0 — атрибуты типа естественно выводят |
| Проверка заимствований | Специализированная перекрёстная проверка | Потоково-чувствительный анализ активности проверщика типов |
| Аннотации времени жизни | Не нужны | Не нужны |
| Накладные расходы выполнения | Ноль | Ноль (типы нулевого размера, исчезают после компиляции) |
| Сообщения об ошибках | «Заимствование не может выйти» | «WriteToken(#3) уже перемещён» (обычная ошибка типа) |
| Ментальная модель пользователя | Понимание «особого статуса заимствования» | &T можно копировать, &mut T нельзя копировать |
3. ref ключевое слово (автооптимизация компилятора)
ref — единственный способ обмена данными между областями видимости. Нижний уровень Rc или Arc — пользователю не нужно об этом думать.
3.1 Базовое использование
p: Point = Point(1.0, 2.0)
shared = ref p # Общее владение, компилятор автоматически выбирает реализацию
# Обмен между задачами
@block
main: () -> Void = {
data = ref heavy_data
spawn { use(data) } # Компилятор: между задачами → Arc
spawn { use(data) } # Компилятор: между задачами → Arc
}
# Обмен в рамках одной задачи
@block
main: () -> Void = {
data = ref heavy_data
use(data) # Компилятор: не между задачами → Rc
}Ментальная модель пользователя: ref = общее владение. Этого достаточно.
3.2 Анализ побега компилятора: Rc vs Arc
Анализ потока данных ref:
Не сбегает в другие задачи → Rc (неатомарный подсчёт ссылок, низкие накладные расходы)
Сбегает в другие задачи → Arc (атомарный подсчёт ссылок, потокобезопасность)3.3 Стратегия обнаружения циклов
Циклы внутри задачи → Безмолвно разрешены.
├── У каждой задачи чёткая граница жизненного цикла — в конце задачи все ресурсы (включая ref циклы) освобождаются вместе.
├── Долгоживущие сервисы должны создавать подзадачи по запросам/соединениям — подзадача завершается, ресурсы автоматически возвращаются, не накапливаются.
├── ref всегда остаётся живым, семантика без разбавления.
└── Пользователь имеет право строить двунаправленные сильные ссылки внутри задачи (например, промежуточные состояния графовых вычислений).
Циклы между задачами → lint (по умолчанию warn, настраивается).
├── Программа работает корректно, настоящих утечек не будет (при завершении родительской задачи все ресурсы подзадач освобождаются).
├── Но межзадачные сильные ссылки означают размытые границы владения — стоит остановиться и переосмыслить.
├── По умолчанию уровень warn, компиляция проходит с предупреждением.
└── Команда может установить deny в конфигурации проекта, включить в quality gate CI.Уровни lint (по аналогии с Rust clippy):
| Уровень | Поведение | Сценарий |
|---|---|---|
allow | Не проверять | Личный проект |
warn (по умолч.) | Компиляция проходит, есть提示 | Этап разработки |
deny | Компиляция не проходит | Quality gate команды в CI |
forbid | Компиляция не проходит, нельзя переопределить | Организационное обязательное правило |
# Цикл внутри задачи: безмолвно разрешён, двунаправленные сильные ссылки
build_graph: () -> Void = {
a = Node("a")
b = Node("b")
a.next = ref b
b.prev = ref a # Цикл. В конце задачи освобождается вместе.
}
# Цикл между задачами: lint (по умолчанию warn)
@block
parent_task: () -> Void = {
shared_a = ref a
shared_b = ref b
spawn {
shared_a.child = ref shared_b # ⚠️ warn: циклическая ссылка между задачами
}
}Пример конфигурации проекта:
# yaoxiang.toml
[lints]
cross-task-cycle = "deny" # Межзадачные циклы отклоняются в CI напрямую| Тип цикла | Поведение | Причина |
|---|---|---|
| Внутризадачный ref цикл | Не проверяется | Право пользователя, освобождение в конце задачи |
| Межзадачный ref цикл | lint (по умолчанию warn) | Напоминание переосмыслить, можно настроить deny |
3.4 Weak: предоставляется стандартной библиотекой
use std.rc.Weak
# Продвинутый пользователь делает явный выбор
a.next = ref b
b.prev = Weak.new(a.next) # Пользователь явно контролирует, какая сторона слабаяWeak не встроен в язык, это тип стандартной библиотеки. Для повседневного использования достаточно ref. Продвинутые пользователи, которым нужна тонкая настройка управления памятью, вручную импортируют Weak.
3.5 Токены заимствования vs ref
&T / &mut T | ref | |
|---|---|---|
| Что делает | Взглянуть / изменить на месте | Общее владение |
| Область действия | По области видимости значения токена | Между областями видимости |
| Стоимость | Ноль (типы нулевого размера) | Rc или Arc (выбирает компилятор) |
| Побег | Можно (токен распространяется с возвратом/структурой/замыканием) | Это его основное назначение |
| Между задачами | Нельзя (токен — доказательство прав времени компиляции, нельзя передавать между задачами) | Можно (компилятор автоматически выбирает Arc) |
| Образование циклов | Не применимо | Внутри задачи безмолвно, между задачами lint |
4. clone() —— Явное копирование
p: Point = Point(1.0, 2.0)
p2 = p.clone() # Глубокое копирование
# p и p2 независимы, не влияют друг на другаКогда использовать: когда нужно сохранить оригинальное значение и сценарий не подходит для Move или совместного использования.
5. unsafe + Голые указатели (системное программирование)
p: Point = Point(1.0, 2.0)
unsafe {
ptr: *Point = &p # Голый указатель
(*ptr).x = 0.0 # Разыменование (пользователь гарантирует безопасность)
ptr2 = ptr + 1 # Арифметика указателей
}Ограничения:
- Можно использовать только в блоках
unsafe - Пользователь гарантирует отсутствие висячих указателей и использования после освобождения
- Используется для FFI, операций с памятью и другого системного программирования
6. Общий обзор градиента владения
Токены заимствования (ноль накладных) Move (ноль накладных) Обмен (по потребности) Копирование
│ │ │ │
&T Move ref Rc/Arc clone()
&mut T По умолчанию Компилятор Явное
│ передача владения автоматически глубокое
│ │ выбирает │
│ Цепное потребление обмен │
│ и возврат │
│
Область видимости В области видимости Между областями В любой момент
значения токена видимости
Можно возвращать/ T -> T возврат ref между задачами → Arc
хранить в структурах ref не между задачами → Rc
Нулевой размер, Внутри задачи безмолвно
исчезает после Между задачами lint
компиляции T -> Void Стандартный Weak
нулевой размер, потребление аварийный выход
исчезает после
компиляцииКомплексный пример
Point: Type = {
x: Float,
y: Float,
# &T: токен только для чтения
print: (self: &Point) -> Void = {
print(self.x)
print(self.y)
}
# &mut T: изменяемый токен
shift: (self: &mut Point, dx: Float, dy: Float) -> Void = {
self.x = self.x + dx
self.y = self.y + dy
}
# Move → Move: потребление и возврат
scale: (self: Point, f: Float) -> Point = {
self.x = self.x * f
self.y = self.y * f
self # Забрать, изменить, вернуть
}
# Возврат ссылки: токен распространяется с возвращаемым значением
get_x: (self: &Point) -> (&Float, &Point) = {
return (&self.x, self)
}
}
# Lambda с явными параметрами
double: (x: Int) -> Int = (x) => x * 2
# Комплексное использование
p = Point(1.0, 2.0)
p.print() # Токен &Point
p.shift(1.0, 1.0) # Токен &mut Point
p = p.scale(2.0) # Move → возврат
shared = ref p # ref обмен
spawn { use(shared) }
# clone независимая копия
backup = p.clone()
# Цикл внутри задачи: безмолвно разрешён
a = Node("a")
b = Node("b")
a.next = ref b
b.prev = ref a # Цикл, освобождается в конце задачи
# unsafe системный уровень
unsafe {
ptr: *Point = &p
(*ptr).x = 0.0
}Ограничения системы типов
Атрибут типа Dup
Dup (Duplicable) — это атрибут типа, автоматически управляемый компилятором, означающий поверхностное копирование: при присваивании/передаче параметров копируется дескриптор/токен, базовые данные разделяются. Это формирует трёхуровневый градиент с Move (передача владения) и Clone (явное глубокое копирование, создание независимой копии).
Dup и Clone — ортогональные концепции — Dup копирует дескриптор с разделением данных, Clone создаёт независимую копию. Тип может одновременно поддерживать Dup и Clone, или только один из них.
| Тип | Dup | Clone | Описание |
|---|---|---|---|
&T | ✅ (копирование токена, несколько представлений указывают на одни данные) | ✅ | Токен только для чтения |
ref T | ✅ (счётчик ссылок+1, куча данных разделяется) | ✅ | Общее владение (компилятор выбирает Rc/Arc) |
| String, Bytes | ✅ (внутренний счётчик ссылок, копирование дескриптора разделяет базовый буфер) | ✅ | Строка/байты |
&mut T | ❌ (линеен, монопольный) | ❌ | Изменяемый токен |
*T | ❌ | ❌ | Голый указатель |
| struct | Производный (автоматически производится, когда все поля Dup) | ✅ | Структура |
Примитивные типы значений (Int, Float, Bool, Char) при присваивании ведут себя как встроенное копирование значений компилятором — два полностью независимых значения. Они не принадлежат к атрибуту типа Dup, а являются нативной обработкой компилятора.
Анализ производительности
| Операция | Стоимость | Описание |
|---|---|---|
| Move | Ноль | Перемещение указателя |
&T / &mut T | Ноль | Типы нулевого размера, исчезают после компиляции, нулевые накладные расходы выполнения |
ref (не между задачами) | Низкая | Компилируется в Rc, неатомарные операции |
ref (между задачами) | Средняя | Компилируется в Arc, атомарные операции |
clone() | Зависит от типа | Маленькие объекты быстро, большие медленно |
unsafe + *T | Ноль | Прямые операции с памятью |
Сравнение
| Язык | Механизм обмена | Управление памятью | Обработка циклов | Сложность |
|---|---|---|---|---|
| Rust | Arc / Mutex + проверка заимствований | Время компиляции | Ручной Weak | Высокая |
| Go | chan / pointer | GC | GC | Низкая |
| C++ | shared_ptr | RAII | weak_ptr | Средняя |
| YaoXiang | ref + токены заимствования | RAII | Освобождение на границе задач / межзадачный lint / стандартный Weak | Низкая |
Компромиссы
Преимущества
- Единство:
&T/&mut T— обычные типы, не специальная языковая особенность. Полностью согласовано сname: type = valueиз RFC-010 - Простота: без времени жизни, проверка заимствований сводится к пропозициям системы типов.
&Tможно копировать,&mut Tнельзя — два атрибута типа - Мощность: можно возвращать ссылки, хранить в структурах, захватывать в замыканиях — выразительность на уровне Rust
- Интеллект компилятора: ref автоматически выбирает Rc/Arc, на стороне вызова автоматический выбор заимствования
- Детерминированность: ref это гарантия живучести, не бывает скрытого ослабления
- Высокая производительность: Move с нулевым копированием, токены с нулевыми накладными расходами (типы нулевого размера, исчезают после компиляции)
- Гибкость:
unsafe + *Tподдерживает системное программирование
Недостатки
- Заражение обобщённых параметров бренда: токены несут идентификаторы брендов, сигнатуры функций с возвратом ссылок содержат дополнительные обобщённые параметры
- Накладные расходы выполнения ref: у атомарных операций есть цена (но это неизбежная плата за обмен)
- Риски unsafe: пользователь должен гарантировать корректность
- Межзадачные циклы — lint, не ошибка компиляции:,不像 Rust那样编译报错,默认 warn,需要团队配置 deny才能作为质量门
Альтернативные решения
| Решение | Почему не выбрано |
|---|---|
| GC | Накладные расходы выполнения, непредсказуемые паузы |
| Проверка заимствований Rust | Нужны времена жизни 'a, крутая кривая обучения |
| Только Move | Не может обрабатывать параллельный обмен |
| Без голых указателей | Невозможно системное программирование |
| Expose Rc/Arc пользователю | Детали реализации перекладываются на пользователя, увеличивается когнитивная нагрузка |
| Упрощённое заимствование (v8) | Стратегия запрета побега жертвует ключевыми возможностями: захватом замыканий, возвратом ссылок и т.д. |
Журнал решений по дизайну
| Решение | Решение | Причина | Дата |
|---|---|---|---|
| Значение по умолчанию | Move (нулевое копирование) | Высокая производительность, нулевые накладные расходы | 2025-01-15 |
| Механизм обмена | ref ключевое слово, автооптимизация компилятора | Простота для пользователя, компилятор берёт на себя ответственность | 2025-01-15 |
| Заимствование | &T/&mut T как типы токенов нулевого размера | Атрибуты типа (Dup/Linear) естественно выводят права, унифицированная система типов | 2025-01-15 |
| Токены заимствования | Замена упрощённого заимствования, &T Dup, &mut T Linear | Устранение специальных правил типа «запрет на побег», поддержка захвата замыканий/возврата ссылок/хранения в структурах | 2026-05-29 |
| Копирование | clone() | Явная семантика | 2025-01-15 |
| Системный уровень | *T + unsafe | Поддержка системного программирования | 2025-01-15 |
| Время жизни | Не реализуется | Токены это значения, жизненный цикл управляется Move/RAII единообразно, заимствование сводится к проблеме владения | 2025-01-15 |
| Rc/Arc | Автовыбор компилятора, невидимо для пользователя | Снижение когнитивной нагрузки | 2025-01-15 |
| Циклические ссылки | Внутри задачи не проверяются, между задачами lint (по умолчанию warn) | Структурированный параллелизм естественно гарантирует, lint настраивается на deny | 2025-01-16 |
| Weak | Предоставляется стандартной библиотекой | Продвинутый пользователь делает явный выбор | 2025-01-16 |
| Анализ потребления | Удалён | Упрощённый проверщик заимствований, не нужен | 2026-05-11 |
| Возврат владения | Удалён | Сигнатура (T) -> T сама по себе является документацией | 2026-05-11 |
| Переиспользование пустого состояния | Удалён (как характеристика) | Move с последующим переприсваиванием — естественное поведение | 2026-05-11 |
| Обратная функция/частичное потребление/трёхуровневая изменчивость полей | Удалён | Чрезмерное проектирование | 2026-05-11 |
| Lambda не неявно захватывает | Lambda использует только явные параметры, не неявно захватывает внешние переменные | Явная философия, упрощение компилятора | 2026-06-16 |
История версий
| Версия | Основные изменения | Дата |
|---|---|---|
| v1 | Первый черновик: на основе модели владения Rust | 2025-01-08 |
| v8 | Удаление чрезмерного проектирования (обратная функция/частичное потребление/трёхуровневая изменчивость полей/анализ потребления/возврат владения/переиспользование пустого состояния), добавлено упрощённое заимствование &T/&mut T | 2026-05-11 |
| v9 | Система токенов заимствования заменяет упрощённое заимствование, унифицированная система типов; корректировка обнаружения конфликтов токенов на пропозиции Хоара, см. RFC-009a | 2026-06-13 |
Нерешённые вопросы
| Вопрос | Описание | Статус |
|---|---|---|
| Синтаксис Drop | Нужна ли явная функция drop() | К обсуждению |
| Алгоритм анализа побега | Обнаружение межзадачности для ref | К обсуждению |
| Обнаружение конфликтов токенов | Пропозиции логики Хоара, см. далее | ✅ Решено (см. RFC-009a) |
Обнаружение конфликтов токенов: пропозиции логики Хоара
Полная схема обнаружения конфликтов токенов описана в RFC-009a: Анализ жизненного цикла токенов — на основе конвейера доказательств Хоара. Ключевые моменты:
Жизненность токена — это пропозиция логики Хоара.{все конфликтующие ReadToken мертвы} write(data) {WriteToken безопасно получен} — разделяет конвейер доказательств RFC-027 с проверкой типов и пользовательскими предикатами. Компилятор автоматически генерирует пропозиции заимствования (borrow_conflict, use_after_move, use_after_drop, mut_violation), конвейер возвращает Proved / Disproved / Unproven.
Проверка заимствований не исчезла — она свелась. BorrowChecker становится BorrowPredicateEmitter, генерирует пропозиции вместо выполнения проверок. Это полностью параллельно концепции «проверщика типов»: проверщик типов генерирует типовые уравнения, генератор пропозиций заимствования генерирует пропозиции заимствования — один конвейер проверки.
ID бренда (#42) и есть 'a. Информация абсолютно идентична, различается кодировка. 'a виден в сигнатуре типа, #42 внутри компилятора. Никакого нового анализа не изобретено — время жизни перенесено с типового уровня на уровень доказательства.
Обзор алгоритма (подробности в RFC-009a):
- Префиксное сопоставление дерева брендов → определение конфликтующих токенов (O(depth), глубина ≤ 3)
- Обратный BFS → от потребителя, break обрывает обратные рёбра, структурный анализ покрывает 95%+ сценариев (быстрый путь)
- Отсечение через SMT-логику → только при while + условиях пути вызывается (медленный путь, крайне редко)
Ссылки
Официальная документация YaoXiang
- Спецификация языка
- Манифест дизайна
- RFC-001 Модель параллелизма и обработки ошибок
- RFC-010 Унифицированный синтаксис типов
- tutorial/ Учебник
Внешние ссылки
Жизненный цикл и статус
| Статус | Расположение | Описание |
|---|---|---|
| Черновик | docs/design/rfc/ | Черновик автора, ожидает рецензии |
| На рецензии | docs/design/rfc/ | Открыто для обсуждения сообщества |
| Принято | docs/design/accepted/ | Становится официальным документом дизайна |
| Отклонено | docs/design/rfc/ | Сохраняется в директории RFC |
