← Все решения

Overloading — перегрузка функций и методов

Решения этой группы описывают единый механизм перегрузки в Nova: по receiver-типу, по типам аргументов, по типу результата, по арности — одно правило резолва, общее для свободных функций, методов и static-функций на типе.

#Решение
D84Перегрузка функций и методов: четыре оси, резолв по самому специфичному матчу

Связанные решения в других файлах:

  • D46 — operator overloading через @plus/@times (частный случай по receiver).
  • D69 — variadic-параметры ...args []T.
  • D73From[T]/Into[T] (частный случай по аргументу/результату).
  • D77TryFrom/TryInto (то же).

D84. Перегрузка функций и методов: четыре оси, резолв по самому специфичному матчу

Что

Имя функции или метода может быть перегружено — одно имя описывает несколько сигнатур, и компилятор выбирает нужную по контексту вызова. Перегрузка работает по четырём осям:

  1. По receiver-типуfn int @m() и fn str @m() — разные методы.
  2. По типам аргументовfn f(s str) и fn f(b []u8) — разные функции.
  3. По типу результатаfn Celsius @into() -> Fahrenheit и fn Celsius @into() -> Kelvin — выбираются по ожидаемому типу из контекста.
  4. По арностиfn exit(code int) и fn exit(code int, msg str) — разное число параметров.

Все четыре оси работают одновременно для свободных функций, методов@-receiver’ом) и static-функций на типе (T.name(...)).

Правило

Декларация

Программист пишет несколько определений с одним именем. Сигнатуры должны различаться хотя бы по одной из осей — иначе compile error «duplicate definition».

// По типам аргументов
fn parse(s str) Fail[ParseError] -> int  => ...
fn parse(b []u8) Fail[ParseError] -> int => ...

// По арности
fn exit(code int) -> never              => ...
fn exit(code int, msg str) -> never     => ...

// По receiver-типу (методы)
fn int @double() -> int  => @ * 2
fn f64 @double() -> f64  => @ * 2.0

// По типу результата (static / instance)
fn Celsius @into() -> Fahrenheit => ...
fn Celsius @into() -> Kelvin     => ...

Резолв на call-site

Компилятор резолвит вызов в четыре фильтра, применяемые по порядку:

Фильтр 1 — арность. Отбрасываются кандидаты с числом параметров, не совпадающим с числом аргументов на call-site. Variadic-кандидат (D69) принимает любое число аргументов ≥ числа non-variadic параметров.

Фильтр 2 — типы аргументов. Каждый аргумент проверяется против типа соответствующего параметра. Если тип аргумента — подтип типа параметра (или совпадает), кандидат остаётся; иначе отбрасывается.

Фильтр 3 — тип результата. Если контекст вызова задаёт ожидаемый тип (см. ниже «Источники контекста для result-резолва»), отбрасываются кандидаты, тип результата которых не совпадает / не приводится.

Фильтр 4 — самый специфичный матч. Из оставшихся кандидатов выбирается тот, у которого сигнатура самая специфичная по правилам:

  1. Concrete побеждает generic. fn f(v int) выбирается раньше, чем fn f[T](v T), при вызове f(42).
  2. Non-variadic побеждает variadic. fn f(s str) выбирается раньше, чем fn f(...args []str), при вызове f("hello").
  3. Subtype побеждает supertype. При иерархии int < any для аргумента типа int выбирается fn f(v int), а не fn f(v any).
  4. Если ни один кандидат не доминирует — compile error «ambiguous overload» с перечислением кандидатов и hint’ом про cast/turbofish.

Источники контекста для result-резолва

Тип результата подсказывает компилятору, какую перегрузку выбрать. Контекст приходит из:

  • let x T = expr — тип из аннотации.
  • Возврат из функцииreturn expr в функции с известным return-type.
  • Аргумент функцииf(c.into()) где f(x Fahrenheit).
  • Поле record-литерала{ temp: c.into() } где temp Fahrenheit.

Если контекста нет — compile error:

let x = c.into()                    // ❌ нет ожидаемого типа
//      ^^^^^^^^ cannot resolve overload `Celsius.@into()`:
//               candidates: -> Fahrenheit, -> Kelvin
//               hint: add type annotation `let x Fahrenheit = ...`

let x Fahrenheit = c.into()         // ✅ контекст из аннотации

Turbofish не обходит concrete

Вызов f[T_value](args) — turbofish задаёт значение generic-параметра, но не меняет правила резолва. Concrete-перегрузка для конкретного типа доминирует над generic-перегрузкой даже при явном turbofish.

