Overloading — перегрузка функций и методов
Решения этой группы описывают единый механизм перегрузки в Nova: по receiver-типу, по типам аргументов, по типу результата, по арности — одно правило резолва, общее для свободных функций, методов и static-функций на типе.
| # | Решение |
|---|---|
| D84 | Перегрузка функций и методов: четыре оси, резолв по самому специфичному матчу |
| D285 | Receiver-compatibility rule: blanket dispatch приоритизирует по receiver-совместимости |
Связанные решения в других файлах:
- 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:
ro x = c.into() // ❌ нет ожидаемого типа
// ^^^^^^^^ cannot resolve overload `Celsius.@into()`:
// candidates: -> Fahrenheit, -> Kelvin
// hint: add type annotation `let x Fahrenheit = ...`
ro 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 (
Type.@munbound, lambda|| obj.m()) — Plan 11 Ф.4 + Plan 132 (boundobj.@mremoved). См. D35 «Method values». - ✅ Disambiguation через
as fn(...)для overloaded method values — Plan 11 Ф.5. Annotation на cast или на let-binding type определяет, какой overload выбрать. - ✅ Receiver-mutability overload (
fn T @m()vsfn T mut @m()— одинаковые param-типы, разный receiver-mut) — Plan 135 (2026-06-09):fn T @m()получает C-имяNova_T_method_m(первая perегрузка),fn T mut @m()—Nova_T_method_m__mut. Call-site dispatch по мутабельности receiver’а:ro a; a.m()→ ro overload,mut b; b.m()→ mut overload. Аналог C++const-overloading. Тесты:nova_tests/plan135/(8/8 PASS). - ✅ Generic-type method overloads в монорфизации (
fn Vec[T] @cap()vsfn Vec[T] mut @cap(n int)— арность;fn Box[T] @tag(int)vs@tag(str)— arg-type) — Plan 153.1 /[M-138.2-generic-method-overload-mono](2026-06-13): раньше mono-диспатч коллапсировал overloads first-by-name (v.cap(10)→ 0-арг геттер → «too many args»). Теперь call-site дизамбигуирует по арности → param- C-типам (через side-mapmono_method_fndecl_for_name), а return-type inference для chained receiver’а (v.cap(n).push(x)) резолвит@/Self тем же arity- aware выбором (Ф.3-fallback). Тесты:nova_tests/plan153_1/generic_overload.nv(3/3: арность + param-type + chain),core_api.nv(fluent). Caveat: вызов overload’а без совпадения по арности всё ещё CC-FAIL’ит (codegen fall- through к первому кандидату), а не чистый type-check error — followup[M-138.2-overload-no-match-typecheck].
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 через lambda с явными типами аргументов:
ro f = fn(s str) -> int => t.m(s)
Дисамбигуация программистом
Когда автоматический резолв даёт 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()— фиксирует тип результата.
Ось режима {ro, mut, consume} — единая для ресивера и параметров (Plan 184, амендмент D326-Р13/Р14)
Статус: ✅ IMPLEMENTED (Plan 184 заход-6, 2026-07-07). Режим параметра — ось перегрузки наравне с receiver-mut (прецедент Plan 135). Факты реализации:
- Ключ перегрузки (чекер):
types/mod.rs— dup-детект сравнивает режим каждого параметра (is_mut+consume) наряду с типом/арностью/возвратом/receiver-mut;f(x T)/f(mut x T)/f(consume x T)— раздельные перегрузки, не «duplicate».- Мангл (раздельные C-символы):
MethodSig.param_modes: Vec<u8>(0=ro/1=mut/2=consume,sig_registry.rs). Мангл-суффикс режима (__r/__m/__cпо параметрам) добавляется ТОЛЬКО при реальной коллизии — когда сиблинг с ИДЕНТИЧНЫМИparam_c_types, но другим режимом уже занял имя (кучевой случай Р6:ref H ≡ H— mut/consume не меняют ABI handle). Чисто-ro/type-различённые наборы byte-identical (существующие символы не тронуты — проверено).mangle_fnрезолвит тело каждой перегрузки под своим символом пассом поparam_modes.- Диспатч (
emit_c.rs::narrow_by_param_mode): среди кандидатов, совпавших поparam_c_types, сужение по режиму от способности аргумента-биндинга (см. правило ниже). Хук на call-site свободной функции И instance-метода (ортогонален receiver-mut tiebreak).- Гейты: conformance 56/0;
nova_tests/inout_ref17/0 (p184_mode_overload_pos7/7 runtime: тройная ось + пара ro/mut + смешениеfn T @m(x T)/fn T mut @m(mut x T)); широкая byte-identity vs main-бинарь = 0.- Ограничение (scoped follow-up
[M-184-value-mut-mode-overload-abi]): для VALUE/примитивногоTперегрузка сmut-режимом меняет ABI параметра на указатель (T*, Р10), что требует согласования адресации на call-site с выбором перегрузки — кучевойT(канонический in-out, Р6) покрыт полностью; value/primmut-mode-overload — отдельный заход. Одиночный value/primmut x T(не перегруженный) работает как в заходе-4.
Амендмент (2026-07-06, sign-off владельца; см. ревизию D326).
Ось «изменяемость ресивера» (Plan 135: fn T @m() vs fn T mut @m(), bootstrap-статус выше,
строка «Receiver-mutability overload») обобщается до единой тройной оси режима
{ro, mut, consume}, действующей одинаково для ресивера (нулевой параметр) и обычных
параметров. Это делает @ формой параметра (Р14): различимы @m / mut @m / consume @m
И f(x T) / f(mut x T) / f(consume x T).
Декларация. Одноимённые функции/методы, различающиеся только режимом одного параметра
(включая нулевой — ресивер), — легальные перегрузки (Р13, по прецеденту ресивера). До
Plan 184 consume в оси перегрузки НЕ описан (пар mut/consume одного имени в std нет) —
расширение чистое, backward-compatible.
Резолв на call-site (правило выбора, Р14). Дополнительный фильтр перед «самым специфичным матчем» (фильтр 4), применяемый пер-параметр (и к ресиверу):
- ro-аргумент (аргумент —
ro-биндинг / rvalue / не в последней точке использования) → выбирается только ro-версия параметра. - mut-биндинг-аргумент → mut-версия приоритетнее ro (прецедент ресивера, Plan 135:
mut b; b.m()→ mut-overload). - consume-версия участвует в резолве ТОЛЬКО когда аргумент в последней точке
использования (это уже вычисляет consume-чекер, D156/D131); тогда consume специфичнее
mut. Иначе consume-кандидат исключён. Это даёт детерминизм без молчаливого потребления
живого биндинга.
Реализация (заход-6): consume-версия выбирается для owned-временного аргумента
(rvalue: результат вызова / литерал / конструктор — заведомо последняя точка,
is_rvalue_temp), где ни один живой биндинг не потребляется молча. Именованный биндинг в его последней точке (полный last-use через consume-чекер) — расширение внутри того же правила (маркер[M-184-consume-dispatch-named-lastuse]); сейчас такой аргумент идёт в ro/mut-версию, а явноеconsume-владение выражается конструктор-/вызов-формой.
Неоднозначность невозможна по построению. mut-версия годна только для изменяемого
места, consume-версия — только для owned-временного; классы аргумента взаимоисключающи, а
ro всегда годна, но наименее специфична. Значит для набора, различающегося режимом ОДНОГО
параметра, победитель единственен. Для (редкого) набора с противоположной вариацией режима
двух параметров, дающего ничью специфичности, диспатч реджектит вызов как ambiguous с
перечислением кандидатов (общий канон D84, фильтр 4) — молчаливого выбора нет.
Следствие для Р10 (форма параметра одна на режим). Поскольку mut x T — единственная
форма mut-параметра (ret. mut ref, D326-ревизия Р3), исходная неоднозначность
f(x T) vs f(mut x T) (D326-старый R4: неразличимы на вызове без маркера) исчезает по
построению: различие несёт изменяемость аргумента-биндинга, как у ресивера. Прежний код
E_OVERLOAD_REF_AMBIGUOUS не нужен.
Почему
Зачем перегрузка вообще
В существующей 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 Hash](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).
D267. Method coherence: extension — да, override чужого метода — E_METHOD_REDEFINITION
Статус: принято и реализовано (Plan 154.0, umbrella 154, 2026-06-13). Закрывает
[M-method-override-silent-noop].
Что
У Nova нет orphan rule (02-types §«Структурная проверка вместо impl»)
— метод на типе можно объявить из любого модуля (extension-методы). Но
переопределение существующего метода fn T @m с той же сигнатурой
(receiver-type + arity + arg-types + return + receiver-mut, все оси D84), что у
метода T.@m, уже определённого в другом модуле (std/prelude/импорт), —
compile-error E_METHOD_REDEFINITION.
Исключение: если пользователь сам объявил type T локально (shadow всего
типа), методы на этом T — его (Plan 62 user-wins, не override чужого).
Почему
Раньше это был silent no-op: type-check классифицировал дубль как prelude-shadow
(user-wins в env.fns), но codegen method_overloads резолвит call-site
first-match, а prelude/std prepend’ится первым → выигрывает существующее
определение, тело пользователя никогда не вызывается. Программист уверен, что
переопределил str.to_lower, а поведение не меняется (мёртвый код). Худший исход —
не ошибка, не override.
Глобальный override built-in/std метода к тому же coherence-хазард: stdlib и
чужие библиотеки, зовущие to_lower внутри, получили бы нелокальный сюрприз
(проблема monkey-patching из JS-прототипов). Прецедент строгости: Rust orphan rule,
Kotlin/C#/Swift (member/inherent wins, extension не переопределяет), Go (нельзя
добавлять к чужим типам).
Как сделать «свой» метод (разрешённые пути)
- Extension с другим именем:
fn str @shout()— ок (нет orphan rule). - Overload по сигнатуре:
fn T @m(int)+fn T @m(str)(D84). - Receiver-mut overload:
fn T @m()+fn T mut @m()(D-Plan 135). - Newtype + own-method:
type Locale { use _ str }+fn Locale @to_lower()— override-precedence (02-types §«Override через own-methods»). - Локальный re-decl типа:
type Range {...}+fn Range @step_by(...)(Plan 62 user-wins; receiver-тип объявлен локально).
Реализация
types/mod.rs check_module, Item::Fn:
для метода (receiver.is_some()), у которого classify_dup = Some(_) (shadow’ит
prelude/merged) и receiver-тип не в user_declared_types (типы из entry-peers) —
E_METHOD_REDEFINITION. Site type-check → компиляция падает до codegen.
Связь
- D84 — оси перегрузки (override = совпадение по всем осям; overload = хотя бы одна различается → разрешён).
- Plan 62 — prelude-shadow для type/const/free-fn + локальный type re-decl (user-wins + W_PRELUDE_SHADOW) — не задет (фикс только для методов на не-локальных типах).
D268. Opt-in конформность протоколов: #impl(P) на метод-декларации
Plan 154.1 Ф.2. Ведущий атрибут #impl(P1 + P2 + ...) (D186, ранее только перед
type-декларацией) распространён на метод-декларации:
#impl(Display)
fn int @display(mut sb StringBuilder) -> () { sb.append(@) }
Семантика — opt-in, НЕ номинальная
Конформность остаётся структурной: «есть подходящий метод → тип годится для бонда
[T Display]». #impl(P) — необязательная пометка, которая:
- проверяет подпись метода против объявления
@mв протоколеP(параметры + возврат + receiver-mutability, с подстановкойSelf↔ receiver-тип); - явно привязывает
Pк receiver-типуT— наполняетtype_impl_protocols[T] += P(та же карта, что у type-уровня#impl), как если быPбыл перечислен наtype-декларацииT.
Отсутствие #impl — не ошибка: структурная конформность по-прежнему удовлетворяет
бонд. #impl только ДОБАВЛЯЕТ проверку + привязку; миграция существующего кода не нужна.
Путь к required-конформности (как в Rust) — отдельным шагом ([M-154.1-required-conformance]).
Три кода ошибок (checker, на методе fn T @m с #impl(P))
| код | условие |
|---|---|
E_IMPL_UNKNOWN_PROTOCOL | P — не известный тип ИЛИ не протокол |
E_IMPL_NOT_A_PROTOCOL_METHOD | имя @m ∉ методов P (или у fn нет receiver’а) |
E_IMPL_SIGNATURE_MISMATCH | подпись @m (параметры / возврат / receiver-mut) ≠ объявлению P |
Renamed-протоколы (D237) дают E_PROTOCOL_RENAMED с подсказкой.
Реализация
- AST:
FnDecl.impl_protocols: Vec<String>(ast/mod.rs). - Парсер:
#implразрешён передtypeИfn(#from_fields/#zero_on_move— по-прежнему только передtype);impl_protocolsпрокидывается вparse_fn(parser/mod.rs). - Чекер:
verify_method_impl_protocolsвcheck_module(types/mod.rs) — переиспользуетcheck_signature_match/check_receiver_mut_matchот type-уровня. - Codegen: метод-уровень
#implбиндитtype_impl_protocols[recv.type_name] += P(emit_c.rs рядом с type-уровневым populate’ом).
Связь
- D186 (Plan 91.9) — type-уровневый
#impl+ bare-call synthesis gate. - D237 — renamed-протоколы (Display/Debug/Equal/…).
- D269 — первый потребитель: конкретные Display/Debug примитивов.
D269. Конкретные Display/Debug примитивов + element-dispatch hardening
Plan 154.1 Ф.1 + Ф.3. Закрывает класс silent-mis-dispatch «primitive-element protocol-метод».
Проблема
Mono-тело контейнера Vec[T] @debug вызывает @data[i].debug(sb) на ЭЛЕМЕНТЕ. Когда
T — примитив без конкретного @debug, single-key method_receivers-fallback в codegen
тихо роутил вызов в erased-стаб самого контейнера (Nova_Vec_method_debug,
type-confused no-op) — vec_debug_pos выдавал мусор.
Fix 1 — громкая ошибка (Ф.1)
В instance-call dispatch (emit_c.rs,
зеркало static-guard’а): если receiver — примитивный C-тип, а single-key fallback
резолвит method в generic-тип (is_instance && is_generic_type), вызов невозможен →
E_PRIMITIVE_NO_PROTOCOL_METHOD (вместо эмиссии неверного/no-op вызова).
Fix 2 — конкретные impl примитивов (Ф.3, Variant B)
int / f64 / bool / char / str получают конкретные #impl(Display) + #impl(Debug)
(std/prelude/protocols.nv). Тогда element-dispatch
резолвит int.debug через direct-dispatch (Nova_int_method_debug) — мис-диспатч
устранён «даром», и guard для int не срабатывает (метод есть).
- Тела:
sb.append(@)через типизированные@append-overload’ы StringBuilder (@append(int)/@append(f64)/@append(bool)добавлены;str/char/[]u8были) — без промежуточнойnova_strот interp-temp. Debugchar/str(кавычки+escape) использует interp${@:?}→ conv.hnova_*_to_debug_strнапрямую (не рекурсирует: interp для примитивов зовёт форматтер, не метод). - f32 (followup 2026-06-14,
e38f30ee): догнал остальные — conv.hnova_f32_to_str/_to_debug_str(widen→double +%g, 6-знач прячет f32→f64 хвост),@append(f32)(x as f64), веткаnova_f32в interp display+debug map + exclusion list. (Остался отдельный коэрсинг-багVec[f32].from([f64-литералы])—[M-154.1-f32-literal-coercion], не про печать.)
Инвариант interp
${x} / ${x:?} для примитивов по-прежнему идут через conv.h-форматтеры напрямую
(interp-путь исключает примитивы из method-dispatch), поэтому конкретные методы их не
перехватывают и не ломают.
Followups codegen-hardening (2026-06-14)
Два общих codegen-фикса, всплывших при доведении f32:
- Self-method-call overload по типам аргументов (
e38f30ee). Вызов@m(args)(receiver@, внутри тела метода) при нескольких overload’ах тайбрейкал только по receiver-mutability (Plan 135 Ф.2), игнорируя типы аргументов. С@append-overload’ами (всеmut)@append(x as f64)брал ПЕРВЫЙ (базовыйstr) → C передавалnova_f64вnova_str- параметр. Фикс: сперва сузить own-instance overload’ы поparam_c_types == arg_c_types, потом тайбрейк поrecv_mutable. (Existing self-@appendвсегда целилиstr-базу → латентный баг.) E_UNKNOWN_STATIC_METHOD(99dee599,[M-154.1-static-call-unresolved-loud]). ВызовPrim.method(...)на примитиве, дошедший до codegen fall-through (все валидные primitive static-методы/интринсики —str.from,str.from_bytes_lossy— резолвятся раньше), раньше эмитилnova_fn_<prim>_<method>→ undefined-символ на линковке. Теперь — громкий compile-error. Узко: только примитив-ресиверы (модуль-qualified free-fn и user-типы не задеты).
Связь
- D268 —
#implopt-in (механизм привязки протокола). - D229 / D237 — Display/Debug протоколы +
${:?}interp-spec. - D267 — sibling из той же зоны диспатча (cross-module override coherence).
D263. Vec restructure-ops + оператор + (@plus ≡ @concat)
Plan 153.5 (commit e8f700e4). Vec[T]/[]T получают слой restructure-ops —
операции, строящие НОВЫЙ вектор из существующих данных или переставляющие целые прогоны
элементов. Реализовано на Nova-body поверх bulk RawMem.copy
(std/collections/vec/restructure.nv, co-equal файл
folder-модуля collections.vec). Закрывает Q-vec-operator-plus (см.
open-questions.md).
Решение
| Метод | Сигнатура | Семантика |
|---|---|---|
| concat | Vec[T] @concat(other Vec[T]) -> Vec[T] | non-mutating join: одна аллокация ровно на a+b + два bulk-copy; операнды нетронуты |
оператор + | Vec[T] @plus(other Vec[T]) -> Vec[T] => @concat(other) | a + b = НОВЫЙ Vec (как str @plus, D46); a += b ≡ a = a + b |
| rotate_left | Vec[T] mut @rotate_left(n int) -> @ requires n >= 0 | циклический сдвиг влево in place; n mod len; O(len) time, O(min(n,len−n)) scratch |
| rotate_right | Vec[T] mut @rotate_right(n int) -> @ requires n >= 0 | сдвиг вправо ≡ left на len − k |
| drain | Vec[T] mut @drain(range Range) -> Vec[T] requires start>=0 && end>=start && end<=@len | вырезать [start,end), вернуть удалённое владеемым; суффикс сдвигается вниз; self короче на range.len() |
| insert_slice | Vec[T] mut @insert_slice(i int, sl []T) -> @ requires 0<=i && i<=@len | вставить срез на i (i==len=append); делегирует в @splice (под D239 []T ЕСТЬ Vec[T]) |
Принципы.
+не мутирует (как Kotlin/Python/Ruby+):a + b— свежий буфер, операнды нетронуты. Ростain place — отдельныйa.append(b)(mutate.nv);a += bлоуэрится в свежий concat-Vec, НЕ в in-place append (+=≡=-через-+, единая семантика оператора).@concat≠@append.@append(Vec[T])(mutate.nv) — in-place bulk-merge вself;@concat— новый Vec. Один слой = одна семантика (инвариант I4 плана).@insert_slice=[]T-вариант@splice(берётVec[T]): имя документирует slice-аргумент (RustVec::splice/ Goslices.Insert), тело — делегация (overlap-safe → self-insert корректен).- Контракты
requires(rotate/drain/insert_slice) — out-of-bounds/отрицательный сдвиг → runtime-panic (D13-семантика контрактов).
Codegen (operator + / +=)
@plus/@concat — Nova-body; operator-lowering +/+= добавлен в
emit_c.rs двумя минимально-таргетными точками:
Stmt::Assign:a += b/a -= bна типе с method-лоуэрингом+(nova_str,Nova_Vec____*, любойNova_*-record с@plus) десугарится в синтез-Binary{Add}→ re-emit через полный binop-dispatch (а не сырой Ca += bна struct/pointer операнде → CC-FAIL). Перегружаемы толькоAdd/Sub.BinOp::Add:Vec[T] + Vec[T]→vec_method_call(.., "plus", ..)ПЕРЕД generic-Nova_*-sum-pointer Add-arm (тот эмитит голый_method_plus(l,r)без инстанцирования mono-тела → undefined symbol на линковке);vec_method_callрегистрирует mono-инстанс первым.
AMEND (2026-06-14, plan-153.5-restructure commits 1c323d0e + 16753d23): flatten реализован
[][]T.flatten() -> []T больше НЕ отложен — реализован вместе с фундаментом
вложенных generic-ресиверов произвольной глубины (см. D145 AMEND,
2026-06-14). Маркер [M-153.5-flatten-nested-receiver] РАЗРЕШЁН.
| Метод | Сигнатура | Семантика |
|---|---|---|
| flatten | Vec[Vec[T]] @flatten() -> Vec[T] (≡ [][]T @flatten() -> []T под D239) | конкатенация внутренних рядов в один новый Vec[T]; pre-size with_capacity(Σ inner.len()) + bulk @append(inner) на ряд (copy-fast-path, операнды нетронуты); пустые ряды/внешний — корректно |
Реализация — Nova-body в
std/collections/vec/restructure.nv:
сперва суммирует все inner.len() для точной пре-аллокации out, затем bulk-копирует
каждый ряд out.append(inner) (тот же RawMem.copy fast-path, что @concat/@append).
Production-форма — carrier Vec[Vec[T]] @flatten() (совпадает с записью stdlib).
Что разблокировало (корень — D145 AMEND): обе принимаемые формы ресивера раньше
теряли вложенность — Vec[Vec[T]] ПАРСЕР отвергал в carrier-слоте, [][]T монорфизатор
биндил T в непосредственный элемент (Vec[int]), не во внутренний (int). Фикс —
структурная унификация typevar для вложенных ресиверов на любой глубине в ОБОИХ
парсере (carrier-слот принимает parse_type + сбор free-typevars; slice-форма считает
глубину [] и спускается до внутреннего Named) И монорфизаторе (рекурсивный
infer_type_param_binding биндит receiver-typevar в element-of-element… до самого
внутреннего, depth-agnostic). Cross-cutting: тот же []T-method-dispatch путь, через
который идут все slice-методы stdlib — flat []T (depth 1) остался byte-identical.
Связь
- D46 — operator overloading через
@plus(str-прецедент; этот D-блок распространяет наVec[T]). - D239 —
[]T ≡ Vec[T](insert_slice-аргумент;Vec[Vec[T]] ≡ [][]Tдля flatten). - D145 AMEND — вложенные generic-ресиверы произвольной глубины (фундамент
@flatten). - Plan 153.1 mutate.nv —
@append/@splice(in-place; restructure НЕ дублирует;@flattenпереиспользует bulk@append).
D285 — Receiver-compatibility rule — blanket dispatch priority (Plan 164 Ф.3)
Status: ACTIVE (Plan 164 Ф.3, 2026-06-16). Зависит от: D355 (blanket protocol-receiver), D84 (dispatch priority). Маркеры: [M-codegen-blanket-generic-param-order] ✅ CLOSED Plan 164 Ф.2; [M-153.2-drop-z-prefix] ✅ CLOSED Plan 164 Ф.4; [M-impl-attr-generic-protocol] ✅ CLOSED Plan 164 Ф.1.
§1 Проблема (root cause). До Plan 164 Ф.3 codegen при резолве метода по имени применял стратегию «last-wins» из method_receivers (HashMap, один FnDecl на ключ (type, name)). При наличии конкретного метода с тем же именем на несовпадающем типе (напр., CharsIter.count() из std/strings) он побеждал над blanket-методом fn[I Next[T]] I @count() для FilterIter — потому что конкретный регистрировался позже и перезаписывал запись. Результат: FilterIter.count() диспетчился в CharsIter.count() → CC-FAIL.
§2 Правило (receiver-compatibility). При резолве вызова recv.method(args):
- Ищется конкретный метод с точным совпадением receiver-типа (
method_receivers[base_type]илиmethod_overloads). Если найден — используется. Конкретный метод всегда приоритетнее blanket (D84 §«по receiver-типу»). - Если конкретный не найден — ищется blanket-метод (typevar-ресивер, bound = протокол). Receiver считается совместимым, если его тип зарегистрирован в
type_impl_protocols[base_type]для соответствующего протокола. - Concrete метод на НЕСОВПАДАЮЩЕМ типе не может победить blanket на СОВМЕСТИМОМ типе. «Несовпадение» = base_type в
method_receiversне совпадает с actual receiver.
§3 Алгоритм (codegen, Plan 164 Ф.3). Перед lookup в method_receivers (last-wins single-key fallback):
- Извлечь base-type из
obj_ty(stripNova_/NovaValue_prefix,*, часть до первого____). - Если не примитив → сканировать
mono_method_declsпо имени метода, найти blanket-FnDecl (typevar-key длиной ≤2 символов, all-uppercase). - Проверить, что все bounds blanket-fn удовлетворены: для каждого bound
Protoпроверитьtype_impl_protocols[base_type]черезimpl_spec_base_name. - Если все bounds OK → диспетчить через blanket (bind receiver typevar → concrete type). Иначе — продолжить в fallback.
§4 Приоритет impl-протоколов. type_impl_protocols[T] пополняется из атрибута #impl(P[U]) на декларации метода (Plan 164 Ф.1, impl_spec_args_text). Наличие записи = декларация намерения разработчика; отсутствие = тип не участвует в blanket для данного протокола.
§5 Ограничения V1. (a) Алгоритм §3 сканирует mono_method_decls O(N) по имени — достаточно для текущего масштаба. (b) Только один blanket-кандидат per-name per-receiver предполагается (конфликт двух blanket = E_BLANKET_CONFLICT, D355 §5). (c) Receiver-type с несколькими #impl протоколами корректен — каждый bound проверяется независимо.
Кросс-ссылки: D355 (blanket-receiver dispatch), D84 §1 (concrete > blanket приоритет по receiver-типу), D268 (method coherence — запрет override чужого метода).
Реализовано в. compiler-codegen/src/codegen/emit_c.rs (~line 25289): блок receiver-compatibility dispatch (Plan 164 Ф.3, commit af33bc76). compiler-codegen/src/parser/mod.rs: impl_spec_base_name() / impl_spec_args_text() helpers (commit 3846a976). compiler-codegen/src/types/mod.rs: verify_impl_protocols + check_signature_match_with_subst + normalize_type_str (commit 3846a976). std/collections/vec_iter_zc.nv: #impl(Next[T/U]) annotations на adapter @next() методах; VecIter[T] @next() также annotated (commit 70079788). Тесты: nova_tests/plan164/ (6/6 PASS).