Skip to content

RFC-011b: Перегрузка операторов и операторы на основе интерфейсов ​

Ссылки:

Резюме ​

Дополнение возможности перегрузки операторов в 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 местах:

yaoxiang
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 == b
error [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 ​

Реализация ? одновременно жёстко прописывает имя типа и номер варианта:

rust
// 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 представляет собой жёстко закодированный белый список:

rust
// 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) -> TypeRFC-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 принял этот подход✅ Механизм установлен
Вычисление семейств типов AssociatedTypeDefdependent_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 (например, типовый Add Peano-арифметики из RFC-011 §5.2 Add: (A: Type, B: Type) -> Type = match ...) проходит через канал разрешения имён и влияет только на разрешение имени Add в этом модуле, не касается реестра и не влияет на работоспособность оператора. Одноимённые, разные сущности, не затеняют друг друга.

Это одновременно отвечает на вопрос «не конфликтует ли типовый Add из RFC-011 с интерфейсом Add из данного RFC»: не конфликтует. Типовый Add — чисто типовое вычисление (Zero/Succ — это типы, а не значения, никогда не будет операции над значениями Zero + Succ(...)), а операторный интерфейс — это два слоя одного и того же имени на разных уровнях.

Уровень 0: фиксированная таблица соответствия ​

ОператорИнтерфейсМетодПервый этап
+Addadd✅
-Subtractsubtract✅
*Multiplymultiply✅
/Dividedivide✅
%Modulomodulo✅
== !=Equalequal✅
[]Indexindex✅
?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: определения интерфейсов ​

Арифметические интерфейсы (три параметра типа) ​

yaoxiang
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):

  1. При жёстко заданном Self метод Add(Int, Float) был бы вынужден возвращать Int, и 1 + 2.5 невозможно было бы корректно выразить;
  2. Когда тип результата сделан явным, семейство повышающих типов Add: (A, B) -> Type = match (A, B) { (Int, Float) => Float, ... } из RFC-011 §8.3 и регистрация интерфейса из данного RFC становятся двумя взглядами на одну таблицу — ядерная регистрация Add(Int, Float, Float) — это и есть строка (Int, Float) => Float, и каждое инстанцирование пользователя — это добавление строки в эту таблицу;
  3. Интерфейс 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).

Пример гетеротипной операции — масштабирование вектора:

yaoxiang
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)
}

Интерфейс равенства (автопроизводный по умолчанию) ​

yaoxiang
Equal: (Self: Type, R: Type) -> Type = {
    equal: (self: &Self, other: &R) -> Bool
}

Equal по умолчанию выводится автоматически по пяти правилам:

  1. Автопроизводство по умолчанию: при определении записи, если все поля сравнимы (базовые типы, String, либо сами сравнимые записи/кортежи/списки, и нет полей &mut), компилятор автоматически генерирует поэлементное ==, идущее по пути нативного кода и не порождающее видимого пользователю метода. Тип, написанный пользователем, естественно сравним без каких-либо церемоний.
  2. Явное переопределение: если в теле типа записано Equal(Point, Point) и предоставлен метод Point.equal, используется пользовательская версия (например, сравнение с допуском для вещественных чисел), автопроизводство более не выполняется.
  3. Когда поле несравнимо: автопроизводство не выполняется, == недоступно, диагностика указывает, какое именно поле стало препятствием; в этом случае пользователь по-прежнему может вручную написать Equal для пользовательского сравнения (например, сравнивать поля-функции по имени).
  4. Предусловие: типы, содержащие линейные токены &mut T (рекурсивно по полям), не участвуют в сравнении — линейный токен считывается однократно и расходуется, невозможно одновременно извлечь оба значения для сравнения. Обратите внимание, что предусловие — «нелинейность», а не «Dup»: примитивные типы значений (Int/Float/Bool/Char) согласно RFC-011 §2.4 не относятся к Dup (для них компилятор встроенно копирует значения), и при условии через Dup Point { x: Float, y: Float } был бы ошибочно отвергнут.
  5. Единый критерий ограничения: разрешение T: Equal и допуск == идут по одному и тому же правилу «проверка реестра или структурного вывода» — ограничение и оператор всегда дают один и тот же ответ.