Следствие: f[u8](7)f(7 as u8). Обе формы резолвятся в одну и ту же overload — concrete если она существует, иначе generic с T = u8.

fn job[T Numeric = f64](a T) => a * 10        // generic
fn job(a u8) Fail => throw "error"             // concrete u8

job(7 as u8)        // → throw "error" (concrete)
job[u8](7)          // → throw "error" (concrete, не generic)
job(5.0)            // → 50.0 (generic, T = f64)
job(7)              // → 70 (generic, T = int — нет concrete для int)

Это согласовано с принципом «concrete побеждает generic» (фильтр 4 выше): автор API, объявивший concrete-перегрузку, делает это специально — generic-версия для этого типа обходится. Turbofish не обходит этот контракт.

Когда нужна именно generic для конкретного типа при существующей concrete — переименовать generic или вынести её в отдельный модуль. В Nova нет специального синтаксиса «вызови именно generic».

Mangling

Компилятор использует name mangling для C-emit: каждая перегрузка получает уникальное C-имя, в которое закодированы типы параметров и receiver’а. Программист этого не видит — на уровне Nova-кода имя одно.

Схема mangling. Первая перегрузка использует короткое имя (backward-compat): Nova_T_method_m / Nova_T_static_m. Вторая+ — с param-types suffix: Nova_T_method_m__nova_str, Nova_T_method_m__nova_int. Общее правило: <original>__<param_type_1>_<param_type_2>_....

Это распространяет существующий механизм Plan 11 (mangling для методов) на свободные функции и static-функции на типе.

Bootstrap-status (Plan 11)

  • static overload по типу аргумента (T.from(int) vs T.from(str)) работает в bootstrap-codegen через method_overloads registry + C-name mangling по param types.
  • instance overload по типу аргумента (@write(str) vs @write([]u8)).
  • arity overload (@log(msg) vs @log(level, msg)).
  • ✅ Одноимённые методы на разных типах (Box1.make() vs Box2.make()) не конфликтуют — multi-key registry (type, name) → Vec<Sig>.
  • Free-functions (без receiver’а) — overload работает (2026-05-10): тот же method_overloads registry с sentinel-key ("", name), C-mangling по param-types. Резолв на call-site по статическим типам args. Тест: nova_tests/syntax/overload_free_fn.nv.
  • ⚠️ Result-type overload (ось 3 D84) — type-checker регистрирует overloads с разным return-type, но codegen на call-site не делает expected-type propagation: при двух кандидатах с одинаковыми arg-types и разным return-type возникает ambiguity error. Реализация требует context-driven resolve через let-аннотации, return-position, argument-types вызывающей функции. Отложено как Q-overload-result-type.
  • Method values как first-class (let f = obj.@m, Type.@m) — Plan 11 Ф.4. См. D35 «Method values».
  • Disambiguation через as fn(...) для overloaded method values — Plan 11 Ф.5. Annotation на cast или на let-binding type определяет, какой overload выбрать.

Strict matching типов

No implicit conversions. buf.write(42) где 42 int — error если нет @write(int). Программист пишет buf.write(42 as char) или buf.write(str.from(42)). Это часть правила «самый специфичный матч»: implicit-конверсия размывает специфичность.

Примеры — методы

fn Buffer mut @write(s str) -> ()
fn Buffer mut @write(b []u8) -> ()
fn Buffer mut @write(c char) -> ()

fn Logger @log(msg str) -> ()
fn Logger @log(level int, msg str) -> ()         // arity overload

Resolution на call-site по статическим типам аргументов:

buf.write("hello")        // → @write(str)
buf.write([0xDE, 0xAD])   // → @write([]u8)
buf.write('A')            // → @write(char)

log.log("ok")             // → @log(str) — arity 1
log.log(2, "ok")          // → @log(int, str) — arity 2

При ambiguity (≥2 кандидатов после фильтрации) — compile error с suggestion’ом disambiguate через as fn(...) annotation:

let f = t.@m as fn(str) -> int

Дисамбигуация программистом

Когда автоматический резолв даёт ambiguous error, программист может явно указать выбор:

  • Cast аргумента: f(42 as i32) — выбирает fn f(v i32), если кандидат был fn f(v int) или fn f(v i32).
  • Turbofish для generic: parse[int]("42") — фиксирует generic-параметр.
  • Аннотация результата: let x Fahrenheit = c.into() — фиксирует тип результата.

Почему

Зачем перегрузка вообще

В существующей Nova-практике перегрузка уже используется — Plan 11 закрыл её для методов, D73 для From/Into, D46 для операторов. Stdlib-типы вроде StringBuilder опираются на это:

