Overloading — перегрузка функций и методов
Решения этой группы описывают единый механизм перегрузки в Nova: по receiver-типу, по типам аргументов, по типу результата, по арности — одно правило резолва, общее для свободных функций, методов и static-функций на типе.
| # | Решение |
|---|---|
| D84 | Перегрузка функций и методов: четыре оси, резолв по самому специфичному матчу |
Связанные решения в других файлах:
- D46 — operator overloading через
@plus/@times(частный случай по receiver). - D69 — variadic-параметры
...args []T. - D73 —
From[T]/Into[T](частный случай по аргументу/результату). - D77 —
TryFrom/TryInto(то же).
D84. Перегрузка функций и методов: четыре оси, резолв по самому специфичному матчу
Что
Имя функции или метода может быть перегружено — одно имя описывает несколько сигнатур, и компилятор выбирает нужную по контексту вызова. Перегрузка работает по четырём осям:
- По receiver-типу —
fn int @m()иfn str @m()— разные методы. - По типам аргументов —
fn f(s str)иfn f(b []u8)— разные функции. - По типу результата —
fn Celsius @into() -> Fahrenheitиfn Celsius @into() -> Kelvin— выбираются по ожидаемому типу из контекста. - По арности —
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 — самый специфичный матч. Из оставшихся кандидатов выбирается тот, у которого сигнатура самая специфичная по правилам:
- Concrete побеждает generic.
fn f(v int)выбирается раньше, чемfn f[T](v T), при вызовеf(42). - Non-variadic побеждает variadic.
fn f(s str)выбирается раньше, чемfn f(...args []str), при вызовеf("hello"). - Subtype побеждает supertype. При иерархии
int < anyдля аргумента типаintвыбираетсяfn f(v int), а неfn f(v any). - Если ни один кандидат не доминирует — 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)vsT.from(str)) работает в bootstrap-codegen черезmethod_overloadsregistry + C-name mangling по param types. - ✅ instance overload по типу аргумента (
@write(str)vs@write([]u8)). - ✅ arity overload (
@log(msg)vs@log(level, msg)). - ✅ Одноимённые методы на разных типах (
Box1.make()vsBox2.make()) не конфликтуют — multi-key registry(type, name) → Vec<Sig>. - ✅ Free-functions (без receiver’а) — overload работает (2026-05-10):
тот же
method_overloadsregistry с 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 перегрузками. - D73 —
From/Into: формализованный частный случай перегрузкиT.from(...)по типу аргумента и@into()по типу результата. - D77 —
TryFrom/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).