RFC-011b: Перегрузка операторов и операторы на основе интерфейсов
Ссылки:
- RFC-011: Проект системы дженериков — ограничения типов
T: Add + Multiply, ассоциированные типы- RFC-011a: Реализация интерфейсов и динамическая диспетчеризация — механизм объявления/инстанцирования интерфейсов
- RFC-009: Проект модели владения — линейный токен
&mut T- RFC-004: Многопозиционное связывание каррированных методов — синтаксис позиционного связывания
f[0]- RFC-010: Унифицированный синтаксис типов — тип-сумма = запись, все поля которой возвращают сам тип
- RFC-013: Спецификация кодов ошибок — существующее позиционирование
Resultв std, E108x- RFC-010b: Завершённость сопоставления с образцом (деконструкция вариантов и исчерпываемость) — деконструкция вариантов (зависимость)
Резюме
Дополнение возможности перегрузки операторов в YaoXiang, чтобы a + b / a == b / a[i] / e? могли быть реализованы для пользовательских типов, а ограничения операторами (T: Add + Multiply) из синтаксиса ограничений, уже записанного в RFC-011, превратились из бумажной возможности в работоспособный механизм (Zero в том же выражении не является оператором, см. открытые вопросы).
В проекте используется разделение обязанностей на три уровня: фиксированная таблица соответствия оператор→метод (уровень 0), база диспетчеризации по имени (уровень 1, переиспользует существующие method_bindings), слой контракта интерфейса (уровень 2, для ограничений дженериков). Приоритет и ассоциативность операторов остаются фиксированными в языке, пользователь перегружает только семантику.
Объём первого этапа: семь интерфейсов — Add Subtract Multiply Divide Modulo EqualIndex. Арифметические интерфейсы используют три параметра типа (Self, R, O) — тип результата O делается явным, чтобы гетеротипные операции, такие как 1 + 2.5 (возвращает Float) и point * 2.0 (масштабирование), были выразимы. Equal по умолчанию выводится автоматически (записи, все поля которых сравнимы, автоматически получают поэлементное ==), явное инстанцирование может переопределить.
Try (интерфейс ?) целиком переносится на этап 2: он зависит от введения RFC-010 (конструирование) и RFC-010b (деконструкция), а форма интерфейса ещё не утверждена (см. открытые вопросы).
Никакого нового синтаксиса, никаких новых ключевых слов, всё переиспользует существующие механизмы RFC-011a (объявление / инстанцирование / внешнее объявление методов / перегрузка).
Мотивация
Зачем нужна эта возможность
1. Ключевой пример RFC-011 зависит от неё, и ситуация хуже, чем «нереализуемо»
RFC-011 (принят) использует имена операторов как ограничения типов в 8 местах:
multiply: (T: Add + Multiply + Zero, Rows: Int, Cols: Int, M: Int) -> (
(a: Matrix(T, Rows, Cols), b: Matrix(T, Cols, M)) -> Matrix(T, Rows, M)
)Ни в одном из RFC-ов не определено, откуда берутся Add / Multiply и как + к ним привязывается. Практическая проверка подтверждает, что ситуация ещё хуже: T: Add прямо сегодня даёт ошибку компиляции — разрешение ограничений запрашивает старую таблицу трейтов (trait_data.rs), в которой нет Add. Имена ограничений Zero / One / PartialOrd / Fn / FnMut также висят в воздухе (их обработка — в разделе «Согласование с другими RFC»).
2. Пользовательские типы не могут участвовать в базовых операциях (проверено)
Point: Type = { x: Int, y: Int }
a = Point(1, 2)
b = Point(1, 2)
a == berror [E6007] Runtime error: type mismatch in comparison Eq:
Struct { type_id: TypeId(0), ... } vs Struct { type_id: TypeId(0), ... }Побочное следствие: list.contains(list_of_structs, p) полностью неработоспособно — внутри оно зависит от ==. А вот (1, 2) == (1, 2) и [1] == [1] через поэлементное сравнение в рантайме всегда работали — структура остаётся единственным пробелом.
3. ? приваривает имя типа к компилятору, блокируя перенос Result в std
Реализация ? одновременно жёстко прописывает имя типа и номер варианта:
// typecheck: жёстко прописанное конструирование типа Result
let expected_result = MonoType::make_result(ok_ty, expected_err);
// ir_gen: жёстко прописанное имя группы и номер варианта
Instruction::VariantTag { group: "Result".to_string(), .. }
variant 0 = ok, variant 1 = errЭто вынуждает Result оставаться в core. Если ? сделать управляемым интерфейсом, любой тип, реализующий этот интерфейс (включая пользовательский), сможет использоваться с ?, и Result можно будет перенести в std (RFC-013 уже содержит позиционирование «std-библиотека Result(T, Error)»).
Обратите внимание, что это лишь половина проблемы: способы конструирования ok(...) / err(...) / some(...) тоже сейчас приварены к парсеру (спецификация языка §1.4.2 перечисляет их как «конструкторы, распознаваемые парсером»). «Перенос Result в std» требует развязки обеих половин — и ?, и конструкторов — что относится к этапу 2 данного RFC.
4. Пользовательские контейнеры не индексируются
Проверка типов для Index представляет собой жёстко закодированный белый список:
// expressions.rs
MonoType::Generic { name, args } if name == "List" => Ok(args[0].clone()),
MonoType::Generic { name, args } if name == "Array" => Ok(args[0].clone()),
MonoType::Generic { name, args } if name == "Dict" => Ok(args[1].clone()),
// остальное → "контейнер, не опознанный на уровне типов, лучше отвергнуть, чем промолчать"Для любого пользовательского типа-контейнера c[0] безусловно выдаст ошибку.
5. Реализация % нарушает уже опубликованную документацию
Проверка показывает, что -7 % 3 возвращает -1 (усечённый остаток). Однако в справочнике по приоритету операторов (reference/index.md) уже написано: «* / % умножение/деление по модулю» — документация обещает взятие по модулю, а реализация даёт остаток. Это не изменение дизайна, это дефект, при котором реализация противоречит документации, который данный RFC заодно исправляет.
Имеющаяся полуфабрикатная база
Исследование показывает, что большая часть механизма уже существует, не хватает лишь подключения:
| Механизм | Расположение | Состояние |
|---|---|---|
Объявление интерфейса (Self: Type) -> Type | RFC-011a Фаза 1 | ✅ Работает |
Инстанцирование интерфейса Animal(Dog) + внешнее объявление метода | RFC-011a Фазы 2–3 | ✅ Работает (есть юнит-тесты) |
Диспетчеризация методов method_bindings["Type.method"] | expressions.rs (регистрация в environment.rs, запрос в expressions.rs на путях разрешения вызовов и fallback для полей) | ✅ Работает |
| Ассоциированные типы (= параметры типа интерфейса) | RFC-011 §3.1; RFC-011a принял этот подход | ✅ Механизм установлен |
Вычисление семейств типов AssociatedTypeDef | dependent_types.rs | ✅ Рабочий кейс IsTrue из std.assert |
Трейты Equal / Dup / Clone / Debug | Зарегистрированы в trait_data.rs | ⚠️ Equal — ноль точек потребления; старое автопроизводство регистрирует только сигнатуру без реализации |
| Соответствие оператор → имя метода | — | ❌ Не существует |
Вывод: это не создание возможности с нуля, а подключение и документирование существующих деталей.
Предложение
Основной дизайн: разделение обязанностей на три уровня
Уровень 0 Фиксированная таблица соответствия (константа уровня языка, неизменяемая пользователем)
+ → add == → equal [] → index ? → residual
│ встроена на этапе компиляции, не участвует в выводе типов, невидима для пользователя
▼
Уровень 1 База диспетчеризации (поиск метода по имени и вызов) ← переиспользует существующий механизм
method_bindings["Point.add"]
▼
Уровень 2 Контракт интерфейса (для ограничений дженериков)
Add / Equal / Index / Try …
Делает ограничение T: Add валидным и служит условием допуска оператораОбоснование разделения:
- Уровни 0 и 1 делают оператор «используемым», уровень 2 делает его «допускаемым в ограничениях». Слияние привело бы к тому, что любой метод с именем
addвызывался бы через+, и ограничениеT: Addиз RFC-011 оторвалось бы от оператора. - Уровень 1 не создаётся заново:
method_bindingsуже ищет по ключуType.method(fallback после неудачного поиска поля), операторы идут по тому же пути. - Уровень 2 — это условие допуска: перед допуском оператор обязан убедиться, что тип реализует соответствующий интерфейс (см. исключение для автопроизводства
Equal— вывод и регистрация — один и тот же критерий).
Имена и регистрация: два независимых канала
Оператор запрашивает «реестр реализаций интерфейса», а не обычное разрешение имён.
- Реестр принадлежит ядру и агрегирует две регистрации: дефолтные регистрации ядра для базовых типов (
Add(Int, Int, Int)и т.д.) и инстанцирования интерфейсов, записанные пользователем в теле типа (Add(Point, Point, Point)). Вне зависимости от того, в каком модуле физически находится код реализации, регистрации стекаются в одну общую таблицу, и+/==/[]смотрят только в эту таблицу. - Локальное определение модуля с тем же именем
Add(например, типовыйAddPeano-арифметики из RFC-011 §5.2Add: (A: Type, B: Type) -> Type = match ...) проходит через канал разрешения имён и влияет только на разрешение имениAddв этом модуле, не касается реестра и не влияет на работоспособность оператора. Одноимённые, разные сущности, не затеняют друг друга.
Это одновременно отвечает на вопрос «не конфликтует ли типовый Add из RFC-011 с интерфейсом Add из данного RFC»: не конфликтует. Типовый Add — чисто типовое вычисление (Zero/Succ — это типы, а не значения, никогда не будет операции над значениями Zero + Succ(...)), а операторный интерфейс — это два слоя одного и того же имени на разных уровнях.
Уровень 0: фиксированная таблица соответствия
| Оператор | Интерфейс | Метод | Первый этап |
|---|---|---|---|
+ | Add | add | ✅ |
- | Subtract | subtract | ✅ |
* | Multiply | multiply | ✅ |
/ | Divide | divide | ✅ |
% | Modulo | modulo | ✅ |
== != | Equal | equal | ✅ |
[] | Index | index | ✅ |
? | Try (форма не утверждена, см. открытые вопросы) | residual (предварительно) | ❌ Этап 2 |
< <= > >= | — (зарезервировано нативными инструкциями) | — | ❌ |
and or | — (короткое замыкание — семантика языка, не перегружается) | — | ❌ |
| 5 побитовых | — | — | ❌ |
Унарные - ! | — | — | ❌ |
Имена интерфейсов пишутся полностью, а не сокращаются (Multiply, а не Mul): ради согласованности с текстом RFC-011 T: Add + Multiply + Zero без изменения уже принятого RFC.
% использует семантику Modulo (математическое взятие по модулю, знак результата — как у делителя). В разделе мотивации уже подтверждено, что текущая реализация remainder нарушает опубликованную документацию (reference/index.md «умножение/деление по модулю»), данный пункт обрабатывается как исправление дефекта, без периода совместимости.
Замечание о текущем состоянии: белый список проверки типов для % сейчас содержит только Int/Float (в отличие от белого списка + с Int/Float/String/List), протокол проверки приведён в приложении A.2.
Уровень 2: определения интерфейсов
Арифметические интерфейсы (три параметра типа)
Add: (Self: Type, R: Type, O: Type) -> Type = {
add: (self: &Self, other: &R) -> O
}
Subtract: (Self: Type, R: Type, O: Type) -> Type = {
subtract: (self: &Self, other: &R) -> O
}
Multiply: (Self: Type, R: Type, O: Type) -> Type = {
multiply: (self: &Self, other: &R) -> O
}
Divide: (Self: Type, R: Type, O: Type) -> Type = {
divide: (self: &Self, other: &R) -> O
}
Modulo: (Self: Type, R: Type, O: Type) -> Type = {
modulo: (self: &Self, other: &R) -> O
}Три параметра типа выполняют каждый свою роль:
Self: тип левого операнда (получатель, заимствуется как&Self, см. токены заимствования в RFC-009 и соглашение о получателе в RFC-011a);R: тип правого операнда — левый и правый могут быть разных типов;O: тип результата, объявлен явно.
Почему тип результата должен быть явным параметром (а не жёстко задан как -> Self):
- При жёстко заданном
SelfметодAdd(Int, Float)был бы вынужден возвращатьInt, и1 + 2.5невозможно было бы корректно выразить; - Когда тип результата сделан явным, семейство повышающих типов
Add: (A, B) -> Type = match (A, B) { (Int, Float) => Float, ... }из RFC-011 §8.3 и регистрация интерфейса из данного RFC становятся двумя взглядами на одну таблицу — ядерная регистрацияAdd(Int, Float, Float)— это и есть строка(Int, Float) => Float, и каждое инстанцирование пользователя — это добавление строки в эту таблицу; - Интерфейс
Indexизначально трёхпараметрический(Self, Key, Value), тип возвратаValue— параметр — после того, как арифметические интерфейсы получаютO, всё семейство операторных интерфейсов становится однообразным, а жёстко заданныйSelf— наоборот, чужеродным.
Дефолтные регистрации ядра (путь нативных инструкций, не через вызов метода): Add(Int, Int, Int), Add(Int, Float, Float), Add(Float, Int, Float), Add(Float, Float, Float), Add(String, String, String) (конкатенация), Add(List(T), List(T), List(T)) (конкатенация элементов) и т.п., пять арифметических интерфейсов для базовых типов — аналогично. 1 + 2.5 из текущей ошибки компиляции (белый список требует одинаковых типов) превращается в корректную операцию, возвращающую 3.5: Float.
Синтаксический сахар ограничений: T: Add ≜ зарегистрировано Add(T, T, T) — самокомпозиция с одинаковым типом, результат — тот же тип. В примере с матричным умножением из RFC-011 a * b + c везде имеет тип T, что и требует это значение. T: Equal аналогично ≜ Equal(T, T).
Пример гетеротипной операции — масштабирование вектора:
Point: Type = {
x: Float,
y: Float,
Multiply(Point, Float, Point),
}
Point.multiply: (self: &Point, other: &Float) -> Point =
Point(self.x * other, self.y * other)
main: () -> Void = {
p = Point(1.0, 2.0)
q = p * 3.0 # Point(3.0, 6.0)
}Интерфейс равенства (автопроизводный по умолчанию)
Equal: (Self: Type, R: Type) -> Type = {
equal: (self: &Self, other: &R) -> Bool
}Equal по умолчанию выводится автоматически по пяти правилам:
- Автопроизводство по умолчанию: при определении записи, если все поля сравнимы (базовые типы,
String, либо сами сравнимые записи/кортежи/списки, и нет полей&mut), компилятор автоматически генерирует поэлементное==, идущее по пути нативного кода и не порождающее видимого пользователю метода. Тип, написанный пользователем, естественно сравним без каких-либо церемоний. - Явное переопределение: если в теле типа записано
Equal(Point, Point)и предоставлен методPoint.equal, используется пользовательская версия (например, сравнение с допуском для вещественных чисел), автопроизводство более не выполняется. - Когда поле несравнимо: автопроизводство не выполняется,
==недоступно, диагностика указывает, какое именно поле стало препятствием; в этом случае пользователь по-прежнему может вручную написатьEqualдля пользовательского сравнения (например, сравнивать поля-функции по имени). - Предусловие: типы, содержащие линейные токены
&mut T(рекурсивно по полям), не участвуют в сравнении — линейный токен считывается однократно и расходуется, невозможно одновременно извлечь оба значения для сравнения. Обратите внимание, что предусловие — «нелинейность», а не «Dup»: примитивные типы значений (Int/Float/Bool/Char) согласно RFC-011 §2.4 не относятся к Dup (для них компилятор встроенно копирует значения), и при условии через DupPoint { x: Float, y: Float }был бы ошибочно отвергнут. - Единый критерий ограничения: разрешение
T: Equalи допуск==идут по одному и тому же правилу «проверка реестра или структурного вывода» — ограничение и оператор всегда дают один и тот же ответ.
Обоснование согласованности: (1, 2) == (1, 2) и [1] == [1] уже сегодня проходят через поэлементное сравнение в рантайме (белый список сравнений в executor.rs включает Tuple/List/Array), запись — единственный исключённый составной тип. Автопроизводство — не новое молчаливое умолчание, а приведение существующего внутреннего поведения языка к последнему недостающему фрагменту.
Вывод из эксплуатации старого механизма: старое автопроизводство Equal в trait_data.rs (регистрирует только сигнатуру, без кода реализации, ноль точек потребления) прекращается; все решения по Equal (дефолтная регистрация базовых типов, структурный вывод, явное инстанцирование) проходят через реестр интерфейсов. Старая таблица трейтов сохраняет обязанности Clone/Dup/Debug (имена не пересекаются с операторными интерфейсами, унификация — предмет отдельного обсуждения).
Интерфейс индексации
Index: (Self: Type, Key: Type, Value: Type) -> Type = {
index: (self: &Self, key: &Key) -> Value
}Value является параметром типа, а не членом ассоциированного типа: открытый вопрос RFC-011a закрыт решением «ассоциированный тип реализуется через параметризацию интерфейса» (изоморфный прецедент — Iterator: (Item: Type) -> Type), вводить синтаксис члена type не требуется.
Замечание о владении: index возвращает полное Value из заимствования &Self. Стандартный list.get: (&Vec(A), Int) -> A из стандартной библиотеки уже имеет ту же форму, данный интерфейс получает тот же режим, что и текущий std; точная семантика «извлечения полного значения из заимствования» для типов элементов с семантикой перемещения будет унифицирована при полном введении RFC-009, данный RFC не изобретает для этого новых правил.
Многопозиционная индексация — через упаковку в кортеж + перегрузку, введения вариативных интерфейсов не требуется:
// Одномерный контейнер
List(T) инстанцирует Index(List(T), Int, T) → arr[0]
// Многомерный контейнер
Grid инстанцирует Index(Grid, Tuple(Int, Int), Float) → g[0, 1]
// └─ Key — кортеж
// Два инстанцирования с разными сигнатурами → сосуществуютДля одного типа разрешены несколько инстанцирований одноимённого интерфейса, различающихся по сигнатуре внедряемого метода, сосуществующих по правилам перегрузки на уровне методов из RFC-011a. Перегрузка в RFC-011a явно прописана только до уровня методов, данный пункт дополняет её правилом уровня инстанцирования: легитимность сосуществования одноимённых инстанцирований интерфейса определяется тем, конфликтуют ли сигнатуры методов после раскрытия (при конфликте — E1097, что соответствует правилам пространства имён полей/методов).
Интерфейс распространения ошибки (этап 2, форма не утверждена)
Целевое имя интерфейса для ? — Try, но полная форма не утверждена и в первый этап не входит. Причины:
?фактически делает три вещи: определяет успех/неудачу (сейчас через жёстко прописанный variant 0), извлекает полезную нагрузку успеха, на пути неудачи возвращает всё значение как есть из текущей функции. Единственный методresidual: (self: &Self) -> Eпокрывает лишь уголок третьей задачи, и при нынешнем жёстком задании, скорее всего, потребует переделки;- Этап 2 уже зависит от введения RFC-010 (конструирование значения
Result) и RFC-010b (деконструкция вариантов); - Сопутствующая проблема жёстко зашитых конструкторов (
ok/err/some, распознаваемых парсером, спецификация языка §1.4.2) и жёсткой привязки имени типа в?— это две стороны одной проблемы; «ПереносResultв std» требует развязки обеих сторон, обе относятся к этапу 2.
Примеры
Пользовательский тип: арифметика и равенство работают из коробки
Point: Type = {
x: Float,
y: Float,
Add(Point, Point, Point),
}
Point.add: (self: &Point, other: &Point) -> Point =
Point(self.x + other.x, self.y + other.y)
main: () -> Void = {
a = Point(1.0, 2.0)
b = Point(3.0, 4.0)
c = a + b # Point(4.0, 6.0)
println(a == b) # false — Equal автопроизводный, инстанцирование не требуется
println(a == a) # true
}Пользовательское равенство (переопределяет автопроизводство)
Vec3: Type = {
x: Float,
y: Float,
z: Float,
Equal(Vec3, Vec3),
}
# Сравнение с допуском для вещественных, переопределяет автопроизводное поэлементное сравнение
Vec3.equal: (self: &Vec3, other: &Vec3) -> Bool =
abs(self.x - other.x) < 0.000001
and abs(self.y - other.y) < 0.000001
and abs(self.z - other.z) < 0.000001Индексация пользовательского контейнера
Box: (T: Type) -> Type = {
data: List(T),
Index(Box(T), Int, T)
}
Box.index: (T: Type)(self: &Box(T), key: &Int) -> T =
list.get(self.data, key)
main: () -> Void = {
b = Box([10, 20, 30])
println(b[1]) // 20
}Ограничения дженериков наконец становятся реализуемыми
// Пример из RFC-011 теперь реализуем
// T: Add ≜ Add(T, T, T) зарегистрировано; T: Multiply ≜ Multiply(T, T, T) зарегистрировано
combine: (T: Add + Multiply)(a: T, b: T, c: T) -> T =
a * b + c(Zero / One из знакового примера RFC-011 T: Add + Multiply + Zero не входят в область данного RFC — они являются членами-константами, а не операторами; «члены интерфейса без получателя» не имеют прецедента в RFC-011a, см. открытые вопросы.)
Изменения синтаксиса
Никакого нового синтаксиса, никаких новых ключевых слов. Все возможности — комбинация существующих механизмов:
| Возможность | Переиспользуемый существующий механизм |
|---|---|
| Объявление интерфейса | RFC-011a Фаза 1 |
| Инстанцирование интерфейса | RFC-011a Фаза 2 (Dog: { Animal(Dog) }) |
| Реализация метода | RFC-011a Фаза 3 (внешнее объявление Point.add) |
| Ассоциированный тип | RFC-011 §3.1 (параметры типа интерфейса) |
| Многопозиционная индексация | Существующий разбор упаковки в кортеж + правила перегрузки уровня инстанцирования |
| Приоритет операторов | Фиксирован в языке, не настраивается пользователем |
Детальный дизайн
Влияние на систему типов
Новые интерфейсы (уровень 2): Add Subtract Multiply Divide Modulo Equal Index (Try — этап 2).
Реестр реализаций интерфейсов: центральный узел принятия решений по операторам. Агрегирует дефолтные регистрации ядра (базовые типы) и пользовательские инстанцирования (ImplementationProof, выдаваемые check_interface_instantiation), проверка допуска + / == / [] и разрешение ограничения T: Add (check_trait_bounds из bounds.rs) запрашивают одну и ту же таблицу — невозможен зазор «ограничение говорит — да, оператор говорит — нет».
Структурный вывод Equal: поверх реестра накладывается структурное правило (все поля сравнимы ⇒ сравнимо), базовые типы подстилаются дефолтными регистрациями ядра, рекурсивно замыкается. Старая регистрация Equal и старое автопроизводство в trait_data.rs прекращаются.
Разделение запроса операторов и разрешения имён: + и другие операторы запрашивают только реестр, не проходят через обычное разрешение имён; локальные одноимённые привязки (например, типовые семейства Add) не влияют на операторы (см. § Имена и регистрация).
Правило сирот: реализация оператора может быть записана только в модуле определения типа — Int определён в ядре, поэтому регистрацию Int может делать только ядро; Point определён в пользовательском модуле, поэтому зарегистрировать его может только его автор. Все типы подчиняются одному правилу, встроенные типы не имеют привилегий.
Поведение в рантайме
Нулевые накладные расходы в рантайме: цель вызова определяется на этапе компиляции (статическая диспетчеризация). Для базовых типов (Int/Float/String/List) сохраняется быстрый путь нативных инструкций, не проходящий через диспетчеризацию интерфейса; автопроизводное == для структур генерирует нативное поэлементное сравнение; == / != в позиции Any (динамический тип) сохраняет существующее рантайм-сравнение (от этого зависит семейство assert_eq тестового фреймворка из RFC-036, не затрагивается).
Исправление семантики %: -7 % 3 с -1 (усечённый остаток) меняется на 2 (математический модуль). Синхронно изменяются три места: интерпретатор (checked_rem), свёртка констант (a % b), байткод (I64_REM).
Изменения в компиляторе
| Компонент | Изменение |
|---|---|
typecheck/inference/expressions.rs | Белый список infer_binary заменяется на двухпутный «быстрый путь нативных инструкций + запрос реестра»; в Expr::Index добавляется ветка запроса интерфейса |
typecheck/inference/bounds.rs | check_trait_bounds для имён операторных интерфейсов переключается на запрос реестра интерфейсов (Equal включает структурный вывод) |
typecheck/checker.rs | Добавляется структурный вывод Equal (после определения записи); правило перегрузки уровня инстанцирования (несколько инстанцирований одноимённого интерфейса сосуществуют по сигнатуре метода) |
middle/core/ir_gen.rs | В Expr::Index добавляется ветка диспетчеризации через вызов метода; для структур без явного equal генерируется нативное поэлементное сравнение; инструкция Mod меняется на математический модуль |
| Реестр интерфейсов (новый) | Константа таблицы соответствия уровня 0 + функция запроса оператор→интерфейс + дефолтные регистрации ядра (Int/Float/String/List и т.д.) |
trait_data.rs | Регистрация Equal и старое автопроизводство прекращаются (Clone/Dup/Debug остаются как есть) |
| Диагностика | Несоблюдение предусловия Equal (линейный токен) переиспользует семейство E1101 (тип не реализует интерфейс); тексты E1081/E1082 избавляются от слова "Result" (этап 2, после введения Try, синхронизация трёх сторон по RFC-013) |
Обратная совместимость
Операции с базовыми типами не меняются: 1 + 2, "a" + "b", [1] + [2], 1 < 2 — все сохраняют нативный путь, поведение и производительность не меняются.
1 + 2.5 из ошибки компиляции становится допустимым: после ядерной регистрации Add(Int, Float, Float) смешанная арифметика, ранее отвергавшаяся белым списком, становится доступной, возвращая Float. Это новая возможность, существующий код не затрагивается.
Исправление семантики %: -7 % 3 с -1 становится 2. Квалифицируется как исправление дефекта (документация давно обещала взятие по модулю, см. мотивацию §5), без периода совместимости; в существующих тестовых материалах нет кейсов, зависящих от % с отрицательными числами (проверено).
== для структур из ошибки рантайма становится работоспособным: ранее Struct == Struct безусловно давал ошибку рантайма E6007, после подключения автопроизводство сразу даёт работоспособность — от плохого к хорошему, ни один существующий корректный код не затрагивается.
== для Any не затрагивается: позиция динамического типа сохраняет рантайм-сравнение (от этого зависит семейство утверждений assert_eq из RFC-036, ранее работало — и после работает).
Позиционное связывание f[0] не меняется: distance[0] — встроенная возможность компилятора из RFC-004, не идёт через интерфейс Index, не затрагивается.
? прозрачна для существующего кода (этап 2): после того, как для Result добавляется инстанцирование Try, существующие использования ? ведут себя полностью идентично.
Компромиссы
Преимущества
- Реализация операторных ограничений из RFC-011:
T: Add + Multiplyс бумаги (фактически — ошибка компиляции) становится реализуемым, без изменения уже принятого текста RFC - Снятие привязки
Resultк core (этап 2): после превращения?и конструкторов в интерфейсыResultможет быть перенесён в std, что соответствует позиционированию RFC-013 - Исправление проверенного дефекта:
Point == Pointработает из коробки, попутно исправляется неработоспособностьlist.containsдля структур - Нулевой новый синтаксис: всё переиспользует существующие механизмы RFC-011a, правила грамматики парсера не затрагиваются
- Нулевые накладные расходы в рантайме: статическая диспетчеризация + быстрый путь нативных инструкций для базовых типов
- Работоспособность пользовательских контейнеров:
Box(T)[0]из «безусловной ошибки» становится работоспособным - Внутренняя согласованность языка: Tuple/List уже сравниваются поэлементно, записи дополняются; арифметические интерфейсы с тремя параметрами однообразны с Index; таблица повышения из RFC-011 §8.3 объединяется с реестром интерфейсов
Недостатки
- Автопроизводство
Equal— молчаливое умолчание: в будущем, если потребуется отозвать «записи по умолчанию сравнимы», это станет ломающим изменением. Принимаем эту позицию — она согласуется с существующим поведением Tuple/List, а явное инстанцирование всегда может переопределить - Предусловие
Equalотвергает типы с линейными токенами: типы, содержащие поля&mut, не могут==, — это цена семантической корректности, требуется ясная диагностика (с указанием, какое именно поле) - Инстанцирование интерфейса — явная стоимость: для каждого арифметического оператора нужно написать строку инстанцирования + метод (
Equalосвобождён — автопроизводный). Синтаксис RFC-011a не допускает неявного вывода (плата за отсутствие магии с параметромSelf) - Исправление семантики
%— изменение поведения: хотя квалифицируется как исправление дефекта, должно быть отмечено в миграционных инструкциях
Альтернативы
Вариант A: только уровень 1 (диспетчеризация по имени метода), без слоя интерфейса
+ только проверяет наличие метода с именем add, не требуя реализации интерфейса Add.
Причина отклонения: ограничение T: Add из RFC-011 оторвётся от оператора — ограничение проверяет интерфейс, оператор проверяет имя метода, и они могут давать несогласованные ответы. Кроме того, невозможно на этапе компиляции дать точную диагностику для «+ применён к типу, не поддерживающему сложение».
Вариант B: введение синтаксиса конструкторов Ok(x) / Some(x) для решения проблемы ?
Не превращать ? в интерфейс, а добавить синтаксис конструкторов для типов-сумм.
Причина отклонения: конфликт с RFC-010 (принят). RFC-010 явно устанавливает «унифицированное использование типов-записей для выражения типов-сумм, две системы синтаксиса не нужны» и явно отвергает синтаксис |. Введение конструкторов — это введение второй системы выражения. К тому же это решает только ?, но не Point == Point и не индексацию пользовательских контейнеров.
Вариант C: операторы сравнения тоже в первом этапе становятся интерфейсами (введение Ordering)
< <= > >= идут через интерфейс Compare, возвращая трёхзначное Ordering.
Причина отклонения: < в YaoXiang уже является IR-инструкцией первого класса (Instruction::Lt/Le/Gt/Ge), превращение в интерфейс заставит базовые типы идти обходным путём. К тому же введение Ordering повлечёт за собой целый ворох проблем — собственно сравнение/сортировку Ordering, PartialOrd vs Ord для NaN вещественных — это объём отдельного RFC. Реально выявленная потребность (Point == Point, list.contains) требует только Equal.
Вариант D: имена операторов сокращаются (метод интерфейса Add называется add, интерфейс — Mul)
Причина отклонения: в тексте RFC-011 уже написано T: Add + Multiply + Zero, использование сокращений потребует изменения уже принятого RFC.
Вариант E: Equal — только явное инстанцирование, без автопроизводства
Причина отклонения: неприемлемо, что тип, написанный пользователем, нельзя сравнивать через ==; к тому же (1, 2) == (1, 2), [1] == [1] уже сегодня сравниваются поэлементно, и только записи исключены, что изначально несогласовано. Явное инстанцирование сохраняется как средство переопределения, учитывая потребности пользовательского сравнения.
Вариант F: тип возврата арифметических интерфейсов жёстко задан как -> Self
Причина отклонения: метод Add(Int, Float) будет вынужден возвращать Int, и 1 + 2.5 невозможно корректно выразить; масштабирование Multiply(Point, Float) аналогично не записывается. К тому же повышающее семейство типов (Int, Float) => Float из RFC-011 §8.3 потеряет место реализации. Три параметра (Self, R, O) однообразны с Index(Key, Value).
Стратегия реализации
Зависимости
| Зависимость | Состояние | Влияние на данный RFC |
|---|---|---|
| Механизм интерфейсов RFC-011a Фазы 1–3 | ✅ Введён | Фундамент уровня 2 (проверено в работе) |
| Путь конструирования записей RFC-010 | ❌ Не введён | Этап 2: сторона конструирования ? + вывод парсером конструкторов |
| Деконструкция вариантов RFC-010b | ❌ На бумаге | Этап 2: запись match для Result.residual, исчерпываемость |
| Вывод линейных токенов RFC-009 | Частично | Проверка предусловия Equal |
По фазам
По зависимости от интерфейсов (ограничение дизайна, не расписание), делятся на две группы:
Этап 1 — не зависит от RFC-010/010b:
- Таблица соответствия уровня 0 + реестр реализаций интерфейсов + подключение диспетчеризации уровня 1
- Семь интерфейсов
AddSubtractMultiplyDivideModuloEqualIndex(форма с тремя параметрами типа) - Автопроизводство
Equal+ предусловие линейности + переключение разрешения ограничений на реестр - Исправление семантики
%(включая интерпретатор / свёртку констант / байткод — три места) ==в позицииAnyсохраняет рантайм-сравнение- Результат:
Point + Point,Point == Point(без церемоний),Box(T)[0],1 + 2.5, ограничениеT: Add— всё становится работоспособным
Этап 2 — зависит от RFC-010 / RFC-010b, форма Try должна быть утверждена до начала работ:
- Утверждение формы интерфейса
Try(см. открытые вопросы) - Превращение
?в интерфейс + вывод парсером конструкторов (ok/err/some), переход на путь конструирования записей RFC-010 - Перенос
Resultиз core в std (согласно существующему позиционированию RFC-013)
Риски
| Риск | Смягчение |
|---|---|
Изменение семантики % влияет на существующий код | Квалифицируется как исправление дефекта + проверено отсутствие в тестовых материалах зависимостей от % с отрицательными числами |
Молчаливое умолчание автопроизводства Equal сложно отозвать | Позиция принята: согласуется с существующим поведением Tuple/List, явное инстанцирование может переопределить |
| Несогласованность двух путей (нативный + реестр) для базовых типов | Контроль: семантика ядерных регистраций должна совпадать с нативными инструкциями (две точки зрения на одну таблицу) |
| Запрос реестра замедляет компиляцию | Таблица индексируется по имени типа + кэш результатов инстанцирования (переиспользует proof из RFC-011a) |
| Перегрузка уровня инстанцирования вносит неоднозначность | То же правило, что и для перегрузки на уровне методов: конфликт сигнатур — E1097 |
Согласование с другими RFC
Позиция данного RFC — инициатор требований со стороны потребителя, необходима синхронизация следующих RFC для согласованности (получено разрешение на редактирование):
| RFC | Что необходимо обновить |
|---|---|
| RFC-011 (принят) | В § об ограничениях отметить, что Add / Multiply определяются и реализуются 011b (T: Add ≜ Add(T, T, T)); пометить Zero / One / PartialOrd / Fn / FnMut как висящие имена ограничений (для последующих RFC); в §8.3 отметить объединение повышающего семейства типов с реестром интерфейсов |
| RFC-010 (принят) | Уточнить требование к реализации «поле записи = конструктор» — это предусловие для вывода парсером конструкторов на этапе 2 |
| RFC-010b (проект, прежде RFC-039) | Конструирование и деконструкция должны быть парными; исчерпываемость не зависит от принадлежности Result к core/std (миграция на этапе 2 из 011b) |
| RFC-009 (принят) | В § о свойствах типов перекрёстная ссылка: предусловие Equal — «отсутствие линейных токенов &mut» (не Dup) |
| RFC-013 (принят) | При введении этапа 2: тексты E1081 / E1082 избавляются от слова "Result"; диагностика предусловия Equal переиспользует семейство E1101 — синхронизация трёх сторон по процессу RFC-013 (codes/*.rs ↔ locales ↔ таблица кодов) |
| RFC-018 (принят) | После перехода % на математический модуль таблица соответствия Mod → srem/urem становится недействительной, требуется переход на srem + коррекция знака (или синтез sdiv+mul+sub), а также исправление смешения терминов «модуль/остаток» |
| RFC-036 (принят) | Изменений не требуется. Отметить отношение: == в позиции Any сохраняет рантайм-сравнение, семейство утверждений assert_eq не подвержено влиянию шлюза |
Основание для переноса
Resultв std: RFC-013 уже позиционирует «std-библиотекаResult(T, Error)», RFC-014 распределяет std по слоям ядра. Этап 2 данного RFC — один из путей реализации, ссылки на несуществующие номера не используются.
Открытые вопросы
- [x]
Нужен ли период совместимости для исправления семантики→ Закрыт: в справочнике по языку давно записано «умножение/деление по модулю», текущая реализация remainder нарушает опубликованную документацию, обрабатывается как исправление дефекта, без периода совместимости (2026-09-22)Modulo? - [x]
Может ли пользователь добавить реализацию оператора к уже существующему типу (правило сирот)?→ Утверждено: реализация оператора может быть записана только в модуле определения типа.Intопределён в ядре, поэтому зарегистрировать его может только ядро; пользователь может зарегистрировать только свой тип. Все типы равны, встроенные типы не имеют привилегий (2026-09-22) - [ ] Должен ли
Indexразличать мутабельную индексацию (аналогIndexMutв Rust)? (только чтение в первом этапе; мутабельная индексация затрагиваетWriteTokenиз RFC-009, и в RFC-011a уже есть прецедент получателя&mut Self— оставлено на будущее) - [ ] Ключ многопозиционной индексации — упаковка в кортеж (текущее состояние) или переход к нескольким параметрам (стиль Swift)? (сохраняем упаковку в кортеж, парсер не меняем)
- [ ] Форма
Zero/One: члены-константы — не операторы, «члены интерфейса без получателя» не имеют прецедента в RFC-011a, требуется отдельное утверждение, после чего вся фразаT: Add + Multiply + Zeroиз RFC-011 становится реализуемой - [ ] Полная форма интерфейса
Try:?требует три вещи — определение успеха/неудачи, извлечение полезной нагрузки успеха, возврат значения из внешней функции на пути неудачи — единственного методаresidualнедостаточно; вместе с выводом парсером конструкторов — утверждается до начала этапа 2 - [ ] Совместное использование символа
?префиксным типом?T(аннотация FFI-обнуляемости из RFC-026, RFC-018) и постфиксным операторомe?: разные позиции (типовая vs выражения) конфликта не создают, достаточно документального пояснения - [ ]
PartialOrd/Ordering(превращение операторов сравнения в интерфейсы): отдельный RFC, данный RFC явно не делает
Приложение A: результаты исследования
Все практические проверки воспроизведены на 0.8.0 (target/debug/yaoxiang-rs.exe).
A.1 Доступность механизма интерфейсов (фундамент данного RFC)
| Возможность | Практическая проверка |
|---|---|
Объявление интерфейса Animal: (Self: Type) -> Type = {...} | ✅ Можно определить |
Инстанцирование + внешний метод Dog: { Animal(Dog) } + Dog.speak | ✅ Работает, вывод корректен (есть юнит-тест в tests/rfc011a.rs) |
| Инстанцирование + внутреннее объявление метода | ❌ E1097 (конфликт полей и методов в общем пространстве имён) |
Диспетчеризация метода d.speak() | ✅ Работает |
Примечание: E1097 означает, что методы операторов должны использовать форму внешнего объявления (Point.add: (self: &Point, ...)), согласующуюся с примерами RFC-011a. Регистрация method_bindings происходит в environment.rs (add_method_binding), запрос — в expressions.rs на путях разрешения цели вызова и fallback при неудачном поиске поля.
A.2 Текущее состояние операторов
| Выражение | Состояние |
|---|---|
1 + 2 / "a" + "b" / [1] + [2] | ✅ Жёстко закодированный белый список (Int/Float/String/List, требует одинаковых типов) |
1 + 2.5 (Int + Float) | ❌ Ошибка компиляции (белый список требует одинаковых типов) |
-7 % 3 | -1 (усечённый остаток; белый список % — только Int/Float, отличается от белого списка +) |
Point(1,2) == Point(1,2) | ❌ E6007 (Eq для Struct падает в рантайме) |
(1,2) == (1,2) / [1] == [1] | ✅ Поэлементное сравнение в рантайме (Struct — единственный пробел) |
== в позиции Any (assert_eq) | ✅ Рантайм-сравнение (подтверждено RFC-036) |
f[0] (позиционное связывание функции) | ⚠️ Допустимо только в объявлении связывания, в выражении — E3006 |
arr[0, 1] (многопозиционное) | ✅ Упаковка в кортеж, list([1, 2]) |
A.3 Места жёсткой привязки ? и конструкторов
// src/frontend/core/typecheck/inference/expressions.rs
let expected_result = MonoType::make_result(ok_ty.clone(), expected_err.clone());
// src/middle/core/ir_gen.rs
Instruction::VariantTag { group: "Result".to_string(), .. }
// variant 0 = ok, variant 1 = errСторона конструкторов: ok(T) / err(E) / some(T) распознаются парсером (спецификация языка syntax.md §1.4.2); is_ok / unwrap из std/result.rs сопоставляют с шаблоном по variant_id 0/1. «Перенос Result в std» требует развязки обеих половин (? + конструкторы) — обе относятся к этапу 2.
Приложение B: Записи о принятых дизайн-решениях
| Решение | Решение | Обоснование | Дата |
|---|---|---|---|
| Архитектура | Разделение на три уровня (таблица соответствия / база диспетчеризации / контракт интерфейса) | Слияние приведёт к отрыву ограничений RFC-011 от операторов | 2026-09-15 |
| Условие допуска оператора | Обязательна реализация интерфейса (уровень 2 — условие допуска) | Гарантирует строгое соответствие T: Add и + | 2026-09-15 |
| Именование интерфейсов | Полные имена (Multiply, а не Mul) | Без изменения уже принятого текста RFC-011 | 2026-09-15 |
Имя интерфейса % | Modulo (математический модуль) | Имя фиксирует семантику | 2026-09-15 |
| Операторы сравнения | В первом этапе не становятся интерфейсами, сохраняются нативные IR-инструкции | Уже являются инструкциями первого класса; Ordering влечёт за собой целый ворох независимых проблем | 2026-09-15 |
Ordering | В первом этапе не вводится | Нет реального драйва потребности, объём отдельного RFC | 2026-09-15 |
| Ассоциированный тип | Через параметры интерфейса, без введения синтаксиса члена type | RFC-011a уже утвердил этот подход (Iterator: (Item: Type)) | 2026-09-15 |
| Унификация привязки метода и индексации | Концептуально объединены, интерфейс отвечает только за индексацию контейнеров | Ключ позиционного связывания — константа этапа компиляции, тип результата требует вычисления семейства типов, нельзя реализовать пользователем | 2026-09-15 |
| Многопозиционная индексация | Упаковка в кортеж + перегрузка по типу Key, без вариативного интерфейса | Без изменения парсера | 2026-09-15 |
| Побитовые / унарные операторы | В первом этапе не делаются | Для пользовательских типов редки, YAGNI | 2026-09-15 |
| Форма арифметических интерфейсов | Три параметра типа (Self, R, O); T: Add ≜ Add(T, T, T) | Явный тип результата: 1 + 2.5, масштабирование выразимы; таблица повышения из RFC-011 §8.3 объединяется с реестром; однообразна с Index(Key, Value) | 2026-09-22 |
Вывод Equal | Автопроизводный по умолчанию + явное инстанцирование переопределяет; разрешение ограничений по тому же критерию | «Тип, написанный пользователем, нельзя ==» неприемлемо; Tuple/List уже сравниваются поэлементно, Struct — единственный пробел | 2026-09-22 |
Предусловие Equal | «Отсутствие линейных токенов &mut», не Dup | RFC-011 §2.4 явно указывает, что примитивы не относятся к Dup; предусловие через Dup ошибочно отвергло бы Point{Float,Float} | 2026-09-22 |
| Разделение имён и регистрации | Оператор запрашивает только реестр реализаций интерфейсов, не идёт через разрешение имён | Локальные одноимённые привязки (типовые семейства Add и т.д.) не мешают операторам; пример RFC-011 §5.2 не требует изменений | 2026-09-22 |
| Правило сирот | Реализация следует за модулем определения типа | Все типы равны, встроенные типы не имеют привилегий | 2026-09-22 |
== для Any | Сохраняет рантайм-сравнение, не запрашивает реестр | От этого зависит семейство утверждений assert_eq из RFC-036 | 2026-09-22 |
Форма Try | Отложено до утверждения перед началом этапа 2 | ? требует три вещи, единственного метода residual недостаточно; вывод парсером конструкторов включается сюда же | 2026-09-22 |
Квалификация семантики % | Исправление дефекта (документация уже обещала взятие по модулю), без периода совместимости | Первичное доказательство — reference/index.md «умножение/деление по модулю» | 2026-09-22 |
Основание переноса Result в std | Ссылка на существующее позиционирование RFC-013, без ссылок на несуществующие номера | RFC-013 уже записал «std-библиотека Result(T, Error)» | 2026-09-22 |
Приложение C: Глоссарий
| Термин | Определение |
|---|---|
| Уровень 0 | Фиксированная таблица соответствия оператор → имя метода, константа уровня языка, неизменяемая пользователем |
| Уровень 1 | База диспетчеризации по ключу Type.method |
| Уровень 2 | Слой контракта интерфейса, обеспечивающий основу для ограничений дженериков и условие допуска оператора |
| Реестр реализаций интерфейсов | Совокупная таблица ядерных дефолтных регистраций и пользовательских инстанцирований; единственный критерий для запросов операторов и разрешения ограничений, не связанный с разрешением имён |
| Быстрый путь нативных инструкций | Сохраняемый жёстко закодированный путь операций для базовых типов, не проходящий через диспетчеризацию интерфейса |
| Позиционное связывание | Синтаксис f[0] из RFC-004, привязывающий позиции параметров функции как метод, поведение этапа компиляции |
| Автопроизводство | Генерируемое компилятором поэлементное == для записи со всеми сравнимыми полями, явное инстанцирование может переопределить |
Ссылки
- RFC-011: Проект системы дженериков — ограничение
T: Add + Multiply + Zero, ассоциированные типы - RFC-011a: Реализация интерфейсов и динамическая диспетчеризация — правила объявления / инстанцирования / перегрузки интерфейсов
- RFC-009: Проект модели владения — линейный токен
&mut T - RFC-004: Многопозиционное связывание каррированных методов — синтаксис
f[0] - RFC-010: Унифицированный синтаксис типов — выражение типов-сумм
- RFC-013: Спецификация кодов ошибок — позиционирование переноса
Resultв std, процесс кодов ошибок - RFC-010b: Завершённость сопоставления с образцом (деконструкция вариантов и исчерпываемость)
- Rust
std::ops::Index— дизайн ассоциированного типаOutput - Swift Subscripts — многопараметрические индексы