external fn StringBuilder mut @append(s str)  -> ()
external fn StringBuilder mut @append(c char) -> ()

Запрет на перегрузку для свободных функций оставался искусственным ограничением, не имеющим обоснования в дизайне. D84 устраняет несимметрию и формализует все четыре оси одним правилом.

Почему четыре оси, а не три

Тип результата (ось 3) часто упускают, но в Nova он уже работает для @into() через context-driven dispatch (D73). Без него Celsius @into() -> Fahrenheit и Celsius @into() -> Kelvin было бы нельзя различить. Включение оси 3 в общее правило формализует существующее поведение.

Почему «самый специфичный матч»

Это согласованное правило в большинстве языков с overloading (Java, Swift, C#, Scala, Rust trait selection). Альтернативы:

  • Last-wins (текущий bootstrap для свободных функций) — проще имплементировать, но создаёт hidden surprises: добавление новой перегрузки молча меняет поведение существующего кода.
  • First-wins — то же, в обратную сторону.
  • Ambiguous → error без подсказки — не помогает программисту выбрать.

«Самый специфичный + ambiguous → error с hint’ом» — баланс между автоматизмом и предсказуемостью.

Почему concrete побеждает generic

Программист пишет конкретную перегрузку, чтобы специализировать дженерик для конкретного типа: например, fn f[T Hashable](v T) — общая реализация, fn f(v str) — оптимизированная для строк. Если бы generic выигрывал, специализация не работала бы.

Почему non-variadic побеждает variadic

f("hello") для fn f(s str) и fn f(...args []str) — оба подходят. Variadic — это «catch-all» на произвольную арность; non-variadic сигнатура конкретно совпадает по форме и поэтому специфичнее. Это естественно отражает намерение программиста: писать non-variadic = «у меня ровно столько-то аргументов», писать variadic = «может быть любое количество».

LLM-критерий

Перегрузка повышает риск, что LLM сгенерирует код, который компилируется, но вызывает не ту перегрузку. Mitigation:

  • Все перегрузки имени должны быть в одном модуле (или явно re-exported в одно место). Не разрешается, чтобы модуль A определил f(int), а модуль B (который импортирует A) — f(str) с тем же именем. Это даёт locality: LLM, читая модуль A, видит все перегрузки f в нём.
  • Hover в LSP показывает все доступные перегрузки с их сигнатурами.
  • Compile error при ambiguity включает список кандидатов — LLM видит конкретный путь починки.

Что отвергнуто

  • Перегрузка только по receiver-типу (текущее частичное состояние). Несимметрия со static и свободными функциями; D73 уже работает иначе.
  • Last-wins резолв. Hidden surprises при добавлении перегрузок.
  • Перегрузка только через protocol-based dispatch (variant 4). Покрывает большинство случаев, но требует явного protocol-объявления для тривиальной перегрузки (exit(int) / exit(int, str) — излишне). Protocol-dispatch остаётся как идиоматичный путь для расширяемых перегрузок (новые типы могут добавлять реализации), но не как единственный механизм.
  • Перегрузка через namespace-prefix (exit::with_msg(code, msg)). Замена синтаксиса — не решение задачи.

Связь

  • D46 — operator overloading: частный случай перегрузки методов с фиксированными именами (@plus, @times).
  • D69 — variadic-параметры ...args []T: D84 фиксирует правило резолва между variadic и non-variadic перегрузками.
  • D73From/Into: формализованный частный случай перегрузки T.from(...) по типу аргумента и @into() по типу результата.
  • D77TryFrom/TryInto: то же для fallible конверсий.
  • D35 — методы и static-функции на типе.
  • D40 — «один способ делать одно»: D84 не нарушает, потому что разные перегрузки решают разные задачи (разные типы), а не одну.

Эволюция

  • Q-overloading в open-questions: статус был ⚠️ PARTIALLY CLOSED — методы через Plan 11, свободные функции запрещены. D84 закрывает Q-overloading полностью.
  • Plan 11 реализовал перегрузку методов через C-name mangling + strict resolution по статическим типам. D84 переиспользует этот механизм для свободных функций и static-функций.
  • D73 ввёл context-driven dispatch для @into(). D84 формализует это как ось 3 общего правила.
  • 2026-05-10: добавлен раздел «Turbofish не обходит concrete» — уточнение что f[T_value](args)f(arg as T_value), обе формы резолвятся одинаково и concrete-перегрузка доминирует. Триггер — обсуждение D87/D88 (specialization для конкретного типа vs generic с default).