Обоснование согласованности: (1, 2) == (1, 2) и [1] == [1] уже сегодня проходят через поэлементное сравнение в рантайме (белый список сравнений в executor.rs включает Tuple/List/Array), запись — единственный исключённый составной тип. Автопроизводство — не новое молчаливое умолчание, а приведение существующего внутреннего поведения языка к последнему недостающему фрагменту.

Вывод из эксплуатации старого механизма: старое автопроизводство Equal в trait_data.rs (регистрирует только сигнатуру, без кода реализации, ноль точек потребления) прекращается; все решения по Equal (дефолтная регистрация базовых типов, структурный вывод, явное инстанцирование) проходят через реестр интерфейсов. Старая таблица трейтов сохраняет обязанности Clone/Dup/Debug (имена не пересекаются с операторными интерфейсами, унификация — предмет отдельного обсуждения).

Интерфейс индексации ​

yaoxiang
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 не изобретает для этого новых правил.

Многопозиционная индексация — через упаковку в кортеж + перегрузку, введения вариативных интерфейсов не требуется:

yaoxiang
// Одномерный контейнер
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, но полная форма не утверждена и в первый этап не входит. Причины:

  1. ? фактически делает три вещи: определяет успех/неудачу (сейчас через жёстко прописанный variant 0), извлекает полезную нагрузку успеха, на пути неудачи возвращает всё значение как есть из текущей функции. Единственный метод residual: (self: &Self) -> E покрывает лишь уголок третьей задачи, и при нынешнем жёстком задании, скорее всего, потребует переделки;
  2. Этап 2 уже зависит от введения RFC-010 (конструирование значения Result) и RFC-010b (деконструкция вариантов);
  3. Сопутствующая проблема жёстко зашитых конструкторов (ok / err / some, распознаваемых парсером, спецификация языка §1.4.2) и жёсткой привязки имени типа в ? — это две стороны одной проблемы; «Перенос Result в std» требует развязки обеих сторон, обе относятся к этапу 2.

Примеры ​

Пользовательский тип: арифметика и равенство работают из коробки ​

yaoxiang
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
}

Пользовательское равенство (переопределяет автопроизводство) ​

yaoxiang
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

Индексация пользовательского контейнера ​

yaoxiang
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
}

Ограничения дженериков наконец становятся реализуемыми ​

yaoxiang
// Пример из 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.rscheck_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
  • Семь интерфейсов Add Subtract Multiply Divide Modulo Equal Index (форма с тремя параметрами типа)
  • Автопроизводство 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] Нужен ли период совместимости для исправления семантики Modulo? → Закрыт: в справочнике по языку давно записано «умножение/деление по модулю», текущая реализация remainder нарушает опубликованную документацию, обрабатывается как исправление дефекта, без периода совместимости (2026-09-22)
  • [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 Места жёсткой привязки ? и конструкторов ​

rust
// 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-0112026-09-15
Имя интерфейса %Modulo (математический модуль)Имя фиксирует семантику2026-09-15
Операторы сравненияВ первом этапе не становятся интерфейсами, сохраняются нативные IR-инструкцииУже являются инструкциями первого класса; Ordering влечёт за собой целый ворох независимых проблем2026-09-15
OrderingВ первом этапе не вводитсяНет реального драйва потребности, объём отдельного RFC2026-09-15
Ассоциированный типЧерез параметры интерфейса, без введения синтаксиса члена typeRFC-011a уже утвердил этот подход (Iterator: (Item: Type))2026-09-15
Унификация привязки метода и индексацииКонцептуально объединены, интерфейс отвечает только за индексацию контейнеровКлюч позиционного связывания — константа этапа компиляции, тип результата требует вычисления семейства типов, нельзя реализовать пользователем2026-09-15
Многопозиционная индексацияУпаковка в кортеж + перегрузка по типу Key, без вариативного интерфейсаБез изменения парсера2026-09-15
Побитовые / унарные операторыВ первом этапе не делаютсяДля пользовательских типов редки, YAGNI2026-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», не DupRFC-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-0362026-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, привязывающий позиции параметров функции как метод, поведение этапа компиляции
АвтопроизводствоГенерируемое компилятором поэлементное == для записи со всеми сравнимыми полями, явное инстанцирование может переопределить

Ссылки ​