Types — record, sum-type, protocol, generic, поля
Решения этой группы задают систему типов Nova: четыре формы объявления
данных, структурные контракты-протоколы, семантику передачи параметров
и мутабельность полей, делегацию через use. Синтаксические детали
(методы через @, generic-применение [T], литералы) — в
03-syntax.md.
| # | Решение | Status |
|---|---|---|
| D17 | Объявление типов: единый синтаксис без | | revised → D52 |
| D52 | Объявление типов revised: newtype, alias, sum через leading | | revised → D406 |
| D406 | Sum-type синтаксис: enum маркер вместо leading |; inline enum в type-позиции | active |
| D53 | Унификация: protocol под type, protocol как kind-токен | active |
| D55 | Literal coercion в позиции с явным типом: sum-конструкторы и record-литералы | active |
| D42 | protocol keyword для структурных интерфейсов | revised → D53 |
| D15 | Структурные интерфейсы | revised → D42 → D53 |
| D39 | Embed и delegation: use name Type (alias обязателен) | active |
| D32 | Семантика передачи параметров | revised для полей → D36 |
| D36 | Поля типа: дефолт mutable у mut-binding’а, readonly для never-mut | active |
| D175 | readonly field — полный freeze, транзитивность (амендмент D36) | active |
| D176 | readonly T — тип-модификатор, coercion rules, zero overhead | active |
| D66 | Self universal: ссылка на обобщающий тип в методах, effects, protocols | active |
| D72 | Generic bounds через [T Protocol] — protocol как тип | active |
| D110 | Ghost state — spec-only bindings | active |
| D122 | Hybrid dispatch для bound-K methods | active |
| D123 | Tuple monomorphization | active |
| D215 | Named tuple fields + value/reference allocation contract | active |
| D119 | Method-level type parameters в generic methods | active |
| D372 | Canonical .new() constructors (convention) | active |
| D181 | Array methods — -> @ fluent mut chain + slice syntax | active |
| D182 | Self в return-type static methods — required form для parametric types | active |
| D183 | Canonical comparison protocols + default method bodies (Plan 91.8a) | active |
| Pointer mutability — running-current flip-scan (Plan 147; RETRACTED 2026-06-12 → D246) | retracted | |
| D246 | Три оси мутабельности (L1 binding / L2 view / L3 pointee); restores *T ≡ *ro T universally; E_REDUNDANT_POINTER_RO (Plan 147) | active |
| D281 | Module-level field privacy type X priv { … } — bare priv = module-private (Plan 160, D281) | active |
| D355 | Blanket protocol-receiver methods (ex-D282, renumber 2026-07-03) fn[I Next[T]] I @m — typevar-ресивер + bound-dispatch (Plan 161, G-F) | active |
| D284 | EnumerateIter[I, T] — zero-cost enumerate adapter; per-type @zenumerate() dispatch; tuple parametric return (Plan 162) | active |
| D290 | Iterator value-records: VecIter[T] value (GC-pointer fields covered by fiber arena) + Range/RangeIter/StepRangeIter/ReverseRangeIter value (int-only, pure stack) — zero malloc in adapter chain (Plan 165) | active |
| D307 | File-private visibility priv(file) fn/type/const — лесенка priv(file) ⊂ module ⊂ export; E_FILE_PRIV_LEAK; file-discriminated codegen; dedup одноимённых в peer-файлах (Plan 170) | active |
| D315 | ResolvedType — единый канонический носитель типа; проверки/совместимость/конверсии/перевод-в-C выводятся из него; type_ref_to_c ретайрится; сахар нормализуется; ABI выводится, не хранится (Plan 172.1, D315) | active |
| D310 | Type-set bounds: type Name set T1 | T2 | … — именованное множество конкретных типов как generic-bound; SignedInt/UnsignedInt; Go-style type-constraint (Plan 172.3) | active |
D17. Объявление типов: единый синтаксис без |
⚠️ REVISED. Заменено D52. Старый синтаксис (
type X = Yдля alias,type X = A, Bдля sum) — запрещён. Новый:type X Y(newtype),type X alias Y(alias),type X | A | B(sum). Текст ниже — для исторической справки.
Что
Все формы объявления типа — record, позиционная структура, unit, alias,
sum-type — используют один разделитель списка (запятая) и
синхронизированы по =: = ставится только когда справа выражение
типа (alias или sum-type), не когда форма данных ({...} или (...)).
Правило
Полный синтаксис:
// alias
type UserId = u64
// record (именованные поля)
type User { id u64, name str }
// позиционная структура
type Point(f64, f64)
// unit-тип (без полей)
type Empty
// sum-type
type Color = Red, Green, Blue
type Shape =
Circle { radius f64 },
Square { side f64 },
Triangle { a f64, b f64, c f64 }
type Result[T, E] = Ok(T), Err(E)
Парсер однозначен по первому токену после имени типа:
После type X идёт | Что это |
|---|---|
{ ... } | record-структура |
( ... ) | позиционная структура |
| ничего | unit-тип |
= потом тип | alias |
= потом список вариантов через запятую | sum-type |
type X { ... } — это record с полями. Методы внутри {...}
запрещены: набор методов = поведение, для него используется protocol
(D42). Эффекты —
это protocol, использованный в позиции эффекта между ) и ->
(04-effects.md → D18).
Создание значений и pattern matching — обычные:
ro p = Point(1.0, 2.0)
ro u = User { id: 1, name: "alice" }
ro c = Circle { radius: 5.0 }
match shape {
Circle { radius } => 3.14159 * radius * radius
Square { side } => side * side
Triangle { a, b, c } => heron(a, b, c)
}
Field punning для record-литералов: если имя поля совпадает с именем переменной в скоупе, можно писать имя один раз:
ro key = "alice"
ro value = 42
ro entry = Entry { key, value } // shorthand
ro entry = Entry { key, value, extra: "data" } // можно смешивать
Парсер однозначен: name: → полная форма, name, или name} →
shorthand. Если переменной нет в scope — compile error.
Partial pattern matching — две эквивалентные формы:
// явная — с маркером ..
match @buckets[idx] {
Occupied { value, .. } => Some(value)
_ => None
}
// неявная — без маркера, остальные поля игнорируются
match @buckets[idx] {
Occupied { value } => Some(value)
_ => None
}
Явная форма — visual cue «здесь ещё поля». Неявная — краткость.
Переименование при деструктуризации остаётся явным: Occupied { key: k, value }.
Construction всегда требует все обязательные поля — частичное
заполнение типа Rust ..default отдельным синтаксисом не зафиксировано.
Почему
- Один разделитель списка на весь язык — запятая. Параметры,
элементы массивов, поля записи, варианты sum-type — везде
,. Меньше правил, меньше ошибок LLM. =означает «справа выражение типа». Когда справа форма данных —=лишний.- Парсер по первому токену — никакого backtracking, чистые сообщения об ошибках.
Что отвергнуто
- ML-style
| Variant(OCaml/Haskell/F#/Rust). Два разделителя подряд (= |), чужд языкам не из ML-семейства, дублирует роль запятой. type Point = | Point(f64, f64)для одно-вариантного sum-type — дубль. Sum-type с одним вариантом и структура — это одно и то же.type User = { id u64, name str }для record.=лишний, когда справа форма данных.
Связь
- 03-syntax.md → D27 — массивы (
[]T,[N]T) как отдельные конструкции типов, не вариантыtype. - 03-syntax.md → D38 — generic-применение
Имя[T]для параметризованных типов. - 02-types.md → D42
— почему
protocolотдельный keyword, а неtype X = { методы }. - 02-types.md → D36
— префиксы полей (
readonly,mut) и group-syntax внутри record.
D52. Объявление типов revised: newtype, alias, sum через leading |
⚠️ REVISED. Синтаксис sum-type заменён D406:
enum Variant | ...вместо leading| Variant | .... Остальные формы (newtype, alias, record, tuple, unit) без изменений.
Что
Полная пересборка D17.
Один keyword type для всех data-форм, никаких = в декларациях,
форма различается первым токеном после имени. Шесть форм:
- newtype —
type X Y(X — новый тип, типизированно отличный от Y, Go-style) - alias —
type X alias Y(X и Y совместимы, для длинных дженериков) - record —
type X { поля }— heap-allocated reference type (GC-managed) - tuple —
type X(типы)— stack-allocated value type (позиционные поля.0/.1) - named tuple —
type X(name1 T1, name2 T2)— stack-allocated value type (именованные поля.name) (D215, Plan 120) - unit —
type X(ничего после имени) - sum —
type X enum A | B | C(enumмаркер обязателен; D406)
Allocation contract (D215, Plan 120): скобки кодируют семантику размещения:
()= stack-allocated value type, copy-семантика при передаче;{}= heap-allocated reference type, GC-tracked. Выбор формы явно документирует производительность и lifetime ожидания.
Sum-варианты могут иметь числовые discriminants с auto-increment.
protocol остаётся отдельным keyword’ом для поведения
(D42).
Правило
Полный синтаксис
// 1. Newtype — type X Y, без =
type UserId u64
type Email str
type Score f64
// 2. Alias — type X alias Y, для сокращения длинных дженериков
type StringMap[V] alias HashMap[str, V]
type Cache[K, V] alias HashMap[K, (V, Time)]
// 3. Record — type X { поля }
type User { id u64, name str }
type Point3D { x, y, z f64 } // group-syntax (D36)
type Account {
ro id u64
balance money
mut last_access time
}
// 4a. Positional tuple — type X(типы)
type Point(f32, f32) // .0 / .1 access
type Pair[A, B](A, B)
// 4b. Named tuple — type X(name type, ...) (D215, Plan 120)
type Vec3(x f64, y f64, z f64) // .x / .y / .z access; stack-allocated
type Color(r u8, g u8, b u8, a u8)
type Generic[T](value T, count int)
// 5. Unit — type X
type Empty
type Sentinel
// 6. Sum — type X enum A | B (enum маркер обязателен; D406)
type Color enum Red | Green | Blue
type Direction enum North | East | South | West
// Sum многострочный — если варианты на новых строках, | обязателен у каждого
type Result[T, E] enum
| Ok(T)
| Err(E)
type Shape enum
| Circle { radius f64 }
| Square { side f64 }
| Triangle { a f64, b f64, c f64 }
Парсер однозначен по первому токену после имени (с учётом дженериков)
После type X (или type X[params]) идёт | Форма |
|---|---|
enum | sum (D406) |
protocol | protocol-тип (D53) |
effect | effect-тип (D53) |
set | type-set bound (D310) |
value { | value-record — stack-allocated (D228/D277/D290) |
( + ident + bare-type | named tuple (D215) — (name1 T1, name2 T2) |
( + bare-type | positional tuple — (T1, T2) |
{ | record — heap-allocated (GC-managed) |
alias | alias |
<base-type> enum | sum с явным базовым типом для discriminants |
| идентификатор/тип, конец строки | newtype |
| конец строки сразу | unit |
Парсер видит первый токен — сразу знает форму. Для ( — один
дополнительный lookahead: если (IDENT type → named tuple,
иначе → positional tuple. Никакого backtracking.
Модификаторы type-declaration
Помимо kind-токена, type-declaration может нести модификаторы — они не меняют «форму» типа, а добавляют квалификаторы:
| Модификатор | Семантика | D-block |
|---|---|---|
export | виден снаружи модуля | D47 |
value | stack-allocated (по значению, не в GC-куче) | D228/D290 |
priv | поля module-private по умолчанию | D281 |
priv(type) | поля type-private по умолчанию | D281 |
priv(file) | символ виден только в этом файле | D307 |
consume | must-be-consumed affine type | D133 |
priv(file) — двойная позиция: prefix перед type (priv(file) type X { … })
или modifier после имени (type X priv(file) { … }). Обе формы эквивалентны.
Для fn и const только prefix-форма: priv(file) fn f(), priv(file) const K.
Грамматика type-declaration:
[export] type Name[T] [value] [priv|priv(type)|priv(file)] [consume] { … }
эквивалентно:
[export|priv(file)] type Name[T] [value] [priv|priv(type)] [consume] { … }
Модификаторы комбинируются: export type Job value priv consume { … }.
Поля внутри {…} могут иметь field-level модификаторы:
ro (D175), mut (D36), priv / priv(type) / priv(file) (D281/D307).
Sum-варианты с числовыми discriminants
// Auto-increment без явных значений (от 0)
type ExitStatus enum Ok | Failure | Critical // 0, 1, 2
// Auto-increment от заданного
type FileMode enum Read = 1 | Write | Execute // 1, 2, 3
// Все явные
type ErrorCode enum
| NotFound = 404
| Unauthorized = 401
| InternalError = 500
// С отрицательными
type Sign enum Negative = -1 | Zero = 0 | Positive = 1
// Decreasing/non-monotonic — разрешено
type Code enum A = 10 | B = 5 | C // A=10, B=5, C=6
// Явный базовый тип
type Bit u8 enum Off = 0 | On = 1
type HttpCode i32 enum Ok = 200 | NotFound = 404
⚠ Явный базовый тип пока не реализован (parser drift, 2026-05-27). Формы с
u8/i32/etc. между именем иenumпарсер отвергает. Работает только дефолтная форма (без базового типа, implicitint). См. Plan 105.
Правила discriminants:
- Базовый тип — дефолт
int. Опционально явный (type X i32 enum,type X u8 enum). - Auto-increment от первого варианта:
- Первый без значения → 0.
- Каждый следующий без значения → предыдущий + 1.
- Отрицательные значения — разрешены.
- Decreasing/non-monotonic последовательности — разрешены.
- Конфликт значений (два варианта с одинаковым discriminant) — запрещён компилятором.
- Mixed (некоторые с полями, некоторые без, у всех discriminants) —
разрешено:
type Event enum | Click(x int, y int) = 1 | KeyPress(key str) = 2 | Idle = 3 | Data { payload []u8, crc u32 } = 10
Cast между sum-типом и числом
Sum → int — безопасный, всегда работает:
ro c = Red // Color
ro n = c as int // 0 (если auto-increment)
ro e = NotFound // ErrorCode
ro n = e as i32 // 404
int → Sum — через pattern match obligation:
ro n = read_from_db()
ro c = match n {
0 => Red
1 => Green
2 => Blue
_ => throw InvalidColor
}
Никакого n as Color — программист сам обрабатывает «нет такого
варианта». Это согласовано с эффектом Fail[E].
stdlib может предоставлять Color.from_int(n) для удобства:
fn Color.from_int(n int) Fail[InvalidVariant] -> Color =>
match n {
0 => Ok(Red)
1 => Ok(Green)
2 => Ok(Blue)
_ => Err(InvalidVariant)
}
Параметризованные sum
type Option[T] enum Some(T) | None
type Result[T, E] enum Ok(T) | Err(E)
type Tree[T] enum
| Leaf
| Node { value T, left Tree[T], right Tree[T] }
Параметры в [...] после имени работают везде, как и раньше.
Сравнение alias и newtype
type AliasUserId alias u64
type NewUserId u64
ro a AliasUserId = 42 // ok
ro b u64 = a // ok — alias совместим с u64
ro c u64 = 42
ro d AliasUserId = c // ok — обратное тоже работает
ro n NewUserId = 42 // ok (литерал подгоняется под целевой тип)
ro e u64 = n // ОШИБКА: NewUserId не u64
ro f u64 = n as u64 // ok через cast
Альтернативу newtype через record-обёртку (type X { value u64 })
никто не запрещает, но type X u64 — компактнее и привычнее
программистам с фоном Go.
Field punning — расширено и обязательно
D52 расширяет field punning из D17 двумя правилами:
1. Shorthand для @field-доступов (новое в D52):
type RangeIter { end int, inclusive bool, mut cur int }
fn Range @iter() -> RangeIter =>
{ @end, @inclusive, cur: @start }
// ↑ ↑ ↑
// @end shorthand полная форма (имя поля cur ≠ start)
{ @end } означает «поле end, значение @end (то есть self.end)».
По симметрии с D17 ({ name } для переменной name в scope) —
теперь { @field } для self-доступа.
2. Shorthand обязателен, когда имя поля совпадает с источником:
// Переменная в scope:
ro key = "alice"
ro value = 42
ro entry = Entry { key, value } // ✓ обязательная форма
ro entry = Entry { key: key, value: value } // ✗ ОШИБКА: избыточная форма
// @field-доступ:
ro r = { @end, @inclusive, cur: @start } // ✓
ro r = { end: @end, inclusive: @inclusive, ... } // ✗ ОШИБКА: избыточная
// Явная форма обязательна, когда имя источника отличается:
ro entry = Entry { name: user_name } // ✓ имя поля ≠ переменной
ro r = { cur: @start } // ✓ имя поля cur ≠ start
ro r = { end: other.end } // ✓ источник — выражение, не @field
Парсер: { name/{ @name/{ name,/{ name } — shorthand;
{ name: expr — полная форма. После : ожидается выражение,
но если выражение — это ровно тот же identifier или @+identifier,
что и имя поля → ошибка компиляции «избыточная форма, используйте
shorthand».
Status: ✅ enforced (2026-05-17, commit 34666922c35). Реализация
в compiler-codegen/src/types/mod.rs RecordLit walker. AST flag
RecordLitField.at_shorthand различает parser-generated @field
shorthand от explicit { field: @field } (одинаковая AST форма).
Test guards: nova_tests/negative_capability/d52_redundant_field_literal_rejected.nv
d52_redundant_self_field_rejected.nv.
Mixed разрешён:
{ @end, @inclusive, cur: @start, kind: "iter" } // shorthand + полные
Когда расширение работает:
| Имя поля | Источник | Правило |
|---|---|---|
name | переменная name в scope | shorthand { name } обязателен |
name | @name (self-поле) | shorthand { @name } обязателен |
name | переменная other (другое имя) | полная форма { name: other } |
name | @other или выражение | полная форма { name: @other } |
name | obj.field | полная форма { name: obj.field } |
name | литерал, вызов, любое выражение | полная форма |
Pattern matching и construction
match @buckets[idx] {
Occupied { value, .. } => Some(value) // partial с ..
Occupied { value } => Some(value) // partial без ..
_ => None
}
Construction всегда требует все обязательные поля. Частичное
заполнение типа Rust ..default отдельным синтаксисом не зафиксировано.
Что запрещено
type X = Yдля alias — старый D17 синтаксис, заменён наtype X alias Y.type X = A, Bдля sum — заменён наtype X enum A | B.type X = { ... }для record — синтаксис никогда не был активным (D17 уже отвергал),=в этой позиции запрещён.,для разделения вариантов sum — заменено на|.- Sum без
enumмаркера — запрещён (type X Red | Green✗,type X enum Red | Green✓). См. D406. - Single-variant sum — запрещён (как в D17), используйте record.
- Конфликт discriminants — запрещён.
- Избыточная форма
{ name: name }— обязателен shorthand{ name }. Аналогично{ field: @field }— обязателен{ @field }. Если имя источника совпадает с именем поля, программист обязан использовать shorthand. См. «Field punning» выше.
Почему
- Системность. В D17 правило «
=для выражений типа, без=для форм данных» работало для alias, но спотыкалось на sum-type:type Color = Red, Green, Blue— справа не «выражение типа» в обычном смысле, а список конструкторов. С D52 sum обрабатывается как именованная форма (через|), как и record/tuple/unit. - Никаких
=в декларациях типов — устраняется напряжение «иногда есть, иногда нет».=остаётся за binding’ом значений (let x = ...) и parameter defaults (если будут). - Newtype как first-class. Domain-modeling (
type Email str,type Score f64) даёт реальную защиту типов без шумной record-обёртки. Прецедент Go (type UserId int64). - Discriminants для wire-протоколов. HTTP-коды, syscall-коды, serialization tags — программист может задать стабильные значения, как в C/TS/Swift enum.
- Парсер однозначен по первому токену — никакого lookahead глубже одного-двух токенов. AI-friendly: LLM с одного взгляда понимает форму.
- Leading
|для sum — visual symmetry: все варианты выровнены, прецедент OCaml/F#/Scala 3. - Согласованность с D1 «protocols + data, без классов» —
typeтолько для данных,protocolотдельно для поведения. - Field punning расширен и обязателен. Один способ записать
«поле = источник с тем же именем» — shorthand. Запрет избыточной
формы
{ name: name }устраняет «два пути к одному результату», что AI-unfriendly (LLM генерирует случайно). Также покрывает{ @field }для self-доступов — частый паттерн в record-литералах методов-конструкторов. Прецедент: TS/Rust имеют shorthand, но не делают его обязательным; Nova идёт строже ради единого стиля (D40/D43-стилевая последовательность).
Что отвергнуто
- Сохранить
type X = Yдля alias. Создаёт асимметрию: alias и sum с=, record/tuple/newtype без — нет единого правила. - Kind-токен
enumс фигурными скобками (type X enum { A, B }). Заменено наtype X enum A | Bбез скобок (D406). - Литералы как sum-варианты (
type State | "open" | "closed", TS-style literal types). Полезно, но это отдельная фича (subtyping, runtime representation), отложена на следующую версию языка. - Итерация по вариантам (
for c in Color). Связано с reflection и stdlib, отложено до Q9. type X protocol { ... }под единымtype. Семантически protocol — поведение, не данные; отдельный keyword чище.type X newtype Yс явным kind-токеном.type X Yбез токена короче и согласовано с Go.- Implicit cast int → Sum. Type-небезопасно (число может не попасть в варианты). Только через pattern match.
Цена
- Большой breaking change. Все существующие декларации в spec/, decisions/, examples/ переписать. Кода пока мало, миграция разовая.
aliasстановится keyword’ом. Раньше был обычным идентификатором.- Программистам с фоном Rust/TypeScript:
type X = Yбольше не alias, а ошибка. Адаптация через документацию. - Парсинг
type X Y(newtype) vstype X(unit) — различие по следующему токену (тип vs конец строки). Просто, но требует внимательности. |имеет двойную роль — разделитель в sum и@orв операторах (D46). Парсер различает по контексту.
Связь
- D17 — старая версия, помечена revised → D52.
- D42 —
protocolостаётся отдельным keyword’ом для поведения. - D36
— префиксы полей (
readonly,mut) и group-syntax внутри record. - D39 —
delegation через
use Type. Newtype с embed (type X { use Y }) — альтернатива alias для случаев, когда нужна обёртка с дополнительными полями. - 03-syntax.md → D44 — числовые литералы
(
0xFF,1_000, негативные) — используются для discriminants. - 03-syntax.md → D46 —
|в operator overloading (@or) — разрешается компилятором по контексту. Полная семантика overloading — D84.
Открытые вопросы
- Литералы как sum-варианты (TS-style
| "open" | "closed") — отложено до следующей версии. - Итерация по вариантам (
for c in Color,Color.values()) — связано с reflection, откладывается до Q9 (stdlib). - Implicit cast литерала в newtype. Сейчас
let u UserId = 42— допустим (литерал подгоняется), ноlet n u64 = 42; let u UserId = n— требует явного cast. Точную семантику зафиксировать в Q (литералы vs binding’и).
Эволюция
D17 был первой
итерацией, основанной на правиле «= для выражений типа». Со
временем выяснилось, что:
- Sum-type с
=— натяжка («справа выражение типа» не точно описывает список вариантов). - Newtype отсутствовал как явная фича — программистам приходилось
делать record-обёртки
type X { value u64 }, что шумно. - Discriminants на sum-вариантах не были специфицированы — но реальные wire-протоколы их требуют.
D52 решает все три, ценой breaking change по syntax-site всех type-объявлений. Подробно — history/evolution.md.
D406. Sum-type синтаксис: enum маркер (2026-07-01)
Revises D52 §«Sum». Остальные формы D52 (newtype, alias, record, tuple, unit) без изменений.
Что
Sum-type теперь объявляется с обязательным ключевым словом enum вместо
leading |. Keyword enum — маркер в грамматике типов, поэтому он
работает везде где допустим тип: в named-type declaration, в позиции
параметра, возврата, поля, binding’а.
Правило
Синтаксис
// Named sum inline — без | перед первым вариантом
type Color enum Red | Green | Blue
type Direction enum North | East | South | West
type Option[T] enum Some(T) | None
// Многострочный — | обязателен у каждого варианта (включая первый)
type Result[T, E] enum
| Ok(T)
| Err(E)
type Shape enum
| Circle { radius f64 }
| Square { side f64 }
| Triangle { a f64, b f64, c f64 }
// С discriminants
type ExitCode enum Ok = 0 | Failure = 1 | Critical = 2
// С явным базовым типом (пока не реализован, Plan 105)
type Bit u8 enum Off = 0 | On = 1
Inline enum в type-позиции
enum Variant1 | Variant2 — это тип-выражение, валидное в любой
позиции где допустим тип:
// Параметр функции
fn job(a enum A | B) { ... }
// Возвратный тип
fn parse() -> enum Ok(int) | Err(str) { ... }
// Поле записи
type Response {
status enum Ok | NotFound | InternalError
}
// Let-binding
ro x: enum Some(int) | None = Some(42)
type Foo enum A | B — это объявление имени для типа-выражения
enum A | B. Named и inline — одна грамматика.
Минимум один вариант.
| в inline и многострочной формах
- Inline —
|разделяет варианты, перед первым не нужен:type Color enum Red | Green | Blue - Многострочный — если варианты на новых строках,
|обязателен у каждого варианта (включая первый), аналогично type-set bounds:
type Color enum Red | Green | Blue // inline — без | перед первым
type Result[T, E] enum // многострочный — | обязателен
| Ok(T)
| Err(E)
Парсер
После type X (или type X[params]) | Форма |
|---|---|
enum | sum |
<base-type> enum | sum с явным базовым типом (Plan 105) |
В type-expression position enum — prefix, парсер строит EnumTypeExpr.
Почему
- Симметрия с
alias.type X alias Yиtype X enum A | B— одна структура: keyword даёт форму, далее описание. Единый паттерн. - Явный grep-маркер.
enumв любой type-позиции мгновенно идентифицирует sum-type — в IDE, grep, LLM-prompt. - Устраняет неоднозначность
|. В D52 leading|конфликтовало с оператором@orи вызывало удивление.enum— незвуковой маркер без операторных коннотаций. - Inline enum. Анонимные sum-типы в позициях параметра/возврата/поля становятся возможными — естественный extension грамматики.
- Named =
type+ inline.type Foo enum A | Bтавтологично читается: «тип Foo это enum A или B». Интуитивно.
Что отвергнуто
- Сохранить leading
|(D52). Конфликт с оператором; не grep-абелен; inline-позиция невозможна. enum { A, B }со скобками (Go/C стиль). Нарушает Nova-правило «{→ record»;,как разделитель заменено на|ещё в D52.sum A | B(другой keyword).enumобщепринятый термин в PL;sumслишком математичен, непривычен программистам.type X = enum A | Bс=. Убрано ещё в D52; D406 следует тому же принципу «никаких=в type-declaration».
Связь
- D52 — заменяемый синтаксис
- D53 —
protocol/effectkind-токены в той же системе - D310 —
setkind-токен в той же системе;|в type-set тоже разделитель, не оператор - D55 — literal coercion в позиции sum-type (inline
enumтоже) - 03-syntax.md → D46 —
|как@orоператор — разрешается по контексту (keywordenum/setили expr-контекст) - Plan 105 — явный базовый тип discriminants
D53. Унификация: protocol под type, protocol как kind-токен
Что
protocol перестаёт быть отдельным keyword’ом. Становится kind-
токеном в системе D52, наряду с alias. Все объявления типов
(включая структурные контракты-protocol’ы) идут через единый keyword
type. Анонимный protocol-тип в позиции параметра пишется через
protocol { ... } (с явным маркером, симметрично []T, (A, B),
fn() -> T).
any — пустой именованный protocol-тип в prelude:
type any protocol { }
Правило
Объявление через type X protocol { ... }
// Раньше (D42): отдельный keyword
protocol Hash {
hash() -> u64
eq(other Self) -> bool
}
// Теперь (D53): kind-токен в системе D52
type Hash protocol {
hash() -> u64
eq(other Self) -> bool
}
type Logger effect {
log(msg str) -> ()
}
type Iterator[T] protocol {
next() -> Option[T]
}
type Db effect {
query(q Sql) Fail[DbError] -> []DbRow
exec(q Sql) Fail[DbError] -> int
}
Парсер: protocol как kind-токен после имени
Расширение таблицы D52:
После type X (или type X[params]) идёт | Форма |
|---|---|
protocol | protocol-тип |
effect | effect-тип |
enum | sum (D406) |
set | type-set bound (D310) |
value { | value-record — stack-allocated (D228/D277/D290) |
( | tuple |
{ | record — heap-allocated |
alias | alias |
<base-type> enum | sum с явным базовым типом (Plan 105) |
| идентификатор/тип, конец строки | newtype |
| конец строки сразу | unit |
protocol, effect, enum, set, alias, value — kind-токены
после имени. Парсер однозначен по первому токену после имени (или
generic-параметров).
Анонимный protocol-тип в позиции параметра
protocol { ... } в позиции типа — анонимный protocol-литерал,
симметрично []T, (A, B), fn() -> T:
fn log_one(x protocol { show() -> str }) Log -> () =>
Log.info(x.show())
fn closer_call(c protocol { close() -> () }) Io -> () =>
c.close()
fn process(x any) -> () => // any — именованный пустой protocol
...
fn process2(x protocol { }) -> () => // эквивалент через анонимный
...
Маркер protocol обязателен — { ... } без префикса в позиции типа
запрещено. Это убирает двусмысленность с record-литералами и
выражениями-блоками.
any в prelude
// В prelude:
type any protocol { }
Любой тип удовлетворяет пустому контракту (структурная типизация),
поэтому any — top-type. Использование:
type Logger effect {
log_event(level int, fields []any) -> ()
// ^^^^^ массив значений любого типа
}
fn dump(x any) Io -> () =>
println(x)
Имя any lowercase — исключение в D30 naming
convention, по аналогии с примитивами (int, str, bool, f64,
()). Top-type концептуально близок к примитивам — встроенный
универсальный тип.
Эффекты — без изменений
Эффект — это protocol-тип, использованный в позиции эффекта (между
) и ->). Меняется только синтаксис объявления, не использования:
type Db effect {
query(q Sql) Fail[DbError] -> []DbRow
exec(q Sql) Fail[DbError] -> int
}
fn list_users() Db -> []User => // Db в позиции эффекта — как раньше
Db.query(sql`SELECT * FROM users`)
Generic-параметры — без изменений
D42-уточнение про две модели (на protocol-уровне и на методе) сохраняется. Меняется только синтаксис объявления:
// Модель A — generic на protocol
type Container[T] protocol {
add(item T) -> ()
get(idx int) -> T
}
// Модель B — generic на методе
type Tracer effect {
span[T](body fn() -> T) -> T
measure[U](body fn() -> U) -> Duration
}
Структурная совместимость — без изменений
Любой тип со структурно совпадающими методами автоматически удовлетворяет protocol’у:
type User { id u64, name str }
type Display protocol {
show() -> str
}
fn User @show() -> str => "User(${@name})"
fn log_one(x Display) Log -> () =>
Log.info(x.show())
log_one(my_user) // ok, User совместим со Display
Self внутри protocol { ... } блока — это «late-bound» тип,
определяется при удовлетворении (см. также D66 — Self
universal во всех type-контекстах).
Почему
- Унификация под одним keyword. Все типы (data + behavior) идут
через
type. Один keyword для объявления, kind-токен различает форму. Согласовано с D52, который вводитaliasкак kind-токен —protocolвстаёт в тот же ряд. - Снимается асимметрия. До D53:
protocol Foo— отдельный keyword, ноFooиспользовался как тип (в позиции параметра). Программист спрашивал «если protocol — тип, почему не объявляется через type?». D53 отвечает: теперь объявляется. - Анонимные protocol-типы становятся явными. Раньше
fn f(x { ... })без префикса — двусмысленно (record-литерал? record-тип? protocol-тип?). Сprotocol { ... }— намерение явно. any— пустой именованный protocol. Простое и согласованное решение для top-type, через ту же систему. Прецедент Go (type any = interface{}), Swift (protocol AnyObject { }).- Прецедент Go. Go объявляет
type X struct { }иtype X interface { }через единыйtypeс kind-токеном. D53 повторяет эту схему точно (толькоinterface→protocol). - AI-friendly. Один keyword
typeв начале — LLM сразу видит «это объявление типа», kind показывает форму. Меньше keyword’ов для запоминания.
Что отвергнуто
- Сохранить
protocol Foo { ... }как отдельный keyword (текущий D42). Создаёт асимметрию: data объявляется черезtype, behavior — черезprotocol, оба используются как типы — два пути к одной концепции «тип». D53 устраняет. type any alias protocol { }как форма дляany. Для protocol’ов alias-форма семантически тождественна newtype-форме (структурная типизация делает имена незначимыми). Дополнительный синтаксис без выигрыша. Прямаяtype any protocol { }короче и яснее.Any(PascalCase). Согласовано с D30 строже, ноanylowercase привычнее (Go, TS) и согласовано с примитивами.- Анонимный protocol без префикса
{ ... }. Двусмысленно с record-литералами и блок-выражениями.protocol { ... }всегда явно. - Литеральные protocol’ы со значениями полей (как
interface{}в Go допускает методы и встраивание других interface’ов через composition). Composition protocol’ов (Foo : Bar) — открытый вопрос (см. D42 раздел «Открытые вопросы»), не входит в D53.
Цена
- Большой breaking change. Все
protocol Foo { ... }в spec/, decisions/, examples/ переписать вtype Foo protocol { ... }. Это — повторение масштаба D52 миграции. - На одно слово длиннее.
type Hash protocol { ... }противprotocol Hash { ... }— лишнийtype(5 символов). protocolтеперь kind-токен, не keyword. Грамматически разные роли (kind-token ≠ leading keyword), хотя пишется одинаково.- Анонимные protocol-типы в позиции параметра — новая форма,
старая (без префикса) запрещена. Все
fn f(x { method() })→fn f(x protocol { method() }). - Q22 закрывается этим решением — больше не открытый вопрос.
Связь
- D17 — старая система объявлений, revised → D52.
- D52
— D53 расширяет:
protocolвстаёт в ряд kind-токенов рядом сalias. - D42 — D53
заменяет
protocolkeyword на kind-токен. Семантика структурной типизации и generic-параметров сохраняется. - 04-effects.md → D18 — эффект как использование protocol-типа в позиции эффекта. Меняется только объявление.
- 08-runtime.md → D26 —
anyдобавлен в prelude. - 03-syntax.md → D30 — naming:
anylowercase как исключение, по аналогии с примитивами.
Открытые вопросы
- Type-pattern-match для значений
any. Извлечение конкретного типа изany-значения (match x { int(n) => ..., str(s) => ... }) требует runtime-tag и новой формы match. Не входит в D53. - Composition protocol’ов (
Foo : BarилиFoo extends Bar) — не входит, см. Q21 «proliferation эффектов» как родственный вопрос.
Эволюция
D42 ввёл
protocol как отдельный keyword. После D52 (kind-токены alias)
выявилась асимметрия: protocol используется как тип, но объявляется
не через type. D53 снимает асимметрию — protocol становится
kind-токеном в системе D52, унифицируя объявление всех типов под
единым keyword’ом.
Q22 («унификация type/protocol») — закрыт принятием D53.
Method-prefix в protocol-блоке (Plan 17 Ф.1)
В protocol-объявлении instance-методы можно писать в обеих формах
— и с префиксом @, и без. Они эквивалентны:
type Hash protocol {
hash() -> u64 // ✅ голое имя
eq(other Self) -> bool
}
type Hash protocol {
@hash() -> u64 // ✅ с @, симметрия с реализацией
@eq(other Self) -> bool
}
@ факультативен потому что в protocol-блоке метод всегда
instance — без receiver-выражения, контекст однозначный. С @
форма читается как «копия декларации из реализации» (точно как fn User @hash() -> u64); без @ — короче. Структурная совместимость
работает одинаково.
Когда писать что:
@method()— для визуальной симметрии с реализацией; для объявлений где соседние static-методы (если они появятся через Q-static-method-protocol) пишутся через.method().method()— для краткости в простых protocol’ах.
Mut-методы — mut @method() обязательно с @ (mut-modifier
требует receiver-маркера; голое mut method() отвергнуто как
двусмысленное с mut-binding’ом):
type Iter[T] protocol {
mut @next() -> Option[T] // ✅
mut next() -> Option[T] // ✅ (текущая prelude-форма, D26)
}
В bootstrap’е (2026-05-08) обе формы парсятся; std/testing/property.nv и std/collections/* используют голую форму.
См. также Q-protocol-method-prefix (closed этой секцией).
Реализация в bootstrap (2026-05-09)
Plan 15 D53 strict-mode (Plan 15 Ф.5) ввёл различие protocol/effect
на уровне AST. Раньше оба keyword’а маршрутизировались в один
TypeDeclKind::Effect(Vec<EffectMethod>), что нарушало D72:
любой method-bag тип permissively принимался как generic-bound.
Текущее состояние:
TypeDeclKind::Protocol(Vec<EffectMethod>)— дляtype X protocol {…}.TypeDeclKind::Effect(Vec<EffectMethod>)— дляtype X effect {…}.- Парсер маршрутизирует по ключевому слову (отдельные match-arm).
- Codegen эмитит vtable только для Effect-kind. Protocol —
compile-time-only; type_ref_to_c для protocol-методов не
вызывается. Это попутно зафиксировало pre-existing bug:
Selfв protocol-методе раньше ломал codegen (искал несуществующийNova_Self*). - Type-checker (D72 enforcement) регистрирует только
Protocol-kind в
protocol_specs. Попытка использовать Effect как bound — compile error c hint’ом «Xis an effect, not a protocol — declare astype X protocol {…}». - Анонимные protocol-литералы в позиции типа (
fn close(c protocol { close() -> () }), §628 этой секции) — ✅ реализованы в Plan 97 Ф.2 через новыйTypeRef::Protocol(ProtocolSig)variant. - Protocol-литералы в expression-position (
let l = protocol Name { ops }) с runtime vtable + dispatch — ✅ реализованы в Plan 97.1 (codegen vtable struct +emit_protocol_lit+ Plan 56 D122 box-pattern). См. также D142.
D55. Literal coercion в позиции с явным типом: sum-конструкторы и record-литералы
Что
В позиции, где компилятор явно знает целевой тип T (let с
аннотацией, аргумент функции, return-выражение), литерал
автоматически подгоняется под T. Четыре случая:
- Sum-coercion. Значение типа
Sоборачивается в единственный unary-конструкторC(S)sum-типаT. - Record-coercion. Анонимный record-литерал
{ field: value, ... }получает типTбез необходимости писать имя типа перед{}. - Map-coercion. Анонимный record-литерал
{ name: value, ... }в позиции, ожидающей str-keyed map (тип помечен атрибутом#from_fields, какHashMap[str, V]), превращается в map: имена полей становятся строковыми ключами. Это не record-coercion (поля литерала ≠ поля struct’аHashMap) — отдельное правило, см. ниже. - Numeric literal coercion. Целочисленный литерал в позиции
numeric-типа (
u8,u16,u32,u64,i8,i16,i32,i64,int) принимается без явногоas-cast если значение влезает в диапазон целевого типа. Это распространяется на аргументы функций (включая generic после конкретизации:Vec[u8].push(1)), let с аннотацией (ro a u8 = 42), поля record-литерала. Выход за диапазон — compile errorE_LIT_OUT_OF_RANGE:ro a u8 = 300→300 > u8.MAX (255),ro b u8 = -1→-1 < u8.MIN (0).
Без runtime-cost, без subtyping. После coercion тип значения — сам T.
// Sum-coercion
type StrOrInt enum S(str) | I(int)
ro a StrOrInt = "test" // компилятор: a = S("test")
ro b StrOrInt = 25 // компилятор: b = I(25)
fn process(x StrOrInt) -> str => ...
process("alice") // компилятор: process(S("alice"))
process(42) // компилятор: process(I(42))
// Record-coercion
type User { id u64, name str }
ro u User = { id: 2, name: "Bob" } // компилятор: u = User { id: 2, name: "Bob" }
fn create_user() -> User =>
{ id: 3, name: "Carol" } // компилятор подставляет User
fn save(u User) -> () => ...
save({ id: 4, name: "Dave" }) // компилятор: save(User { ... })
Правило
Позиции с «явно ожидаемым типом»
Coercion (и sum-, и record-вариант) применяется только там, где компилятор точно знает целевой тип:
| Позиция | Coercion применяется? | Реализовано (bootstrap)? |
|---|---|---|
let x T = value (явная аннотация) | да | ✅ record (Plan 51 Ф.1) |
const X T = value | да | ✅ record |
fn f() -> T => value (return-выражение) | да | ✅ record |
fn f(x T) — на caller-стороне (f(value)) | да | ✅ sum/record/map (Plan 52 Ф.3a) |
Generic-параметр после конкретизации — numeric (Vec[u8].push(1)) | да | ✅ numeric literals |
| Generic-параметр после конкретизации — record/sum | да | ⛔ ещё нет |
| Match-arm result (когда тип ветки фиксирован) | да | ⛔ ещё нет |
Литерал коллекции с явным типом ([]T) | да для каждого элемента | ⛔ ещё нет |
let x = value (без аннотации) | нет — выводится тип значения | — |
В позициях без явного типа никакая coercion не применяется — литерал
имеет «свой» тип ({ id: 2 } — анонимный record, 42 — int, и т.д.).
Статус реализации (2026-05-15). В bootstrap-компиляторе sum-/record-/map-coercion для безымянного литерала реально работает в позициях, помеченных ✅ (включая аргумент-позицию после Plan 52 Ф.3a —
f({...}),f([k:v]), named-args). Для ⛔-позиций безымянный{ ... }пока даёт codegen-ошибку — там пишиT { ... }. Полная реализация D55 во всех позициях — отдельная задача (investigation в Plan 51 показал, что «~900 избыточных мест» — переоценка; основная масса — это перенос имени, а не устранение).⚠️ Пример
save_all([{id:1,name:"a"}, ...])ниже некорректен для bootstrap’а. Элемент-позиция литерала коллекции ([]T) помечена ⛔ — coercion на элементах массива пока не работает. Пример станет валиден после расширения Ф.3a на element-positions (за scope Plan 52). Пока там нужен[User{...}, ...]с явным именем типа на каждом элементе.
Запрет дублирования имени типа (Plan 51)
Там, где компилятор знает целевой тип, имя типа в record-литерале избыточно и запрещено — тип объявляется ровно один раз. Enforce’ится в двух позициях:
| Форма | Вердикт |
|---|---|
fn f() -> T => { ... } | ✅ каноничная |
fn f() -> T => T { ... } | ⛔ тип дважды |
fn f() => T { ... } | ⛔ нет return-типа — тип «спрятан» в литерале |
let x T = { ... } | ✅ каноничная |
let x = T { ... } | ✅ (тип один раз — в литерале) |
let x T = T { ... } | ⛔ тип дважды |
-> Self резолвится к типу receiver’а (-> Self => Counter { ... } в
методе Counter — тоже избыточно). Правило не срабатывает, когда
тип литерала ≠ целевой тип — это sum-coercion (fn f() -> Result[U,E] => U { ... }, fn g() -> Shape => Circle { ... }): имя варианта
обязательно. Применяется к fn, @-методам и closure-full с =>-телом.
Sum-coercion
В позиции с явным ожидаемым типом T (sum-тип) значение типа S
оборачивается, если:
- У
Tровно один unary-конструкторC(S), принимающий типS. - Значение точного типа
Tуже не подходит (нет exact match).
Стандартные prelude-типы:
ro m Maybe[int] = 42 // Just(42)
ro r Result[User, str] = User { ... } // Ok(User { ... })
ro opt Option[str] = "alice" // Some("alice")
Коллекции:
type SqlValue enum I(i64) | F(f64) | S(str) | B(bool) | Bytes([]u8) | Null
ro args []SqlValue = [42, "alice", true] // [I(42), S("alice"), B(true)]
// В sql`...` тэге интерполяции тоже coerce'ятся: i64 → I, str → S, bool → B
ro q = sql`SELECT * FROM users WHERE id = ${42}` // args = [I(42)]
Генерики:
type Wrapper[T] enum W(T) | Empty
ro w Wrapper[int] = 42 // W(42)
ro w Wrapper[str] = "test" // W("test")
Record-coercion
В позиции с явным ожидаемым record-типом T анонимный record-литерал
{ field: value, ... } подгоняется под T. Имя типа перед {}
писать не нужно — компилятор подставляет.
type User { id u64, name str }
ro u User = { id: 2, name: "Bob" }
// эквивалент:
ro u User = User { id: 2, name: "Bob" }
fn save(u User) -> () => ...
save({ id: 4, name: "Dave" }) // эквивалент save(User { ... })
fn create() -> User =>
{ id: 5, name: "Eve" } // эквивалент User { id: 5, name: "Eve" }
fn make_default() -> Account =>
{ id: 1, balance: 0, closed: false } // в return-позиции с типом Account
Правила:
- Все обязательные поля должны присутствовать в литерале — как и для именованного record-литерала (D17 construction всегда требует все поля).
- Имена и типы полей должны точно соответствовать
T. Лишнее поле или несовпадение типа — ошибка компиляции. - Field punning (D17)
работает:
let u User = { id, name }еслиidиname— переменные в скоупе. - Без явного целевого типа литерал
{ id: 2, name: "Bob" }остаётся анонимным record-значением. Тип параметра функции или аннотацииletактивирует coercion.
Композиция с sum-coercion:
ro r Result[User, str] = { id: 2, name: "Bob" }
// шаг 1 (record-coercion): { id: 2, name: "Bob" } → User { id: 2, name: "Bob" }
// шаг 2 (sum-coercion): User → Ok(User { ... })
Записывается как одно действие компилятора в позиции с явным типом
Result[User, str]. Один-единственный record-литерал → User → Ok.
Симметрия с массивами:
То же type-driven поведение работает для массивов и других литералов в позиции аргумента — это та же модель, которой Nova уже пользуется для пустых массивов:
fn first[T](xs []T) -> Option[T] => ...
ro r = first([]) // [] : []T, T выводится из контекста
fn save(u User) -> () => ...
save({ id: 2, name: "Bob" }) // { ... } : User, тип параметра известен
fn save_all(us []User) -> () => ...
save_all([{ id: 1, name: "a" }, { id: 2, name: "b" }])
// каждый { ... } получает тип User из контекста []User
Аннотация типа параметра — единственный «локальный контекст», который читается, и он рядом с вызовом.
Sum-варианты с record-формой не получают анонимной формы — программист пишет конструктор:
type Shape enum Circle { radius f64 } | Square { side f64 }
ro s Shape = Circle { radius: 5.0 } // явный конструктор обязателен
ro s Shape = { radius: 5.0 } // ОШИБКА: по полям невозможно
// выбрать между Circle и Square
// (даже если у них разные поля,
// программист пишет имя варианта)
Это сознательное ограничение: sum-варианты с record-формой требуют имени конструктора всегда. Иначе at parse-time нужно матчить по структуре полей — type-driven parsing, антипаттерн.
Map-coercion
В позиции с явным ожидаемым типом HashMap[str, V] анонимный
record-литерал { name: value, ... } превращается в str-keyed map:
имена полей литерала становятся строковыми ключами, значения —
значениями map.
ro h HashMap[str, bool] = { debug: true, verbose: false }
// эквивалент: HashMap[str, bool] с ключами "debug", "verbose"
fn configure(opts HashMap[str, int]) -> () => ...
configure({ width: 80, height: 25 }) // ключи "width", "height"
Почему отдельное правило, а не record-coercion. HashMap[K, V] —
это struct (type HashMap[K, V] { buckets, count, ... }). Обычная
record-coercion матчила бы { debug: ... } против полей struct’а
HashMap (buckets, count) и падала бы. Map-coercion трактует
имена полей литерала как ключи, а не как поля struct’а. Чтобы
компилятор знал, какое из двух правил применить, целевой тип несёт
#from_fields атрибут (TypeAttr, ставится перед type):
- Это не opt-in ради эргономики (которое D55 отвергает для
sum/record) — marker здесь load-bearing для дисамбигуации:
«трактовать
{...}как поля этого struct’а» vs «как строковые ключи». Без него правило неоднозначно. - Gating:
HashMap[str, V]несёт marker; случайный struct — нет, и не начнёт принимать произвольные record-литералы. - Bootstrap: marker захардкожен для
HashMap. ПротоколFromFields[V]как точка расширения (OrderedMap,BTreeMap[str, V]) — позже. - Протокол
#from_fields: тип обязан иметь:static with_capacity(n int) -> Self— предаллоцировать подnзаписей;mut @insert_new(key str, val V) -> ()— вставить новую запись (без возвратаOption; дублей нет по construction). Отсутствие любого из методов → CC-FAIL при десугаринге.
Правила:
- Ключи — только str (имена полей литерала). Нестроковые ключи,
не-идентификаторные строки, вычисляемые ключи — это map-литерал
[k: v](03-syntax.md → D108), не{...}. - Значения гомогенны — все поля одного типа
V(после возможной sum-coercion на каждом значении). - Композиция с sum-coercion:
ro j HashMap[str, JsonValue] = { name: "alice", age: 30.0 } // "alice" → Str("alice"), 30.0 → Num(30.0); оба → JsonValue - Десугаринг — без промежуточных объектов: block-expression с
with_capacity+@insert_new, никакой промежуточный record не материализуется (литерал — только синтаксис):
({ mut _m0 = HashMap[str, V].with_capacity(n) _m0.insert_new("debug", true) _m0.insert_new("verbose", false) _m0 }@insert_newвместо@insert— мапа только что создана, дублей нет, нет нужды вOption[V]-возврате.) - Пустой
{}— это НЕ пустая мапа.{}всегда парсится как пустой block-expression с типомunit— даже в позиции, ожидающейHashMap[str, V]. Пустая мапа записывается как[]+ ожидаемый тип (03-syntax.md → D108):ro h HashMap[str, bool] = [] // ✅ пустая мапа (тип из контекста) ro h HashMap[str, bool] = {} // ⛔ {} — пустой блок, тип unit ≠ HashMapРевизия (Plan 52 Ф.0). Прежняя формулировка §5 ошибочно допускала
{}в map-позиции →HashMap[str, V].new(). Это требовало type-directed parsing блока — Nova этого не делает (D43). Правило удалено; пустая мапа — только[]. - Дубликаты ключей невозможны — имена полей record-литерала уникальны by construction.
Граница с map-литералом [k: v]: {...} — когда ключи это
статические имена-идентификаторы; [...] — когда ключи это
выражения (см. D108).
Когда coercion НЕ применяется
Ambiguity — несколько конструкторов с тем же типом (sum-coercion):
type Ambiguous enum A(int) | B(int)
ro x Ambiguous = 42 // ОШИБКА: ambiguous, A(42) или B(42)?
ro x = A(42) // явный конструктор — ok
Несоответствие — ни один конструктор не принимает тип значения:
type Color enum Red | Green | Blue
ro c Color = "red" // ОШИБКА: ни один конструктор не принимает str
ro c = Red // unit-конструктор
Без аннотации — coercion отключён:
type StrOrInt enum S(str) | I(int)
ro a = "test" // a : str (не StrOrInt, аннотации нет)
ro b StrOrInt = "test" // b : StrOrInt = S("test") (аннотация есть)
ro r = { id: 2, name: "Bob" } // r : анонимный record { id int, name str }
ro u User = { id: 2, name: "Bob" } // u : User (через record-coercion)
Newtype через D52 — coercion следует типу значения, не возможным кастам:
type UserId u64
type Wrapper enum W(UserId) | N(int)
ro w Wrapper = 42 // 42 : int → N(42) (тип значения int)
ro w Wrapper = 42 as UserId // → W(42 as UserId) — явный as, потом coercion
ro w Wrapper = UserId(42) // явный конструктор UserId
Несовпадение полей record:
type User { id u64, name str }
ro u User = { id: 2 } // ОШИБКА: missing field `name`
ro u User = { id: 2, name: "Bob", age: 30 } // ОШИБКА: unknown field `age`
ro u User = { id: "two", name: "Bob" } // ОШИБКА: id expects u64, got str
Coercion не строит цепочку конверсий — только одна обёртка вокруг exact-type значения.
Multi-parameter и tuple-варианты
Multi-parameter конструкторы — coercion не применяется в MVP:
type Event enum Click(int, int) | KeyPress(str)
ro e Event = "enter" // ok — KeyPress("enter"), unary с str
ro e Event = (5, 10) // ОШИБКА в MVP: tuple-coercion не вводится
ro e = Click(5, 10) // явный конструктор
Tuple-coercion (5, 10) → Click(5, 10) — отложено. Усложняет правила
(как различать «tuple как значение» vs «tuple-coercion в multi-param»),
не критично для use-case’ов.
Unit-конструкторы — coercion бессмыслен
Unit-варианты не принимают значение, coercion не нужен — программист пишет конструктор напрямую:
type State enum Open | Closed
ro s State = Open // unit, coercion не применяется
Почему
- Огромный win в эргономике для prelude-типов.
Option[T]иResult[T, E]— самые частые sum’ы языка. Без coercion программист пишетSome(42),Ok(user)каждый раз. С coercion —42,user. Убирает значительную часть boilerplate. - Без subtyping. Тип значения после coercion — сам sum или
сам record, не подтип. На уровне типов всё чисто: pattern match
exhaustive, variance не возникает. Anonymous unions (TS-style
string | number) не вводятся — coercion не делает того же эффекта семантически. - Без runtime-cost. Sum-обёртка — обычный конструктор, runtime-tag уже есть в representation sum’а (D52). Record-coercion — это просто подстановка имени типа, никакого runtime-преобразования.
- Закрывает use-case’ы
any(sum) и убирает шум именования (record).sql\…${value}`теперь type-safe —valuecoerce'ится вSqlValueбез[]anyи безis-extract.let u User = { id: 2, name: “Bob” }` — без повтора имени типа. - AI-friendly. LLM пишет
[42, "alice"]для SQL-аргументов естественно, без думания о конструкторах.{ id: 2, name: "Bob" }в позиции с явным типом — естественный способ создать record. Имя типа из аннотации — единственный «локальный контекст», который нужно прочитать, и он уже рядом. - Прецеденты:
- Swift
ExpressibleByStringLiteral/ExpressibleByIntegerLiteral— opt-in protocol’ы для coercion. Nova делает это автоматически для unary-конструкторов sum’ов (без opt-in). - Scala 3
Conversion[A, B]— opt-in given-конверсии. - TypeScript — через subtyping для anonymous union, через
structural typing для record (
const u: User = { id, name }работает). Nova даёт похожую эргономику без subtyping. - Rust struct expressions требуют имени (
User { id, name }) — прецедент против record-coercion. Nova выбирает TS-эргономику для record в позиции с явным типом, но только в этой позиции.
- Swift
Что отвергнуто
- Subtyping (
int <: StrOrInt) — TS-style anonymous unions. Серьёзное расширение системы типов (variance, type inference, exhaustiveness), runtime-cost (boxing на каждой границе). Coercion даёт то же удобство без subtyping. Записан как Q-anonymous-union для возможного пересмотра. - Anonymous record-coercion вне позиций с явным типом.
let x = { id: 2, name: "Bob" }остаётся анонимным record-типом, не превращается вUser. Только явный целевой тип активирует coercion. AI-locality сохраняется. - Record-coercion для sum-вариантов с record-формой
(
type Shape | Circle { radius f64 } | Square { side f64 },let s Shape = { radius: 5.0 }). Программист обязан писать имя варианта (Circle { radius: 5.0 }), даже если поля уникальны для одного варианта. Альтернатива — type-driven parsing по совпадению полей, антипаттерн в Nova. - Tuple-coercion в MVP. Двусмысленность с tuple-литералами как значениями. Отложено до v1.0+.
- Coercion на цепочках конверсий (
int → UserId → Wrapper). Только одна обёртка. Иначе правила усложняются, и легко получить неожиданный результат. - Coercion без явной аннотации типа (
let x = "test"→ выводитьStrOrInt?). Type inference не должен «угадывать» sum или record. Только явный target type активирует coercion. - Opt-in coercion через protocol (Swift-style
ExpressibleBy*Literal). Программист объявляет sum/record, поведение работает автоматически без дополнительного opt-in. Это менее гибко, но проще. - Coercion для multi-parameter конструкторов через tuple
(
(5, 10) → Click(5, 10)). Отложено как tuple-coercion в MVP.
Цена
- Implicit conversion — первая в Nova. До D55 язык избегал неявного. Это философский сдвиг, обоснованный эргономикой prelude-типов и анонимных record. AI-friendly: LLM не должна угадывать конструктор или имя типа.
- Type-checker сложнее. В позиции с явным типом нужно проверить exact match, потом coercion (sum или record). Стандартное расширение, но code path не нулевой.
- IDE-подсказки усложняются. «Ожидается
StrOrInt, переданstr→ coerce вS», «ОжидаетсяUser, передан анонимный record → подгонка подUser» — IDE должна это показывать. - Migration sum’а опасна: добавление нового unary-конструктора
с тем же типом параметра ломает существующий код (был exact match
через coercion в
S(str), стал ambiguous из-заS(str) | S2(str)). Это breaking change для sum’а — программист должен учитывать. - Migration record’а тоже: добавление обязательного поля в record ломает все анонимные литералы без него. Это известная проблема record-типов вообще, не специфическая для D55.
- Закрывает большую часть use-case’ов
any— это плюс, но требует пересмотра примеров (args []any→args []SqlValue). - Парсер — без type-driven decisions. Coercion работает в
позициях, где целевой тип уже известен type-checker’у —
парсер по-прежнему чисто синтаксический.
{...}парсится как record-литерал/block-выражение по обычным правилам D17/D49, а тип ему присваивает type-checker по аннотации.
Связь
- D52 — sum-типы и unary-конструкторы, на которых coercion работает.
- D53
—
anyостаётся для подлинно открытых случаев (plugins, reflection), D55 закрывает большую часть use-case’ов через closed sum’ы. - 03-syntax.md → D44 — numeric literal coercion
(
100подгоняется подu8/u32в позиции типа) — D55 расширяет эту идею на sum’ы и record’ы. - 03-syntax.md → D54 —
as/isостаются явными для конвертации/проверки. D55 не вводит implicit cast между обычными типами, только для sum-обёрток и record-литералов. - 08-runtime.md → D26 —
Option[T],Result[T, E]в prelude получают эргономичный синтаксис через D55. - #d17-объявление-типов-единый-синтаксис-без-
(revised → D52) — record-литерал
User { id: 1, name: "alice" }с именем типа — обязательный, когда тип не выводится из контекста. D55 разрешает опускать имя в позиции с явным целевым типом. - 03-syntax.md → D108 — map-литерал
[k: v]; комплементарен map-coercion ({...}— ключи-имена,[...]— ключи-выражения). Реализация обоих — Plan 52.
Открытые вопросы
- Tuple-coercion для multi-parameter конструкторов. Отложено.
- Anonymous unions (
type StrOrInt | type str | type int) — TS-style без обёрток. Записан как Q-anonymous-union (требует subtyping, серьёзное расширение системы типов). См. open-questions.md. - Стандартные closed sum’ы в prelude (
SqlValue,JsonValue) — что именно положить, формат и набор операций. См. Q9 (stdlib). - Cross-type numeric coercion в D55 (
42→f64дляNumber(f64)). Сейчас строгий exact match. См. Q-numeric-coercion.
Style-guide: когда coerce, когда писать тип явно (Plan 17 Ф.1)
D55 разрешает обе формы — coerce и явный конструктор. Чтобы кодовая
база не превращалась в смесь стилей, ниже рекомендации для nova fmt/линтера и code review (это не правило компилятора, оба
варианта остаются валидными).
Coerce (короче, тип в аннотации) — предпочитать когда:
// 1. let с явной аннотацией — тип сразу слева, имя справа лишнее
ro u User = { id: 1, name: "alice" } ✅
ro m Maybe[int] = 42 ✅
// 2. return-position в expression-body, есть -> T
fn make_default() -> Account => { id: 0, balance: 0 } ✅
// 3. call-site с явным типом параметра — coercion даёт чистый литерал
serve({ ...SERVER_DEFAULTS, port: 9000 }) ✅
// 4. коллекции с разнородными элементами в позиции []SqlValue
ro args []SqlValue = [42, "alice", true] ✅
// [I(42), S("alice"), B(true)] ❌ шумно
Явный конструктор — предпочитать когда:
// 1. let без аннотации — coercion не работает, имя обязательно
ro r = if cond { Some(value) } else { None } ✅
ro r = if cond { value } else { None } ❌ — нет аннотации
// 2. match-arms где хотя бы одна ветка — unit-вариант (None / Empty)
// — для визуальной симметрии писать ВСЕ ветки с конструкторами
match @cache.get(key) {
Some(v) => Some(v) ✅ симметрично с None
None => fallback()
}
match @cache.get(key) {
Some(v) => v ❌ value слева, None справа —
None => fallback() // асимметрично, читать сложнее
}
// 3. nested record-литерал внутри блока — { {...} } визуально шумно
fn compute() -> Money =>
if special { Money { amount: 100, currency: usd } } ✅
else { Money { amount: a + b, currency: c } }
fn compute() -> Money =>
if special { { amount: 100, currency: usd } } ❌ шум
else { { amount: a + b, currency: c } }
// 4. ambiguous unary-конструкторы (compile-error без явного имени)
type Mixed enum A(int) | B(int)
ro x Mixed = 42 ❌ ambiguous — обязателен A(42) / B(42)
Сводка:
| Контекст | Рекомендация |
|---|---|
let x T = ... (есть аннотация) | coerce |
let x = ... (нет аннотации) | явный конструктор |
fn f() -> T => ... (есть -> T) | coerce |
fn f(x T) call-site f(...) | coerce |
| match с unit-веткой | явный (симметрия) |
nested { ... } в блоке после if/else | явный (избежать { {...} }) |
| ambiguous unary-конструкторы | явный (обязательно) |
Аргумент. nova fmt не должен переписывать одну форму в другую —
выбор стилистический. Линтер может в будущем выдавать подсказку
для самых тяжёлых случаев (например, { {...} } в block-context),
но без флага --strict-style — это рекомендация, не ошибка.
См. также Q-style-coercion (закрыт этой секцией).
Эволюция
До D55 sum-варианты требовали явный конструктор на каждом значении
(Some(42), Ok(user), S("test")), а record-литералы — имя типа
перед {} (User { id: 1, name: "alice" }).
После D55 в позиции с явным целевым типом:
- sum-значение оборачивается автоматически (
42в позицииMaybe[int]→Just(42)), - анонимный record-литерал получает имя из аннотации (
{ id: 1, name: "alice" }в позицииUser→User { id: 1, name: "alice" }).
Это эргономический сдвиг уровня D52, без слома типовой модели.
Альтернатива (anonymous unions через subtyping) рассмотрена и отвергнута — слишком серьёзное расширение системы типов для эргономического выигрыша. D55 даёт похожее удобство более узким и контролируемым механизмом.
D42. protocol keyword для структурных интерфейсов
⚠️ REVISED. Заменено D53.
protocol— теперь не отдельный keyword, а kind-токен в системе D52:type Foo protocol { ... }. Семантика структурной типизации, generic-параметров и эффектов сохраняется. Текст ниже — для исторической справки.
Что
Структурные интерфейсы объявляются отдельным keyword protocol. type
— для данных (record, sum-type, alias), protocol — для
поведения (набор методов как контракт). Любой тип со структурно
совпадающими сигнатурами автоматически удовлетворяет protocol’у — без
явных impl-блоков.
Эффекты — это тоже protocol, использованный в позиции эффекта
(между ) и ->). Один и тот же protocol может играть роль эффекта
или роль структурного контракта-параметра — различение по контексту
использования (04-effects.md → D18).
type без полей с одними методами не допускается — нужен protocol.
Правило
type Hash protocol { // D52/D53: kind-токен `protocol` под `type`
hash() -> u64
eq(other Self) -> bool
}
type Iterator[T] protocol {
next() -> Option[T]
}
type Login { // record (данные) — голый type
username str
password str
}
Self внутри protocol-блока — late-bound. См. D66 для других
контекстов где Self тоже валиден (static/instance методы, effects).
Структурная совместимость — автоматическая. Метод определяется у типа
через @-синтаксис (03-syntax.md → D35) и без
дополнительных деклараций удовлетворяет protocol’у:
type User { id u64, name str }
type Display protocol {
show() -> str
}
fn User @show() -> str => "User(${@name})"
fn log_one(x Display) Log -> () =>
Log.info(x.show())
log_one(my_user) // ok, User автоматически совместим
Параметр функции может декларировать требования прямо в типе, без именованного protocol’а:
fn log_one(x { show() -> str }) Log -> () =>
Log.info(x.show())
В protocol fn-префикс не нужен — там по определению все «члены»
это методы. В record-типе поле-функция объявляется явно с fn:
type Button {
text str
on_click fn() Io -> () // поле-функция в record, не protocol
}
Generic-параметры: на protocol-уровне vs на методе
В Nova есть две явных модели generic-параметров для protocol’а. Программист выбирает по семантике.
Модель A — generic на protocol (protocol P[T] { ... }).
T фиксирован для всего protocol’а: один handler = один T. Все методы
видят один и тот же T. Разные T = разные сущности (Iterator[Int] и
Iterator[String] несовместимы).
type Iterator[T] protocol {
next() -> Option[T]
peek() -> Option[T]
}
type Container[T] protocol {
add(item T) -> ()
get(idx int) -> T
size() -> int // методы без T тоже допустимы
}
type Channel[T] effect { // effect — нужен with-substitution
send(value T) -> ()
recv() -> T
}
type Cache[K, V] effect {
get(key K) -> Option[V]
set(key K, value V) -> ()
}
Когда применять: когда T — фундаментальная характеристика protocol’а, все или большинство методов работают с этим T, и разные T = разные handler’ы имеют смысл.
Модель B — generic на методе (method[T](...)).
T живёт только в скоупе одного метода. Один и тот же handler protocol’а
вызывает метод с разными T для каждого вызова.
type Tracer effect {
span[T](body fn() -> T) -> T // T живёт только здесь
measure[U](body fn() -> U) -> Duration // U независим от T
set_attr(key str, value Json) -> () // методы без generic тоже
}
type Db effect {
query(q Sql) Fail[DbError] -> []DbRow
in_transaction[T](body fn() Db Fail -> T) Fail -> T
// ↑ один Db handler оборачивает любой T
}
Когда применять: когда метод принимает/возвращает любой тип, а сам protocol не привязан к этому типу — один handler работает с любым T для каждого вызова.
Различие в семантике handler’а:
| Модель A | Модель B | |
|---|---|---|
| Объявление T | protocol P[T] | method[T] в сигнатуре |
| Scope T | весь protocol | один метод |
| Один handler работает с | одним T | любым T (per-call) |
| Использование | with P[Int] = ... | with P = ...; P.method[Int](...) |
| Реализация | мономорфизация по T | rank-2 polymorphism в handler’е |
В одном protocol’е можно комбинировать оба механизма:
type Stream[T] protocol {
next() -> Option[T] // T на protocol-уровне
fold[Acc](init Acc, f fn(Acc, T) -> Acc) -> Acc // Acc на методе
}
T фиксирован для stream (Stream[int]), Acc независим — fold
может собирать в разные accumulator-типы из одного и того же stream’а.
Почему
- Намерение должно быть явным. Старая форма
type X = { методы }визуально совпадала с record-формойtype X { поля }, различаясь только знаком=. LLM и человек различали намерение по единственному символу — хрупко. Отдельный keyword делает намерение явным с первого токена. - Прецедент.
protocolкак keyword для интерфейсов используется в Swift, Objective-C, Clojure, Elixir, Python (typing.Protocol). Семантически Nova ближе всего к Pythontyping.Protocol— чисто структурный subtyping. - Эффекты в сигнатурах методов делают protocol строже Go interface — реализация не может привнести эффект сверх объявленного. Это уникальное свойство Nova.
Что отвергнуто
type X = { методы }— слишком похоже на record, отличается одним знаком=. См. «Почему» выше.contract— занято под pre/post-условия (09-tooling.md → D24).promise— массовая ассоциация с async (JS Promise).interface— слишком сильный nominal-bias (Java/C#).trait— обещает Rust-фичи (default impl, supertraits, blanket impl), которых в Nova нет.shape— короче, но менее знакомо как keyword.ability— образно, но без знакомства; навязывает-ableсуффикс именам.- Implicit shared scope для generic-параметров (T в нескольких
методах одного protocol’а автоматически означает один и тот же тип).
Снижает локальность: чтобы понять
[T]в одном методе, нужно прочитать весь protocol-блок и проверить остальные методы. Невозможно выразить «независимый T в разных методах» без смены convention (использования других букв). Прецедентов нет — Rust/Swift/Scala/Haskell все используют либо явный protocol-уровень, либо явный method-уровень. Альтернатива (protocol P[T]) уже даёт ту же семантику явно.
Связь
- 02-types.md → D15 — D15 ввёл структурные интерфейсы; D42 уточняет грамматику отдельным keyword.
- 02-types.md → D39
—
use Typeдля делегации между record-типами;protocolне embed’ится. - 03-syntax.md → D35 — методы через
@как способ удовлетворить protocol. - 01-philosophy.md → D1
—
protocols+dataкак фундамент парадигмы.
Открытые вопросы
- Bounds на дженерики —
HashMap[K: Hash, V]требует отдельного решения. Сейчас параметр без bound, компилятор полагается на структурное соответствие при использовании. - Default-методы в protocol — пока запрещены.
- Inheritance protocol’ов —
protocol A : Bпока запрещено; эквивалент достигается явным включением методовBвA.
Эволюция
Изначально структурные интерфейсы описывались через type X = { методы }
(см. D15). D42 заменил эту форму на
отдельный keyword protocol. Детали — в history/evolution.md.
D15. Структурные интерфейсы
Status: revised. Роль перешла к
protocolkeyword (D42).
Что
Изначальный механизм структурных «интерфейсов» в Nova: отдельной
концепции interface или trait нет; контракт — это набор сигнатур,
любой тип со совпадающими методами автоматически совместим. Сейчас
этот механизм обогащён keyword protocol (D42), который делает
объявление контракта синтаксически явным.
Правило
Структурная совместимость — автоматическая. Имя контракту даёт
protocol:
type Display protocol {
show() -> str
}
type User { id u64, name str }
fn User @show() -> str => "User(${@name})"
fn log_one(x Display) Log -> () => Log.info(x.show())
log_one(my_user) // ok, User автоматически совместим
Анонимный структурный тип прямо в сигнатуре параметра — без отдельного имени:
fn log_one(x { show() -> str }) Log -> () =>
Log.info(x.show())
Что сохранено:
- Эффекты в полях-функциях — часть сигнатуры, проверяются как обычно. Реализация не может привнести эффект сверх объявленного. Это ключевое отличие Nova от Go: контракт жёстче, потому что эффекты — часть сигнатуры.
- Структурная совместимость автоматическая, как в Go.
- Дженерики без bound’ов — требования описываются типом параметра.
Почему
- Следует из принципа «не добавлять фичи без оправдания центральной идеей или AI-first». Rust-style traits ни тому, ни другому не служат.
- Унификация: одна концепция «структурный тип» вместо двух («record»
- «interface»). Меньше синтаксиса — проще для LLM.
- Эффекты в сигнатурах методов делают структурный тип строже, чем Go interface — это уникальное свойство Nova, которое нельзя получить простым заимствованием Go.
Что отвергнуто
trait/interfaceкак отдельный keyword с nominal-семантикой (Java/C#/Rust).impl Trait for Typeблоки.[T: Trait]bounds в дженериках.dyn Traitvsimpl Traitразделение.- Ассоциированные типы.
- Дефолтные методы.
- Trait-наследование, specialization, HKT.
Цена
- Нет имени для контракта иначе как через
protocol. В IDE нельзя «найти всех, кто реализует X» так же легко, как в Rust/Java — поиск идёт по совпадению методов. - Нет номинальности. Если очень нужна — через newtype-обёртку (паттерн, не фича).
Связь
- 02-types.md → D42
—
protocolкак явное имя для контракта. - 02-types.md → D39 — embed/delegation как механизм композиции, не subtyping.
- 03-syntax.md → D35 —
@-методы как способ удовлетворить protocol.
Эволюция
Ранние черновики описывали контракт через type X = { методы } —
визуально неотличимо от record. D42 ввёл отдельный keyword protocol,
сохранив структурную семантику D15. Подробно — в
history/evolution.md.
D39. Embed и delegation: use name Type (alias обязателен)
Что
Композиция типов через use name Type внутри record-декларации. Имя
поля всегда явное — программист пишет alias в snake_case по
D30. Default-имя по типу (Go-style use Type →
поле Type) не вводится — нарушает D30 (поля snake_case, типы
PascalCase).
Это delegation, не наследование: обёртка не является подтипом встроенного.
Правило
Базовое использование
type AuditedAccount {
use account Account // имя поля = "account" (snake_case)
audit_log []AuditEntry
}
ro acc AuditedAccount = ...
// Auto-proxy: прямой доступ к полям и методам Account
println(acc.balance) // = acc.account.balance
println(acc.owner) // = acc.account.owner
acc.is_solvent() // = acc.account.is_solvent()
// Доступ к встроенному объекту целиком — через имя поля
ro just_account = acc.account
use Account без имени — ошибка компиляции: имя поля обязательно.
type AuditedAccount {
use Account // ОШИБКА: имя поля обязательно
audit_log []AuditEntry
}
Auto-generated прокси-методы
При use name Type компилятор генерирует прокси для каждого метода
Type:
type Account { balance money }
fn Account @balance_pct(of money) -> f64 => @balance / of * 100.0
type AuditedAccount {
use account Account
audit_log []AuditEntry
}
// Компилятор генерирует:
// fn AuditedAccount @balance_pct(of money) -> f64 =>
// @account.balance_pct(of)
ro aa AuditedAccount = ...
aa.balance_pct(1000.0) // через auto-proxy
Zero-cost — компилятор инлайнит вызов, никакой vtable.
Грамматика согласована с record-полями
use name Type использует тот же порядок «имя тип», что и обычные
поля, параметры функций, let-bindings, for-loop:
type Wrapper {
item str // обычное поле: имя тип
use iter HashMapIter[K, V] // embed: use + имя тип
extra int
}
fn deposit(mut acc Account) -> () => ... // параметр: имя тип
ro user User = ... // ro: имя тип
for id u64 in ids { ... } // for: имя тип
Везде имя слева, тип справа — одно правило для всего языка.
use — keyword, не имя поля
use — зарезервированное слово (D29 для импортов
- embed-конструкция здесь). Имя поля
useзапрещено.
В декларации {use name Type} use — keyword embed-формы; имя
поля — alias после use:
type Set[T] {
use map HashMap[T, ()] // имя поля — "map"
}
// record-литерал — имя поля
ro s Set[int] = { map: HashMap[int, ()].new() } // ✓
ro s Set[int] = { use: HashMap[int, ()].new() } // ✗ use — keyword
// доступ — имя поля
fn Set[T] @len() => @map.len() // ✓
fn Set[T] @len() => @use.len() // ✗ use — keyword
Override метода
Если тип-обёртка определяет метод с тем же именем — он затмевает делегированный:
type AuditedAccount {
use account Account
audit_log []AuditEntry
}
fn AuditedAccount mut @deposit(amount money) {
@account.deposit(amount) // явный вызов «родителя» через имя поля
@audit_log.push(AuditEntry.deposit(amount))
}
mut acc AuditedAccount = ...
acc.deposit(100) // вызовет AuditedAccount.deposit
Без @account. в теле — бесконечная рекурсия. Программист обязан
явно обращаться к встроенному через имя поля.
Конфликт имён — разные alias-имена
Если два use вводят одинаковые имена методов — программист даёт
разные alias-имена и явно решает, через какой:
type Logger effect { log(msg str) -> () }
type Auditor { log(msg str) -> () }
type Combined {
use console Logger
use audit Auditor
}
ro c = Combined { ... }
c.log("...") // ОШИБКА: ambiguous (оба имеют log)
Решение — явный вызов через имя поля:
fn Combined @log_all(msg str) {
@console.log(msg)
@audit.log(msg)
}
ro c = Combined { ... }
c.console.log("...")
c.audit.log("...")
Anonymous embed: use _ Type (без alias-имени)
Альтернатива явному alias — anonymous embed через _:
type Set[T] {
use _ HashMap[T, ()]
}
ro s = Set[int].new()
s.insert(item, ()) // ✓ через auto-proxy на HashMap.insert
s.contains(item) // ✓ через auto-proxy
s.len() // ✓ через auto-proxy (D117 method-only)
_ — это wildcard: программист сознательно отказывается
от имени поля, потому что не нуждается в прямом доступе к встроенному.
Когда использовать
use _ подходит для simple wrappers где:
- Нет необходимости в прямом доступе к встроенному (
@base.method()). - Wrapper-методы не вызывают delegated в своём теле.
Set[T] — типичный case: вся семантика приходит из HashMap через
auto-proxy + override на одно поведение (insert возвращает bool
вместо Option).
Override через own-methods — работает
Программист может определить wrapper-метод того же имени что у embedded:
type Set[T] {
use _ HashMap[T, ()]
}
// Override @insert — заменяем семантику
fn Set[T] mut @insert(item T) -> bool {
// Здесь нельзя обратиться к HashMap.insert напрямую — нет имени
// поля для @<base>.insert(...). Override полностью заменяет
// логику.
Log.info("inserting...")
// ... custom impl, не делегируя к HashMap
}
Resolution через call-site overload resolution
(D84) с override-precedence: own-method
(определённый напрямую на receiver) wins over delegated (через
use).
ro s Set[int] = ...
s.insert(42)
// → resolve_overload("insert", "Set[int]", [int])
// → 2 candidates: Set.@insert (own), HashMap.@insert (delegated)
// → override-precedence: own wins → Set.@insert
// → no ambiguity error
Когда не использовать
Если wrapper-метод нуждается в @base.method() для делегирования —
нужен named alias:
// ✓ named alias — есть `@account` для явного call
type AuditedAccount {
use account Account
audit_log []AuditEntry
}
fn AuditedAccount mut @deposit(amount money) {
@account.deposit(amount) // explicit base call
@audit_log.push(AuditEntry.deposit(amount))
}
// ✗ anonymous embed не подходит — нет имени для base call
type AuditedAccount {
use _ Account
audit_log []AuditEntry
}
fn AuditedAccount mut @deposit(amount money) {
??? // как вызвать Account.deposit?
// НИКАК — anonymous embed не даёт имени
}
Compile error в этом случае возникает естественно на call-site:
программист пишет @deposit(amount) (без имени поля), это рекурсивный
вызов Self — бесконечная рекурсия, которая, скорее всего, не то
что хотел программист.
Lint-warning (не error) предложит: «possible infinite recursion in anonymous embed override; use named alias for base-call».
Что запрещено
Два anonymous embed одного типа — недопустимо:
// ✗ COMPILE ERROR
type Wallet {
use _ Account
use _ Account // ambiguous — два anonymous Account
}
При вызове w.balance resolution даёт два candidates с одинаковым
priority — ambiguity unresolvable, потому что нет имени поля
для disambig’а. Compile error при declaration.
Решение — named alias:
type Wallet {
use primary Account
use backup Account
}
Резолвинг — общий механизм overload
Anonymous embed не вводит специальных правил в компилятор.
Resolution использует тот же resolve_overload (D84)
с двумя расширениями:
- Анонимные embed-методы регистрируются в overload registry с
kind = MethodKind::Delegated(via_use_anonymous)— флагом «delegated». - Override-precedence: own-methods (без флага) wins over delegated, при прочих равных (тот же receiver, та же arity, те же arg-types).
Это даёт желаемое поведение «own override затмевает delegated» без отдельной declaration-time проверки collision’а.
Сводка use _ Type vs use name Type
| Аспект | use name Type | use _ Type |
|---|---|---|
| Имя поля | явное (name) | нет |
| Auto-proxy | да | да |
| Override через own-method | да | да |
Доступ к base через @<name>.method() | да | нет |
| Multiple embed одного типа | да (разные имена) | нет (compile error) |
| Construction через literal | T { name: ..., ... } | через factory T.new(...) |
| Pattern destructure | возможен через имя | unsupported |
use для встроенных типов ([]T, tuples)
use поддерживает не только именованные record-типы, но и встроенные
конструкции — массивы ([]T), tuples ((A, B)), и т.п. Имя
поля обязательно (как и для именованных типов):
// VecBuf через embed []T — все методы массива доступны
type VecBuf[T] {
use data []T
extra str
}
ro v = VecBuf[int] { data: [1, 2, 3], extra: "info" }
ro n = v.len // прокси-метод к data.len ([]T API)
v.push(42) // прокси-метод к data.push
ro x = v.get(0) // прокси к data.get
Этим механизмом строятся «именованные обёртки над массивами» с дополнительными полями/методами без переписывания базового API.
API расширяется обычными методами на типе (D35):
fn VecBuf[T] @first_or_default(def T) -> T =>
@data.get(0).unwrap_or(def)
API самих встроенных типов ([]T.len, []T.push, etc.) — открытый
вопрос Q-array-api в open-questions.md, формализуется в Q9 stdlib.
Что это НЕ
Не наследование. AuditedAccount не является Account:
fn process(a Account) -> () => ...
ro aa AuditedAccount = ...
process(aa) // ОШИБКА
process(aa.account) // ок: извлекли Account-часть через имя поля
Если нужен полиморфизм — структурный protocol:
type HasBalance protocol {
balance() -> money
}
fn process(a HasBalance) -> () => ...
process(aa) // ок: AuditedAccount имеет balance()
// через delegation auto-proxy
Не множественное наследование. Можно use несколько типов, но
конфликты решаются alias’ом или явным обращением. Diamond-problem не
возникает — нет иерархии.
Почему
- Замена наследования (D1) — embed решает 80% задач композиции без сложности subtyping.
- Согласованность с D30 naming. Поля Nova — snake_case (D30). Default-имя по типу (Go-style) дало бы PascalCase-поле — нарушение D30. Явный alias обязывает программиста выбрать snake_case, всё единообразно.
- Согласованность с language-wide порядком.
use name Type— тот же порядок «имя тип», что параметры, поля, let-bindings, for-loop. Одно правило для всего языка. - AI-friendly. Никакой magic-conversion (
HashMap→hashmap/hash_map?), программист явно выбирает имя поля. LLM не догадывается.
Что отвергнуто
- Default-имя поля по типу (
use Account→ полеAccount, Go-style). Создаёт исключение в D30 (поля PascalCase в одном record-блоке с snake_case полями). Auto-conversion PascalCase → snake_case (HashMap→hash_map?) — magic, не очевидное правило. use Type as name(Rust import-style).asзафиксировано для cast в выражениях (D54) и импортов (07-modules.md → D29). В embed — «объявление поля», порядок «имя тип» согласован с остальным языком.- Subtyping — противоречит D1; полиморфизм через protocol.
- Множественное наследование — известный антипаттерн (diamond, fragile base).
Связь
- 01-philosophy.md → D1
—
useкак замена наследования. - 02-types.md → D17
—
useвнутри record-блока. - 02-types.md → D15, D42 — полиморфизм для embed-типов идёт через protocol, не через subtyping.
- 03-syntax.md → D30 — naming convention (поля snake_case, типы PascalCase). Обязательность alias следует из D30.
- 03-syntax.md → D35 —
@field.method()для явного вызова из метода обёртки. - 03-syntax.md → D38 — generic-применение в
embed:
use iter HashMapIter[K, V].
Эволюция
Первая редакция D39 разрешала default-имя = имя типа: use Account → поле Account (PascalCase, Go-style). Это создавало
нарушение D30 (поля должны быть snake_case) — в одном record-
блоке audit_log (snake) и Account (Pascal) выглядели несогласованно.
Что стало: alias обязателен. use Account без имени — ошибка
компиляции, программист пишет use account Account. Default-имя
отменено, никакой magic-conversion HashMap → hash_map.
Также поменялся синтаксис конфликтов: раньше предлагался «явный вызов
через имя типа» (c.Logger.log(...)), теперь только через alias-
имя поля (c.console.log(...)). Это согласовано с тем, что все
поля имеют alias-имя, и в коде используется оно.
Q-embed-syntax в open-questions всё ещё открыт — это отдельный
вопрос про keyword (use vs embed vs голый тип), а не про
обязательность имени.
Anonymous embed (2026-05-08): добавлена форма use _ Type для
simple wrappers где явное имя поля бессмысленно (use _ HashMap[T, ()]
в Set[T]). Программист не выбирает alias из bikeshedding map/inner/
s/value — _ явно говорит «безымянный embed, прямой доступ
не нужен».
Resolution для anonymous через lazy mechanism — общий call-site overload-resolution (D84) с override-precedence (own-method wins over delegated). Никаких declaration-time проверок collision’ов. Это упрощает компилятор — один путь для named и anonymous.
Trade-off anonymous vs named: anonymous теряет @<name>.method()
(прямой base-call) и pattern-destructure через имя поля. Эти возможности
трактуются как «escape hatches» — для них программист пишет
use name Type явно.
Прецеденты:
- Go
embedded interface{}— anonymous, прямой доступ через имя типа (s.Account). Nova не следует — D30 запрещает PascalCase поля. - D
alias this— anonymous embed с implicit conversion. Nova не следует — нет subtyping (D1). - Rust composition — нет anonymous embed; программист пишет
field + manual delegation. Nova
use _экономит boilerplate.
Bootstrap status (2026-05-08)
Реализовано в bootstrap-codegen (Plan 11 Ф.9):
- ✅ Parser:
use name Type(named embed) иuse _ Type(anonymous). Anonymous имя поля — синтетическое__embed_<TypeName>. - ✅ AST:
RecordField.is_embed: bool,RecordField.embed_anonymous: bool. - ✅ Codegen auto-proxy generation:
embed_fieldsregistry per record-type; для каждого Own-метода embedded-типа эмитится Delegated MethodSig + C-функция, которая делегирует черезnova_self->field. - ✅ Override-precedence (Own > Delegated) в emit_call и infer paths (Plan 11 Ф.9.3). Strict-match candidates сначала, затем фильтр Own.
- ✅ Multi-anonymous detection: declaration-time error если ≥2 anonymous embeds одного типа в одном record’е (Plan 11 Ф.9.4).
- ✅ Lint warning
possible infinite recursion: при detect own-method override на anonymous embed — stderr-warning о невозможности base-call’а (Plan 11 Ф.9.5).
Bootstrap-ограничения:
- C-name mangling по param-types: для overloaded delegated proxy
имена с suffix’ом
__<types>, как для own overload. - Generic embed (
use map HashMap[K, V]в generic wrapper) — работает для конкретных type-параметров; full generic monomorphization — открытый вопрос.
D32. Семантика передачи параметров
Status: revised для полей. D36 переписал семантику
mutна поле типа. Семантикаmutна параметре (этот D32) — без изменений.⚠️ AMENDED by Plan 118 (D216) —
&valueoperator restored для создания typed pointer (*T/*mut T). Это НЕ Rust borrow (нет lifetime checker, нет XOR aliasing); safety обеспечивается через escape analysis + auto-promote (Go-style) + unsafe gating. D32 spirit «no borrow» preserved —*Tэто explicit unsafe-gated raw pointer с safety net через GC, не lifetime-checked reference. See Plan 118 §«&value operator + escape analysis с auto-promote» и D216 §4.⚠️ D32 align (Plan 138.5; amend Plan 147 D246) — мутируемость в указательном типе — это L3 pointee (target), из ТИПА позиционно-независимо:
*mut T= writable target,*T= ro-pointee (*T ≡ *ro TУНИВЕРСАЛЬНО; pointee-mut НЕ наследуется от binding — flip-scan-draft отклонён). Перепривязываемость самого указателя — это L1 binding (ro/mut), как у любого параметра/переменной по D32/D36, НЕ часть типа и НЕ влияет на pointee. Prefix-модификатор перед*(mut * T) запрещён (D216 §1E_POINTER_PREFIX_MODIFIER). Семантика передачи параметров (этот D32) не меняется:mut p *T= mutable binding указателя (p reassignable), доступ к target — по pointee-модификатору типа.Amended Plan 114 D184 (2026-05-31): default immutable binding теперь выражается через
ro X = …(immutable) иmut X = …(mutable);letretracted. Семантика default-immutable не меняется — только keyword. См. D184.
Что
Параметры функций передаются by reference в managed heap (как Java/C#
для объектов, Go для maps/slices). Без mut — immutable view, с
mut — мутации видны вызывающему. Примитивы (int, bool, f64,
…) — by value в регистре. Borrow &T отсутствует как концепция.
Правило
Базовое поведение.
type Account { balance money }
// без mut — функция только читает
fn show(acc Account) Io -> () =>
println("balance: ${acc.balance}")
// с mut — функция меняет, изменения видны вызывающему
fn deposit(mut acc Account, amount money) {
acc.balance += amount
}
mut my_acc = Account { balance: 100 }
deposit(my_acc, 50)
// my_acc.balance == 150 — мутация видна
Примитивы — by value. Числа, bool, char, u8, () —
всегда копия в регистре. С mut x int это локальная переменная
функции, изменения не видны вызывающему:
fn weird(mut x int) {
x = 999 // меняет локально
}
ro n = 5
weird(n)
// n == 5 — примитив всегда by value
Явная таксономия value vs reference типов (D215 amend, Plan 120; Receiver mut-ABI column added Plan 128 Ф.5, 2026-06-05):
| Категория | Примеры | Размещение | Передача | Receiver mut-ABI (fn T mut @...) |
|---|---|---|---|---|
| Примитивы | int, bool, f64, char, u8, () | register/stack | by value (копия) | forbidden — E_PRIMITIVE_MUT_METHOD (Plan 128 Ф.3) |
| Tuples (positional или named) | type X(T1, T2), type Vec3(x f64, ...) | stack | by value (копия) | NovaTuple_<X>* pointer (Plan 128 Ф.2) — &v/hoist+&temp call-site |
| Value records | type X value { ... } | stack | by value (копия) | NovaValue_<X>* pointer (D228) — &v/hoist+&temp call-site |
| Records | type X { ... } | managed heap | by reference (указатель) | Nova_<X>* pointer (unchanged — already by-reference) |
| Sum types | type X | A | B | managed heap | by reference | Nova_<X>* pointer |
| Arrays | []T | managed heap (handle inline) | by reference | Nova_<X>* pointer |
str (Plan 139) | str | stack (16-байт value {ptr,len}; буфер на heap/rodata) | by value (копия) | nova_str value — handle-copy |
strreclassified (Plan 139, 2026-06-11): ранееstrстоял в одной строке с[]Tкак «managed heap / by reference». Теперьstr— value type, несущий heap-backed буфер: само значение — 16-байт stack-valuetype str value priv { ptr *u8, len int }с copy-семантикой (как примитив/tuple/value-record), а UTF-8 байты живут в heap (RawMem, GC-tracked) либо rodata (литералы). Поэтому передача str — by-value копия 16-байт handle’а (НЕ pointer-to-heap-object), а buffer разделяется immutably через*u8(ro-pointee, ≡*ro u8; D246). См. D26 MAJOR AMEND
- D228 «str — канонический reference-field value-record».
Bracket choice явно кодирует size/lifetime semantics: () =
stack, {} = heap. Tuple value types (D123): zero GC pressure,
predictable lifetime — ideal для hot-path math types, FFI returns,
iterator state.
Receiver mut-ABI rationale (Plan 128): value categories (tuples,
value-records) — by-value normally, но mut @ receiver требует
pointer чтобы мутации были видны caller’у. Reference categories
(records, sum-types, arrays, strings) уже passed by pointer — no extra
ABI flip. Primitives никогда mut-method (Nova-first idiom: int.add
returns new value, не mutates self) — Plan 128 Ф.3 E_PRIMITIVE_MUT_METHOD
diagnostic enforces. Threading: MethodCallInfo::recv.mutable flag
консолидирует решение, propagated через emit_c.rs::prepare_method_recv
(Plan 128 Ф.1).
Объекты (record / sum-type / массивы) — managed reference.
Указатель в managed heap, отслеживаемый GC. В синтаксисе программист
пишет просто o Order — никакого & или *:
type Order { items []Item, total money }
fn add_item(mut order Order, item Item) {
order.items.push(item)
order.total += item.price
}
mut my_order = Order { items: [], total: 0 }
add_item(my_order, item1)
// my_order содержит item1 и обновлённый total
&T (borrow в Rust-стиле) не существует в Nova. Escape analysis
закрывает большинство perf-кейсов автоматически; для real-time —
region { ... } (05-memory.md → D6).
Иммутабельный binding. Без mut параметр нельзя мутировать ни
одно поле (кроме помеченных mut per-field — см.
D36):
type Account { balance money }
fn read_only(acc Account) {
acc.balance += 50 // ОШИБКА: acc immutable
println(acc.balance) // ок, чтение
}
Семантика mut на параметре и mut на поле взаимодействуют через
правила D36 — для записи нужно соответствие на обоих уровнях.
Производительность. Когда нужна максимальная производительность
без GC overhead — escape analysis (автоматически) или
region { ... } (05-memory.md → D6):
fn process_audio(samples []f32) Realtime -> []f32 =>
region {
ro buf = []f32.with_capacity(1024)
// обработка, без GC pauses
buf.to_owned()
}
Никаких &T borrow, никаких lifetime-аннотаций в обычном коде.
Сводка
| Форма параметра | Передача | Мутация видна снаружи |
|---|---|---|
x int (примитив) | by value | нет (примитив всегда копия) |
mut x int | by value | нет (локальная копия) |
o Order (объект) | managed reference | нет (immutable view) |
mut o Order | managed reference | да |
Почему
- Согласовано с managed heap (05-memory.md → D6) — объекты уже в куче, передача указателя дешёвая, копировать бессмысленно.
- AI-first видимость в типах (01-philosophy.md → D10)
— сигнатура
fn deposit(mut acc Account, …)противfn show(acc Account)сразу показывает контракт. Java/C#: всё mutable references по умолчанию, программист помнит наизусть. mut— единый префикс для разных случаев (let, поле, параметр). Везде «mut = разрешена мутация» — одно понятие, не разные. Согласовано с D36 и 03-syntax.md → D33.
Что отвергнуто
- By-value для всех типов (Go-стиль). Копирование больших structs дорого, несовместимо с managed heap, программист удивляется «изменил поле — не сохранилось».
- By-reference с обязательным
&mut(Rust-стиль). Слишком много синтаксиса для прикладного кода; в Novamutуже работает для let и полей. - Move-семантика (Rust для не-Copy). Сложна для прикладного программиста, не нужна с GC.
- Borrow
&T. Скопирован в раннем дизайне рефлекторно. Borrow существует в Rust, потому что нет GC; в Nova с GC передача = указатель. Escape analysis +regionзакрывают остальное. Lifetime checker — research-уровень, цена реализации высокая. Go показывает: без borrow инфраструктура интернета работает.
Связь
- 02-types.md → D36
— пересмотр семантики
mutдля полей типа. Параметры — без изменений. - 05-memory.md → D6 — managed heap делает
by-reference дешёвым;
regionдля real-time. - 04-effects.md → D62 —
Mut[T]как generic эффект удалён; мутация черезmutполя/параметры (локально) или специализированные state-эффекты (Counter/Cache/IdGen). - 01-philosophy.md → D10 — AI-first видимость мутации в типе.
- 03-syntax.md → D35 —
fn Type mut @methodиспользует тот жеmutдля self-binding’а.
Эволюция
В D32 поле типа mut field мутировалось только у mut-binding’а.
Для аккумуляторов (все поля mutable) приходилось писать mut 18 раз —
шум без пользы. D36 переписал это: дефолт mutable у mut-binding’а,
readonly для never-mut, mut per-field — только для cache/lazy.
Семантика параметров не менялась.
D36. Поля типа: дефолт mutable у mut binding’а, ro для never-mut
Amended Plan 114 D184 (2026-05-31):
readonly→rokeyword rename в полях. Sample обновлён. Error codeE_READONLY_FIELDсохранён как stable API. Семантика per-field freeze не меняется.
Что
Поле без префикса мутируется, если binding mutable. readonly
запрещает мутацию даже у mutable binding’а (для id, foreign keys,
invariants). mut per-field разрешает мутацию даже у immutable
binding’а (для cache, lazy init, atomic counters — аналог C++
mutable). Group-syntax: несколько полей одного типа через запятую.
Правило
Базовое использование.
// Аккумулятор — все поля мутируемые, никаких префиксов не нужно
type RunAcc {
att_wins int, def_wins int, draws int
total_rounds int
total_moon_chance f64
atk_lost_m int, atk_lost_s int, atk_lost_h int
}
mut acc = RunAcc { att_wins: 0, def_wins: 0, ... }
acc.att_wins += 1 // ок — binding mut, поле без ro
// Структура с invariant'ами — readonly для read-only полей
type Account {
ro id u64 // никогда не меняется
ro owner str // тоже
balance money // мутируется у mut binding'а
closed bool
}
ro acc = Account.new("alice")
acc.balance = 100 // ОШИБКА: binding не mut
mut acc2 = Account.new("alice")
acc2.balance = 100 // ок
acc2.id = 999 // ОШИБКА: id объявлено ro
// Cache/lazy — mut для полей, мутируемых через immutable binding
type LazyConfig {
path str
mut cached_value Option[str] // обновляется при первом read
}
fn LazyConfig @get() -> str {
if Some(v) = @cached_value { return v }
ro v = read_file(@path)
@cached_value = Some(v) // мутация через @-метод даже у ro-binding
v
}
Group-syntax. Несколько полей одного типа — через запятую:
type Point { x, y, z f64 } // три f64
type Color { r, g, b u8 } // три u8
type RunAcc {
att_wins, def_wins, draws int
atk_lost_m, atk_lost_s, atk_lost_h int
atk_lost_pts, def_lost_pts f64
}
С префиксами:
type Account {
ro id, owner_id u64 // два immutable
balance money // дефолт (mutable у mut-binding)
mut last_access_time time // mutable всегда
}
Сводная таблица
| Объявление поля | Mutable у let acc | Mutable у let mut acc | Use case |
|---|---|---|---|
field T (без префикса) | нет | да | большинство полей |
readonly field T | никогда | никогда | id, immutable invariants |
mut field T | да | да | cache, lazy init, atomic counters |
Почему
- Меньше шума для типичного случая. Аккумулятор с 18 mutable
полями писать без префиксов — все поля «обычные», никаких
акцентов. Раньше 18 раз
mut— визуальный мусор. - Сигнатура показывает только важное. Префикс ставится только
на исключения (
readonlyдля invariants,mutдля cache). LLM, читая тип, видит:readonly id— «не трогай», обычное поле — «можно мутировать с mut-binding’ом». - Прецедент Rust/Go/C++ — поля без префикса мутируются у
mut-binding’а;
readonlyдля never-mut близко к C++constmember.
Что отвергнуто
- Старая семантика D32 (поле
mutмутируется только уmut-binding). Заставляет писатьmutперед каждым полем аккумулятора; если все поля mut — выделение теряет смысл. - Rust-полное (поле всегда mutable у mut-binding, нет never-mut). Невозможно зафиксировать read-only invariant без приватного поля + getter.
type X mut { … }(mut на тип). Один маркер вместо 18 — короче, но при 90% mut + 10% read-only нужен опт-аут per field. Усложнение. Конфликт с современным паттерном «struct + immutable defaults + явная мутация» из Swift/Rust.final(Java-стиль) для never-mut полей. Короче, прецедент Java/Dart/Kotlin, но семантически перегружен (final method,final class,final var).readonlyпрямо говорит «только для чтения».letдля never-mut полей. Короче (3 символа), прецедент Swift, ноletуже значит «binding имени со значением» (03-syntax.md → D33). На поле без=необычно, не самообъясняемо.readonlyпрямо говорит цель.const(C++-стиль). Конфликт с 03-syntax.md → D33 — тамconst= compile-time константа. Здесь — runtime-immutable. Перегрузка термина, AI-first против — невозможно.
Связь
- 02-types.md → D32 —
пересмотр семантики
mutдля полей. Передача параметров (fn f(mut o Order)) остаётся:mutна параметре = mutable binding, внутри — мутации полей по правилам D36. - 02-types.md → D17 — group-syntax для полей одного типа внутри record.
- 03-syntax.md → D33 —
letэто immutable binding; на поле — аналогия в ролиreadonly. - 03-syntax.md → D35 —
fn Type mut @methodдаёт mutable-binding self, поля затем по правилам D36.
Эволюция
До D36 поле помечалось mut field T, мутируемое только у
mut-binding’а (D32). Для аккумуляторов это требовало 18 раз
повторить mut — шум без пользы. D36 инвертировал дефолт: «обычное
поле — мутируется у mut-binding’а», readonly — для исключений.
Семантика параметров (D32) не менялась. Подробно — в
history/evolution.md.
Enforcement (Plan 108.2, 2026-05-30)
Плановое поведение D36 («let без mut — immutable») de-facto
существовало с самого начала, но компилятор не enforce’ил его
строго: на binding’е без mut можно было вызывать mut-методы
(.push, .append, .insert и т.п.) и присваивать поля
(b.field = ...). Plan 108.2 закрывает этот gap:
ro b = Box.new(1)
b.value = 99 // ✗ E_LOCAL_NOT_MUT
b.push(2) // ✗ E_LOCAL_NOT_MUT
mut b2 = Box.new(1)
b2.value = 99 // ✓
b2.push(2) // ✓
Правила (Plan 108.2):
| Операция | let x = ... | let mut x = ... | consume x = ... |
|---|---|---|---|
| read field | ✓ | ✓ | ✓ |
| non-mut method | ✓ | ✓ | ✓ |
x.field = ... | ✗ E_LOCAL_NOT_MUT | ✓ | ✓ |
x.mut_method() | ✗ E_LOCAL_NOT_MUT | ✓ | ✓ |
x[i] = ... | ✗ E_LOCAL_NOT_MUT | ✓ | ✓ |
rebind x = newval | ✗ existing E_REBIND | ✓ | n/a (move) |
consume X = ... неявно подразумевает mut (как consume param в D176
amend Plan 108.1) — ownership transfer → владелец может мутировать.
Symmetry с D176 (Plan 108.1):
| Контекст | Default = readonly? | Opt-in mut |
|---|---|---|
| Param | ✓ (Plan 108.1) | mut name T |
| Local binding | ✓ (Plan 108.2) | let mut x = ... |
| Loop variable | ✓ (Plan 108.3) | for mut x in iter |
| Pattern element | ✓ (Plan 108.3) | let (mut a, b) = pair (per-name) |
| Field | ✓ (D36 default = mutable у mut-binding) | n/a |
Loop-var и pattern-binding (Plan 108.3, 2026-05-30)
Loop-var mutability: в for-цикле переменная итерации по умолчанию
read-only. Opt-in mut через for mut x in iter:
for x in arrs { x.push(1) } // ✗ E_LOCAL_NOT_MUT — x immutable
for mut x in arrs { x.push(1) } // ✓ — x mutable
Pattern-binding per-name mut: при destructure (tuple, record) mut
ставится на каждое имя отдельно, parallel Rust pattern semantics:
ro (a, b) = pair // оба immutable
ro (mut a, b) = pair // a mutable, b immutable
ro (a, mut b) = pair // a immutable, b mutable
ro (mut a, mut b) = pair // оба mutable
Запрет group-mut: let mut (a, b) = ... отвергается parser-level
(E_PATTERN_GROUP_MUT) — keyword mut относится к одному имени,
не к pattern целиком (consistent с Rust):
mut (a, b) = pair // ✗ E_PATTERN_GROUP_MUT
Использование mut внутри pattern — единственно правильная форма.
Указатели: перепривязываемость = binding (D216 cross-ref, Plan 138.5)
Перепривязываемость указательной переменной (можно ли p = other_ptr)
регулируется её binding’ом (ro = фиксирован, mut = reassignable) —
ровно как у любой другой переменной по правилам D36 выше. Это НЕ часть
типа указателя: указательный тип несёт только мутируемость pointee
(target) постфиксом (*mut T / *ro T). Никакого type-level «outer
pointer-mut» нет.
ro p *mut int = &x // p фиксирован (binding ro); target writable (pointee mut)
mut q *ro int = &y // q reassignable (binding mut); target read-only
q = &z // ✓ q — mut binding
p = &w // ✗ existing E_REBIND — p ro binding
Две роли чисто разделены: ведущий ro/mut перед именем = binding;
*mut/*ro после * = pointee. Prefix-модификатор перед *
(mut * T) запрещён (D216 §1 E_POINTER_PREFIX_MODIFIER).
Связь
- 02-types.md → D175 — readonly field полный freeze.
- 02-types.md → D176 — readonly T modifier + Plan 108.1 param default flip.
- 02-types.md → D216 §1/§V2.6 — указатель: pointee-mut в типе (postfix), reassignability = binding (Plan 138.5).
- 03-syntax.md → D33 —
letэто immutable binding.
D175. ro field — полный freeze (амендмент D36)
📌 Полная модель мутабельности — D246 (3 оси: L1 binding / L2 content-view / L3 pointee). Этот D-блок описывает только L2: freeze поля через
ro field. Для общей картины, дефолтов и error-кодов читай D246.
⚠️ См. D216 V3 §V3.1 (Plan 118.5 V3, 2026-06-04/05) для storage-class-aware rules о
ro+mutadjacency: type-formro mut Tзапрещён на value-T (E_MUTABILITY_CONFLICT_VALUE_TYPE), binding-formro x mut Tallowed regardless of T storage class (Ф.6 relaxation).
Status: active (Plan 108, 2026-05-28); amended Plan 114 D184 (2026-05-31):
readonly→rokeyword rename. Error codeE_READONLY_FIELDсохранён как stable API. Семантика freeze + транзитивность не меняется.
Что
Уточнение D36: ro field T запрещает и переприсвоение поля,
и мутацию содержимого — транзитивно.
| Объявление | Переприсвоить | Мутировать содержимое | Use case |
|---|---|---|---|
field T | у mut binding | у mut binding | большинство полей |
ro field T | ❌ никогда | ❌ никогда | id, invariants, frozen state |
field ro T | у mut binding | ❌ никогда | mutable ref, immutable content |
mut field T | ✅ всегда | у mut binding | cache, lazy init |
mut field ro T | ✅ всегда | ❌ никогда | swappable readonly view |
Транзитивность: если поле объявлено ro, доступ через него
также запрещает мутацию вложенных полей и вызов mut-методов:
type Tags { mut items []str }
type Account {
ro id u64
ro tags Tags // нельзя acc.tags.items.push("x")
}
mut acc = ...
acc.id = 999 // E_READONLY_FIELD
acc.tags = Tags{} // E_READONLY_FIELD
acc.tags.items.push("x") // E_READONLY_FIELD (транзитивно)
Связь
D176. ro T — тип-модификатор
📌 Полная модель мутабельности — D246 (3 оси: L1 binding / L2 content-view / L3 pointee). Этот D-блок описывает только L2:
ro Tкак type-modifier + параметры ro по умолчанию. Для общей картины, дефолтов и error-кодов читай D246.
⚠️ См. D216 V3 §V3.1 (Plan 118.5 V3, 2026-06-04/05) для storage-class-aware rules о
ro+mutadjacency: type-formro mut Tзапрещён на value-T (E_MUTABILITY_CONFLICT_VALUE_TYPE), binding-formro x mut Tallowed regardless of T storage class (Ф.6 relaxation).
Status: active (Plan 108, 2026-05-28); amended (Plan 108.1, 2026-05-30); amended Plan 114 D184 (2026-05-31):
readonly→rokeyword rename; return-type defaults +@-inheritance section added. Error codesE_READONLY_CONTENT/E_READONLY_COERCE/E_PARAM_NOT_MUTсохранены как stable API.
Что
ro как prefix-модификатор типа в любой позиции:
fn str @as_bytes() -> ro []u8 // возвращаемый тип
fn process(data ro []u8) { ... } // параметр
type Wrapper { field ro []u8 } // поле
ro view ro []u8 = s.as_bytes() // binding с ro-content
Двойное
roв последней строке — не tautology: первоеro— binding mutability (нельзяview = …), второе — type-modifier (нельзяview[0] = …). См. D184 для полного дизайна.
Семантика
- Запрещает вызов
mut-методов на значении типаro T - Запрещает запись через индекс:
view[i] = x→E_READONLY_CONTENT T→ro Tcoercion разрешён автоматически (сужение прав)ro T→Tзапрещён:E_READONLY_COERCE
ro arr []u8 = [1, 2, 3]
ro view ro []u8 = arr // ✅ []u8 → ro []u8
mut back []u8 = view // ❌ E_READONLY_COERCE
view[0] = 99 // ❌ E_READONLY_CONTENT
take_ro(arr) // ✅ auto-coerce при вызове
Return-type defaults + @-inheritance (Plan 114 D184)
Асимметрия с параметрами — намеренная. Plan 108.1 сделал параметры
default ro (callee не может мутировать без opt-in). Для возвращаемых
значений правило противоположное: default = mutable (caller
получает значение, делает с ним что хочет).
fn make_buf(n int) -> []u8 // -> mutable []u8 by default
fn read_view(s str) -> ro []u8 // explicit ro в возврате
Обоснование. Param ro default — defensive (callee не имеет права).
Return mut default — permissive (caller владеет результатом). Это совпадает
с Rust/Swift/Kotlin: fn foo() -> Vec<T> отдаёт owned mutable; чтобы
вернуть read-only view — explicit -> ro T.
Особый случай: pointer-returns (D216 amend, Plan 138.5; D246). Pointee-mut
возвращаемого указателя — это L3, из ТИПА (*T=ro / *mut T=mut), а НЕ
return-mut-default: дефолтная mut-семантика возврата относится только к L1
binding результата у caller’а (reassign), не к pointee. У указательного типа
нет «outer pointer-mut» (retracted, D216 §V2.6).
-> *ro T — E_REDUNDANT_POINTER_RO (используй -> *T).
| Return type | Pointee (L3, из типа) | L1 binding результата |
|---|---|---|
-> *T (≡ -> *ro T) | read-only | bind-site (ro p/mut p) |
-> *mut T | writable | bind-site (ro p/mut p) |
-> *unsafe T | possibly-uninit (FFI) | bind-site (ro p/mut p) |
fn alloc_cell() -> *mut int // writable target
fn peek_head(buf []u8) -> *u8 // *u8 = ro target (D246; *ro u8 → E_REDUNDANT_POINTER_RO)
ro p *mut int = alloc_cell() // binding ro: p фиксирован; target writable
mut q *u8 = peek_head(buf) // binding mut: q reassignable; target ro
Реассайнабельность результата (p = other_ptr) задаётся bind-site
(ro/mut, D36),
не возвращаемым типом. Это устраняет прежнюю двусмысленность «двух mut» в
return-позиции (pointee-mut в типе vs reassignability указателя).
Ревизия (Plan 184, 2026-07-06). Наследование мутируемости ниже — исходная D326-R7/R8 формулировка через оракул/decay. По ревизии D326 (Plan 184, Р7)
-> @имеет конкретный тип:-> ref Selfу стекового (value) типа,-> Selfу кучевого — см. Ревизия D326 (Plan 184). Таблица ниже сохранена как исходная семантическая модель (поведение цепочек эквивалентно).
Особый случай: -> @ (self-return для fluent chains, D181).
Возвращаемая @ наследует мутируемость от receiver:
| Receiver | Return -> @ | Пример |
|---|---|---|
fn T @method() -> @ (implicit/ro receiver) | ro @ (read-only self-view) | ro r = obj.method() |
fn T mut @method() -> @ | mut @ (mutable self-view) | obj.push(1).push(2) — fluent mut chain |
fn T consume @method() -> @ | parse error E_CONSUME_RECEIVER_RETURNS_AT | consume already moves ownership; return @ создал бы dangling-view |
Почему такое правило для @. @ это тот же экземпляр что
receiver — его access-mutability не может быть строже, чем у receiver’а:
ro @receiver →@уже view; return view’а — view; consistent.mut @receiver →@mutable handle; return mutable handle; consistent — именно так работают fluent chainsxs.push(1).push(2).consume @receiver → ownership уже перемещён внутрь method’а; вернуть@= alias на consumed value = use-after-move; запрещено. Если нужно fluent после consume — возвращайте новый owned (fn T consume @transform() -> T), не@.
Что НЕ меняется в return-семантике:
- Любой явный return type (
-> T,-> []u8,-> ro T,-> mut T,-> *mut T,-> *ro T) — берётся как написан (для указателей модификатор всегда относится к pointee — postfix; prefix перед*=E_POINTER_PREFIX_MODIFIER, D216 §1). -> Self(статический Self-тип, D182) — owned-by-caller; не наследует receiver-мут.-> @без receiver-method context (free fn) →E_AT_RETURN_OUTSIDE_METHOD.
Escape hatch
Снять readonly в Nova-коде нельзя. Кому нужен mutable доступ —
явно копирует: let copy []u8 = view.to_owned(). Если необходим
обход через FFI, это делается в external fn на C-стороне.
Рантайм
Zero overhead — readonly только compile-time проверка, не влияет
на codegen. ABI readonly []u8 = NovaArray_uint8_t* (идентично []u8).
Применение
str.as_bytes() -> readonly []u8 — zero-copy view в UTF-8 буфер строки
без memcpy. UTF-8 invariant защищён: записать в буфер нельзя.
Параметры функций (Plan 108.1)
Default = read-only. Параметр без явного модификатора эквивалентен
readonly param T — callee может только читать, не вызывать mut-методы,
не присваивать через индекс.
fn f(b []int) { b.push(1) } // ✗ E_PARAM_NOT_MUT — нет `mut`
fn f(mut b []int) { b.push(1) } // ✓ explicit mut
fn f(ro b []int) { ... } // ✓ synonym default (для документации)
fn f(consume b []int) { ... } // ✓ owned move — mut по умолчанию
Правила сочетания модификаторов:
| Сочетание | Результат |
|---|---|
param T | readonly (default) |
mut param T | mutable view |
readonly param T | readonly (явно) — synonym default |
consume param T | owned move, mut by default |
mut consume param T | ✗ parser-level E_PARAM_MOD_CONFLICT |
consume mut param T | ✗ parser-level E_PARAM_MOD_CONFLICT |
mut readonly param T | ✗ parser-level E_PARAM_MOD_CONFLICT |
readonly mut param T | ✗ parser-level E_PARAM_MOD_CONFLICT |
Coercion (передача аргумента в параметр).
После Plan 108.1 T в позиции параметра уже readonly по умолчанию.
Поэтому readonly T → T (param) — это readonly → readonly (тождество),
а единственное реальное нарушение это readonly → mut:
| caller-type → callee-param-type | OK? |
|---|---|
T → T (param default readonly) | ✓ (caller-T → callee-readonly = сужение) |
T → readonly T (param explicit readonly) | ✓ (synonym default) |
T → mut T (param explicit mut) | ✓ (caller разрешает mut доступ) |
readonly T → T (param default readonly) | ✓ — оба readonly |
readonly T → readonly T | ✓ |
readonly T → mut T (param explicit mut) | ✗ E_READONLY_COERCE — единственное нарушение |
mut T → T (param default readonly) | ✓ (сужение, mutable можно показать как readonly) |
mut T → mut T | ✓ |
Closure-параметры — аналогично функциональным.
Закрытые маркеры (Plan 108.1)
- ✅
[M-108-readonly-mut-method-check]— вызовmut-метода на параметре безmutтеперь даётE_PARAM_NOT_MUT. - ✅
[M-108-readonly-coerce-on-param]— closed дефакто: старая формулировка маркера предполагала, что paramTmutable; после Plan 108.1 paramTуже readonly, поэтому coercereadonly T → T (param)— этоreadonly → readonly(no violation). Единственный остаточный case —readonly T → mut T (param explicit)— отдельный followup[M-108.1-readonly-to-explicit-mut-coerce](узкий нишевый сценарий, не блокирует).
Связь
- D36 —
readonly fieldпредшественник - D175 — readonly field enforcement
- D144 — слайсы
arr[a..b] - D157 — view-borrow для consume-типов (Plan 108.1 распространяет принцип на не-consume)
- Plan 108 — реализация D175/D176
- Plan 108.1 — params readonly by default + закрытие 2 markers
D66. Self universal — ссылка на обобщающий тип в методах, effects, protocols
Что
Self — keyword-ссылка на «тот тип, к которому принадлежит метод»,
валиден в любом контексте, ассоциированном с конкретным типом:
- Внутри
protocol { ... }—Self= тип, удовлетворяющий контракту (как сейчас по D42 (REVISED)/D53). - Внутри
effect { ... }—Self= тип эффекта (Db,Net, …). - В static-методе
fn T.name(...)—Self≡T. - В instance-методе
fn T @method(...)/fn T mut @method(...)—Self≡T. - Для generic-типа
T[A, B]—Self≡T[A, B](с теми же параметрами).
Правило
type Box[T] {
value T
}
// static method — Self вместо повтора Box[T]
fn Box[T].of(v T) -> Self =>
Self { value: v }
// instance method — Self в return type для builder pattern
fn Box[T] @with_value(v T) -> Self =>
Self { value: v }
// protocol — для type-safe equality
type Hash protocol {
hash() -> u64
eq(other Self) -> bool // Self = тот тип, что реализует
}
// effect — для transactional/recursive handler-операций
type Db effect {
query(q Sql) -> []DbRow
nested(body fn() Self -> ()) -> () // Self = Db
}
// sum-type method
type Tree enum Leaf | Node(int, Tree, Tree)
fn Tree @clone() -> Self => match @ {
Leaf => Leaf
Node(v, l, r) => Node(v, l.clone(), r.clone())
}
Семантика
Selfподставляется в момент использования метода/протокола, не в момент объявления.- Для concrete-типа
T(record, sum, newtype)Self≡T. - Для generic
T[A, B]Self≡T[A, B](наследует ту же специализацию). - Внутри protocol-объявления
Selfостаётся «late-bound» — конкретный тип определяется при удовлетворении.
Static-методы знают свой тип через Self
Static-метод в Nova связан с типом на уровне компилятора — не
«просто функция в namespace» (как Go), а полноценный метод типа
с доступом к Self. Это влияет на три use-case’а:
1. Self в return type (DRY-форма)
type Box[T] {
value T
}
fn Box[T].of(v T) -> Self => // Self ≡ Box[T]
Self { value: v } // generic-параметры наследуются
// Эквивалент без Self (verbose):
fn Box[T].of(v T) -> Box[T] =>
Box[T] { value: v }
Без Self программист пишет Box[T] дважды; с Self — один раз
(в receiver). Compiler знает что Self ≡ Box[T] потому что метод
объявлен на Box[T].
2. Self в expression position — вызов другого статического
type Account { balance money }
fn Account.new() -> Self =>
Self.with_initial(0) // другой static-метод того же типа
fn Account.with_initial(amount money) -> Self =>
Self { balance: amount } // Self { ... } literal
Self.with_initial(0) резолвится compiler’ом в Account.with_initial(0).
То же для Self { ... } — это Account { ... } literal.
Это canonical pattern для default-конструктор → parameterized-конструктор:
fn HashMap[K, V].new() -> Self =>
Self.with_capacity(16) // default делегирует к parameterized
fn HashMap[K, V].with_capacity(n int) -> Self =>
Self { buckets: new_buckets(n), count: 0, ... }
Refactoring-safe: переименование HashMap → Map меняет только
заголовки методов, не тела. Все Self авто-резолвятся.
3. Self в полиморфных контекстах (через protocol bound)
type FromStr protocol {
from_str(s str) -> Self // late-bound
}
fn parse[T FromStr](s str) -> T => T.from_str(s)
// ^^^^^^^^^^^^
// На каждой инстанциации parse[int](...) / parse[Money](...)
// T резолвится в конкретный тип. Compiler через monomorphization
// знает Self ≡ T для каждого вызова.
Это post-monomorphization — для каждого parse[X] генерится свой
код где X.from_str(s) это конкретный static-метод X. Static-метод
знает что он на X в каждом инстанциации.
Что это не значит
- Нет runtime-рефлексии. Static-метод не имеет
cls-параметра (как Python@classmethod), не может узнать своё имя как строку, не может сравнить два типа в runtime. Знание чисто compile-time. - Self в expression — синтаксическая подстановка. Compiler
заменяет
Selfна имя receiver-типа в момент codegen’а; runtime никаких type-id не передаёт. - Нет inheritance / virtual dispatch. Self ≠ виртуальный reference на subclass. У Nova нет наследования (D1) — только generic-bound через protocol.
Прецеденты
- Rust:
impl Foo { fn make() -> Self { Self::new(2) } }— активно используется.Selfдоступен везде в impl-блоке. - Swift:
static func make() -> Self,Self.method(),Self()initializer. - Kotlin:
companion objectс methods, доступ кthis::class. - C#:
staticметод имеет доступ к containing type.
Не следуем:
- Go: static-методов нет, только receiver-функции. Static в Nova = named function в namespace типа.
- Python
@staticmethod: не получаетcls, не знает свой тип.@classmethodполучаетclsruntime — мы делаем то же на compile-time черезSelf.
Где запрещено
- На top-level (вне типа/protocol/effect) — compile error «Self не в type-контексте».
- Внутри лямбды, объявленной не в method-теле — compile error.
- В сигнатуре свободной (top-level) функции
fn name(...)— compile error.
Почему
- DRY. До D66 в каждом методе
fn Box[T].of(v T) -> Box[T]имя типа повторялось 2-3 раза. Refactoring (Box→Container) ломал копипастой.Selfустраняет повтор. - Generic-параметры наследуются автоматически.
fn Box[T].ofсSelfкорректно подставитBox[T], неBoxбез параметров — программисту не нужно указывать generics в методе. - AI-friendly. LLM генерирует
Selfдля return type без знания точного имени — снижает количество ошибок при автогенерации builder-методов. - Унификация. До D66
Selfработало только в protocol — это создавало впечатление, что для других контекстов нужен другой механизм. На самом деле семантика одинаковая — «текущий тип». Один keyword для всех контекстов = D40 «один способ». - Прецеденты. Swift, Rust используют
Selfуниверсально (везде где естьimpl T { ... }блок). Nova следует тому же паттерну.
Что отвергнуто
@type— конструкция вида@typeдля ссылки на свой тип в методе. Отвергнуто:@уже занят под self-field, добавление второго смысла создаёт двусмысленность.- Имя типа повторять везде. Отвергнуто: см. п.1 «DRY».
Selfтолько в generic-методах (как в Java<T extends Self>). Отвергнуто: семантика остаётся та же, ограничение лишнее.
Связь
- D42 (REVISED) / D53 —
Selfв protocol’ах (исходное правило, расширено D66). - 03-syntax.md → D35 —
@-методы и@field. - 04-effects.md → D61 — effect-типы и handler’ы.
Эволюция
В D42 Self был валиден только внутри protocol { ... } блока —
это ограничение унаследовано от первой редакции, где Self вводился
именно для type-safe equality (Hash.eq(other Self)). На
practice’е Self оказался полезен также в:
- static-методах для DRY возврата того же типа,
- instance-методах для builder pattern’а,
- effect-методах для self-referential операций (transactions),
- sum-вариантах для
@clone/@with_*методов.
D66 убирает ограничение: Self валиден везде, где есть type-контекст.
AMEND (Plan «self-nested-generic», 2026-06-15) — Self как вложенный generic type-arg
Правило (расширение «generic-параметры наследуются», п.2 «Почему»).
Self валиден не только как самостоятельный return/param-тип, но и как
type-argument внутри другого Named generic — в любой глубине вложения,
в return- и в param-позиции:
type MapIter[I, T, U] value { src I, f fn(T) -> U }
// Self как вложенный type-arg — receiver-mono наследуется в позицию I
fn MapIter[I, T, U] @zmap(g fn(U) -> V) -> MapIter[Self, U, V] => ...
// ^^^^^^^^^^^^^^^^^^^^ Self ≡ MapIter[I,T,U]
fn MapIter[I, T, U] @zfilter(p fn(U) -> bool) -> FilterIter[Self, U] => ...
// и в param-позиции (симметрично)
fn FilterIter[I, T] @combine(other FilterIter[Self, T]) -> ... => ...
Семантически MapIter[Self, U, V] ≡ MapIter[MapIter[I,T,U], U, V] —
Self подставляется на тот же mono-инстанс receiver’а, что и в
return--> Self. Это устраняет повтор имени receiver-типа в
adapter-on-adapter цепочках (zero-cost ленивые итераторы,
std/collections/vec_iter_zc.nv).
Где было сломано (codegen, до фикса). Call-site return-inference
биндил Self value-aware (без trailing-*), но эмиссия метода
(register_mono_method_instance fwd-decl + emit_monomorphized_method
body) строила current_type_subst только из receiver-generics — без
записи для Self. Вложенный Self промахивался мимо early-lookup
current_type_subst["Self"] и падал в общий "Self"-арм
type_ref_to_c, который даёт POINTER-форму (Nova_X*) → лишний trailing
* → _p в mangle → C-имя mono на call-site ≠ имя в fwd-decl/body →
forward-decl≠def. Это тот же класс рассинхрона, что
D182
закрыл для голого -> Self.
Фикс. В обоих emit-местах после установки current_receiver_type
биндить Self → value_aware_generic_c_type("Nova_{recv}*") в
current_type_subst (через .entry().or_insert() — no-clobber), guard
recv_type.contains("____") (только mono-инстансы). value_aware_*
оставляет heap-generic / non-value формы без изменений, поэтому top-level
heap-generic -> Self (где Self — owned-by-caller heap-ref, не
value-record) не затронут. Подробности маркера —
docs/plans/backlog-followups.md → [M-138.2-self-in-param].
Известное ограничение (НЕ покрыто фиксом). Self, равный
single-param generic-ресиверу (VecIter[T]), использованный как
type-arg внутри multi-param адаптера (MapIter[Self, T, U]),
по-прежнему мис-резолвит receiver (chain-ENTRY методы на VecIter[T]).
Фикс покрывает Self, где ресивер — тот же adapter-family, который
ре-вкладывается. См. ОСТАЁТСЯ в маркере.
D72. Generic bounds через [T Protocol] — protocol как тип
Что
Параметр-тип в generic-списке может иметь bound — protocol-тип, которому должны удовлетворять конкретизации параметра. Синтаксис — единое правило «name type» без двоеточия:
[T Hash]
[K Hash, V]
[K, T From[K]]
Без bound — [T] — параметр без ограничений (структурное соответствие
проверяется при использовании, как было до D72).
Bound — это protocol-тип (D53) ИЛИ type-set (D310, Plan 172.3: именованное множество конкретных типов, [T SignedInt]). Тот же Hash стоит и в
позиции типа значения (fn f(x Hash) — existential), и в позиции
bound’а (fn f[T Hash](x T) — universal). Одна сущность —
тип со структурным контрактом — в трёх позициях:
- Тип значения:
fn f(x Hash) -> u64 - Bound:
fn f[T Hash](x T) -> u64 - Эффект (между
)и->):fn f(...) Db -> ()(D18)
Различение по позиции, не по keyword’у. Закрывает Q-bounds.
Правило
Синтаксис
generic-params = '[' generic-param { ',' generic-param } ']'
generic-param = identifier [ type ]
generic-param следует общему правилу Nova «name type», как
параметры функции (x int), поля record (id u64), let-bindings
(let x int = 5), for-loops (for x int in xs), embed
(use w HashMapIter[K, V]).
fn sort[T](xs []T, less fn(T, T) -> bool) -> []T
// ^ без bound — структурное соответствие при использовании
fn dedup[T Hash](xs []T) -> []T
// ^^^^^^^^^^^ T должен реализовывать Hash
type HashMap[K Hash, V] {
// ^^^^^^^^^^^ K — Hash, V — без bound
...
}
fn fold[T, Acc](xs Iter[T], init Acc, f fn(Acc, T) -> Acc) -> Acc
// ^^^^^^ ни T, ни Acc bound'а не имеют
fn[T] ReceiverType @method префикс (Plan 101.1 partial, 2026-05-24)
Generic-параметры также декларируются через fn[T] префикс —
для receiver’ов без carrier-brackets ([]T, bare T, tuple). Параллель
D145.
Bound syntax из D72 применим в этой позиции — fn[T Hash] []T @method.
fn[T] []T @map[U](f fn(T) -> U) -> []U // T через fn[T] (нет carrier)
fn[T Hash] []T @dedup() -> []T // bound в fn[T] (D72 + Plan 101.2)
Plan 101.1 status (2026-05-24): parser + базовый codegen работают
для []int element type. Codegen mono-per-T для других element-types
([]str, []User) — marker [M-fn-prefix-int-only-mono]
✅ RESOLVED (Plan 101 Group I, vec_map_int_str fix).
Порядок объявления параметров
Generic-параметры читаются слева направо. Имя в bound’е должно
быть уже объявлено — либо ранее в том же списке [...], либо в
type-контексте (top-level type, окружающий тип для метода).
fn func[K, T From[K]](v K) -> T => T.from(v)
// ^ ^
// объявлен раньше используется в bound
fn func[T From[K], K](v K) -> T // ОШИБКА: K используется до объявления
fn func[T Test[K]](v K) -> T // ОШИБКА: K не объявлен вообще
Это согласовано с правилом параметров функции: fn f(x int, y T) —
имена читаются слева направо, ранее объявленные доступны позже.
Forward-references запрещены ради простоты type-checker’а и
читаемости (LLM не нужно держать «отложенный контекст»).
Bound — это protocol-тип
Hash, From[T], Into[T] и т.д. — обычные protocol-типы (D53):
type Hash protocol {
hash() -> u64
eq(other Self) -> bool
}
// Bound в generic-объявлении:
fn map[K Hash, V](m HashMap[K, V]) -> ...
// Тот же Hash в позиции типа значения (existential):
fn dump_one(x Hash) -> u64 => x.hash()
Existential vs universal — различение по позиции:
| Форма | Семантика | Dispatch | Аналог Rust |
|---|---|---|---|
fn f(x Hash) | existential («какое-то значение типа Hash») | dynamic (vtable) | fn f(x: &dyn Hash) |
fn f[T Hash](x T) | universal («для любого T : Hash») | static (mono) | fn f<T: Hash>(x: T) |
В обоих случаях Hash — тип. Различие только в позиции:
внутри [...] — generic-параметр и его bound; в обычной позиции —
тип значения. Прецедент — Go (interface { M() } используется и как
тип, и как constraint).
Multiple bounds — анонимный protocol
Если параметру нужно несколько bounds, объединяются в анонимный
protocol-тип через protocol { ... } (D53):
fn min[T protocol { @lt(other Self) -> bool, @eq(other Self) -> bool }](xs []T) -> T
Долго, но без специального синтаксиса для intersection bound’ов. Если паттерн повторяется — выносится в именованный protocol:
type Ord protocol {
@lt(other Self) -> bool
@eq(other Self) -> bool
}
fn min[T Ord](xs []T) -> T => ...
Сокращённая форма [T A & B] — открытый вопрос
(Q-multi-bound).
Self в bounds
Self (D66) валиден внутри protocol/method-контекста. В bound’е
generic-параметра свободной функции — запрещён:
fn merge[T Eq](a T, b T) -> T => ... // ok
fn merge[T Eq Self](a T, b T) -> T => ... // ОШИБКА: Self вне type-контекста
В method-контексте (fn Box[T] @method[U Self]) — открытый вопрос,
пока запрещено.
Bound как effect — запрещено
Bound — это protocol-тип. Effect — тоже protocol, но используется
в позиции эффекта (между ) и ->). Использовать Db как bound
запрещено — это ошибка категории (D62: effect ≠ protocol для
generic-bound):
fn run[T Db](handler T) -> () // ОШИБКА: Db — effect, не bound-protocol
Если нужно «принимает Effect[Db]» — пишется явно: fn run(h Effect[Db]).
Bound на типах (не функциях)
Тот же синтаксис в declaration типов:
type HashMap[K Hash, V] {
ro buckets []Slot[K, V]
}
type Set[T Hash] {
ro inner HashMap[T, ()]
}
type Sorted[T Ord] | Empty | Node(T, Sorted[T], Sorted[T])
Bound применяется при инстанциировании: HashMap[User, int] требует
чтобы User реализовывал Hash.
Проверка bound’а — структурная (D53)
Bound удовлетворён, если у конкретного типа есть методы из
protocol’а (структурно). Никаких явных impl/declaration не нужно:
type User { id u64 }
fn User @hash() -> u64 => @id
fn User @eq(other Self) -> bool => @id == other.id
// User автоматически удовлетворяет Hash, потому что есть @hash и @eq
ro m HashMap[User, str] = HashMap.new() // ok
Если методов нет — compile error на месте использования (HashMap[User, str]
с инстанциированием), не на declaration type User.
Почему
-
Закрывает Q-bounds. Generic-инфраструктура (HashMap, From/Into, collect, FromIter) требует bound’ов. Без них либо безопасности нет, либо ошибки откладываются до места использования с непонятным сообщением.
-
Согласовано с правилом «name type». Параметр функции
x int, полеid u64, generic-параметрT Hash— единая грамматика. Двоеточие в Nova зарезервировано под key-value, использовать его для bound — нарушение D17. -
Protocol = тип (D53).
Hashуже тип в Nova. Использовать его как bound — естественное расширение, не новый механизм. Existential (x Hash) и universal ([T Hash]) различаются позицией. -
Прецедент Go. Go 1.18+:
interface { M() }используется и как тип значения, и как constraint в generics. Один синтаксис, два контекста, проверено в большом продакшне. -
Структурная проверка вместо impl. Nova не имеет orphan rule (D42/D53) — нет
impl Trait for Typeблоков. Bound удовлетворяется автоматически, как и existential. Это последовательно. -
AI-friendly. LLM пишет
[T Hash]без специальных keyword’ов (where,impl,:). Грамматика читается как естественный язык: «параметр T типа Hash».
Что отвергнуто
[T: Hash](Rust/Scala/Kotlin/Swift). Конфликтует с D17 — двоеточие в Nova только для key-value (record-литералы, dict). Делать исключение для generic-list — нарушение единства.[T is Hash].isуже занят под runtime type-check (D54). Третий смысл (compile-time bound) перегружает keyword.where-clauses после сигнатуры (C# / Haskell-style). Многословно, раздваивает информацию между списком параметров и where-блоком. Bound у параметра — единое место.[T impl Hash](Swiftsome-style). Нестандартно,implне используется в Nova ни для чего ещё.- Bounds через контракты (
requires implements(T, Hash)). Контракты (D24) проверяются SMT на значениях, bound — type-checker’ом на типах. Разные уровни. - Sealed/closed bound’ы («только эти типы»). Открытый вопрос, не входит в D72.
Цена
- Type-checker сложнее. Проверка structural-bound при мономорфизации — дополнительная работа.
- Сообщения об ошибках. «
Userне реализуетHash: missing method@hash» — нужно генерировать понятные диагностики. - Множественные bounds через анонимный protocol — многословно
для частых пар (
Hash + Eq). Сокращённая форма откладывается.
Связь
- 02-types.md → D53 — protocol = тип, основа D72.
- 02-types.md → D42 — структурная типизация, две модели generic-параметров.
- 02-types.md → D66
—
Selfв protocol-контексте. - 03-syntax.md → D16
—
[T]синтаксис для generic’ов. - 04-effects.md → D18 — protocol в effect-position, отличается от bound-position.
- 08-runtime.md → D73 —
From[T]/Into[T]используют bound[U From[T]]для generic-функций конверсии. - Q-bounds — closed by D72.
- Q-collect-mechanism — становится решаемой после D72.
Открытые вопросы
- Множественные bounds: сокращённая форма (
[T Hash & Eq],[T (Hash, Eq)]) — Q-multi-bound. - Bound на эффект-параметре: можно ли
[E SomeProtocolOnEffects]— связано с Q-effect-params. Selfв bound в method-контексте — отложено.- Conditional methods через
where-clause (fn Vec[T] @sort() where T Ord) — отложено вместе с conditional impls.
Эволюция
В MVP bounds были отвергнуты (D42 «Открытые вопросы»,
history/rejected.md: «[T: Bound] отвергнут
в MVP»). Пользовались структурным соответствием при использовании —
ошибка вылезала на месте вызова, не объявления. С ростом stdlib
(HashMap, From/Into, collect) стало ясно что без bound’ов нельзя:
generic-функции не могут опираться на методы T без явного контракта.
Q-bounds зафиксировал синтаксис заранее ([T Bound] без двоеточия).
D72 принимает это как формальное решение, расширяет до полной семантики
(structural check, existential-vs-universal через позицию, multiple
bounds через анонимный protocol).
⚠️ D72 AMENDED by Plan 108.4 (2026-06-09) — When a type participates as bound
[T Iterable[U]]or any protocol bound, the type-checker now verifies receiver_mut consistency for all protocol methods at the use-site. A type that declares@method()(ro) does not satisfy a protocol requiringmut @method(). Errors:E_PROTO_IMPL_RO_FOR_MUT,E_PROTO_IMPL_MUT_FOR_RO,E_PROTO_IMPL_MUT_FOR_CONSUME,E_PROTO_IMPL_CONSUME_FOR_MUT. See D209.
D110. Ghost state — spec-only bindings
Статус: Принято (Plan 33.3 Ф.10, реализовано в AST и type-checker)
Решение
ghost let / ghost var объявляют spec-only переменные — они видимы
в requires/ensures/invariant и других ghost-statements, но
никогда не эмитируются в C-код (ни в debug, ни в release).
fn fill(xs mut []int) -> ()
ensures forall i in 0..xs.len() : xs[i] == 0
{
ghost ro n = xs.len() // spec-only: виден в invariant
for i in 0..xs.len()
invariant forall j in 0..i : xs[j] == 0
{
xs[i] = 0
}
}
Правила видимости ghost:
- Ghost-binding виден: в других
ghost-stmts; вrequires/ensures/invariant; в теле#pureфункций. - Использование ghost-binding в non-ghost emit-code → compile error.
- Codegen: ghost-stmts и ghost-bindings полностью стираются (паритет с Dafny).
Следствие: invariants, использующие ghost-данные, в debug не проверяются runtime — только через SMT. Это задокументированное design-решение.
Обоснование
Ghost state позволяет писать контракты в терминах вспомогательных
концепций (счётчики, логические флаги, промежуточные значения), не
засоряя runtime-код. Паритет с Dafny ghost var, F* Ghost.
Реализация
compiler-codegen/src/ast/mod.rs— полеis_ghost: boolвLetDecl; enum-вариантStmt::Ghostдля ghost-блоков (Ф.10 scope).compiler-codegen/src/types/mod.rs— type-check: reject ghost-ref в non-ghost context.compiler-codegen/src/codegen/emit_c.rs— ghost-stmts стираются (пустой emit).compiler-codegen/src/verify/encode.rs— ghost-vars участвуют в SMT-encoding как обычные fresh-vars.
D122. Hybrid dispatch для bound-K methods
Status: active (spec). Реализация — Plan 56.
Что
Generic-bound method call’ы dispatch’аются по hybrid strategy:
-
Mono path — для concrete K на call-site (e.g.
HashMap[str, int]): compiler instantiates generic method с substituted K, V. Bound methods (key.hash(),key.eq()) resolve в direct call к concrete K methods (nova_str_hash(key)). Zero-cost — паритет Rustimpl<T: Hash>. -
Erased path — для generic body emit (когда compiler не может / не должен mono’d, e.g. recursive generic call на Self type внутри generic method body): generic body эмитится как stub (call’еры полагаются на mono path для concrete instances). Bootstrap не использует vtable — простая stub-fallback стратегия.
-
Vtable path (future, Plan 56 Ф.2 full): для truly erased contexts (cross-crate generic,
dyn Trait-like), bound methods dispatch’аются через vtable structure. Vtable runtime defined вcompiler-codegen/nova_rt/vtables.h(Plan 56 Ф.1).
Bootstrap status (2026-05-16)
- ✅ Mono path для bound methods works (HashMap.clone() пример).
- ✅ Vtable runtime infrastructure готова (
NovaVtable_Hash,NovaVtable_Compare,NovaVtable_Display+ 4 primitive K vtables: int/bool/u8/f64/str). - ✅ Erased emit для bound-method-using generic methods stub’ится
(
emit_generic_method_erased— wider stub condition включает Array fields с generic inner type). - ⏸️ Vtable codegen integration (truly erased dispatch) — deferred до cross-crate compilation (Plan 03).
Acceptance criteria для bound methods
Type-checker (Plan 15 / D72) enforces:
- Bound должны быть protocol-типами (D53).
- Concrete K на call-site должен implement все bound methods (D72 enforcement).
Codegen (Plan 56 Ф.1 + Ф.2):
- Protocol-методы могут иметь эффекты (
Fail/Io/Db) — напр.type TryFrom[T, E] protocol { try_from(t T) Fail[E] -> Self }. Под mono-dispatch (текущий bootstrap) эффект protocol-метода пробрасывается как у обычной effectful-функции — без спец-кейса. (D122 amended 2026-05-20: снят запрет Plan 56 Ф.2.7 на pure-only bound methods.) Ограничение: true-vtable dispatch (Plan 03) не пробрасывает effect-handlers через vtable-ABI — в truly-erased контексте effectful-protocol bounds обязаны mono-dispatch’иться; чистая vtable-диспетчеризация effectful-метода — будущая работа Plan 03. - Self type в bound method signature substitutes runtime receiver type.
Связь
- D72 — generic bounds enforcement (type-checker side).
- D53 — protocol-типы.
- D24 — vtable lookups compatible с proven-contracts skip (no-op).
D123. Tuple monomorphization
Status: active (spec, 2026-05-17 EOD+2 — Phase 7 production polish applied). Реализация — Plan 59 (6 phases + Phase 7).
Plan 59.1 amend (2026-06-01): general generic anonymous tuple monomorphization —
fn[T] f() -> (A[T], B[T])— закрывает gap в Plan 59 Ф.7.5. Schema_NovaTuple_<arity>_<L1>_<T1>_..._<LN>_<TN>(length-prefixed) теперь применяется не только к Result, но к любому generic anonymous tuple в return position. См. D354 для full spec.Plan 148 Ф.4 amend (2026-06-12,
[M-codegen-unify-tuple-repr]): typed representation унифицирован, legacy all-int путь сжат до on-demand. Три изменения:
- Blanket pre-decl retired. Раньше каждый C-файл получал
typedef … _NovaTuple1; … _NovaTuple8;(8 all-nova_intструктур) в преамбуле — вне зависимости от использования. Теперь legacy_NovaTupleNэмитится on demand только для арностей, которые erased-generic fallback реально запрашивает (на практике — только arity 2, от erasedHashMap[K,V]/Set(K, V)пар). Concrete tuples всегда используют typed mono’d путь. Регистрируется черезregister_legacy_tuple(n), splice в/*__LEGACY_TUPLE_TYPEDEFS__*/.- Self-describing field decode. Field access (
t.0,t.0.1) и type inference больше не зависят исключительно от per-Ident side-tabletuple_element_types— элемент-тип декодируется напрямую из имени mono’d структуры вobj_tyчерезparse_mono_tuple_elements. Это чинит field-read на fn-параметрах, call-result кортежах и вложенныхt.0.1цепочках (раньше collapse’или вnova_intfallback → дроп второго.0/ неверный тип). Закрыло 5 pre-existing CC-FAIL в Plan 59 (f2/f10/f13/f15/f16).- Arity diagnostic code. Destructure-arity-mismatch diagnostic (3 codegen-сайта: let / for-pattern / match-variant inner Tuple) несёт код
[E_TUPLE_DESTRUCTURE_ARITY](раньше — bare message).
Что
Tuple типы (T1, T2, ..., TN) monomorphized — для каждой concrete
комбинации element types compiler generate’ит отдельную struct
с real field types (не nova_int slot erasure).
Mangle scheme (Plan 59 Phase 5, length-prefixed)
Itanium ABI / Rust v0 mangle analog — unambiguous для любой глубины nesting:
_NovaTuple_<arity>_<L1>_<T1>_<L2>_<T2>_..._<LN>_<TN>
где <Ln> — десятичная byte length sanitized name <Tn>. Parser
читает length, берёт точно столько chars, переходит к следующему.
Самоописательный, никаких ambiguity даже для tuple-of-tuples.
Примеры:
(int, int)→_NovaTuple_2_8_nova_int_8_nova_int(str, int)→_NovaTuple_2_8_nova_str_8_nova_int((int, int), int)outer →_NovaTuple_2_34__NovaTuple_2_8_nova_int_8_nova_int_8_nova_int(L1=34 — точно столько chars как T1)
Distinguishable от legacy _NovaTupleN (e.g. _NovaTuple2) по _
после NovaTuple.
Правило
ro p (str, int) = ("a", 1)
// ^^^^^^^ generates _NovaTuple_2_8_nova_str_8_nova_int
// { nova_str f0; nova_int f1; }
for (k, v) in hashmap {
// ^^^^^^^^^^^^^^^^ implicit Iter (D58) + tuple destructure через
// mono'd struct (k: nova_str, v: nova_int direct
// field access)
}
match some_kv {
Some((k, v)) => ...
// ^^^^^^^ Plan 59 Phase 6 — variant payload mono'd tuple,
// heterogeneous types работают (str + int)
}
Параллель: Rust (T1, T2) mono’d per concrete instantiation,
zero-cost. C++ std::tuple<T1, T2> template — то же. Nova bootstrap
паритет (vs предыдущий int-slot erasure breaking struct elements).
Decision tree
При codegen tuple type:
- All elements concrete (resolved via current_type_subst,
no type-param placeholders) → use mono’d
_NovaTuple_<arity>_<L1>_<T1>...struct. Zero erasure cost. - Erased context (one or more element types unresolved) →
fallback legacy
_NovaTupleN(nova_int slot) с runtime cast. Bootstrap-compat для truly generic contexts. Plan 148 Ф.4: этот typedef эмитится on demand (per requested arity, idempotent-guarded#ifndef) — не blanket_NovaTuple1..8. На практике достигается только arity 2 (erasedHashMap/Set(K, V)пары).
Constraints
- Tuple field access (
p.0,p.1) — direct C field access (.f0,.f1) на mono’d struct. - Tuple destructure (
let (a, b) = ...) — direct binding, no cast. - Nested tuples (
((int, str), bool)) — recursive mono’d (inner tuple registered first; length-prefix encoding handles нестинг любой глубины — validated 5-level tests). - Tuple в variant payload (
Option[(K, V)],Result[(K, V), E]) — match destructureSome((k, v))/Ok((k, v))propagate mono’d element types через registry (Phase 6 + Plan 63 Fix F+). - Tuple in collections (
HashMap[K, V]returnsOption[(K, V)]fromiter().next()) — mono’d через template + subst at iter mono pass.
Diagnostics (Plan 59 Phase 7.1)
- Arity mismatch (
[E_TUPLE_DESTRUCTURE_ARITY], code added Plan 148 Ф.4) — destructure pattern имеющий разное число элементов чем actual tuple, reject’ится Nova-level clear error (file:line + hint) до C-emit’а. Покрывает 3 sites: let-destructure, for-pattern, match-variant inner Tuple. Раньше упирался в нечитаемый “no member named ‘fN’” C error.
Lint warnings (Plan 59 Phase 7.3)
- Large tuple warning — mono’d tuple с >5 элементов OR >128 bytes estimated size emit’ит W-warning suggesting record type (clarity + stable ABI). Estimate sums known element sizes: pointers=8, nova_str=16, scalars per type. Threshold выбран эмпирически — typical cache line 64 bytes, 2× giving safe margin.
Stdlib idiom (Plan 59 Phase 7.2)
После Plan 63 Fix E (mono’d tuple iter в generic method body
работает) — stdlib коллекции используют идиоматичный
for (k, v) in self / for (k, v) in @iter() вместо
direct-field workaround’ов. HashMap.@clone/@merge_from/@filter все
idiomatic.
Field literal style (related, D52 §2)
Record literal для tuple struct полей ({ end, idx: 0 } для
{end int, idx int} где end — variable в scope) — shorthand
обязателен при совпадении имени поля с источником ({ end: end }
запрещено, см. D52 §2).
Почему
- Correctness — struct value types (nova_str, user records)
не fit’ят в nova_int slot. Без mono
(str, int)was broken. - Zero-cost — direct field access, no intptr_t cast, no heap alloc для tuple value.
- Параллель Rust/C++ — индустриальный standard для tuples.
- Diagnostics quality — Plan 36 R7 bar (file:line + hint).
- Self-describing mangle — length-prefix encoding debug’абельно, ABI-tools (debuggers) могут decode.
Что отвергнуто (deferred с rationale)
- Universal tuple type (all elements
any) — type-erased, runtime type-tag overhead, breaks AOT zero-cost goal. Named tuple fields (✅ REOPENED (Plan 120, 2026-05-31). Отклонение основывалось на неполном reasoning: tuple и record имеют fundamentally different allocation semantics (D32: stack vs heap). Named tuple fields не эквивалентны record — они value types с именованным доступом, zero GC overhead. See D215.(x: T1, y: T2)) — ОТКЛОНЕНО окончательно (Plan 59 Ф.7.4, 2026-05-21). Именованные поля кортежа почти идентичны record’у; заводить два почти одинаковых синтаксиса для одной семантики в Nova нет причин. Нужен агрегат с именованными полями — это record (type T { x int, y int }). Tuple остаётся позиционным (.0/.1).- Tuple subtyping (
(int, str) <: (any, any)) — ОТКЛОНЕНО окончательно (Plan 59 Ф.7.6, 2026-05-21). Реализация дорогая (требует variance-системы covariance/contravariance в type-checker, которой в Nova нет — язык не использует structural typing); под фичу не нашлось ни одной реальной задачи. Не реализуется. Full mono’d Result (✅ РЕАЛИЗОВАНО (Plan 59 Ф.7.5 increment 2, 2026-05-21): Result полностью мономорфизирован — per-(T,E) C-типNovaRes_<T>_<E>typedefs analogous Option) — Plan 63 Fix F+ targeted boxed-pointer tracking покрывает все observable cases без full sum-type mono refactor. Defer до Plan 65.NovaRes_<ok>_<err>*(аналогNovaOpt_<T>). Legacy единыйNova_Resultустранён; targeted Fix F+ boxed-tracking больше не нужен — Ok/Err payload типизируется реальным T/E inline.
Связь
- D27 — tuple литерал синтаксис.
- D52 §2 — field shorthand mandatory.
- [D58 Iter protocol] —
for (k, v) in collиспользует mono’d tuple через implicit.iter(). - Plan 48 — monomorphization infrastructure (mono pass).
- Plan 63 — Fix E (mono’d iter в generic method body) + Fix F/F+ (Result Ok payload tuple unboxing).
D215. Named tuple fields + value/reference allocation contract
Status: active (spec, 2026-05-31). Реализация — Plan 120. Extends D52 tuple form; amends D32 с explicit value/reference taxonomy; amends D123 с named field codegen. Withdraws Plan 59 Ф.7.4 rejection (corrected reasoning).
Что
Extension D52 tuple form: поля кортежа могут быть именованными
(parallel с positional). Named tuple — stack-allocated value type
с именованным доступом (.x, .y), identical performance к
positional tuple (D123).
Allocation contract — bracket choice кодирует semantics:
| Синтаксис | Тип | Размещение | Семантика |
|---|---|---|---|
type X(T1, T2) | positional tuple | stack | value (копия при передаче) |
type X(name1 T1, name2 T2) | named tuple | stack | value (копия при передаче) |
type X { name T } | record | heap (GC) | reference (pointer при передаче) |
Синтаксис
// Named tuple declarations
type Point(x f64, y f64)
type Vec3(x f64, y f64, z f64)
type Color(r u8, g u8, b u8, a u8)
type Generic[T](value T, count int)
// Construction — named args
ro v = Vec3(x: 1.0, y: 2.0, z: 3.0)
ro c = Color(r: 255, g: 0, b: 128, a: 255)
// Field access — by name
v.x // 1.0
v.y // 2.0
// Methods — identical to records
fn Vec3 @add(other Vec3) -> Vec3 =>
Vec3(x: @x + other.x, y: @y + other.y, z: @z + other.z)
Грамматика (extends D52)
tuple_fields ::= positional_list | named_list
positional_list ::= type ("," type)*
named_list ::= named_field ("," named_field)*
named_field ::= IDENT type
// Mixed positional+named в одном декларации — forbidden (E_TUPLE_MIXED_FIELDS)
Parser disambiguation: если после ( стоит IDENT type-start →
named tuple; иначе → positional. Один lookahead, никакого backtracking.
Type errors
| Ситуация | Ошибка |
|---|---|
.0 на named tuple | E_TUPLE_POSITIONAL_ACCESS_ON_NAMED |
.name на positional tuple type | E_TUPLE_NAMED_ACCESS_ON_POSITIONAL |
| mixed named+positional в declaration | E_TUPLE_MIXED_FIELDS |
Codegen (extends D123)
Named tuple → C named struct (not anonymous):
typedef struct NovaTuple_Vec3 NovaTuple_Vec3;
struct NovaTuple_Vec3 {
double x;
double y;
double z;
};
Symbol prefix NovaTuple_<Name> distinguishes от positional
_NovaTuple_<arity>_... и от records Nova_<Name>*. Named tuple
= value type (no pointer in C signature); всегда stack-allocated.
Method receiver passing (Plan 128 Ф.2, 2026-06-05)
Named tuple @-методы получают receiver через ABI-conditional форму:
| Receiver mode | Parameter type | Call-site form |
|---|---|---|
fn NamedTuple @method(...) (ro receiver) | NovaTuple_<Name> (by value, copy) | f(v) — copy semantics |
fn NamedTuple mut @method(...) (mut receiver) | NovaTuple_<Name>* (pointer) | f(&v) для identifier; для rvalue — hoist в temp + &temp |
Mutation visibility: mut @method мутирует caller’s slot через
pointer; copies стека не делается. Это symmetric с D228 value-record
mut @method (NovaValue_X*).
Call-site emission (emit_c.rs::prepare_method_recv):
- Identifier receiver: emit
&local_vardirectly. Var must bemutbinding (D33 + D215 amend «binding-level mutability»); ro binding + mut @method =E_BINDING_NOT_MUT(caught в type-checker). - Lvalue projection receivers:
b.v.method(),arr[i].method(),@field.method(), multi-levela.b.c.method()— when each base в projection chain is an lvalue (Ident/SelfAccess/Member-of-lvalue/ Index-of-lvalue), emit&(b->v)/&(arr->data[i])/&(nova_self->field)directly. Mutation flows к original slot, no temp hoist. Plan 128.1 Ф.1 implementation. - Rvalue receiver: hoist в
NovaTuple_<Name> __tmp_recv_<id> = expr;и pass&__tmp_recv_<id>. Мутации в temp видны только внутри expression chain — corresponds к D32 «mutate-by-copy для rvalue» spirit. - Chained
.method()на trailing receiver — recurse same rule.
Symmetric правило с records (D32): records передаются Nova_<Name>*
unconditionally; named tuples — by-value кроме mut @ receiver path,
который промоутится к NovaTuple_<Name>*. Это codifies «no pointer in C
signature кроме mut receiver» refinement над D215 original wording.
Wired через recv.mutable flag (MethodCallInfo::recv) — Plan 128
Ф.1 thread’нул flag через emit_c.rs helpers; Ф.2 consume’нул для
NamedTuple codegen branch. См. также §D228 Ф.4 «Method receiver
passing» — параллельный pointer pattern для value-records.
Use cases (recommended patterns)
| Паттерн | Тип | Почему |
|---|---|---|
| Hot-path math (Vec3, Matrix, Quaternion) | named tuple | zero GC, predictable |
| Pixel formats (Color, Pixel) | named tuple | small, copy-cheap |
| FFI multi-value returns | named tuple | stack return, fit в registers |
| Iterator state | named tuple | local-lifetime, no heap |
| Domain entities (User, Order, Account) | record | identity, sharing |
| Large aggregates | record | copy expensive |
Почему: Plan 59 Ф.7.4 rejection был неполным
Plan 59 rejection (2026-05-21) argued: «named tuples ≈ records, нет причин иметь два похожих синтаксиса». Reasoning flaw: tuple и record имеют fundamentally different allocation semantics:
- Tuple → stack, zero GC pressure, copy semantics
- Record → heap (D32, D123), GC-tracked, reference semantics
Разные allocation characteristics = разные performance + lifetime characteristics = different syntactic forms justified. Plan 120 (2026-05-31) reopens с corrected reasoning.
Out of scope (followups)
[M-120-positional-fallback]: allow.0/.1на named tuples (Rust-style fallback). V1 = Option B: forbid (Q120 decision).[M-120-named-positional-mix]: mixed positional+named в одном decl.[M-120-stack-arrays]: stack-allocated fixed-size arrays[3]Vec3.
Связь
- D32 — value vs reference taxonomy (amended)
- D52 — tuple syntax (amended + named form)
- D123 — positional tuple codegen (named form extends)
- Plan 120 — реализация
D354. Generic anonymous tuple monomorphization
Renumber 2026-07-03: блок был D216 — номер коллидировал с D216 Typed pointer family; anon-tuple-mono перенумерован в D354 (решение владельца; приёмник из резерва Plan 174 §6, прецедент D109/D110/D111). Status: active (spec, 2026-06-01). Реализация — Plan 59.1. Extends D123 с generic-aware substitution path. Closes gap в Plan 59 Ф.7.5 (Result mono landed; general generic anonymous tuple оставался под V1 erasure fallback до 2026-06-01).
Что
Generic anonymous tuple в return position функции с type-параметрами —
fn[T] f() -> (A[T], B[T]) или fn[T, U] g() -> (T, U) — мономорфизируется
per instantiation. Element types конкретизируются через
current_type_subst, получают C-name через type_ref_to_c, регистрируются
через register_mono_tuple, и emit’ятся как unique typedef’ы per element
combination.
fn[T] dup(v T) -> (T, T) => (v, v)
test {
ro (a, b) = dup[int](42) // → _NovaTuple_2_8_nova_int_8_nova_int
ro (s, t) = dup[str]("hi") // → _NovaTuple_2_8_nova_str_8_nova_str
// Два разных typedef'а в одной compilation unit, каждый с real types.
}
fn[T, U] pair(a T, b U) -> (T, U) => (a, b)
ro (i, s) = pair[int, str](7, "x") // → _NovaTuple_2_8_nova_int_8_nova_str
Правило
Mangling schema
Length-prefixed mangling: _NovaTuple_<arity>_<L1>_<T1>_<L2>_<T2>...
<arity>— количество элементов<Li>— длина sanitized C-name i-го элемента<Ti>— sanitized C-name (точки/звёздочки заменены на_, pointer suffix retained как_p)
Примеры:
(int, int)→_NovaTuple_2_8_nova_int_8_nova_int(str, bool)→_NovaTuple_2_8_nova_str_9_nova_bool(ChanWriter[T], ChanReader[T])после mono[T=int] →_NovaTuple_2_18_Nova_ChanWriter_p_18_Nova_ChanReader_p
Length prefix обязателен — без него parsing неоднозначен для nested tuples (tuple of tuples) и user types с underscores в имени.
Per-instantiation deduplication
mono_tuple_instances (HashSet) хранит set element-type vectors.
register_mono_tuple([elem1, elem2, ...]) идемпотентен — повторные
вызовы с same elements не emit’ят дубликаты typedef’а.
Finalize emit (typedef ordering)
В module finalize все registered tuples emit’ятся с topological sort’ом (внутренний tuple раньше outer’а):
- Tuple A depends on tuple B если B’s mangled name появляется как element type в A → emit B first.
- Cycle detection: impossible для value-tuple struct’ов; если обнаружен — emit anyway без depth-check (no hang).
Codegen в emit_call
- Call-site
f[T1, T2, ...](args)lookupsmono_fn_decls[f.name]. resolve_mono_type_argsстроит type_subst из turbofish + arg-inference.compute_mono_name(base, subst)→ unique mono fn name.register_mono_instanceenqueue в worklist.- Args emit без erasure boxing (concrete types).
- Variable type at call site = mono’d tuple via
type_ref_to_c(return_type)с активнымcurrent_type_subst.
Body emission (emit_monomorphized_fn)
current_type_subst устанавливается перед body emit; type_ref_to_c(TypeRef::Tuple)
возвращает mono’d name; tuple-литералы emit’ятся как value-struct
compound literals (no heap-box).
Destructure
emit_tuple_destructure использует actual mono’d return type для temp
variable (получает через infer_expr_c_type). Element types парсятся
через parse_mono_tuple_elements (length-prefixed inverse). Arity
mismatch → Nova-level diagnostic с pattern/scrutinee arity (Plan 59 Ф.7.1).
Value semantics, no heap-box
Mono’d tuple — value type (C struct), passed by value, returned by value. No heap allocation для anonymous tuple wrapper’а (Result mono Ф.7.5 parity). Element pointers (если elements — pointer types) остаются heap-allocated независимо.
Edge cases (covered V1)
- ✅ Multi-instantiation: same fn → разные T → unique typedef’ы per instantiation.
- ✅ Multi-param tuple:
fn[T, U] pair(a T, b U) -> (T, U). - ✅ Nested generic tuple:
fn[T] nest() -> (T, (T, T))— recursive subst черезregister_tuples_in_typeref. - ✅ Tuple-in-Option:
fn[T] f() -> Option[(T, T)]— Option mono + inner tuple mono. - ✅ Tuple-in-Result: уже работает (Plan 59 Ф.7.5).
- ✅ Non-generic tuple:
fn make() -> (int, str)— без T, substitution тривиален, мономорфизация single instance. - ✅ Arity 3+:
fn[T] triple() -> (T, T, T)— generic mangling параметризован по arity. - ✅ Positional field access:
pair.0/pair.1после mono.
Edge cases (V1 limitations — followups)
-
🟡
[M-59.1-array-of-mono-tuple]:fn[T] f() -> []((T, T))— array-of-mono-tuple. Body falls back наNovaArray_nova_int*(boxed pointer storage, как records/sums в bootstrap), call-site infer выдаётNovaArray_<mono_tuple>*(typedef которого не существует). Mismatch → CC-FAIL. Fix: align infer с body fallback ИЛИ packedNovaArray_<mono_tuple>typedef + element retrieval cast. Низкий приоритет — workaround через explicit Nova_record type. Update 2026-06-08 (Plan 91 Ф.1,
[M-91.1-composite-array-storage]): Для pointer-элементов (record/sumNova_<Name>*) этот класс РЕШЁН выбранным здесь подходом «align infer с body fallback» + завершением side-channelarray_element_types. Контракт хранения composite-массивов: элементы record/sum — boxed-pointer вnova_intслоте (NovaArray_nova_int*), а реальный elem C-тип проносится черезarray_element_types(var→Nova_<X>*) и проставляется на результат genericmap/filter(register_array_result_elem), так что[i],for-inи.get()кастят слот назад к указателю. Tuple-by-value ([]((T,T))— value-struct >8 байт) НЕ покрыт erasure-подходом и остаётся открытым как[M-91.1-value-struct-array-elem](тот же класс, что и[]Option[T]). Подробности — plan-91 Ф.1 closure. -
🟡
[M-59.1-tuple-field-oob-nova-diag]:pair.5на arity-2 tuple leaks к C-level error «no member named ‘f5’». Should be Nova-level diagnostic в type-checker. Cosmetic — error caught, но not optimal UX. -
🟡
[M-59.1-channel-new-cleanup]: Channel.new продолжает использовать 3 ad-hoc special-case branches в emit_c.rs:18435/20159/22694 + Nova_ChannelPair runtime struct. После Plan 59.1 generic mono path способен обработать Channel.new если добавить Nova-side declarationfn[T] Channel[T].new(cap int) -> (ChanWriter[T], ChanReader[T])через external fn (Plan 115 Pattern B). Cleanup deferred to отдельный план (runtime + std API surgery). Spec D91 signature остаётся буквальной реальностью после cleanup’а; до того — implementation detail, aspirational notation.
Backward compatibility
- Все existing non-generic anonymous tuple usages (
(int, str)returns, destructures) — продолжают работать unchanged. Plan 59 Ф.7.5 mono’d path был активен только для Result; теперь активен для всех anonymous tuples. - Plan 59 Ф.7 legacy
_NovaTuple<arity>schema (без underscore — nova_int placeholders) технически остаётся как fallback вtype_ref_to_cдля cases где type_subst не доступен (degenerate case — non-generic context с unresolved tuple). На практике не наблюдается после fix.
Cross-refs
- D52 — anonymous tuple type syntax.
- D123 — positional tuple codegen baseline (Plan 59 Ф.7.5).
- D215 — named tuple types (Plan 120 D215, ortho к D354).
- D91 — Channel.new signature now буквально implementable; cleanup ad-hoc paths — [M-59.1-channel-new-cleanup].
- D141 — bulk slice-операции (orthogonal к tuple mono).
- Plan 59.1 — implementation plan.
- Plan 59 Ф.7.5 — Result mono prior art.
D119. Method-level type parameters в generic methods
Status: active (spec, 2026-05-17). Реализация — Plan 48 Ф.9. Закрывает частично Q-generic-receiver-method (для user-defined generic типов; built-in
[]Tостаётся V2).
Что
Generic methods могут иметь собственные type-параметры, независимые
от type-параметров receiver’а. Метод Wrapper[T] @map[U](f fn(T) -> U) -> Wrapper[U]
имеет два уровня generics: receiver-level T и method-level U.
Compiler через monomorphization создаёт отдельную mono-instance
для каждой комбинации (T, U).
Правило
export type Wrapper[T] { inner T }
// Receiver-level T, method-level U.
export fn Wrapper[T] @map[U](f fn(T) -> U) -> Wrapper[U] {
Wrapper[U].of(f(@inner))
}
// Call-site:
ro w = Wrapper[int].of(5)
ro a = w.map(|x| x * 2) // (T=int, U=int) instance
ro s = w.map(|x| str.from(x)) // (T=int, U=str) instance
ro s2 = s.map(|x| x + "!") // (T=str, U=str) instance
Compiler emits 3 distinct mono’d methods:
Wrapper____nova_int_method_map____nova_intWrapper____nova_int_method_map____nova_strWrapper____nova_str_method_map____nova_str
Параллель: Rust impl<T> Wrapper<T> { fn map<U>(self, f: impl Fn(T) -> U) -> Wrapper<U> }
— то же monomorphization per (T, U). C++ template<T> class Wrapper { template<U> Wrapper<U> map(...) } — то же. Nova bootstrap теперь паритет.
Decision tree
При codegen call’а obj.method[U](args):
- Receiver T — резолвится из obj C-type (
Nova_Wrapper____<T>*→ T =<T>). Существующая infrastructure (D72 + Plan 48 Ф.0). - Method-level U — резолвится через bidirectional inference
из call args:
- Non-closure args:
infer_expr_c_type(arg)→ bind U черезinfer_type_param_binding. - Closure-typed args (
|x| body): pre-populate closure-param types с T-substituted C-types, recurse в body для return type → bind U.
- Non-closure args:
- Method C-name включает обa уровней:
<TypeBase>____<T>_method_<m>____<U>.
Constraints
-
Method-level generics declared в
@method[U]— synтаксис как у free-function generics (fn name[U](...)); receiver[T]parsed отдельно. -
Closure args drive inference — без explicit turbofish (
obj.map::<int>(...)), U inferенtsя из closure return type. Если нет args или U не появляется в parameter types, compiler emit’ит clean diagnostic:cannot infer method-level type argument `U` for generic method `<TypeBase>____<T>.<method>` (only in return type — provide arg whose type binds it); provide a closure/arg whose type fixes `U`(См. реализацию в
compiler-codegen/src/codegen/emit_c.rspath 5b.) Раньше unresolved method-level params silently dropped →Nova_U_pplaceholder leak в emitted C → undefined-struct CC-FAIL. -
Per-(T, U) instances — каждая уникальная пара получает свою mono’d function. Worklist enrollment предотвращает дубликаты.
-
Return type substitution —
Wrapper[U]в return type корректно resolves вNova_Wrapper____<U>*(неNova_U_pplaceholder).
Почему
- Параллель Rust/C++ — индустриальный standard для generic methods.
- Zero-cost — каждая mono-instance это direct call, инлайнится, no void* boxing/cast.
- Composability —
w.map(f).map(g).filter(p)typical functional chain работает без erasure penalty. - Был CC-FAIL — без method-param mono
let m = w.map(|x| str.from(x))эмиттилNova_Wrapper____Nova_U_p* m = ...(undefined struct, C-compile fail).
Что отвергнуто
- Method-level type-erasure (
void*U) — для bootstrap проще, но ломает первый-class closures + breaks struct-typed U (record-value не fit’ит вvoid*без heap-box). Equivalent проблема к Plan 48 receiver-level erasure отвергнутой в V1. - Explicit-only U (
obj.map::<U>(...)обязателен) — verbose, не matches industry standard. Inference из args — first-class.
Связь
- D72 — generic bounds на type params; method-level U могут иметь bounds.
- D122 — hybrid dispatch для protocol-bound type params; orthogonal к method-level vs receiver-level.
- D123 — tuple mono пользуется тем же worklist infrastructure.
- Plan 48 Ф.9 — реализация (emit_call path 5b + infer_mono_method_ret_with_args).
- Plan 63 Fix C — remaining edge case Plan 63, закрытый этим D119.
- Q-generic-receiver-method
D367. Удаление byte: каноническое имя — u8
Решение: Тип byte удалён из языка. Единственное каноническое имя
для 8-битного беззнакового целого — u8. Срез байт пишется []u8.
Мотивация. Наличие двух равнозначных имён (byte и u8) порождает
неоднозначность в коде, документации и стандартной библиотеке: один и тот же
тип можно было написать двумя способами, что усложняло чтение и тулинг.
Миграция. Все вхождения byte как типа заменяются на u8:
[]byte→[]u8- параметры/поля типа
byte→u8 - в примитивном перечислении:
byteубирается из списка
Исключения (не меняются):
- Тег шаблонных строк
bytes`...`(D48) — это имя функции, не тип. - Слово «byte» в английском/русском тексте комментариев (единицы памяти).
Реализовано: Plan 69 — 2026-05-22.
byte удалён из builtin-типов компилятора (lexer/parser/type-checker/
codegen); все вхождения в spec/ / std/ / nova_tests/ мигрированы
на u8. C-typedef nova_byte (= uint8_t) сохранён как внутреннее имя
codegen — не пользовательская поверхность.
D368. Strict type propagation в codegen — no silent nova_int fallback
Решение. Codegen pass (compiler-codegen/src/codegen/) обязан
производить deterministic, явный C-type для каждого Nova expression и
type reference. Silent fallback к nova_int при failure type
resolution — запрещён. Любой site где type_ref_to_c(...)
возвращает Err без strict-error должен производить compile-time
diagnostic [E7001] и failing build, а не подставлять placeholder
type.
Мотивация. До Plan 70 паттерн type_ref_to_c(&ty).unwrap_or_else(|_| "nova_int".into()) встречался в codegen в 117 местах (audit 2026-05-18).
Семантика: «если type translation failed → silently emit nova_int
(long long) и продолжай». Результат — silent miscompilation:
- pointer cast to int → garbage address как число
- bool/char печатается как code-point (Plan 67 закрыл частный случай)
- record/sum-type memcpy с неправильным sizeof
- float → int truncation
Программа «работает», но возвращает мусор. Debug невозможен — компилятор ничего не сигналит.
Industry baseline. Rust / Swift / Go (post-1.18) — все производят compile error на любом unresolved type в codegen. Nova до Plan 70 был хуже всех baseline (silent default). D368 закрывает регрессию.
Категории erasure (Cat A/B/C/D). Audit разделил 154 fallback sites на четыре категории:
| Cat | Pattern | Семантика | Действие |
|---|---|---|---|
| A1 | type_ref_to_c(...).unwrap_or_else(|_| "nova_int") | Silent fallback при resolution failure | Strict error |
| A2 | _ => "nova_int" wildcard без комментария | Wildcard fallback unknown type | Strict error или Cat D classification |
| B | _ => "nova_int", // erased T (commented) | Pre-mono generic body emit — type-param ещё unresolved | Documented intentional erasure |
| C | WithResultCategory::IntLike => "nova_int" | Categorical mapping для int-family aliases | Legit, keep |
| D | Dispatch wildcard на известный receiver | Known type, unknown method (type-checker уже rejected) | Legit, keep |
Только Cat A даёт silent miscompilation. После Plan 70 closure все Cat A sites мигрированы к strict error path. Cat B/C/D documented в docs/codegen-erasure-sites.md.
Strict-error architecture. Две helper-функции в emit_c.rs:
-
err_no_int_fallback(context, cause) → String— для functions возвращающихResult<_, String>. Используется с?propagation:let ty = self.type_ref_to_c(&p.ty).map_err(|e| self.err_no_int_fallback("parameter `x`", &e) )?; -
record_strict_error(context, cause) → "nova_int"— для cascade-blocked sites (functions whose signature нельзя менять без massive caller-chain refactor:infer_expr_c_type(135 callers),register_mono_instance, etc). Pushes E7001 вstrict_errors: RefCell<Vec<String>>field; finalization gate вemit_moduleпроверяет non-empty и failit codegen pass с aggregated error message.
Оба helper’а используют unified diagnostic format [E7001] (range
E7001-E7099 reserved для Plan 70 family). Plan 36 R7 structured
diagnostic compatibility.
Production-grade default. Strict mode — always on, без opt-in env var. ANY silent fallback = build failure (Rust/Swift baseline). Это breaking change для user code который полагался на silent int default (R20 в Plan 70). Bootstrap convention: clean break с machine-applicable migration suggestions.
Diagnostic format (E7001).
[E7001] cannot infer C type for parameter `x`: <cause>. Silent
fallback к `nova_int` produced wrong runtime output для non-int
types (record/string/float/bool). Add explicit type annotation,
ensure generic is monomorphized, или register type в external_registry.
См. Plan 70 ([M-no-silent-nova-int-fallback]).
Internal lint guard (CI). scripts/lint-no-silent-int-fallback.sh
greps compiler-codegen/src/ против baseline counts из
docs/codegen-erasure-sites.md. Bumping baseline требует:
- Inline comment с rationale «почему erasure безопасна»
- Entry в
docs/codegen-erasure-sites.mdсо file:line + причина - PR review
CI gate fails если added counts превышают baseline без updates.
Acceptance criteria (Plan 70 closure).
- Helper infra
err_no_int_fallback+record_strict_error(Ф.1 / Ф.B0) - Cat A1/A2 migration: 90 → 8 (only Cat B holdovers remain)
- Cat B documentation: 10 sites listed в codegen-erasure-sites.md
- Internal lint guard
scripts/lint-no-silent-int-fallback.sh - Spec D368 (этот блок)
- 796+ PASS / 0 FAIL nova test (0 regressions vs baseline 761)
Реализовано: Plan 70 — sessions 1+2 (2026-05-18); 90+ Cat A1 sites migrated, infrastructure complete, lint guard active.
Связь:
- D118 — typed
Fail[E]codegen (similar precision-by-construction pattern) - Plan 67 — println overload fix (sibling: один из видимых частных случаев)
- Plan 48 — monomorphization (упрощает Cat B → меньше erasure)
- Plan 36 — diagnostic infra (R7 structured format)
- docs/codegen-erasure-sites.md — Cat B/D inventory
D128. char distinct from int в codegen mono’d generics
AMEND (Plan 152.8, 2026-06-16). nova_char переведён с int64_t на
uint32_t. Codepoints fit in 21 bits (U+0000..U+10FFFF); uint32_t —
естественный unsigned type (как Rust char ABI). ABI cost минимален:
nova_char-поля в structs layout’ятся как 4-byte, Box-pointer смещается
с 8 на 8 (4-byte char + 4-byte padding → align 8). GC layout обновлён:
char_size = (4, 4) (было (8, 8)). Char-literal суффикс: U вместо
LL; is_typed_int_c_ty / emit_typed_int_literal включают nova_char.
Решение (исходное, Plan 70.3). Тип char имеет собственный C-typedef
nova_char (alias над int64_t → uint32_t D128 AMEND), distinct C
identifier). Generic mono mangling использует nova_char separately от
nova_int, поэтому Option[char] и Option[int] производят разные
C-типы NovaOpt_nova_char vs NovaOpt_nova_int — структурно
неотличимы становятся различимы.
Мотивация. До Plan 70.3 оба char и int map’ились в один C-тип
nova_int. Результат — silent type collapse в generic mono:
Option[char]иOption[int]mangle в идентичныйNovaOpt_nova_int[]charи[]intобе →NovaArray_nova_int*Map[char, V]иMap[int, V]→ одинаковая mangled name
Concrete observed bug (триггер плана): str @char_at(idx int) -> Option[int]
declared, returned Option[char] де-факто. Type-checker не ловил
поскольку C-level structural compatibility. ~50 callers использовали
char literals (Some('/'), unwrap_or('.')) в slot expecting
Option[int] — silent collapse через NovaOpt_nova_int. User pre-fix
2026-05-19 corrected signature, Plan 70.3 — архитектурное предотвращение.
Industry baseline. Rust/Swift char is distinct primitive (char
vs u32); Go has rune distinct from int32. Nova до Plan 70.3 был
unusual в C-level collapse. D128 закрывает регрессию.
Implementation (Plan 70.3 Ф.1-Ф.2).
- Typedef:
typedef int64_t nova_char;→typedef uint32_t nova_char;(D128 AMEND Plan 152.8) вcompiler-codegen/nova_rt/nova_rt.h. - Codegen mapping:
type_ref_to_c "char" => "nova_char"(was"nova_int") вemit_c.rsиexternal_registry.rs(двойная sync). - Array element:
[]char → NovaArray_nova_char*(separate instantiation parallelNovaArray_nova_int*). - Option element:
NovaOpt_nova_chartypedef + constructors +nova_opt_eq_nova_charhelper. - CharLit emission:
'x' → ((nova_char)<codepoint>U)(wasLL; D128 AMEND Plan 152.8 — uint32_t requires U-suffix). - infer_expr_c_type:
CharLit => "nova_char"(was"nova_int"). - Runtime fn signatures:
nova_str_char_atupdated returnNovaOpt_nova_char(wasNovaOpt_nova_int).
GC layout (D128 AMEND Plan 152.8). char_size = (4, 4) (was (8, 8)).
In a struct with a char field followed by a Box pointer: char occupies
4 bytes + 4-byte pad → Box at offset 8. gc_layout.rs::prim_emit("char") => Some((4, 4)).
Backward compat. В emit_binary_op special-case для
Nova_StringBuilder* + char accepts обе nova_char AND nova_int
для backward-compat — pre-fix existing code emitted char as nova_int,
existing test binaries reference legacy form. After full migration of
existing generated C (regen test fixtures), nova_int branch может
быть удалён.
ABI cost. Minimal. nova_char is typedef uint32_t — 4 bytes vs 8
bytes. Struct layout changes where char is followed by a pointer (padding
shrinks from 0 to 4 bytes). GC scanner updated. C type identifier remains
distinct from nova_int.
Acceptance criteria.
- Ф.1 codegen mapping switch (
emit_c.rs+external_registry.rs) - Ф.2 runtime helpers parallel (
NovaArray_DECL(nova_char),NovaOpt_nova_charconstructors + eq helper) - Ф.3 audit + fixtures (2 PASS в
nova_tests/plan70_3/) - Ф.4 type-checker tightening (reject
let x Option[int] = Some('a')) - Ф.5 spec D128 (этот блок)
- 0 regressions в
nova test(801 PASS sustained)
Реализовано: Plan 70.3 — Ф.0-Ф.5 closed 2026-05-19. D128 AMEND (Plan 152.8) — nova_char int64_t→uint32_t, 2026-06-16.
Связь:
- D26 — Q-string-indexing (char = codepoint convention)
- D54 —
as-cast narrowing (explicit char↔int conversion) - Plan 70 — parent family (silent type bugs от Nova↔C collapse)
- Plan 70.4 — sibling proposal (f32/f64 generic-container distinct mangling)
D129. int как alias i64 в bootstrap Nova
⚠️ AMEND (Plan 133) — ЧИТАЙ ПЕРВЫМ:
int=intptr_t, НЕint64_t
int=nova_int=intptr_t— ЗНАКОВОЕ ADDRESS-SIZED ЦЕЛОЕ (модель Go C-эрыintgo), а НЕint64_t.
int→nova_int(typedef intptr_t) — ширина = ширине указателя платформы (64 бита на x86_64/ARM64, 32 бита на 32-bit/WASM). См.nova_rt.h:typedef intptr_t nova_int; /* int — signed address-sized (Go C-era intgo, Plan 133) */.i64→int64_t— ВСЕГДА ровно 64 бита, независимо от платформы.- На bootstrap-таргете (x86_64, 64-битный указатель)
intиi64СОВПАДАЮТ по ширине и значению — отсюда историческое слово «alias» в заголовке/теле ниже — НО это РАЗНЫЕ C-типы:primitive_name_to_cдаётint → nova_intиi64 → int64_t, поэтому их mangle-имена РАЗЛИЧАЮТСЯ (NovaOpt_nova_int≠NovaOpt_int64_t;Map[int,V]≠Map[i64,V]по C-имени).- «int ≡ i64» — совпадение ШИРИНЫ на 64-бит, НЕ тождество типов. Не считать
intравнымi64/int64: аналогия — Goint≠int64, Rustisize≠i64(platform-pointer-width). На 32-bit/WASMintстанет 32-битным,i64останется 64-битным.- Следствие для §0/named-priority: числовая константа/выражение типа
i64НЕ должна схлопываться вnova_int(и наоборот) — это разные типы (см. Plan 172.1 P67 ФАЗА 2 STEP 1, де-коллапсi64.MAX→int64_t). То жеchar(codepoint,nova_char) ≠int.Текст «Решение / Мотивация / Codegen» НИЖЕ — ИСТОРИЧЕСКИЙ (Plan 70.4, до Plan 133): его утверждения «оба →
nova_int(typedef int64_t)» и «mangle идентичен» УСТАРЕЛИ — читать как «совпадают по ширине на 64-bit», а C-типnova_intтеперьintptr_t, неint64_t.
Решение. Тип int в Nova bootstrap является alias для i64
(64-bit signed integer) ПО ШИРИНЕ/ЗНАЧЕНИЮ на 64-bit таргете (см. ⚠️ AMEND выше —
C-типы РАЗНЫЕ: int→nova_int=intptr_t, i64→int64_t). Оба маппируются в C-тип
[УСТАРЕЛО Plan 133]. Отсутствие distinction в codegen
nova_int (typedef int64_t)намеренно относится к bootstrap-x86_64 (где ширины совпадают), но int и i64 —
различимые типы (разный C-typedef, разный mangle): это не collapse-баг, а address-sized
vs fixed-width архитектурное различие (Plan 133).
Мотивация. Audit Plan 70.4 выявил, что int и i64 используют
один C-тип. Mangle для Map[int, V] и Map[i64, V] идентичен. В
отличие от других collapse-паттернов Ф.1/Ф.2 плана 70.4 (ABI-real
silent miscompilation) или Plan 70.3 char/int (semantically distinct
types), int ≡ i64 является семантическим инвариантом — оба
означают 64-bit signed integer без разницы в значении или поведении.
Nova bootstrap targets x86_64 only (fixed 64-bit pointer width).
Industry baseline.
- Rust:
isizedistinct отi64(platform-pointer width varies на 32-bit) - Go:
intdistinct отint64(platform-pointer width) - C#:
int= aliasSystem.Int32(semantically identical) - Python/Java: нет fixed-width integer aliases
- Nova:
int= aliasi64— правильная аналогия C# для fixed-width platform
Future evolution path. Если Nova добавит multi-arch targets
(32-bit, WASM), int может стать platform-pointer-width type аналогично
Rust’s isize. На этот момент потребуется breaking change в codegen
mangling — Map[int, V] и Map[i64, V] станут distinct. D129
explicitly documents текущее bootstrap decision как alias-based,
чтобы будущий architect не принял отсутствие distinction за bug.
Migration path: introduce nova_iptr (platform-width) typedef, make
int resolve to it, maintain nova_int = int64_t for i64.
Codegen. Без изменений. type_ref_to_c "int" => "nova_int" и
"i64" => "nova_int" — оба корректны и эквивалентны по спецификации.
Distinct mangling не вводится, т.к. это создало бы необходимость явно
выбирать int vs i64 для каждого generic instantiation — user-hostile
и ортогонально семантической разнице (которой нет).
Acceptance criteria.
- Ф.3 spec D129 (этот блок) — формализует alias decision
- Нет codegen изменений — intentional collapse документирован
- Future: multi-arch migration path зафиксирован (Migration note выше)
Реализовано: Plan 70.4 — Ф.3 closed 2026-05-19.
Связь:
- D54 —
as-cast narrowing semantics - D128 — Plan 70.3 char/int distinction (contrast: там distinction нужна)
- Plan 70.4 — parent plan (этот блок = Plan 70.4 Ф.3)
- Plan 70 — parent family (silent type bugs)
D130. uint — unsigned 64-bit alias в bootstrap Nova
AMEND (Plan 133). C-тип uint — nova_uint (typedef uintptr_t, address-sized
unsigned), а не сырой uint64_t. На x86_64-bootstrap (фикс. 64-бит указатель) nova_uint
≡ uint64_t по ширине/знаку; u64 остаётся фиксированным uint64_t. Канонический словарь —
primitive_name_to_c (uint → nova_uint).
AMEND (Plan 172.1-K1, 2026-06-28). uint (и вся РАЗЛИЧНАЯ int-семья) лоуэрится в свой
точный C-тип во ВСЕХ позициях — включая method-receiver. Ранее receiver_c_type
схлопывал примитивный ресивер (uint/u8..u64/i8..i32) в nova_int (Plan 70.5
«64-бит слот») — нарушение §0/§10/D368 (второе окно правды) и soundness-баг:
Nova_uint_method_compare(nova_int, nova_int) давал знаковое сравнение беззнаковых (неверный
порядок для uint с установленным старшим битом). Теперь scalar-arm receiver_c_type делегирует
в единый primitive_name_to_c (тот же лист, что resolved_type_to_c): uint-ресивер = nova_uint
→ @-операции беззнаковые ПО ПОСТРОЕНИЮ. int≡i64→nova_int сохраняется (D129 — намеренный
alias, НЕ схлопывание различных типов). Acceptance: spec_tests/conformance/d130_uint_method_compare.nv.
receiver_c_type как отдельная функция ретайрится в receiver-aware resolved_type_to_c (U.4.5/FIN).
Решение. Тип uint является alias для u64 (64-bit unsigned
integer) в Nova bootstrap. Маппируется в C-тип uint64_t (AMEND Plan 133: → nova_uint). Отличие
от int/i64 (alias pair, signed) — uint/u64 является
симметричным unsigned pair. int as uint cast saturates (negative → 0);
int as u64 — direct bit-cast (существующее поведение сохранено).
Дизайн (Q1-Q4, подтверждены 2026-05-19).
| Вопрос | Решение | Обоснование |
|---|---|---|
| Q1: alias или distinct? | Alias u64 (= uint64_t) | Mirror int = i64 alias pattern; нет multi-arch story в bootstrap |
| Q2: int→uint cast | as uint saturates (neg → 0) | D54 precedent (float→int); Rust bit-cast hostile; Swift trap verbose |
| Q3: Indexing | Keep int (no change) | Breaking change для 100+ APIs; Swift/Go/Kotlin используют signed indexing |
| Q4: Literal default | int (keep current) | Backward compat; 42 as uint или let x uint = 42 для opt-in |
Saturation semantics (int as uint).
-1000 as uint → 0
-1 as uint → 0
0 as uint → 0
1 as uint → 1
Реализован через nova_int_to_uint(int64_t x) helper в nova_rt/cast.h.
u64 as uint — direct cast (no-op; uint64_t → uint64_t).
Codegen mapping.
type_ref_to_c "uint" => "uint64_t"(scalar)[]uint → NovaArray_uint64_t*(parallel сu64)Option[uint] → NovaOpt_uint64_t(parallel сu64)uint.MAX— не поддержан parser’ом (parser не распознаётuintкак type-path prefix; используйu64.MAX= эквивалент).
Будущая эволюция. Аналогично D129 (int/i64): если Nova добавит
multi-arch, uint может стать platform-pointer-width unsigned (как
Rust’s usize). Bootstrap-grade alias.
Acceptance criteria.
-
let x uint = 42 as uintкомпилируется -
int as uintsaturates (neg → 0) —nova_int_to_uinthelper -
int as u64остаётся bit-cast (no saturation) -
[]uint→NovaArray_uint64_t* -
Option[uint]→NovaOpt_uint64_t - 3 fixtures
nova_tests/plan70_5/PASS - 0 regressions
-
uint.MAX— defer (parser keyword support)
Реализовано: Plan 70.5 — Ф.1-Ф.3 closed 2026-05-19.
Связь:
- D54 —
as-cast saturation precedent - D129 — int/i64 alias (signed symmetric pair)
- Plan 07 — original float→int saturation
- Plan 70.5 — parent plan (этот блок)
- Plan 70.4 — sibling (codegen type distinction family)
D133. type X consume — обязательная consume-семантика (must-be-consumed)
Plan 100.1. Принято 2026-05-23 (proposed; implementation pending). Extends D131 affine
consumequalifier.
Что
Квалификатор consume на type-decl. Помечает, что инстансы такого
типа обязаны быть потреблены до выхода из scope’а на каждом code-
path’е. Compile error если live consume-переменная остаётся на exit-
point’е.
type Transaction consume { id int }
type File consume { fd i32 }
type Lock consume { mutex *Mutex }
Расширяет D131 с противоположной стороны:
| Свойство | D131 affine consume (Plan 73) | D133 type-level consume (Plan 100.1) |
|---|---|---|
| Потребить ≤1 раз | ✅ enforce | ✅ enforce (наследуется) |
| Потребить ≥1 раз (обязательно) | ❌ забыть OK | ✅ enforce — must-be-consumed |
| Помечается на | receiver / param метода | type-decl + поле + binding |
Канонический use-case — Transaction.commit() / .rollback(),
File.close(), lock-guard .release().
Синтаксис
consume стоит после имени типа, перед {:
type Transaction consume { // type-decl marker
id int,
}
fn Transaction consume @commit() -> () // consume-method (D131)
fn Transaction consume @rollback() -> ()
consume на type-decl + хотя бы один consume-метод (D131) — обязательное
сочетание (compile error: «consume-type требует ≥1 consume-method»).
Правило — must-consume на каждом exit-path’е
Compiler проводит flow-sensitive анализ (расширение Plan 73 D131
check_consume pass’а). Для каждой переменной consume-типа отслеживается
VarState:
Live— значение доступно, обязательство активно.Consumed— значение потреблено (через consume-метод / consume- параметр /return).MaybeConsumed— потреблено лишь на части путей (branch join).
На каждой точке выхода scope’а проход по active consume-переменным:
LiveилиMaybeConsumed→ compile error E (D133-not-consumed) с указанием консьюм-методов.Consumed→ OK.
Точки выхода:
- конец function body (последний statement);
return expr— все live consume-vars (кроме возвращаемой) → error;panic/expr!!/expr?/ unwinding-paths;loop break;- branch join
if/match—Live ⊔ Consumed = MaybeConsumed.
defer / errdefer могут покрывать обязательство (см. D158+ Plan
100.4 family).
Что считается consume
| Действие | Эффект на VarState |
|---|---|
tx.commit() — вызов consume-метода | tx → Consumed |
f(tx) где f(consume tx Tx) — consume-param | tx → Consumed |
f(make_tx()) где f(consume t Tx) — rvalue → consume-param | rvalue ownership передаётся напрямую (без binding) ✅ |
return tx (тип consume) | tx → Returned (передача caller’у) |
record.field = tx где field declared consume | tx → Moved (в record) |
consume new_owner = tx (transfer alias) | tx → Consumed, new_owner → Live |
f(tx) где f(tx Tx) — view-param (no qualifier) | tx остаётся Live (callee — view-borrow) |
f(make_tx()) где f(t Tx) — rvalue → view-param | ❌ E (D133-consume-rvalue-in-view) |
f(tx) где f(mut tx Tx) — mut-view-param | tx остаётся Live (callee — mut-borrow) |
f(make_tx()) где f(mut t Tx) — rvalue → mut-view-param | ❌ E (D133-consume-rvalue-in-mut-view) |
let alias = tx — view-alias | оба в alias-class (Plan 73); consume любого инвалидирует |
let mut alias = tx — mut-view-alias | то же + mut-методы через alias |
let _ = tx (silent drop) | ❌ compile error D133-suppress-not-allowed |
Заразность через поля + explicit double-marker
Record/sum, имеющий поле consume-типа, обязан быть объявлен
consume:
type TxState consume { // ← ОБЯЗАТЕЛЬНО
consume tx Transaction, // ← ОБЯЗАТЕЛЬНО (тип = consume)
writes []Write, // обычное поле
}
Compiler enforces consistency:
- consume-поле без
consume-маркера → error E (D133-field-marker-missing); - consume-маркер на field без
consumeна type-decl → error E (D133-type-marker-missing); consume f int(тип поля не consume) → error E (D133-marker-on- non-consume) — keyword использован но не нужен.
consume-type БЕЗ consume-полей разрешён — каноничный паттерн
для opaque-resource типов (StringBuilder consume с runtime backing
через external type; consume-method @into() потребляет; никаких
consume-полей в декларации). Достаточно хотя бы одного declared
consume-метода.
Field-aware flow внутри методов record’а
@field отслеживается как независимый VarState slot. На exit’е метода:
| Тип метода | consume-поля должны быть |
|---|---|
fn X consume @method(...) | Consumed (record closes) |
fn X mut @method(...) | Live (invariant preserved) |
fn X @method(...) (regular) | Live (invariant preserved) |
Это позволяет реальные паттерны (rotate / reopen / replace):
type Service consume {
consume file File,
}
fn Service mut @reopen() -> Result[(), OpenErr] {
consume new_file = File.open()? // сначала добываем замену
@file.close() // только теперь закрываем старое
@file = new_file // rebind — @file опять Live;
// new_file → Consumed (transfer в @file)
} // mut exit: @file Live ✅
Compiler ловит реальные баги:
- забытый rebind на ветке → exit MaybeConsumed → error.
- early return без rebind → error.
- наивный close-then-open с error-path (
@file.close(); @file = open()?) → error если open Err (@file Consumed, не rebinded).
Assign в Live consume-поле / locals — запрещено
Прямое присваивание @field = expr разрешено только когда @field
уже Consumed (для simple-typed consume-поля) либо все consume-sub-
fields внутри @field уже Consumed (для nested-consume-record-поля).
Иначе compile error E (D133-assign-live-field).
fn Service mut @overwrite_naive() {
@file = File.open()? // ❌ @file Live, silent overwrite
}
fn Service mut @overwrite_correct() {
@file.close() // @file → Consumed
consume new = File.open()?
@file = new // ✅ @file Consumed → assign OK
}
Nested case — @inner содержит consume tx; assign в @inner
разрешён когда внутренний @inner.tx уже Consumed (recursively для
deep nesting):
fn Outer mut @reset() {
@inner.tx.commit() // @inner.tx → Consumed;
// @inner effectively «empty container»
consume new = Inner.new()
@inner = new // ✅ all consume-sub-fields Consumed
// → @inner replace OK
}
То же для локальных consume-var: повторный consume tx = ... без
consume старой — error.
Nested field paths
Multi-level field tracking — ConsumeCtx хранит state по произвольно
глубокому пути @f1.f2.f3:
type Inner consume { consume tx Transaction }
type Outer consume { consume inner Inner }
fn Outer mut @commit_inner() {
@inner.tx.commit() // deep path consume; @inner.tx → Consumed
// @inner — «empty container» (consume-sub-field Consumed)
consume new = Inner.new()
@inner = new // rebind inner — assign OK
// (внутренний tx был Consumed)
}
Реализация — ConsumeCtx::states: HashMap<FieldPath, VarState> где
FieldPath = Vec<String>.
Заразность через generic-args
type_is_consume(TypeRef) — рекурсивная функция (общая, не Option-
специфичная):
- тип в
LinearityRegistry(объявленconsume)? - record/sum с ≥1 consume-полем?
- generic-wrap
G[T1, ..., Tn]— хотя бы одинTiconsume? - generic-param
T(без bound) — false (bootstrap silent-ignore; закрывается D156 Plan 100.2 через[T consume]bound).
Option[Transaction] / Result[Transaction, E] / Box[Transaction] /
user Wrapper[Transaction] — все автоматически consume через wrap.
Никакого Option-специфичного хардкода — общее правило для любого
generic-wrapper’а.
Три mode’а binding-position: view / mut-view / consume
Единое правило везде (param / for / match / if-let / let-binding):
consume keyword маркирует ownership. Без него — view (read-
only borrow). mut — view + mutation.
fn read(tx Transaction) -> int // view (default; callee читает)
fn modify(mut tx Transaction) // mut-view (+ mut методы)
fn close(consume tx Transaction) // consume (transfer; tx → Consumed)
View (default — без qualifier’а)
| Действие | OK? |
|---|---|
tx.field (read) | ✅ |
tx.regular_method() | ✅ |
t.mut_method() | ❌ (нужен mut tx) |
t.consume_method() | ❌ E (D133-consume-via-view) |
| передача в view-param другой fn | ✅ |
передача в consume-param | ❌ E (D133-move-via-view) |
передача в mut-param | ❌ (нужен mut tx) |
return tx (escape) | ❌ E (D133-view-escape-return) |
| store в record-field | ❌ E (D133-view-escape-store) |
| capture в closure, returned | ❌ E (D133-view-escape-closure) |
let alias = tx (alias) | ✅ view-alias (Plan 73) |
Mut-view (mut tx qualifier)
То же что view, но mut-методы разрешены. Не consume, не escape.
Consume (consume tx qualifier)
Полный ownership-transfer. Callee/binding обязан consumed до scope- exit’а через один из 5 механизмов (см. §«Когда consume binding считается удовлетворённым»).
Consume-rvalue в arg-position (без binding)
Прямой call f(make_tx()), где make_tx() -> Tx consume возвращает
fresh consume-owner, без сохранения через consume name = … —
правила по qualifier’у callee-param:
| Callee param | OK? |
|---|---|
f(consume t Tx) — consume-param | ✅ ownership передаётся напрямую; callee обязан consumed внутри |
f(t Tx) — view-param (default) | ❌ E (D133-consume-rvalue-in-view) |
f(mut t Tx) — mut-view-param | ❌ E (D133-consume-rvalue-in-mut-view) |
Почему запрет на view / mut-view: view/mut-view-param не
consume’нят callee-стороной. После возврата из f rvalue остаётся
не consumed и не bound к локальной переменной → flow-checker не имеет
slot’а в ConsumeCtx для tracking’а → must-consume gate его не
увидит → ресурс утечёт молча. Запрет — единственное безопасное
правило: consume-value требует именованного owner’а либо немедленной
передачи ownership через consume-param.
Hint в diagnostic: «привяжи через consume name = make_tx(),
затем f(name); после consume-method/consume-param/return name
будет Consumed». Альтернатива — заменить sig f на consume-param,
если callee действительно должен потребить.
Цепочки (g(f(make_tx()))) — рекурсивно: rvalue-результат f
анализируется по тому же правилу для соответствующего param’а g.
Если f возвращает consume-value, а g-param это view → error на
внешнем вызове.
Глубокий peek без consume
match @file { // view-match (default)
Some(f) => f.fd, // f: view File, read-only
None => 0,
}
// @file остаётся Live ✅
См. D157 (Plan 100.3) — match-pattern в view-mode + closure capture analysis.
consume + -> @ несовместимы
fn Tx consume @prepare() -> @ { ... } → parse error. Противоречие
между «забираю целиком» и «возвращаю тот же объект» (D132 fluent-
return).
Binding: consume keyword обязателен для ownership
Для consume-типов consume keyword обязателен в LHS, когда binding
становится Live-linear-owner:
ro tx = begin() // ❌ ERROR D133-consume-needs-keyword:
// consume-type требует `consume` keyword
consume tx = begin() // ✅ initial binding — owns
ro alias = tx // ✅ view-alias (no ownership; Plan 73)
mut alias = tx // ✅ mut-view-alias
consume new_owner = tx // ✅ transfer: tx → Consumed
Без consume keyword’а LHS = view-alias (alias-class Plan 73,
read-only borrow). Это симметрично param/for/match — везде «no qualifier
= view, consume = transfer».
Когда consume binding считается удовлетворённым
Live consume-binding обязан к scope-exit’у оказаться в одном из 5 состояний:
- Closed locally —
tx.commit()(consume-метод). - Returned —
return tx. - Transferred —
f(tx)гдеf(consume tx T). - Stored in record-field, который сам уходит наверх:
consume tx = begin() return Wrapper { tx: tx } // tx → record-field, record returns - Covered by defer/errdefer/okdefer (D158-D162 Plan 100.4 family).
Иначе error E (D133-not-consumed).
AI-first explicit-ness — почему mandatory
consume keyword обязателен специально — для loud visibility:
- 🟢 Каждое появление ownership видно с первого взгляда.
- 🟢 Refactor-safety — добавил
consumeк типу → compiler ловит все существующиеlet x = T.new()sites, force review. - 🟢 Единое правило симметрии с param / for / match.
Verbose-ness bounded — только для consume-типов (rare; resource- management).
Runtime mental model (Option-projection, не ABI)
Концептуально consume-тип проецируется в Option[T]-space:
Live≡Some(t).Consumed≡None.MaybeConsumed≡ branch-зависимо.
Это mental model для spec/docs. Реализация остаётся pragmatic (D131-style):
- pointer-based consume: NULL = None (zero overhead);
- value consume: zero-out fields после consume;
- compile-time
check_consume— основной механизм; runtime null-deref panic — defense-in-depth.
User-facing pattern-match match tx { Some(t) => ... } для runtime-
проверки не вводится — ослабит compile-time гарантии.
Что отвергнуто
- Universal affine/linear для всех
let— отвергнуто в D75 §«Compile-time token-scope enforcement»: «это Rust borrow checker ради одной фичи, несоразмерно для GC-языка». D133 — opt-in per-type, не default. - Suppress-механизм
let _ = v— anti-Rust#[must_use]gateway. Единственный канал — consume-метод. Если «иногда хочу забыть» — знак, что тип неправильно помеченconsume. - Drop-method auto-cleanup (Rust-style RAII) — размывает выбор commit/rollback. D133 требует явный consume-метод.
- Pattern-match destructure consume-record (
let { tx } = state) — ломает encapsulation (consume-поле уходит в независимый linear- binding). Вынос через явный consume-метод record’а:fn TxState consume @into_parts() -> (Transaction, []Write) => (@tx, @writes). - Strict-mode binding-form (
let tx =«обязан передать наверх» vsconsume tx =«обязан закрыть здесь») — отвергнуто (overspec, refactor friction). Финальная модель:consumekeyword mandatory для ownership;letдля consume-types = error либо view-alias (в alias-position). view Tkeyword как explicit qualifier — отвергнуто (default- view достаточно).viewmode = absence ofconsume/mutqualifier (см. D157 Plan 100.3).- Implicit
_ = txdiscard — суррогат suppress; force compile- error.
Сравнение с другими языками
| Свойство | Rust | TS (ES2024) | Kotlin | Go | Nova D133 |
|---|---|---|---|---|---|
| Compile-time enforcement | ⚠️ #[must_use] warning, suppressable | ❌ runtime via dispose | ❌ runtime via use{} | ❌ | ✅ error |
| Suppress escape hatch | ✅ mem::forget(v) / let _ = v | n/a | n/a | n/a | ❌ by design |
| Distinct cleanup methods (commit/rollback) | ⚠️ enum-в-Drop, awkward | ⚠️ single dispose | ⚠️ use{} block | ⚠️ convention | ✅ native (consume-методы) |
| Lifetime / borrow-checker cost | ❌ есть | n/a | n/a | n/a | ✅ нет (поверх GC) |
D133 строже Rust на suppress (нет mem::forget), expressive Rust на
distinct cleanup methods. Не требует lifetime’ов / move-семантики.
Связь
- D131 — affine
consumefoundation. D133 — extension on type-decl level. - D132 —
-> @fluent-return; sound builder-chain alias через-> @нужен для consume-checker’а builder API. - D75 — почему universal consume отвергнут.
- D90 —
defer/errdeferfoundation; интеграция через Plan 100.4 family (D158-D162). - D85 — kinded throws, cancel-routing; взаимодействие через D162 Plan 100.4.5.
- D156 Plan 100.2 — generic
[T consume]strict-mode bound. - D157 Plan 100.3 —
view Tread-only borrow для deep peek. - D158-D162 Plan 100.4.1-5 — defer/errdefer integration для cleanup- on-failure.
- D163 Plan 100.5 — FFI
external consume fn. - D164 Plan 100.6 — cross-module consume visibility + mangling.
- D165 Plan 100.7 — stdlib migration playbook.
- D166 Plan 100.8 — performance + IDE tooling.
D156. Generic [T consume] bound + collection-aware iteration
Plan 100.2. Принято 2026-05-23 (proposed; implementation pending). Extends D133 на generic-код. Closes silent-leak hole для consume-T в generic-функциях.
Что
Bound [T consume] на generic-параметр — opt-in strict mode: внутри
generic-body параметр T трактуется как possibly-consume; silent-forget
T-значения → compile error. Backward-compat: generic-функции без
bound сохраняют silent-ignore behavior (Plan 100.1 default), чтобы
existing stdlib generic-код продолжал работать.
// Strict mode — compiler enforces strict consume handling внутри:
fn box[T consume](consume x T) -> Box[T] => Box { val: x }
// Без bound — silent-ignore:
fn drop[T](x T) -> () // silent forget если T consume
Плюс — collection-aware iteration с 3 mode’ами (unified с D133):
for tx in vec (view default) / for mut tx in vec (mut-view) /
for consume tx in vec (consume, vec → Consumed).
Зачем
Без D156 generic-код имеет дыру:
type Transaction consume { id int }
fn Transaction consume @commit() -> ()
fn first[T](pair (T, T)) -> T => pair.0 // silent leak pair.1 если T=consume
consume tx1 = Transaction { id: 1 }
consume tx2 = Transaction { id: 2 }
consume chosen = first((tx1, tx2)) // tx2 уехала в first и потерялась
chosen.commit()
// tx2 LEAK — compiler молчит.
Это самый серьёзный hole D133 bootstrap’а — именно generic-helpers есть
в каждой stdlib. Rust решает через Move trait + ownership; D156 решает
через [T consume] bound + collection-aware iteration.
Синтаксис bound
fn box[T consume](consume x T) -> Box[T]
fn map[T consume, U consume](items []T, f fn(consume T) -> U) -> []U
fn id[T consume](consume x T) -> T => x
consume — bound в generic-position, мирится с другими bounds ([T Iter[U]]
из D72) — но bootstrap не поддерживает комбинации ([T consume + Clone] — parse error; будущее расширение).
Strict mode внутри [T consume] body
Внутри функции с [T consume] bound параметр T трактуется как
possibly-consume; compiler обращается строго:
| Действие с T-значением | Без bound | С [T consume] |
|---|---|---|
let _ = x (silent drop) | ✅ OK | ❌ error E (D156-strict-forget) |
| передача в non-consume fn | ⚠️ silently | ❌ error |
| destructure tuple, discard part | ⚠️ silently | ❌ error |
return x | ✅ | ✅ (передача наверх) |
передача в consume fn-param | ✅ | ✅ (consume) |
Force’ит honest API. Чтобы legitimately drop элемент — нужен явный
consume-параметр для drop:
fn first[T consume](consume a T, consume drop_b T) -> T => a
// ^^^^^^^^^^^^^^^^^^ — caller обязан передать
// drop_b как consume; внутри
// first drop_b силен забыть
// (это локальный binding).
Backward-compat и migration policy
- Default = silent-ignore для generic-functions без bound (Plan 100.1 behavior preserved). Иначе сломается весь stdlib generic-код.
- Opt-in
[T consume]для функций, которые хотят strict mode. - Migration: stdlib generic-functions (Plan 17/26/30/52/57
collection API) — постепенно аннотируются
[T consume]черезnova consume-migrateCLI (Plan 100.7).
Collection-aware iteration — 3 mode’а
Симметрично D133 param/match mode’ам:
consume tx1 = begin()
consume tx2 = begin()
consume txs = [tx1, tx2] // []Transaction — generic-заразность (D133 D6)
// txs владеет (consume keyword обязателен)
// View (default) — read-only, vec stays Live:
for tx in txs {
println(tx.id) // ✅ read field
// tx.commit() // ❌ view → не consume-метод
}
// txs Live после for; нужно consume другим способом.
// Mut-view — vec stays Live, элементы mutated in-place:
for mut tx in txs {
tx.update() // ✅ mut method
}
// txs Live, элементы updated.
// Consume — consume каждое, vec → Consumed:
for consume tx in txs {
tx.commit() // ✅ consume-метод
}
// txs → Consumed после for ✅
Loop-handling pragmatic: for consume tx in vec помечает vec Consumed
после loop (даже если break early — D161 multi-defer LIFO error
accumulation gracefully handles partial-consumed state).
Каждый tx в arm-теле проверяется стандартным check_consume
правилом для соответствующего mode’а (view / mut-view / consume).
Alternative consume-methods для collection
Чтобы consume collection без iteration:
vec.pop() -> Option[T]— single-element consume (Option auto- consume через D133 D6 generic-заразность).vec.drain() -> Iter[T]— consume через iterator.vec.into_first() -> Tconsume-method record’а возвращает один элемент (consume rest internally).
stdlib audit (Plan 100.7) аннотирует эти методы с [T consume] bound.
Generic propagation для HOF (map/filter/fold)
Closure-параметры HOF используют те же 3 mode’а через qualifier:
fn map[T consume, U consume](consume items []T, f fn(consume T) -> U) -> []U
fn filter[T consume](consume items []T, f fn(t T) -> bool) -> []T
// ^^^ — view (default; read-only)
fn for_each[T consume](consume items []T, f fn(consume T) -> ())
fn modify[T consume](mut items []T, f fn(mut T) -> ())
// ^^^^ — mut-view (in-place modify)
filter использует view-closure (default) — predicate читает T без
consume. map consume’ит каждое T → producer’ит U. modify mut-view
для in-place.
Compiler enforces consume-handling в closure-body через generic-bound propagation + view-default rules.
HashMap / user-generic propagation
type_is_consume рекурсивно (D133 D6): wrapper’ы с consume-arg сами
становятся consume:
consume tx_map = HashMap[str, Transaction].new()
// ↑ Transaction consume → HashMap consume
// через generic-заразность
// consume keyword обязателен (D133)
tx_map.insert("a", consume begin()) // insert требует consume value (transfer)
// На scope-exit tx_map должен быть Consumed (через consume-метод HashMap).
for consume (_, tx) in tx_map.drain() { // consume через drain-iteration
tx.commit()
}
HashMap (и другие collection API) — должны аннотировать [V consume]
на методах, манипулирующих consume-values (insert(k K, consume v V),
remove() -> Option[V], drain() -> Iter[(K, V)], etc.). Migration
audit — часть Plan 100.7.
Runtime cost
Zero. Все проверки compile-time. Runtime-представление generic’ов
не меняется. Bound [T consume] — type-level only, не влияет на
codegen mono’d functions.
Сравнение
| Capability | Go | Rust | TS | Kotlin | Nova D156 |
|---|---|---|---|---|---|
| Generic linear bound | n/a | ✅ T: Move (default) | n/a | n/a | ✅ [T consume] opt-in |
| Detection «generic drops linear arg» | n/a | ✅ compile-error | n/a | n/a | ✅ |
| Backward-compat: generic без bound | n/a | n/a | n/a | n/a | ✅ silent-ignore остаётся |
Vec<T> ownership iteration | n/a | ✅ | n/a | n/a | ✅ for tx in vec |
Nova превосходит Rust на одной оси — backward-compat: generic без bound сохраняет existing behavior; opt-in strict — choice.
Что отвергнуто
[T consume + Clone]combined bound — bootstrap parse-error; будущее расширение (комбинация с другими D72 bounds).[T !consume]anti-bound — не вводится; нет use-case в bootstrap.- Variance linear-typed wrappers — отдельный план (общая variance system).
Связь
- D133 — foundation type-level consume; D156 — generic-уровень.
- D72 — generic bounds
[T Protocol]; D156 идиоматически близок. - D157 —
view T(Plan 100.3);filter-style HOF использует view для read-only inspection. - D158-D162 (Plan 100.4 family) — defer/errdefer integration; orthogonal.
D163. FFI consume integration — type-driven, без отдельного keyword’а
🔴 RETRACTED 2026-05-30 (Plan 91.10).
needs <Cap>syntax удалён. Capability tracking via отдельный mechanism — redundant с effect system (Plan 33). Structurallyneeds Cap≡ effect-без-операций: same propagation, same static tracking, different syntax. Если в будущем понадобится capability gating — вводить как formal effect declarations (type Fs effect { ... }) с handler’ами. Конкретный pain:consume(ownership/linearity) vs capability (authority) — orthogonal concerns, D163 их жёстко связал. См. [docs/plans/91.10-d163-retract-capability-syntax.md].Что осталось от D163:
external type X consumeв любом module — продолжает работать (D126 + опаковая FFI-семантика).consumekeyword на параметрах external fn — продолжает работать (D131 ownership). Но external fn остаётся stdlib-only (D82) — user-module external fn через D163 capability path больше не валидны.Plan 100.5 historical original record: Принято 2026-05-23. Ред. 2 (2026-05-24): drop
external consume fnkeyword. Ред. 3 (2026-05-27): РЕАЛИЗОВАНО — parserneedsclause, type-checker D163-missing-cap, C codegen стабы. Extends D82external fn+ D126external type+ D63 capability.Удалено (Plan 91.10):
- Parser
needsclause (hard error w/ migration hint).check_external_fn_needs_caps(D163-missing-cap diagnostic).emit_d163_external_stub(C codegen стаб generator).FnDecl.needs_capsAST field — удалён (Plan 91.15 Ф.5,[M-91.10-remove-needs-caps-field]✅).- Test fixtures
nova_tests/plan100_5/external_*(6 files) иnova_tests/plan100_7/{file_open_read_close,mutex_lock_release, socket_listen_accept}.nv(3 files).Текст ниже — historical reference для контекста.
Что
Никакого нового keyword’а для external fn — унифицировано с regular fn:
return-type carrying consume-ness (через D133 type-decl consume)
автоматически передаёт ownership caller’у. consume keyword
используется только на параметрах/receiver’ах (D131 semantic).
// Opaque consume-type (D126 + D133):
external type File consume
external type Mutex consume
external type Socket consume
// Return consume-type → caller получает ownership (через type, не keyword):
external fn nova_file_open(path str) -> File
needs Fs // capability required (D63)
// Param-side consume — D131 semantic, тот же keyword `consume` на param:
external fn nova_file_close(consume f File)
needs Fs
// Result wraps consume — generic-заразность из D133 D6:
external fn nova_open(path str) -> Result[File, IoErr]
needs Fs
// Caller обязан consume Result через match-Ok-arm.
Зачем drop keyword
Параллель с regular fn:
fn factory() -> Transaction => Transaction.new()
// ^^^^^^^^^^^ — return type carries consume-ness. NO `consume`
// keyword on fn declaration.
fn finish(consume tx Transaction) -> () { ... }
// ^^^^^^^ — consume on PARAM (D131).
Применяем то же к external — symmetry без нового keyword’а.
Capability requirement (D63)
external fn касающийся OS resource обязан declare capability —
это независимо от consume-семантики (общее правило D63):
external fn nova_file_open(path str) -> File
needs Fs // OS access → cap required
external fn nova_socket_accept(consume srv ServerSocket) -> ClientSocket
needs Net
Capability и consume — две ortogонные concern. Capability для OS privilege; consume для ownership. Combined через type-decl + needs-clause.
C runtime defensive helpers
C-side nova_file_close(consume f File) обязан:
nv_consume_validate(f)— assertf != NULLна entry.- После работы —
memsetполяFile*в zero / NULL (defense-in-depth per D131 Plan 73 pattern).
Это даёт двойную защиту: compile-time (D133 check_consume) + runtime (NULL-deref panic на use-after-consume).
Generic-заразность через FFI — uniform
external fn nova_open() -> Result[File, IoErr] needs Fs
// ^^^^^^^^^^^^^^^^^^^ — Result consume через generic-arg
// Caller обязан consume Result (через match Ok-arm с consume File).
Никакого FFI-специфичного правила — общее D133 D6 generic-заразность.
Cross-fiber FFI safety
FFI-call может суспендиться (libuv async I/O). Plan 47/22/49 fiber infra preserves consume-state через migration; D163 verify через runtime tests (Plan 100.5 Ф.6).
Сравнение
| Capability | Rust | Kotlin/JNI | Go cgo | TS Node N-API | Nova D163 |
|---|---|---|---|---|---|
| Ownership через FFI | ✅ unsafe fn + manual contract | ⚠️ manual | ⚠️ manual | ⚠️ manual | ✅ type-driven, без extra keyword |
| Auto-close на panic при FFI handle | ✅ через Drop wrapper | ⚠️ try-finally | ⚠️ defer | ⚠️ try-finally | ✅ через D162 |
| Capability tracking | ⚠️ unsafe fn | ⚠️ manual | ⚠️ manual | n/a | ✅ D63 needs-clause |
unsafe keyword нужен | ✅ да | n/a | n/a | n/a | ❌ нет (D6) |
| Уникальный FFI-syntax | ⚠️ unsafe fn | ⚠️ JNI prefix | ⚠️ cgo annotation | ⚠️ napi macro | ✅ унифицировано с regular fn |
Nova превосходит Rust — (a) нет unsafe keyword (D6 + D63
capability); (b) унифицировано с regular fn (одна mental model для
FFI и Nova-side functions).
Что отвергнуто
external consume fnkeyword (Ред. 1) — избыточный, return-type уже carries consume-ness. Drop в Ред. 2.- Vacuous-marker warning (Ред. 1 W D163-vacuous-consume) — отпадает вместе с keyword.
Связь
- D82 —
external fnfoundation; D163 расширяет. - D126 —
external typeopaque; combine’ится сconsume. - D63, D64 — capability enforcement.
- D131, D133 — consume foundation.
- Plan 18 — основной consumer (File/Mutex/Socket migration).
D164. Cross-module consume — visibility + mangling + package contracts
Plan 100.6. Принято 2026-05-23 (proposed). Extends D26 visibility + D134 mangling v0 + Plan 03 package ecosystem.
Что
consume-маркер (D133) — part of exported type signature. Visibility
(D26, D47 Plan 35 R26) propagates marker. Symbol mangling (extends
D134 Plan 81) включает consume-bit — ловит cross-version ABI break.
Plan 03 nova audit verifies cross-package consume-contracts.
Cross-package visibility
// package A, module a/types.nv
export type Transaction consume {
id int,
}
// package B, module b/main.nv
import a.types.Transaction
fn main() {
consume tx = Transaction { id: 1 } // ✅ consume-marker visible
tx.commit()
}
consume propagates через export + import. Plan 35 R26 (visibility
enforcement) — без special-case’ов; consume — обычный type-attribute.
Mangling extension (D134 amend)
Plan 81 D134 определил symbol-mangling v0:
nova_fn_<pkg>_<mod>_<name>_<param-types>_<return-type>
D164 amend:
nova_fn_<pkg>_<mod>_<name>_<consume-bit>_<param-types>_<return-type>
^^^^^^^^^^^^^^^
`c` если consume-маркер на type-decl, `_` иначе
Это ловит ABI mismatch — package A v1.0 имеет Transaction consume,
v2.0 убрал marker; linker ловит cross-version mismatch на load.
Re-export через export import (Plan 42.09)
// package B re-exports A.Transaction
export import a.types.{Transaction}
Re-export preserves consume-marker. Plan 42.09 уже работает; D164 verifies.
Folder-modules (Plan 42) + relative imports (Plan 84)
consume-types работают идентично в folder-modules + relative imports: не вводятся special-case rules. Plan 42 / Plan 84 уже работают; D164 verifies.
Package version contracts (Plan 03)
nova.toml consume-contracts:
[package]
name = "my_lib"
version = "1.0.0"
[exports.consume_types]
Transaction = "1.0" // consume contract v1
File = "1.0"
Cross-version compat:
- v1.0 → v1.x — consume-status unchanged.
- v1.x → v2.0 — consume-status может change (major-bump required).
nova audit (Plan 03.4) verifies — ловит «v1 → v1.1 breaking change»
unauthorized.
Cross-module diagnostic
error: consume value `tx` (type a::Transaction) not consumed
note: type defined in package 'a' v1.0 at a/types.nv:5
note: consume via .commit() or .rollback() (declared in 'a')
Includes package origin, version, consume-method hint.
Private consume не leak
type InternalCache consume { ... } // no `export`
// usable только в этом package; cross-package — invisible
Plan 35 R26 — без special-case’ов.
Сравнение
| Capability | Rust | Kotlin/Java | Go | TS | Nova D164 |
|---|---|---|---|---|---|
| Pub visibility consume-маркера | ✅ pub Drop visible | ⚠️ AutoCloseable interface | ⚠️ exported method | ⚠️ TS types | ✅ D164 propagation |
| ABI mangling включает ownership-info | ✅ через type | ⚠️ via signature | ❌ | n/a | ✅ consume-bit |
| Cross-package consume contracts | ✅ Cargo + Rust types | ⚠️ Maven coordinates | ⚠️ go modules | ⚠️ npm types | ✅ nova.toml |
| Re-export preserves marker | ✅ через pub use | n/a | n/a | n/a | ✅ Plan 42.09 |
Nova matches Rust на всех осях; превосходит на consume-bit-in- mangling (ловит silent ABI mismatch которого Rust не видит через type-id alone).
Связь
- D26, D47, Plan 35 R26 — visibility foundation.
- D134 — mangling v0 (Plan 81); D164 extends.
- D29 — modules + folder-modules.
- D126 — opaque types; cross-package consume может быть opaque.
- D131, D133 — consume foundation.
- Plan 03 / Plan 03.4 — package ecosystem,
nova audit. - Plan 42, Plan 42.09, Plan 84 — folder-modules, re-export, relative imports.
D135. Type-checker completeness — «no silent fallback» на уровне типов
Статус: принято, реализовано (Plan 79).
Контекст. D368 закрыл silent-fallback в кодогене («no
silent nova_int»). Но bootstrap type-checker (types/mod.rs) проверял
имена, структуру, эффекты, контракты — и не базовую совместимость
типов. Эмпирическая перепроверка 2026-05-21 показала: ряд элементарных
ошибок типов компилировался молча (silent miscompilation) либо
ловился только C-компилятором (CC-FAIL, поздняя нечитаемая диагностика):
| Случай | До Plan 79 | Severity |
|---|---|---|
let x int = true | компилируется И выполняется неверно | 🔴 silent |
want_bool(42) (int в bool-параметр) | то же | 🔴 silent |
fn g() -> Result[int] (1 type-arg вместо 2) | компилируется тихо | 🔴 silent |
let c = Foo (имя типа как значение) | CC-FAIL | 🟡 поздняя |
f.nonexistent (нет поля) | CC-FAIL | 🟡 поздняя |
Go / Rust / TS ловят все пять на compile-time. По базовой проверке типов Nova была позади всех трёх.
Решение. Type-checker обязан ловить базовые ошибки типов на этапе
компиляции собственной диагностикой (серия E73xx), а не молча и не
перекладывая на C-компилятор. Отдельный проход TypeCheckCtx (паттерн
NameResCtx / MapLitCtx):
- E7310 — арность type-аргументов. Использование generic-типа с
явно указанным, но неверным числом аргументов (
Result[int],Result[A,B,C],Foo[int]для не-genericFoo). Опущенные аргументы (fn f() -> Result { Ok(1) }) — легальны (выводятся из контекста), это не arity-ошибка. - E7301 — assignability.
let-аннотация ↔ RHS и аргумент ↔ параметр. Целочисленный литерал полиморфен (D44):let x u8 = 200валиден;let x int = true,want_bool(42)— нет. Сравнение по категориям типов; structural-конформность протоколов — забота D72, не этой проверки. - E7320 — существование поля / метода.
obj.name, гдеobj— concrete record:nameобязан быть полем либо методом (into/try_intoсинтезируются из D73/D77). - E7330 — type-vs-value. Имя непустого record/sum-типа в
value-позиции (
let c = Foo,Foo + 1) — ошибка: тип не значение.
Принцип «no any-hole» (строже TS). Ни один путь проверки не
присваивает выражению результат «молча неверно». Там, где тип
выражения не выводится (bootstrap type-checker по дизайну не
типизирует каждое выражение — вывод завершается в кодогене), проверка
пропускается локально — это не silent miscompilation: программа не
становится неверной, недостающая проверка либо ловится дальше по
пайплайну, либо случай корректен. any — только из явной аннотации
([]any), он не «заражает» и не отключает проверку соседних выражений.
Полная типизация каждого выражения на уровне type-checker’а — задача
пост-bootstrap full inference engine, вне scope Plan 79.
Сравнение. Go/Rust/TS ловят все пять случаев на compile-time;
Plan 79 выводит Nova на их уровень для перечисленных проверок. Строже
TS: у TS any молча гасит ошибки — в Nova такого пути нет.
Связь:
- D368 — sibling: «no silent fallback» для кодогена (Plan 70).
- D44 — полиморфизм числовых литералов.
- D72 — structural bounds (конформность протоколов — там).
- D73 / D77 —
into/try_intoсинтез. - Plan 79 — родительский план (этот блок).
- Plan 37 — newtype/alias
as-cast строгость (смежная, отдельная).
D142. protocol/effect declaration ↔ literal symmetry
Plan 97. Принято 2026-05-23. Объединяет
Q-keyword-symmetry(open-questions.md) сQ-static-method-protocol(D58).
Что
Декларация и литерал и для протоколов, и для эффектов — симметричны по ключевым словам:
// Declaration:
type Cron effect { run() -> () }
type Fan protocol { run() -> () }
// Literal (значение, реализующее контракт):
ro h = effect Cron { run() => spawn_cron() } // value of type Effect[Cron]
ro p = protocol Fan { run() => spin_blades() } // value реализующее Fan
Раньше литерал эффекта писался ключевым словом handler, а
литерала протокола не было. Теперь:
- литерал эффекта —
effect X { ... }(тот же keyword, что в declaration); - литерал протокола —
protocol X { ... }(тот же keyword, что в declaration); - встроенный тип
Handler[E, IRT]→Effect[E, IRT](Effect[E]≡Effect[E, Never]через D88 default).
Clean break — старое ключевое слово handler (литерал) удалено
без deprecated-алиаса; парсер при встрече выдаёт diagnostic
«handler keyword removed; use effect (D142)».
Правило
Декларация (без изменений)
type Db effect { query(q str) -> [str] }
type Hash protocol { hash() -> u64 }
Литерал — symmetry
// effect-литерал (value)
ro h = effect Db {
query(q) => mock_rows()
}
with Db = h { ... }
// protocol-литерал (value реализующий контракт) — instance-only
ro l = protocol Locker { lock() => state.lock() }
Анонимный protocol в type-position (D53 §628)
fn close_all(items []protocol { close() -> () }) {
for it in items { it.close() }
}
fn min[T protocol { @lt(other Self) -> bool }](xs []T) -> Option[T] => ...
Body анонимного protocol — тот же синтаксис, что у named: bare-имена =
instance; leading-точка .method = static (D143).
protocol-литерал: instance-only
Static-методы — это методы типа (Type.method, D35);
у литерала нет «своего типа» (анонимная impl). Попытка реализовать
static в protocol-литерале → diagnostic «static methods cannot be
implemented in protocol-literal; they belong to a type (D35) — use a
named type».
Capture-rules
Закрытие над окружающим scope’ом — как обычное closure (D22 / D6 managed heap). Никаких особых правил поверх closure не вводится.
Почему
- Симметрия снижает когнитивный налог. Один keyword из declaration
работает и в literal — нет «двух жаргонов» (
handlervsprotocolvseffect). - Анонимный protocol-литерал разблокирует pattern «capability-split
factory» —
Lock.new() -> (Locker, Unlocker)без двух named-обёрток. Кандидаты в stdlib Plan 18:Process.spawn,HttpServer.bind,Db.transaction. - Symmetry побеждает локальную точность.
let h = effect X { ... }читается чуть точнее как «handler», ноprotocol X { ... }-литерал всё равно нужен — приходится либо ввести ещё keyword, либо унифицировать. Унификация чище. - Clean break без deprecated — текущая база
.nvмаленькая (~30 файлов); миграция атомарным sweep’ом дешевле двух-keyword’ового периода + последующей чистки.
Что отвергнуто
Protocol[P]first-class тип — отвергнут как избыточный. Для эффектовEffect[E, IRT]нужен, потому что значение эффекта передаётся вwith X = h(нужна типизация значения). У протоколов «значение, реализующее контракт» — это тип реализации; обёртка не нужна. Тривиальныйaliasрешит, если когда-нибудь понадобится (Q-protocol-type-wrapping).deprecated handleralias — отвергнут (clean break, ~30 файлов миграции).- Static в protocol-литерале — отвергнут (нет «своего типа»; см. D35).
- Изменение семантики handler’ов — нет, только rename keyword’ов.
Связь
- D53 — protocol declaration; D53 §628 (анон-protocol в type-position) ✅ реализовано (Plan 97 Ф.2).
- Protocol-литерал codegen — value
protocol Name { ops }с runtime vtable + dispatch — ✅ реализовано в подплане Plan 97.1 (emit_protocol_lit+ расширенный Plan 56 D122 box-pattern). Capability-split factory pattern работает end-to-end. - D61 — handler-литерал; rename keyword
handler→effect(Plan 97 Ф.3). - D87 —
Effect[E, IRT]; rename вEffect[E, IRT](Plan 97 Ф.3). - D88 — default generics (
Effect[E]≡Effect[E, Never]). - D143 —
.method-префикс для static в protocol-body (закрывает Q-static-method-protocol). - D35 — static vs instance методы.
- D22 — closure capture-rules.
- Q-keyword-symmetry — закрывается этим D-блоком.
- Plan 97 — имплементация parser + AST + type-checker.
- Plan 97.1 — runtime codegen (vtable + dispatch) + followup-hardening (Nova-side enforcement, capture-mode by-value snapshot для factory, shadowing fix, scan_fwd recurse, GC stress, multi-method, nested).
- Ориентиры: Java/Kotlin (anonymous interface), TS (object-literal structurally), Koka/Eff (handler-literal).
Canonical example — capability-split factory pattern
Use-case D142, разблокированный Plan 97.1 codegen’ом:
type Reader protocol { read() -> int }
type Writer protocol { write(v int) -> () }
type Cell { mut value int }
fn Cell.new(initial int) -> (Reader, Writer) {
ro state = Cell { value: initial }
ro r = protocol Reader { read() => state.value }
ro w = protocol Writer { write(v) { state.value = v } }
(r, w)
}
// caller:
ro (r, w) = Cell.new(10)
ro initial = r.read() // 10
w.write(99)
ro after = r.read() // 99 — shared state через protocol-литералы
Реализация (Plan 97.1 emit_protocol_lit, Approach A):
- Литерал
protocol Reader { read() => state.value }создаёт synthetic structNova_ProtoLit_<N>с capture-fieldstate. - Free fn
Nova_ProtoLit_<N>_method_read(self, ...)используетself->state->value. - Allocate
NovaVtable_Reader*+ ctx; patch vt->read = impl_fn. - Возврат
NovaBox_Reader { .data = ctx, .vtable = vt }(fat-pointer pattern Plan 56 D122).
Method dispatch r.read() → r.vtable->read(r.data) — стандартный
vtable indirect call.
Capture-rules:
- Heap obj /
let mut→ by-pointer (alias, mutation visible). - Immutable scalar / fn-param → by-value snapshot (factory-safe, survives fn exit).
D144. Sub-slice views для []T и str — arr[a..b] / s[a..b]
Amended (Plan 138 D238+D239, 2026-06-10):
arr[i]для user-типов (Vec[T],HashMap[K,V]и т.д.) теперь через@indexprotocol (D238).[]T=Vec[T](D239); typed-storage gap закрыт —[]Option[int],[]Recordи другие exotic-element типы получают правильное typed хранение. Range-slicingv[2..5]дляVec[T]— через@index(Range)overload. Предложение «future language version» из D232 Migration path снято: D239 фиксирует[]T≡Vec[T]как текущую спецификацию.
Источник: Plan 96 (2026-05-23). Закрывает Q-array-slicing, Q-array-api.5, D27 §1663 drift («Слайсинг отложен»), D27 §1632 drift (raw
arr[i]без bounds-check). Зависит от D6 non-moving GC; D58 Range; D27[]TAPI; Plan 90 / D141 bulk-ops.
Семантика — sub-slice view
arr[range] где range : Range возвращает view — новый
24-байтовый header NovaArray_T* с data = orig->data + from,
len = cap = to - from. Без копии данных backing’а (O(1) creation).
str[range] возвращает codepoint-indexed view (двухпроходный walk
UTF-8 → byte offsets; structurally идентично nova_str_slice, но с
panic при OOB вместо clamp).
5 форм Range (Rust RangeBounds parity)
| Форма | Семантика | Open-ended? |
|---|---|---|
arr[a..b] | exclusive: [a, b) | нет |
arr[a..=b] | inclusive: [a, b] | нет |
arr[a..] | от a до конца | да (end = len) |
arr[..b] | от начала до b | да (start = 0) |
arr[..] | весь массив | да |
Open-ended формы — только в slice-context (arr[range]). В
materialize / for-loop / quantifier / parallel-for они отвергаются
с compile-time diagnostic «open-ended Range without bound (Plan 96)».
Single-type design
[]T — один тип для owner и view. Нет Slice[T] (Rust-модель
раздельных типов). View передаётся в функцию ждущую []T без
дополнительной конверсии.
cap == len invariant
View имеет cap == len == to - from. Push на view → realloc (как
обычно при exhausted cap) → view silent detach от parent.
Parent backing никогда не молча перезаписывается — это устраняет
Go-append-footgun без borrow checker’а.
mut parent = [1, 2, 3, 4, 5]
mut view = parent[1..4] \ view: [2, 3, 4]
view.push(99) \ realloc; view detached
\ parent == [1, 2, 3, 4, 5] — НЕ затронут
\ view == [2, 3, 4, 99]
Mut-семантика
mut-view только от mut-источника. Через mut-view write идёт в
shared backing — изменения видны parent. Несколько mut-view
одного backing’а разрешены (как в Go); caller responsibility,
никакого borrow checker’а.
Iterator invalidation
for x in view — len берётся snapshot’ом в начале цикла (Go-style).
Push на parent во время итерации view’а не виден view’у: parent
реаллоцирует, view продолжает указывать на старый backing через
interior-pointer.
GC requirement — interior pointers stable
Необходимое условие: runtime гарантирует stable interior pointers
(non-moving GC, D6). View хранит data = backing->data + from — это
указатель внутрь backing’а; Boehm (GC_set_all_interior_pointers(1))
держит backing alive по interior-ptr.
Любая будущая замена GC-backend на moving GC требует одновременной замены slice-представления (separate header struct + ptr-update on move). Это закрепляется здесь как нормативный invariant.
Bounds-check
from < 0→ panicto < from→ panicto > len→ panic (для str —to > total_codepoints)- Empty slice (
arr[a..a]) → валиден - Отрицательные индексы → panic, не Python-style wrap
Сообщение panic’а: "array: slice [N..M] out of bounds for length L"
(паритет с Go/Rust).
Также: raw arr[i] bounds-check (D27 §1632 drift)
D144 одновременно фиксирует pre-existing drift: codegen arr[i]
теперь эмитит runtime bounds-check (раньше эмитил голый
(arr)->data[i] — controlled buffer overflow на запись, UB на чтение).
Сообщение: "array: index N out of bounds for length L".
Concurrency / M:N
Slice-view = shared mut backing между fiber’ами в M:N runtime =
формально UB по D79. В D71 single-threaded
bootstrap — OK по факту. Передача view через Channel[]T] или
spawn-capture в M:N — inherits D79 disclaimer.
Header layout
24 байта (ptr + len + cap) — тот же что у owner. Не оптимизировано
до 16 байт (которое требовало бы отдельного типа Slice[T] — отвергнуто
single-type-design’ом).
str[a..b] — bracket syntax для строк
Bracket-форма унифицирует idiom: arr[a..b] ≡ str[a..b].
Codepoint-indexed (как существующий nova_str_slice метод).
Panic при OOB (consistent с arr[a..b]).
Старый s.slice(a, b) метод — сохраняется с clamp-семантикой
для backwards-compat; align на panic откладывается в Plan 94
(см. [P-str-slice-clamp-vs-panic] в docs/simplifications.md).
Verified против
- Go
s[a:b]— паритет, без append-footgun. - Rust
&[T]— близко, без borrow checker (caller responsibility для multi-mut). - TypeScript
TypedArray.subarray— паритет. - Swift
ArraySlice<T>— без CoW-disconnect (view сразу видит mut). - Python
memoryview— паритет.
Связь
- D6 — non-moving GC; interior-ptr invariant амендится здесь.
- D27 —
[]TAPI; §1632 bounds-check (D144 чинит drift); §1663 «Слайсинг отложен» (D144 закрывает). - D58 — Range-литералы; D144 расширяет до 5 форм (open-ended).
- D79 — shared mut между fiber’ами = UB в M:N; slice inherits.
- D141 — Plan 90 bulk-ops; работают на view автоматически.
D145. fn[T] префикс — receiver-generic decl + bounds (Plan 101)
Status: MOSTLY CLOSED (2026-05-25, ред. 6 — Plan 101.1/2/3/4 ✅, 101.5 partial). Plan 101.1 codegen для non-int mono-dispatch — marker [M-fn-prefix-int-only-mono] ✅ RESOLVED (Plan 101 Group I, vec_map_int_str fix).
AMEND (2026-06-14, Plan 153.5): вложенные generic-ресиверы произвольной глубины (
fn[T] [][]T @m/fn[T] Vec[Vec[T]] @m— structural typevar-bind в самый внутренний элемент, depth-agnostic) — см. секцию «AMEND … вложенные generic-ресиверы» ниже. Разблокировало@flatten(D263); закрыло[M-153.5-flatten-nested-receiver].Реализовано (Plan 101.1–101.4 + 101.2):
- 101.1 ✅ — Parser
fn[T] ReceiverType @method+ 5 disambiguation error codes (E_UNDECLARED_TYPEVAR_IN_RECEIVER, E_BARE_TYPEVAR_NEEDS_PREFIX, E_DUPLICATE_GENERIC_DECL, E_PREFIX_SHADOWS_NAMED_TYPE, E_UNUSED_PREFIX_TYPEVAR). Codegen mono[]intelement + bare-T + non-int element (через Plan 95 array-ext infrastructure). vec.nv migration: 7 методов.- 101.2 ✅ — Bound integration: method-call bound enforcement (check_method_call_bounds в types/mod.rs); receiver-generic
fn[T Bound] []T @mловит violation на call-sitexs.m().- 101.3 ✅ — Multi-bound
[T A + B]: GenericParam.bound → bounds Vec, parser+ Typechain, type-check iterate all bounds (conjunction), strict check_generic_bound_declarations (E_BOUND_UNKNOWN / E_BOUND_NOT_PROTOCOL).- 101.4 ✅ — Protocol composition
use TypeNameв protocol body: AST TypeDeclKind::Protocol { methods, embeds }, parser parse_protocol_body, type-check flatten DFS + 5 диагностик (E_PROTOCOL_EMBED_{UNKNOWN, NOT_PROTOCOL, CYCLE, DUPLICATE, AFTER_METHOD, NOT_NAMED}).- 101.5 partial — stdlib audit: только vec.nv использует fn[T] prefix (7 методов работают; non-int — deferred). HashMap/PQ/Lru используют carrier-brackets (Plan 15 D72 path, unchanged).
Deferred (followup):
- vec_map_int_str — T=int U=str cross-type case ✅ RESOLVED (Plan 101 Group I, M-fn-prefix-int-only-mono).
- LSP quick-fixes (Plan 101.5 V2).
Ред. 3 (2026-05-24): complete rewrite после critical review. Ред. 1 описывала narrow
fn[T]only. Ред. 2 ошибочно ввела implicit-T (моя misinterpretation D35). Ред. 3 — finalized design: никакого implicit T,fn[T]префикс обязателен везде где receiver не имеет carrier-brackets, + bounds через existing D72,
- multi-bound
+, + protocol compositionuse Foo.Ред. 5 (2026-05-25): Plan 101.3 (multi-bound
[T A + B]) и Plan 101.4 (protocol compositionuse TypeName— pivot от earlier discussion A1use A, Bк более читаемому line-per-use) финализированы и реализованы.Ред. 3 (2026-05-24): complete rewrite после critical review. Ред. 1 описывала narrow
fn[T]only. Ред. 2 ошибочно ввела implicit-T (моя misinterpretation D35). Ред. 3 — finalized design: никакого implicit T,fn[T]префикс обязателен везде где receiver не имеет carrier-brackets, + bounds через existing D72,
- multi-bound
+, + protocol compositionuse Foo.
Что
Generic-параметры функции в receiver-position декларируются по одному из двух механизмов, в зависимости от формы receiver’а:
- Carrier-brackets на named generic-типе — existing
D119:
fn Option[T] @map[U]— T вOption[T]декларирует T.fn HashMap[K, V] @keys()— K, V вHashMap[K, V].fn Result[T, E] @ok()— T, E.- С bound (D72):
fn HashMap[K Hash, V] @from_pairs(...).
fn[T]префикс (новое, D145) — для receiver’ов без carrier brackets: bare T,[]T, tuple(T, U), composite без carrier:fn[T] T @identity() -> T => @— bare typevar.fn[T] []T @map[U](f fn(T) -> U) -> []U => ...— array.fn[T, U] (T, U) @swap() -> (U, T) => (@1, @0)— tuple.fn[T Hash] []T @dedup() -> []T => ...— bounds через D72.fn[T A + B] []T @method() => ...— multi-bound через+(Plan 101.3).
Правило
Когда fn[T] обязателен
fn[T1, ..., Tn] префикс обязателен для каждого typevar в
receiver-position, который не декларируется через carrier-brackets
именованного generic-типа. Конкретно:
| Receiver-shape | Carrier? | fn[T] нужен? |
|---|---|---|
Option[T], HashMap[K, V] | да named-brackets | нет |
[]T | нет — [] not bracket-decl | да fn[T] []T |
T bare | нет | да fn[T] T |
(T, U) tuple | нет — tuple-parens not bracket-decl | да fn[T, U] (T, U) |
(T, Option[U]) mix | T нет, U через Option | да fn[T] (T, Option[U]) |
[]Option[T] composite | T через Option[T] | нет |
Запрет дублирования
fn[T] запрещён для typevar, который ТАКЖЕ декларируется через
carrier-brackets:
fn[K Hash, V] HashMap[K, V] @method // ERROR E_DUPLICATE_GENERIC_DECL
// K, V уже декларированы через HashMap[K, V]; используй
// fn HashMap[K Hash, V] @method
Disambiguation: bare T vs named type
fn-prefix | Receiver | type T в scope? | Result |
|---|---|---|---|
| — | T | да | OK — метод на named T (D35 status quo) |
| — | T | нет | error E_BARE_TYPEVAR_NEEDS_PREFIX |
[T] | T | нет | OK — generic, T = typevar |
[T] | T | да | error E_PREFIX_SHADOWS_NAMED_TYPE |
| — | []T | да или нет | parse OK — но если есть named T, T = named (silent miscompile risk; см. ниже) |
[T] | []T | да или нет | OK — explicit prefix wins, T = fn-generic |
Critical: fn []T @method без fn[T] префикса и без type T в scope —
type-check error: «T не объявлен ни через carrier-brackets, ни через
fn[T] префикс, ни как named type». Закрывает silent-miscompile gap
(vec.nv pre-Plan-101 поведение).
Bound syntax (через D72)
fn[T Hash] []T @dedup() -> []T => ...
fn[T A + B] []T @method() => ... // multi-bound (Plan 101.3)
fn[K Hash, V] (K, V) @key_value() -> (K, V) => @
fn[T From[K], K] T @construct_from(v K) -> T => T.from(v) // parametric protocol
Bound = protocol-тип (D72) ИЛИ type-set (D310, Plan 172.3). Type-set — именованное
множество конкретных типов (type SignedInt set i8 | i16 | …), используемое как bound:
fn[T SignedInt] T.parse(...). Композиция type-set ∧ protocol — через тот же +
([T SignedInt + Hash]): T ∈ set И реализует protocol, проверки независимы per-member;
не более одного type-set в одном bound-листе (E_MULTIPLE_TYPE_SETS). Произвольные
representation/underlying bounds (~int, structural) — по-прежнему open question
Q-representation-bound, Plan 102 (future);
D310 закрывает только explicit-member-set, не representation.
Protocol composition (Plan 101.4 — закрывает D53 open question)
Protocols composed через use A, B keyword внутри protocol body.
Параллель D39 record-embed (same keyword, разная семантика). Composition
валиден в type-decl и anonymous type-position.
Literal-position — composition ОТВЕРГНУТА (см. ниже).
type Reader protocol { read(buf []u8) -> int }
type Writer protocol { write(buf []u8) -> int }
// 1. Multi-composition в type-decl:
type ReadWriter protocol {
use Reader, Writer // embed
close() -> () // own method
}
// 2. Single-composition (естественно, без ambiguity):
type ReadExt protocol {
use Reader
job() -> ()
}
// 3. Pure composition без own methods:
type Streamable protocol {
use Reader, Writer, Closeable
}
// 4. Mix anywhere в block — order independent:
type Complex protocol {
init() -> ()
use Reader
helper() -> int
use Writer
}
// 5. Anonymous-composition в type-position (extension D53):
fn process(rw protocol { use Reader, Writer }) { ... }
// 6. Использование как bound — composed protocol работает как named:
fn[T ReadWriter] []T @process() => ...
// эквивалентно fn[T Reader + Writer] []T @process() (101.3 multi-bound)
Семантика:
use A, B, C— flatten method-signatures из A, B, C в этот protocol.- Resulting method-set = union(A, B, C, own_methods).
- Multiple
use-statements аккумулируются:use A, B; use C≡use A, B, C. - T satisfies composed-protocol ⟺ T has все methods из union.
Реализация ред. 5 (2026-05-25, Plan 101.4):
- Парсер поддерживает обе формы:
use A, B(comma-list, как в spec) иuse A\n use B(line-per-use, более читаемо в большом protocol’е). - Все
use-items должны идти В НАЧАЛЕ protocol body — interleaving с методами запрещён (E_PROTOCOL_EMBED_AFTER_METHOD). Это упрощает чтение: сначала видишь “состав”, потом “новое”. - Type-check ловит:
- E_PROTOCOL_EMBED_UNKNOWN — embed target не объявлен.
- E_PROTOCOL_EMBED_NOT_PROTOCOL — target существует, но не protocol.
- E_PROTOCOL_EMBED_CYCLE —
A use B↔B use A(или self-embed). - E_PROTOCOL_EMBED_DUPLICATE — после flatten’а ≥2 method из разных embed-источников с тем же (name, arity). Override-механизм отложен.
- E_PROTOCOL_EMBED_NOT_NAMED —
use <complex type>запрещено.
Literal-composition — отвергнута:
// ❌ ОТВЕРГНУТО:
ro v = protocol Foo {
use Reader // error: E_LITERAL_COMPOSITION_NOT_ALLOWED
read(buf) => impl1
close() => impl2
}
// Workflow: extract в named type:
type MyRW protocol { use Reader, Writer }
ro v = protocol MyRW {
read(buf) => impl1
write(buf) => impl2
}
Почему literal-composition отвергнута: literal — value-construction (impls), composition — type-level operation. Смешивать слои когнитивно нагружено. Industry-aligned — Rust/Go/Java/Kotlin/Scala не разрешают anonymous-composition в literals.
Asymmetry с multi-bound (101.3) [T A + B] оправдана: разные
contexts — multi-bound = use-site intersection при satisfaction-check;
protocol composition = decl-time method-set union. Разные scopes,
разные операторы.
Differences vs D39 (record-embed):
- D39 record
use name Type(field-form, runtime delegation+field). - D53+ protocol
use Type[, Type]*(нет field, compile-time method-set union). - Same keyword
use— same intuition «include this stuff». Parser распознаёт по контексту (record-body vs protocol-body).
Многократное использование одного имени
Одно имя — один generic во всей сигнатуре (existing D119 / D72 convention):
fn[T] (T, T) @duplicate(a T) -> (T, T) => (a, a) // T дважды → один T
fn[T] [][]T @flatten() -> []T => ... // T в receiver и return — один T
AMEND (2026-06-14, Plan 153.5 commit 1c323d0e): вложенные generic-ресиверы произвольной глубины
fn[T]-typevar в receiver-position теперь связывается структурной унификацией на
ЛЮБОЙ глубине вложенности — не только на верхнем уровне элемента. Это закрывает дыру,
из-за которой [][]T @flatten() (= carrier-форма Vec[Vec[T]] @flatten() под
D239) не работал: тело должно назвать
внутренний T, а компилятор биндил его в непосредственный элемент.
Корень (обе формы записи теряли вложенность до фикса):
- Carrier
Vec[Vec[T]]— ПАРСЕР отвергал вложенный тип в carrier-слоте (parse_generic_decl_paramsждалparse_identна каждый слот → «expected], got identifier»). - Slice
[][]T— ПАРСИЛСЯ, но монорфизатор биндил receiver-typevarTв непосредственный элемент (Vec[int]), не во внутренний (int) — вложенность[][]Tсхлопывалась в один"[]T"-ресивер → тело строилоout []T == Vec[Vec[int]], возвращало неверный тип (verified probe RUN-FAIL, mono’dout=Nova_Vec____Nova_Vec____nova_int_p).
Правило (после AMEND):
- Обе формы записи приняты и эквивалентны под D239:
fn[T] Vec[Vec[T]] @m≡fn[T] [][]T @m. Парсер несёт полный структурированный тип ресивера вReceiver.receiver_ty(type_nameего flatten’ит в"[][]T"и теряет глубину — поэтому нужен отдельный структурный слот). - Receiver-typevar биндится в самый внутренний элемент, рекурсивно (depth-agnostic):
для
Vec[Vec[T]]/[][]TT = element-of-element; дляVec[Vec[Vec[T]]]/[][][]TT= element третьего уровня; и так далее. Унификация — структурная (по форме типа), не one-level-hardcoded. - Свободные typevar’ы collect’ятся рекурсивно из вложенного carrier-слота для
проверки
E_UNUSED_PREFIX_TYPEVAR(typevar объявлен вfn[T], но не упомянут в ресивере → ошибка) — собираются изreceiver_ty, а не из flatten’енного имени. E_UNDECLARED_TYPEVAR_IN_RECEIVERсохраняется дляfn []T @m/fn [][]T @mбезfn[T]-префикса (scope-typevar НЕ сидится изreceiver_ty— это намеренно, иначе ошибка маскировалась бы; см. checker-заметку).
fn[T] [][]T @flatten() -> []T => ... // T = innermost (depth 2) — РАБОТАЕТ
fn[T] Vec[Vec[T]] @flatten() -> Vec[T] => ... // carrier-форма, ≡ выше под D239
fn[T] [][][]T @deep_count() -> int => ... // depth 3 — T = innermost
fn[T] [][]T @first_row() -> []T => ... // вложенный-типизированный return
Реализация (depth-agnostic, без one-level-hardcoding):
- AST —
Receiver.receiver_ty: Option<TypeRef>несёт полный структурированный тип вложенного ресивера (единственное место, где глубина переживает —type_nameflatten’ит в"[][]T"). - Parser — slice
[][]T: счёт глубиныArray+ спуск до внутреннегоNamed→ строитArray(Array(Named T)). CarrierVec[Vec[T]]: новый разбор принимает ВЛОЖЕННЫЙparse_typeв слоте (детектIdent[) + рекурсивный сбор free-typevars; структурные слоты сворачиваются вreceiver_ty. Free-fn[T Bound=D]-разбор не тронут. - Mono — переиспользован существующий рекурсивный
infer_type_param_bindingдля структурного бинда receiver-typevar (Array-арм также снимает mono-формуVec____, восстанавливая элемент черезgeneric_type_instance_info); override применён на ВСЕХ путях, биндящих receiver-typevar (emit-dispatch carrier +[]T-sentinel slice + call-site return-inference). Depth-aware sentinel-ключи"[]"*N+"T"заменили hardcoded"[]T". Flat[]T(depth 1) остался byte-identical (legacyNovaArray_-путь); override гейтитсяreceiver_ty_is_nested— только для реально вложенных ресиверов. - Checker — вложенные typevar’ы из
receiver_tyсобираются вreferenced-множество дляE_UNUSED_PREFIX_TYPEVAR; scopegsНЕ сидится изreceiver_ty(сохраняетE_UNDECLARED_TYPEVAR_IN_RECEIVER— verified, что seed был бы регрессией).
Cross-cutting заметка. Это путь, через который идут ВСЕ []T-методы stdlib (slice-
dispatch). Изменение специально гейтнуто на genuinely-nested ресиверы → flat-случай
неизменен. См. D263 AMEND
(@flatten использует этот фундамент).
Известное ортогональное ограничение (pre-existing, вне scope): slice-форма
fn[T] [][]T -> []T, чьё тело строит свежий Vec[T].new(), упирается в
pre-existing erased-base-body лимит, который ЛОМАЕТ и flat fn[T] []T с Vec[T].new()
на baseline (expected struct 'Vec____Nova_T_p'). Production-flatten — CARRIER-форма
Vec[Vec[T]] @flatten (как все stdlib), работает полностью; slice-form nested-receiver
binding доказан отдельно (@count_all/@first_row).
Backward-compat
- 100% преserve для existing
fn Option[T] @map[U],fn HashMap[K, V] @keys,fn Result[T, E] @ok,fn HashMap[K Hash, V] @method— D145 строго аддитивно. std/collections/vec.nvсодержит 7 методов patternfn []T @method[U](написан как-если-бы T дженерик). Это bug — T silently трактуется как named type, codegen падает. Plan 101.1 включает migration vec.nv →fn[T] []T @method[U].
Параллель индустрии — таблица
| Lang | Synтакс для array-method | Bound syntax |
|---|---|---|
| Rust | impl<T> Vec<T> { fn map<U> } | <T: A + B> |
| Go | func (v Vec[T]) Map[U] | [T A | B] (union, не intersection!) |
| TypeScript | function map<T, U>(arr: T[], f) | T extends A & B |
| Kotlin | fun <T, U> Array<T>.map(f) | <T : A> + where T : B |
| Scala 3 | extension [T](arr: Array[T]) def map[U] | T <: A & B |
| Java | <T, U> U[] map(T[] arr, ...) | <T extends A & B> |
| Nova D145 | fn[T] []T @map[U] | [T A + B] (Rust-style +) |
Nova edge:
- Cleanest receiver syntax —
fn[T] []T @mapкороче Rustimpl<T> Vec<T> { fn map<U> }(2 nested blocks → 1 line). - Bound syntax без двоеточия —
[T Hash](D72) — параллель Novaname typeconvention (params, fields, let). - Multi-bound
+familiar — Rust audience узнаёт. - Protocol composition через
use— параллель D39 record-embed, единое правило. - Loud disambiguation —
E_BARE_TYPEVAR_NEEDS_PREFIX/E_PREFIX_SHADOWS_NAMED_TYPEявные, не silent miscompile. - Future-proof —
Q-representation-boundоткрыт для extension на concrete-type bounds (Plan 102).
Lineage
- Plan 48 / D119 — method-level + receiver-via-carrier generics.
- Plan 72 / D72 — bound syntax
[T Bound](free fn + type-decl). D145 переиспользует в новой позиции (fn[T Bound]prefix). - Plan 88 — static-method-on-typevar.
- Plan 99 — Option/Result closure-applying на Nova-body (paritет).
- D39 —
use Typeembed для records. D145 переиспользует pattern для protocol composition (Plan 101.4). - D53 —
type X protocol { ... }. D145 закрывает open question «Composition protocol’ов» через 101.4.
См. также
- D72 — bound syntax.
- D119.
- D39 —
useдля embed. - D53 — protocol decl.
- Plan 101 master
- 5 sub-plan’ов:
- Q-representation-bound — concrete-type bounds (newtype/embed-aware), Plan 102 future.
D372. Canonical .new() constructors (convention)
Статус: convention (stdlib provides, compiler does NOT auto-generate).
stdlib предоставляет .new() для типов с единственным очевидным
default-значением:
| Тип | .new() возвращает | Файл декларации |
|---|---|---|
int, u8–u64, i8–i64 | 0 | std/runtime/defaults.nv |
f32, f64 | 0.0 | std/runtime/defaults.nv |
bool | false | std/runtime/defaults.nv |
str | "" | std/runtime/string.nv |
[]T (для любого T) | [] (empty array) | builtin (emit_c.rs) |
Также []T.with_capacity(n int) -> Self — empty с pre-allocated capacity
(builtin). Retracted — см. амендмент ниже (2026-07-06): with_capacity
удалён, ёмкость теперь свойство cap.
Для своих типов разработчик пишет .new() явно. Компилятор НЕ
автогенерирует для user records / sum types / consume types.
Это design discipline:
- Явный конструктор виден в
nova docи IDE. - Имена кодируют намерение (
User.new(name, email)vsUser.guest()). - Валидация инвариантов в момент создания.
- Эволюция типа: добавление поля заставляет обновить конструктор — good failure (компилятор поймает breaking change).
НЕ имеют canonical .new() (convention — не использовать;
enforcement diagnostic — followup [M-91.7-default-new-enforcement]):
char('\0'сомнителен как «default»)Result[T, E](OkилиErr? ambiguous)Option[T]— каноничен, но codegen ограничение для generic builtin sum static methods откладывает Nova-side декларацию (followup[M-91.7-option-new-static]). До закрытия — использоватьNoneнапрямую.- tuples (
(int, str)etc.) - user-defined records / sum / consume types — по конвенции этого блока
- protocols, fn types, external/opaque
Пример
// stdlib provides:
ro x = int.new() // 0
ro s = str.new() // ""
ro a = []int.new() // []
ro buf = []u8.with_capacity(1024)
// User type — explicit:
type User { name str, email str, is_admin bool }
fn User.new(name str, email str) -> Self => { name, email, is_admin: false }
fn User.guest() -> Self => { name: "guest", email: "", is_admin: false }
Связь
- D26 — prelude auto-availability.
- D66 —
Selfв return type. - D131 — consume / fluent.
- D182 —
Selfrequirement. - Plan 91.7.
Амендмент (vec-sweep, 2026-07-06):
with_capacityудалён, ёмкость — свойствоcap.with_capacity(n) -> Selfкак отдельный static-конструктор УДАЛЁН дляVec[T]/[]T,HashMap[K,V],Set[T],StringBuilder,WriteBuffer,Queue[T](везде, где он существовал). Ёмкость теперь read/write СВОЙСТВО по D117 (arity-overload):X.cap() -> int(getter) /X.mut cap(n int) -> @(setter). Конструкция с pre-allocated capacity —X.new().cap(n)вместо бывшегоX.with_capacity(n).Semantics по типу:
Vec[T]/[]T:cap(n)— ТОЧНАЯ ёмкость (requires n >= @len(), без округления), полностью взаимозаменяема с бывшимwith_capacity(n).HashMap[K,V]/Set[T]:cap(n)гарантирует МИНИМУМ n вставок без rehash (entry-count → bucket-count, как раньше), но только РАСТЁТ на непустой карте; на СВЕЖЕЙ (@_count == 0) карте допускает и уменьшение до точного целевого bucket-count — такnew().cap(n)воспроизводит староеwith_capacity(n)побайтово.WriteBuffer/StringBuilder:cap(n)делегирует к внутреннему[]u8’s собственному точномуcap(n).Queue[T]:cap(n)резервирует ёмкость НА ОБОИХ backing-массивах (_inbox/_outbox) одновременно.from_raw_parts → перегрузка
new(амендмент, vec-sweep 2026-07-06).Vec[T].from_raw_parts(ptr, len, cap) -> Selfпереименован вVec[T].new(ptr *T, len int, cap int) -> Self— арность-перегрузка статическогоnew(0-арг = пустой Vec, 3-арг = raw components). Контракт unsafe-обязательства на call site — БЕЗ ИЗМЕНЕНИЙ (см. текст на месте декларации,std/collections/vec/core.nv).@into_raw(обратная операция) не переименован.Известные компиляторные гэпы, обнаруженные при миграции:
[M-vec-spelling-array-value-position-cap-collision]— цепочка[]T.new().cap(n)В ОДНОМ выражении может мис-дispatch’иться по NAME+ARITY на «cap»-метод НЕСВЯЗАННОГO ко-компилируемого типа (не только erased-array случай D239-амендмента выше — воспроизведено и для произвольных типов). Обход: биндинг в explicitly-typed локаль ПЕРЕД вызовом.cap(n)отдельной инструкцией.[M-vec-spelling-consume-chain-cap-collision]—consume x = T.new().M(...)(ЛЮБОЙ 2-звенный chain, забинженный черезconsume, дляT consume-типа) ломает D133 consume-tracking для ВСЕХ ОСТАЛЬНЫХconsume ... = T.new()site’ов в той же compile unit (воспроизведено вне зависимости от имени второго метода — не специфично дляcap). Обход:consume x = T.new()(один вызов) +x.M(...)отдельной инструкцией.[M-vec-spelling-maplit-desugar-cap-ice]— попытка добавить.cap(n)pre-sizing statement в desugar[k:v]-map-литерала (послеHashMap.new(), доinsert_new-цикла) роняла компилятор (ICE: “method call.insert_newreturn type unknown”) при компиляции нескольких файлов вместе в одной CU. Обход (принят): pre-sizing убран из desugar’а полностью — map-литералы строятся через голый.new()+ amortized growth в insert_new-цикле (perf-only регрессия, corretness не затронута).
D181. Array methods — -> @ fluent mut chain + slice syntax
Статус: active (Plan 91.7, 2026-05-28).
Амендмент-упрощение (Plan 184, 2026-07-06). Тип
-> @больше не выводится эвристикой «heap-алиас / value-копия-с-распадом» (исходные D326-R7/R8: RETURN-оракул D246 + escape-decay D228). Теперь-> @имеет конкретный тип по категорииSelf(таблица Р7 ревизии D326-Plan184): у стекового (value) типа-> @=-> ref Self; у кучевого (heap) —-> Self. Эвристики bind-site заменены типами; поведение fluent-цепочек (a.push(1).push(2)) не меняется.
-> @ для всех mut-методов []T
Все мутирующие методы массива возвращают @ (receiver pointer)
для fluent chain (D131):
| Метод | Сигнатура |
|---|---|
@push(v T) | -> @ |
@reserve(extra int) | -> @ |
@truncate(n int) | -> @ |
@fill(v T) | -> @ |
@copy_from(src readonly []T) | -> @ |
@extend_from(src readonly []T) | -> @ |
@insert_from(i int, src readonly []T) | -> @ |
@copy_within(src_from, dst_from, len) | -> @ |
@sort() (Nova-side) | -> @ |
@sort_by(cmp) | -> @ |
Non-mut методы (@get(i), @pop()) возвращают Option[T] —
unchanged.
Пример
mut a = []int.new()
a.push(1).push(2).push(3).reserve(10)
a.sort() // direct call
ro r = a.sort_by(|x,y| ...) // can also return into binding
Slice — только bracket syntax (Plan 96)
Метод @slice(from, to) -> []T удалён. Используйте arr[a..b]
(zero-copy view, см. Plan 96 / D-str-slice). Один очевидный путь.
Известные ограничения
- Mixed Nova-method + builtin chain:
a.sort().push(99)— codegen пока эмититa->sort()(struct field access) вместо function call. Followup[M-91.7-mixed-method-chain]. Workaround: разнесите вызовы. - Generic sort/min/max для
[T Ord]— followup[M-91.7-sort-generic]. Текущий MVP — concrete[]int @sort()(Plan 91.3).
Связь
- D131 — fluent API
семантика
-> @. - D177 — Nova-body dispatch механизм.
- Plan 90.1 — extend-family (extend_from, insert_from, reserve).
- Plan 96 —
arr[a..b]slice syntax.
D182. Self в return-type static methods — required form для parametric types
Статус: active (Plan 91.7, 2026-05-28).
Правило
Для static-методов на параметризованных типах (fn Option[T].new(),
fn HashMap[K, V].new(), etc.) return-type должен использовать Self,
а не explicit-form -> Option[T] / -> HashMap[K, V].
Rationale:
- Explicit-form дублирует тип-параметры — redundant.
Selfустойчив к переименованию типа (rename-safe).Selfявно говорит «возврат того же receiver-типа» — semantic clarity.- Single canonical form — D9 «один очевидный путь».
Примеры
// ✅ Correct (canonical):
export fn Option[T].new() -> Self => None
export fn HashMap[K, V].new() -> Self => { ... }
export fn StringBuilder.new() -> Self => { ... }
// ❌ Wrong (explicit redundant form):
export fn Option[T].new() -> Option[T] => None
export fn HashMap[K, V].new() -> HashMap[K, V] => { ... }
Для primitive receiver types
Self тоже рекомендуется для consistency:
export fn int.new() -> Self => 0 // канонично
export fn int.new() -> int => 0 // допустимо, но не canonical
Codegen requirement
Self в return-type корректно resolved через current_receiver_type ⇒
правильный C type:
- primitive receiver → primitive value type (
nova_int,nova_bool, …) - Option/Result → sum repr (
NovaOpt_<T>,NovaRes_<ok>_<err>*) - user record →
Nova_<TypeName>*
См. emit_c.rs::type_ref_to_c "Self" case — делегирует в receiver_c_type.
Enforcement
Validation rule — followup [M-91.7-self-required-parametric]. Текущий
compiler принимает обе формы; canonical форма документирована здесь.
Связь
D183. Canonical comparison protocols + default method bodies (Plan 91.8a)
Статус: active (Plan 91.8a, 2026-05-29).
Канонические протоколы (renames)
| Было | Стало | Файл |
|---|---|---|
Iter[T] | Iterable[T] → Next[T] + Iter[I] (Plan 138 D241+D242) | std/prelude/collections.nv |
Display | Display | std/prelude/protocols.nv |
Equal.eq(other Self) -> bool | Equal.equals(other Self) -> bool | std/prelude/protocols.nv |
Compare.cmp(other Self) -> Ordering | Compare.compare(other Self) -> int | std/prelude/protocols.nv |
Hash.hash() -> u64 | unchanged | std/prelude/protocols.nv |
Rationale renames:
-ablesuffix convention — unified naming (Iterable/Equal/Compare/Hash/Display).Compare.compare -> int— единый стиль сstr.compare()(D178) и Cmemcmp/strcmp.Orderingsum-type удалён.Equal.equals— явнее чемeq(Java convention).Display→Display— действие через-able, не имя-noun.
Compare embeds Equal
export type Equal protocol {
equals(other Self) -> bool
}
export type Compare protocol {
use Equal
compare(other Self) -> int
equals(other Self) -> bool => @compare(other) == 0 // default body
}
use Equal (D39 embed) делает каждый Compare также Equal.
Локальная декларация equals в Compare с default body overrides
embedded default — implementer пишет только @compare, @equal
auto-synthesized из default body как @compare(other) == 0.
Default method bodies в protocols
Правило (новое в D183):
Метод в protocol-декларации может иметь тело (
=> exprили{ ... }). Тело используется как default-реализация: если тип-implementer не задаёт свой@method, компилятор использует body из протокола, подставляяSelf= receiver type. Если implementer задал@methodявно — explicit version используется (override).
Семантика:
- Метод без тела = abstract — implementer ОБЯЗАН реализовать.
- Метод с телом = default — implementer МОЖЕТ override.
Пример:
type Compare protocol {
use Equal
compare(other Self) -> int // abstract
equals(other Self) -> bool => @compare(other) == 0 // default
}
type MyDate { y int, m int, d int }
fn MyDate @compare(other MyDate) -> int { ... }
// @equal НЕ объявлен — используется default из Compare.
// Override для perf:
type FastHashed { hash_cache u64, ... }
fn FastHashed @compare(other FastHashed) -> int { ... }
fn FastHashed @equal(other FastHashed) -> bool {
@hash_cache == other.hash_cache && @compare(other) == 0
}
Cleanup
Orderingsum-type удалён изstd/prelude/core.nv.Less/Equal/Greaterexports удалены изstd/prelude.nv.std/sort.nvsort_by(cmp fn(int, int) -> int)— memcmp-style convention.PRELUDE_VERSIONbumped 12 → 13.
Memcmp-compatible int return
compare(other) -> int returns:
- negative if
@ < other - zero if
@ == other - positive if
@ > other
Caller должен использовать только sign (< 0, == 0, > 0), НЕ magnitude.
Совместимо с C memcmp/strcmp convention. Implementer для primitive numerics
рекомендуется использовать safe signum form:
fn int @compare(other int) -> int =>
if @ < other { -1 } else if @ > other { 1 } else { 0 }
Не использовать => @ - other — overflow risk для больших int.
Реализация (части)
- Парсер (
compiler-codegen/src/parser/mod.rs::parse_effect_methods): добавлен parser default body после return_type/contracts. Body ==> exprили{ ... }. ПолеEffectMethod.default_body: Option<Block>в AST. check_protocol_embeds(compiler-codegen/src/types/mod.rs): local override embedded methods разрешён — locally declared метод в protocol с тем же именем что embedded не считается duplicate. Используется дляCompare.equalsoverrides embeddedEqual.equalsdefault.- Codegen synthesis для defaults: followup
[M-91.8a.2-default-codegen]. Сейчас implementer пишет default-method explicitly для compatibility (как boilerplateequals(o) => @compare(o) == 0).
Известные ограничения / followups
- Codegen synthesis (
[M-91.8a.2-default-codegen]): type T который имеет@compareно не@equalпока компилируется только если@equalобъявлен явно. Eager synthesis из default body — отдельный codegen pass. - Operator dispatch (D363, Plan 91.8b):
==всё ещё dispatches к@eq(D46). Renaming@eq→@equalв operator dispatch — задача Plan 91.8b. До 91.8b implementer пишет оба:@equal(protocol) +@eq(operator). - Structural
==для mono’d generic-sum + Result ✅ (Plan 153.3, commit1cc82de5): дефолтное структурное==(tag + payload, без user@equal/@compare) теперь покрывает мономорфизированные generic-sum (Foo[int].A(1) == A(1)) и Result (NovaRes_*). Раньше оба тихо деградировали в pointer-identity: legacysum_schemaskeyed generic-именем (mono’d-ключ отсутствовал →emit_field_eqпромахивался мимо schema), а Result-NovaRes_*(спец-ABI с typed-error-полями) не матчилNova_-sum-тест. Фикс:reconstruct_mono_sum_schema(substituted-схема вариантов из generic-шаблона + recorded type-args; tag-префикс = полныйNova_<mono>) +NovaRes_-ветка вemit_field_eq/==-операторе черезnovares_ok_err.result == Ok(x)/== Err(x)✅ ([M-153-result-eq-literal-expected-type]RESOLVED): голыйOk/Err-литерал с non-default-E (напр.binary_search→Result[int,int]) дефолтилE=strи не совпадал по типу с LHS → CC-FAIL. Фикс codegen-local: в==-NovaRes_-ветке, если типы операндов расходятся и одна сторона — голый result-ctor, она переэмитится под concreteNovaRes_<n>другой (reemit_result_variant_as). General expected-type propagation для overload-резолва (@into) остаётся вQ-overload-result-type. - Generic sort/min/max (D373, ex-D185, Plan 91.8c): generic
fn[T Compare]array methods — реализовано, см. D373. - D328 — Value-record
==СТРУКТУРНОЕ (Plan 172.4 Ф.2, 2026-06-28): value-record (type P value {…}— C-репрезентацияNovaValue_<Name>by-value, D228/D277/D290) сравнивается структурно (field-by-field, как sum/heap-record), а НЕ сырым C-==на struct (была acceptance-CC-FAIL «invalid operands to binary expression» — C не имеет struct-==). Обоснование: value-record — значение (нет heap-идентичности); равенство = по значению полей. Маршрутизируется через ЕДИНЫЙemit_field_eq-диспетчер (§0 «один источник per-type операций»): добавленNovaValue_-арм (by-VALUE доступ(l).field, не(*l)->field) + top-level-==/!=-роутинг в тот же диспетчер; user-@equal(если объявлен) приоритетен, иначе структурная рекурсия поrecord_schemas. Вложенные value-record-поля рекурсируют тем же армом. heap-record==(type P {…},Nova_P*) — ОТДЕЛЬНАЯ ось: сейчас reference-eq; структурить ли — open (Plan 172.4 Ф.2 дизайн-вопрос, НЕ решается этим блоком — value-records однозначно структурны, heap-records обсуждаемо). Арифметика на value-record (@plus/…) — отдельный value-ABI концерн (Plan 172.4 Ф.3), не этот блок.
Связь
- D26 — prelude auto-availability.
- D39 —
useembed. - D58 — structural typing.
- D72 — bounds.
- D109 — split policy (Hash не embeds Equal; Compare embeds Equal в D183).
- D178 —
str.compare -> int. - Plan 91.8a — implementation.
D183 amendment — Plan 91.8a.2 part 1: protocols refactor (orthogonal) + Self в param
Статус: active (Plan 91.8a.2 part 1, 2026-05-29).
Refactor: orthogonal protocols (canonical coercion form)
Было (91.8a part 1):
type Equal protocol {
equals(other Self) -> bool
}
type Compare protocol {
use Equal
compare(other Self) -> int
equals(other Self) -> bool => @compare(other) == 0 // override of embedded default
}
Стало (91.8a.2 part 1) — canonical:
type Equal protocol {
equals(other Self) -> bool {
ro cmp Compare = @ // coercion-style (explicit dependency)
cmp.compare(other) == 0
}
}
type Compare protocol {
compare(other Self) -> int
}
Rationale:
- Orthogonal protocols — каждый stand-alone, без embed-зависимости.
- Coercion canonical (Q6 decision): explicit cross-protocol dependency visible при чтении декларации; codegen devirtualizes к direct call когда тип known statically (zero runtime cost).
- Conditional default: T satisfies Equal если has @equal explicit ИЛИ satisfies Compare (default body synth via @compare). Type только Equal (Vector3, Complex, etc.) пишет @equal явно — coercion fails potential потому что @compare отсутствует.
- Direct form
=> @compare(other) == 0тоже валидна — terser; same C output after devirtualization. Coercion form preferred в stdlib для documentation.
Display.fmt default body
type Display protocol {
fmt(sb StringBuilder) {
sb.append(str.from(@))
}
}
- Primitives — works via primitive
Nova_int_to_stretc. - User types — implementer пишет @display явно (perf) OR provides
fn str.from(MyType) -> stroverload.
From identity blanket (D183 amendment)
export fn[T] T.from(t T) -> T => t
- Аналог Rust
impl<T> From<T> for T. - Override запрещён (Q4 strict decision): попытка
fn Money.from(m Money) -> MoneyдаётE_BLANKET_IDENTITY_OVERRIDE. Identity is identity (D9 single canonical path). - Resolution order для
T.from(value):- Explicit
fn T.from(value_type)→ win - Blanket identity — match только если
value_type == T - D77 auto-derive из From[value_type] chain
- Error E_NO_FROM_IMPL
- Explicit
- Identity Into auto-derived через D77.
- Coexistence: blanket additive с existing
From[T]protocol decl (std/prelude/protocols.nv:81-83) +emit_c.rs::from_targets/into_targetsregistries (D77 4-way derive).
Self в param-type position (М-91.8a-self-in-param closed)
Раньше fn T @method(other Self) -> R давал E7001 «Self type used outside
receiver context». Fix: emit_c.rs::emit_module method overload registration
устанавливает current_receiver_type перед param_c_types calculation
(mirror return-type path). Закрыто Plan 91.8a.2 part 1.
Codegen lazy synthesis + devirtualization — followup (Plan 91.8a.2 part 2)
Часть 1 (текущая) ограничена структурным refactor + Self fix. Часть 2 (отдельный sub-session) реализует:
- Lazy synthesis at use-site:
- Bound contexts (
[T Equal]etc.) — synth default body для типов которые satisfy abstract methods - Protocol coercion (
let x Equal = m) - Operator dispatch (Plan 91.8b)
- String interpolation (Plan 91.10)
- NOT triggered: bare method call (
m.equals(other)— direct lookup only)
- Bound contexts (
- Devirtualization pass — coercion form
let cmp Protocol = @становится type ascription + direct call при synthesis для concrete T. Result: same C output что direct form. - Cache per compilation unit:
HashMap<(TypeId, MethodName), SynthFnDecl>. - From blanket mono — extension Plan 101 mono pass на
fn[T] T.methodstatic на generic T. - Error diagnostics: E_SYNTH_CYCLE, E_SYNTH_AMBIGUOUS, W_DEVIRT_FAILED, E_BLANKET_IDENTITY_OVERRIDE.
До части 2 — implementer пишет default body methods явно (boilerplate compatibility). Это работает но дублирует код.
Связь
- D183 (part 1) — base D183.
- D26 — prelude.
- D58 — structural typing.
- D77 — From/Into 4-way auto-derive.
- Plan 91.8a.2.
⚠️ См. D229 — Debug sibling protocol с distinct debug semantics (diagnostic vs user-facing display);
${expr:?}syntax routes к Debug.@debug vs bare${expr}к Display.@display (Plan 91.14, 2026-06-05).
D186 — #impl(P1 + P2 + ...) opt-in annotation для protocols
Когда: 2026-05-29 (Plan 91.9). Plan: 91.9-impl-annotation.md. Зависит от: D58 (structural protocols), D72 (generic bounds), D183 (canonical protocols Equal/Compare/Display + default body).
Проблема
Nova protocols — structural (D58). Compiler разрешает obj.method()
если у типа есть соответствующий метод, без явного opt-in. С добавлением
default body synthesis (D183) ситуация ухудшилась:
type Greetable protocol {
greet() -> str { "Hello, " + @name() }
}
type User { display_name str }
fn User @name() -> str => @display_name
u.greet() // ??? — без D186 это работало structurally (TypeScript-style)
Проблемы:
- Невидимая мутация behavior: добавление протокола в одном модуле тихо добавляет методы всем типам подходящей сигнатуры.
- Reader-hostile: глядя на
type User, нельзя понять что у него есть методgreet(он синтезирован). - Ambiguity: два протокола с methods одинакового имени и default bodies — порядок resolution не детерминирован.
- Verification: type-author не получает feedback что type соответствует intended protocol.
Решение
#impl(P1 + P2 + ...) annotation перед type declaration. Меняет
два аспекта:
1. Gate semantics (bare-call / interpolation требуют opt-in)
Контексты, где synthesis fires:
| Context | Требует #impl(P)? | Почему |
|---|---|---|
Bare call u.method() | ✅ да | Ambient — type-author opt-in нужен |
Interpolation "${u}" | ✅ да | Ambient — Display.fmt synthesis |
Generic bound [T P] | ❌ нет | Caller opted in через bound |
Coercion let x P = u | ❌ нет | Caller opted in через annotation |
Cast (u as P).method() | ❌ нет | Caller opted in через cast |
Param func(...args []P) | ❌ нет | Caller opted in (signature) |
Принцип симметрии: хотя бы один из (type-author, use-site) должен
opt’нуться явно. Структура #impl — type-author side; bound/coercion/cast/
param — use-site side.
2. Verification (auto-check соответствия)
При декларации #impl(P) compiler проверяет:
- E_UNKNOWN_PROTOCOL —
Pне найдено как type name. - E_IMPL_NOT_PROTOCOL —
Pнайдено, но не protocol kind. - E_IMPL_MISSING_METHODS — T не provides метод P:
- не имеет explicit
fn T @method(...), - и default body P.method не synthesizable для T (зависит от другого метода которого T не имеет).
- не имеет explicit
Verification работает at type-declaration site — error появляется сразу, не при первом использовании.
Синтаксис
#impl(Equal + Compare + Display)
type Coin { value int }
fn Coin @compare(other Self) -> int => ...
fn str.from(c Coin) -> str => ...
// equals auto-derived через Equal.equals default (uses @compare)
// fmt auto-derived через Display.fmt default (uses str.from)
+ separator consistent с multi-bound [T A + B + C] (D72, Plan 101.3).
Order arbitrary: #impl(A + B) ≡ #impl(B + A).
Multiple #impl annotations не разрешены — single annotation with +.
Position
#impl(...) ставится перед type T (рядом с #stable, #from_fields):
#stable(since = "0.1")
#impl(Hash + Equal)
type UserId { value u64 }
Семантика
Use-site остаётся structural (D58 preserved). #impl не делает тип
nominal. Он добавляет:
- Gate на ambient synthesis (bare call / interpolation).
- Verification в точке декларации.
Через bound / coercion / cast / param-coercion использование любого
structurally-подходящего типа всё ещё работает — #impl не требуется.
Что НЕ делает
- НЕ создаёт nominal typing (use-site structural preserved).
- НЕ обязателен — opt-in, existing types работают через use-site coercion.
- НЕ меняет runtime —
#implтолько compile-time проверка/gate.
Codegen
emit_c.rs::try_synthesize_default_method_with_gate(t, c, m, gate_on_impl):
gate_on_impl = true— bare call / interpolation; restricts candidates к protocols вtype_impl_protocols[t].gate_on_impl = false— vtable thunk (coercion), bound mono; structural.
type_impl_protocols: HashMap<String, HashSet<String>> populated в
forward-decl pass из TypeDecl.impl_protocols.
Type-checker verification
types/mod.rs::verify_impl_protocols walks каждый Item::Type с
non-empty impl_protocols:
- Each
Plookup вself.types. None → E_UNKNOWN_PROTOCOL. - Kind check — must be
TypeDeclKind::Protocol. Иначе → E_IMPL_NOT_PROTOCOL. - Each required method
mвP.methods:t_provides_method(T, m.name)→ ok (explicit).m.default_body.is_some() && default_body_calls_satisfy_for(body, T)→ ok (synthesizable).- Else → list в missing, emit E_IMPL_MISSING_METHODS с hint.
default_body_calls_satisfy_for — AST walker проверяет body’s referenced
calls resolve for T (через t_provides_method + t_satisfies_str_from для
auto-derive str.from(@) pattern).
Compatibility
- Existing structural use-sites (bound
[T P], coercionlet x P = u, cast(u as P), parameter coercion) continue работать без#impl. - Existing types без
#implмогут потерять bare-call:fn User @name() -> str => ...; u.greet()(Greetable.greet default) — раньше работало, теперь error (без#impl(Greetable)). - Migration trivial: добавить
#impl(Protocol)перед type decl.
Связь
- D58 — structural protocols (use-site preserved).
- D72 — generic bounds (use-site opt-in alternative).
- D183 — canonical protocols + default body synthesis (что gate’ится).
- D109 split policy.
- Plan 101.3 — multi-bound
+syntax.
⚠️ D186 AMENDED by Plan 108.4 (2026-06-09) —
#impl(P)annotation now checks receiver_mut in addition to method signature. If the protocol declaresmut @method()and the implementing type declaresfn T @method()(ro receiver), type-checker emitsE_PROTO_IMPL_RO_FOR_MUTat the type-declaration site (where#impl(P)appears). The 4 new error codes are:E_PROTO_IMPL_RO_FOR_MUT,E_PROTO_IMPL_MUT_FOR_RO,E_PROTO_IMPL_MUT_FOR_CONSUME,E_PROTO_IMPL_CONSUME_FOR_MUT. See D209.
D200. Associated constants — const field в type X
Plan 114.4 Ф.2 (extracted from Plan 114 Ф.10 safety hatch). Status: 🆕 draft (финализируется в Ф.4).
Что
const declaration внутри type X { … } body — associated constant
типа. Не часть instance layout; accessible через namespace
Type.CONST_NAME.
type Config {
const VERSION int = 2 // associated const
const PROTOCOL str = "v2"
const MAX_PEERS int = 1024
name str // instance field
timeout Duration // instance field
}
// Access — только namespace
Config.VERSION // ✓ 2
Config.MAX_PEERS // ✓ 1024
// Instance access — error
ro c = Config { name: "alice", timeout: SECOND }
c.VERSION // ✗ E_CONST_INSTANCE_ACCESS
// Layout
sizeof(Config) == sizeof(name) + sizeof(timeout) // const fields НЕ в layout
Семантика
- Strict constexpr — RHS должен быть literal-eligible.
- Zero storage в instance. Codegen не emit’ит const-field в struct
layout. Каждый const-field живёт как top-level C-symbol
Type_FieldNameв .rodata. - Namespace access only.
Type.NAMEresolution через type’s const-table.instance.NAME→E_CONST_INSTANCE_ACCESS. - Не указывается в record literal. Указание →
E_CONST_FIELD_IN_LITERAL. export constfield — publicly accessible cross-module.- Modifier-conflicts:
mut const/const mut→E_CONST_MUT_CONFLICT.ro const/const ro→E_CONST_RO_REDUNDANT.consume const→E_CONST_CONSUME_CONFLICT.
- SCREAMING_SNAKE_CASE convention — lint warning (D30 carry-over).
Sum-type associated constants
const decl внутри sum-type body — associated на sum-type-level:
type Status = Active | Inactive | Pending {
const VERSION int = 2
const MAX_TRANSITIONS int = 100
}
Status.VERSION // ✓ 2
Per-variant const’ы (Active { const X = 1 }) — out-of-scope V1, followup
[M-115-per-variant-const].
Generic-type associated constants
T-independent — RHS не reference’ит generic params:
type Box[T] {
const TAG int = 0
value T
}
Box.TAG // ✓ emit single Box_TAG
T-dependent — RHS reference’ит generic param:
type Box[T] {
const SIZE int = sizeof(T)
value T
}
Box[int].SIZE // ✓ 8 — per-mono Box_int_SIZE
Box[str].SIZE // ✓ 16 — per-mono Box_str_SIZE
Box.SIZE // ✗ E_GENERIC_CONST_REQUIRES_INSTANTIATION
Allowed в T-dependent RHS (V1):
sizeof(T)гдеT— generic param.- Арифметика над
sizeof(T_i)и literals. - Ссылки на T-independent
constчерезType.CONST.
НЕ allowed в V1:
T.METHOD()calls —[M-115-t-method-in-const].const fncalls с generic args —[M-115-generic-const-fn].- Recursive type refs (
Tree[T] { const X = sizeof(Tree[T]) }) →E_GENERIC_CONST_CYCLE.
Codegen
- Non-generic + T-independent: top-level
static const T Type_FieldName = …;в .rodata. ResolutionType.FieldName→ C-symbolType_FieldName. - Generic T-dependent: per-mono symbol naming coherent с existing generic-fn mono (Plan 70.5). Emit при каждой monomorphization.
export constfield: public C-symbol visibility.
Сравнение с mainstream
| Язык | Синтаксис | Storage |
|---|---|---|
| Java | static final int VERSION = 2; (внутри class) | top-level C-static |
| Rust | impl Config { const VERSION: i32 = 2; } | top-level |
| Kotlin | companion object { const val VERSION = 2 } | companion slot |
| Swift | struct Config { static let version = 2 } | type-metadata |
| TS | class Config { static readonly VERSION = 2 } | class-static |
| Nova | type Config { const VERSION int = 2; … } | top-level .rodata |
Use cases
- Version / protocol identifiers:
Config.VERSION,Protocol.MAGIC_BYTES. - Capacity / size limits:
Buffer.DEFAULT_CAPACITY. - Math constants:
Circle.PI,Complex.UNIT_IMAGINARY. - Per-mono sizes:
Box[int].SIZE,Pair[T,U].TOTAL.
Cross-ref
- D36 — field-decl extended.
- D184 — Plan 114 master keyword refresh.
- D199 —
const fn(могут использоваться для assoc const RHS). - D27 —
[N]Tarrays.
Acceptance
См. Plan 114.4 A5-A13 (T2 series).
D214. ptr opaque pointer type + tuple FFI returns + opaque handle pattern
Plan 115 (foundational FFI). Status: ✅ V1 closed 2026-06-01.
⚠️ SUPERSEDED by Plan 134 (2026-06-09) —
ptrbuilt-in primitive type removed. Replace all occurrences with*()(pointer to unit type =void*in C).*()is the idiomatic expression of an opaque pointer in the*Ttype system (Plan 118 D216) — no compiler special-case required. Migration:ptr→*();0 as ptr→0 as *();type X(ptr)→type X(*()).nova checkemitsE_TYPE_UNKNOWN(typeptris removed — use*()…) onptr/nova_ptrin type position, with a migration hint (type ptr = *()user-level alias). A defensive codegen-time error mirrors the same message if a use ever bypasses the checker.Pointer types after Plan 134:
*()— opaque pointer (pointer to unit type =void*in C)*T— typed pointer to T (read-only pointee by default, D216)*mut T— typed pointer to mutable T (writable pointee)*unsafe T— pointer to possibly-uninit T (pointee contracts off; pointer itself non-null — nullable =Option[*unsafe T], Plan 138.5 §1/§V2.4)⚠️ AMENDED by Plan 118 (D216) —
ptrredefined astype ptr Option[*unsafe ()]newtype над nullable unsafe void pointer (D216 §11). ABI preserved (singlevoid*); semantics formalized as nullable Option (NPO emits NULL).null ptrliteral retracted (E_NULL_PTR_RETRACTED_USE_OPTION); migrate кNone.Closes followup [M-115-null-ptr-to-option-after-npo] от Plan 115. See Plan 118 §«ptr redefine» и D216 §11. Existing usages (handle pattern, tuple FFI returns) — no migration required (semantically equivalent post-amend).
Plan 91.12 V2 amend (2026-06-02): generic tuple-newtype
type X[T](ptr)now supported (was V1-limited to non-generictype X(ptr)). См. §«Generic opaque handle» ниже. Closes[M-115-newtype-constructor-generic].⚠️ AMENDED by Plan 118 Ф.5.7 (A23) 2026-06-02 —
null ptrliteral hard-retracted (E_NULL_PTR_RETRACTED_USE_OPTION). After Ф.5 NPO codegen (A19/A21),Option[ptr]/Option[*T]provide null-safety через type-system (single-pointer NPO layout, NULL=None convention).null ptrliteral становится redundant и ambiguous (Some(null ptr) indistinguishable от None под NPO). Migration:null ptr→(0 as ptr)(mechanical, NULL=(void*)0 в C ABI) либоOption[ptr] = Noneдля new code. Closes followup[M-115-null-ptr-to-option-after-npo].
Что
Foundational FFI infrastructure для bindings к произвольным C libraries
(libsqlite3, libpng, libcurl, etc.) без участия compiler-team.
Три компонента:
ptrbuilt-in primitive type — opaque pointer-sized integer, ABI-эквивалентенvoid*в C.- Tuple-by-value returns в
external fn— multi-value через struct-return calling convention. - Opaque handle pattern через
type X(ptr)(D52 tuple newtype) — compile-time-distinct typed wrappers.
1. ptr built-in primitive type
ro p ptr = null ptr // NULL pointer literal
ro q ptr = some_external_fn() // получили ptr из FFI
if p == null ptr { /* handle NULL */ } // null check
ro as_int = q as u64 // explicit cast → integer
ro back_to_ptr = (0x1000 as ptr) // explicit cast int → ptr
Семантика
- Size.
int/intptr_t(8 bytes на 64-bit). Bootstrap таргетит только 64-bit платформы (Linux x86_64, Windows x64, macOS ARM64/x86_64). Note:usizeудалён (Plan 133) — pointer-sized =int. - ABI.
void*в C. Передаётся в registers по платформенному ABI; identity при passing через external fn boundary. - Opaque. Nova не имеет
*pderef-операции (никогда не было), нет field/method access наptr. Только comparison + cast + pass-through. - Default value.
null ptr(bitwise 0). Zero-init valid и обозначает «нет указателя». - Equality.
==/!=— bitwise pointer comparison (стандартная C semantics). - Casts.
ptr as u64/ptr as i64— извлечь integer representation.u64 as ptr/i64 as ptr— собрать ptr из integer (для opaque handle storage).ptr as ptr— no-op (identity).
- Arithmetic banned.
ptr + N,ptr - ptr,ptr * 2—E_PTR_ARITHMETIC_BANNED. Pointer arithmetic — unsafe operation, отложен на followup ([M-115-ptr-arithmetic]). - GC. Conservative GC сканирует pointer-sized слоты как potential
references —
ptrслот с GC-allocated адресом будет pin’ить allocation (defensive, correct).ptrслот с non-GC адресом (e.g. sqlite3 handle) не tracs ничего (адрес вне GC arena). Зачем это работает: Boehm-style conservative collector реагирует только на адреса внутри tracked heap. - Memory ownership. FFI domain — user responsibility.
ptr, returned’ый из C library, должен быть освобождён matching C-side call (sqlite3_close,png_destroy_read_struct, etc.). Pattern: типизированный handle +consume close()метод на Nova-wrapper.
null ptr литерал
ro p = null ptr // valid expression
if p == null ptr { ... } // null check
Синтаксис: keyword null + type-name ptr. Two-token literal, parser
expects null followed by ptr ident. Распространение синтаксиса на
другие pointer types (Plan 118 *T family) — null *T — спроектировано
forward-compatible, но не реализуется в V1.
V1 ограничение: только null ptr valid. null int, null str, null SomeRecord — E_NULL_LITERAL_REQUIRES_PTR.
⚠ INTERIM construct (Plan 115 V1 only).
null ptrдублирует функциональностьNoneизOption[T](sum-type из D-блока Option/ Result). Идиоматический Nova-путь —Option[ptr]с явнойNone/Some(p)диспозицией и compiler-enforced null check’ом.Почему
null ptrсуществует в V1.Option[ptr]в bootstrap представлен какNovaOpt_nova_ptrstruct (tag + value) — НЕ ABI-совместим с rawvoid*из C library. FFI shim пришлось бы оборачивать pointer’ы в Option struct’у — лишний overhead + struct return convention вместо register return.null ptr= bitwise 0 = идентично CNULL→ zero-cost FFI.Plan 118 NPO (Null Pointer Optimization). После Plan 118 V2 добавит
Option[*T]с NPO codegen —Noneпредставляется как bitwise 0,Some(p)какp. Zero-cost + type-safe + ABI-compatible одновременно. См.[[project-plan118-status]]§«Option[*T] NPO codegen».После Plan 118 landed:
null ptrполностью удаляется — retract из spec, parser emit’итE_NULL_LITERAL_REPLACED_BY_OPTIONс migration hint кOption[ptr] / None. См. marker[M-115-null-ptr-to-option-after-npo]вdocs/simplifications.mdдля migration tracking.
Type-checker rules
| Операция | Результат | Diagnostic |
|---|---|---|
null ptr | Ty::Ptr | — |
ptr == ptr / ptr != ptr | bool | — |
ptr == null ptr | bool | — |
ptr as u64 / ptr as i64 | integer | — |
u64 as ptr / i64 as ptr | ptr | — |
ptr as ptr | ptr | no-op |
ptr + N / ptr - ptr / etc. | error | E_PTR_ARITHMETIC_BANNED |
ptr.field / ptr.method() | error | E_PTR_NO_MEMBER (нет деf членов на opaque) |
int as ptr (для int = i64-style) | ptr | — (transparent через i64 path) |
ptr as int | int | — |
ptr в record-field | OK | — (storage в struct slot) |
ptr distinct от i64/u64/int на type-check уровне (нельзя смешать
без cast’а). Distinction enforced через отдельный Ty::Ptr variant.
2. Tuple-by-value returns в external fn
external fn nova_sqlite3_open(path str) -> (Sqlite3Handle, i64)
// ↑ ↑
// handle error code
Соответствующий C shim:
typedef struct {
void* _0; // handle slot
int64_t _1; // error code slot
} Nova_Sqlite3OpenResult;
Nova_Sqlite3OpenResult nova_sqlite3_open(nova_str path) {
sqlite3* db;
int rc = sqlite3_open(path.data, &db);
return (Nova_Sqlite3OpenResult){ db, (int64_t)rc };
}
ABI rules
- Layout Nova tuple type
(T1, T2, ..., Tn)↔ C struct{ T1 _0; T2 _1; ...; Tn _{n-1}; }. Element order preserved, no padding inserted beyond what C compiler emits по target ABI. - Mangling. Compiler emits
_NovaTuple_<arity>_<elem_mangles>typedef (Plan 59 mechanism, существующий — переиспользуется). C-side shim должен иметь struct с тем же layout (struct typedef name произвольное — ABI layout совпадает). - Calling convention — определяется C компилятором на target платформе:
- Sys V AMD64 (Linux, macOS x86_64): structs ≤ 16 bytes (2 GPR) →
return через
%rax:%rdxregisters. Bigger → caller passes hidden out-pointer в%rdi. - AArch64 (macOS ARM64, Linux ARM64): structs ≤ 16 bytes →
X0:X1registers. Bigger → hidden out-pointer. - Win x64 MSVC: structs ≤ 8 bytes →
RAX. Bigger → hidden out-pointer вRCX, shifting all other args.
- Sys V AMD64 (Linux, macOS x86_64): structs ≤ 16 bytes (2 GPR) →
return через
- Compiler responsibility. Codegen эмитит struct return-type declaration; платформенный C compiler делает rest. Nova не пытается override calling convention — соответствие platform ABI делегировано toolchain.
- Element type compatibility. Tuple elements должны быть:
- Primitives (
int/i32/etc.,f64,bool,u8-u64,ptr), - Newtype handles (
type X(ptr)), - Pointer-like types (
str— actually{ data ptr; len u64 }layout-equivalent struct), - Other tuples (nested struct return) — supported, transitive.
- Primitives (
- Прохибиции (V1). Elements типа
[]T(NovaArray pointer), Option, Result, sum-types — не рекомендуется, т.к. GC-tracked layouts. Pass them отдельно через out-params (если действительно нужно) или переупаковывайте в opaque handle. Followup[M-115-tuple-gc-types]— formal V2 support.
Layered FFI pattern
LAYER 1 Public Nova API (Database.open)
↓
LAYER 2 Nova wrapper (construct typed handle from raw)
↓
LAYER 3 external fn declaration (typed handle + tuple return)
external fn nova_sqlite3_open(path str) -> (Sqlite3Handle, i64)
↓
LAYER 4 C shim (~5-10 lines per fn — adapts out-param convention → struct)
Nova_Sqlite3OpenResult nova_sqlite3_open(nova_str path) { ... }
↓
LAYER 5 Actual C library (libsqlite3.so / sqlite3.dll)
int sqlite3_open(const char* path, sqlite3** db_out);
Layer 4 (shim) — единственное место «где Nova ABI встречается с C library ABI». User пишет один раз per fn. ~5-10 строк per shim.
3. Opaque handle pattern через type X(ptr) (D52 tuple newtype)
type Sqlite3Handle(ptr) // typed wrapper
type PngImageHandle(ptr)
type CurlEasyHandle(ptr)
// Construct
ro h = Sqlite3Handle(some_raw_ptr)
// Destructure inner ptr (used rarely; usually pass-through)
ro raw_ptr = h.0
// Type safety: distinct types prevent mixing
fn close_sqlite(h Sqlite3Handle) -> i64 { ... }
ro png = PngImageHandle(other_raw_ptr)
close_sqlite(png) // ✗ E_TYPE_MISMATCH — PngHandle ≠ Sqlite3Handle
Семантика
- D52 tuple newtype (
type X(Y)) — existing mechanism, leveraged как-есть. Никаких новых parser/checker rules для handle pattern — он buisness layer convention, не language feature. - ABI. Newtype = transparent wrapping. C-level Sqlite3Handle ≡ ptr ≡
void*. Zero runtime overhead. - Distinct type. Compile-time check
Sqlite3Handle ≠ PngHandle ≠ ptr— нельзя передать без явного wrap/unwrap. - Construct:
Sqlite3Handle(ptr_value)— standard tuple constructor. - Destructure:
handle.0— D52 tuple field access.
Generic opaque handle — type X[T](ptr) (Plan 91.12 V2, 2026-06-02)
Generic newtype над ptr поддерживается для type-parameterized FFI
handles (phantom T для compile-time discrimination):
type Region[T](ptr) // generic phantom T
type RegionKind = Persistent
type RegionKind = Transient
// Distinct types at compile-time, identical ABI at runtime
ro p = Region[Persistent](some_ptr)
ro t = Region[Transient](other_ptr)
// fn drop_persistent(r Region[Persistent]) — нельзя передать Region[Transient]
// Multi-param OK
type DualHandle[T, U](ptr)
ro h = DualHandle[int, str](raw)
Семантика. T параметр — type-system fiction; C-level ABI identical
(Nova_Region ≡ nova_ptr). All monomorphizations share typedef.
Codegen emit’ает single typedef nova_ptr Nova_X; (не per-T), .0 access
- constructor — identity cast same как non-generic case.
Use case: phantom type discrimination для same-runtime-shape handles (prepared statement kinds, region/arena ownership classes, FFI buffer mutability flags, и т.д.).
Inner non-ptr types (Plan 91.12 V2 followup, 2026-06-02) — generic
newtype над любым primitive типом supported: type Counter[T](int),
type Tag[T](str), type Flag[T](bool), type Measure[T](f64).
Семантика идентична ptr-case: phantom T для compile-time discrimination,
single shared typedef над inner C type, zero runtime overhead. Use cases:
typed int counters, tagged strings (Email/UserId), tagged booleans
(Visible/Hidden), tagged floats (measurement units).
Inner uses generic param (type Wrap[T](T)) — REJECTED type-checker’ом
с [E_GENERIC_NEWTYPE_INNER_USES_PARAM]. Tuple newtype = transparent
typedef (shared C ABI across T’s); per-T storage variance — record-semantics:
// ✗ E_GENERIC_NEWTYPE_INNER_USES_PARAM
type Wrap[T](T) // inner depends on T → not newtype
// ✓ Correct migration to record form (per-T mono)
type Wrap[T] { value T } // properly mono'd по T
Closes [M-91.12-generic-newtype-non-ptr-inner].
consume close() cleanup convention
Recommended pattern для handle types с resource ownership:
type Database { ro handle Sqlite3Handle }
fn Database.open(path str) Fail[DbError] -> Database {
ro (h, rc) = nova_sqlite3_open(path)
if rc != 0 { Fail.throw(DbError.OpenFailed(rc)) }
Database { handle: h }
}
fn Database consume @close() -> () {
nova_sqlite3_close(self.handle)
// Plan 100.4 defer machinery интегрируется автоматически:
// failable cleanup body допустим, ошибки propagate'ятся caller'у.
}
Combined с D90 defer / errdefer для automatic cleanup — leak-resistant
без runtime cost.
4. Coexistence с D126 external type
Plan 115 не retracts D126. Оба паттерна остаются valid:
| Pattern | Use case | Trade-offs |
|---|---|---|
D126 external type X | stdlib internals (Nova-team владеет C struct) | Tighter integration; C-side knows Nova types; no .0 boilerplate |
D214 type X(ptr) | user FFI к third-party libs ИЛИ stdlib opting in | Universal; C-side не знает Nova internal layouts; .0 для inner access |
Recommendation. Stdlib мигрирует на Plan 115 pattern для consistency
с user-FFI conventions (Plan 91.12 amend в Pattern B). D126 deprecation —
followup [M-115-d126-deprecation] после migration audit.
Diagnostic codes
E_PTR_ARITHMETIC_BANNED— попытка арифметики наptr(V1 banned).E_PTR_NO_MEMBER— попыткаptr.field/ptr.method()—ptropaque.E_NULL_LITERAL_REQUIRES_PTR—null Tгде T ≠ ptr (V1 ограничение; Plan 118 expand для*T).E_PTR_CAST_INVALID_TARGET—ptr as Tгде T ≠ {i64, u64, int, ptr} — string/float/bool casts не имеют semantic meaning для opaque pointer.
Implementation notes
- Parser добавляет
"ptr"вis_primitive_typeallowlist (дляptr.method/ static-dispatch namespace).null ptrliteral — special case вparse_atom/parse_primary. - Type-checker добавляет
Ty::Ptrvariant;ty_of_refmapping"ptr" => Ty::Ptr; arithmetic / member access reject hooks. - Codegen добавляет
"ptr" => "void*"mapping вtype_ref_to_c;null ptr→((void*)0); cast emissions((void*)(uint64_t)(...))для int→ptr;((uint64_t)(...))для ptr→int. - GC — no changes. Conservative GC handles
void*слоты by-default. - Tuple FFI — leveraging existing
_NovaTuple_*mono’d struct pipeline (Plan 59 mechanism). C-side shim author writes matching struct typedef с теми же elements.
Mainstream comparison
| Язык | Opaque pointer type | Typed wrappers |
|---|---|---|
| Rust | *mut c_void / *const c_void | struct H(*mut c_void) |
| Zig | *anyopaque / ?*anyopaque | const H = opaque {}; *H |
| Go | unsafe.Pointer | type H = unsafe.Pointer |
| Haskell FFI | Ptr () | newtype H = H (Ptr ()) |
| OCaml ctypes | unit ptr | type h = unit ptr |
| Python ctypes | c_void_p | subclass c_void_p |
| Java JNI | jlong | (just long) |
| .NET P/Invoke | IntPtr / nint | struct H { IntPtr h; } |
| Nova V1 | (нет) — нужны compiler hacks | — |
| Nova V2 (Plan 115) | ptr (built-in) | type H(ptr) (D52 tuple newtype) |
Nova V2 = Rust/Zig tier (typed wrappers без runtime overhead, opaque deref, arithmetic banned by default).
Use cases
- libsqlite3 binding (
type Sqlite3Handle(ptr),type Sqlite3StmtHandle(ptr)). - libpng / libjpeg / libwebp image processing.
- libcurl HTTP client (Plan 117/118 prerequisite).
- rustls / OpenSSL TLS handles (Plan 116 prerequisite).
- Plan 91.12 std/net Pattern B migration (replaces D126 для TcpListener / TcpStream / UdpSocket если migration deemed worthwhile).
- Any third-party C library без Nova-team coordination.
Cross-ref
- D52 — tuple newtype
type X(Y)(leveraged). - D82 — external fn syntax (extended для tuple returns).
- D126 — external type (coexists; alternative pattern для stdlib internals).
- D54 —
as-cast operator (added ptr↔integer casts).
Acceptance
См. Plan 115 A1-A10 (T1, T2, T3 series).
D216. Typed pointer family + unsafe model + null-safety через NPO
Коллизия номера разрешена 2026-07-03: второй блок «D216 Generic anonymous tuple monomorphization» перенумерован в D354; typed-pointers сохраняет D216 (несёт цепочку V2/V3-амендментов и сеть ссылок D246/118.x/174.5). Plan 118 (typed pointers + unsafe model). Status: 🟢 ACTIVE 2026-06-02 (Ф.0 + Ф.1.5 + Ф.2 scaffold + Ф.3 + Ф.3.2 + Ф.3.3 + Ф.3.5 + Ф.4 partial + Ф.5 partial + Ф.6 partial — 13 acceptance criteria closed).
D216 АМЕНДМЕНТ «всё через методы» (Plan 174.5, 2026-07-09, решения владельца 2026-07-06 — таблица §3 плана): value-доступ и адресная арифметика указателей — ТОЛЬКО unsafe-методы; операторные формы РЕТРАКТИРОВАНЫ ошибкой
E_POINTER_OP_USE_METHOD(§6/§8 этого блока в операторной части устарели):
Метод Семантика p.read() -> T/p.write(v T)голый deref *(p)(D141; заменяют*p)p.read_at(i) -> T/p.write_at(i, v)*(p+i)(заменяютp[i]); write_at — единая точка write-cap (E_POINTER_RO_ASSIGN)p.offset(n) -> *Tадресная арифметика element-units (заменяет p±i); тип НЕ деградируетp.dist(q) -> intsigned element count (заменяет p-q; порядок = знак dist,p<qретрактирован)p.read_unaligned()/p.write_unaligned(v)memcpy-семантика (невыровненный доступ) p.read_volatile()/p.write_volatile(v)volatile p.write(v *T) -> *mut Tкопия из указателя-источника (без value-копии) p.copy_from/copy_to(src, n)memmove; _nonoverlapping— memcpy (обёртки RawMem byte-level)ОСТАЮТСЯ операторами:
p == q/!=, auto-derefp.field/p.method()(one-level),p as *U(cast; unsafe при U≠T).[]-индексация — ТОЛЬКО безопасные контейнеры (D138), у указателей её нет.wrapping_offsetотложен ([M-174.5-wrapping-offset-deferred]). Ретрактированы:*p,*p=v,p[i],p[i]=v,p±i,p-q,p</<=/>/>=q. Conformance: d174_5_* pos+neg (8 neg — по одному на форму). Эталон 79/0.D216 §4 AMEND (Plan 118.6, 2026-06-17):
&xsafe for all types (nounsafe {}required for promote path).addr_of()/addr_of_mut()retired →E_ADDR_OF_REMOVED.mutbinding →*mut Tauto;robinding →*Tauto. Escape analysis extended to primitives. 15/15 tests PASS.D216 §4 AMEND 2 (Plan 118.7, 2026-06-18):
raw &x— новый унарный оператор для сырого стек-адреса без escape analysis / auto-promote. Требуетunsafe {}контекст (E_UNSAFE_REQUIRED).raw— контекстное ключевое слово (не зарезервировано в lexer, аналогbench/measure).Инвариант после 118.7:
&x— всегда safe + escape analysis + auto-promote. Работает везде.raw &x— всегда сырой стек-адрес, без промоута. Только вunsafe {}.unsafe { &x }— эквивалентен&x(unsafe-контекст не влияет на&).Дополнительные диагностики:
E_UNSAFE_REQUIREDтеперь также дляraw &xвне unsafe (§4 amend 2).E_AMP_LITERAL/E_AMP_RECORD_LITERAL/E_ARRAY_INDEX_PTR_BANNED— применяются и кraw &expr(те же lvalue-ограничения).4/4 plan118_7 tests PASS. Migration: 7 файлов
unsafe { &x }→unsafe { raw &x }.Enforced diagnostics (V1):
E_UNSAFE_REQUIRED(D216 §8) — A8 ✅ commit 5c0d2c975ceE_UNSAFE_CALL_REQUIRES_WRAP(D216 §9) — A11 ✅ commit abd4be4603bE_CALLBACK_THROWS_OVER_C_ABI(D216 §10/§20) — A25 ✅ commit e4cff57142eE_EXTERNAL_FN_FAIL_EFFECT(D216 §20) — A26 ✅ commit 7ff3007f3afE_REALTIME_POINTER_OP(D216 §20 + D172 cross-ref) — A33 ✅ commit 6752565f453E_INVALID_POINTER_MODIFIER(D216 §1) — commit 6d6a18a2ab7E_AMP_LITERAL/E_AMP_RECORD_LITERAL/E_ARRAY_INDEX_PTR_BANNED(D216 §4 amend + §15) — commits d9d3084ed69 + 986fdb04c0d + 7d61617bcf8E_ADDR_OF_REMOVED(D216 §4 amend, Plan 118.6) —addr_of/addr_of_mutretiredRemaining Session 4+ work (V1.1):
- Ф.4 full auto-deref codegen integration (A12-A17)
- Ф.5 NPO codegen (A19-A23 + closes [M-115-null-ptr-to-option-after-npo])
- Ф.6 full *fn cast checks (A24 — E_CLOSURE_HAS_ENV)
- Ф.7 W_UNSAFE_GC_TRIGGER + Debug fmt (A27, A28)
- Ф.8 cross-platform CI + ABI snapshot + perf bench (A31, A32)
- Plan 118.1/118.2/118.3 sub-plans
Cross-amend: D2 (unsafe keyword restored as effect-handler sugar), D214 (ptr redefined как newtype), D32 (
&valueis typed pointer construction, NOT Rust borrow), D52 (tuple newtypetype Handle(*T)canonical для FFI handles).
Что
Foundational language addition: typed pointer family *T + unsafe gating
model + NPO null-safety. Replaces ptr opaque-only model из Plan 115 V1
с typed alternative; backward-compatible через D214 amend.
Plan 118 family scope:
- Plan 118 core (этот D216):
*Tfamily + unsafe + NPO + escape +*fn+ GC honor-system - Plan 118.1 (D217): FFI memory intrinsics + C-string convention
- Plan 118.2 (D218): slice fat-pointer + MaybeUninit + ManuallyDrop
- Plan 118.3 (D219): pointer concurrency safety + AtomicPtr[T]
§1. *T family типов
✅ FINAL — три оси мутабельности (Plan 147 D246, 2026-06-12): flip-scan (flip-scan-draft) ОТКЛОНЁН adversarial-критикой (
*Tконтекстно-зависим → тип не самодостаточен). FINAL = L3 pointee-capability из ТИПА, позиционно-независимо (D246 ниже). Восстановлено*T ≡ *ro TУНИВЕРСАЛЬНО (во всех позициях: param/return/generic/alias/cast/field/local). Pointee-mut НЕ наследуется от binding (running-current/flip-scan убраны).*mut T— единственный опт-ин mut-pointee. Текст ниже про postfix-canonical / prefix-ban / Option-NPO сохраняется.
138.5 baseline (postfix pointee, prefix-ban) — KEPT под D246: указательный ТИП несёт pointee-мутабельность постфиксом, сразу после
*(*mut T/*unsafe T).*ro T— HARD ERRORE_REDUNDANT_POINTER_RO(избыточно:*Tуже ro; fix-it*T). Перепривязываемость самого указателя (p = other_ptr) — это L1 binding (ro= фиксирован,mut= переприсваиваемый, D36), НЕ часть типа и НЕ влияет на pointee-capability. Запрещены ВСЕ prefix-модификаторы перед*(mut * T/ro * T/unsafe * T) —E_POINTER_PREFIX_MODIFIER(§1 ниже). Nullable =Option[*T]только (NPO §7). Это ретрактит D216 V2 «outer pointer-mut как type-wrapper» (mut * T = Mut(Pointer(T))) и V3 propagation/safe-stopper машинерию — см. §V2.2/§V2.6/§V3.2/§V3.3/§V3.4 ниже.
*T— typed pointer; pointee ro (дефолт).*T ≡ *ro Tуниверсально (D246: pointee-capability из типа, НЕ наследуется от binding).*mut T— explicit mut pointee (postfix only) — единственный опт-ин на запись*p = ….*ro T→ HARD ERRORE_REDUNDANT_POINTER_RO(fix-it*T)
Flagship
*u8(ro-pointee) use-case (Plan 139; D246, 2026-06-12):str— Nova value-record lang-itemtype str value priv { ptr *u8, len int }. Полеptr *u8— указатель на иммутабельный UTF-8 буфер (immutability строки выражена типом указателя:*T= ro-pointee, не отдельной меткой;*ro u8избыточен →E_REDUNDANT_POINTER_RO).*u8(ro) гарантирует: нет write-path сквозьstr.ptr, поэтомуclone= shallow 16-байт handle-copy с общим буфером безопасен, а compile-time interning литералов (один общий rodata-буфер на distinct content) семантически невидим. ABI поля —T*в C (const uint8_t*), layout-идентично старомуnova_str. См. D26 MAJOR AMEND + D228 content-eq override.
Amend (Plan 139.2 Ф.0+Ф.2, 2026-06-12):
str { ptr, len }record-литерал + producer-миграция. str — declared lang-item, поэтому str type-методы (receiverstr⇒current_recv_type == "str", privacy type-based, не module-based — D220) конструируютstrvalue-record литераломstr { ptr: …, len: … }в своём модуле. Codegen НЕ эмититNovaValue_str(str ∈RUNTIME_DEFINED_TYPESskip-list), поэтомуstr{…}лоуэрится спец-кейсом вemit_record_litнапрямую в C compound-literal(nova_str){.ptr=(const uint8_t*)(…), .len=(int64_t)(…)}(без schema, без NovaValue-структуры). Внешние caller’ы по-прежнему ловятE_PRIV_FIELD_INIT(priv-поля). Это разблокировало миграцию producer-форм external-C → Nova-body:
@split(sep) -> ro []str— byte-scan, каждый сегмент = zero-copy sub-viewstr{ptr:@ptr+off, len}(raw-ptr арифметика подunsafe), push вVec[str];from_bytes_unchecked/from_bytes_lossy— читают(ptr,len)источника через публичные Vec-геттеры@as_ptr()/@len(), alloc(len+1)+memcpy+NUL наdata[len](D26 §3); lossy валидирует UTF-8 и заменяет невалид на U+FFFD;from_bytes_unchecked_steal(consume bytes []u8)— zero-copy reuse буфера приcap>len(NUL in-place), иначе alloc+copy. consume-обязательство закрыто новымVec[T] consume @into_raw() -> *mut T(инверсVec.from_raw_parts: потребляет Vec-обёртку, отдаёт сырой writable-буфер).Amend (Plan 139.2 Ф.3, 2026-06-12):
@concat/@compare→ Nova-body (operator-lowering ОСТАЁТСЯ на C — option (b)).@concat(other) -> strи@compare(other) -> intмигрированы из external-C в Nova-body:
@concat: alloc[]u8размера@len()+other.len(), копирует байты обоих операндов через@as_bytes()(zero-copy view), затемstr.from_bytes_unchecked(owned + NUL-term D26 §3). Байт-в-байт идентично Cnova_str_concat.@compare: byte-loop над@as_bytes()обоих операндов (как C strcmp / memcmp), length-aware tiebreak; возвращаетa_byte - b_byteна первом различии (u8 0..255 ⇒ тот же знак, что memcmp), иначеsign(@len() - other.len()). Идентично Cnova_str_compare(array.h:989). РЕШЕНИЕ по operator-lowering — option (b) (оставить C-fn для операторов): операторы+/</<=/>/>=/==/!=надnova_strлоуэрятся ОТДЕЛЬНО, НАПРЯМУЮ в Cnova_str_concat/nova_str_lt/ … /nova_str_eq(emit_c.rs, BinOp-armlty == "nova_str"), НЕ через method-dispatch. ПРЯМЫЕ method-вызовы (s.concat(t)/s.compare(t), Compare-протокол@compare(o)==0-synthesis,@plus-body,@replacechained.concat()) маршрутизируются в Nova-body (убраны"concat"/"compare"изstr_method_to_rt). Почему option (b), не (a) (роутить операторы через методы + retire C-fn): (1) perf — Cnova_str_concat= одинnova_alloc
- два
memcpy; Nova-body =with_capacity+ два byte-push-loop’а (по байту, с bounds-check на каждом push). Cnova_str_cmp= одинmemcmp; Nova-body = byte-loop сas int-конверсией на байт. Operator-формы — горячий путь (string building, sort-сравнения), C оптимальнее. (2) ортогональность — operator- lowering (BinOp codegen) и method-dispatch (str_method_to_rt/ Nova-body) — независимые механизмы; миграция тела метода НЕ требует трогать operator-arm, и наоборот. Чистое retirement C-fn потребовало бы СОВМЕСТНОЙ миграции обоих + perf-харнесс для подтверждения отсутствия регрессии — orthogonal, низкий приоритет. Дубль (Nova-body метод + C-fn для оператора) — приемлемая цена: C-fn’ы малы (inline), и они единственные горячие; метод-форма редка. См. reframed[M-139.1-operator-lowered-methods].Остаётся C только
@hash(SipHash-1-3 + crypto-seed, DoS-resistance — см.[M-139.1-hash-irreducible-crypto-seed]). 9/10 str-методов — Nova-body (@concat/@compareзакрывают Ф.3); operator-lowering+/</… — сознательно C (perf, option (b)).
*unsafe T— pointer к possibly-uninit T (pointee init/layout contracts off); также degraded-форма после арифметики (alignment/bounds gone)- Size: pointer-width (8 bytes на 64-bit; bootstrap = 64-bit only)
- ABI:
T*в C (compiler emits соответствующий C-type для FFI) - Validity: always non-null (compile-time invariant); nullable
variant —
Option[*T](NPO §7)
Pointee (L3) vs binding (L1) — две ОРТОГОНАЛЬНЫЕ оси (D246):
L1 binding (ro/mut перед именем) задаёт reassignability указателя (p = other_ptr) — и больше ничего про pointee. L3 pointee-capability — из типа,
позиционно-независимо: *T=ro (нельзя *p = …), *mut T=mut. Оси НЕ влияют друг
на друга — pointee-mut НЕ наследуется от mut-binding (flip-scan убран).
| Запись | L1 binding (p =) | L3 pointee (*p =) | Вердикт |
|---|---|---|---|
mut p *T | ✅ reassign | ❌ pointee ro | *p = … запрещён (E_POINTER_RO_ASSIGN) |
mut p *mut T | ✅ reassign | ✅ pointee mut | оба ✅ |
mut p *ro T | — | — | ❌ E_REDUNDANT_POINTER_RO (use mut p *T) |
ro p *T | ❌ фиксирован | ❌ pointee ro | оба ❌ |
ro p *mut T | ❌ фиксирован | ✅ pointee mut | p = …❌, *p = …✅ |
ro p *ro T | — | — | ❌ E_REDUNDANT_POINTER_RO (use ro p *T) |
*T ≡ *ro T (восстановлено D216 §V2.6; flip-scan-draft отклонён): pointee-mut
задаётся только через *mut T; *ro T избыточен → E_REDUNDANT_POINTER_RO.
Reassignability указателя — L1 binding (ro/mut, D36), независима от L3.
АМЕНДМЕНТ (2026-07-08, Plan 172.13, [M-redundant-param-ro-diagnostic]) —
избыточные модификаторы на границе fn, тот же принцип redundancy:
(а) явный ro в позиции параметра избыточен (параметры — ro-вид по
умолчанию, D176) → hard error E_REDUNDANT_PARAM_RO, обе синтаксические
формы: префикс (ro x T) и тип-модификатор x ro T (fix-it: x T);
комбинация V3-амендмента ro x mut T (ro-binding + явный mut content-view,
ортогональные оси Plan 118.5 V3) — НЕ избыточна, остаётся легальной;
(б) явный mut в позиции возврата избыточен (возвращённое значение —
собственность вызывающего, мутабельность решает его биндинг) → hard error
E_REDUNDANT_RETURN_MUT (fix-it: -> T); НЕ задевает -> *mut T
(L3 pointee-capability) и -> ro T (осмысленный readonly-view, oracle row D).
Тесты: conformance/neg/d246_redundant_param_ro_{prefix,type}_neg,
d246_redundant_return_mut_neg; позитив-граница d246_param_ro_mut_view.
Запрет prefix-модификаторов (E_POINTER_PREFIX_MODIFIER): токены
ro/mut/unsafe непосредственно перед * в type-position запрещены.
Расширяет E_INVALID_POINTER_MODIFIER (D216 §1, commit 6d6a18a2ab7).
mut * T // ❌ E_POINTER_PREFIX_MODIFIER — prefix перед *
ro * T // ❌ E_POINTER_PREFIX_MODIFIER
unsafe * T // ❌ E_POINTER_PREFIX_MODIFIER
Сообщение: «модификаторы указателя — на pointee (после *: *mut T/*ro T/
*unsafe T) или на binding (mut x *T); перед * не допускаются». Валидно:
*mut T/*ro T/*unsafe T/*T (pointee, postfix), mut name *T (binding).
*T // pointee ro (≡ *ro T, D246); pointee-mut из типа, не от binding
*ro T // ❌ E_REDUNDANT_POINTER_RO (fix-it: *T) — избыточно
*mut T // explicit mut pointee — единственный опт-ин на запись *p = …
*unsafe T // pointer к possibly-uninit T; deref требует unsafe layer
§2. Binding (L1) vs pointee (L3) — ортогональны (D246)
L1 binding (ro/mut перед именем) задаёт только reassignability указателя.
L3 pointee-capability — из типа (*T=ro / *mut T=mut), позиционно-независима,
НЕ наследуется от binding (flip-scan/current отклонены D246).
ro p *Acc // p фиксирован; pointee ro (*p = … ❌)
mut p *Acc // p reassignable; pointee ro (*p = … ❌ — L1 mut НЕ даёт mut-pointee)
mut p *mut Acc // p reassignable; pointee mut (*p = … ✅)
ro p *mut Acc // p фиксирован; pointee mut (*p = … ✅) — оси независимы
mut q = &acc // pointer mut auto (no &mut acc needed); pointee из контекста
Восстановление D216 §V2.6: mut-pointee при mut-binding НЕ автоматичен —
mut p *T даёт ro-pointee; writable pointee требует явного mut p *mut T.
Тип самодостаточен (C1): *T всегда ro независимо от позиции/binding. «One
canonical syntax» per смысл: ro-pointee — только *T, mut-pointee — только *mut T.
§3. Chain order (multi-level pointers)
Pointee-модификатор пишется постфиксом, сразу после каждого *, и относится
к target этого *-уровня; читается left-to-right.
*mut *ro Acc // mut pointer НА (ro pointer на Acc)
*ro *mut Acc // ro pointer НА (mut pointer на Acc)
Prefix-форма (mut * ro * Acc) запрещена — E_POINTER_PREFIX_MODIFIER (§1).
Canonical Rust grammar.
§4. &value operator + escape analysis с auto-promote
⚠️ D216 §4 AMEND (Plan 118.6, 2026-06-16):
&xis now safe (nounsafe {}required) for all types including primitives.addr_of()/addr_of_mut()retired →E_ADDR_OF_REMOVED. Escape analysis extended to primitives (previously only records). See amendment block at end of §4.
Safe pointer creation (no unsafe {} required, Plan 118.6+):
ro p = &x // *ro T — safe; heap-promote if &x escapes function scope
ro p = &y // *mut T if y is mut binding, otherwise *ro T
Raw stack pointer (unsafe required):
unsafe { ro p = &x } // no heap-promote; raw stack address; programmer's responsibility
Field address:
ro p = &x.field // chain root (x) is promoted to heap if pointer escapes scope;
// whole binding moves to heap, NOT individual field.
Heap-promote semantics:
- Compile-time static escape analysis decision (not runtime).
xstarts on stack; promoted to heap at its declaration point if address escapes function scope (return / closure / heap-field store / fn arg).- Conservative V1: promote if ANY uncertainty. Precise inlining followup:
[M-118-escape-precise]. - Primitives (
int,bool,f64, etc.) now subject to same escape analysis as records (Plan 118.6 extension).
Records (heap references): &record creates pointer to the reference.
Result C type: Nova_Record** (double-pointer because record is already
Nova_Record* in C ABI). Used primarily for FFI out-params:
external fn try_init(out *Acc) -> i64 — C side fills *out.
&Record { ... } literal без named binding forbidden —
E_AMP_RECORD_LITERAL. Anonymous-local auto-promote from temporary
слишком implicit для production-grade reader clarity. Required pattern:
// ❌ implicit anonymous local
ro p = &Acc { name: "Piter" }
// ✓ explicit named local
ro acc = Acc { name: "Piter" }
ro p = &acc
D32 amend rationale: &value — typed pointer construction, не Rust
borrow. Safety через escape analysis + auto-promote (не lifetime checker).
До Plan 118.6 дополнительно требовал unsafe {} block; amend снимает это
требование для safe-promote path. Raw stack pointer остаётся unsafe.
D216 §4 AMEND (Plan 118.6, 2026-06-16):
&xsafe for all types (nounsafe {}for the promote path).addr_of(x)/addr_of_mut(x)retired →E_ADDR_OF_REMOVED. Use&xinstead.- Escape analysis extended to primitives.
E_UNSAFE_REQUIREDis NOT triggered by AddrOf starting Plan 118.6 (only by Deref*exprand unsafe fn calls).
addr_of / addr_of_mut builtins (Plan 118.1 Ф.3 closeout, 2026-06-05) — RETIRED Plan 118.6
— RETIRED Plan 118.6 (2026-06-16).
Use addr_of(x) / addr_of_mut(x)&x instead. Calling addr_of(x) or addr_of_mut(x) now emits
E_ADDR_OF_REMOVED. History preserved below for reference.
Historical (Plan 118.1 Ф.3, 2026-06-05 — Plan 118.5): Zig-style builtin function aliases для
&x(UnOp::AddrOf). Identical codegen path; rewriter-desugared в const_fn_eval pass. Used when explicit function-call syntax preferred over operator syntax (FFI patterns).Retired in Plan 118.6 —
&xis now safe and universally preferred.E_ADDR_OF_REMOVEDfires on any remaining call site.
Enforcement (same as UnOp::AddrOf):
E_UNSAFE_REQUIRED — outside unsafe {} block(AddrOf no longer triggers this; only Deref and unsafe fn calls do, starting Plan 118.6)- E_REALTIME_POINTER_OP — inside #realtime fn
- E_AMP_LITERAL / E_AMP_RECORD_LITERAL — invalid lvalues (literal / record literal)
- E_ARRAY_INDEX_PTR_BANNED — operand is, or its field-access chain passes through,
an array index (
arr[i]/arr[i].field) — unstable base (buffer resize / GC compaction), D216 §15 - E_ADDR_OF_NON_LVALUE — the operand’s field-access chain roots in an rvalue
(call result, arithmetic, …). The lvalue check walks the WHOLE chain to its
root (Plan 118.1 [M-118.1-addr-of-chains], amended 2026-06-08):
a.b.c/(*p).f/self.x.yrooted in a named local orselfare accepted, whilemake().f/arr[i].f/(x+1).fare rejected (previously only the top operand node was inspected, so chains rooted in temporaries were wrongly accepted — a dangling-pointer gap).addr_of(x)and&xshare one walker (ast::addr_of_chain_root), so the intrinsic and operator forms agree, andaddr_of(*p)≡&(*p)(the walker descends an explicit deref to the pointer root, matching thep.fauto-deref sugar). - E_ADDR_OF_MUT_REQUIRES_MUT_BINDING —
addr_of_muton a binding whose root is notmut; the mut-check also walks the field-chain root (addr_of_mut(s.field)requiresmut s), not just a bare Ident (NEW).
Known V1 gaps (2026-06-08 followups): (1) CLOSED Plan 118.6 —
addr_of_mut((*p).field) is not yet
gated on p being *mut/mut-bound — the mut-check skips deref roots and the
desugar is a bare UnOp::AddrOf with no *mut castaddr_of_mut retired; &x with mut binding auto-infers *mut T.
(2) The MOOT — addr_of(...) intrinsic chain-check runs in the const-fn rewrite pass, so
nova check/LSP does not surface itaddr_of retired Plan 118.6;
&x chain-check runs at check-time.
([M-118.1-addr-of-chains-checktime] — closed by retirement).
Closes [M-118.1-addr-of-macros] (was: «add addr_of! macro» — macro framework not shipped, builtin-fn alternative landed).
Closes [M-118.1-addr-of-mut-deref-ptr-mut] (Plan 118.6 — addr_of_mut retired; &x mut-inference covers this).
§5. Auto-deref
unsafe {
p.field // ✓ auto-deref one-level read
p.method() // ✓ auto-deref method call (one-level)
p.field = v // ✓ auto-deref assignment (requires *mut T)
*p // ✓ explicit deref read
*p = v // ✓ explicit assignment (requires *mut T)
(*p).field // ✓ multi-level chain через explicit *
}
Rules:
| Op | *ro T | *mut T | Notes |
|---|---|---|---|
p.field read | ✓ | ✓ | auto-deref one-level |
p.field = v | ❌ E_POINTER_RO_ASSIGN | ✓ | requires *mut |
p.method() (ro recv) | ✓ | ✓ | auto-deref |
p.method() (mut recv) | ❌ E_POINTER_RO_MUT_METHOD | ✓ | requires *mut |
*p read | ✓ | ✓ | yields T |
*p = v | ❌ E_POINTER_RO_ASSIGN | ✓ | requires *mut |
One-level only для auto-deref (Go-style); multi-level requires explicit
(*p).field chain. Только в unsafe context — все pointer ops gated.
Pattern match Option[*T] — safe outside unsafe (inspection, не deref).
§6. Pointer arithmetic + order comparison
unsafe {
ro p1 = some_ptr + 1 // *unsafe T (degraded)
ro p2 = some_ptr + offset
ro diff = p2 - p1 // int (element count, signed)
unsafe { *p1 } // *unsafe T deref требует ещё unsafe layer
ro lt = p1 < p2 // order-compare allowed inside unsafe
}
// Equality `==`/`!=` — safe anywhere (identity check, no ordering):
ro p = unsafe { &x }
ro q = unsafe { &x }
ro same = p == q // OK outside unsafe — identity check
+/-/+=/-=only вunsafe { }block- Result
*unsafe Tдляptr ± int;intдляptr - ptr(signed element count) - Units: sizeof(T)-scaled (C/Rust convention)
*///etc. —E_PTR_ARITHMETIC_INVALID(не математически осмыслено)- Order compare
<,<=,>,>=— require unsafe context —E_PTR_ORDER_COMPARE_REQUIRES_UNSAFEACTIVE 2026-06-02 (V1 syntactic, commit 601af30fc30) — closes acceptance A17 partial. Rationale: pointer addresses не stable ordinals (GC-relocation invariant + OS ASLR random layout). V2 (Session 4+): full type-aware enforcement черезinfer_expr_type. - Equality
==/!=— safe everywhere (identity check; OK outside unsafe).
Nova codegen note (Plan 131, 2026-06-08): For typed pointer
*mut T, the expressionptr + nemits(ptr + n)in C — the C compiler scales bysizeof(T)automatically (standard C pointer arithmetic).*(ptr + n) = vemits an lvalue deref-write.p as *mut Temits(T*)(p)reinterpret cast. This is the foundation Vec[T] uses for its element buffer (see D232).
§7. Null safety: Option[*T] + NPO codegen
*T — non-null guaranteed. Option[*T] — nullable через NPO codegen.
Status: ACTIVE 2026-06-02 (Plan 118 Ф.5 V1 landed, commit 6b90e698437). Closes acceptance A19 ✅ (sizeof verification + struct layout).
- Layout: single pointer (8 bytes), не tagged struct (16 bytes)
// NPO-eligible: pointer-typed inner (c_ty ends_with('*')) typedef struct NovaOpt_const_Nova_X_p { const Nova_X* value; } ...; // Non-NPO: scalar/composite inner — tagged form retained typedef struct NovaOpt_nova_int { int tag; nova_int value; } ...; - Construction:
Some(p)→{.value = p};None→{.value = NULL} - Pattern match:
if (ptr == NULL) None_branch else Some_branch(ptr) - Direct C-FFI compatible (matches
malloc/fopen/dlopenreturns)
V1 detection (c_ty.ends_with('*')): covers *T family (T*,
const T*, void*), pre-existing stdlib pointer types (UdpSocket,
TcpStream, SocketAddr, TcpListener, File, etc — все benefit automatically).
V2 detection (Plan 118 Ф.5.4 ACTIVE 2026-06-02, commit cd168a4d53b):
Option[ptr]—nova_ptrtypedef (= void*) — now NPO-eligible (A21 partial closes). Plan 115 backward-compat preserved.
V3 detection (Plan 118 Ф.5.8 ACTIVE 2026-06-02, commit 9fe42f39c51):
Option[X]гдеtype X(*T)/type X(ptr)— newtype-over-pointer transparent typedef. LookupNova_Xв type_aliases (registered emit_type_decl); if underlying alias_c ends ’*’ or == “nova_ptr”, NPO-eligible. Closes A20 ✅. Note: для canonical Plan 115 pattern Nova type system pre-collapses к underlying (nova_ptr) — V2 branch fires directly; V3 defensive coverage для future paths.
V4 detection (Plan 118 Ф.5.9 ACTIVE 2026-06-02, commit 3725af23fcd):
Option[Option[*T|ptr]]nested — emitsW_OPTION_DOUBLE_NESTEDwarning через lint framework (lints.rs lint_option_double_nested). Outer Option uses tagged fallback (correctly — inner c_ty = struct); semantically ambiguous (None vs Some(None)). Closes A22 ✅.
V5 detection (Plan 118 Ф.5.10 ACTIVE 2026-06-02):
Option[*fn(...)]— function pointer types. После audit type_ref_to_c lowering:TypeRef::Pointer(modif, TypeRef::Func{...}, _)→void**(Func →void*, outer Pointer adds another*). c_ty ends with*→ V1 detection (Ф.5 A19) ALREADY triggers NPO. Closes A21 remainder ✅ через existing infrastructure без code changes. Test fixture t5_7_npo_option_fn_pointer_ok verifies.
All Ф.5 NPO acceptance criteria CLOSED: A19 ✅, A20 ✅, A21 ✅, A22 ✅, A23 ✅.
Other deferred (Session 4+):
— A23 ✅ CLOSED 2026-06-02. D214 amended; parser emitsnull ptrliteral retractionE_NULL_PTR_RETRACTED_USE_OPTION; 14 fixtures migrated к(0 as ptr). Closes[M-115-null-ptr-to-option-after-npo].
external fn malloc(sz int) -> Option[*u8]
// → C: uint8_t* malloc(size_t); (codegen casts intptr_t→size_t)
unsafe {
match malloc(1024) {
Some(buf) => use(buf), // buf: *u8 non-null
None => Fail.throw(OutOfMemory),
}
}
NPO applies к:
Option[*T]всех вариантовOption[*fn(...) -> ...]Option[ptr](D214 amend)Option[NewtypeOверPtr]гдеtype X(*T)/type X(ptr)
Excluded: nested Option[Option[*T]] — fallback к tagged repr +
W_OPTION_DOUBLE_NESTED warning.
§8. unsafe { } block
- Pointer ops require unsafe context (compile-time gating через
E_UNSAFE_REQUIRED) - Implementation: sugar над
with unsafe_handler { perform UnsafeOps.* }(D2-consistent; см. D2 amend) unsafe_handler— built-in, не user-overridable (E_UNSAFE_HANDLER_BUILTIN_ONLY)- Effect не propagates up (encapsulates per fn — canonical Rust pattern)
Inside unsafe разрешено: &value, *p, p[i] (pointer index),
p.field, p.method(), p.field = v, pointer arith, int as *T,
</> compare, &record.field, calling unsafe fn, newtype construction
wrapping pointer.
ptr[i] pointer index (D216 §8, [M-118-ptr-index-unsafe], 2026-06-09):
ptr[i] ≡ *(ptr + i) — derefs the pointer without bounds guarantee.
Semantically identical к explicit *ptr deref, hence requires the same
unsafe context. E_UNSAFE_REQUIRED fired when ptr[i] is used outside
unsafe { } block or unsafe fn body. Detection: syntactic —
expr_is_typed_pointer(obj) (covers *T/*mut T/*unsafe T bindings via
ptr_vars frame OR explicit type-annotation *T on binding). Example
migration: unsafe { *(@data + i) } → unsafe { @data[i] } (more
ergonomic; enables C (data)[i] pointer-arithmetic emission which the C
compiler scales automatically by sizeof(T)).
Outside unsafe safe: type declarations *T, external fn declarations,
field read acc.next (where next *T), pattern match Option[*T],
==/!= compare, newtype declarations, p as int (hash hazard warning).
§9. unsafe fn keyword syntax (Plan 118.1.7 amend, 2026-06-09)
Plan 118.1.7 migrates from
#unsafeattribute tounsafe fnkeyword (type-consistent, per Plan 118.5 TypeRef::Unsafe + Plan 118.1.6*unsafe fnptr type).#unsafe fn→ hard errorE_UNSAFE_ATTR_DEPRECATED.
unsafe fn foo(...)— declares function of unsafe fn typeexternal unsafe fn foo(...)— external fn of unsafe type- Body of
unsafe fn— implicit unsafe context (pointer ops безunsafe { }wrap) - Call
unsafe fn— requiresunsafe { ... }wrap у caller (visual marker) —E_UNSAFE_CALL_REQUIRES_WRAPиначе - Type:
unsafe fn(Args) -> Ret(consistent with*unsafe fn(...)fn-ptr type, Plan 118.1.6) - No propagation up — каждая fn decides encapsulate or propagate
#unsafe fn/#unsafe external fn→E_UNSAFE_ATTR_DEPRECATED(hard error)
§10. *fn(...) function pointers
*fn(Args) -> Retdistinct отfn(Args) -> Retclosure- Cast
fn → *fn— captureless required (E_CLOSURE_HAS_ENVиначе) - Cast
*fn → fn— unsafe (wraps в captureless closure;E_CAST_RAW_FN_TO_CLOSUREбез unsafe) - Callback no-throw: Fn-with-Fail effect cast → *fn —
E_CALLBACK_THROWS_OVER_C_ABI(C ABI не propagates Nova exceptions) - External fn no-Fail:
external fn ... Fail -> ...—E_EXTERNAL_FN_FAIL_EFFECT - Calling convention: default C ABI текущей платформы (single ABI V1;
stdcall/vectorcall —
[M-118-stdcall-fn-ptr]followup) - Vararg —
E_VARARG_NOT_SUPPORTED([M-118-vararg-ffi]followup)
unsafe fn as part of fn-ptr type (Plan 118.1.6 closeout, 2026-06-08; amend Plan 118.1.7, 2026-06-09)
Function pointer тип encodes unsafe fn keyword:
*fn(...)— safe function pointer*unsafe fn(...)— unsafe function pointer (postfix pointee; prefixunsafe * fn(...)retired —E_POINTER_PREFIX_MODIFIER, Plan 138.5)
Coercion rules:
*fn → *unsafe fn: ✅ allowed (covariant — safe это «подмножество» unsafe)*unsafe fn → *fn: ❌ E_UNSAFE_FN_PTR_COERCION (нельзя «забыть» unsafe)
Call-site:
- Call через *unsafe fn ptr без unsafe { } → E_UNSAFE_CALL_REQUIRES_WRAP (mirrors direct
unsafe fncall).
addr_of propagation:
- addr_of(safe_fn) → *fn(…)
- addr_of(unsafe fn) → *unsafe fn(…) (тип propagated из FnDecl.unsafe_attr)
Rust precedent: fn() ≠ unsafe fn() — same model.
Закрывает [M-118.1.5-unsafe-fn-pointer-type].
ABI-тег fn-ptr: *extern "C" fn (Plan 174.6 M0 cross-amend, 2026-07-04)
*fn(...) / *unsafe fn(...) (выше) — Nova-ABI captureless fn-ptr: Nova-типы в сигнатуре
допустимы (Nova ABI их передаёт; «captureless» — про отсутствие env, не про типы). Для передачи
Nova-функции как настоящего C-callback введён C-ABI-тегированный fn-ptr тип
*extern "C" fn(...) — параллель к объявлению extern "C" fn (08-runtime.md#d282).
Типы его сигнатуры (параметры + возврат) обязаны быть C-ABI-совместимы (рекурсивный тип-лист,
D282 rule 2); коэрция fn → *extern "C" fn проверяет C-ABI + captureless +
effect-free/total (callback не должен объявлять никакого эффекта — C зовёт его без Nova-handler-фрейма
на стеке, поэтому любая effect-операция unsound; это обобщает Fail-специфичный гейт §20 /
E_CALLBACK_THROWS_OVER_C_ABI на все эффекты). Полная спецификация ABI-тега и обоснование условия (3) —
D353. Реализация (парсер/чекер/тесты) — Plan 174.6 M1–M3; M0 = только спека.
Полная cast/коэрция-матрица fn / *fn / *extern "C" fn (какой источник во что коэрцится и с какой
диагностикой) + правило «*fn и *extern "C" fn — разные типы, нет неявной конверсии» + легальность тега
в non-extern "C" fn позициях (Nova-fn-параметр / поле value-record) — D353 «Cast/коэрция-матрица»
(Plan 174.6 M2). Строка «Calling convention: default C ABI» выше (§10 список) относится к эмиссии fn-ptr
на платформе; ABI-тег на ТИПЕ (*fn = Nova-ABI против *extern "C" fn = C-ABI) — ортогональное
измерение, введённое здесь (полная ретракция формулировки §10 про «default C ABI» у bare *fn — остаток
[M-174.6-ffi-abi], семантически связан с дефолтным ABI *fn).
§11. ptr redefine (D214 amend cross-ref)
⚠️ RETIRED by Plan 134 (2026-06-09) — the
ptrbuilt-in name is fully removed; there is notype ptr Option[*unsafe ()]builtin anymore. The idiomatic opaque pointer is*()(pointer-to-unit =void*).ptr/nova_ptrin type position →E_TYPE_UNKNOWN(use*()). See the D214 SUPERSEDED banner. The note below is preserved for historical context only.
type ptr Option[*unsafe ()]
- ABI preserved (single
void*) null ptrliteral retracted (useNone); closes[M-115-null-ptr-to-option-after-npo]✅- Backward-compatible для existing
ptrusages (handle patterns, tuple FFI returns, etc.)
§11a. Typed pointer instance methods (Ф.4 V1, amend 2026-06-03)
Primitive-T typed pointer instance methods landed (V1 scope —
primitive T only, struct-T deferred):
| Method | Receiver | Returns | C codegen |
|---|---|---|---|
(*ro T).read() | any *T/*ro T/*mut T/*unsafe T | T | (*p) |
(*mut T).write(v T) | *mut T / *unsafe T | nova_unit | ((*p) = v, NOVA_UNIT) |
Detection: obj_ty ends в * AND not a known Nova typedef
(Nova_*/NovaArray_*/NovaOpt_*/NovaRes_*/NovaBox_*/NovaValue_*)
AND not void* / nova_ptr. is_const derived от const prefix on
obj_ty; controls write availability.
Safety convention: caller wraps в unsafe { ... } block. Enforcement
[M-118.1-unsafe-attr-on-external-fn] ✅ RESOLVED (Plan 118.1.5 — capability
ships; note: syntax later superseded by 118.1.7).
Diagnostic:
(*ro T).write(v)— currently emits generic “method not found” via fall-through to default dispatcher; typedE_PTR_WRITE_ON_RO_TARGETdeferred — followup[M-118.4-typed-ro-write-error]
Limitations (V2 follow-up):
- Struct
T(obj_tystarts сNova_) —read/writenot dispatched (deep copy + ownership semantics required) —[M-118.4-struct-ptr-read] - Pointer arithmetic (
p.add(n),p.offset(n)) —[M-118-ptr-arithmetic] - Volatile variants (
read_volatile/write_volatile) —[M-118.1-volatile-ops]
Closes followup [M-118.1-typed-pointer-instance-methods] для primitive
T scope.
§12. Casts
| From | To | Safe? |
|---|---|---|
*T | int | ✓ (см. hash hazard) |
int | *T | unsafe |
*ro T | *mut T | unsafe |
*mut T | *ro T / *T | ✓ |
*T | *unsafe T | ✓ |
*unsafe T | *T | unsafe |
*T1 | *T2 (T1≠T2) | unsafe |
fn → *fn | ✓ если captureless | E_CLOSURE_HAS_ENV иначе |
*fn → fn | unsafe | wraps |
*T | bool / f64 / etc. | ❌ E_PTR_CAST_INVALID_TARGET |
Hash hazard: p as int для GC-tracked objects + HashMap key →
W_PTR_AS_INT_GC_HASH_HAZARD (address can change via GC compaction).
Note: usize/isize removed (Plan 133) — use int for pointer-as-integer casts.
§13. Comparison
==/!=safe (identity check)</>/<=/>=unsafe (cross-allocation UB + moving GC concern)
§14. &record.field only в unsafe
GC compaction concern: address меняется при collection. Inside unsafe — user обещает no GC trigger (honor-system §16).
§15. Forbidden ops
&arr[i]всегда —E_ARRAY_INDEX_PTR_BANNED(array buffer может relocate via realloc / GC compaction)nullliteral —E_NULL_LITERAL_USE_NONE(useNone; one-way-to-do)undefined—E_UNDEFINED_USE_NONE_INIT_PATTERN(useOption[*T] = None + init; полноценныйMaybeUninit[T]— Plan 118.2)- Vararg calls —
E_VARARG_NOT_SUPPORTED
§16. GC honor-system
Контракт unsafe-блока: внутри unsafe { ... } user обещает no GC
trigger между pointer creation и use. GC trigger = heap allocation,
yield-point (await/spawn/supervised{}), string formatting which allocates,
#parks/#wakes fn calls.
Compiler warns: W_UNSAFE_GC_TRIGGER per violation site.
Silence: // noqa: W_UNSAFE_GC_TRIGGER comment marker.
Rationale V1:
- Boehm-style conservative GC не двигает объекты → V1 безопасно (warning = awareness, not error)
- Future moving GC → potрebует formal pin API (
[M-118-pin-api]followup) - Honor-system + warning = pragmatic trade-off (no runtime cost, spec contract clear, future-compatible)
§17. Pointer Debug formatting
Canonical form (Plan 91.14, D229 — 2026-06-05):
*Timplements Debug (sibling protocol — см. D229) через built-in@debug(sb StringBuilder)который emits hex address + type name ("0x7f... -> Account").- Canonical interpolation:
"${p:?}"— routes кDebug.@debug(debug semantics: diagnostic, machine-oriented). *TНЕ implementsDisplay— bare"${p}"остаётся ошибкой (forces explicit decision; pointer debugging = deliberate; addresses non-deterministic, leak ASLR info).
Legacy alias (backwards-compat):
(*T).to_debug_str() -> str— built-in method (in unsafe context only). Эквивалентlet sb = StringBuilder.new(); p.@debug(sb); sb.to_str(). Сохраняется для пред-D229 кода; новый код пишет"${p:?}".
Bare ${p} enforcement:
"${p}"interpolation →E_PTR_NO_DISPLAY_USE_DEBUG_STR— ACTIVE 2026-06-02 (V1 syntactic, commit a9327c65d3f) — closes acceptance A28 partial. V1 detects:- direct
${&x}/${*p}(Unary AddrOf/Deref) ${expr as *T}(cast к pointer type)${var}где var bound черезlet var = AddrOf/Deref/As(*T)V2 (Session 4+): full type-aware enforcement черезinfer_expr_type— fires на returned pointer values, field access, generic-bound*T.
- direct
- Hint в диагностике после Plan 91.14: «use
${p:?}for pointer debug formatting (Debug, D229)».
См. также: D229 —
Debug protocol + ${expr:?} format-spec syntax.
§18. FFI handle allocation contract
Production-grade guidance:
| Form | Allocation | ABI | When |
|---|---|---|---|
type Handle(*T) tuple newtype | stack | single pointer | opaque handles, no extra state |
type Handle(ptr) tuple newtype | stack | single pointer | untyped opaque handles |
type Handle { p *T, extra State } record | heap | pointer-to-struct | handle с extra state |
Canonical (zero-overhead):
type Sqlite3Handle(*sqlite3)
external fn open(path str) -> (Option[Sqlite3Handle], i64)
Plan 115 V1 cookbook examples (record form type Db { ro value ptr }) —
migrated к tuple newtype в Plan 118 Ф.9 ([M-118-handle-migration]).
§19. Function call argument passing
*Tparameters — pass by value (single pointer-word; standard C ABI)&valueat call site creates*Targument- Auto-promote applies к escape-via-fn-arg (conservative: ESCAPE always
for fn args; precise inlining
[M-118-escape-precise]followup)
§20. extern "C-unwind" story (NEGATIVE — not V1)
V1: external fn + *fn callbacks must not have Fail effect on Nova→C
boundary. Diagnostics: E_EXTERNAL_FN_FAIL_EFFECT,
E_CALLBACK_THROWS_OVER_C_ABI. Workaround: catch внутри callback, return
sentinel.
V2 — research extern "C-unwind" (Rust 2024 model);
[M-118-extern-c-unwind] followup.
§22. CStr type (Plan 118.1 Ф.4 closeout, 2026-06-05)
type CStr(*u8) newtype declared в std/ffi/cstr.nv — FFI-compatible C-string handle.
ABI: marshals к const char* / uint8_t* (single positional *u8 field).
Invariant: instances must satisfy ptr[strlen(ptr)] == '\0'. Per D26 §«Nul-termination»,
full Nova str already ships с this invariant, enabling zero-copy conversion.
Conversion methods (Plan 118.1 closeout amend, 2026-06-06): str → CStr
conversions реализованы как pure-Nova methods в std/ffi/cstr.nv:
export fn str @as_cstr() -> CStr {
ro bytes = @as_bytes()
for b in bytes {
if b == 0 { panic("as_cstr: embedded NUL byte in str (would truncate C-string)") }
}
unsafe { CStr(bytes.as_ptr()) }
}
export unsafe fn str @as_cstr_unchecked() -> CStr { // scan-free O(1) hatch
ro bytes = @as_bytes()
unsafe { CStr(bytes.as_ptr()) }
}
// @to_cstr() — owning always-copy form; NOT in V1, deferred to Plan 118.2.
Использует existing builtins: str.as_bytes() (D176 zero-copy view) +
[]u8.as_ptr() (Plan 118.2 Ф.1, commit e80a57e54e7). C primitives НЕ
требуются — D26 ptr[len]==‘\0’ invariant + Nova-side wrapping достаточно.
V1 simplifications (explicit followups, not silent):
[M-118.1-cstr-nul-check]— ✅ CLOSED 2026-06-08.@as_cstr()scans the str bytes for an embedded NUL and panics (interior0x00would truncate the C-string at that byte);@as_cstr_unchecked()is the scan-freeunsafe fnhatch.panicis reachable via a module-privateexternal fn panicdecl — cstr.nv is ExternalRegistry-loaded and gets no auto-prelude, and a plainimport std.prelude.*would trip the R27 auto-import opt-out for importers.[M-118.1-cstr-to-cstr-distinct-copy]— DEFERRED → Plan 118.2.@to_cstr()is NOT shipped in V1: theas_X/to_Xconvention makesto_cstran OWNED copy (buffer outliving the source str), which needs an allocator/free API. Rather than ship a misleadingly-named zero-copy alias, the method was removed (2026-06-08); the owning copy lands in Plan 118.2.
Closes [M-118.1-cstr-literal] (was: «add c”hello” prefix-literal»; superseded by D26 invariant). Closes [M-118.1-cstr-runtime-wiring] (was: «C primitive ABI wiring»; pure-Nova approach makes it unnecessary).
Diagnostic codes (new)
Errors:
E_UNSAFE_REQUIRED— pointer op (*exprDeref / unsafe fn call) outside unsafe context (block.is_unsafe = false AND not вunsafe fnbody). Active enforcement черезcheck_unsafe_context_in_modulewalker pass с depth counter — D216 §8 V1 ENFORCED 2026-06-02. Plan 118.6 amend (2026-06-16):&xAddrOf (safe promote path) no longer triggersE_UNSAFE_REQUIRED. Only raw stackunsafe { &x }and Deref/unsafe fn calls remain gated.E_UNSAFE_CALL_REQUIRES_WRAP— callingunsafe fnбезunsafe { }wrap. Active enforcement черезcheck_unsafe_context_in_modulewalker с pre-collected unsafe_fns: HashSet. D216 §9 V1 ENFORCED 2026-06-02 (commit abd4be4603b) E_UNSAFE_ATTR_DEPRECATED—#unsafe fn/#unsafe external fnsyntax; useunsafe fn/external unsafe fninstead (Plan 118.1.7)E_ARRAY_INDEX_PTR_BANNED—&arr[i]E_NULL_LITERAL_USE_NONE—nullliteral (general)E_NULL_PTR_RETRACTED_USE_OPTION—null ptr(Plan 115 V1) retractedE_UNDEFINED_USE_NONE_INIT_PATTERN—undefinedusedE_CLOSURE_HAS_ENV— fn → *fn cast attempted с closure envE_CALLBACK_THROWS_OVER_C_ABI— Fn-with-Fail → *fn cast. Active enforcement — D216 §10/§20 V1 ENFORCED 2026-06-02 (commit e4cff57142e)E_EXTERNAL_FN_FAIL_EFFECT— external fn declaration с FailE_PTR_ARITHMETIC_INVALID—p * 2,p / 4, etc.E_POINTER_RO_ASSIGN—*p = v/p.field = vгде p roE_POINTER_RO_MUT_METHOD—p.mut_method()где p roE_PTR_CAST_INVALID_TARGET—p as bool / f64 / ...E_INVALID_POINTER_MODIFIER—*const Tи др.E_POINTER_PREFIX_MODIFIER—ro/mut/unsafeперед*в type-position (mut * T/ro * T/unsafe * T); use postfix*mut T/*ro T/*unsafe Tили bindingmut x *T. ExtendsE_INVALID_POINTER_MODIFIER(Plan 138.5 §1).E_DUPLICATE_POINTER_MODIFIER—*ro mut TE_PARSE_POINTER_TYPE_INCOMPLETE—*без typeE_REALTIME_POINTER_OP— pointer op в#realtime fnbody. Active enforcement — D216 §20 + Plan 113 D172 V1 ENFORCED 2026-06-02 (commit 6752565f453)E_UNSAFE_HANDLER_BUILTIN_ONLY— user-defined unsafe_handler attemptE_AMP_CONST_BINDING—&const_valueE_AMP_LITERAL—&42E_AMP_RECORD_LITERAL—&Record { ... }без named binding (Plan 118 §4 amend)E_ADDR_OF_NON_LVALUE—addr_of/addr_of_mutapplied к non-Ident / Member / SelfAccess expression (rvalue / temporary). Mirrors Rust’s «cannot take address of a temporary». Plan 118.1 closeout 2026-06-05.E_ADDR_OF_MUT_REQUIRES_MUT_BINDING—addr_of_mutapplied к ro binding (let безmut, ro parameter, ro field). MirrorsE_PARAM_NOT_MUT/E_LOCAL_NOT_MUTpattern (D108.1 / D108.2). Plan 118.1 closeout 2026-06-05.E_ADDR_OF_REMOVED—addr_of()/addr_of_mut()called after retirement (Plan 118.6 D216 §4, 2026-06-16). Use&xinstead. Both functions are removed from prelude; any surviving call site raises this error.E_PTR_NO_DISPLAY_USE_DEBUG_STR—"${p}"E_VARARG_NOT_SUPPORTED— vararg FFI callE_CAST_RAW_FN_TO_CLOSURE—*fn → fncast outside unsafe
Warnings:
W_UNSAFE_GC_TRIGGER— GC trigger внутри unsafe с pointer in scopeW_PTR_AS_INT_GC_HASH_HAZARD—p as intкак HashMap keyW_OPTION_DOUBLE_NESTED—Option[Option[*T]]NPO fallback
Mainstream comparison
| Язык | Typed ptr | Unsafe model | Null safety | Auto-deref | Arithmetic |
|---|---|---|---|---|---|
| Rust | *const T/*mut T/&T/&mut T | unsafe { } + unsafe fn | Option<&T> + NPO | через ref | unsafe only |
| Zig | *T/*const T/[*]T | (нет keyword; intrinsics) | ?*T + NPO | .* postfix + . | + для [*]T only |
| C# | T* (unmanaged) / ref T / in T / out T | unsafe modifier | T? | p->field | unsafe only |
| Swift | UnsafePointer<T> / UnsafeMutablePointer<T> | Type-based (Unsafe* prefix) | Optional + NPO | .pointee | only через advanced(by:) |
| D | T* / ref T / scope T* | @safe/@trusted/@system | Nullable!T | p.field auto | @system only |
| Go | *T (managed); unsafe.Pointer | unsafe package | Nil runtime | p.field auto | unsafe.Pointer only |
| Nova V1 (Plan 115) | ptr only | (нет) | null ptr | (нет) | banned |
| Nova V2 (Plan 118) | *T family + unsafe | unsafe { } + unsafe fn (D2 amend) | Option[*T] + NPO | p.field/p.method() one-level | gated unsafe → *unsafe T |
Use cases
- Typed FFI buffers (libpng image data, libcurl headers, sqlite blobs) — full impl Plan 118.1 (memory primitives) + 118.2 (slice fat-pointer)
- Memory-mapped I/O (registers, framebuffers) — Plan 118.1 volatile RW
- Manual linked structures (intrusive lists, lock-free queues, custom allocators) — Plan 118.3 AtomicPtr
- Performance-critical hot loops (escape analysis + GC-pressure reduction)
- Out-params для FFI (
int func(out int* result)) — Plan 118.1 addr_of_mut!
Cross-ref
- D2 (amend) —
unsafe { }keyword restored - D32 (amend) —
&valuenot Rust borrow - D52 — type forms (tuple newtype canonical для FFI handles)
- D214 (amend) — ptr redefine
- D215 — Plan 120 stack tuples (escape interaction)
- D172 —
#realtimeban для pointer ops - D217 — Plan 118.1 (FFI intrinsics)
- D218 — Plan 118.2 (slice + uninit)
- D219 — Plan 118.3 (concurrency)
- Plan 118 — implementation
Acceptance
См. Plan 118 A1-A35 (T1-T8 + R1-R5 series).
D216 V2 amend (2026-06-04) — universal right-binding rule для type-level modifiers + unsafe T first-class
⚠️ PARTIALLY SUPERSEDED — Plan 138.5 (2026-06-11): часть V2, касающаяся указателей (outer pointer-mut как type-wrapper:
mut * T = Mut(Pointer(T)),unsafe * T = Unsafe(Pointer(T)), NPO-таблица §V2.4 по outer-wrapper), РЕТРАКТИРОВАНА. FINAL pointer model = pointee-mut постфикс только (*mut T/*ro T/*unsafe T), reassignability = binding (let/mut, D36), nullable =Option[*T]только. Все prefix-формы перед*запрещены (E_POINTER_PREFIX_MODIFIER, §1). СОХРАНЯЕТСЯ: §V2.3unsafe Tvalue-wrapper (MaybeUninit-style) — про значение, ортогонально указателям; и универсальное right-binding для value-T модификаторов (ro T/mut T/unsafe Twrappers, codegen-transparent). См. retract-пометки в §V2.1/§V2.2/ §V2.4/§V2.6 и §V3.2/§V3.3/§V3.4 ниже.
Status: 🆕 SPEC LANDED 2026-06-04 (parser/codegen migration — Plan 118.5 NEW sub-plan, см. follow-up markers ниже). Этот amend был breaking change для existing
*ro T/*mut T/*unsafe Tsyntax — pointer-часть позже ретрактирована (Plan 138.5, см. banner выше).
Motivation
Inconsistency discovered 2026-06-04: parser применяет «right-binding rule»
для ro T (TokenKind::KwRo → recursive parse_type() → Readonly(inner)),
но pointer-modifier syntax *ro T / *mut T / *unsafe T использует
inline modifier-after-star form. V2 попытался унифицировать через
prefix-wrappers; Plan 138.5 (2026-06-11) выбрал обратное — postfix pointee
canonical, prefix запрещён (один указатель-модификатор = pointee, постфикс).
| Token | Семантика (FINAL, Plan 138.5) |
|---|---|
ro T | Readonly(T) — value-T wrapper, codegen-transparent (KEPT) |
mut T | Mut(T) — value-T wrapper, codegen-transparent (KEPT, §V2.2b) |
unsafe T | Unsafe(T) — value-T wrapper, MaybeUninit (KEPT, §V2.3) |
consume T | consume wrapper (receiver/field/decl, см. D162) |
*T | Pointer(T) — pointee ro (≡ *ro T, D246; pointee-mut из типа, не от binding) |
*ro T | ❌ E_REDUNDANT_POINTER_RO (избыточно: *T уже ro; fix-it *T) — D246 |
*mut T | Pointer(Mut(T)) — explicit mut pointee (единственный опт-ин на *p = …) |
*unsafe T | Pointer(Unsafe(T)) — CANONICAL pointer к possibly-uninit T |
mut * T | ❌ E_POINTER_PREFIX_MODIFIER (prefix перед * запрещён; use mut x *T binding) |
ro * T | ❌ E_POINTER_PREFIX_MODIFIER |
unsafe * T | ❌ E_POINTER_PREFIX_MODIFIER (RETIRED Unsafe(Pointer); nullable = Option[*T], FFI nullable-uninit = Option[*unsafe T]) |
§V2.1 — universal right-binding rule (value-T modifiers)
Правило (KEPT для value-T): type-level modifier применяется к ВСЕМУ что
справа от него до конца type-expression, либо до следующего modifier. Это про
value-T wrappers (ro/mut/unsafe/consume), codegen-transparent.
⚠️ Pointer-часть RETRACTED (Plan 138.5):
*НЕ «pure constructor с prefix-модификатором снаружи». Указатель несёт postfix pointee-модификатор, сразу после*(*mut T/*ro T/*unsafe T). Prefix перед*(mut * T/ro * T/unsafe * T) —E_POINTER_PREFIX_MODIFIER(§1).
Modifiers (выровненная иерархия) — применимы к value-T и как postfix pointee:
ro— readonly (compile-time immutability)mut— mutable (compile-time mutability marker)unsafe— unsafe (init/layout/aliasing contracts off)consume— consume (unique ownership, D162 follow rules)
Parser pattern (FINAL):
TYPE := MODIFIER TYPE | BASE_TYPE | '*' POINTEE | '[' ']' TYPE | ...
POINTEE := POINTEE_MOD POINTEE | TYPE // постфикс после '*'
MODIFIER := 'ro' | 'mut' | 'unsafe' | 'consume'
POINTEE_MOD := 'ro' | 'mut' | 'unsafe'
Каждый value-modifier — TypeRef::<Modifier>(Box<TypeRef>) wrapper. * —
конструктор Pointer(Box<TypeRef>), чей pointee может нести ro/mut/unsafe
постфиксом. Prefix-модификатор перед * запрещён (см. banner выше).
§V2.2 — chain semantic (multi-modifier / multi-pointer)
⚠️ RETRACTED (Plan 138.5): трактовка «
mut * T=Mut(Pointer(T))(outer pointer-mut как type-wrapper)» отозвана. Outer-pointer-mut в типе больше не существует — reassignability указателя выражается binding’ом (let/mut, D36), а pointee-мутабельность — postfix после*.
FINAL chains читаются с postfix-pointee; reassignability — отдельно через binding:
*T // Pointer(T) ≡ Pointer(Readonly(T)) — pointee ro (D246; *T ≡ *ro T)
*mut T // Pointer(Mut(T)) — explicit mut pointee (единственный опт-ин)
*unsafe T // Pointer(Unsafe(T)) — valid (non-null) ptr к possibly-uninit T
ro p *mut *T // L3 из типа (D246): внешний *mut (writable), внутренний *T (ro pointee)
// позиционно-независимо; binding ro = только p не reassignable
ro p *T // binding ro: p фиксирован; pointee ro
mut p *T // binding mut: p reassignable; pointee ro (*p = … ❌ — L1 mut ≠ mut-pointee)
mut p *mut T // binding mut: p reassignable; pointee mut, writable (*p = … ✅)
mut p *unsafe u8 // binding mut: p reassignable; pointee possibly-uninit byte
RETRACTED (теперь parse error E_POINTER_PREFIX_MODIFIER):
mut * T // ❌ — вместо: binding `mut p *T`
ro * T // ❌ — вместо: binding `let p *T` (или `ro p *T`)
unsafe * T // ❌ — RETIRED Unsafe(Pointer); вместо: Option[*T] / *unsafe T (pointee)
mut * ro * T // ❌ — вместо: `mut p *ro *... ` postfix-chain
ro p mut * unsafe T // ❌ — вместо: `mut p *unsafe T` (binding mut + pointee unsafe)
Канонический пример FFI out-param (FINAL):
external fn os_read(fd int, buf *unsafe u8, n int) -> int
// pointee uninit byte; buf non-null *; OS fills, returns count.
// Если sam buf переприсваивается в теле — `mut buf` на стороне caller's binding.
§V2.3 — unsafe T semantic (MaybeUninit-style)
✅ KEPT (Plan 138.5):
unsafe Tvalue-wrapper сохраняется без изменений — он про значение (maybe-uninit T-typed память), ортогонален указателям и prefix-запрету.mut x unsafe T= mut-binding к maybe-uninit value. Указательная форма «ptr к uninit T» =*unsafe T(postfix pointee, §1), а НЕ* unsafe Tс пробелом и НЕunsafe *(последнее retired).
unsafe T означает «T-typed memory с снятыми init/layout/aliasing contracts».
Caller asserts validity at use sites. Concretely:
- Init: значение может быть uninitialized — read без prior write — UB
- Layout: alignment / size — same as T (не bitwise opaque)
- Identity: bit-pattern valid для T at каждом read site
- Aliasing: Nova exclusivity rules off (но atomicity не гарантирована)
Operations:
- Read
unsafe Tvalue — requiresunsafe { }block (caller asserts init) - Write
unsafe Tslot — safe (transitions to valid) - Cast
unsafe T → T— requiresunsafe { }+ value-level assertion (e.g.unsafe { x as T }или dedicatedassume_initbuiltin)
Соответствует Rust MaybeUninit<T> semantic, но как type modifier вместо
generic wrapper.
Default-init для unsafe T bindings: mut x unsafe T — slot выделена,
но не initialized. Compiler-emitted runtime check НЕ выполняется (это и
есть escape hatch).
§V2.4 — Option B / niche optimization (FINAL, Plan 138.5)
⚠️ RETRACTED-and-simplified (Plan 138.5): старая таблица зависела от «outermost pointer modifier» с
Unsafe(Pointer)(16-байтные строки). После retireunsafe *(Unsafe(Pointer)) — указатель ВСЕГДА non-null, поэтому NPO применяется универсально (8 байт), без зависимости от модификатора.
Все указатели (*T/*ro T/*mut T/*unsafe T) — guaranteed non-null
(§1). Поэтому Option[*…] всегда NPO-eligible (8 байт, null = None):
| Тип | Может содержать null? | NPO размер |
|---|---|---|
Option[*T] (≡ Option[*ro T]) | ❌ | 8 байт ✅ |
Option[*mut T] | ❌ | 8 байт ✅ |
Option[*unsafe T] | ❌ pointer non-null; pointee uninit OK | 8 байт ✅ |
Option[*mut *ro T] (chain) | ❌ | 8 байт ✅ |
Nullable raw-uninit (FFI, C T* может быть NULL): Option[*unsafe T] —
None = NULL, Some = non-null ptr к possibly-uninit T (validity асертится на
deref в unsafe {}). Это единственная nullable-форма; отдельного raw-nullable
unsafe * T больше нет. Nested Option[Option[*T]] — fallback к tagged repr +
W_OPTION_DOUBLE_NESTED (без изменений, см. §7 V4).
§V2.5 — migration path
⚠️ REVISED (Plan 138.5): шаги про prefix-pointer-форму отозваны. FINAL = postfix pointee canonical; prefix перед
*— hard errorE_POINTER_PREFIX_MODIFIER. Value-T wrappers (Mut/Unsafe/Readonly, codegen-transparent) сохраняются (AST/codegen/checker шаги ниже про них KEPT).
-
AST changes (KEPT для value-T):
TypeRef::Mut(Box<TypeRef>)/TypeRef::Unsafe(Box<TypeRef>)— value-T wrappers (codegen-transparent / MaybeUninit). Также используются как pointee содержимое внутриPointer(...)(*mut T=Pointer(Mut(T))).TypeRef::Pointer(Box<TypeRef>)— конструктор; pointee несётro/mut/unsafeпостфиксом. Outer-pointer-mut wrapper (Mut(Pointer)) больше не строится из синтаксиса (prefix запрещён).
-
Parser changes (FINAL):
mut T/unsafe Tparse arms (value-T) — recursive. KEPT.*Star branch парсит postfix pointee modifier (*mut/*ro/*unsafe).- Reject
ro/mut/unsafetoken непосредственно перед*→E_POINTER_PREFIX_MODIFIER(НЕ warning — hard error в Plan 138.5 enforce-фазе).
-
Codegen changes (KEPT):
Mut(T)/Unsafe(T)value-wrapper — C-level no-op для primitive T.Pointer(Mut(T))/Pointer(Readonly(T))/Pointer(Unsafe(T))— pointee модификаторы; все emitT*ABI (mut/ro различаются на assignment-check).
-
Type-checker changes (KEPT):
Unsafe(T)value read →E_UNSAFE_T_READ_REQUIRES_WRAP(§V2.3 value-wrapper).Unsafe(T)value write — safe;T → Unsafe(T)implicit;Unsafe(T) → Texplicit (unsafe cast).- NPO — универсальный (§V2.4, все
*…non-null → 8 байт).
-
Migration sweep (Plan 138.5 Ф.2 enforce):
- prefix usages
mut */ro */unsafe *→ postfix*mut T/*ro T/*unsafe T(или bindingmut x *T/ nullableOption[*T]). Основной call-site:std/runtime/raw_mem.nv(dst mut * u8→dst *mut u8). - Retire
Unsafe(Pointer)(unsafe * T): →Option[*T]/Option[*unsafe T].
- prefix usages
-
Spec amends downstream:
§V2.6 — backward compatibility
✅ RESTORED (Plan 147 D246, 2026-06-12): утверждение «
*T ≡ *ro T» (always-ro pointee) восстановлено — flip-scan-draft (*Tнаследует binding-current) ОТКЛОНЁН (тип не самодостаточен). Под D246 pointee-mut задаётся только типом (*mut T), позиционно-независимо. Поэтомуmut p *mut T— явная и единственная форма writable-pointee при mut-binding;mut p *Tдаёт ro-pointee (L1 mut ≠ mut-pointee).*ro T→E_REDUNDANT_POINTER_RO(избыточно). Codegenpromote_pointer_pointee_mut(flip-scan seed) УДАЛЯЕТСЯ в Ф.3 (наследование pointee-mut от binding запрещено D246). Постфикс-canonical и prefix-ban (ниже) — сохраняются.
Codegen-status UPDATE (D246, Plan 147 Ф.3, 2026-06-12): legacy codegen inherit-current (commit
38360c30d80,field_type_with_binding_mut/promote_pointer_pointee_mut— auto-promotemut p *T-поля вPointer(Mut(…))) — УДАЛЯЕТСЯ под три-осевой моделью: наследование pointee-mut от binding запрещено (L1 ⊥ L3). После удаления writable buffer требует явного поляmut data *mut T(vec_owned.nv уже его несёт, line 104).[M-138.2-v2-propagation-impl-gap]остаётся закрытым (V2-propagation как механизм — moot под D246).
FINAL (Plan 138.5, amend Plan 147 D246) — НЕТ grace-period для prefix:
*mut T/*unsafe T(postfix) — canonical.Pointer(Mut(T))/Pointer(Unsafe(T)).*mut T— единственный опт-ин на mut-pointee.*T— pointee ro,*T ≡ *ro TУНИВЕРСАЛЬНО (D246; pointee-mut из типа, не от binding).*ro T(postfix) — hard errorE_REDUNDANT_POINTER_RO(избыточно:*Tуже ro; fix-it*T). D246.mut * T/ro * T/unsafe * T(prefix перед*) — hard errorE_POINTER_PREFIX_MODIFIER(НЕ warning). Migrate → postfix или binding-mut.unsafe * T(Unsafe(Pointer), старый raw-nullable) — RETIRED. Nullable =Option[*T](NPO); FFI nullable-uninit =Option[*unsafe T].*unsafe T(postfix) сохраняет смысл «ptr к possibly-uninit T» (pointee unsafe).
Historical (V2 grace-period draft, отозвано Plan 138.5): ранее планировался
W_DEPRECATED_POINTER_INLINE_MODIFIERдля postfix-формы и миграция на prefixmut * T. Это направление обратно финальной модели и не реализуется.
§V2.7 — follow-up markers
[M-118.5-right-binding-migration]— SUPERSEDED by Plan 138.5 (prefix pointer-form retired; postfix pointee canonical +E_POINTER_PREFIX_MODIFIERenforce — Plan 138.5 Ф.2). Value-T wrapper parsing остаётся.[M-118.5-unsafe-t-readwrite-semantics]— type-checker E_UNSAFE_T_READ_REQUIRES_WRAP (KEPT, §V2.3)[M-118.5-mut-t-vs-binding-distinction]— ✅ CLOSED (§V2.2b:mut Tvalue-wrapper transparent; pointer-mut = pointee postfix / binding, Plan 138.5)[M-118.5-consume-as-type-modifier]— generalizeconsume(currently receiver/field/decl) к универсальный type wrapper[M-118.5-d218-maybeuninit-duplication]— ✅ CLOSED (D218 RETRACTED — MaybeUninit subsumedunsafe T)[M-118.5-npo-recalculation]— NPO теперь универсальный (§V2.4 FINAL — все*…non-null → 8 байт); recalculation сводится к «всегда NPO для pointer-inner»- three-axis (Plan 147 D246, supersedes flip-scan-draft):
*T ≡ *ro Tвосстановлено УНИВЕРСАЛЬНО; pointee-mut из типа (*mut T), не от binding;*ro T→E_REDUNDANT_POINTER_RO. См. D246.
Cross-amend impact
- D33 (binding propagation) — extend rule к value-T
mut/unsafe/consume - D216 §1-3 — pointer modifier syntax = postfix pointee canonical
(Plan 138.5; prefix перед
*запрещён) - D36 (binding readonly default) — reassignability указателя = binding (не тип)
- D184 (return mut default) — pointer-return ставит pointee-mut
- D217 (FFI intrinsics) — RawMem signatures postfix pointee
- D218 (slice + MaybeUninit) — MaybeUninit subsumed
unsafe T(см. D218 RETRACTED) - D162 (consume types) — extend для
consume Ttype wrapper position
Why now
Right-binding rule уже работает для ro (parser-verified) — для value-T
модификаторов это правило сохраняется. Для указателей Plan 138.5 выбрал
postfix-pointee canonical: один модификатор = pointee, постфикс после *;
reassignability = binding (let/mut). Это убирает путаницу «двух mut» (outer
pointer-mut в типе vs pointee-mut), особенно в return-позиции (D184×D216).
unsafe T first-class также unlock’ает MaybeUninit semantic без duplication
с Plan 118.2 D218 — это simplification, не addition.
flip-scan-draft. Указатели: running-current flip-scan модель (RETRACTED)
Status: ❌ RETRACTED 2026-06-12 (Plan 147 Ф.1). Черновик flip-scan (commit
befe92c, SPEC-ONLY — кода никогда не было) ОТКЛОНЁН adversarial- критикой: модель делала*Tконтекстно-зависимым (наследует binding-current), поэтому указательный тип переставал быть самодостаточным (4 BLOCKER: один и тот же*Tозначал ro в одной позиции и mut в другой; double-ptr flip-chain нечитаем; cast/generic-позиции требовали спец-исключений; «redundant»-проверка ломалась на параметрах). Заменён на D246 — три оси мутабельности.*T ≡ *ro Tвосстановлено универсально. ОшибкаE_REDUNDANT_POINTER_MODIFIER(была бы flip-scan-only) никогда не реализовывалась; под D246 заменена наE_REDUNDANT_POINTER_RO(*ro T→ fix-it*T). Всё содержимое flip-scan-draft ниже удалено как недействительное; оставлен только этот retract-баннер для якоря ссылок.
D246. Три оси мутабельности: L1 binding / L2 view / L3 pointee
Status: ✅ FULLY IMPLEMENTED. Ф.1-Ф.6 LANDED 2026-06-12 (Plan 147). AMENDED Ф.7 2026-06-17 (Plan 147 Ф.7): checker enforcement gaps closed — ro-binding + param index-freeze (
E_READONLY_CONTENT), redundant modifier oracle. Реализация — Plan 147 (Ф.2 parser / Ф.3 checker / Ф.4 migration / Ф.5 tests / Ф.7 enforcement). Supersedes flip-scan-draft (отклонён). Восстанавливает D216 §V2.6 «*T ≡ *ro T». Источник: 2 design-workflow (critiquewkx3dytr1, value-sidewlqgc2nyk, synthesisw9nktq8x1) + ~15 раундов ревью. Гейтит Plan 139[M-139-f0-lang-item-decl](str-поляptr *u8).
Что
Происхождение: ro = read-only, mut = mutable.
Мутабельность в Nova задаётся тремя ортогональными осями; каждая самодостаточна (C1 — тип/binding читается без контекста):
| Ось | Что задаёт | Синтаксис |
|---|---|---|
| L1 — binding | переприсваиваемость имени (x = …) + корень прав записи через имя | ro/mut перед именем (никогда в типе) |
| L2 — view | транзитивный ro/rw по owned-графу значения (.field/[i]); СТЕНА на каждом * | ro/mut перед типом value/record |
| L3 — pointee-capability | можно ли писать за * (*p = …); реально В ТИПЕ, позиционно-независимо | постфикс: *T(ro) / *mut T(mut) |
Принцип (1 строка): ro — дефолт везде; пишется только опт-ин (mut x,
mut T, *mut T). L2 транзитивно морозит owned-граф и упирается в стену на
*; за указателем — только L3 из типа. Soundness в GC (нет borrow-checker,
есть aliasing): ro = «это имя/путь не пишет», НЕ «объект заморожен».
Отмена flip-scan (flip-scan-draft): *T НЕ наследует binding-current. *T ≡ *ro T
во всех позициях (param/return/generic/alias/cast/field/local). Pointee-mut —
только через явный *mut T.
Канон синтаксиса
*T= ro-pointee (канон, дефолт, ≡*ro T).*mut T= mut-pointee (единственный опт-ин на запись*p = …).*ro T→ HARD ERRORE_REDUNDANT_POINTER_RO(«избыточно → используй*T»; fix-it*T). Выбор (a): потребителей мало, std в формировании.mut *T/ro *T(prefix перед*) →E_POINTER_PREFIX_MODIFIER(модификатор на*запрещён; reassign = L1 binding).ro T/mut T(перед типом value/record) = L2 content-view.ro x ro T/*ro ro T→E_REDUNDANT_TYPE_MODIFIER.mut x mut T→E_REDUNDANT_TYPE_MODIFIER(тип без модификатора уже mutable по умолчанию; явныйmut Tприmut-binding избыточен). Аналогично для параметра:func(mut a mut T)→E_REDUNDANT_TYPE_MODIFIER.- Параметр: binding ro по умолчанию (D176) → явный
ro Tна типе избыточен.func(a ro T)≡func(ro a ro T)→E_REDUNDANT_TYPE_MODIFIER(fix-it: убериroс типа, используйfunc(a T)). Исключение:func(mut a ro T)— валидно (явныйmutна binding снимает ro-default;ro Tна типе = L2 freeze, не redundant). - Параметр
*T:func(a *T)— pointee уже ro по умолчанию (L3 дефолт). Явныйfunc(a *ro T)→E_REDUNDANT_POINTER_RO(то же что в любой другой позиции). **T ≡ *(*T), дефолт ro вниз; mut-уровни — явный*mutна нужном уровне (*mut *T,**mut T,*mut *ro …нельзя —*roredundant).*T ≡ *ro TУНИВЕРСАЛЬНО — во ВСЕХ позициях. НЕТ наследования pointee-mut от binding. НЕТ cast-исключения (x as *T, неas *ro T).
Дефолты
binding — пишешь явно ro/mut; параметр ro (D176); return mut-binding у
caller’а (D184 — свойство binding, не значения); pointee ro (*T); поле
mutable-у-mut-binding (D175).
Асимметрия L2 vs L3: тип value/record без модификатора (T) — content mutable по умолчанию
(L2; заморозка = явный ro T). Указатель без модификатора (*T) — pointee ro по умолчанию
(L3; запись = явный *mut T). Разные дефолты намеренны: value-тип — твой, владеешь, пишешь;
указатель — чужая/aliased память, по умолчанию не пишешь.
Таблица мутабельности (binding × content)
Знаки: ✅ разрешено · ❌ запрещено · E = ошибка
Локальные binding’и:
| Форма | reassign | .field/[i] |
|---|---|---|
ro a T | ❌ | ❌ (P7 freeze) |
mut a T | ✅ | ✅ |
ro a mut T | ❌ | ✅ (R2-split) |
mut a ro T | ✅ | ❌ (R2-split) |
Параметры (ro по умолчанию, D176):
| Форма | v = x | .field/[i] write |
|---|---|---|
v T ≡ ro v T | ❌ E_LOCAL_NOT_MUT | ❌ E_READONLY_CONTENT |
mut v T | ✅ | ✅ |
mut v ro T | ✅ | ❌ E_READONLY_CONTENT |
v = x внутри fn — reassign локальной копии binding’а (не виден снаружи).
Указатели в параметре (L1 × L3):
| Форма | v = q | *v = x |
|---|---|---|
v *T ≡ ro v *T | ❌ | ❌ |
mut v *T | ✅ | ❌ |
v *mut T ≡ ro v *mut T | ❌ | ✅ |
mut v *mut T | ✅ | ✅ |
Локальные указатели (L1 × L3):
| Форма | p = q | *p = v |
|---|---|---|
ro p *T | ❌ | ❌ |
mut p *T | ✅ | ❌ |
ro p *mut T | ❌ | ✅ |
mut p *mut T | ✅ | ✅ |
10 принципов (P1-P10)
- P1 — три оси ортогональны. L1×L2×L3 не влияют друг на друга. mut-binding НЕ даёт mut-pointee; mut-pointee НЕ делает имя reassignable.
- P2 — тип самодостаточен (C1).
*Tозначает ro-pointee в ЛЮБОЙ позиции, без running-current/контекста. - P3 —
roдефолт везде; опт-ин на запись явный (mut x/mut T/*mut T). - P4 — L2 freeze СТОИТ на каждом
*. Транзитивный ro-view не проникает за указатель; за*действует только L3 (из типа pointee). - P5 — L3 из типа, позиционно-независимо.
*mut T= writable target где угодно;*T= ro где угодно. - P6 — split (L1,L2) явны.
ro r mut Point(reassign❌/content✅),mut r ro Point(reassign✅/content❌). Разрешает[M-138-binding-type-mut-conflict]. - P7 — голый
ro r= freeze (binding dominates, D175 §V2): и reassign, и весь owned-граф (до стены на*). - P8 — coercion по оси content (L2), независимо от L1. ro-источник →
mut-content-цель =
E_READONLY_COERCE; → ro-цель OK.*mut T → *Tавто-сужение;*T → *mut T❌. - P9 — deep-immutable НЕ навязывается снаружи сквозь
*mut(trade-off):-> ro VRморозит свои слоты, ноunsafe{*v.p=w}проходит (L2 не лезет за*). Deep-ro → производитель объявляет поле*T(какstr { ptr *u8 }). - P10 — owned-vs-aliased heap статически неразличим → граница рисуется
синтаксически на
*(L2 стоп на*), не по aliasing-статусу.ro= per-path write-ban, не object-freeze (GC, нет эксклюзивности).
R1 vs R2 (обе живут — на разных осях)
- R1 (transitive-ro) = закон L2:
-> ro Value/-> ro HeapValueморозят owned-граф (стена на*). - R2-split = явный opt-in пары (L1,L2):
ro r mut Point/mut r ro Point(см. P6). Голыйro r= freeze (P7).
Нормативный ORACLE (тест-корпус; чтение всегда ✅, знаки = ЗАПИСЬ)
A. VALUE-record Point (копия): mut r: r=X✅ r.x=5✅ · ro r: ❌/❌ ·
mut r ro Point: ✅/❌ · ro r mut Point: ❌/✅
B. HEAP-record Acc (handle): те же знаки (семантика: запись видна
co-handle’ам; ro = это имя не пишет).
C. POINTER (unsafe-ops): mut p *T: p=q✅ *p=v❌ · mut p *mut T: ✅/✅ ·
ro p *T: ❌/❌ · ro p *mut T: ❌/✅ · ro p **T: p❌ *p❌ **p❌ ·
ro p *mut *T: ❌/*p=q✅/**p❌ · ro p **mut T: ❌/*p❌/**p=v✅
D. RETURN: -> Value: caller mut-default (a=X✅,a.x=5✅) · -> ro Value:
mut a Value=f()→E_READONLY_COERCE, mut a ro Value=f()✅, ro a mut Value=f()→
E_READONLY_COERCE, ro a Value=f()✅ · -> *mut T: *a=v✅(unsafe) · -> *T:
*a=v❌ · -> *ro T: ❌ E_REDUNDANT_POINTER_RO.
E. Generic/Option/cast: Vec[*T]: *v[i]=x❌ (L3 элемента) · Vec[*mut T]:
*v[i]=x✅¹ · Option[*mut T]: Some(p)→*p=v✅ · x as *mut T; ro a=x: a=y❌
*a=v✅ (из типа) · mut a=x: ✅/✅ · vr{p *mut T}→ro v: v.p=q❌
unsafe{*v.p=w}✅ · str{ptr *u8,len int}: s.ptr=q❌, буфер ro.
¹
Vec[*mut T]: *v[i]=x— семантически верно (тип элемента*mut T), но codegen не реализован:Vec.new()для pointer-element-type вызывает generic-заглушкуNova_Vec_static_new()→ NULL → SEGFAULT. Граница[M-138-vec-pointer-element-mono](Plan 138), P2.Option[*mut T]: Some(p)→*p=vработает (проверено e7_option_mut_ptr_deref_write).
Error codes
E_REDUNDANT_POINTER_RO(NEW, Plan 147 Ф.2) — postfix*ro T(избыточно:*Tуже ro). Fix-it: «используй*T». Применяется во ВСЕХ позициях (тип самодостаточен).E_POINTER_RO_ASSIGN(NEW, Plan 147 Ф.3) — запись*p = …через*T/*ropointee (ro-pointee read-only). Pointer-ops — вunsafe {}.E_POINTER_PREFIX_MODIFIER(существует, D216 §1) — модификатор перед*.E_REDUNDANT_TYPE_MODIFIER(существует) —ro x ro T/*ro ro T/func(a ro T)/mut x mut T/func(mut a mut T). (Ф.7: oracle-test f7_neg3 подтверждает parser-level enforcement дляro a ro T;func(a ro T)— parser аналогично.)E_READONLY_CONTENT(существует, D176) — запись через ro-view L2:view[i] = xнаro T-типе ИЛИ (NEW Ф.7) черезro-binding (L1 dominates P7):ro a = [...]→a[i] = x→E_READONLY_CONTENT;func(v []int)→v[i] = x→E_READONLY_CONTENT(param ro-by-default D176, P7 freeze).E_READONLY_COERCE(существует) — ro-content-источник → mut-content-цель (P8).
ORACLE F — Index-write through ro binding/param (Ф.7, 2026-06-17)
// F1: ro local binding → index write forbidden (P7 freeze)
ro a = [1, 2, 3]
a[0] = 99 // ❌ E_READONLY_CONTENT (ro binding dominates, P7)
_ = a[0] // ✅ reads always ok
// F2: mut binding → index write allowed
mut a = [1, 2, 3]
a[0] = 99 // ✅
// F3: param without mut → index write forbidden (ro by default D176, P7 freeze)
fn fill(v []int, val int) {
v[0] = val // ❌ E_READONLY_CONTENT
}
// F4: mut param → index write allowed
fn fill(mut v []int, val int) {
v[0] = val // ✅
}
// F5: ro local via fn return → index write forbidden
fn make_slice() -> []int { [1, 2, 3] }
ro a = make_slice()
a[1] = 99 // ❌ E_READONLY_CONTENT (ro binding P7)
Осознанные trade-off’ы (намеренно)
- Deep-immutable сквозь
*mutнельзя навязать снаружи (P9, C++ shallow-const): deep-ro → производитель объявляет поле*T. - Shared-mut heap-record под чужим
roвозможен (GC, нет эксклюзивности):ro= per-path write-ban, не object-freeze (P10). - owned-vs-aliased heap статически неразличим → граница на
*синтаксическая (P4).
Cross-amend impact
- D216 §V2.6 — «
*T ≡ *ro T» (always-ro pointee) RESTORED (flip-scan-draft retract). - D33 (binding propagation) — L1 ось; не propagates в L3 (стоп на
*, P4). - D36 (binding readonly default) — L1 binding = reassignability, только; НЕ задаёт pointee-capability (L3 из типа).
- D175 §V2 (binding dominates / access-time) = L2 view-семантика — KEEP;
добавлено «freeze STOPS at every
*» (P4) + пример vr-с-*mut-полем. - D176 (
ro Tтип-модификатор) — L2 content-view на параметре (ro дефолт). - D184 (return mut default) — свойство binding у caller’а (L1), не значения.
- D26 / Plan 139 — str lang-item
type str value priv { ptr *u8, len int }:ptr *u8(ro-pointee, ≡*ro u8);*ro u8избыточен →E_REDUNDANT_POINTER_RO. Снимает гейт[M-139-f0-lang-item-decl].
Связь
[M-138-binding-type-mut-conflict]— разрешён P6 (split на оси L1×L2).[M-ptr-cast-reinterpret-unsafe]— учитывается в L2-coercion (P8, авто-сужение*mut → *T).[M-138-double-pointer-codegen-test]— multi-level pointer (oracle C:ro p *mut *T).- Гейтит Plan 139
[M-139-f0-lang-item-decl].
Acceptance
См. Plan 147 A1-A6: A1 —
oracle A-E (~20 форм): pos компилируются, neg дают E_REDUNDANT_POINTER_RO /
E_POINTER_PREFIX_MODIFIER / E_READONLY_COERCE / E_POINTER_RO_ASSIGN; A2 —
*T ≡ *ro T ВЕЗДЕ (позиционные фикстуры); A3 — L2 freeze транзитивен + СТЕНА на
* (vr с *mut-полем); A4 — split + return-coercion 4 случая; A5 — flip-scan-draft
retracted, pointer-таблица + str переписаны; A6 — 0 регрессий pointer-dirs.
D220. Per-field visibility — priv keyword + type-level default flip
Status: V1 ACTIVE (spec + parser/AST infrastructure landed, 2026-06-02). AMENDED by D281 (2026-06-15): type-level
privтеперь = module-private (не type-private); type-private type-level default =priv(type). Field-level explicitprivостаётся type-private (без изменений). Реализация — Plan 124. Empirical validation — docs/research/06-field-visibility-go-kubernetes.md. Amends D47 (replaces deprecated_prefixconvention с compile-time enforcement).
Что
Per-field visibility modifier priv для records + named tuples (D215). По умолчанию все поля публичны (D47 unchanged, validated: kubernetes 92% public в API surface). Explicit priv — field accessible только из методов own type’а (instance + static).
Type-level default flip syntax type X priv { ... } — для invariant-heavy types где majority of fields private; explicit pub modifier overrides priv default.
Правило
§1 Syntax
// Per-field priv modifier (field-level).
export type Account {
priv mut money f64
ro name str
priv id u64
}
// Type-level default flip — fields default = priv.
export type Secret priv {
pub ro tag str
mut salt u64
key u64
}
Modifier ordering в field decl: priv/pub → ro/mut/consume → name TYPE. priv и pub mutually exclusive (E_PRIV_PUB_CONFLICT).
§2 Effective visibility
Field’s effective priv_field = first matching:
- Explicit
pubfield modifier → priv_field = false (public). - Explicit
privfield modifier → priv_field = true (private). - Type-level default (
type X priv {...}→ priv_field = true). - Otherwise (D47 default) → priv_field = false (public).
§3 Access rules
priv field access РАЗРЕШЁН только из методов own type’а:
- Instance methods:
fn TypeX @method() { @priv_field } - Static methods:
fn TypeX.factory(...) { ... } - Cross-instance:
fn TypeX @eq(other TypeX) -> bool => @f == other.f— доступ кprivполям другого экземпляра того же типа разрешён внутри метода этого типа.other— параметр типаTypeX, метод принадлежитTypeX→ privacy scope совпадает.
priv field access ЗАПРЕЩЁН во всех других контекстах:
- Read:
outside.priv_field→ E_PRIV_FIELD_READ - Write:
outside.priv_field = X→ E_PRIV_FIELD_WRITE - Init via record literal:
Foo { priv_f: X }→ E_PRIV_FIELD_INIT - Pattern destructure:
Foo { priv_f }→ E_PRIV_FIELD_PATTERN
§4 Diagnostic codes
- E_PRIV_FIELD_READ — read priv field outside type-method scope.
- E_PRIV_FIELD_WRITE — write priv field outside type-method scope.
- E_PRIV_FIELD_INIT — init priv field via literal outside.
- E_PRIV_FIELD_PATTERN — destructure priv field в pattern outside.
- E_PRIV_PUB_CONFLICT — both priv и pub modifiers на одном field.
- E_PRIV_FIELD_PROTOCOL (V4 deferred).
- E_PRIV_TUPLE_POSITIONAL_ACCESS (V4 deferred).
§5 Семантика: организация, не security
priv/priv(type) — организационный инструмент, не security-барьер. Цель: защита от случайного обращения к деталям реализации, не от намеренного.
Nova не ограничивает добавление методов на тип из любого модуля. Следствие: пользователь намеренно может написать:
fn Test @id() -> int => @id // в любом модуле — легально
Это не считается «вскрытием» — это осознанный выбор пользователя. Nova — «публичное по умолчанию» (D47, validated в docs/research/06-field-visibility-go-kubernetes.md), и priv(type) означает «используй методы типа», а не «запрещено».
Настоящая граница инкапсуляции — модуль (priv = module-private, D281): другой модуль не может случайно прочитать поле — только намеренно добавив метод.
Аналог: Go unexported защищает от случайного обращения из другого пакета, но не является security-boundary.
Nova не имеет reflection API → priv enforcement compile-time, без reflection-bypass (в отличие от Java/Kotlin/C#).
§6 Composition
priv composes orthogonally с:
- ro/mut/consume mutability modifiers
- use NAME Type (D39 embed) — V2 deferred [M-124.2-priv-embed]
- const NAME T = expr — reserved future use
§7 Backward compatibility
Existing Nova code = all-public fields → migration purely additive. priv opt-in keyword — старый код не ломается. _prefix convention deprecated 2026-06-02.
Почему
Empirical validation: kubernetes audit 35239 fields — 92.4% public в API surface. Public-default minimum boilerplate. Bimodal distribution → bimodal syntax (field-level priv + type-level priv {} flip).
Compile-time enforcement vs convention: prior _prefix hint-only privacy — false safety. priv keyword вводит compile-time guarantee → refactoring safety + invariant enforcement + API clarity.
Что отвергнуто
- Private-by-default — отклонено после kubernetes data.
- Edition default flip — отклонено (per-type granular лучше).
- #strict_visibility per-module attribute — отклонено (fragmentation).
Cross-refs
- D5 (07-modules.md) — module-level visibility.
- D29 (07-modules.md) — modules.
- D35 (03-syntax.md) — method declaration.
- D47 (07-modules.md) — export keyword; _prefix deprecated.
- D52 (this file) — record/sum/alias syntax.
- D131 (05-memory.md) — consume types.
- D215 (this file) — named tuples.
Acceptance
V1 (Plan 124.1) — ALL closed 2026-06-02:
- A1.1-A1.3 ✅ Parser/AST infrastructure (Ф.1 + Ф.4 commits).
- A1.4 ✅ E_PRIV_FIELD_READ enforcement (Ф.2 — f3_check_member hook).
- A1.5 ✅ E_PRIV_FIELD_WRITE enforcement (Ф.2.2 — check_target_readonly hook).
- A1.6 ✅ E_PRIV_FIELD_INIT enforcement (Ф.2.3 — RecordLit walk_expr hook).
- A1.7 ✅ E_PRIV_FIELD_PATTERN enforcement (Ф.2.4 — Pattern::Record f1_block hook).
- A1.8 ✅ Regression 0 new FAIL.
- A1.9 ✅ plan124_1 fixtures 9/9 PASS (4 positive + 5 negative).
- A1.10 ✅ Spec D220 NEW (this section).
Followup markers
- ✅ [M-124.1-checker-enforcement] CLOSED 2026-06-02 — all 4 codes via TypeCheckCtx current_recv_type RAII tracking.
- ✅ [M-124.2-pattern-sites-extension] CLOSED 2026-06-02 — Match/IfLet/WhileLet/For/ParallelFor + nested + spread (D221).
- [M-124.2-priv-embed] — priv use NAME Type.
- [M-124.4-tuple-priv] — named tuple priv (D215 ext).
- [M-124.4-protocol-impl-boundary].
- [M-124.5-doc-lsp].
- [M-124.6-test-access].
D221. Pattern destructure + literal init edge cases (Plan 124.2)
Status: ✅ ACTIVE since 2026-06-02 (Plan 124.2 closure). Extends: D220 §3-§4. Self-contained sub-decision: covers pattern-site и literal-spread edges not addressed в D220 §4. Plan: Plan 124.2. Cross-refs: D220 (core priv semantics), D52 §1-§2 (record declaration /
@fieldshorthand), D17 (pattern syntax).
§1 Scope
D220 §4 описывает priv-access rules для базовых форм (Member access
- Stmt::Let pattern + RecordLit named fields). D221 расширяет coverage на:
- Дополнительные pattern sites: match, if-let, while-let, for-in, parallel-for.
- Nested Pattern::Record — recursive descent через sub-field types.
- Record literal spread
Type { ...other }. - Rest pattern
{ field, .. }— non-binding semantics.
§2 Pattern sites — complete enumeration
Following pattern-bearing forms ALL apply priv-pattern enforcement (Plan 124.2 implementation hook each site):
| Site | AST node | Scrutinee source |
|---|---|---|
let PAT = EXPR | Stmt::Let { pattern, value } | type of value |
if PAT = EXPR { ... } | ExprKind::IfLet { pattern, scrutinee } | type of scrutinee |
while PAT = EXPR { ... } | ExprKind::WhileLet { pattern, scrutinee } | type of scrutinee |
match EXPR { PAT => ... } | ExprKind::Match { scrutinee, arms[].pattern } | type of scrutinee |
for PAT in EXPR { ... } | ExprKind::For { pattern, iter, elem_type } | elem_type ∥ inferred element type |
parallel for PAT in EXPR { ... } | ExprKind::ParallelFor { pattern, iter, elem_type } | same |
В каждой точке: for each Pattern::Record outside type-method scope,
each explicitly-named RecordPatternField corresponding к priv-field
→ E_PRIV_FIELD_PATTERN.
§3 Rest pattern .. — non-binding
ro Account { name, .. } = acc // outside-of-Account ok if `name` public.
// `..` does NOT bind priv `money`.
Pattern::Record.rest = true маркирует syntactic ... Семантика:
игнорировать остальные поля, no bindings produced. Priv-fields
не leak’аются через .. потому что нет binding’а.
NB: explicit field names ARE checked даже если .. присутствует —
{ money, .. } outside Account → E_PRIV_FIELD_PATTERN на money.
§4 Nested Pattern::Record
type Address { priv mut zip str, ro city str }
type User { ro name str, ro addr Address }
// Outside any method scope:
ro User { addr: Address { zip }, name } = u // ❌ E_PRIV_FIELD_PATTERN
// on `zip` (Address-internal)
Recursive descent: для каждой RecordPatternField { name, pattern: Some(sub), .. }
sub-pattern проверяется against sub-field’s declared type (via outer
type’s RecordField.ty). Outer field accessibility (User.addr public)
не освобождает inner check (Address.zip priv).
§5 Record literal spread — E_PRIV_FIELD_INIT_SPREAD
type Account { priv mut money f64, ro name str }
// Outside Account-method scope:
Account { ...orig, name: "new" } // ❌ E_PRIV_FIELD_INIT_SPREAD
Spread ...src implicitly копирует все fields (включая priv).
Outside type-method scope, это нарушает encapsulation. Эмитим
E_PRIV_FIELD_INIT_SPREAD на spread field span с hint’ом
использовать factory method.
Inside type-method scope — allowed (recv = T → каноническая ситуация).
Note: type без priv fields → spread OK везде (нет encapsulation boundary).
§6 Diagnostic codes
| Code | Where | Plan |
|---|---|---|
E_PRIV_FIELD_PATTERN | Pattern sites §2 + nested §4 | D220 §4 (reused) |
E_PRIV_FIELD_INIT_SPREAD | RecordLit spread §5 | D221 NEW |
Format (Plan 50 D102):
[E_PRIV_FIELD_INIT_SPREAD] cannot use spread `...` in record literal
of `T` outside type-method scope: type has private fields which would
be implicitly initialized via copy (Plan 124 / D221 §5). Hint: use
factory method `T.new(...)` or list each public field explicitly.
§7 Cross-refs
- D17 — pattern syntax.
- D52 §2 — record literal + field shorthand.
- D220 — core priv semantics (default vis, scope, access rules).
- D215 — named tuples; D221 covers ONLY record form, tuple-pattern priv в D222 (Plan 124.4).
§G1 Generic types — uniform enforcement (Plan 124.3 amend)
Added 2026-06-02. Plan 124.3 closure.
Per-field priv modifier applies uniformly к generic record types:
export type Stack[T] {
priv mut len int
ro capacity int
}
Enforcement architecture: check site reads RecordField.priv_field
из AST (pre-monomorphization). Mono’d instances (Stack[int],
Stack[str]) inherit identical enforcement — T-substitution не
изменяет field metadata.
Receiver-type tracking (TypeCheckCtx.current_recv_type) uses type-name only:
fn Stack[T] @push(...)body sees recv =Some("Stack").- Generic parameters не factor в name comparison.
- Inside generic methods, accessing priv fields на ЛЮБОЙ T instantiation OWN type’а — allowed.
- Cross-type access (
Stack[int].@fieldизQueue[T].method) — blocked (recv = “Queue”, не “Stack”).
Bootstrap parser limitation: explicit generic prefix в record
literal expression position (Stack[int] { len: 5, capacity: 10 })
не парсится в bootstrap (parser-ambiguity с array-literal opening
[). Canonical form — anonymous literal { len: ..., capacity: ... }
с return-type inference:
export fn Stack[T].with_len(initial int, cap int) -> Stack[T] =>
{ len: initial, capacity: cap } // ✅ anonymous, inferred
Pattern destructure аналогично: Stack { fields } = expr (без
generic args) — resolved through scrutinee type.
INIT path (E_PRIV_FIELD_INIT) testing для generic types использует non-generic specialized variant ИЛИ relies на anonymous literal form’s type inference (target type known via expected return type of enclosing method).
Acceptance — Plan 124.3
ALL closed 2026-06-02:
- A3.1 ✅ Generic type
Stack[T] { priv ... }parser PASS. - A3.2 ✅ Mono’d instance external access: write → E_PRIV_FIELD_WRITE, read → E_PRIV_FIELD_READ.
- A3.3 ✅ Inside
Stack[T].method— @field access OK. - A3.4 ✅ Generic method calling another OK.
- A3.5 ✅ Multiple instantiations (Stack[int] + Stack[str]) — same enforcement.
- A3.6 ✅
Option[Account]— outer Option public, inner Account rules unchanged. - A3.7 ✅ plan124_3 10/10 fixtures PASS.
- A3.8 ✅ Regression plan124_1 9/9 + plan124_2 14/14 unchanged.
D222. Named tuple priv + protocol impl boundary (Plan 124.4)
Status: ✅ ACTIVE since 2026-06-02 (Plan 124.4 closure). Extends: D220 (per-field priv) + D215 (named tuples, Plan 120) + D221 (pattern check). Self-contained sub-decision: covers named tuple form + protocol impl boundary explicitly. Plan: Plan 124.4. Cross-refs: D215 (named tuple form), D220 (core priv), D221 (pattern), D52 §2 (record field syntax).
§1 Named tuple priv syntax
type Vec3(priv x f64, priv y f64, priv z f64)
type Account(priv balance f64, name str) // mixed
type Secret(pub key str, priv salt []u8) // explicit pub override
Same modifier semantic as RecordField: priv before field name (or
pub for explicit public override; reserved для D220 type-level flip
extension Plan 124.7). Mutual exclusion enforced —
priv pub x f64 / pub priv x f64 → E_PRIV_PUB_CONFLICT.
§2 Access rules — uniform with D220
Read (v.x):
- Inside named tuple’s own methods (instance
@fieldor staticT.method(...)) — OK regardless of priv. - Outside →
E_PRIV_FIELD_READесли field marked priv.
Init via named-arg constructor (Vec3(x: 1.0, y: 2.0, z: 3.0)):
- Inside type-method scope — OK (recv = T).
- Outside →
E_PRIV_FIELD_INITдля каждого priv-named arg.
Pattern destructure (Vec3 { x, y, z } = v record-style):
- Same as record (D221 §2-§4): outside →
E_PRIV_FIELD_PATTERNper priv field;..rest is non-binding; nested descent recursive.
Write — N/A: named tuple fields are immutable by D215 design
(no mut modifier на field). Все assignments к v.x = ... fail
E_READONLY_FIELD before priv check.
Positional access .0/.1 — already blocked by Plan 120
E_TUPLE_POSITIONAL_ACCESS_ON_NAMED (D215 Q120 Option B); priv
не нужно добавлять отдельный код.
§3 Protocol implementation boundary
Protocol satisfaction в Nova реализуется ДВУМЯ способами (D186 / Plan 91.9):
-
Type-method impl —
fn Vec3 @to_string() -> str. Receiver = T;current_recv_type = Some("Vec3")→ priv access OK канонически. ✅ Allowed. -
External free-fn —
fn compute_sum(v Vec3) -> f64 => v.x + v.y + v.z. No receiver tracking;current_recv_type = None→E_PRIV_FIELD_READfires при touching priv. ✅ Blocked.
→ Encapsulation guarantee: protocol impls cannot bypass priv boundary unless declared as type-method. Mirrors Rust trait impl rules; stricter than Go/Kotlin (pkg-wide-allowed).
§4 Diagnostic codes
| Code | Site | Plan |
|---|---|---|
E_PRIV_FIELD_READ | Member access на priv named-tuple field | D220 §4 (reused) |
E_PRIV_FIELD_INIT | T(field: ...) named-arg ctor priv field | D220 §4 (reused) |
E_PRIV_FIELD_PATTERN | T { field, ... } = v priv field | D221 §2 (reused) |
E_PRIV_PUB_CONFLICT | priv pub / pub priv mutual exclusion | D220 §6 (reused) |
E_TUPLE_POSITIONAL_ACCESS_ON_NAMED | .0 access | D215 / Plan 120 (preexisting) |
No new codes — D222 reuses D220/D221 codes uniformly. Spec mentions named-tuple context в hint text.
§5 Implementation hooks
| Layer | Site | Change |
|---|---|---|
| AST | NamedTupleField struct | Added priv_field: bool |
| Lexer | KwPriv, KwPub | Already declared (Plan 124.1) |
| Parser | is_named_tuple_decl | Recognize priv/pub as named-marker |
| Parser | parse_named_tuple_fields | Accept priv/pub modifier с conflict-check |
| Checker | f3_check_member NamedTuple arm | Added priv check (mirror Record) |
| Checker | f5_check_tuple_construct | INIT priv check on named-args |
| Checker | check_priv_pattern_recursive | Unified для Record + NamedTuple |
§6 Cross-refs
- D215 — named tuple syntax + access rules.
- D220 — core priv semantics, error codes definition.
- D221 — pattern destructure + spread.
- D52 §2 — record field syntax (mirror form).
- D186 — protocol satisfaction (Plan 91.9): type-method primary, external-fn-impl secondary; D222 §3 formalizes boundary impact.
Acceptance — Plan 124.4
ALL closed 2026-06-02:
- A4.1 ✅ Named tuple
type Vec3(priv x f64, ...)parser PASS. - A4.2 ✅ Positional
.0access — handled by preexisting Plan 120 E_TUPLE_POSITIONAL_ACCESS_ON_NAMED (priv-orthogonal). - A4.3 ✅ Named
.xaccess на priv field outside → E_PRIV_FIELD_READ. - A4.4 ✅ Inside type-method scope — read + init + pattern + protocol method все allow.
- A4.5 ✅ Protocol impl (type-method-based,
fn Vec3 @method()) — priv access OK. - A4.6 ✅ Protocol impl external-fn-based (
fn compute(v Vec3)) — priv access BLOCKED (E_PRIV_FIELD_READ). - A4.7 ✅ plan124_4 10/10 fixtures PASS.
- A4.8 ✅ Regression Plan 120 (8/8) + plan124_1 (9/9) + plan124_2 (14/14) unchanged.
- A4.9 ✅ D222 NEW + D215 cross-ref + D220/D221 code reuse.
- A4.10 ✅ plan120 backward compat — все existing named-tuple
fixtures без
privmodifier работают unchanged.
§T1 nova doc + LSP integration (Plan 124.5 amend)
Added 2026-06-02. Plan 124.5 closure. Cross-references D107 (nova doc schema) + Plan 104.x (LSP infrastructure).
nova doc behavior:
- Default: priv fields hidden from rendered documentation (markdown / HTML / JSON).
--include-privateflag shows all fields сprivkeyword preserved in signature rendering (type X { priv mut f T }).- JSON output emits
"priv_field": true|falseper field regardless of--include-private— consumed by tooling.
LSP integration (forward-ref):
- AST
RecordField.priv_field+NamedTupleField.priv_fieldflags available для LSP hover (Plan 104.2) и completion (Plan 104.3) integration once these ship. - Expected behavior: priv-field filter в autocomplete outside type-method scope; 🔒 priv badge в hover popups; priv-field code-lens decoration.
User-facing documentation:
docs/field-visibility-guide.md— comprehensive guide: use cases, syntax, composition, diagnostics, tooling, comparison vs Go/Rust/TS/Java/Swift/C#, migration, common patterns.
Acceptance — Plan 124.5
ALL closed 2026-06-02:
- A5.1 ✅
nova dochides priv fields by default. - A5.2 ✅
nova doc --include-privateshows priv с keyword preserved. - A5.3 🟡 LSP autocomplete filter — forward-ref Plan 104.3 (infra data source ready).
- A5.4 🟡 LSP hover badge — forward-ref Plan 104.2 (infra ready).
- A5.5 🟡 Quick-fix suggestion — forward-ref Plan 104.x (Plan 50 D102 format hints уже в error messages).
- A5.6 ✅ plan124_5 fixtures 3/3 PASS (parser + smoke; doc behavior e2e verified manually).
- A5.7 ✅
docs/field-visibility-guide.mdcreated (~330 lines). - A5.8 ✅ Regression: existing nova doc fixtures unchanged.
D224. Escape hatches — #test_access + #visible_to (Plan 124.6)
Status: ✅ ACTIVE since 2026-06-02 (Plan 124.6 closure). Extends: D220 §3 (scope rules) + D222 §3 (protocol boundary). Plan: Plan 124.6. Cross-refs: D220, D221, D222 (priv core + pattern + tuple).
§1 Motivation
D220 устанавливает strict type-method-only scope для priv field
access — strictнее чем эталоны (Kotlin internal module-wide,
Rust pub(crate) crate-wide, Java package-private). В некоторых
production scenarios нужна controlled relaxation:
- Unit tests должны verify internal state (balance, cache size, internal cursor pos) без публикации getter в public API.
- Sibling helper types (
Account+Bankaudit utilities) — coordinated access без friend boilerplate.
D224 вводит two explicit opt-in escape hatches — каждый syntactically marked, никаких неявных relaxation.
§2 #test_access(TypeX[, TypeY...]) — fn-level access grant
Attribute перед fn declaration: body fn получает priv-field access
ко всем listed types (READ + WRITE + INIT + PATTERN).
export type Account {
ro name str
priv mut balance f64
}
#test_access(Account)
fn assert_balance_eq(acc Account, expected f64) -> bool =>
acc.balance == expected // ✅ allowed by #test_access
Multi-type form:
#test_access(Account, Vault)
fn cross_audit(a Account, v Vault) -> bool =>
a.balance == 0.0 && v.amount == 0.0
Scope: applies only к body of marked fn. Caller scope unchanged.
Composable: можно combine с #realtime, #blocking, #verify,
etc. — порядок attribute parsing уже supports multi-attribute.
§3 #visible_to(OtherType[, ...]) — field-level friend declaration
Attribute перед field declaration в type X { ... } или
type X(...): methods listed types получают priv access только
к этому field.
export type Account {
ro name str
#visible_to(Bank) priv mut balance f64
}
export type Bank {
ro id str
}
export fn Bank @audit_account(a Account) -> f64 =>
a.balance // ✅ allowed: Bank ∈ Account.balance.visible_to
Per-field granularity:
- Different fields могут have different friend lists.
- Other Account fields without
#visible_to— strict type-only. - Other types (НЕ Bank) — no access:
export fn Auditor @check(a Account) -> f64 => a.balance // ❌ E_PRIV_FIELD_READ — Auditor not in visible_to
§4 Combined access predicate
priv-field access allowed когда любое из:
current_recv_type == tname— canonical type-method scope (D220).tname ∈ current_fn.test_access_for—#test_accessgrant.current_recv_type ∈ field.visible_to— friend grant.
Implementation: TypeCheckCtx::priv_field_access_allowed(tname, &visible_to)
combines all three checks. priv_access_allowed_base(tname) covers
(1)+(2); per-field visible_to requires field-specific context
(handled at each callsite).
§5 Diagnostic codes
D224 reuses Plan 124.1-124.4 codes (no new codes), but hints now mention escape hatches:
[E_PRIV_FIELD_READ] cannot read private field `Account.balance` ...
Hint: add public getter method on `Account`, move accessing code
into a method of `Account`, or use `#test_access(Account)` on test
fn (escape hatch — D224).
Parser-level errors:
#test_access(...)without(→ “требует list:#test_access(TypeX, ...)”.- Empty list
#test_access()→ “требует хотя бы один Type”. #visible_to(...)same shape.
§6 Anti-patterns + lint guidance
Escape hatches — opt-in, syntactically explicit. Recommended discipline:
#test_access— only on test fns or dedicated assertion helpers. Production-code uses должны trigger code-review concern.#visible_to— explicit, named friend types only. Cross-module abuse — code-smell.- Future lint (Plan 124.x): warn if
#test_accessused >N times per project (suggests missing public API).
§7 Cross-refs
- D220 — core priv semantics, scope rules.
- D221 — pattern destructure / spread sites.
- D222 — named tuple + protocol impl boundary.
- D102 — diagnostic format (Plan 50).
- Plan 104.x — LSP hover/completion will display escape-hatch badges (forward-ref).
Acceptance — Plan 124.6
ALL closed 2026-06-02:
- A6.1 ✅
#test_access(TypeX)attribute parser PASS. - A6.2 ✅ Test fn с attribute получает priv access к TypeX.
- A6.3 ✅
#visible_to(TypeY)field attribute parser PASS. - A6.4 ✅ TypeY’s methods get access к marked priv field of TypeX.
- A6.5 ✅ Conservative: только marked fields, не whole type (per-field granular).
- A6.6 ✅ plan124_6 fixtures 7/7 PASS (4 positive + 3 negative).
- A6.7 ✅ Regression plan124_1 9/9 + plan124_2 14/14 + plan124_4 10/10 unchanged.
- A6.8 ✅ D224 NEW + cross-refs к D220-D222.
D225. Type-level priv flip для named tuples (Plan 124.7)
Status: ✅ ACTIVE since 2026-06-02 (Plan 124.7 closure). Extends: D220 §3.3.1 (record-form type-level flip) + D222 (named tuple priv per-field) + D215 (named tuple form, Plan 120). Plan: Plan 124.7. Cross-refs: D220, D215, D222.
§1 Syntax
Symmetric extension to record-form D220 §3.3.1:
// Record form (Plan 124.1 / D220 §3.3.1)
type Account priv {
pub ro name str // explicit pub override
mut balance f64 // default = priv (inherits type-level)
}
// Named tuple form (Plan 124.7 / D225 — this section)
type Secret priv (key str, salt str)
// ^^^^ priv ПОСЛЕ имени type'а, ДО `(`
type Credential priv (pub id str, secret str)
// ^^^ explicit pub override per field
priv keyword position между type name (+ optional generics) и
opening ( — same position как для record form’s {.
§2 Effective priv_field resolution
Per-field priv_field для named-tuple field resolves в parser
identical к record form (D220 §3.3.1):
| field-level modifier | type-level flip | effective |
|---|---|---|
explicit pub | flip or no-flip | false (overrides) |
explicit priv | flip or no-flip | true |
| neither | flip = false | false (default public) |
| neither | flip = true | true (inherits) |
Bidirectional priv pub / pub priv → E_PRIV_PUB_CONFLICT (D220 §6).
§3 Implementation hooks
- AST
TypeDecl.default_field_priv: bool— пере-used (no extension needed; Plan 124.1 уже добавила). - Parser
parse_type_decl: KwPriv после type-name установится вdefault_field_priv(existing — Plan 124.1). - Parser
parse_named_tuple_fields_with_default(default_priv)— NEW wrapper around oldparse_named_tuple_fields. Propagates default в effectivepriv_fieldresolution per field (mirror кparse_record_fields_with_defaultprecedent). - Backward-compat shim
parse_named_tuple_fields()calls _with_default(false).
§4 Access rules — unchanged (D220 §4 / D221 / D222 / D224)
Effective priv_field после resolution applied identically к explicit
per-field priv. All Plan 124.1-124.6 enforcement sites (READ /
WRITE / INIT / PATTERN / spread, + escape hatches #test_access,
#visible_to) work uniform.
§5 Use cases
Invariant-heavy types где majority of fields should be private:
// Encapsulated handles (private impl detail)
type Mutex priv (state u32, owner_fid u64, pub kind MutexKind)
// Sensitive data + opaque session
type Session priv (token []u8, expires_at Instant, pub user_id u64)
// Tightly-coupled coordinate types
type Vec3 priv (x f64, y f64, z f64)
Bimodal coverage matches kubernetes empirical: core/v1 API surface
92% public (use no flip), pkg/internal 53% private (use flip + few pub).
§6 Cross-refs
- D215 — named tuple base syntax.
- D220 §3.3.1 — record-form type-level flip (D225 symmetric).
- D222 — named tuple per-field priv (D225 builds on this).
- D102 — diagnostic format.
Acceptance — Plan 124.7
ALL closed 2026-06-02:
- A7.1 ✅ Parser принимает
type X priv { ... }syntax (record form — Plan 124.1 preserved). - A7.2 ✅ Parser принимает
type X priv (...)syntax (named tuple form — D225 NEW). - A7.3 ✅
pubmodifier на field overrides type-level priv default. - A7.4 ✅ Field без modifier inherits type-level default (priv).
- A7.5 ✅ Type-level + field-level combinations 4 cases verified: default-default ✅, default-explicit ✅, flip-default ✅, flip-explicit ✅.
- A7.6 ✅ plan124_7 fixtures 8/8 PASS (5 positive + 3 negative).
- A7.7 ✅ Regression Plan 120 8/8 + plan124_1 9/9 + plan124_4 10/10 unchanged.
- A7.8 ✅ D225 NEW + cross-refs.
Acceptance — Plan 124.2
ALL closed 2026-06-02:
- A2.1 ✅ Match arm pattern outside → E_PRIV_FIELD_PATTERN.
- A2.2 ✅ IfLet pattern outside → error.
- A2.3 ✅ WhileLet pattern outside → error.
- A2.4 ✅ For-loop pattern outside → error (positive case verifies no false-positive on public-only types).
- A2.5 ✅ Nested Pattern::Record с priv inner → error.
- A2.6 ✅ Spread outside → E_PRIV_FIELD_INIT_SPREAD.
- A2.7 ✅ Inside type-method scope — все hooks allow.
- A2.8 ✅
..rest pattern — no false-positive. - A2.9 ✅ plan124_2 fixtures 14/14 PASS (8+ positive, 6 negative).
- A2.10 ✅ Regression plan124_1 9/9 unchanged.
D226. Signed indexing convention — int для len / capacity / index
D226 RETIRED (Plan 133, 2026-06-09):
usizealias удалён.int= address-sized signed integer на 64-bit Nova target. Используйintдля размеров, индексов, счётчиков байт.usize/isizeбольше не являются допустимыми Nova-типами — компилятор выдаёт ошибку с подсказкой «useint».
Принято 2026-06-03. Формализует существующую практику: extracts из D130 Q3 (2026-05-19, «Indexing → Keep
int, no change»), поднимает в самостоятельный D-блок.
Что
Все API длины, ёмкости и позиции в коллекциях Nova принимают и
возвращают signed int (D129 alias i64), а не unsigned
uint/u64. Это касается @len(), @capacity(), with_capacity(n),
reserve(n), truncate(n), индексных параметров (arr[i],
s.byte_at(i)), позиций (indexOf/find возвращают int с -1 или
Option[int]), и slice-границ (arr[a..b] где a, b — int).
Правило
-
Stdlib invariant. Любая публичная функция в
std/, принимающая или возвращающая «количество элементов» / «размер в байтах» / «индекс позиции», используетint. -
Защита от негатива через контракты. Capacity-API (
with_capacity/reserveи аналоги) добавляютrequires n >= 0(D24). Это compile-time проверка при Z3 backend / runtime debug-assert при TrivialBackend. Compile-error наwith_capacity(-1)без attempted конверсии типа. -
Negative-as-sentinel разрешён. Поиск/позиция могут возвращать
-1как «не найдено» (Java/Go convention) ИЛИOption[int]— stdlib-конвенция в пользуOption[int]для type-safety, но-1sentinel допустим в low-level API (str.find_byte). -
Разности — естественно signed.
a.len() - b.len(),xs.len() - 1, обратные циклыfor i in (0..n).reverse()работают без явных кастов или underflow-паник. На пустой коллекцииxs.len() - 1даёт-1, что валидно как loop-guard вход (for j in 0..-1— пустой range). -
uint/u64— только для bit-twiddling, FFI и pointer bridge. Hash-значения, битовые маски, raw memory addresses, sized-integer аргументы C-API — этоu64/uint.usize/isizeудалены (Plan 133, 2026-06-09) — используйintдля address-sized операций и FFI size parameters. FFI-сигнатуры теперь пишутn int, C-codegen каститintptr_t→size_tвнутри. -
Future-arch path. При миграции Nova на multi-arch (32-bit / WASM)
int= platform-pointer-width signed (=intptr_t),i64остаётся fixed-64. Index API не меняется — auto-scale без breaking change. См. D129 migration note. -
Pointer interactions. Pointer arithmetic и pointer-integer bridges имеют свою numeric matrix, ортогональную stdlib index-API (Rule 1). Все signed для offset/diff (в духе Rule 4 «разности естественно signed»).
usize/isizeудалены — FFI-ABI черезint.Операция Тип Где coll.len()/coll[i]intstdlib index-API (Rule 1) arr[a..b]slice boundsintsub-slice views (D144) ptr + N/*T + Noffsetintpointer arith (D216 §6) — scaled by sizeof(T)ptr - ptr/*T - *Tdiffintelement count (D216 §6) — signed external fn(..., sz int)intFFI ABI (D214, D216 FFI) — codegen casts intptr_t→size_tp as int/int as *Tintexplicit address cast — opaque handle, hash key, GC-hazard (D214 §casts) ptr as u64/i64 as ptru64/i64opaque handle storage (D214 §casts) Правило: stdlib API никогда не использует
uint/u64для index/len/capacity (Rule 1); FFI / pointer arithmetic / cast bridges — единственные легальные exemptions.
Почему
Industry baseline (2026-06).
| Язык | Index/len тип | Знак | Hindsight |
|---|---|---|---|
| Go | int (platform-word) | signed | Сознательный выбор после C |
| Swift | Int (platform-word) | signed | Apple: «harder to make off-by-one errors» |
| Java | int (i32) | signed | Историческое; принято |
| Kotlin | Int (i32) | signed | Mirror Java |
| C# | int (i32) | signed | LongLength для >2B |
| Python | int (arbitrary) | signed | Negative-index slicing |
| TypeScript | number (f64) | signed (de facto) | Один тип |
| Rust | usize (platform) | unsigned | Community regrets vocal |
| C++ STL | size_t (platform) | unsigned | Stroustrup: «I regret using unsigned for size in STL» |
| Zig | usize (platform) | unsigned | Embedded-first рационал |
Счёт 7:3 в пользу signed. Двое из трёх unsigned-языков (C++ и Rust) имеют публичные authorial regrets.
Конкретные выгоды signed int для Nova:
-
Нет underflow-trap.
xs.len() - 1на пустом vec не паникует (даёт-1), в отличие от Rust0_usize - 1→ overflow panic. Это — самая частая newbie-trap в Rust. -
Sentinel
-1. Эргономика find/indexOf без обязательнойOption-аллокации. -
Разности и diff-логика.
a.len() - b.len()валидно signed; sorting comparators, position deltas, scroll offsets — все естественны. -
Mixed arithmetic без ceremony. Никакого
(x as int) + i,(len as int) - 1. AI-first killer-use (D10): LLM пишет signed-индексацию правильно чаще, чем балансируетuint/i64касты. -
Bit-width аргумент мёртв на 64-bit. Signed-
int(=i64) даёт2⁶³ − 1≈ 9.2 × 10¹⁸ элементов — никакая коллекция в адресном пространстве этого не достигнет. -
Совместимость с overflow-семантикой. Plan 33.8 Ф.1:
intoverflow →nv_panic(__builtin_*_overflow). Если ввестиuintдля len, мы заменим один trap (overflow on saturation) на другой (underflow on0 - 1) — без выигрыша. -
Effect/protocol симметрия. Все примитивные методы (
hash/eq/lt/etc., D109) уже работают сint. Введение второго numeric vocabulary для размеров удвоит type-checker complexity без semantic gain.
Type-encoded invariant («n ≥ 0») — частично покрывается контрактами
(requires n >= 0), которые при Z3 backend (D24)
дают compile-time гарантию того же уровня, что unsigned type.
Что отвергнуто
-
uint/u64для index/len (как Rustusize, C++size_t). Отвергнут D130 Q3 (2026-05-19): breaking change для 100+ APIs; underflow-trap хуже missing type-invariant; runtime contract-based check покрывает основной use-case. -
Mixed convention (
uintдля capacity,intдля index). Отвергнут: создаёт постоянные касты на границе API, удваивает protocol-method matrix. -
Refinement type
nat = {x int | x >= 0}как параметр капасити. Отвергнут на bootstrap: refinement-types — long-term Plan 33.x (после full SMT integration); сейчасrequires n >= 0даёт ту же проверяемость без grammar-changes.
Связь
- D129 —
int = i64alias decision (foundation). - D130 —
uintsymmetric pair + Q3 indexing decision (historical origin). - D24 —
requires/ensuresконтракты для compile-time проверки. - D54 —
int as uintsaturation (cross-type bridge). - D109 — встроенные методы примитивов (включая
int). - D141 —
byte_at/bulk slice API используетintиндексы. - D144 — sub-slice views
arr[a..b]— границыint. - D214 —
ptropaque type + cast rules (usize removed, use int for ABI bridge). - D216 —
*Ttyped pointer family + arithmetic (intoffset + diff) + FFI. - Plan 33.8 —
intoverflow → panic (soundness).
Эволюция
- 2026-05-19 (D130 Q3): Решение «keep
intfor indexing, no change» принято внутриuintplan’а — внутри одного из четырёх Q-вопросов, не findable отдельно. - 2026-06-03 (D226, этот блок): формализация в самостоятельное
D-решение + правило
requires n >= 0на capacity-API + cross-language baseline + future-arch migration path. - 2026-06-03 (D226 amend, pointer-aware): §5 расширен для
usizeABI bridge + pointer-integer casts; §7 «Pointer interactions» с numeric matrix для всех ptr ops; cross-refs на D214 + D216. Закрывает gap research’а §3 (docs/research/08). - 2026-06-09 (Plan 133):
usize/isizeудалены из Nova.int= address-sized signed integer (intptr_tна 64-bit). FFI-сигнатуры используютint, codegen каститintptr_t→size_tвнутри. D226 RETIRED в частиusize/isizealias-semantics.
Acceptance criteria
-
std/collections/hashmap.nvwith_capacity(min_capacity int) requires min_capacity >= 0 -
std/collections/set.nvwith_capacity(cap int) requires cap >= 0 -
std/runtime/string_builder.nvwith_capacity(n int) requires n >= 0 -
std/runtime/write_buffer.nvwith_capacity(n int) requires n >= 0 - D226 spec block с industry baseline + rationale + rejected alternatives
-
[]T.with_capacity/[]T.reservebuilt-in: requires-clause вcompiler-codegenexternal_registry — followup[M-D226-builtin-capacity-requires] -
nova checklint W_D226_NEGATIVE_LITERAL — warn наwith_capacity(-N)при literal-args (без Z3) — followup[M-D226-negative-literal-lint] -
_experimental/capacity APIs (Queue.with_capacity) — sweep после promotion в stable. -
isize/usizeудалены (Plan 133, 2026-06-09) — closes[M-D226-isize-usize-alias-D-block].
Amend 2026-06-03 — usize / isize formal alias D-block
RETIRED (Plan 133, 2026-06-09):
usizeиisizeудалены как Nova-типы. Используйintдля размеров, индексов и address-sized операций.uintостаётся для беззнаковых битовых операций и FFI. Раздел оставлен как исторический контекст.
Closes followup [M-D226-isize-usize-alias-D-block].
Definition (HISTORICAL — типы удалены в Plan 133):
| Alias | Bootstrap (64-bit) | Future arch |
|---|---|---|
usize | u64 (= uint64_t) | platform-pointer-width unsigned |
isize | i64 (= nova_int) | platform-pointer-width signed |
Use cases:
-
FFI ABI bridge (primary use) — C
size_t/ptrdiff_t(HISTORICAL, до Plan 133):// БЫЛО (до Plan 133): external fn malloc(sz usize) -> Option[*u8] // C: size_t external fn read(fd int, buf *mut u8, n usize) -> isize // C: size_t, ssize_t // СТАЛО (Plan 133): external fn malloc(sz int) -> Option[*u8] // C: size_t — codegen casts intptr_t→size_t external fn read(fd int, buf *mut u8, n int) -> int // C: size_t, ssize_t -
Pointer differences (D216 §6) —
ptr - ptr → int, signed semantically. -
Platform-pointer-width —
int=intptr_tна текущих 64-bit targets (Plan 133).
НЕ для:
len/capacity/index APIs — используютint(per D226 Rule 1). Reason: signed convention; arithmetic safety (signed underflow detectable).- General-purpose unsigned arithmetic — используют
uint(=u64alias) per Plan 70.5.
Casts (после Plan 133):
Spec drift fix: isize/usize использовались в D216/D214 examples и
Plan 118 FFI без formal D-block aliasing. Plan 133 удаляет эти типы целиком.
Implementation: compiler-codegen/src/codegen/emit_c.rs + types/mod.rs
type_ref_to_c + TyCat::Int + BUILTIN_TYPE_NAMES registry updated 2026-06-03.
Cross-refs:
- D129 —
int=i64aliasing - D216 §6 — pointer arithmetic
- Plan 118.1 — FFI signatures use usize
Plan 124.8 — Tuple+Value-Record design refinement (2026-06-02)
Sub-plan Plan 124 V2 refinement. Amends 6 D-blocks + introduces 1 NEW. Status: ✅ ACTIVE since 2026-06-02 (Ф.0-Ф.5 closed).
D33 amend §«binding propagation» (Plan 124.8 Ф.2)
⚠️ AMENDED by D216 V3 §V3.1 (2026-06-04, refined 2026-06-05 Ф.6) — rows «
ro x mut T» / «mut x ro T» в этой таблице storage-class qualified:
- Type-form
ro mut T/mut ro T(без имени между modifier’ами): forbidden когда T = value type (см. §V3.1 — primitives, value records, named/anonymous tuples, Unit). ErrorE_MUTABILITY_CONFLICT_VALUE_TYPE.- Binding-form
ro x mut T/mut x ro T(с именем между): allowed regardless of T storage class (Ф.6 relaxation, 2026-06-05).
ro/mut на binding по default распространяется на тип справа.
Explicit повторение модификатора — redundant error.
| Декларация | Парсится | Семантика |
|---|---|---|
ro x T | ✅ default | binding ro, type implicit ro |
mut x T | ✅ default | binding mut, type implicit mut |
ro x ro T | ❌ E_REDUNDANT_TYPE_MODIFIER | то же что ro x T |
mut x mut T | ❌ E_REDUNDANT_TYPE_MODIFIER | то же что mut x T |
ro x mut T | ✅ NEW | binding ro, content mut (cannot reassign, can mutate) |
mut x ro T | ✅ existing | binding mut, content ro (can reassign, cannot mutate) |
Closes D176 §«type-modifier в любой позиции» partial parser implementation
gap — mut T теперь принимается в binding type annotation position.
D33 amend §«consume binding-only — distinction rationale» (Plan 118.5 V2, 2026-06-04)
Closes [M-118.5-consume-as-type-modifier].
consume НЕ становится type-level wrapper parallel к ro / mut / unsafe.
Stays binding-only modifier (per D131, D133, D162, D164).
Rationale:
ro / mut / unsafe — syntactic compile-time modifiers expressing
mutability / safety contract на the type. consume — fundamentally
different: it expresses ownership transfer / linearity / drop
obligation — semantic D-блоки D131 (linearity), D133 (consume types),
D162 (consume types implementation), D164 (D-block consume types).
Examples of the asymmetry:
ro Tvalue — readonly view; multiple readonly aliases allowed.mut Tvalue — mutable; subject к binding-dominates rule (D175 amend).unsafe Tvalue — MaybeUninit; read requires assertion (D216 V2 §V2.3).consume Tvalue — owned uniquely; passing transfers ownership, drops invariants at scope exit. Not a syntactic property of T; a structural property of the binding.
Hypothetical consume * T («consume pointer») would mean a pointer that
the caller must consume — но это уже expressed via record-wrapped pointer
(type Handle consume(* T)). Plain consume T за T уже-not-consume
makes no semantic sense.
Decision: keep current consume design (D162, D164). Right-binding
rule applies only к ro/mut/unsafe. Reject Plan 118.5 V2 followup
[M-118.5-consume-as-type-modifier] as NO ACTION — consume already
fits its semantic-binding role correctly.
D33 amend §«Projection-chain mutability check» (Plan 128.2, 2026-06-06)
Closes
[M-128.1-ro-binding-field-chain-not-mut](P1 safety hole opened в Plan 128.1 Ф.3).
D33 locality-of-mutation invariant (Plan 108.2 D36 enforcement) is extended: mut-method dispatch проверяет mutability root binding’а lvalue projection chain, не только когда receiver — голый identifier.
Rule:
Для Call obj.method(...), где method — mut-method (fn T mut @method),
type-checker walks Member/IndexAccess chain от obj к root:
walk_root(e) =
| Ident(name) → Some(name)
| Member { obj, .. } → walk_root(obj)
| IndexAccess { obj, .. } → walk_root(obj)
| _ → None
Если walk_root(obj) = Some(name):
local_mut[name] == Some(false)(ro local) →E_LOCAL_NOT_MUT(илиE_RECEIVER_BINDING_NOT_MUT— chain hint в note).param_mut[name] == Some(false)(ro param) →E_PARAM_NOT_MUT.- otherwise — OK.
Если walk_root(obj) = None (chain начинается с rvalue base — Call
result, literal, …): no enforcement; mutation в hoisted temp семантически
no-op (D32 «mutate-by-copy для rvalue» — D215 amend «Method receiver
passing» Ф.2 §rvalue receiver).
Receiver shapes table:
| Receiver shape | binding | Result |
|---|---|---|
b.set_x() | ro b | E_LOCAL_NOT_MUT (existing) |
b.v.set_x() | ro b | E_LOCAL_NOT_MUT (NEW — chain root) |
arr[0].set_x() | ro arr | E_LOCAL_NOT_MUT (NEW — chain root) |
b.parts[i].v.set_x() | ro b | E_LOCAL_NOT_MUT (NEW — chain root) |
b.v.set_x() | mut b, mut v field | OK |
make_body().v.set_x() | rvalue base | OK (no-op semantically — temp) |
Symmetry с D175 (readonly field freeze): projection-chain root check
независимая ось от per-field readonly enforcement. mut b; b.v.set_x()
с ro v field остаётся E_FIELD_NOT_MUT (D175 invariant); orthogonal
к D33 root walk.
Cross-ref: D215 amend «Method receiver passing» (Plan 128.1 Ф.1)
implements call-site codegen (&(b->v), &(arr->data[i])) для lvalue
projection — это codegen pair того же chain-walking; D33 root check —
type-checker pair. Symmetric infrastructure: оба обходят
Member/IndexAccess chain (codegen — для emit, type-checker — для
binding-mutability gate).
Implementation: helper lvalue_root_ident в types/mod.rs
вызывается из consume_walk_expr Call arm для receiver. Pure-read
methods (x.abs()) не подпадают под gate — registered || builtin_mut_method guard сохраняется.
D215 cross-ref §«projection root binding mutability» (Plan 128.2, 2026-06-06)
Pair note к D33 amend §«Projection-chain mutability check».
D215 lvalue-projection mut-method ABI (Plan 128.1 Ф.1) — &(b->v) для
b.v.method(), &(arr->data[i]) для arr[i].method() — corresponds к
codegen pair того же invariant’а. Type-checker side (D33 projection-chain
root walk) проверяет, что root binding lvalue chain’а — mut; codegen
side (D215 §«lvalue projection receivers») emit’ит pointer на slot.
Implication для users: writing b.v.set_x() requires mut b
binding AND mut v field (D175 cross-ref) AND mut-receiver
target method (D32). All three axes independent; missing любой —
distinct error code (E_LOCAL_NOT_MUT / E_FIELD_NOT_MUT / receiver-mode
mismatch).
Plan 128.2 §Markers closure: [M-128.1-ro-binding-field-chain-not-mut]
(P1) — closed via root-walking enforcement.
D216 V2 amend §V2.2b «mut T transparent» (Plan 118.5 V2, 2026-06-04)
Closes [M-118.5-mut-t-vs-binding-distinction].
TypeRef::Mut(T, span) AST wrapper introduced in Plan 118.5 V1 (per V2
right-binding rule §V2.1) is purely transparent at C codegen level:
- AST representation:
Mut(inner, span)carries no extra semantic vsinner - C codegen:
mut Temits same C type asT(через transparent recurse) - Type-checker:
mut Tdoes NOT impose mutability requirement at the type level — the mutability semantic belongs к binding-level mut (Plan 108 D176), не к type-level.
Why mut T exists at all:
Only purpose is syntactic uniformity under right-binding rule §V2.1.
Without mut T arm в parse_type, the user’s natural extension of ro T
к mut T would fail к parse (no recursive arm). Adding the arm makes the
grammar regular and predictable. The wrapper has zero semantic at runtime.
Practical implications:
mut intparses asMut(Named("int"))— wrapper transparent for codegen (emits justnova_int). Type-checker doesn’t validate Mut(non-Pointer) as anything special.*mut Tparses asPointer(Mut(T))—Mutis the pointee modifier (postfix, INNER of Pointer), означает writable target.mut * T(Mut снаружи Pointer) НЕ строится из синтаксиса — prefix перед*запрещён (E_POINTER_PREFIX_MODIFIER, §1, Plan 138.5). «Mut pointer» (reassignable binding) выражается binding’омmut p *T, не type-wrapper’ом.let mut x int = ...— the binding mut (Plan 108) provides mutation rights.let x mut int = ...would parse but themutwrapper onintdoesn’t grant mutation (bindingxis implicit ro per Plan 108).
Disambiguation reference:
| Form | Meaning | Source |
|---|---|---|
let mut x T = ... | Binding x is mutable | Plan 108 D176 |
let x mut T = ... | Binding x is ro; type wrapper transparent (no mutation rights) | Plan 118.5 V2 §V2.2b |
let mut x mut T = ... | Binding mut; type wrapper transparent (same as let mut x T = ... semantically) | combined |
User-visible recommendation: prefer binding-level mut; type-level
mut T is for syntactic uniformity only.
D218 RETRACTED (Plan 118.5 V2, 2026-06-04)
Closes [M-118.5-d218-maybeuninit-duplication].
D218 (Plan 118.2 — «Slice fat-pointer + MaybeUninit[T] + ManuallyDrop»)
partially retracted — the MaybeUninit[T] sub-design is subsumed by
Plan 118.5 V2 §V2.3 first-class unsafe T wrapper.
What D218 proposed:
MaybeUninit[T] generic wrapper providing «memory typed as T but may be
uninitialized» semantic. Caller asserts validity via assume_init()
method.
Why subsumed:
Plan 118.5 V2 §V2.3 promoted unsafe T to first-class type wrapper with
exactly the MaybeUninit semantic:
- init/layout/aliasing/identity contracts off (per §V2.3)
- read requires
unsafe { }wrap (E_UNSAFE_T_READ_REQUIRES_WRAP) - write safe (transitions к valid)
- narrow
unsafe T → Trequires explicit unsafe cast (E_UNSAFE_T_NARROW_REQUIRES_UNSAFE)
This first-class wrapper:
- Composes orthogonally с pointer modifier («ptr к uninit T» =
*unsafe T, postfix pointee — Plan 138.5) - Doesn’t require generic instantiation
- Uses universal right-binding rule for grammar uniformity
- Provides finer-grained codegen control (NPO recalc per §V2.4)
What survives in D218:
The «slice fat-pointer» portion of D218 remains a valid sub-plan (Plan 118.2 Ф.1+Ф.2). ManuallyDrop redesign is separate.
Migration path для users:
| D218 form (deprecated) | Plan 118.5 V2 form |
|---|---|
MaybeUninit[i32] | unsafe i32 |
MaybeUninit<T>::uninit() | mut x unsafe T = uninit_value (write-safe init) |
m.assume_init() | unsafe { m as T } (narrow cast) |
*mut MaybeUninit<T> | *unsafe T (postfix pointee к uninit T) |
MaybeUninit[T] type itself not added в std. D218 spec section marked
RETRACTED для MaybeUninit subset; slice + ManuallyDrop subsets remain
unchanged pending Plan 118.2 implementation.
D216 V2 amend §V2.3b «E_UNSAFE_ARG_REQUIRES_WRAP + E_UNSAFE_T_NARROW_REQUIRES_UNSAFE» (Plan 118.5 V2)
Closes [M-118.5-narrow-cast] + [M-118.5-arg-coerce-unsafe] spec slots.
Two new error codes added для Plan 118.5 V2:
E_UNSAFE_T_NARROW_REQUIRES_UNSAFE— explicit narrow castx as T(where x: unsafe T binding and T is non-unsafe target) outside unsafe block. Caller must assert value validity viaunsafe { x as T }.E_UNSAFE_ARG_REQUIRES_WRAP— passing unsafe-T binding as Ident argument к function/method parameter whose declared type is NOTunsafe T. Param- level mismatch detected via ConsumeRegistryfn_non_unsafe_params/method_non_unsafe_paramsregistries.
Both errors are suppressible by:
- Wrapping the entire enclosing expression в
unsafe { ... }block (depth > 0 disables the checks). - Re-declaring the callee parameter as
unsafe T(then arg-coerce matches).
D216 V3 amend (Plan 118.5 V3, 2026-06-04) — 4 modifier composition rules
⚠️ LARGELY SUPERSEDED — Plan 138.5 (2026-06-11): V3 строился вокруг prefix-модификаторной пропагации (outer modifier перед
*распространяется через Pointer на pointee) +safe-стоппера. После запрета prefix перед*(E_POINTER_PREFIX_MODIFIER, §1) пропагировать нечего:
- §V3.3 (right-binding propagation через Pointer) — SUPERSEDED (нет outer-модификатора над Pointer → ничего не пропагируется).
- §V3.4
safeстоппер +Unsafe(Pointer)пропагация — RETIRED (safeостанавливал outer-unsafe-пропагацию, которой больше нет). Type-level E_REDUNDANT дляro * ro T/unsafe * unsafe T— moot (prefix запрещён).- §V3.2 ordering — flip на
ro unsafe T(safety-INNER, вплотную к базе, какexternal unsafe fn); касается только value-T / pointee, редкий случай.- §V3.1 (ro+mut adjacency на value-T) — KEPT (про value-T mutability class, не про указатели); pointer-примеры в нём заменены на postfix/binding. Builds on (residual): D216 V1 (postfix pointee) + V2 §V2.3 (
unsafe Tvalue).
§V3.1 — Storage-class-aware ban на ro+mut adjacency
ro и mut — modifiers same mutability class. Their combination at type
level OR binding+content level requires storage-class qualification.
Rule:
Distinction binding-form vs type-form (V3 amend 2026-06-05):
§V3.1 storage-class ban applies ONLY к type-position ro mut T /
mut ro T (where both modifiers appear consecutively на одном уровне
TypeRef). Binding-position ro x mut T / mut x ro T (modifiers
вокруг имени параметра/локала) — orthogonal binding modifiers, ALWAYS
allowed regardless of T’s storage class. Closes
[M-118.5-V3-binding-context-relaxation] (was V4 deferred).
-
For value-type T (storage IS value):
- primitives (
int,bool,f64, etc.) - value records (
type X value { ... }per Plan 124.8 D228) - named tuples (
type Point(x f64, y f64)per Plan 120 D215)
Type-position conflicting combinations →
E_MUTABILITY_CONFLICT_VALUE_TYPE:fn f(p *ro mut int) // ❌ — pointee ro+mut on value T (postfix chain) fn f(p *mut ro Point) // ❌ — same (postfix) fn f(p ro mut int) // ❌ — type-form (name absent before modifiers) fn f() -> mut ro str // ❌ — return type-form type X { field ro mut Acc } // ❌ if Acc is value record(Note Plan 138.5: prefix
* ro mut intсам по себе —E_POINTER_PREFIX_MODIFIER; pointee-adjacency*ro mut intpostfix — это §V3.1 value-T conflict.)Binding-form ALLOWED для value-T:
fn f(ro x mut int) // ✅ — binding-form (ro pre-name, mut post-name) // ro x: no rebind (binding-level) // mut: mut-method access (binding-level) fn f(mut x ro int) // ✅ — symmetric let ro x mut int = 5 // ✅ — local-binding form (parser may flag E_LOCAL_* // for non-mut mutation attempts — orthogonal) - primitives (
-
For reference-type T (T-as-pointer-к-data semantically):
- records (
type X { ... }default — heap) - arrays
[]T - heap-tracked types
Both type-form AND binding-form VALID:
fn f(ro mut Acc) // ✅ type-form, ref-T (Readonly(Mut(Acc))) // semantically: ro binding to mut content fn f(ro acc mut Acc) // ✅ binding-form // ro acc: no rebind / mut access on binding fn f(mut acc ro Acc) // ✅ symmetric binding-form - records (
For-loop exception: for y in iter — loop variable y semantically
ro but reassigned per iteration. Plan 108.3 loop-var rule preserves this
behavior; V3 §V3.1 does NOT fire on loop-var-introduced rebindings.
Value types per V3 (user-confirmed 2026-06-04):
- Primitives (full list):
- Numeric:
int(address-sized, = i64 on 64-bit; use for sizes, indices, counts),uint,i8/i16/i32/i64,u8/u16/u32/u64,f32,f64 - Other:
bool,char,byte(aliasu8),str,ptr - Note:
usize/isizeremoved (Plan 133, 2026-06-09) — useint.
- Numeric:
- Value records:
type X value { ... }(Plan 124.8 D228) - Named tuples:
type Point(x f64, y f64)(Plan 120 D215) - Anonymous tuples:
(A, B, C)literal type syntax - Unit:
()(zero-size value)
Reference types per V3:
- Records (
type X { ... }default — heap) - Arrays
[]T/ FixedArray[N]T(heap-tracked для elements) - Pointer (any modifier wrapping)
- Func, Protocol
Storage class detection (compiler-codegen/src/types/mod.rs helper):
fn is_value_type_for_v3(ty: &TypeRef, type_decls: &TypeDeclRegistry) -> bool {
use TypeRef::*;
match ty {
Named { path, .. } if path.len() == 1 => {
let name = path[0].as_str();
// Primitives (Plan 133: isize/usize removed; int = address-sized)
if matches!(name,
"int" | "uint"
| "i8" | "i16" | "i32" | "i64"
| "u8" | "u16" | "u32" | "u64"
| "f32" | "f64"
| "bool" | "char" | "byte" | "str" | "ptr") { return true; }
// User type: value record OR named tuple
if let Some(td) = type_decls.get(name) {
return td.is_value_record() || td.is_named_tuple();
}
false
}
Tuple(..) => true, // anonymous tuples — value
FixedArray(..) => false, // [N]T — heap-tracked elements
Array(..) => false, // []T — heap
Pointer(..) => false,
Func { .. } => false,
Protocol { .. } => false,
Unit(..) => true,
// Modifier wrappers strip к inner
Readonly(inner, _) | Mut(inner, _) | Unsafe(inner, _) => {
is_value_type_for_v3(inner, type_decls)
}
}
}
Note re int (Plan 133 amend, 2026-06-09):
int = address-sized signed integer (= i64 on 64-bit); isize и usize удалены.
uint = address-sized unsigned (= u64 on 64-bit). V3 storage-class check
распознаёт int/i64 как одно и то же value type.
Conflict detection — at check_decl_type (compiler-codegen/src/types/mod.rs):
fn check_v3_ro_mut_conflict(
ty: &TypeRef,
type_decls: &TypeDeclRegistry,
errors: &mut Vec<Diagnostic>,
) {
// Recursive walk:
// For each Readonly(Mut(inner)) or Mut(Readonly(inner)) AST shape found,
// determine inner's storage class. If value-type → error.
// Pure-type-level (in pointer/array interior) — also error for
// value-type T (no binding context to disambiguate).
// For reference-type T, allow at binding-context (let/param/field),
// disallow at nested-constructor position.
...
}
Retracts D33 amend rows: the previous unconditional «ro x mut T ✅»
and «mut x ro T ✅» entries get storage-class qualification per §V3.1.
§V3.2 — Modifier ordering (FLIPPED to safety-inner: ro unsafe T)
⚠️ FLIPPED (Plan 138.5): прежнее правило «safety-outer / mutability-inner» (
unsafe ro T) перевёрнуто на safety-inner / mutability-outer (ro unsafe T) —unsafeвплотную к базе T, какexternal unsafe fnставитunsafeвплотную кfn. Семантически свободно: оси ортогональны (Readonly(Unsafe(T))≡Unsafe(Readonly(T))). Касается только value-T и pointee (после forbid-prefix*ro unsafe T— редкий случай); для указателя-binding ordering не возникает.
Rule (FINAL): ro/mut (mutability class) — outer; unsafe (safety
class) — inner (вплотную к базе). Reverse → E_MODIFIER_ORDER.
Rationale: консистентность с keyword-формой external unsafe fn (unsafe
непосредственно перед тем, что оно квалифицирует). Оси независимы, выбор —
вопрос единообразия записи.
| Form | AST | Status |
|---|---|---|
unsafe T | Unsafe(T) | ✅ |
ro unsafe T | Readonly(Unsafe(T)) | ✅ (safety-inner) |
mut unsafe T | Mut(Unsafe(T)) | ✅ |
unsafe ro T | Unsafe(Readonly(T)) | ❌ E_MODIFIER_ORDER |
unsafe mut T | Unsafe(Mut(T)) | ❌ E_MODIFIER_ORDER |
*ro unsafe T | Pointer(Readonly(Unsafe(T))) | ✅ (pointee: ro outer, unsafe inner) |
*unsafe ro T | Pointer(Unsafe(Readonly(T))) | ❌ E_MODIFIER_ORDER (pointee) |
Note (Plan 138.5): prefix-формы (
ro * unsafe T/unsafe * ro T) сами по себе теперьE_POINTER_PREFIX_MODIFIER(§1) — ordering-проверка применяется только к value-T и к pointee содержимому (постфикс после*).
Detection: parser KwRo/KwMut arms check inner.contains_unsafe_in_chain()
helper — recursive walk через Readonly/Mut wrappers (stopping at Pointer /
Named/Array/Tuple/Func boundaries). If found, emit E_MODIFIER_ORDER.
§V3.3 — Right-binding propagation semantics (SUPERSEDED, Plan 138.5)
⚠️ SUPERSEDED (Plan 138.5): §V3.3 описывал, как outer модификатор (перед
*) семантически распространяется через Pointer на pointee. После запрета prefix перед*(E_POINTER_PREFIX_MODIFIER, §1) outer-модификатора над Pointer не существует → пропагировать нечего → правило отозвано.
⚠️ FINAL amend (Plan 147 D246, 2026-06-12; supersedes flip-scan-draft): «никакого наследования нет» — подтверждено. Под три-осевой моделью (D246) bare
*T= pointee ro (*T ≡ *ro T) во ВСЕХ позициях; pointee-mut НЕ наследуется от binding. Только*mut Tпишется явно для mut-pointee;*ro T→E_REDUNDANT_POINTER_RO. Outer-prefix-пропагация остаётся отозванной (prefix-ban).
В FINAL-модели (D246) bare *T = pointee ro (*T ≡ *ro T); writable pointee —
только через явный *mut T:
mut p *T // Pointer(T) ≡ Pointer(Readonly(T)) — pointee ro (D246; L1 mut ≠ mut-pointee)
ro p *T // Pointer(T) — pointee ro; p фиксирован
*mut T // Pointer(Mut(T)) — explicit mut pointee (единственный опт-ин на *p = …)
*ro T // ❌ E_REDUNDANT_POINTER_RO (избыточно; fix-it *T)
*unsafe T // Pointer(Unsafe(T)) — pointee possibly-uninit
Реассайнабельность указателя — binding (let/mut, D36), не пропагация типа.
Старые prefix-примеры (ro * T, unsafe * T, unsafe * safe T) — теперь
parse error (E_POINTER_PREFIX_MODIFIER). Helpers contains_unsafe_in_chain /
contains_same_class_in_chain остаются нужны лишь для value-T ordering
(§V3.2), не для pointer-пропагации.
§V3.4 — safe keyword RETIRED + E_REDUNDANT_TYPE_MODIFIER (binding-level only)
⚠️ RETIRED (Plan 138.5):
safeмодификатор был propagation-stopper для outer-unsafe, существовавшей только в prefix-формеunsafe * safe T. После запретаunsafe *(prefix, §1) outer-unsafe-пропагации нет → стопить нечего →safeбесполезен → RETIRED. usage в std = 0. Standalonesafe T≡Tи так был no-op. Lexer-токенsafeв type-position — теперь ошибка (E_POINTER_PREFIX_MODIFIER family / E_SAFE_RETIRED, см. §V3.5);safe_stoppers/is_safe_stopped_betweenparser-машинерия — dead.
E_REDUNDANT_TYPE_MODIFIER (FINAL scope): V2 covered binding-level
(ro x ro T) — KEPT. V3 type-level prefix-chain extension
(ro * ro T / unsafe * unsafe T) опиралась на prefix-пропагацию — moot,
т.к. prefix перед * сам по себе E_POINTER_PREFIX_MODIFIER (§1).
| Form | Status |
|---|---|
ro x ro T — binding ro + duplicate type-level ro | ❌ E_REDUNDANT_TYPE_MODIFIER (binding-level, KEPT) |
ro T ro — duplicate ro at same level | ❌ E_REDUNDANT_TYPE_MODIFIER |
*ro ro T — duplicate pointee ro (postfix) | ❌ E_REDUNDANT_TYPE_MODIFIER (pointee chain) |
ro * ro T (prefix) | ❌ E_POINTER_PREFIX_MODIFIER (prefix перед * — §1; не доходит до redundancy) |
unsafe * unsafe T (prefix) | ❌ E_POINTER_PREFIX_MODIFIER (§1) |
ro * safe ro Tunsafe * safe unsafe T | RETIRED — safe отозван |
Detection (parser/mod.rs, FINAL):
// Value-T / pointee chains only (no prefix-before-* allowed):
// In KwRo/KwMut/KwUnsafe arm after recursive parse_type:
// if inner immediately repeats same modifier class → E_REDUNDANT_TYPE_MODIFIER
// `safe` stopper machinery removed (no propagation to stop).
§V3.5 — New error codes registered
| Code | Description | Spec section |
|---|---|---|
E_MUTABILITY_CONFLICT_VALUE_TYPE | ro+mut adjacency on value-type T | §V3.1 |
E_MODIFIER_ORDER | unsafe wrapping ro/mut (safety-INNER rule, flipped Plan 138.5) | §V3.2 |
E_REDUNDANT_TYPE_MODIFIER | same-class modifier repetition (binding-level + value-T/pointee chains) | §V3.4 |
E_POINTER_PREFIX_MODIFIER | ro/mut/unsafe token перед * в type-position (Plan 138.5; extends E_INVALID_POINTER_MODIFIER) | §1 |
safeмодификатор RETIRED (Plan 138.5):safe Tв type-position больше не валиден (был V3.4 propagation-stopper; пропагации нет). Parser трактуетsafeперед*/типом какE_POINTER_PREFIX_MODIFIERfamily (или dedicatedE_SAFE_RETIRED— выбор enforce-фазы Ф.2).safe_stoppersmachinery dead.
E_PARAM_MOD_CONFLICT preserved для дисциплинирующих случаев:
mut consume name T/consume mut name T(D131 conflict)mut readonly name T(legacy form)
§V3.1 amend (2026-06-05): E_PARAM_MOD_CONFLICT LIFTED для
ro x mut T (pre-name ro + post-name mut) — orthogonal binding modifiers
per binding-context relaxation. Symmetric mut x ro T уже работал
(pre-name mut + post-name unhandled — falls to type-level Readonly(T)).
§V3.6 — Migration impact (V2 → V3)
Low breakage per discovery audit:
nova_tests/plan108_1/readonly_mut_conflict_neg.nv+mut_readonly_conflict_neg.nv— already NEG tests; keep expected error code OR migrate к newE_MUTABILITY_CONFLICT_VALUE_TYPE(binding-level distinction preserved per §V3.5)nova_tests/plan118/t1_9_chain_modifiers_ok.nv:15—*ro mut Acc(postfix pointee chain) — Acc context-dependent. Plan 138.5 Ф.2: rewrite к value-record для NEG demonstration, OR drop the redundant modifier.nova_tests/plan118_5/*+plan118_5_v3/*— built on prefix forms +safe-stopper (retired). Plan 138.5 Ф.2: convert к NEG fixtures (mut * T/safe T→E_POINTER_PREFIX_MODIFIER/E_SAFE_RETIRED) OR delete/rewrite к postfix-pointee equivalents.- stdlib
std/runtime/raw_mem.nv— uses*u8and prefixmut * u8→ migrate prefix к postfix*mut u8(Plan 138.5 Ф.2). bare*u8unchanged.
§V3.7 — Followup markers opened
[M-118.5-V3-safe-keyword-impl]— RETIRED by Plan 138.5 (safeмодификатор отозван; пропагации нет — стоппер бесполезен).[M-118.5-V3-ro-mut-storage-class]— Ф.2 type-checker storage-class detection + check (§V3.1 value-T — KEPT)[M-118.5-V3-modifier-order]— §V3.2 ordering (FLIPPED к safety-inner, Plan 138.5) — value-T / pointee only[M-118.5-V3-redundant-extension]— E_REDUNDANT binding-level + pointee chains (prefix-chain part moot —safeescape retired)- ✅
[M-118.5-V3-binding-context-relaxation]— CLOSED 2026-06-05 — binding-formro x mut T(и симметричноеmut x ro T) allowed regardless of T storage class. Parser E_PARAM_MOD_CONFLICT lifted для pre-name ro + post-name mut combo. Type-form §V3.1 storage-class check unchanged. [M-138.5-pointer-prefix-enforce]— Ф.2 parser/checker enforceE_POINTER_PREFIX_MODIFIER+ retiresafe/Unsafe(Pointer)+ migrate prefix usages (Plan 138.5).
D52 amend (Plan 124.8 Ф.2)
7-я форма declaration: value record — type X value { ... }.
Stack-allocated reference type с copy-on-pass semantics (D32 amend).
Composable с consume/priv модификаторами в каноническом порядке
value consume priv (см. amend ниже — order-independence RETIRED).
value — contextual keyword (recognized только в type Name[Generics] [modifiers] value [modifiers] { position; identifier value остаётся
валидным во всех других позициях для backward compat).
Canonical modifier order: type X value consume priv { ... } —
allocation → ownership → visibility (outer → inner).
AMEND 2026-06-12 (Plan 148 Ф.1 / D241, [M-138-canonical-modifier-order]): Parser больше НЕ order-independent — out-of-canon порядок модификаторов теперь hard error
E_MODIFIER_ORDER(с machine-applicable fix-it), а не отложенный lintW_NON_CANONICAL_TYPE_MODIFIER_ORDER. Полное правило (canonical ranks, обобщение на новые модификаторы) — D241 в 03-syntax.md.
D175 amend §«binding dominates» (Plan 124.8 Ф.3)
ro acc binding теперь блокирует write к любому полю объекта, даже
если поле помечено mut field T. Rust-style правило.
| Field declaration | ro acc binding | mut acc binding |
|---|---|---|
field T | ❌ переприсвоить, ❌ мутировать | ✅ / ✅ |
ro field T | ❌ / ❌ | ❌ / ❌ (always frozen) |
field ro T | ❌ / ❌ | ✅ / ❌ |
mut field T | ❌ / ❌ (was ✅ / ❌ before amend) | ✅ / ✅ |
mut field ro T | ❌ / ❌ (was ✅ / ❌ before amend) | ✅ / ❌ |
Changed: mut field теперь НЕ “always-mutable” — binding dominates.
D175 amend §V2 «binding dominates — explanatory consolidation» (2026-06-04)
Rationale: Clarifying amend без semantic change. Consolidates rules разбросанные между D33 amend, D36, D175 amend, D176 V1, Plan 108.1-108.3 в единое explanatory section. Никаких behavior changes — все existing rules stay valid.
✅ KEEP = L2-ось (Plan 147 D246, 2026-06-12): этот §V2 «binding dominates / access-time enforcement» — в точности L2 view-семантика (транзитивный ro/rw freeze по owned-графу значения, access-time). Под три-осевой моделью (D246) сохраняется без изменений + добавлено уточнение P4: L2 freeze ОСТАНАВЛИВАЕТСЯ на каждом
*(см. ниже §«L2 wall-at-*»). За указателем транзитивный freeze не действует — там работает только L3 (pointee-capability из типа). L1 (reassignability имени) и L3 (pointee-mut) — отдельные оси.
Принцип: binding dominates → access-time enforcement
Mutability при доступе к value (поля, индексы, mut-методы) определяется комбинацией двух факторов, в порядке приоритета:
- Binding mutability (call-site decision) — DOMINATES
- Type / field declaration (definition-site intent) — refines what binding permits
Никакой transitive type-modifier propagation в spec НЕТ. Вместо неё —
runtime/check-time enforcement: каждое acc.field/arr[i] access валидируется
по обоим уровням. Это эквивалентно Rust &T vs &mut T philosophy — modifier
живёт на binding/reference, не propagates через type structure.
Полная таблица combination (5 axes × 2 binding modes)
| Field declaration | Access под ro acc | Access под mut acc | Reasoning |
|---|---|---|---|
field T (default) | ❌ read-only, ❌ mutate | ✅ read, ✅ mutate | binding dominates ro; mut binding allows default |
ro field T | ❌ / ❌ | ❌ / ❌ (always frozen) | type-author intent enforced regardless |
mut field T | ❌ / ❌ (dominates!) | ✅ / ✅ | binding ro blocks even «explicit mut» field |
field ro T | ❌ / ❌ | ✅ reassign, ❌ content | content modifier independent of binding |
mut field ro T | ❌ / ❌ | ✅ reassign, ❌ content | same: content ro is invariant |
Ключевая asymmetry: ro acc dominates ВСЕ field declarations
(everything frozen). mut acc respects field declarations (ro field stays
ro, mut/default field becomes mut).
L2 wall-at-* (Plan 147 D246 P4 — freeze STOPS at every pointer)
Транзитивный freeze L2 идёт только по owned-графу значения (.field /
[i] — поля value/heap-record, элементы массива). Он упирается в стену на
КАЖДОМ *: за указателем possibility-to-write определяется исключительно
L3 (pointee-capability из типа *T=ro / *mut T=mut), а НЕ L2-binding.
Причина: owned-vs-aliased heap статически неразличим (нет borrow-checker), а GC
допускает shared-mut под чужим ro — поэтому ro = per-path write-ban, не
object-freeze (D246 P10). Deep-immutable сквозь *mut снаружи не навязывается
(D246 P9, C++ shallow-const trade-off); deep-ro → производитель объявляет
поле *T (как str { ptr *u8 }).
type Cell { mut v *mut int } // поле — mut-pointee (L3)
// 1. owned-граф: L2 freeze работает транзитивно (стоп НЕ на *, а на каждом .field)
type Tags { mut items []str }
type Account { mut tags Tags }
ro acc Account = ...
acc.tags.items.push("x") // ❌ E_READONLY_FIELD — L2 freeze транзитивен (нет *)
// 2. за указателем: L2 freeze ОСТАНАВЛИВАЕТСЯ, действует L3 из типа
ro c Cell = Cell{ v: p } // ro-binding морозит owned-граф c
c.v = q // ❌ — reassign поля .v (owned-граф, до стены) заблокирован L2
unsafe { *c.v = 7 } // ✅ — за `*` L2 не действует; pointee mut (L3 = *mut int)
// запись разрешена ИМЕННО потому, что поле объявлено *mut int
// 3. если производитель хочет deep-ro — объявляет поле *T (ro-pointee)
type RoCell { v *int } // *int = ro-pointee (L3)
ro r RoCell = RoCell{ v: p }
unsafe { *r.v = 7 } // ❌ E_POINTER_RO_ASSIGN — L3 pointee ro (из типа)
Почему НЕТ transitive type-modifier inflation
User’s mental model «fn f(b []str) == fn f(ro b ro [] ro str) (полная
inflation всех ro on each nesting level)» — НЕ работает в Nova spec.
Reasons:
- D33 amend explicit:
ro x ro T→E_REDUNDANT_TYPE_MODIFIER. Singleroon binding is enough — propagation handled access-time. - Implementation simplicity: access-time enforcement requires single binding-modifier check; transitive inflation would require type-level modifier propagation through all nested wrappers.
- Rust precedent:
&Tdoesn’t inflate to&&Tfor nested fields — same logic.
let keyword retracted — нет «neutral binding»
Plan 114 D184 retracted let keyword. No third binding mode — binary
ro / mut choice only. Rationale:
- 2-state model symmetric с Plan 108.x default-ro rule
- 3-state (
letneutral /rofrozen /mutwritable) ambiguous для default-prefix fields — would require defining «default field under let» semantic - Call-site explicit choice (
roormut) dominates type author’s per-field intent — call-site has full safety knowledge
If user wants «trust type author’s per-field declarations»:
- Use
mut accbinding — ro fields stay ro, mut/default fields mut - This is already what type author’s intent maps к under
mutbinding
Hypothetical neutral let acc = X{...} mode adds no expressiveness —
either синоним ro acc (least permissive) or mut acc (respects field
intent). Rejected to keep binding-modifier landscape minimal.
Function return type interaction
Return type modifier (Plan 114 D184 default = mut, explicit ro allowed) participates в same rules:
// 1. Return default mut Acc, ro binding dominates
fn make_acc() -> Acc => Acc{...}
ro b = make_acc()
b.name = "x" // ❌ E_LOCAL_NOT_MUT — binding dominates
// 2. Return default mut Acc, mut binding allows full access
mut c = make_acc()
c.name = "x" // ✅
// 3. Explicit ro return, mut binding without type annotation
fn make_ro_acc() -> ro Acc => Acc{...}
mut c = make_ro_acc() // type inferred as `ro Acc`
// → D33 amend row «mut x ro T»: binding mut,
// content ro — split semantics
c = different_acc // ✅ reassign OK (binding mut)
c.name = "x" // ❌ E_READONLY_CONTENT (content ro)
// 4. Explicit ro return + explicit mut type annotation = coerce error
mut c Acc = make_ro_acc() // ❌ E_READONLY_COERCE
// ro Acc → mut Acc forbidden (D176)
Cross-refs
- D33 amend (binding propagation) — 02-types.md:8927
- D36 / Plan 108.2 enforcement — 02-types.md:2684
- D175 V1 amend (this section, just above)
- D176 (readonly T modifier + Plan 108.1 param default flip) — 02-types.md:2763
- D184 (Plan 114 —
letkeyword retracted) — 03-syntax.md#d184 - D216 V2 (right-binding rule + universal type modifiers) — 02-types.md:7790
Status
✅ ACTIVE since 2026-06-04 — explanatory consolidation, no behavior change. Implementation across D33 / D36 / D175 V1 / D176 / Plan 108.1-108.3 / Plan 114 unchanged. This amend documents existing rules в единую читаемую секцию.
D176 amend §«mut T в binding position» (Plan 124.8 Ф.2)
mut T теперь принимается в binding type annotation после name.
Раньше parser принимал только в return-type и parameter positions.
Pre-amend rendered impossible the legitimate ro view mut []u8 = arr
form documented в D176 V1 §«type-modifier в любой позиции».
D215 amend (Plan 124.8 Ф.1)
Named tuples (type X(name1 T1, name2 T2)) получают:
- Multi-line support — newlines между fields после comma.
- Trailing comma support —
type X(a int, b int,). - Binding-level mutability — Rust-style:
mut p = Vec3(...)позволяет мутировать все поля;ro pблокирует все. Per-fieldmut/romodifiers запрещены (E_TUPLE_NO_PER_FIELD_MOD).
Asymmetry с record {} form (which supports newline-as-separator)
preserved: tuples требуют comma + optional newline после. Это
intentional — tuples = compact pure-data form.
D215 amend — record {} same-line comma enforcement (2026-06-15)
Record {} fields тоже поддерживали только «newline-as-separator» по D49, но при
fields на одной строке без запятой (type P value { x int y int }) парсер
молча принимал оба поля — баг (обе ветки if/else в parse_record_fields_with_default
делали одинаковый skip_newlines()). Уточнение:
- Newline — допустимый разделитель (multi-line record).
- Comma — допустимый разделитель (inline или multi-line).
- Ни того ни другого (next token — не Newline/Semicolon/RBrace) →
E_RECORD_FIELD_MISSING_SEPARATOR.
Иначе говоря: на одной строке запятая обязательна, как и в named-tuple ().
Это унифицирует поведение обеих форм и закрывает парсер-баг.
Применяется к обоим видам record (heap type X {} и value type X value {}).
D215 amend — named tuple field defaults (2026-06-17)
Поля named tuple могут иметь значение по умолчанию (default value):
type Complex(re f64 = 0.0, im f64 = 0.0)
type Rect(x f64, y f64, width f64 = 100.0, height f64 = 50.0)
const DEFAULT_SCALE f64 = 1.0
type Transform(tx f64 = 0.0, ty f64 = 0.0, scale f64 = DEFAULT_SCALE)
Конструктор может опускать любое подмножество полей с дефолтами:
ro z = Complex() // re=0.0, im=0.0
ro z2 = Complex(re: 3.0) // im=0.0
ro r = Rect(x: 1.0, y: 2.0) // width=100.0, height=50.0
ro t = Transform(scale: 2.5) // tx=0.0, ty=0.0
Grammar (extends D215 original)
named_field ::= IDENT type ("=" expr)?
До этого amend named_field ::= IDENT type — без optional default. Совместимость с существующим кодом: дефолты additive.
Семантика
- Required field (без
= expr) — обязателен в каждом вызове конструктора. - Optional field (с
= expr) — можно опустить; absent → default expression инжектируется на call-site в declaration order. - Порядок полей в declaration не ограничен: required и optional могут чередоваться (хотя рекомендуется ставить optional в конце).
- Default expression вычисляется на call-site (не хранится и не кэшируется); может ссылаться на module-level constants, literals, fn-calls.
Arity check (amend E_TUPLE_CONSTRUCT_ARITY_MISMATCH)
Проверка min/max arity вместо точного совпадения:
min_arity= количество required-полей (без дефолта)max_arity= общее количество полей- Provided-field count ∉ [min_arity, max_arity] →
E_TUPLE_CONSTRUCT_ARITY_MISMATCH
AST (compiler-codegen/src/ast/mod.rs)
pub struct NamedTupleField {
pub name: Ident,
pub ty: TypeRef,
pub default: Option<Box<Expr>>, // NEW
}
Checker (types/mod.rs)
named_tuple_field_defaults: HashMap<String, Vec<(String, Expr)>> —
ключ = bare type name, value = список (field_name, default_expr).
Заполняется в step 1 (type-decl scan).
При вызове конструктора — missing optional полей → default expr инжектируется
в reconstructed arg list.
Codegen (emit_c.rs)
Default expressions инжектируются на call-site в emit_tuple_construct.
C-struct initializer включает все поля в declaration order.
Acceptance criteria
| # | Критерий | Status |
|---|---|---|
| AC-1 | type X(f T = expr) принимается парсером | ✅ |
| AC-2 | Mixed required+optional fields в одном типе | ✅ |
| AC-3 | X() when all fields have defaults | ✅ |
| AC-4 | Partial override X(a: v) — remaining defaults injected | ✅ |
| AC-5 | Missing required field → E_TUPLE_CONSTRUCT_ARITY_MISMATCH | ✅ |
| AC-6 | Default references module-level constant | ✅ |
| AC-7 | std/_experimental/math/complex.nv migrated to named tuple | ✅ |
| AC-8 | «без упрощений как для прода» — production-grade, no stubs | ✅ |
| AC-9 | plan120 12/12 PASS (t4_defaults×8, t5_defaults_methods×4, neg_t4×1, neg_t5×1) | ✅ |
Реализация: Plan 120 D215-amend.
D222 amend (Plan 124.8 Ф.1)
«Named tuple priv» portion retract: priv/pub на tuple field —
parser-error E_TUPLE_NO_PRIV. Tuples = pure data carriers (как Rust
tuples, C# ValueTuple), always all-public. Encapsulation на стеке —
через type X value { priv field T } form (D228 NEW).
«Protocol impl boundary» portion preserved для records (heap + value).
D225 retract (Plan 124.8 Ф.1)
«Type-level priv flip для named tuples» — fully retracted. Tuples
всегда all-public; type X priv (...) syntax больше НЕ supported.
Records keep type-level priv flip (D220 §3.3.1 unaffected).
D228 NEW — Value-record allocation contract (Plan 124.8 Ф.2/Ф.4)
Extended by D277 (Plan 153.2, 2026-06-15): by-value стек-codegen распространён с не-generic value-records на generic
type X[T] value {…}— каждый mono-инстанс = inlineNovaValue_<short>, passed/returned/copied by value, 0nova_allocдля wrapper (зеркаля str-путь).Renumbered from D226 (2026-06-03) — D226 in main concurrently assigned to «signed indexing convention» commit
8827f8ec132. D227 taken by «numeric literal inference» commit41d4be096fa. D228 next free.Optimizer consumer (Plan 123 V7.6 V2 refactor, 2026-06-05): field-cache
is_reference_type_refclassifier consultsAllocKindviaTypeKindRegistry.Record(AllocKind::Heap)→ ref-typed slot (pointer);Record(AllocKind::Value)→ inline slot (D228 — mut methods write slot bits черезNovaValue_X*pointer per §«Method receiver» above) → not slot-stable for V7.5/V7.7 own-field cache invalidation refinement. Seedocs/plans/123-followups-2026-06-05.md§2.1 для design + acceptance.
type X value { ... } — stack-allocated value type с copy-on-pass
semantics. Symmetric extension D52 §«record form» через value keyword.
Semantic (V2 production-grade, landed 2026-06-03):
- Allocation: stack (inline C struct
NovaValue_Xв callee frame). V2 codegen landed in Plan 124.8 V2.1-V2.4 — closes [M-124.8-value-codegen-stack]. - Pass: copy on parameter pass (D32 amend) — C handles natively для value types.
- Method receiver
@: pointer на stack-slot (NovaValue_X*) — мутации видны caller’у. См. D215 amend «Method receiver passing» (Plan 128 Ф.2) — NamedTuple uses the same pointer pattern (NovaTuple_X*для mut receiver), wired черезrecv.mutableflag вemit_c.rs. Same lvalue-projection rule applies to NovaValue_X mut-receivers (D228) and NovaTuple_X mut-receivers (D215) — Plan 128.1 Ф.1:b.v.method(),arr[i].method(),@field.method(), multi-levela.b.c.method()emit&(b->v)/&(arr->data[i])/&(nova_self->field)directly без temp hoist (mutation flows к original slot). - Reference fields: handles inline (ptr+len+cap для
[]T, ptr+len дляstr); data on heap (GC-tracked). str— канонический reference-field value-record (Plan 139, 2026-06-11).type str value priv { ptr *u8, len int }— 16-байт stack-значение, inline handle (ptr+len) над иммутабельным heap/rodata UTF-8 буфером. Это flagship-пример паттерна «value-record несёт shared-immutable reference-поле»: copy-семантика значения (16 байт), но буфер разделяется через*u8ro-pointee (*T ≡ *ro T, D246 — нет write-path → sharing безопасен → clone shallow, literal-interning невидим). Все остальные value-record-правила (D228) кstrприменяются единообразно; единственный opt-out — content-eq (ниже), потому что field-by-field над reference-полем сравнил бы pointer-identity. См. D26 MAJOR AMEND.- Equality of reference-field value-records (Plan 139 Ф.3, content-eq
override). Default value-record
==is field-by-field (Plan 141:emit_field_eqrecurses each field). For a reference field whose pointee is shared + immutable (thestr.ptr *u8ro-pointee buffer), naive field-by-field would compare pointer identity — WRONG: two distinct buffers with equal bytes must be equal. Thereforestropts out of field-by-field and uses content-eq:emit_field_eqspecial-casescty == "nova_str"→nova_str_eq(len && memcmp) before any field-by-field path (emit_c.rs:11161), and direct==/</… onstrlower in BinOp codegen tonova_str_eq/nova_str_lt/… (emit_c.rs:16985). Consequence: str-in-tuple, str-in-record, str-in-sum eq, and str-keyedHashMap(hash vianova_str_hashSipHash-over-bytes) are all content-keyed automatically.str.@clone= 16-byte handle copy over the immutable shared buffer (no deep copy;*u8ro-pointee makes sharing safe). General rule: a value-record carrying a shared-immutable pointer field must register content-eq for that field rather than inherit pointer-identity from the field-by-field default. - Fixed array fields: fully inline (
[32]u8= 32 bytes inline). - Forward decls:
typedef struct NovaValue_X NovaValue_X;(no pointer alias unlike heap records). - Constructor:
NovaValue_X tmp; tmp.f1 = v1; tmp.f2 = v2;(stack init, nonova_alloc). - Field access:
.field(struct member), not->field(pointer). - Call-site receiver:
&vfor identifier; hoisted temp +&tempдля rvalue expressions (viaprepare_method_recvhelper).
Codegen V2 helpers (compiler-codegen/src/codegen/emit_c.rs):
emit_value_record_type(name, fields)— emits inline C struct + registers in record_schemas + type_aliases + value_record_names.prepare_method_recv(obj_c, obj_ty)— wraps obj в&for value-record receivers (identifier-fast-path или temp-hoist для expressions).is_value_typerecognizesNovaValue_prefix.struct_name_from_c_typerecognizesNovaValue_Xstrip.
Composability:
value+priv— composable (D220 §3.3.1 для type-level flip также применима).value+consume— composable; value-record содержащий consume field автоматически становится consume (user decision; orthogonal axes).value+mut/roper-field — composable (D175 binding-dominates rule applies).
Composition с эталонами:
- Kotlin
value class(1.5+, single-field) — Nova value-record более powerful (multi-field). - Java Valhalla
value class(incoming) — Nova alignment ahead-of-curve. - Rust
struct(default stack) — Nova explicit allocation marker (vs Rust implicit). - C#
structvsclass— Novavaluemodifier ≈ C#struct; reference record default ≈ C#class. - Industry: Nova становится **первым языком с single declaration syntax
- explicit allocation modifier**.
V2 known limitations (defer-able):
[]NovaValue_Xarray storage — currently boxes elements (V3 followup for inline element storage).Escape analysis для— CLOSED by Plan 127 (V1, 2026-06-05) — см. §«escape & auto-promote» ниже.&valueauto-heap-promote- Auto-derive methods (Equal / Hash / Clone / Compare / Display) — Plan 126 (orthogonal feature).
- Generic value-record cross-module instantiation — works for simple cases; complex multi-T patterns may require V3 review.
#zero_on_moveopt-in — followup attribute для security-critical consume value-records.
D228 amend — §«escape & auto-promote» (Plan 127, 2026-06-05)
Trigger: closes Plan 124.8 V2 followup
[M-124.8-value-heap-promote]. Extends Plan 118 Ф.2 escape walker на value-record locals. Plan 127 phases landed (branchplan-127-value-record-escape): Ф.1 AllocKind tri-state —40815f7d960; Ф.2 escape_analyze walker extension —6ce9d2a4698; Ф.3 codegen heap-allocation path —6948d2ba9dc; Ф.4 diagnostic codes —0a0d7e2cf65; Ф.5 fixtures (18 = 12 POS + 6 NEG) —adb6850e7e0.
&v на value-record local разрешён. Compiler выбирает stack vs heap
allocation для v based on escape analysis result (Go-style — без
Rust lifetimes, без borrow checker).
AllocKind tri-state
AllocKind enum расширен с binary {Heap, Value} до tri-state:
| Variant | C output | Когда |
|---|---|---|
AllocKind::Heap | Nova_X* (heap, nova_alloc) | reference records (type X { ... }) |
AllocKind::Value | NovaValue_X (stack-inline) | value records без escape |
AllocKind::ValueHeapPromoted | Nova_X* (heap, nova_alloc) | value records с detected escape |
User-visible type — Vec3 (или *Vec3 для address-of), та же declaration
type Vec3 value { ... }. Diff виден только в codegen (emit_c.rs).
prepare_method_recv helper расширен: ValueHeapPromoted → obj уже
указатель, не нужен & (vs Value где emit’ится &v).
5 escape trigger conditions (V1 OVER-promote)
Local value-record v promote’ится на heap если любое из:
- Return:
&vвозвращается из функции —fn f() -> *Vec3 => &v. - Heap field store:
&vсохраняется в heap-аллоцированное поле —acc.field = &v(гдеacc: Nova_X*). - Closure capture:
&vзахватывается closure —let cb = || &v. - Global / module binding:
&vсохраняется в module-levellet/const— escape вне fn scope. - Fn arg sink (conservative):
&vпередаётся в fn arg — conservative assume sink escape transit (V1 OVER-promote: callee analysis не делается, любой&v → fntriggerит promote).
Conservative fallback (V1): если chain dataflow analysis cannot
prove «no escape», promote. Matches Plan 118 Ф.2 V1 OVER-promote stance —
любая uncertainty → heap. Precise mode = followup [M-127-precise-escape]
(gated на Plan 118 [M-118-escape-precise]).
Mixed branch (escape в одной ветке, no-escape в другой) — conservative
promote. Path-sensitive analysis = [M-127-path-sensitive-escape].
Cross-ref D228 ↔ D216 §4 (Plan 118 escape machinery reuse)
D228 escape rules используют ту же walker infrastructure, что и
D216 §4 «&value operator + escape analysis с auto-promote» (Plan 118
primitives + named tuples). Контракт reuse:
escape_analyzewalker (compiler-codegen/src/types/mod.rs, Plan 118 Ф.2) — единая dataflow walker, value-record locals добавлены как новая type category поверх существующих primitives/tuples.- Trigger conditions унифицированы — same 5 conditions работают
identically для primitives (
int), tuples ((a, b)), и value-records (Vec3 value). Different type category, same analysis. - AllocKind decision route — для primitives/tuples: stack vs heap-box;
для value-records:
ValuevsValueHeapPromoted. Decision point shared, codegen branches diverge. - V1 OVER-promote stance shared — Plan 118 и Plan 127 promote on ANY uncertainty. Precise mode landings будут coordinated (V2 followups обоих планов могут landed independently).
E_AMP_RECORD_LITERAL(D216 §4) применяется и к value-record literals — anonymous&Vec3 { ... }без named binding forbidden, требует patternro v = Vec3 { ... }; ro p = &v.
См. также D216 §4 для primitive/tuple side контракта.
Diagnostic codes (Plan 127 Ф.4)
| Code | Kind | Trigger |
|---|---|---|
W_VALUE_RECORD_UNNECESSARY_PROMOTE | Lint | Escape detected, но user мог return by-value; suggestion hint emit |
E_VALUE_RECORD_ESCAPE_AFTER_CONSUME | Error | &v после consume v — D162 violation, escape после ownership transfer |
W_VALUE_RECORD_UNNECESSARY_PROMOTE suppressed на synthesized
FnDecl bodies (Plan 126 auto-derive coordination — compiler_generated
flag в FnDecl). Auto-derived Clone::clone/Equal::equal/
Hash::hash bodies могут эмитить &self без необходимости user
attention; lint бы создавал noise. Suppression channel прописан в
compiler-check/src/lints/value_record_promote.rs — skip emit, если
fn.compiler_generated == true.
Composability (D228 amend)
consumevalue-record:&vпослеconsume v→ hard errorE_VALUE_RECORD_ESCAPE_AFTER_CONSUME. До consume — escape analysis как обычно.privfield: на promoted value-record (Nova_X*) field privacy preserved (Nova_X имеет те же priv markers, что и NovaValue_X).readonlybinding:robinding propagates через promote —Nova_X* constв C output для ro path.
Method receiver compatibility
@-методы работают идентично в обоих modes via prepare_method_recv:
fn prepare_method_recv(obj_expr, alloc_kind) -> CExpr {
match alloc_kind {
AllocKind::Value => emit_address_of(obj_expr), // &v
AllocKind::ValueHeapPromoted => obj_expr, // already Nova_X*
AllocKind::Heap => obj_expr, // already Nova_X*
}
}
User-side syntax не меняется — v.method() works в обоих modes.
Field-access codegen для ValueHeapPromoted (Plan 127.1 Ф.1)
Member-access на value-record local выбирает . vs -> operator based
on AllocKind:
| AllocKind | C output | Reason |
|---|---|---|
AllocKind::Value | obj_c.field | stack struct member access |
AllocKind::ValueHeapPromoted | obj_c->field | heap pointer dereference (Nova_X*) |
AllocKind::Heap | obj_c->field | existing reference-record behavior |
До Plan 127.1: Member-access path в codegen всегда emit’ил .field для
value records, что давало invalid C code (Nova_Vec3* obj; obj.x)
после Plan 127 Ф.3 heap-promote — runtime miscompile (3 Plan 127
fixtures broke: t3/t8/t9).
Plan 127.1 Ф.1 (commit a2b6f9c9518) добавил AllocKind::ValueHeapPromoted
branch в Member-access emit path — symmetric с prepare_method_recv
helper из D228 Method receiver compatibility section. Closes
[M-127-codegen-field-access-promoted-ptr] P1 runtime bug.
Plan 127 regression: 12/6 → 15/3 (t3 + t8 + t9 now PASS).
Nested record-literal per-field type resolution (Plan 124.9 Ф.1)
Trigger: discovered Plan 128.2 Ф.2 — nested record literal
A { b: B { c: C { d: D { value: 0 } } } }(4-level) сообщённо аллоцировалNova_Aна каждом уровне вместо declared field typesB/C/D; workaround был explicit.new()constructors. Plan 124.9 closes[M-124.9-nested-record-literal-codegen].
Record-literal field-value codegen (emit_record_lit,
compiler-codegen/src/codegen/emit_c.rs) резолвит тип каждого
field-value по declared field type из record schema
(record_schemas[struct_name][field]), а НЕ по outer/expected record
type родителя. Вложенный RecordLit с собственным type_name
(B { ... }) всегда аллоцирует свой declared field-type, независимо от
контекста окружающего литерала:
/* mut a = A { b: B { c: C { d: D { value: 0 } } } } */
Nova_A* _t1 = nova_alloc(sizeof(Nova_A)); /* outer */
Nova_B* _t2 = nova_alloc(sizeof(Nova_B)); /* field b's declared type B */
Nova_C* _t3 = nova_alloc(sizeof(Nova_C)); /* field c's declared type C */
Nova_D* _t4 = nova_alloc(sizeof(Nova_D)); /* field d's declared type D */
Каждый уровень аллоцирует свой declared field-type — outer type НЕ
leak’ится во вложенные typed литералы. Правило применяется к reference
records, value records (D228), и generic nested literals; nested literal
в fn-arg / return-position резолвится идентично. Empty-context fallback
(top-level binding без expected type) не переопределяет inner
type_name. Lifts Plan 128.2 explicit-.new() workaround.
Plan 124.8 Acceptance (A8.1-A8.20) — ALL ✅
- A8.1 ✅ Multi-line tuple
type X(\n a, \n b\n)parses. - A8.2 ✅ Trailing comma
type X(a, b,)parses. - A8.3 ✅ Multi-line + trailing comma parses.
- A8.4 ✅
type X(priv f int)→ E_TUPLE_NO_PRIV. - A8.5 ✅
type X(pub f int)→ E_TUPLE_NO_PRIV. - A8.6 ✅
type X(mut f int)→ E_TUPLE_NO_PER_FIELD_MOD. - A8.7 ✅
mut p = Vec3(...)+ binding tuple works. - A8.8 ✅
ro pblocking (D175 amend). - A8.9 ✅
type Vec3 value { ... }parses. - A8.10 ✅ V2 LANDED 2026-06-03: value-record real stack codegen через NovaValue_X inline struct (closes [M-124.8-value-codegen-stack]).
- A8.11 ✅ V2 LANDED 2026-06-03: method receiver = NovaValue_X* pointer to stack-slot через prepare_method_recv helper.
- A8.12 ⚠️ V2 partial: scalar/struct fields inline;
[]NovaValue_Xarray elements currently boxed (V3 followup для inline element storage). - A8.13 ✅ V2 LANDED 2026-06-03: param pass = value copy (C-native); return-by-value works через RVO.
- A8.14 ✅
type Token value consume { ... }works (composition). - A8.15 ✅
type X value priv { ... }works (D220 §3.3.1 preserved). - A8.16 ✅
ro x ro T→ E_REDUNDANT_TYPE_MODIFIER. - A8.17 ✅
mut x mut T→ E_REDUNDANT_TYPE_MODIFIER. - A8.18 ✅
ro x mut Tparses + works (D176 gap closed). - A8.19 ✅
ro accblocksacc.mut_field = X(D175 amend). - A8.20 ✅ Regression: plan120 8/8 + plan124_1 9/9 + plan124_2 14/14 + plan124_3 10/10 + plan124_6 7/7 + plan108_3 14/14 unchanged.
Plan 124.8 V2.1 Acceptance — followup markers closed (A8.21-A8.30)
V2.1 closes 3 [M-124.8-*] markers landed 2026-06-03:
- A8.21 ✅ [M-124.8-ro-binding-scope]:
ro_binding_namesblock-scoped.f1_blocksnapshots/restores на entry/exit. Innerro x = ...не leak’ит в outer scope. Cross-module contamination (stdlibro v→ usermut v) fixed. - A8.22 ✅ [M-124.8-ro-binding-scope] shadow:
Stmt::Letshadow-aware.ro x; { mut x; x.field = ... }works через inner mut shadow remove prior ro entry; outer state restored на block exit. - A8.23 ✅ [M-124.8-tuple-mut-field-write-codegen]: end-to-end positive (single field write, multi-field write, sequential overwrite, compound expressions, cross-field arithmetic, write-then-method-read).
- A8.24 ✅ [M-124.8-tuple-mut-field-write-codegen]: negative — D175 binding-dominates на tuple field write через ro binding → E_READONLY_FIELD.
- A8.25 ✅ [M-124.8-zero-on-move] V1 parser:
#zero_on_moveattribute recognized наряду с #from_fields/#from_pairs/#impl. Duplicate detection. Только передtypeдекларацией валидно. - A8.26 ✅ [M-124.8-zero-on-move] V1 AST: TypeDecl.zero_on_move: bool flag (default false; backward-compat preserved).
- A8.27 ✅ [M-124.8-zero-on-move] V1 checker validation: allowed kinds Record (heap + value), NamedTuple, Newtype. Reject Effect/Protocol/ Sum/Alias/Opaque с E_ZERO_ON_MOVE_INVALID_KIND.
- A8.28 ✅ [M-124.8-zero-on-move] V1 codegen: per-type
static inline void Nova_T_zero_storage(<C_type>* p)helper emit. Picks correct C type (Nova_T для heap+newtype, NovaValue_T для value, NovaTuple_T для named tuple). - A8.29 ⚠️ [M-124.8-zero-on-move-auto-inject] V2: auto memset at consume call sites — deferred (requires consume codegen Plan 100.x integration; regression risk для sync primitives).
- A8.30 ✅ Regression V2.1: plan120 8/8 + plan124_1 9/9 + plan124_3 10/10
- plan108_3 14/14 unchanged. plan124_8 27/27 → 40/40 PASS (+13 new fixtures для 3 markers).
Followups
- ✅ [M-124.8-value-codegen-stack] — V2 LANDED 2026-06-03 — proper stack codegen реализован (emit_value_record_type + prepare_method_recv).
- ✅ [M-124.8-tuple-mut-field-write-codegen] — CLOSED 2026-06-03.
4 fixtures cover named-tuple field-write end-to-end через release
nova-cli + clang: single field write, multi-field write, sequential
overwrite, compound expression (
p.value = p.value * 2 + p.step), cross-field arithmetic, write-then-method-call visibility, binding-dominates negative (ro vблокирует write). - ✅ [M-124.8-ro-binding-scope] — CLOSED 2026-06-03. Root cause:
ro_binding_namesбыла monotonic per-ctx (никогда не очищалась). Stdlibro v = ...(sha1.nv / semver.nv) leak’ало в пользовательские fixtures сmut v = .... Fix:f1_blocksnapshots/restoresro_binding_namesна entry/exit;Stmt::Letshadow-aware (всегда removes prior entry, добавляет назад только если ro). 3 fixtures positive + negative. - ✅ [M-124.8-zero-on-move] V1 CLOSED 2026-06-03 — opt-in
#zero_on_moveattribute. V1 parser + AST + checker validation + per-typeNova_T_zero_storagehelper emit. Allowed kinds: Record (heap + value), NamedTuple, Newtype. Reject Effect/Protocol/Sum/ Alias/Opaque с E_ZERO_ON_MOVE_INVALID_KIND. Auto-injection отложено к V2 followup [M-124.8-zero-on-move-auto-inject]. - [M-124.8-zero-on-move-auto-inject] V2 — auto memset of source при consume call. Требует deep integration с consume codegen (Plan 100.x sync primitives) — non-trivial regression risk.
- [M-124.8-value-record-mut-literal-codegen] — pre-existing bug
(exposed at 2026-06-03 testing):
mut t = ValueT { ... }direct literal binding emitsNova_T*вместоNovaValue_T. Workaround:ro+ constructor pattern. Not zero_on_move specific. - [M-124.8-value-record-array-inline] —
[]NovaValue_Xinline element storage (V3, deferred — currently boxed). - [M-124.8-value-heap-promote] —
&valueescape analysis + auto-heap-promote. Scope assigned to Plan 127 (2026-06-03) после consultation с Plan 118 owner: Plan 118 Ф.2 V1 покрывает primitives + named tuples, value-records остаются вне scope. Plan 127 extends Plan 118 Ф.2 walker на value-records; reuseescape_analyze+ extend trigger conditions. - Plan 126 — auto-derive Equal/Hash/Clone/Compare/Display.
- Plan 127 — value-record escape & auto-promote (см. выше).
D229 — Debug protocol + format spec ${expr:?}
Plan 91.14 (2026-06-05). New protocol parallel к D183 Display — debug-specific representation.
Rationale
-
Debug semantics distinct from Display. Plan 91.8a.2 (D183) shipped Display (user-facing display) only. После Plan 118.5 V3 closure, pointer debugging via
(*T).to_debug_str()proved inelegant — leaks unsafe context, no protocol-level extensibility, can’t recursively debug struct holding pointers. Debug closes this gap. -
Diagnostic representation ≠ user-facing display. Default Debug output is memberwise (
TypeName { field1: value, field2: value }) — matches Rust#[derive(Debug)]. Display can be user-friendly (Point(1, 2)или"hello"без escapes). -
Pointer integration.
*Timpls Debug (в unsafe context only) — emits"0x7f8a..->Account". Bare${p}без:?остаётся E_PTR_NO_DISPLAY_USE_DEBUG_STR (per D216 §17). Only explicit${p:?}opt-in unlocks debug formatting.
Protocol declaration
#stable(since = "0.1")
export type Debug protocol {
@debug(mut w Write) -> ()
// D374 AMEND: parameter renamed sb→w, type StringBuilder→Write (decoupled sink).
// NO default body в protocol decl. Compiler synthesizes per-type
// via inject_synthesized_methods (auto_derive.rs) for #impl(Debug) types.
}
Метод name = @debug (НЕ @display) — avoid collision с D183 Display.@display. Distinct method names enable both protocols on same type simultaneously.
Hybrid default body strategy (Plan 91.14 design decision #1, user-confirmed 2026-06-05):
- Protocol decl ships БЕЗ default body — explicit synthesis через
inject_synthesized_methods(auto_derive.rs) для#impl(Debug)типов. - Primitives (int/f64/f32/bool/char/str): explicit
@debugbodies вstd/prelude/protocols.nv; routes через@field.debug(w)в synthesized method bodies. - User types (records):
#impl(Debug)→inject_synthesized_methodsappends synthesizedItem::Fnto AST → codegen sees it as ordinary method. Option[T Debug]иResult[T Debug, E Debug]: explicit Nova-body вstd/prelude/core.nv; codegen dispatches via DeclaredBody в string interpolation path (Plan 91.15 Ф.2).- Known limitation: checker does not validate field Debug bounds at
#impl(Debug)synthesis time — missing bound produces CC-FAIL, not E_BOUND_MISSING.
Format spec syntax
Inside Nova interp-string ${expr:SPEC}:
${expr}— calls Display.@display (D183, unchanged)${expr:?}— calls Debug.@debug (NEW)${expr:foo}— E_FORMAT_SPEC_UNKNOWN (foundation, extensions per [M-91.14-format-dsl-extensions])
Default body synthesis (#impl(Debug))
Когда user type X помечен #impl(Debug):
inject_synthesized_methods(auto_derive.rs) синтезируетfn X @debug(mut w Write) -> ()и append’ит вmodule.itemsперед codegen.- Body:
w.write("X { "); w.write_str("field1: "); @field1.debug(w); w.write(", field2: "); @field2.debug(w); w.write(" }"). - Все поля (primitive и record) — через
@field.debug(w). Primitives имеют @debug вstd/prelude/protocols.nv. - Known limitation: checker не проверяет Debug bounds у полей при синтезе. Отсутствие @debug у поля даёт CC-FAIL, не checker error.
Primitives (int/f64/f32/bool/char/str) — explicit @debug в std/prelude/protocols.nv:
- int → decimal string (
str.from(@)) - f64/f32 → float string
- bool → “true”/“false”
- char →
"${@:?}"(quoted with escapes) - str →
"${@:?}"(quoted with escapes)
Error codes registered
| Code | Description | Trigger |
|---|---|---|
| E_DEBUG_PRINTABLE_NOT_IMPLEMENTED | Type doesn’t impl Debug, no auto-synthesis possible | ${x:?} where typeof(x) lacks impl |
| E_FORMAT_SPEC_UNKNOWN | Unknown format spec | ${x:foo} (only :? valid in V1) |
| E_PTR_NO_DISPLAY_USE_DEBUG_STR (preserved) | Bare ${p} для pointer | unchanged from D216 §17 |
Cross-refs
- D183 — Display protocol (sibling, distinct semantics — Display equivalent)
- D216 §17 — Pointer Debug formatting (
(*T).to_debug_str()method superseded by Debug.@debug +${p:?}syntax; E_PTR_NO_DISPLAY_USE_DEBUG_STR diagnostic preserved для bare${p}) - D73 — From/Into protocol pair (orthogonal — conversion vs formatting)
- Plan 91.14 (this D-block’s home plan)
- Plan 91.13 — JSON conformance (sibling, just landed)
- Plan 91.8a.2 — Display infrastructure (foundation, ~80% mechanism reused)
D230 NEW — Clone protocol (Plan 126 Ф.1)
Plan 126 (2026-06-05). New built-in protocol для deep recursive copy
пользовательских типов. Аналог Rust Clone trait, completing the gap в
D109 family (Equal/Hash/Compare/Display + Clone).
Rationale
-
Value-record + heap-record нуждаются в structural deep-copy. До Plan 126 пользователь писал boilerplate
fn T.copy(other T) -> T => ...per type. После Plan 124.8 (D228 value-record) этот pattern стал универсальным — auto-derive снимает boilerplate burden. -
Memberwise recursive semantics. Compiler synthesize’ит body
Self { f1: @f1.clone(), f2: @f2.clone(), ... }— каждое поле рекурсивно clone’ится. Primitives (int/f64/bool/char/byte/str/u*/i*) копируются по значению (@fieldбез.clone());strclone = новая аллокация с тем же содержимым. -
Distinct от built-in
Hash.@hash/Equal.@equal— clone возвращает Self (not bool / not u64), требует особого synthesizer’аsynthesize_clone(Plan 126 Ф.3).
Protocol declaration
#stable(since = "0.1")
export type Clone protocol {
clone() -> Self
}
Объявлен в std/prelude/protocols.nv (Plan 126 Ф.1, commit c7ff5a319ea).
Auto-derive семантика
Record / NamedTuple (Plan 124.8 D228 / Plan 120 D215):
- Synthesized body:
Self { field1: <clone_expr_1>, field2: <clone_expr_2>, ... }. - Per-field clone:
- Primitive (
int/f64/bool/char/byte/str/u*/i*) → shallow copy@field(compiler routes к built-in copy semantics). - User type (record/tuple) → recursive
.clone()method call. []T→ new array с recursive clone элементов (Plan 90.1 deep-copy infrastructure).
- Primitive (
Sum-type — V1 placeholder: returns @ (self-reference). Rich
match-arms clone (per-variant payload recursion) — followup
[M-126-sum-clone-rich].
Field eligibility check (check_field_eligibility):
- Каждое поле либо primitive (always eligible),
- Либо имеет
#impl(Clone)annotation, - Либо имеет explicit
fn FieldType @clone() -> FieldType. - Иначе →
E_AUTO_DERIVE_FIELD_LACKS_PROTOCOL.
Cycle detection (visited set (type, "Clone") pair):
type A { b B }; type B { a A }+#impl(Clone)на обоих →E_AUTO_DERIVE_CYCLE(unbounded synthesis non-terminating).- User должен переписать как explicit
fn A @clone() -> A => ....
Examples
#impl(Clone)
type Vec3 {
x f64
y f64
z f64
}
ro a = Vec3 { x: 1.0, y: 2.0, z: 3.0 }
ro b = a.clone() // synthesized: Vec3 { x: @x, y: @y, z: @z }
// (primitives → shallow copy, no .clone() recursion).
#impl(Clone)
type Container {
name str // primitive — shallow
items []int // array — recursive deep-copy
inner Vec3 // user type — calls Vec3.@clone()
}
ro c = Container { name: "test", items: [1,2,3], inner: a }
ro d = c.clone() // d.items != c.items (new allocation), но d.items[0] == 1
Manual impl (override default):
fn Vec3 @clone() -> Vec3 =>
Vec3 { x: @x, y: @y, z: @z } // explicit, same result
Explicit > auto-derive — user wins resolution в verify_impl_protocols.
Error codes registered
| Code | Description | Trigger |
|---|---|---|
| E_AUTO_DERIVE_CYCLE | Cyclic recursion non-terminating | type A↔B + #impl(Clone) |
| E_AUTO_DERIVE_FIELD_LACKS_PROTOCOL | Field type не impl Clone | #impl(Clone) type X { y Y } где Y не Clone |
| E_AUTO_DERIVE_UNSUPPORTED_KIND | Newtype/Alias/Effect/Protocol/Opaque | #impl(Clone) type Foo = newtype Bar |
Runtime codegen (Plan 126.2 Ф.1/Ф.2)
Plan 126 V1 (2026-06-05) синтезировал @clone body и проверял field
eligibility на type-check уровне, но synthesized FnDecl не достигал
runtime dispatch — method call a.clone() не находил эмитнутую функцию.
Plan 126.2 (2026-06-06) закрывает этот gap:
- Ф.1 — method_table registration. Synthesized
@cloneFnDecl регистрируется вmethod_tableнаравне с user-declared методами, так что resolver видит его при разрешенииa.clone()/ operator dispatch. - Ф.2 — C codegen emit. Synthesized body эмитится как C-функция
Nova_<T>_method_clone(<T> self) -> <T>с deep-copy семантикой: primitive поля копируются по значению, user-type / array поля рекурсивно черезNova_<FieldType>_method_clone. Recursive.clone()вызовы резолвятся через ту же method_table запись.
Result: a.clone() теперь runtime-dispatched, completing Plan 126 V1
promise. То же codegen применяется к остальным auto-derived методам
(@equal/@hash/@compare/@display).
Cross-refs
- D109 amend — auto-derive rules для всех 5 built-in protocols.
- D109 method_table dispatch note — Plan 126.2 runtime dispatch via method_table.
- D186 —
#impl(P)annotation foundation. - D183 — Display protocol (sibling, Display semantics).
- D229 — Debug (sibling, Debug semantics).
- Plan 126 (this D-block’s home plan).
- Plan 124.8 — value-record D228 (primary use-case).
- Plan 120 — NamedTuple D215 (secondary use-case).
- Plan 90.1 —
[]Textend/copy_from family (used для array field clone).
D237. Protocol naming convention: method-name capitalized (Plan 137, 2026-06-09)
Status: ACTIVE
Prelude-протоколы именуются по принципу «имя протокола = заглавная форма имени метода».
Принцип: [T Hash] означает ровно один метод @hash(); [T Display] — @display().
Conversion protocols (From/Into/TryFrom/TryInto) уже следовали принципу.
Domain-specific protocols (Consumable, WithExitTimeout) — исключения.
Таблица переименований
| Старый протокол | Новый протокол | Старый метод | Новый метод |
|---|---|---|---|
Hash | Hash | @hash() | @hash() (неизменён) |
Compare | Compare | @compare() | @compare() (неизменён) |
Clone | Clone | @clone() | @clone() (неизменён) |
Equal | Equal | @equal() | @equal() |
Display | Display | @display() | @display() |
Debug | Debug | @debug() | @debug() |
Диагностика (E_PROTOCOL_RENAMED)
Использование старого имени протокола в #impl(...) или bound [T OldName]
производит ошибку E_PROTOCOL_RENAMED с подсказкой использовать новое имя:
error[E_PROTOCOL_RENAMED]: protocol `Hash` was renamed to `Hash`
--> file.nv:3:12
|
3 | #impl(Hash)
| ^^^^^^^^ use `Hash` instead
D109 amend (Plan 137)
Протоколы Hash, Equal, Compare, Display, Debug, Clone
переименованы в Hash, Equal, Compare, Display, Debug, Clone соответственно.
Методы @equal → @equal, @display → @display, @debug → @debug.
D183 amend (Plan 137)
Equal.@equal → Equal.@equal; Compare.@compare unchanged.
Display.@display → Display.@display; default-body synthesis обновлена.
D229 amend (Plan 137)
Debug.@debug → Debug.@debug.
${expr:?} по-прежнему маршрутизируется к Debug.@debug.
D230 amend (Plan 137)
Clone → Clone; метод @clone неизменён.
Все auto-derive references обновлены в auto_derive.rs и stdlib.
Cross-refs
- D109 — built-in protocol family
- D183 — Display protocol (was Display)
- D229 — Debug protocol (was Debug)
- D230 — Clone protocol (was Clone)
- Plan 137 — home plan
D230 amend (Plan 138.3, 2026-06-10) — Clone = deep/recursive; collections element-wise
Status: ACTIVE
Фиксирует семантический контракт Clone и отделяет его от shallow value-copy.
Триггер: ревью Vec.clone (Plan 138.2 design-cleanup) — текущая реализация была
shallow (bitwise RawMem.copy), что нарушает протокол-контракт Clone.
Контракт: Clone — DEEP / рекурсивный
@clone() возвращает полностью независимую копию. Мутация клона никогда
не должна затрагивать оригинал (и наоборот). Для композитных типов @clone
рекурсивно клонирует каждый член через его собственный @clone():
// memberwise рекурсия (auto-derive, см. D230 §Auto-derive выше):
Self { f1: @f1.clone(), f2: @f2.clone(), ... }
Primitive-поля (int/f64/bool/char/byte/u*/i*) копируются по значению
(@field без .clone() — для них deep == shallow). str.clone = новая аллокация
с тем же содержимым. User-type / []T поля — рекурсивный deep-clone.
Collections (Vec / HashMap / Set) — element-wise deep + conditional bound
Коллекции клонируются поэлементно через @clone() каждого элемента, с
conditional bound на параметре типа (зеркало Rust impl<T: Clone> Clone for Vec<T>):
| Тип | Сигнатура @clone | Семантика |
|---|---|---|
Vec[T] | Vec[T Clone] @clone() -> Self | per-element out.push(@data[i].clone()) |
HashMap[K,V] | HashMap[K Clone, V Clone] @clone() -> Self | per-entry copy.insert_new(k.clone(), v.clone()) |
Set[T] | Set[T Clone] @clone() -> Self | делегирует @map.clone() (deep HashMap[T, ()]) |
Bound [T Clone] означает: Vec[T].clone() компилируется только если T
сам реализует Clone (примитивы — всегда; records — через #impl(Clone) или
manual fn T @clone; см. D230 §Field-eligibility). Это намеренно: коллекция
non-Clone-типа не может дать deep-копию.
SHALLOW value-copy — ОТДЕЛЬНАЯ операция (не @clone)
Следующие операции — shallow value-copy: bit-copy значений элементов, любой
T (без Clone-bound). Для ref-типовых элементов копия разделяет pointee с
источником (это корректно для move/переноса/построения буфера, не для clone):
| Операция | Bound | Назначение |
|---|---|---|
Vec.from(items) / @extend / @push | любой T | построение буфера, move-перенос |
@copy_from / @copy_within / @insert / @remove / @append | любой T | сдвиг/перенос значений в буфере |
@realloc_to | любой T | рост capacity (перенос байт) |
HashMap.from_iter / Set.from_iter | любой T | построение из итератора (per-element insert) |
RawMem.copy / RawMem.copy_nonoverlapping (bitwise) — только для этих
shallow-контекстов. Никогда не для @clone (bitwise-копия pointer’ов = aliasing-баг:
shallow-clone коллекции ref-типов тихо разделяет вложенные объекты, мутация «копии»
бьёт оригинал — ровно класс багов, который закрывает этот amend).
Различие deep vs shallow (резюме)
@clone() | from/extend/push/copy_from/realloc | |
|---|---|---|
| Семантика | deep — рекурсивный клон via element @clone() | shallow value-copy (bit-copy) |
| Bound | [T Clone] (conditional) | любой T |
| ref-T элементы | независимый pointee (рекурсия) | разделяет pointee |
| Реализация | per-element .clone() loop | RawMem.copy / per-element push |
| Назначение | независимая копия | move / перенос / построение |
KNOWN GAP — ✅ ЗАКРЫТ (Plan 138.4 Ф.1, 2026-06-11)
UPDATE (2026-06-11): этот gap ЗАКРЫТ — impl догнал prose. Deep element-wise clone для
Vec/HashMap/Setтеперь РЕАЛИЗОВАН и GREEN (Plan 138.4 Ф.1 G-C, commits88432dd6f02+363f4b53788). Блокер[M-138.3-clone-bound-unsupported]CLOSED. ROOT CAUSE отличался от гипотезы ниже: НЕ «монформизатор мис-диспатчит», а single-key last-winsmethod_receivers["clone"]instance-fallback (lookup ТОЛЬКО по имени метода, игнорируя receiver-тип) роутил unbound primitive-T.clone()в произвольный неродственный@clone. FIX:PrimBuiltin::Identityvariant —.clone()на любом primitive-C-типе = bitwise self (вариант (a)/(c) рекомендации ниже) + record/heap identity-clone arm + зеркало вinfer_expr_c_type. Реальный user/synthesized@cloneсохраняет precedence. Текст ниже — историческое описание gap’а до фикса.
Контракт выше — целевой. Деталь auto-derive для records (memberwise
field.clone() recursion) РЕАЛИЗОВАНА и работает (#impl(Clone) —
plan126/plan126_2 p3_cloneable_runtime_ok + p7_nested_record_clone_deep_ok
PASS; record @clone() корректно эмитит рекурсию по полям).
(ИСТОРИЧЕСКОЕ, до 138.4) deep element-wise clone для коллекций (Vec/HashMap/Set)
НЕ был реализован — все три были shallow (@clone() с bound «любой T»,
без [T Clone]): bit-copy / per-(k,v) value-copy. Блокер —
[M-138.3-clone-bound-unsupported]: bootstrap-монформизатор мис-диспатчил
per-element generic T.@clone() / K.@clone() / V.@clone() для примитивного
T/K/V (нет int.@clone() / str.@clone() — примитивы copy-built-in per
этому D), резолвя unbound generic .clone() в произвольный неродственный
@clone. Эмпирика: deep Vec[int].clone() → runtime crash + регрессия
plan131/vec_clone_pos; deep HashMap[str,int].clone() → CC-FAIL
passing 'nova_str' to parameter of incompatible type. Bound [T Clone] сам по
себе парсится и type-check’ается (R1 Plan 138.3 — подтверждено), и для
record element-типов emit корректен (Vec[Point].clone() даёт верную
Point.@clone() рекурсию); сломан был только primitive-T dispatch.
Следствие для bound-audit (G5): так как collection-clone оставались shallow
(@clone() любой T, без T: Clone-требования), нового нарушения bound на
call-site’ах не было — deep-направление, требующее T: Clone, удерживалось. Для
примитивных элементов shallow == deep (нет разделяемого pointee), для record
элементов расхождение было gap’ом. После 138.4 deep-форма с [T Clone] активна —
collection-clone теперь требует T: Clone, recursion корректна для record-элементов.
Cross-refs
- Q31 — conditional
[T Clone]bound на generic instance-методе (design clarification) - Plan 138.3 — home plan (deep-clone collections)
[M-138.3-clone-bound-unsupported]— primitive-Tgeneric.clone()мис-диспатч; блокирует deep collection-clone[M-138-rawmem-bulk-ops]— clone→RawMem.copyбыл shallow-баг; этот amend фиксирует контракт- Plan 90.1 —
[]Tdeep-copy infrastructure (used для array-field clone)
D231. RawMem allocator API — nova_alloc / nova_alloc_uncollectable / nova_free_uncollectable
Status: ACTIVE (Plan 131, 2026-06-08)
Low-level GC-tracked allocation exposed to Nova for implementing Nova-native data structures (Vec[T], custom allocators, FFI).
Что
Three extern C functions wrapped as RawMem static methods in
std/runtime/raw_mem.nv, all gated behind unsafe fn (E_UNSAFE_CALL_REQUIRES_WRAP
per D216 §9).
API
// GC-tracked allocation. Memory zeroed. 8-byte aligned.
// Must be called inside unsafe {} block.
export external unsafe fn RawMem.alloc(n usize) -> *mut u8
// Not GC-tracked. Caller must call RawMem.free_uncollectable.
export external unsafe fn RawMem.alloc_uncollectable(n usize) -> *mut u8
// Free pointer from alloc_uncollectable. UB on GC-tracked pointer.
export external unsafe fn RawMem.free_uncollectable(ptr *mut u8) -> ()
C mapping
| Nova | C (nova_rt/alloc.h) |
|---|---|
RawMem.alloc(n) | nova_alloc(n) |
RawMem.alloc_uncollectable(n) | nova_alloc_uncollectable(n) |
RawMem.free_uncollectable(ptr) | nova_free_uncollectable(ptr) |
Safety rules
nova_allocreturns zeroed, GC-collectable, 8-byte aligned memory.- Do NOT call
nova_freeon GC-tracked pointers — Boehm GC handles collection. alloc_uncollectablefor long-lived buffers not visible to the conservative GC scanner (e.g. Windows fiber arena buffers where fiber stacks shadow heap).- Every call must be inside
unsafe {}—unsafe fnkeyword enforced (E_UNSAFE_CALL_REQUIRES_WRAP from D216 §9, ACTIVE 2026-06-02). n = 0is implementation-defined; usen > 0in practice.
Типичный use case
// Allocate a typed buffer of n elements of T:
fn alloc_buf[T](n int) -> *mut T {
unsafe {
RawMem.alloc((n as usize) * (size_of[T]() as usize)) as *mut T
}
}
Cross-refs
- D216 §8 — unsafe gating model.
- D216 §6 — pointer arithmetic (ptr + N scaled by sizeof(T)).
- D232 — Vec[T] built on D231.
std/runtime/raw_mem.nv— implementation.
D232. Vec[T] — Nova-native generic growable array
Status: ACTIVE (Plan 131, 2026-06-08)
A production-grade growable array implemented entirely in Nova on top of D231 (RawMem.alloc), D199 (size_ofT), and D216 pointer arithmetic. Demonstrates that a collection with correct typed storage needs no compiler magic beyond what Plans 118/114.4 provide.
Layout
export type Vec[T] {
priv mut data *mut T // raw element buffer, cap slots wide (writable pointee — explicit *mut T, D246)
priv mut len int // number of live (initialised) elements
priv mut cap int // number of allocated element slots
}
Pointer model note (Plan 147 D246, 2026-06-12; supersedes flip-scan-draft):
priv mut data *mut T— writable buffer требует явного*mut T(L3 pointee-mut из типа).mut data(L1) даёт лишь reassignability поля; pointee-mut НЕ наследуется от mut-binding. Прежний flip-scan-вариантmut data *Tдавал бы ro-pointee (*T ≡ *ro T) → запись@data[i] = …была бы запрещена. См. D246 P5.
Construction
| Call | Effect |
|---|---|
Vec[T].new() | empty, no allocation (cap = 0) |
Vec[T].with_capacity(n) | empty, pre-allocated n slots |
Vec[T].from(items []T) | copy from built-in slice |
Vec[T].from_raw_parts(ptr *T, len, cap) | build from a raw (ptr,len,cap) triple (cross-type bridge, unsafe-obligated; D247) |
Key methods
| Method | Signature | Description |
|---|---|---|
push | mut @push(v T) -> () | Append element, grow if needed |
pop | mut @pop() -> Option[T] | Remove and return last |
get | @get(i int) -> Option[T] | Element by index, bounds-checked |
get_mut | mut @get_mut(i int) -> Option[*mut T] | Raw mutable pointer (unsafe) |
insert | mut @insert(i int, v T) -> () | Shift-insert at index |
remove | mut @remove(i int) -> T | Shift-remove at index |
swap_remove | mut @swap_remove(i int) -> T | O(1) order-disrupting remove |
len | @len() -> int | Live element count |
cap | @cap() -> int | Allocated slot count |
is_empty | @is_empty() -> bool | True if len = 0 |
clear | mut @clear() -> () | Set len = 0 (retains buffer) |
truncate | mut @truncate(n int) -> () | Shorten to n elements |
reserve | mut @reserve(additional int) -> () | Ensure room for more elements |
shrink_to_fit | mut @shrink_to_fit() -> () | Cap → len |
shrink_to | mut @shrink_to(min_cap int) -> () | Cap → max(len, min_cap) |
reverse | mut @reverse() -> () | Reverse in place |
extend | mut @extend(items []T) -> () | Append all from slice |
append | mut @append(mut other Vec[T]) -> () | Move all from other |
retain | mut @retain(pred fn(T) -> bool) -> () | Keep matching elements |
first | @first() -> Option[T] | First element |
last | @last() -> Option[T] | Last element |
as_slice | @as_slice() -> []T | Copy into built-in slice |
as_ptr | @as_ptr() -> *T / mut @as_ptr() -> *mut T | Raw data-buffer pointer; recv-mut overload yields writable *mut T (cross-type bridge getter, D247) |
into_raw | consume @into_raw() -> *mut T | Consume the Vec, surrender its buffer pointer (inverse of from_raw_parts; powers zero-copy str.from_bytes_unchecked_steal, D247) |
iter | @iter() -> VecIter[T] | Index-cursor iterator |
clone | @clone() -> Vec[T] | Deep copy (Clone) |
equals | @equal(other Vec[T]) -> bool | Element-wise equality |
fmt | @display(mut sb StringBuilder) -> () | Display (format: Vec[e0, e1, ...]) |
debug_fmt | @debug(mut sb StringBuilder) -> () | Debug |
Growth strategy
- Initial capacity: 8 slots on first push into an empty Vec.
- Doubling:
new_cap = current_cap * 2untilnew_cap >= needed. - Amortised O(1) push. Realloc copies live prefix via typed pointer loop.
Typed storage (key property)
Elements are stored at the real C type of T in a T* buffer.
Pointer arithmetic data + i is C-scaled by sizeof(T) automatically by
the C backend. This means:
Vec[Option[int]]stores eachNovaOpt_nova_intstruct inline (16 bytes/element).Vec[MyRecord]stores record pointers (Nova_MyRecord*8 bytes/element).- No int64-slot erasure — pre-D239
[]Tbuilt-in usedNOVA_ARRAY_DECL(T)macro with int64 element slots; after D239[]T ≡ Vec[T]so this gap is closed.
[]T = Vec[T] — D239
[]T — синтаксический псевдоним Vec[T] (D239). Typed-storage gap
закрыт: элементы хранятся по реальному C-типу T в буфере T*. Никакого
int64-erasure.
| Criterion | []T / Vec[T] (unified, D239) |
|---|---|
| Default choice | ✅ yes |
| Primitives (int, f64, str, …) | ✅ typed |
| Records (heap pointer) | ✅ pointer-in-slot |
| Value-struct T (Option[U], tuple, >8-byte value-record) | ✅ typed (gap closed) |
for x in loop | ✅ via VecIter |
| Compiler magic needed | no (pure Nova) |
[]T и Vec[T] взаимозаменяемы: fn f(a []int) принимает Vec[int]
и наоборот. Лексически предпочтительна краткая форма []T; Vec[T]
используется там где явность важна (generic bounds, type params).
Note (Plan 138.2): в единицах компиляции без явного
Vec-импорта компилятор временно используетNovaArray_TC-backing для примитивных типов ([]int,[]str,[]u8). После того какVecвойдёт в prelude (Plan 138.2 [M-138.1-vec-in-prelude]),NovaArrayбудет удалён полностью. Поведение идентично — layout{ data*, len, cap }совпадает.
Protocols implemented
Index[int, T](via@index(i int) -> T, panic OOB) — powersv[i](D238)Index[Range, Vec[T]](via@index(r Range) -> Vec[T]) — powersv[a..b]zero-copy (D238)MutIndex[int, T](viamut @index(i int, val T)) — powersv[i] = val(D240)Iter[VecIter[T]](via@iter()) +Next[T]наVecIter[T](via@next())Equal(element-wise via@equal)Clone(deep copy via@clone)Display(via@display)Debug(via@debug)
Cross-refs
- D239 —
[]T=Vec[T]; этот D232-тип является backing-типом[]T. - D238 —
Index[K,V]protocol;v[i]иv[a..b]через@index. - D240 —
MutIndex[K,V];v[i] = valчерезmut @index. - D231 — allocator used by Vec[T].
- D199 —
size_of[T]()const fn used in buffer size calc. - D216 §6 — ptr arithmetic (
data + iC-scaled). - D226 —
intfor len/cap/index. - D228 — value-records are valid T (stored by value in buffer).
- D247 —
from_raw_parts/as_ptr/into_rawcross-type bridge (Plan 139.2 str-method migration). std/collections/vec_owned.nv— implementation.- Plan 131 — home plan.
D239. []T — синтаксический псевдоним Vec[T]
Status: ACTIVE (Plan 138, 2026-06-10)
Added: Plan 138 Ф.1–Ф.4 (D239 NEW, 2026-06-10). Closes: D144 «future
language version» wording; amends D27 (arr[i] now through @index). Depends
on: D232 (Vec[T]
implementation); D238
(Index protocol).
Что
[]T — синтаксический псевдоним Vec[T]. Компилятор разворачивает
любое использование []T в Vec[T] на уровне type-resolution.
// Эти объявления полностью эквивалентны:
mut a []int = [1, 2, 3]
mut b Vec[int] = [1, 2, 3]
// Передача по типу:
fn process(xs []int) { ... }
mut v Vec[int] = [4, 5, 6]
process(v) // OK — []int = Vec[int]
// Литерал строит Vec[T]:
[1, 2, 3] → Vec[int] { push 1, push 2, push 3 }
Typed storage — gap закрыт
До D239 []T использовал NOVA_ARRAY_DECL(T) C-макрос с int64-slot erasure.
Vec[T] хранит элементы по реальному C-типу T в буфере *mut T:
| Элемент T | []T до D239 | Vec[T] / []T после D239 |
|---|---|---|
int, f64, bool | ✅ типизировано | ✅ типизировано |
str, указатель на record | ✅ pointer-in-slot | ✅ pointer-in-slot |
Option[int] (16 байт) | ❌ int64-erasure → UB | ✅ NovaOpt_nova_int inline |
(int, str) tuple | ❌ int64-erasure | ✅ typed tuple inline |
| value-record > 8 байт | ❌ int64-erasure | ✅ typed by-pointer |
Индексация через @index (D238)
После D239 v[i] и v[a..b] работают через Index[K, V] protocol (D238),
а не через compiler built-in magic:
mut v []int = [10, 20, 30]
v[1] // → v.index(1) → 20 (panic OOB)
v.get(1) // → Some(20) (safe)
v[0..2] // → v.index(Range { start: 0, end: 2 }) // [10, 20] zero-copy view
v[1] = 99 // → v.@index(1, 99) write-overload через MutIndex (D240)
Статус реализации
Plan 138 Ф.1–Ф.4 (2026-06-10):
[]T → Vec[T]flip активен в единицах компиляции, которые импортируютVec(явно или транзитивно). Примитивные единицы безVec-импорта продолжают временно использоватьNovaArray_TC-backing с идентичным layout.Plan 138.2 Ф.0-final (2026-06-11):
Vec/VecIterвошли в prelude (PRELUDE_VERSION13→14) → flip УНИВЕРСАЛЕН:[]Tлоуэрится вNova_Vec____<T>*в КАЖДОМ prelude-юните (Vec-free юнит[]int→Nova_Vec____nova_int*, typed storage). Юниты с#no_preludegraceful-degrade на legacyNovaArray_T-путь (4 gate-сайтаgeneric_type_templates.contains_key("Vec")вemit_c.rsостаются — проходят для prelude-юнитов, fallback для#no_prelude).Plan 153.0 CONFIRM (2026-06-13):
Vec[T]переехал в folder-modulestd/collections/vec/(co-equalcore/access/mutate/slice/iter/protocols+_module.nv), модульcollections.vec_ownedретайрнут (имя исчезло; ~55 import-сайтов мигрированыvec_owned→vec; prelude re-export’итVec/VecIterиз folder). Eager-комбинаторы (map/filter/fold/any/all) вынесены в отдельный explicit-importcollections.vec_seq(НЕ prelude-global — иначе их generic/param-идентификаторы засоряют каждый юнит, [M-153-vec-combinators-prelude-global]).[]T ≡ Vec[T]подтверждён для инферированных значений; ОСТАЁТСЯ residual: ЯВНАЯ аннотацияv Vec[int]не коэрсится в[]int-параметр (E7301, [M-153-d239-explicit-vec-to-slice-param]). См. docs/vec-internals.md.
NovaArray retirement — частичный (BLOCKED на Plan 139 Ф.2)
Status (Plan 138.2 Ф.2-Ф.5, 2026-06-11): ЧАСТИЧНО — полный retire BLOCKED. Универсальный flip (Ф.0-final) приземлён, но физическое удаление
NOVA_ARRAY_DECL/IMPLизarray.hзаблокировано пятью load-bearing NovaArray-потребителями, которые переживают flip:
- Строковый/byte слой (главный блокер — Plan 139 Ф.2 scope-out).
nova_str_as_bytes→NovaArray_nova_byte*,nova_str_split→NovaArray_nova_str*,from_bytes_lossy/from_bytes_unchecked/stealостаются C-примитивами (требуют str@ptrfield-access value-record’а — gated на[M-139-f0-lang-item-decl]). Эмпирически:nova_byte-NovaArray = ~35 700 вхождений по корпусу,nova_str= ~2 100,nova_char= ~1 200 (to_chars). WriteBuffer/StringBuilder bulk-ops (nova_array_append_nova_byte~3 300,compare/append_zero/truncate/reserveнаnova_byte) маршрутят через C-builtin bridge. УдалитьNOVA_ARRAY_DECL(nova_byte/nova_str)= сломать base64/json/encoding/text. Risk RG. →[M-139-f2-ptr-field-producers]. #no_preludegraceful-degrade.#no_prelude-юниты (tested feature: plan107/plan62/plan110_9_np) лоуэрят[]TвNovaArray_T(Vec-template отсутствует). 4 gate-сайтаcontains_key("Vec")это и обеспечивают. Risk RE.- Closure-array
[]fn→NovaArray_void_p*(намеренное исключение,[M-138.1-closure-array]/[M-138.2-closure-array-vec]). - parfor (D71) — internal result-collection буферы (
NovaArray_{nova_int, nova_bool,nova_f64,nova_str}), layout-identical с Vec, никогда не escape’ят как user[]T; миграция = риск без семантического выигрыша →[M-138.2-parfor-vec]. - Literal-bridge
Vec[T].from(items []T)— static-method param[]Tвсё ещё лоуэрится вNovaArray_nova_int* items(dead stub в каждом flipped-юните, класс[M-138.2-self-in-param]— generic-static-method param-type substitution).
После того как Plan 139 Ф.2 мигрирует string/byte слой на Nova_Vec____nova_byte*
(закрытие [M-139-f2-ptr-field-producers]), retirement можно завершить:
NovaArray_TC-макрос иarray.hNOVA_ARRAY_DECL/IMPL — удаляютсяnova_array_*runtime helpers для примитивов — заменяютсяVec-методами (уже сделано для generic-Vec пути; остаётсяnova_bytestring-layer bridge)- Строковый слой (
nova_str_to_bytes,nova_str_to_chars,split,string_builder.h) — мигрирует наNova_Vec____nova_byte*
§2. Scalar min/max методы на числовых типах (D239 amend, 2026-06-16)
Added: [M-153-scalar-min-max] CLOSED (2026-06-16). Скалярные методы сравнения
нужны для shrink-to-min идиомы (Vec capacity management) и @min/@max-терминаторов
lazy-итератора (D260). Реализованы в std/runtime/defaults.nv.
// Все числовые типы: int / u8 u16 u32 u64 uint / i8 i16 i32 i64 / f32 f64
(5).max(3) // → 5
(5).min(3) // → 3
(10).min(20).min(5) // → 5 (chaining)
// Сигнатура (int; аналогично для u8…f64 с Self):
fn int @min(other int) -> int
fn int @max(other int) -> int
Семантика: равенство → возвращается @ (левый операнд). Для f32/f64 не используются
C-макросы fmin/fmax (нет специальной NaN-семантики — NaN не меньше и не больше ничего,
результат детерминирован через </>). Не протокол — встроенные методы-примитивы, как @compare.
Связь
- D232 —
Vec[T]— backing тип для[]T - D238 —
Index[K,V];v[i]иv[a..b]через@index - D240 —
MutIndex[K,V];v[i] = valчерезmut @index - D144 — sub-slice views; D144 «future language version» снято D239
- D27 —
[]Tсинтаксис сохраняется; семантика меняется:[]T ≡ Vec[T] - Plan 138 — полный план миграции; Plan 138.2 — NovaArray retirement
Amended (vec-sweep, 2026-07-06):
[]T— КАНОНИЧЕСКАЯ запись во всех контекстах: аннотации типов, возвраты, вложенные формы ([][]u8), кортежи ([](str, str)), статические вызовы-конструкторы ([]u8.new(),[]int.of(1,2,3),[]int.from(...)).Vec[...]остаётся ТОЛЬКО definition-site — внутри самого модуляstd/collections/vec/(реализацияVec[T]). Везде за пределами этого модуля предпочтителен[]T.Исключение — известный compiler-gap [M-153.x-array-new-not-vec]: bracket- spelling
[]Tв VALUE-позиции (конструктор-вызов, напр.[]T.new()) иногда лоуэрится через legacy int64-erased NovaArray-путь, а НЕ через типизированный Vec-путь, что при определённых сочетаниях (см.std/collections/hashmap.nv::new_buckets) даёт RUN-FAIL. В таких точечных местахVec[...]остаётся исключением из канона с явным маркером в коде.
D248. Запрет chained comparison + relational-операнды требуют ordered-категорию (Plan 150)
Status: 🆕 SPEC LANDED 2026-06-13 (Plan 150 Ф.0). Реализация — Plan 150 Ф.1 (parser + checker). Резолвит Q35. Решение автора: hard-error (как Rust); chained comparison НЕ добавляем (только Python чейнит). Разблокирует
[M-140-bounds-as-contract].
Проблема (security-дефект)
0 <= i < n парсится как (0 <= i) < n = bool < n. Так как bool ∈ {0,1}, выражение вакуумно-истинно
для любого n > 1 — предикат нейтрализуется молча. Следствие: канонический bounds-контракт
requires 0 <= i < @len молча вакуумен → проверка границ тихо обходится. Nova была хуже всех peers
(даже untyped JS коэрсит в детерминированно-неверный результат; Nova аннулирует предикат).
Решение
-
Chained comparison ОТКЛОНЁН (hard error). Цепочка из ≥2 relational/equality операторов одного precedence-уровня (
a OP1 b OP2 c, гдеOP ∈ {< <= > >= == !=}) →E_CMP_CHAIN_UNSUPPORTEDс machine-applicable fix-it «a OP1 b && b OP2 c». Применяется даже к ordered-типам (1 < 2 < 3— тоже ошибка) и к equality-цепочкам (a == b == c). Rationale: только Python чейнит; Go/Rust/Kotlin/Java/Swift — hard-error.&&явно, без нового синтаксиса/парсер-сложности. -
Relational-операнды требуют ordered-категорию.
</<=/>/>=требуют mutually-ordered тип (int/float/str/char или@compare-несущий тип).bool/unitкак операнд →E_RELATIONAL_OPERAND_NOT_ORDERED.ptr-relational →E_PTR_ARITHMETIC_BANNED(existing, Plan 115 — см. ptr type-checker rules).==/!=наbool/unit— остаются легальны (под баном только relational). Консистентно с дизайном: bool ordering уже method-only через@compare(D183,std/runtime/defaults.nv), а не через оператор<.
Канон
- Диапазон:
a <= b && b < c.requires 0 <= i && i < @len— реальный (не вакуумный) bounds-контракт. - Permissive-на-Unknown/generic-категориях сохраняется (чекер фейлит только на definitively-known bool/unit
- concrete cross-category mismatch — не ломать generics).
Диагностические коды
E_CMP_CHAIN_UNSUPPORTED— ≥2 сравнения в цепочке (a < b < c,0 <= i < n,a == b == c); сообщение «comparison operators cannot be chained» + fix-it «split intoa OP1 b && b OP2 c». Эмитится в parser.E_RELATIONAL_OPERAND_NOT_ORDERED—bool/unitкак операнд</<=/>/>=. Эмитится в checker.
Связь
- D183 — comparison-протоколы
(
@compare/@equal); ordered-категория =@compare-несущие типы. - Plan 115 /
E_PTR_ARITHMETIC_BANNED— ptr relational ban (reuse, без дублирования). - Q35 — резолвит.
[M-140-bounds-as-contract]— разблокирует (requires 0 <= i && i < @len). - Отложено (сознательно): Python-style chaining (
a<b<c≡a<b && b<c) — НЕ добавляем; если будет спрос — отдельное предложение в будущем.
D374. Write-sink протокол — декаплинг Display/Debug от StringBuilder (Plan 152.7.1)
Status: CLOSED 2026-06-16 (Plan 152.7.1, commits a313926b + 3d0e30fa).
Depends on: D183 (Display/Debug протоколы),
D229 (Debug протокол), Plan 137 (protocol naming convention).
Amends: D183 (@display sig), D229 (@debug sig). Breaking: да.
Решение
Вводится тонкий sink-протокол Write, от которого зависят Display и Debug:
protocol Write {
mut @write_str(s str) -> ()
}
protocol Display {
@display(mut w Write) -> ()
}
protocol Debug {
@debug(mut w Write) -> ()
}
StringBuilder реализует Write через @write_str, делегируя в @buf.append(s):
fn StringBuilder @write_str(s str) -> () {
@buf.append(s)
}
Кодогенерация (статическая мономорфизация)
Write в C-кодогене статически мономорфизируется в Nova_StringBuilder* — vtable
отсутствует, нулевая overhead. Интерполяция "${x}" всегда монтирует StringBuilder
как единственный sink-тип на данном этапе.
type_ref_to_c:"Write"→"Nova_StringBuilder*"extract_protocol_type_name: bypass для"Write"→ не эмититE7201
Мигрированные реализации
Все встроенные @display/@debug impl переведены с sb StringBuilder на w Write:
int, f64, f32, bool, char, str (в std/prelude/protocols.nv),
Vec[T] (в std/collections/vec/protocols.nv), auto-derive синтезируемые методы.
Прецеденты
- Rust:
fmt::Write(write_str),Display::fmt(&self, f: &mut Formatter)— не вString. - Go:
io.Writer(Write([]byte)) —Fprintf/Fprintlnпринимаютio.Writer. - Java:
Appendable(append(CharSequence)) — базовый sink дляStringBuilder/Writer.
Связь
- D183 —
Displayпротокол (amended:sb→w Write). - D229 —
Debugпротокол (amended:sb→w Write). - Plan 152.7.1 — план; Plan 137 — источник
Display/Debug;StringBuilder— единственный concrete-sink в codegen. [M-152.7-write-sink]— CLOSED 2026-06-16.
D259. Конструктор-конвенция Vec[T] — of для литерала, from для конверсии (Plan 153.1)
Status: ACTIVE (Plan 153.1, формализована 2026-06-14). Depends on:
D232 (Vec[T]),
D239 ([]T ≡ Vec[T]).
AMEND (2026-07-06, of-guard) — пустой
of()запрещён контрактом. Я закрываю footgun, который ниже сам же и называю «идиоматичнееnew(), но можно»: пустой вызов вариадикаVec[T].of()(0 аргументов) теперь запрещён контрактомrequires args.len() > 0наVec[T].of(std/collections/vec/core.nv). Пустой вектор строится ТОЛЬКО черезVec[T].new()— двух равнозначных путей для одного и того же значения больше нет. Нарушение — runtime-panic (requires failed: args.len() > 0); фикстура —std/collections/vec/neg/vec_of_empty_panic.nv. Мигрированы все прежние вызовы.of()без аргументов на.new()(std/http/server/server.nv,std/http/server/wire.nv,nova_tests/http_decompress/decompress_test.nv); тесты, нарочно проверявшие легальность пустогоof()(spec_tests/conformance/d259_vec_of_vs_from.nv,nova_tests/plan153_0/variadic_of.nv), приведены в соответствие. Правило «КАНОН» ниже устарело в частиVec[int].of()— актуальный текст только в этом амендменте.
Что
Два конструктора Vec[T] несут разные роли, и их нельзя путать:
Vec[T].of(a, b, c)(вариадик) — построить вектор из литерального списка элементов. Аналог Rustvec![a, b, c].Vec[T].from(coll)(from(items []T)) — конвертировать существующую коллекцию / слайс в новыйVec. Аналог RustVec::from(iter)— этоclone-подобная копия.
Правило
// КАНОН
Vec[int].of(1, 2, 3) // литерал элементов → of (1 аллокация)
Vec[int].new() // пустой вектор — ЕДИНСТВЕННЫЙ путь (см. AMEND выше)
Vec[int].from(existing_vec) // конверсия существующей коллекции → from
Vec[u8].of(1, 2, 3) // of сужает так же, как from (args []T)
// АНТИ-ПАТТЕРН
Vec[int].of() // ❌ contract violation (AMEND 2026-07-06): requires
// args.len() > 0 — пустой of() запрещён, → new()
Vec[int].from([1, 2, 3]) // ❌ избыточно: под D239 литерал [1,2,3] УЖЕ Vec[int],
// поэтому from его КОПИРУЕТ во второй буфер (2 аллокации).
// `of(1,2,3)` берёт элементы напрямую (1 аллокация).
Vec[int].from([]) // ❌ → Vec[int].new()
from([литерал])— избыточный clone. Под D239 массив-литерал[1, 2, 3]сам по себе ужеVec[int](одна аллокация в точке литерала).fromкопирует его во второй буфер ⇒ две аллокации ради того же результата, чтоof(1, 2, 3)даёт за одну. Поэтомуfrom([литерал])— анти-паттерн; doc-commentfrom(вstd/collections/vec/core.nv) направляет наof.from— ТОЛЬКО для конверсии существующей коллекции (from(переменная)/from(другой_слайс)). Это легитимныйclone-подобный путь и НЕ анти-паттерн.- Когда тип выводится из контекста — просто
[a, b, c](литерал =Vec[T]), безof/from.of/fromнужны лишь для inline-указания типа (return-position, generic-контекст, когда контекст не выводит элементный тип).
Почему
Cost-transparency (D135): идиома, которая выглядит как «построить вектор из этих
элементов», должна стоить ровно одну аллокацию. of(...) это даёт; from([...]) —
скрыто удваивает. Разделение ролей убирает footgun и читается как Rust (vec![] vs
Vec::from(iter)), Kotlin (listOf vs toList), Swift ([a,b] vs Array(seq)).
Связь
- D239 —
[]T ≡ Vec[T]; именно поэтому литерал уже вектор иfrom([литерал])избыточен. - D232 —
Vec[T]-тип и его конструкторы. - Миграция тестов/stdlib
from([литерал])→of(...)— Plan 153.1 sweep ([M-153.1-of-vs-from-sweep]).
D260. Ленивый итератор Vec[T] — boxed-fluent адаптеры (Plan 153.2)
AMEND (2026-07-06, решение владельца): терминатор
@nth(n)РЕТРАКТИРОВАН (вместе сCharsIter @nth) — тождественskip(n).next()и провоцирует индексные привычки на итераторах (скрытый O(n)-«индекс», в циклах O(n²)). Канонический общий набор адаптеров:skip/take/step_by/enumerate/filter/chain+ терминаторыcount/collect/fold/find/… Миграция ~57 мест — волной[M-d73-d77-retraction-migration](переписывание на целевую итерацию, НЕ механикуskip(n).next()).
Status: ACTIVE (Plan 153.2 Phase A, 2026-06-14). Amended by
D277 (2026-06-15): BoxIter[T] помечен value →
wrapper-рекорд лоуэрится by-value (0 heap-аллокаций обёртки, Stage 1); добавлен
allocation-free generic-over-source sibling collections.vec_iter_zc (Stage 2).
Depends on:
D232 (Vec[T]),
D239 ([]T ≡ Vec[T]),
D58 (Iter/Next — VecIter). Закрывает:
Q-iterator-laziness, Q-iter-mut (Phase A).
Решение
Ленивый итератор Vec[T] реализован по boxed-fluent-модели. Канон лени — этос
cost-transparency (D135): цепочка v.lazy().map(f).filter(p).collect() не делает
промежуточных аллокаций; каждый адаптер оборачивает upstream-step-замыкание и тянет
по одному элементу на запрос; цепочку приводит в движение только терминатор.
type BoxIter[T] { priv step fn() -> Option[T] } // boxed-курсор
fn Vec[T] @lazy() -> BoxIter[T] // вход (мост VecIter→BoxIter)
fn BoxIter[T] @map[U](f fn(T) -> U) -> BoxIter[U] // адаптер → новый BoxIter
fn BoxIter[T] mut @collect() -> Vec[T] // терминатор драйвит цепочку
BoxIter[T]держит единственныйstep-thunk:Some(x)(следующий элемент) /None(исчерпан). Адаптеры строят новыеBoxIterобёрткойstep; ничего не выполняется, пока терминатор не потянет.- Вход
v.lazy()моститVecIter[T]→BoxIter[T](free-fnbox_iter[T], захватывающий курсор).[]TтождественноVec[T](D239) →lazy()есть и на слайсе. - Адаптеры (lazy, возвращают новый
BoxIter, без аллокации):map/filter/filter_map/enumerate/take/skip(Phase A). Каждый копирует receiver (mut src = @) в свежее захватывающее замыкание → цепочка реентерабельна на терминатор-вызов и не мутирует BoxIter вызывающего, пока терминатор её не сдренит. - Терминаторы (драйвят/коротят):
collect/fold/reduce/count/sum(zero T)/any/all/find/for_each/min/max/last(Phase A;nth— ретрактирован, см. AMEND выше).min/max— на[T Compare];sum(zero T)— аддитивная идентичность вместо числового протокола.
Модульное размещение
BoxIter/адаптеры/терминаторы — в sibling FILE-модуле
std/collections/vec_lazy.nv (module collections.vec_lazy), доступном через import std.collections.vec_lazy, НЕ внутри
prelude folder-модуля collections.vec. Причина та же, что у eager vec_seq (D239
status-note): prelude-global generic-type-метод с CLOSURE-телом утекает свои method-level
generics ([U]/[Acc]) и callback-параметры (f/pred) в merged-body КАЖДОГО юнита →
коллизия с top-level fn f/type Acc ([M-codegen-var-types-fn-scope] + D145). Адаптеры
closure-dense → opt-in import.
Eager vec_seq сосуществует
Eager collections.vec_seq (v.map(f) -> new Vec, материализует каждый шаг) оставлен
без изменений как переходный eager-surface. Lazy — канонический allocation-free путь
(Q-iterator-laziness); оба за раздельными explicit-import (eager НЕ переписан в сахар над
lazy, чтобы не навязывать lazy-import eager-пользователям).
Codegen-инварианты (обязательны для мономорфизации closure-несущих методов)
Реализация потребовала фиксов в compiler-codegen/src/codegen/emit_c.rs (без них —
silent CC-FAIL / drain-0 / segfault), зафиксированных как контракт:
- mut-capture box-реестр (
var_boxed) флашится per-test (emit_test) — box_box_<name>не должен утекать между C-функциями тестов. Stmt::Returnэмитит значение с типом возврата функции как target — голыйreturn Noneв mono-замыкании резолвится вNovaOpt_<mono>, не erasedNovaOpt_nova_int.infer_expr_c_typeрегистрирует generic-инстанс типа-возврата, когда generic free-fn ИЛИ метод generic-типа возвращает generic-инстанс (box_vec[int](it) -> BoxIter[int],Vec[T] @lazy() -> BoxIter[T]) — иначе.method()на временном промахивается мимо generic-instance dispatch-path (block 5b) и попадает в erased NULL-stub.
(Лифт mono×closures — register_generic_instances_in_typeref + closure-capture в loop-arms,
commit 996ca01a.)
Phase B — ✅ ЗАКРЫТА (2026-06-16, амендмент D260)
Критерии приёмки Phase B (все выполнены):
- G-B1.
zipвозвращает правильные пары; останавливается на более коротком операнде. - G-B2.
flat_mapкорректно обрабатывает пустой внешний итератор, пустые внутренние итераторы и смешанные пустые/непустые внутренние. - G-B3. Все адаптеры компилируются через C-codegen без CC-FAIL — без упрощений как для прода.
- G-B4. 0 новых регрессий по blast-radius (plan153_0/138/139/147/165/91_12).
Реализовано:
- ✅
step_by(n int)(BoxIter,vec_lazy.nv) + zero-costStepByIter[I,T](vec_iter.nv): yield каждый n-й элемент. Contractn > 0(requires). Тест:plan153_2/phase_b_lazy+plan153_2_zc/step_by_zc. - ✅
chain(other BoxIter[T])(BoxIter): дренирует self, затем other. Тест:plan153_2/phase_b_lazy. - ✅
zip(other BoxIter[B])(BoxIter): возвращаетBoxIter[(A,B)], останавливается когда любой из операндов исчерпан. Codegen-фикс: receiver typevar aliasA(fn BoxIter[A] @zip[B]) теперь биндится вtype_substпри dispatch — tuple return(A,B)резолвируется в mono_NovaTuple_2_8_nova_int_8_nova_intвместо erased_NovaTuple2. Тесты:plan153_2/zip_basic(9 pos),plan153_2/zip_neg(3 neg),plan153_2/zip_min. - ✅
flat_map(f fn(T)->BoxIter[U])(BoxIter): дренирует каждый inner-итератор, возвращённыйf. Codegen-фикс:NovaOpttypedef дляBoxIter[T]payload (NovaValue_ by-value) теперь эмитируется ПОСЛЕ generic struct body черезnovaopt_vr_typedefs_buf. Тесты:plan153_2/flat_map_basic(7 pos),plan153_2/flat_map_neg(4 neg).
Остаток Phase B (не реализован):
unzip/flatten/scan/inspect/take_while/skip_while/peekable/
min_by[_key]/max_by[_key]/partition/chunk_by/into_iter;
мут-итерация for mut x/mut @iter() (Q-iter-mut write-through — отдельный путь).
collect-target FromIterator — ✅ ЗАКРЫТ (Plan 153.6 / D264):
@collect()->Vec (default) + @collect_set()->Set (терминаторы) + from/from_iter/@extend
(прочие таргеты/источники); статический generic-конструктор + tuple-@collect_map gated
([M-153.6-collect-static-generic]/[M-153.6-collect-map-tuple-receiver]).
Zero-cost generic-over-source — реализован Stage 2, см. D277 ([M-153.2-generic-over-source-zerocost] → 🟡 PARTIAL).
Tuple-PRESERVING-адаптер сразу после enumerate — [M-153.2-tuple-elem-adapter]
(residual Option[<mono-tuple>] closure-typing gap; схлопнуть tuple через map).
Связь
- D232 —
Vec[T]; D239 —[]T ≡ Vec[T]. - D58 —
Iter/Next(VecIter— upstream-источник дляlazy()). - D135 — cost-transparency (no hidden O(n)) — обоснование лени.
- Q-iterator-laziness — закрыта (lazy = канон).
- Q-iter-mut — Phase A закрывает терминаторами/адаптерами; мут-итерация — Phase B.
- Plan 153.2 — план;
vec_seq.nv/vec_lazy.nv— реализация.
D264. Vec-протоколы: Hash + FromIterator / collect-target (Plan 153.6)
Статус: ✅ IMPLEMENTED — Hash (2026-06-13) + FromIterator/collect-target (2026-06-14).
Vec[T] дополняет набор протоколов (Equal/Compare/Clone/Display/Debug —
D230 amend)
двумя возможностями: content-Hash и FromIterator / collect-target (мост к
ленивому слою D260). Оба — под conditional-bound на T (Rust impl<T: Hash> Hash for Vec<T> / impl<T> FromIterator<T> for Vec<T>).
Vec[T Hash] @hash() -> u64
Order- и length-sensitive content-hash (protocols.nv): FNV-1a (64-bit), сворачивает
длину + per-element @hash() (h = (h ^ x) * prime). Consistency с @equal: равные
Vec (равная длина + element-wise ==) → равный hash (контракт Hash+Equal). u64-mul
врапается (Nova-семантика = FNV mixing-шаг); offset-basis — hex-литерал (десятичная
форма > i64::MAX). Делает Vec[T: Hash] сам Hash → элемент HashSet. (Вторая,
equality-половина ключ-контракта HashMap — [M-153.6-vec-hashmap-key-eq], pre-existing
generic-key-dispatch gap; @hash готов.)
FromIterator / collect-target
Nova структурно-типизирует итераторы (D58: любой mut @next() -> Option[T] итерируем; Next[T]/Iter[I] — протоколы), поэтому FromIterator — НЕ
отдельный enforced-протокол с одним методом, а набор конструкторов/терминаторов,
строящих коллекцию из любого итератора. Канон:
- Default collect-target →
Vec(ленивый слой D260):BoxIter[T] mut @collect() -> Vec[T]— материализует pipeline в один проход, без промежуточногоVecна стадию. Setcollect-target:BoxIter[T Hash] mut @collect_set() -> Set[T](dedup; Rustiter.collect::<HashSet<_>>()). Allocation-free над pipeline (pull + insert на лету).- Прочие таргеты — композицией над собранным
Vec:Set[T].from_iter(it.collect()),HashMap[K, V].from(pairs.collect()).Set.from_iter([]T)иHashMap.from([](K,V))уже принимают собранныйVec(под D239[]T ≡ Vec[T]). - FromIterator из произвольного
Iter-источника (без ленивой стадии):Vec[T].new().extend(src)— instance-метод@extend[S Iter[T]](mutate.nv) мономорфизируется корректно для любогоS(Range/VecIter/Vec). Прямой call-site идиом — НЕ требует обёртки.
Gated (compiler-gaps, не упрощение)
[M-153.6-collect-static-generic]— статический generic-конструкторVec[T].from_iter[S Iter[T]](src S)с for-in поSв теле не компилируется: boundS Iter[T]не резолвится для for-in dispatch внутри static generic-метода (typevar остаётсяNova_S). Тот же класс, что generic-method-dispatch-collapse (@cap/@splice). Instance-@extend(#4) — рабочий обход. NEG-фикстураcollect_static_generic_negлочит границу (EXPECT_COMPILE_ERROR for-in: type 'S').[M-153.6-collect-map-tuple-receiver]— прямой терминаторBoxIter[(K, V)] mut @collect_map() -> HashMap[K, V]не парсится: receiver type-аргумент кортежем (BoxIter[(K, V)]) отвергается парсером (expected identifier, got '('). HashMap collect-target остаётсяHashMap.from(pipeline.collect())(#3).
Зачем структурный набор, а не enforced-протокол
Один enforced FromIterator[T]-протокол с методом-конструктором потребовал бы
static-generic-method-dispatch (gated, см. выше). Структурный набор — это паритет Rust
(collect/FromIterator/extend) при существующей инфре: @collect/@collect_set
(терминаторы), from/from_iter (конструкторы из собранного), @extend (из источника).
Cost-transparency сохраняется: ленивый путь без промежуточных аллокаций (D260), материализация
именами collect*/from*/extend.
Связь
- D260 — ленивый слой;
@collect/@collect_set— его терминаторы. - D58 —
Iter/Nextструктурный duck-typing (основа FromIterator). - D239 —
[]T ≡ Vec[T](собранныйVec=[]T-аргументfrom_iter). - D230 amend — conditional-bound протоколы коллекций.
- Plan 153.6 — план;
vec_lazy.nv(@collect_set) /protocols.nv(@hash) /set.nv(from_iter) — реализация.
D277. By-value мономорфизация generic value-records + generic-over-source zero-cost адаптеры (Plan 153.2 Ф.2)
Status: ACTIVE (Plan 153.2 Stage 1 + Stage 2 + Stage 3 + Stage 4,
2026-06-14/15). Amends: D228 (value-record allocation contract —
распространён на generic type X[T] value {…}),
D260
(lazy-итератор — добавлен allocation-free sibling-слой). Зависит от:
D226 (always-pointer receiver ABI), D123/
D354 (mono-инфраструктура).
Маркеры: [M-153.2-generic-over-source-zerocost] → 🟡 PARTIAL (Stage 2 done),
[M-153.2-Z-closure-devirt] → 🟡 PARTIAL (Stage 3 alloc-elimination done),
[M-153.2-Z-noalloc-terminator] → ✅ DONE (Stage 4),
[M-153.2-closure-as-mono-type] (P3 остаток — call-инлайн).
Контекст
D228 дал by-value стек-codegen для не-generic value-рекордов (6
языковых типов: str + 5). D260
зашипил lazy-итератор по boxed-fluent-модели: BoxIter[T] — единый erased
курсор, держащий step-thunk; адаптеры не аллоцируют промежуточный Vec, но
сам wrapper-рекорд BoxIter[T] (как любой generic-рекорд) лоуэрился в heap
(nova_alloc(sizeof(Nova_BoxIter…)), один на адаптер), плюс per-element fn-ptr
индирекция через step(). Решение закрывает обе статьи накладных расходов в две
композируемые стадии.
Stage 1 — by-value мономорфизация generic value-records
type X[T] value {…} теперь лоуэрится BY VALUE для КАЖДОЙ конкретной
mono-инстанции, зеркаля не-generic value-record-путь (D228) и str-путь
(D26). Mono-инстанс X[int] = inline-struct NovaValue_<short>
(<short> = sanitized mono-имя), передаётся/возвращается/копируется по
значению, без nova_alloc для wrapper-рекорда. Receiver-ABI — always-pointer
(D226): NovaValue_<short>* на стек-слот (&obj / hoist+&temp
на call-site через prepare_method_recv), как у не-generic value-record.
BoxIter[T] помечен value (std/collections/vec_lazy.nv) —
тот же fluent API D260, но теперь 0 wrapper-аллокаций.
Codegen-контракт (обязателен — gate на AllocKind::Value, heap-generics не
тронуты):
receiver_c_typevalue-generic-mono receiver →NovaValue_<short>*, order-free относительноtype_aliases(helpersvalue_generic_mono_short/value_aware_generic_c_type).- generic-instance method-dispatch (block 5b) маршрутит value-receiver
через
prepare_method_recv→ передача по адресу. - fn-typed-field вызов
(@step)()— accessor.vs->выбирается по value-ness receiver’а ((*nova_self).step). - return-type inference (
infer_mono_method_ret_with_args+ dispatch 5b + overload-pool rt-strip) снимаетNovaValue_ПЕРЕДNova_, чтобыNova_<rt>-lookup в реестре попал (иначе method-level-generic-цепочка.map[U]коллапсировала вint).
Stage 2 — generic-over-source zero-cost адаптеры
Новый sibling FILE-модуль std/collections/vec_iter_zc.nv
(module collections.vec_iter_zc, opt-in import — тот же D145/leak-rationale, что
у vec_lazy/vec_seq). Rust-style Map<I,F>/Filter<I,P>: каждый адаптер —
generic-over-source value-рекорд (MapIter[I,T,U] / FilterIter[I,T] /
FilterMapIter[I,T,U]), держащий upstream-итератор INLINE полем src I (НЕ
boxed step-замыкание). @next() диспетчит (@src).next() статически,
мономорфизованно. Цепочка v.ziter().zmap(f).zfilter(p).zcollect()
мономорфизуется в ОДИН вложенный конкретный тип
FilterIter[MapIter[VecIter[int],int,int],int]; каждый next() инлайнится до
базового VecIter.next(). Вход — Vec[T] @ziter(); адаптеры zmap/zfilter/
zfilter_map; per-type терминаторы zcollect/zfold/zcount/zsum/zfor_each/zany/zall/
zfind.
Дополнительные codegen-фиксы (все gate на AllocKind::Value):
value_aware_subst_to_ref(новый&self-зеркало статическогоapply_type_subst_to_ref): nested-generic-арг несётNovaValue_-префикс → worklist-enqueued mono-имя СОГЛАСУЕТСЯ сtype_ref_to_c/field/type-decl именем (иначе две расходящиеся инстанции → undefined-struct CC-FAIL).split_top_level_mono_args+mono_type_args_of(registry-backed, depth-aware): наивныйargs_str.split("__")рвал nested generic-over-source арг на 3 фрагмента, мис-биндяI/T→ Vec[nova_int*] garbage. Применено в 3 split-сайтах.erased_type_ref_c— type-param-чек сделан РЕКУРСИВНЫМ (uses_any_type_param) → erased-stub возвращает erased-base-pointer, не placeholder-laden mono-имя.drain_generic_type_worklistplaceholder-guard (value-GATEDmangled_has_nested_placeholder) — skip эмита value-mono, чьё by-value поле ссылалось бы на undefined inner-placeholder struct, БЕЗ подавления нужного heapVec[Slot[K,V]]forward-typedef. Регресс-урок: over-eager ранняя версия guard’а (НЕ value-gated) сломала 15 HashMap/value-record файлов (plan139 t3/ neg_t3, plan152_4 case/normalize/graphemes/sentences/words+conformance, plan152_5 collation) — пойман baseline-бинарём @0da18125, FIXED гейтингом на value-шаблон; все 15 восстановлены.
Stage 3 — devirtualizация capture-free замыканий (alloc-elimination)
Замыкание БЕЗ свободных переменных (env = {int _dummy}) stateless: каждый
инстанс байт-идентичен и тело-функция никогда не читает env. Вместо ДВУХ
nova_alloc на каждый call-site (env-box + NovaClos_xx-box) эмитится ОДИН
file-scope static singleton (nova_lambda_N_clos_singleton +
nova_lambda_N_env_singleton) на closure-литерал, а call-site возвращает
(void*)(&singleton). Хирургическая правка — emit_lambda
(compiler-codegen/src/codegen/emit_c.rs ~31427),
capture-free fast-path. Соундно безусловно: static-адрес immortal — может
escape/store/outlive любой scope без dangling (Boehm видит его как root).
Захватывающие замыкания (free_vars ≠ ∅) — heap-путь БЕЗ изменений (immutable
by-value snapshot + mut by-ref box нужны per-instance, singleton нельзя шарить).
Это alloc-elimination половина closure-devirt’а: сам per-element ВЫЗОВ
(@f)(x) ещё идёт через NOVA_CLOS_CALL fn-ptr-макрос — true call-devirt =
закладка env как конкретного type-param (MapIter[I,T,U,F],
[M-153.2-closure-as-mono-type]). Маркер [M-153.2-Z-closure-devirt] (P3,
PARTIAL).
Stage 4 — alloc-free терминаторы + collect_into
В vec_iter_zc добавлен терминатор mut @zcollect_into(out mut Vec[T]) -> () на
каждый адаптер (MapIter/FilterIter/FilterMapIter): тело = zcollect-drain
МИНУС Vec[U].new() header-аллокация — пушит в переданный буфер out.
Семантика APPEND (НЕ чистит out; для свежего sink caller делает
out.clear() — len=0, буфер сохранён → амортизированный 0 аллокаций при
переиспользовании). Возвращает () (буфер виден через caller-биндинг — Vec[T]
heap-ref). Стриминг-терминаторы (zfold/zsum/zcount/zfor_each/zany/
zall/zfind) уже alloc-free по конструкции (скаляр/bool/Option-аккумулятор, без
out-Vec). Маркер [M-153.2-Z-noalloc-terminator] (✅ DONE).
Дизайн-решение: sibling, не замена
vec_iter_zc — НОВЫЙ sibling-модуль, boxed-fluent vec_lazy/BoxIter
сохранён как closure-fluent-альтернатива (единый erased курсор, единообразный
BoxIter[T] на каждой стадии). vec_iter_zc — allocation-free сиблинг (один
вложенный mono-тип на цепочку). Оба за раздельными explicit-import. Задокументировано
в шапке vec_iter_zc.nv.
Измерено (канон v.lazy/ziter().map().filter().collect())
boxed-fluent vec_lazy | zero-cost vec_iter_zc | |
|---|---|---|
| wrapper-record heap allocs (адаптер-цепочка) | 6 → 0 (Stage 1: by-value NovaValue_<short>) | 0 |
source-box (_box_src) | 9 | 0 (source inline полем src I) |
per-element step() fn-ptr индирекция | есть | убрана (статический dispatch) |
| capture-free closure env/box (Stage 3) | 4→0 (collect) / 6→0 (fold) — static singleton | то же |
| терминатор-тело (Stage 4) | — | 0 nova_alloc (collect_into/fold/sum/count/for_each/any/all/find) |
| остаточный heap | env ЗАХВАТЫВАЮЩИХ замыкания f/pred + VecIter source-курсор | то же — irreducible без closures-as-mono-types |
Stage 1 verify: grep nova_alloc(sizeof(Nova_BoxIter = 0 во всех
сгенерённых plan153_2/*.c; NovaValue_BoxIter… by-value struct — повсюду
(adapters 89 / chains 98 / laziness 64 / terminators 74 вхождений).
Stage 3 verify: nova_alloc(sizeof(nova_lambda_N_env)) /
nova_alloc(sizeof(NovaClos…)) для capture-free замыканий = 0 (заменены
file-scope static singleton); канон zmap(f).zfilter(p).zcollect() driver-тело
closure-allocs 4 → 0, та же цепочка .zfold(0,…) 6 → 0.
Stage 4 verify: все четыре мономорфизованных …method_zcollect_into тела = 0
nova_alloc (vs zcollect с …_static_new()); zfold/zsum/zcount/
zfor_each/zany/zall/zfind мономорфизованные тела = 0 nova_alloc
каждый.
Остаток (честно)
- per-element ВЫЗОВ
f/predещё fn-ptr-индирекция (void*+NOVA_CLOS_CALLна элемент) — Stage 3 убрал АЛЛОКАЦИЮ closure-env (capture-free → singleton), но не сам вызов. Rust-style инлайн мэппера требует closures-as-mono-types (env как конкретный type-param) — отдельный крупный лифт.[M-153.2-closure-as-mono-type](P3). - захватывающие замыкания всё ещё heap-env (per-instance; singleton нельзя шарить — by-value snapshot / by-ref box нужны свежими).
VecItersource-курсор — heap-ref-type alloc на.ziter()(свойствоVecIter[T], не замыкание; вне scope ступеней 3–4).take/skip/enumerate(stateful / tuple-element) остаются на boxedvec_lazy— порт = wiring, не новая compiler-способность.
Связь
- D228 — value-record allocation contract (распространён на generic).
- D260 — boxed-fluent lazy (0 wrapper-allocs via Stage 1; zero-cost sibling via Stage 2; capture-free closure devirt via Stage 3; alloc-free терминаторы +
collect_intovia Stage 4). - D226 — always-pointer receiver ABI (mono value-receiver).
- Plan 153.2 — план;
vec_lazy.nv/vec_iter_zc.nv— реализация;emit_c.rs::emit_lambda— Stage 3 singleton. - Cross-ref: D355 — blanket methods on
Next[T]implementors (терминаторы blanket).
D281. Module-level field privacy — type X priv { … } (Plan 160)
Status: ACTIVE (2026-06-15, Plan 160). Amends: D220 — расширяет type-level privacy-flip двумя уровнями. Зависит от: D78 (module-path convention), D29 (module model).
Мотивация
D220 дал priv (field-level, только own-type) и type X priv {...} (type-level flip, тоже only-own-type). Недоставало среднего уровня: module-private — поле видно во всём модуле (паттерн Go unexported + Rust pub(crate)). Позволяет folder-module или multi-file-module иметь shared internal state без публичного поля и без экстра accessor-boilerplate.
Синтаксис
// type-level modifier → meaning (задаёт дефолт для полей без явного modifier'а)
// (bare) → fields default public (D47, unchanged)
// priv → fields default module-private
// priv(type) → fields default type-private (only own methods)
// priv(file) → fields default file-private (only methods in same file)
export type Job value priv { // module-private by default
mut id int // module-private (наследует type-level default)
kind int // module-private
priv(type) secret int // type-private (stronger: only Job methods)
priv(file) internal int // file-private (только методы в этом файле)
}
priv без квалификатора = module-private — на уровне типа (задаёт дефолт полей) и на уровне поля (задаёт видимость явно).
priv(type) = type-private — аналогично: на уровне типа (дефолт) и на уровне поля (явно).
priv(file) = file-private — аналогично: на уровне типа (дефолт) и на уровне поля (явно). Поле доступно только из методов, определённых в том же файле.
Правило симметрично: все priv-квалификаторы ведут себя одинаково на type-уровне и field-уровне.
priv(module) — ОШИБКА (E_PRIV_QUALIFIER); используй priv без квалификатора.
Правило
§1 Effective visibility (четыре уровня)
| Контекст | Effective visibility |
|---|---|
Explicit pub field | public |
Explicit priv field | module-private |
Explicit priv(type) field | type-private |
Explicit priv(file) field | file-private |
Type-level priv(file) default, no explicit field modifier | file-private |
Type-level priv(type) default, no explicit field modifier | type-private |
Type-level priv default, no explicit field modifier | module-private |
| (ничего — D47 default) | public |
Лесенка строгости: public ⊃ module-private ⊃ file-private ⊃ type-private.
§2 Module identity
Модуль = папка (Nova module model, D29/D78). Файл принадлежит модулю P.Q если его rev-3 path = [P, Q]. Все файлы в project/src/foo/ декларируют module project.foo — они один модуль.
module_priv_access_allowed(type T): true iff current_module == type_defining_module(T).
§3 Access rules
Module-private field (из type-level priv default, без explicit field priv):
- Same module: read / write / init / pattern — РАЗРЕШЕНЫ.
- Other module: read →
E_FIELD_MODULE_PRIVATE, write →E_FIELD_MODULE_PRIVATE, init →E_FIELD_MODULE_PRIVATE, pattern →E_FIELD_MODULE_PRIVATE.
Type-private field (explicit priv(type) field, OR type-level priv(type) default):
- Same module, non-method: read →
E_PRIV_FIELD_READ(D220 error codes unchanged). - Own-type method: РАЗРЕШЁН — включая cross-instance:
fn T @eq(other T) -> bool => @f == other.fчитаетother.fвнутри методаT(см. D220 §3).
§4 Error codes
| Code | Context |
|---|---|
E_FIELD_MODULE_PRIVATE | Read/write/init/pattern of module-private field from outside module |
E_PRIV_FIELD_READ | Read of type-private field from non-method (includes same-module free fn) |
E_PRIV_FIELD_WRITE | Write of type-private field (D220, reused) |
E_PRIV_FIELD_INIT | Record-literal init of type-private field (D220, reused) |
E_PRIV_FIELD_PATTERN | Pattern destructure of type-private field (D220, reused) |
E_PRIV_QUALIFIER | priv(module) or unknown qualifier — use bare priv |
§5 Критерии приёмки (без упрощений, как для прода)
- Позитив:
type T priv { f int }в модуле M — свободные функции, методы и record-literal constructors в том же модуле читают/пишут/инициализируютfбез ошибок. - Негатив-read: import T из другого модуля,
t.f→E_FIELD_MODULE_PRIVATE. - Негатив-write:
t.f = xиз другого модуля →E_FIELD_MODULE_PRIVATE. - Негатив-init:
{ f: v }конструктор из другого модуля →E_FIELD_MODULE_PRIVATE. - Layering:
priv(type)field внутриtype T priv {...}остаётся type-private —E_PRIV_FIELD_READдаже в том же модуле из свободной функции. Bareprivfield внутриtype T priv {...}= module-private (accessible in same module free fn). - Public export: type + его методы (возвращающие/принимающие T) публично экспортируются — клиентский модуль может использовать API без доступа к внутренним полям.
- Regression:
nova testcore-suite без новых FAIL. - Негатив-pattern:
ro { f } = tиз другого модуля →E_FIELD_MODULE_PRIVATE(неE_PRIV_FIELD_PATTERN). Позитив-pattern:ro { f } = tв том же модуле — работает без ошибок.
AST / Checker-реализация
ast::FieldDefaultVisibility::Module— новый вариант enum (наряду сPublic,Private).RecordField.priv_module_field: bool— true если поле получило module-private (из type-level default ИЛИ из explicitprivfield modifier).TypeCheckCtx.type_defining_modules: HashMap<String, Vec<String>>— строится изpeer_files.items_here.TypeCheckCtx.current_module: RefCell<Vec<String>>— RAIICurrentModuleGuardпри входе вcheck_module.module_priv_access_allowed(tname)— compare maps.- 5 check-сайтов: INIT (record-literal), READ (member expr), WRITE (assign), PATTERN (destructure) — каждый distinguishes
priv_module_field→E_FIELD_MODULE_PRIVATEvspriv_field→ old D220 codes.
Связь
- D220 — per-field
priv+ type-level flip (D281 amends:priv= module-private,priv(type)= type-private). - D78 — module-path enforcement (defines module identity).
- D47 — default-public baseline (unchanged).
- Plan 160 — план реализации.
D307. File-private visibility — priv(file) (Plan 170)
Status: ACTIVE (2026-06-19, Plan 170). Зависит от: D281 / D220 (инфраструктура
priv/priv(type)), D29 (folder-module model), D78 (module-path). Нумерация: план назвал блок «D304», но к моменту реализации D304 уже был занят (Test Category Selectors, 09-tooling.md, Plan 169.1.1, 2026-06-19); D305/D306 были временно зарезервированы за proposed-планом 104.10 (LSP), но при реализации 104.10 получил D378-D380 (09-tooling.md) → D305/D306 СВОБОДНЫ; этому блоку присвоен D307.
Мотивация
folder-module = один compile unit из co-equal peer-файлов (D29/D78): все .nv в папке с одинаковым module X делят одно top-level пространство имён — fn/type/const каждого файла видны всем peer-файлам. Текущая лесенка видимости top-level бинарна:
export— виден снаружи модуля;- (без модификатора) — module-private (виден всем peer-файлам модуля).
Недоставало самого узкого уровня — file-private. Без него одноимённые helper-функции/типы в разных файлах одного folder-module конфликтуют (E_DUP_DEFINITION) и требуют некрасивого ordinal-rename (helper1/helper2). priv(file) закрывает дыру: символ виден только в своём файле, не утекает к peer-файлам. Аналог Rust pub(self). Польза шире тестов — любой folder-module (std/collections/vec/ и т.п.) получает file-local helper’ы без загрязнения общего namespace.
Синтаксис
priv(file) type Acc { … } // тип виден только в этом файле (prefix-форма)
type Job priv(file) { … } // эквивалент — priv(file) как type-modifier после имени
priv(file) fn helper() -> int // free fn не виден peer-файлам модуля
priv(file) const K = 42 // file-local константа
// (без модификатора) // module-private (как сейчас, D281)
export fn api() … // публичный (как сейчас)
Для type: priv(file) допустим и как prefix (priv(file) type X), и как modifier после имени (type X priv(file) { … }) — обе формы эквивалентны, выбор стилевой.
Для fn и const: только prefix-форма.
Лесенка видимости top-level символов: priv(file) ⊂ (module-default) ⊂ export.
priv(file) применим на двух уровнях:
- Top-level символ (
priv(file) type X,priv(file) fn f,priv(file) const K) — символ не виден peer-файлам модуля. - Поле типа (
priv(file) secret int) — поле доступно только из методов, определённых в том же файле. Симметричноpriv(module) иpriv(type)(D281).
priv(file) — это visibility-hint, НЕ смена module-резолва: модуль остаётся один (D29 не нарушается), символ лишь помечен «не виден peer-файлам». file — НЕ ключевое слово (распознаётся как Ident("file") внутри priv(...)), что исключает коллизию с идентификаторами.
Нейминг-обоснование (priv(file), не local): единая ось видимости под priv + scope-квалификатор — симметрично priv(type) (D281). local двусмысленно (вложенные функции — тоже «локальные») и потребовало бы нового KW с риском коллизии идентификаторов.
Применимость top-level. fn / type / const. Не применим к test/bench/lemma/let/ro и к методам (fn T @m — receiver-qualified, вне scope file-private). Scope-local const внутри тела fn — уже block-scoped, priv(file) не нужен.
Правило
§1 Резолв (checker)
При построении группового namespace folder-module из shared-набора исключаются priv(file)-имена каждого peer-файла. Для файла F видимый набор имён = (shared group namespace без любых file-private) ∪ (собственные file-private имена F). Поэтому sibling-файл никогда не резолвит чужой priv(file) символ.
Если файл F ссылается на имя, которое priv(file) в ДРУГОМ peer-файле той же группы (и иначе не резолвится) → специфичная диагностика E_FILE_PRIV_LEAK (вместо родового «undefined identifier»), с подсказкой «remove priv(file) … or move the symbol».
§2 Дедупликация (no-conflict)
Два priv(file) символа с одинаковым именем в разных peer-файлах — НЕ конфликт (непересекающиеся file-scope). Проверка «duplicate top-level name» для такого имени снимается тогда и только тогда, когда каждая декларация этого имени file-private И они в разных файлах. Если хотя бы одна декларация module-private/export — имя живёт в общем namespace → коллизия как раньше (E_DUP_DEFINITION, D29).
§3 Codegen (mangling)
priv(file) free-fn получает file-дискриминированное C-имя — nova_fn_<module>_f<file_id>_<name> — где дискриминатор = стабильный file_id объявляющего файла. Два одноимённых priv(file) fn helper из разных файлов дают разные C-символы → нет коллизии линковки. Call-site внутри файла резолвит в свой вариант по тому же (file_id, name) ключу, что и checker §1. File-private free-fns НЕ регистрируются в shared overload-registry (file-local, не участвуют в cross-file overload resolution). Точный паттерн переиспользует существующий private_const_c_names (Plan 160, уже keyed по file_id).
§4 Error codes
| Code | Context |
|---|---|
E_FILE_PRIV_LEAK | Ссылка из peer-файла на priv(file) символ другого файла группы |
E_PRIV_QUALIFIER | priv(file) + export вместе; bare top-level priv без (file); priv(<other>) на top-level; priv(file) перед не-fn/type/const |
§5 Критерии приёмки (без упрощений, как для прода)
- Парсинг:
priv(file) fn/type/constпарсится;priv(file)+export→E_PRIV_QUALIFIER. - Позитив own-file:
priv(file) fn h()вызывается в своём файле → компилируется и работает. - Позитив co-exist: два peer-файла, оба объявляют
priv(file) fn helper()с разным телом, каждый вызывает свой → компилируются (file-discriminated codegen, нет линк-коллизии) и работают. - Негатив leak: ссылка из peer на чужой
priv(file)→E_FILE_PRIV_LEAK. - Регрессия: module-private (
priv/default) символ из одного peer ВИДЕН другому (D281 без изменений);exportбез изменений. - Регресс: plan160 / plan124* / modules / std — 0 новых FAIL.
AST / Checker / Codegen-реализация
ast:FnDecl.file_private: bool/TypeDecl.file_private: bool/ConstDecl.file_private: bool(defaultfalse). Минимальный путь —bool(без enum-рефактораis_export).parser: распознаваниеpriv(file)в общем parse-item ДОKwExport-eat; пробрасывается вparse_fn/parse_type_decl/parse_const_decl. Взаимоисключение сexport.types: file-private dedup вcheck_module_impl(снятие dup-проверки для disjoint file-scope);NameResCtx.file_priv_leak: HashMap<FileId, HashMap<name, owner_path>>— per-file leak-table; групповой namespace вычитает file-private каждого peer.codegen:file_priv_fn_c_names: HashMap<(FileId, String), String>+current_emit_file_id— file-дискриминированный C-символ, резолвится вfree_fn_c_name/mangle_fn.
Связь
- D281 — module-private (соседний уровень лесенки); этот блок добавляет узкий file-private.
- D29 — folder-module model (не нарушается: модуль остаётся один).
- D78 — module-path / file identity (file_id источник дискриминатора).
- Plan 170 — план реализации.
D355 — Blanket protocol-receiver methods (Plan 161, 2026-06-15)
Renumber 2026-07-03: блок был D282 — номер коллидировал с D282 extern “nova”/“C” fn (двух-ABI FFI, Plan 91.12; тот сохраняет D282 — канон README-индекса, амендится Plan 174.6). Перенумерован в D355 (Plan 174 §6).
Status: ACTIVE (Plan 161 Ф.0-Ф.4, 2026-06-15). Amends: D241 (добавлен §3 «≤1 impl, cross-ref D355 (ex-D282)»). Зависит от: D72 (generic bounds), D119 (method-level type params), D277 (D277/vec_iter_zc). Маркеры: [M-161-blanket-receiver] → ✅ Ф.0-Ф.4 CLOSED; [M-161-parametric-return] (V2 followup).
§1 Синтаксис. fn[I Proto[T₁,…,Tₙ]] I @name[U₁,…](params) -> R { body } — blanket-объявление: I — typevar-ресивер, Proto[…] — bound. T₁,…,Tₙ выводятся из bound (не нужно объявлять явно). Запись fn[…] (glued, без пробела) = prefixed-generic header (уже разобрана парсером).
§2 Диспетч. При вызове expr.name(args), где тип expr — конкретный C, реализующий Proto[…]: blanket-метод виден на C, typevar I биндится в C. Конкретный метод (method_table[C]) всегда имеет приоритет над blanket.
§3 Мономорфизация. Mono-key = (C, name, extra_type_args). Тело компилируется как обычный generic-метод с заполненным I. Внутри тела I = конкретный C, typevar’ы из bound (T) = конкретные типы из impl-записи C.
§4 Инвариант (≤1 impl). Тип не может реализовывать Next[T] для двух разных T одновременно. Нарушение = E_DUPLICATE_PROTOCOL_IMPL.
§5 Область действия. Blanket-метод виден в модуле где объявлен + его importers (те же правила видимости, что у обычных методов). Конфликт двух blanket-методов с одним именем на одном протоколе = E_BLANKET_CONFLICT.
§6 Ограничения V1. Работает для методов с конкретными или fully-resolved возвращаемыми типами. Параметрические возвращаемые типы T, Option[T], Vec[T] — V2 ([M-161-parametric-return]). Один bound-уровень (I Proto[…]); цепные bounds — V2. Ресивер должен быть typevar, не конкретный тип.
Реализовано в. std/collections/vec_iter_zc.nv: @zfold, @zcount, @zfor_each, @zany, @zall — blanket на Next[T] (5 терминаторов, concrete return type). Перевод 12 per-adapter → 5 blanket деклараций (O(N²) → O(N)).
D284 — EnumerateIter — zero-cost enumerate adapter (Plan 162)
Status: ACTIVE (Plan 162 Ф.0-Ф.5, 2026-06-16). Зависит от: D277 (value-record mono / generic-over-source), D355 (blanket protocol-receiver для терминаторов, ex-D282), D260 (lazy-iterator layer). Маркер: [M-153.2-enumerate-zc] → ✅ CLOSED Plan 162; [M-153.2-tuple-elem-adapter] OPEN (chained adapter сразу после enumerate гейтнут на closure-type propagation). Было: [M-153.2-enumerate-zc] (enumerate deferred из Plan 153.2, было в boxed vec_lazy).
§1 Синтаксис. EnumerateIter[I, T] — zero-cost value-record adapter:
export type EnumerateIter[I, T] value { mut src I, mut i int }
Поля: src I — источник (inline by-value, статически диспетчится), i int — текущий индекс (стартует с 0). Результат @next() — Option[(int, T)]; Some((i, elem)) на каждый Some у источника, None транзитивно. Реализует Next[(int, T)] → совместим с blanket-терминаторами D355 (@zcount, @zfold, @zfor_each, @zany, @zall, @zfind, @zsum).
§2 Диспетч (@zenumerate). Метод @zenumerate() объявлен per-type (не blanket), потому что возвращаемый тип явно называет EnumerateIter (не параметрический конкретный тип в смысле D355 §6):
export fn VecIter[T] @zenumerate() -> EnumerateIter[Self, T] => { src: @, i: 0 }
export fn MapIter[I, T, U] @zenumerate() -> EnumerateIter[Self, U] => { src: @, i: 0 }
export fn FilterIter[I, T] @zenumerate() -> EnumerateIter[Self, T] => { src: @, i: 0 }
export fn FilterMapIter[I,T,U]@zenumerate() -> EnumerateIter[Self, U] => { src: @, i: 0 }
export fn TakeIter[I, T] @zenumerate() -> EnumerateIter[Self, T] => { src: @, i: 0 }
export fn SkipIter[I, T] @zenumerate() -> EnumerateIter[Self, T] => { src: @, i: 0 }
Self — mono-ресивер (D66 §«Self как вложенный generic type-arg»). Каждый adapter-тип регистрирует собственный @zenumerate. Не может быть blanket, потому что тело конструирует EnumerateIter[Self, _] — return-тип содержит конкретное имя адаптера, не generic typevar, раскрываемый из протокола-bound.
§3 Мономорфизация (tuple parametric return). @next() возвращает Option[(int, T)]. Компилятор разрешает тип кортежа (plan162 fix, emit_c.rs): перед вызовом type_ref_to_c для Option[(int, T)] биндинги type-variable из protocol-bound (I Next[T] → T = elem-тип источника) устанавливаются в type_subst_overrides. Arm Tuple в type_ref_to_c получает T уже разрешённым → эмитит типизированный mono’d struct (NovaTuple_2_8_nova_int_11__nova_int для int), не erased _NovaTuple2. Без этого фикса T оставался нераспознанным → fallback на erased legacy-форму → CC-FAIL при использовании .0/.1 полей.
§4 Инвариант (счётчик i). i стартует с 0. Инкрементируется ровно на 1 при каждом Some-ответе источника (пропуски на None не считаются — EnumerateIter.@next() прозрачно прокидывает None, не инкрементируя). Это соответствует enumerate() в Rust/Python: индекс = порядковый номер доставленного элемента, непрерывно с 0.
§5 Ограничения V1. (a) Нет blanket @zenumerate — return-тип называет EnumerateIter конкретно (contra D355 §6). Добавление нового adapter-типа требует явного @zenumerate. (b) Tuple-PRESERVING chained adapter сразу после enumerate (enumerate().filter(..) когда элемент остаётся (int,T)) гейтнут на closure-type-propagation codegen fix → [M-153.2-tuple-elem-adapter]; workaround: enumerate().map(|p| ...) (Map схлопывает кортеж). (c) EnumerateIter был deferred из Plan 153.2 (маркер [M-153.2-enumerate-zc] в boxed vec_lazy) — зашиплен в Plan 162.
Кросс-ссылки: D355 (blanket-receiver, гейтирует терминаторы на EnumerateIter), D260 (boxed lazy layer — предшественник), D277 (generic-over-source model — база для EnumerateIter).
Реализовано в. std/collections/vec_iter_zc.nv: EnumerateIter[I,T] value-record + @next() + chaining methods + 6 per-type @zenumerate() adapters. compiler-codegen/src/codegen/emit_c.rs: tuple parametric T-subst fix в blanket infer_type_refs_for_blanket. Тесты: nova_tests/plan162/ (9 basic + 8 chain + neg). D284 NEW.
D290 — Value-record iterator types (Plan 165, 2026-06-16)
Status: ACTIVE (Plan 165, 2026-06-16). Зависит от: D277 (generic value-record mono), D215 (value/reference allocation contract), D228 (fiber-arena GC root coverage для value-record с GC-pointer полями). Маркеры: [M-codegen-value-type-generic-forward-decl] ✅ CLOSED для VecIter/Range*Iter (Plan 165 Ф.1).
§1 Правило. Iterator-тип объявляется как value (stack-allocated), если выполняется одно из условий:
(a) Тип содержит только примитивные поля (int, u8, bool, f64, и т.д.) без GC-managed указателей → stack-аллокация безопасна без каких-либо оговорок.
(b) Тип содержит GC-pointer-поля, но является cursor’ом (итерационным состоянием над уже существующей структурой данных), не новым владельцем памяти → fiber arena автоматически корнирует value-record со ссылочными полями (D228 §escape), поэтому GC-безопасность сохраняется.
§2 Типы (Plan 165). Применено к:
VecIter[T] value(std/collections/vec/iter.nv) — содержитVec[T](GC-pointer-ссылка); cursor-тип, правило (b). Stack-slot + fiber-arena root.Range value,RangeIter value,StepRangeIter value,ReverseRangeIter value(std/collections/range.nv) — содержат толькоint-поля; правило (a). Чистый stack, zero GC involvement.
§3 Эффект на производительность. for x in v { } компилируется в VecIter[T] на стеке — malloc отсутствует. Цепочка 0..n (Range → RangeIter) — два int64_t на стеке. Adapter-цепочка zero-cost iterators (Plan 153.2 / D260 / D277) остаётся нулём аллокаций при нулевой escape (стековый инлайнинг адаптеров).
§4 Codegen инварианты (Ф.1 fix, коммит 1f92f106). Generic value-тип type X[T] value { … } при мономорфизации:
- Forward declaration должна содержать полное mono-имя:
typedef struct NovaValue_X____nova_int NovaValue_X____nova_int;(неNovaValue_X). - Несоответствие forward-declaration и struct definition → CC-FAIL «incomplete return type». Исправлено:
emit_forward_decl_for_generic_value_typeпередаёт mono-имя в полной форме. field_cache.rsпредикат примитивного листа включает"never"(строчные).
§5 Конвенция для новых iterator-типов. При добавлении нового iterator-адаптера (type FooIter[…] { … }):
- Если поля — только примитивы или ссылки на уже живущие GC-объекты (cursor-semantics) → объявить
value. - Если адаптер создаёт новую GC-память (например, буферизующий адаптер) → оставить heap-record.
- Проверить, что forward declaration генерируется с полным mono-именем (см. §4).
Реализовано в. std/collections/vec/iter.nv (VecIter value), std/collections/range.nv (Range/RangeIter/StepRangeIter/ReverseRangeIter value), compiler-codegen/src/codegen/emit_c.rs (generic value forward-decl fix), compiler-codegen/src/field_cache.rs ("never" лист).
D364 — Net C FFI pattern: opaque handles + value-record wrapping (Plan 91.12)
Source: Plan 91.12 Ф.0–Ф.4, 2026-06-16. Status: ✅ ACTIVE.
Связь: D214, D282 (extern "C" fn), D365.
Паттерн
Стандартный способ обёртки C-библиотечного ресурса в Nova (используется в std/net, типичен для std.ffi-слоя):
Шаг 1. Opaque handle (D214 §newtype): type CX(*()) — newtype над *() (void*). Даёт типобезопасность на FFI-границе без раскрытия внутренней C-структуры. Nova не может перепутать CTcpListener и CUdpSocket.
type CTcpListener(*())
type CTcpStream(*())
type CUdpSocket(*())
Шаг 2. Приватный FFI-слой (ffi.nv, без export): все extern "C" fn — module-private. Именование: <resource>_<action> (snake_case, без Nova-префиксов). C-side принимает typed handle pointer, возвращает новый handle (NULL = error) или числовой результат.
extern "C" fn tcp_listener_bind(addr CSocketAddr) -> CTcpListener // NULL on error
extern "C" fn tcp_stream_write(s CTcpStream, data str) -> int // bytes or -1
Шаг 3. Публичный value-record: export type TcpListener value { priv handle CTcpListener }. priv делает handle module-private (Plan 160 D281). Публичные методы делегируют в эффект (не в C напрямую).
export type TcpListener value { priv handle CTcpListener }
export fn TcpListener.bind(addr SocketAddr) TcpNet -> Result[TcpListener, NetError] {
TcpNet.bind(addr)
}
Шаг 4. Эффект-handler: real_tcp_net() — конкретный Effect[TcpNet] (D365), содержит прямые вызовы C FFI. Тесты заменяют handler на mock без изменения кода пользователя.
Инварианты
- FFI-функции НЕ экспортируются из модуля (
exportзапрещён наextern "C" fnвffi.nv-слое). - Конструктор public-типа через
_from_raw(h CX)— package-private factory. - Опасный
close()—consume @close(): потребляет тип, предотвращает double-close. - Ошибки: null/отрицательный результат + TLS
net_last_error()(cooperative-safe: нет yielding между C-вызовом и чтением ошибки).
Реализовано в
std/net/ffi.nv (Ф.1), std/net/tcp.nv (Ф.3), std/net/udp.nv (Ф.4). Тесты: nova_tests/plan91_12/ (19/19 PASS). D364 NEW (ex-D292 — renumber 2026-07-03, коллизия с ModuleSigTable-D292 07-modules).
D299 — AsSlice[T] protocol: contiguous-buffer abstraction (Plan 153.1, 2026-06-17)
Source: Plan 153.1 [M-153.1-append-extend-consolidation], 2026-06-17. Status: ✅ ACTIVE.
Связь: D141 (@extend / bulk-copy family), D238 (@index protocol).
Мотивация
Vec[T] @append(other Vec[T]) принимал только конкретный Vec[T]. Чтобы @append мог принять и slice-view ([]T = тот же тип Vec[T] с интерьерным указателем), и любой пользовательский contiguous-буфер, вводится protocol AsSlice[T] с двумя методами: @as_ptr() -> *T и @len() -> int.
Спецификация
// std/prelude/protocols.nv
export type AsSlice[T] protocol {
@as_ptr() -> *T
@len() -> int
}
Vec[T] реализует AsSlice[T] через #impl(AsSlice[T]) на @as_ptr(). Это покрывает и []T (алиас Vec[T]), включая slice-views (v[a..b]).
@append переписан с конкретного аргумента на generic bound:
// std/collections/vec/mutate.nv
export fn Vec[T] mut @append[S AsSlice[T]](other S) -> @ {
ro m = other.len()
if m > 0 {
@reserve(m)
unsafe {
RawMem.copy(other.as_ptr() as *u8, (@data + @len) as *mut u8, m * size_of[T]())
}
@len = @len + m
}
@
}
RawMem.copy (memmove) корректен для self-append: после @reserve регионы [0, m) и [@len, @len+m) не пересекаются.
Правила
- Реализующий тип должен гарантировать, что
@as_ptr()возвращает указатель на непрерывный буфер из не менее@len()живых элементов. - Дереференс
@as_ptr()внеunsafe { }запрещён (как уVec[T] @as_ptr). @extend(итерация черезIter) остаётся для не-contiguous источников.
Реализовано в
std/prelude/protocols.nv (protocol declaration), std/collections/vec/access.nv (#impl(AsSlice[T]) на Vec[T] @as_ptr), std/collections/vec/mutate.nv (обновлённый @append). Тесты: nova_tests/plan153_1/append_as_slice.nv (6 кейсов: vec→vec, пустые случаи, self-append, slice view). D299 NEW.
D300 — Vec generic forward-decl: body-site scan + tuple-elem fwd-decl (Plan 168, 2026-06-17)
Проблема: Vec[u32] в теле функции (локальная переменная, TurboFish-конструктор)
генерировал C-тип Nova_Vec____Nova_u32_p (через generic stub path),
тогда как в сигнатурах и полях тот же тип даёт Nova_Vec____uint32_t.
Pre-pass collect_array_elem_typerefs не заходил в тела функций →
typedef struct Nova_Vec____Nova_u32_p Nova_Vec____Nova_u32_p; отсутствовал в
глобальном preamble → CC-FAIL «unknown type name» в tuple typedefs.
Два исправления:
-
Body scan (
emit_c.rs): добавленыcollect_array_elem_typerefs_in_fnbody,collect_array_elem_typerefs_in_block,collect_array_elem_typerefs_in_stmt,collect_array_elem_typerefs_in_expr.scan_itemдляItem::Fnтеперь вызываетcollect_array_elem_typerefs_in_fnbody(&f.body, acc). При нахожденииExprKind::TurboFish { base: Ident("Vec"), type_args }— напрямую добавляет type_args как Vec elem TypeRefs. -
Tuple-elem fwd-decl (
emit_c.rs, строки ~3915): перед spliceMONO_TUPLE_TYPEDEFSпроходит по всем mono’d tuple instances, и для каждого pointer-field видаNova_...__...*(mono’d instance = содержит__) добавляетtypedef struct X X;в началоtuple_decls. Это обеспечивает forward-decl дляNova_Vec____Nova_u32_pи любых аналогичных типов, которые появляются в tuple field-types до своего полного struct-определения.
Результат: nova_tests/plan168 2/2 PASS; nova_tests/plan153_1 8/9 PASS
(1 pre-existing CODEGEN-FAIL resize_with_free_fn_shadow — не связан с fix’ом).
Инварианты:
Nova_Vec____<elem>— полная struct-definition эмитируется вgeneric_type_defs_buf(до fn-definitions, via marker splice)typedef struct Nova_Vec____<elem> Nova_Vec____<elem>;— вuser_type_fwd_decls(до tuple typedefs, via marker splice)- Tuple typedef может ссылаться на
Nova_Vec____<elem>*как incomplete pointer — OK по C99
D300 NEW.
D373 — Generic array API: sort/min/max/binary_search + _by variants (Plan 91.8c, 2026-06-17)
Статус: ACTIVE.
Мотивация
Plan 91.3 давал []int @sort() (concrete), Plan 91.8a закрепил Compare.compare -> int.
После 91.8a str и user-types с @compare должны быть sortable через generic dispatch.
D373 добавляет полный generic API поверх Compare.
API surface
Bound-based (T Compare)
fn[T Compare] []T mut @sort_of() -> @
fn[T Compare] []T @min_of() -> Option[T]
fn[T Compare] []T @max_of() -> Option[T]
fn[T Compare] []T @binary_search_of(target T) -> Option[int]
Суффикс _of избегает name collision с concrete []int @sort() / @min() / @max()
(те сохранены как fast-path для int; codegen выбирает concrete для []int exact-receiver).
Callback-based (без bound на T)
fn[T] []T mut @sort_by_of(cmp fn(T, T) -> int) -> @
fn[T] []T @min_by_of(cmp fn(T, T) -> int) -> Option[T]
fn[T] []T @max_by_of(cmp fn(T, T) -> int) -> Option[T]
_by_of variants не требуют Compare — порядок задаётся callback’ом.
Utility (без bound на T)
fn[T] []T mut @reverse_of() -> @
fn[T] []T @position_of(pred fn(T) -> bool) -> Option[int]
fn[T] []T @count_of(pred fn(T) -> bool) -> int
fn[T] []T @find_of(pred fn(T) -> bool) -> Option[T]
Алгоритмы
- sort_of / sort_by_of: stable insertion sort, O(n²). Подходит для массивов до ~1000
элементов. Followup
[M-91.8c-pdq-sort]для pdq/intro-sort на крупных данных. - binary_search_of: classic O(log n) binary search; requires pre-sorted array.
Returns
Option[int](index если найдено, None если нет). Отличие от Vec canonical@binary_search -> Result[int,int](D239 / vec/access.nv): тот возвращаетOk(idx)/Err(insertion_point)— более информативно; sort.nv вариант — legacyOptionform для backward compat. - min_of / max_of: linear scan O(n), None для empty.
- min_by_of / max_by_of: linear scan с callback, None для empty.
Backward compat
Concrete []int @sort() / @sort_by() / @min() / @max() в std/sort.nv сохранены.
Generic sort_of / min_of / max_of работают на любом T Compare включая int и str.
Overload resolution: конкретный receiver ([]int) выбирает concrete method, generic — _of.
Связь
- D183 (Plan 91.8a) — Compare.compare -> int convention.
- D178 — str.compare via native
nova_str_compare. - D72 — generic bounds
[T Protocol]. - D239 —
[]T ≡ Vec[T]alias. - Plan 91.3 — concrete
[]int @sort.
D373 NEW (ex-D185, renumber 2026-07-03).
D373 §amend-1 — direct @[i].method() dispatch in generic array methods
Дата: 2026-06-17. Закрывает: [M-91.8c-direct-index-method].
Codegen fix: при вызове @[j].compare(key) внутри generic fn[T Compare] []T-тела
компилятор не мог вывести тип элемента для SelfAccess-объекта и пропускал dispatch.
Изменения в compiler-codegen/src/codegen/emit_c.rs:
compute_array_elem_type_for_obj— добавлен arm дляExprKind::SelfAccess(ранее толькоExprKind::Identи другие;SelfAccessбез arm → элемент-тип не выводился).emit_monomorphized_method— при входе в метод заполняетarray_element_types["nova_self"]из receiver-типа mono-инстанса, делая тип элемента доступным при обработке вложенных@[i].method()-вызовов до того, как в теле появится explicit присвоение.
Результат: @[j].compare(key) в fn[T Compare] []T-телах (sort_of, binary_search_of и т.д.)
теперь диспетчится корректно без промежуточного binding’а.
D373 §amend-2 — @sort_unstable* переведены с heapsort на pdqsort (Plan 153.3.1, 2026-06-18)
Дата: 2026-06-18. Закрывает: [M-153.3-sort-pdqsort] + [M-91.8c-pdq-sort].
@sort_unstable / @sort_unstable_by / @sort_unstable_by_key теперь вызывают @_pdqsort вместо @_heapsort.
Алгоритм @_pdqsort (итеративный, без рекурсии — Nova-safe):
- n ≤ 1 → return immediately (no work)
- n ≤ 16 →
@_ins_sort_range(insertion sort, cache-warm) stack.len ≥ depth_limit(depth_limit = 2·ilog2(n)+2) → heapsort fallback на диапазон через temp Vec- иначе: median-of-3 pivot (→
@_median3_to_end) + Lomuto partition + Vec[int] work-stack (lo/hi pairs)
O(n log n) worst case, O(log n) stack space. Heapsort сохранён как depth-guard fallback и для @select_nth_unstable (не удалён). Stable @sort* (merge sort) не тронут.
D315. ResolvedType — единый канонический носитель типа (Plan 172.1, 2026-06-21)
Статус: ACTIVE. Single source of type truth. Реализует compiler-conventions.md §0. Supersedes
врезку M2 Plan 172.1 («ResolvedType достаточен как носитель» — спайк 2026-06-21 доказал обратное).
Что
Тип в компиляторе имеет ОДНО каноническое представление — ResolvedType. Из него выводится ВСЁ:
проверки, совместимость типов, конверсии и перевод в C. Legacy-путь «TypeRef → C»
(type_ref_to_c, который резолвит И переводит в одном проходе) — ретайрится.
Правило
TypeRef— только синтаксис (выхлоп парсера). Может нестиSelf, свёрнутые алиасы, неразрешённые имена, generic-параметры. Это сырая, неразрешённая форма → НЕ вход для перевода в C (сперва его надо разрешить).- Семантический анализ разрешает тип ОДИН раз в канонический
ResolvedType:Self→приёмник, алиасы развёрнуты, имена→конкретные объявления, дженерики. Каноничность: семантически равные типы → структурно равныеResolvedType(иначе совместимость = сравнение типов ломается). ResolvedTypeнесёт ПОЛНУЮ семантическую личность: разрешённая идентичность (а неpath.last()), generic-аргументы, ширина/знак (Scalar{width,signed,wide_default}), эффекты, все оси изменяемости (L1 binding / L2 view / L3 pointee, D246), верность модификаторов указателя.- Сахар нормализуется прочь (намеренно): имя алиаса, написание
Self. Это не потеря — это и делает окно единым. Исходное написание, нужное для текста ошибки, — отдельный диагностический канал, не часть канонического типа. - ABI/бэкенд-факты — НЕ хранятся в типе, а ВЫВОДЯТСЯ. Мангл-имя, erasure
(
Option[void*]→NovaOpt_nova_int),NovaValue_-префикс (D228), int64-слоты — это решения лоуэринга, не свойства типа. Их выводитresolved_type_to_c(живёт в codegen: финальная mono-подстановка + побочки эмиссии typedef’ов, но БЕЗ повторного резолва — резолв уже сделан чекером). - Один вывод (чекер), один лоуэринг (
resolved_type_to_c).type_ref_to_cретайрится: объявленные типы (сигнатуры, поля) тоже идут через «разрешить → опустить».
Почему
- §0 (единый источник истины).
type_ref_to_cсегодня делает ДВЕ вещи: (1) резолвит — разворачивает алиасы, превращаетSelfв приёмник, подставляет mono; (2) переводит в C. Первая половина дублирует чекер — ровно §0-анти-паттерн «codegen re-derive». Разделение «разрешить один раз (чекер) → опустить (codegen)» убирает дубль. - Два окна правды неизбежно дрейфуют.
ResolvedTypeдля проверок +TypeRef-driven перевод в C = тот самый класс баговVec[u32]-мис-манглинга (simple_type_ref_to_cдрейфнул, пропустил u32). Одно каноническое окно убивает класс дрейфа в корне. - Спайк (2026-06-21) доказал: текущий
ResolvedTypeлосси для C — берётpath.last()(теряет модуль), схлопывает*mut T(формаPointer(Mut)) вTypedPtr(Ro,…), разворачивает L2readonly. Значит прежнее допущение плана (M2: «носитель достаточен») неверно; D315 ставит целью обогащение до lossless-canonical + ретайрtype_ref_to_c.
Что отвергнуто
- «
TypeRef→ C — нормальный лоуэринг». Отвергнуто:TypeRefнеразрешён (Self/алиас) → не вход для перевода, его надо сперва разрешить. - «Ничего не обогащать; реконструировать
TypeRefизResolvedTypeна лоуэринге». Отвергнуто: лосси round-trip (выбрасываемый костыль); возвращает резолв-в-codegen — тот самый анти-паттерн. - «Второй лоуэринг
resolved_type_to_cрядом сtype_ref_to_c». Отвергнуто: два лоуэринга = фрагментация, которую §0 запрещает; цель — ОДИН. - «Хранить ABI-факты в
ResolvedType». Отвергнуто: засоряет семантический тип бэкенд-заботами; ABI выводится, не хранится.
Связь
compiler-conventions.md§0 — D315 это его конкретная формулировка про носитель типа (§10 анти-паттерн «два окна правды»).- D246 — три оси мутабельности;
ResolvedTypeобязан нести все три. - D129 / D227 —
int=i64wide-default vs sized; несётся черезScalar{width,signed,wide_default}. - D239 (
[]T≡Vec[T]), D228 (value-record), D216 (typed pointers) — это лоуэринг (ABI) факты, выводятся не хранятся. - Plan 172.1 U.4/U.5/U.6.1 — реализация: U.5
унифицировал внутреннее представление чекера; U.4 делает
ResolvedTypeносителем для codegen; ретайритtype_ref_to_c.
Эволюция
- U.5 (2026-06-20):
ResolvedTypeвведён как внутренний width/sign-тип чекера (заменилTy/TyCat/cat_of). - 2026-06-21 (D315): поднят из «внутренний тип чекера» в «единый канонический тип
компилятора»; спайк нашёл текущий носитель лосси → обогащение-до-lossless + ретайр
type_ref_to_cпоставлены целью. Supersedes врезку M2 Plan 172.1. - 2026-06-21 (Plan 172.1 U.4.6→U.4.8): цель реализована. U.5.5(a) сделал
ResolvedTypelossless для C (модуль-путь /*mut/ L2 readonly); U.4.6 построил единыйresolved_type_to_c(ABI-лоуэринг ЧТЕНИЕМ полейResolvedType, без повторного резолва) до byte-identical паритета; U.4.7 флипнулtype_ref_to_cна делегирование; U.4.8 (e1f1d96a) удалил дублирующийtype_ref_to_c_impl—resolved_type_to_cсталResult<String,String>(несёт причину отказа сам: usize/isize/ptr removed, Self-no-recv). Production type→C теперь ОДИН лоуэринг. Остаток — свернуть синтаксический адаптер-хопTypeRef→ResolvedTypeна объявленных-тип сайтах (U.6.1). - 2026-06-21 (Plan 172.1 U.5.5c,
f7511bda): носитель стал lossless и для ЭФФЕКТОВ —ResolvedType::Func.effectsбылVec<String>(только имя,from_type_ref→path.last(), дропал generics →Fail[E]терялE, нарушение «несёт ПОЛНУЮ семантическую личность»). Обогащён доVec<ResolvedType>(имя + module + type-args через losslessNamed). Разблокирует typed-errors Plan 173 (Fail[E]-dispatch поtype_id) + Plan 174.3 (any/is) — садятся на готовый носитель, не переделывая. Byte-identical конструкцией (effects write-only до consume).
D326 — ref как режим передачи параметра (safe in-out / borrow); @/-> @ формализация (Plan 172.5, 2026-06-26)
⚠️ РЕВИЗИЯ (Plan 184, 2026-07-06, sign-off владельца). Нижеследующая исходная формулировка D326 («
ref— режим, НЕ тип»; явные формыmut ref x T/ro ref x T; call-site маркерf(ref x)) ПЕРЕВЁРНУТА. Актуальная нормативная модель — раздел «## Ревизия D326 (Plan 184):ref T— ограниченный тип» ниже (правила Р1–Р14). Читать исходные R1–R12 как историю: R1 (ref не тип) и R4 (call-site маркер) — ретрактированы; их места помечены «⛔ РЕТРАКТИРОВАНО Р-184». R2/R3-формы параметра (mut ref/ro ref) заменены наmut x T/x T(Р10).
Source: Plan 172.5 (in-out ref params), 2026-06-26 — owner переоткрыл Q29 в param-mode.
Status: 🔄 REVISED (Plan 184, 2026-07-06): исходное ядро (Plan 172.5) реализовано, но модель пересмотрена — ref T становится ограниченным типом (параметры-внутренне / возвраты / локалы), маркер вызова и формы mut ref/ro ref в сигнатуре удалены. Актуальные правила — раздел «Ревизия D326 (Plan 184)» ниже. Историческая справка о landed-ядре 172.5 (mut ref in-out params, эксклюзивность E_REF_ALIAS_OVERLAP, addressability/mut-place/escape-ban; 2 pos + 11 neg фикстуры) сохранена в исходном тексте.
Amends: Q29 (open-questions.md — снимает отвержение param-mode; ref-ТИП остаётся отвергнут), D132 (03-syntax.md — alias-гарантия -> @ ↔ R7), D228 (value-record @ escape-decay R8).
Adopt verbatim: D181/D184 (режим возврата @). Bounds: D157 (05-memory.md) + D246-P10 (эксклюзивность УЗКАЯ, не Rust/Swift).
Cross-ref: Plan 172.4 / Q-value-abi-auto-placement (авто-ro ref + heap↔stack — НЕ дублировать), D315 (ABI выводится), D246 (L3 pointee-cap / RETURN-оракул), D131/D133/D180 (consume — borrow≠move), D156 (consume-bound), Plan 174.5/174.6 (raw pointers / FFI).
Ревизия D326 (Plan 184): ref T — ограниченный тип
Source: Plan 184 (ref-type-revision), обсуждение 2026-07-06, дизайн надиктован владельцем
(четыре раунда решений). Status: 🟢 РЕАЛИЗОВАНО ПОЛНОСТЬЮ (заходы 1-6, 2026-07-07).
Факт по правилам: Р1✅ Р2✅ Р3✅ Р4✅ Р5✅ Р6✅ Р7✅ Р8✅ Р9✅ Р10✅ Р11✅ Р12✅
Р13✅ Р14✅ (заход-6: режим параметра {ro,mut,consume} — ось перегрузки, единая с
receiver-mut; раздельные C-символы через MethodSig.param_modes + collision-triggered
mode-tag; диспатч narrow_by_param_mode по изменяемости аргумента-биндинга / owned-
временному; см. D84-подраздел «Ось режима» в 10-overloading.md и docs/plans/184). Носитель — TypeRef::Ref(T); коды
E_REF_TYPE_POSITION (Р1/Р6), E_REF_ESCAPE (Р8), E_REF_ALIAS_OVERLAP (Р9/Р12),
E_MUT_ARG_NOT_MUTABLE (Р10); ref-локалы — истинный указатель-алиас (write-through).
Amends: переворот исходного D326-R1 («режим, НЕ тип» → «ограниченный тип»); ретракция
D326-R4 (call-site маркер); замена форм D326-R2/R3 (mut ref/ro ref в сигнатуре) на
mut x T / x T (Р10). Q29-история уточняется: Q29 отвергал НЕограниченный ref-тип
(ссылки в полях/коллекциях/кучевые утечки → лайфтаймы); ограниченный ref T (только
параметры-внутренне, возвраты, локалы; запрет утечки в кучу) — НЕ то, что отвергал Q29.
Суть
ref T — валидный тип (аналог C++ T&), но ограниченный позициями: параметр
(внутренне), возврат, локальная переменная. Мотивация-первопричина: легализовать тип
приёмника — @ это всегда ref Self (для стековых value-типов), и -> @ — тоже. Ссылка на
стек не должна утекать в долгоживущую кучу — отсюда позиционное ограничение.
Правила (нормативно)
(Р1) Позиции ref T. Легален: возврат (-> ref Self — де-сахар -> @ для
value-типов), локал-алиас (ro y ref T = x.a.b / mut y ref T = … — именованная ссылка на
место; истинный указатель-алиас в кодогене, mut-запись достигает цели), тип приёмника
(@ = ref Self). ЗАПРЕЩЁН в: полях record/value, коллекциях (Vec[ref T]), суммах
(enum), Option[ref T], тип-аргументах дженериков (turbofish f[ref T],
size_of[ref T] и любой f[...]) И в параметрах (формы ref сняты Р3/Р10). Диагностика в
запрещённой позиции — E_REF_TYPE_POSITION. Причина запрета: ссылка на стек-сторадж не
должна попасть в долгоживущую кучу (без этого нужны лайфтаймы); ref нехраним и не имеет
«размера как тип» — только ABI-деталь передачи, поэтому в тип-аргументе он = код, зависящий от
режима передачи. Взаимодействие с Р6: проверка позиции идёт ПОСЛЕ нормализации ref H ≡ H — для КУЧЕВОГО H ref H легален в любой позиции (в т.ч. тип-аргументе: f[ref H] ≡ f[H], size_of[ref H] == size_of[H]), реджектится только ref <value/generic/unknown>.
Реализация: types/mod.rs walk_typeref Ref-арм + ref_target_confirmed_heap;
легальные top-level позиции снимают ведущий ref через walk_ref_return.
(Р2) mut при ссылке. В mut-контексте ссылки mut = право ПИСАТЬ через ссылку в
цель. Сама ссылка непереселяема (как C++ T&: связал — навсегда; переприсваивания
ссылки на другой сторадж нет).
(Р3) ref ИСЧЕЗАЕТ из сигнатур параметров (Р10-развёртка). Формы mut ref x T /
ro ref x T / ref x T в списке параметров — удалены. Синтаксис параметра — тройная
ось режима БЕЗ слова ref:
ro:f(x T)— представление (копия ≤~16 Б либо скрытая ссылка) выбирает компилятор по размеру; ненаблюдаемо (с оговоркой Р12). Явногоro ... refв параметре нет.mut:f(mut x T)≡ in-out ссылка ВСЕГДА (унификация стека/кучи: на куче приватной мутируемой копии и так нет). Локальная приватная копия — явным локаломmut y = x.consume:f(consume x T)— владение; представление считает компилятор.refостаётся ТОЛЬКО как: тип приёмника (@=ref Selfу value),-> @(=-> ref Selfу value), локальные алиасы (Р1). Диагностики парсера при попытке старых форм:E_REF_PARAM_FORM_REMOVED(mut ref/ro ref/refв параметре → hint «пишитеmut x T(in-out)»),E_REF_CALL_MARKER_REMOVED(маркер вызова, ниже Р4).
(Р4) МАРКЕР ВЫЗОВА ref x — УДАЛЁН. Вызов везде f(x) — без ref x. Обоснование
владельца: кучевые объекты и так мутируются без маркера — маркер на стековых давал ложное
чувство «нет ref = нет мутации». f(ref x) → парс-ошибка E_REF_CALL_MARKER_REMOVED
(hint «маркер удалён (D326-ревизия): пишите f(x)»). ⛔ РЕТРАКТИРУЕТ исходный D326-R4.
(Р5) Автоконверсия. ref T -> T автоматически (чтение = разыменование).
T -> ref T на месте вызова автоматически для адресуемых аргументов (неадресуемый аргумент
к mut-ссылке — ошибка E_REF_ARG_NOT_ADDRESSABLE, как в исходном D326).
(Р6) Нормализация над кучей. Для кучевого H: ref H ≡ H (значение уже handle;
ссылка на handle не вводится). Важно для обобщённых f[T](mut x T) при T = H — mono не
плодит ref-обёрток над handle.
(Р7) Типизация приёмника (таблица — заменяет эвристики D181/R7-R8).
Категория Self | ro @ | mut @ | -> @ |
|---|---|---|---|
| Стековый (value) тип | ref Self (или копия по размеру, невидимо) | mut ref Self | -> ref Self |
| Кучевой (heap) тип | Self | Self | -> Self |
Магия D181-R7/R8 («heap алиас / value копия-с-распадом» через RETURN-оракул и escape-decay)
ЗАМЕНЯЕТСЯ этими типами: -> @ теперь имеет конкретный тип (-> ref Self у value,
-> Self у heap), а не разрешается эвристикой на bind-site. consume @ -> @ остаётся
парс-ошибкой E_CONSUME_RECEIVER_RETURNS_AT.
(Р8) Безопасность без лайфтаймов. Висячие ссылки исключаются связкой: запрет утечки в кучу (Р1) + существующий escape-анализ с авто-промоутом (D216 §4: убегающий источник поднимается в кучу) + запрет захвата ссылок замыканиями/spawn/parallel. Только синхронный lifetime вызова.
Уточнение Р8 (заход-5): &<ref-цель> и авто-промоут. Для ССЫЛКИ-цели (приёмник @
value-типа = ref Self; ref-локал) авто-промоут D216 §4 НЕ применяется — промоутится только
СВОЁ значение-локал, а ref указывает на ЧУЖОЕ хранилище (промоут копии тихо меняет
семантику; промоут исходного места функции недоступен). Правило:
&<ref-цель>эскейпящий НАРУЖУ (return / захват замыканием / запись в поле) → реджектE_REF_ESCAPEс подсказками (взять владениеconsume @+ промоутить своё; либо явная локальная копияro v = @и вернуть&v). Живой пример-негатив:fn T @addr() -> *T => &@.&<ref-цель>ВНИЗ по стеку (аргумент вызова, ffi out-параметр) → легален без промоута (заём короче исходного) — действующий канонnova_str_parse_f64(s, &v).- адрес КУЧЕВОГО содержимого через ресивер (
Vec @ptr => @data) — легален (не путать: у heap-приёмника@= handle, промоут копии handle корректен). Реализация:types/mod.rs check_ref_addr_escape— value-приёмник,&@в escaping-позиции (возврат/trailing/return/тело замыкания/RHS-в-поле). Захват ref-локала замыканием — follow-up.
(Р9) Эксклюзивность/адресуемость/FFI — без изменений от исходного D326 (узкая
синтаксическая E_REF_ALIAS_OVERLAP; на extern-границе только сырые */*mut, никогда
ref), но эксклюзивность расширяется — см. Р12.
(Р10) ref исчезает из сигнатур параметров вовсе (развёрнуто в Р3): единая тройная ось
{ro, mut, consume} без ref-слова; представление ro выбирает компилятор по размеру;
mut = in-out всегда; consume = владение. Неоднозначность перегрузок исчезает по
построению (форма параметра одна на режим).
(Р11) Аудит-миграция mut-параметров. Семантика mut x T меняется (была приватная копия →
станет in-out): перед реализацией Ф.2 — grep-аудит всех mut-параметров std/тестов; кто
полагался на приватность копии (мутирует параметр как рабочую копию, а вызывающий использует
исходное значение после) — переписать на явный локал mut y = x. Результат аудита — в отчёте
Ф.0/Ф.1 плана 184.
(Р12) Расширение узкой эксклюзивности. ro-автопредставление наблюдаемо при алиасинге
ro×mut в ОДНОМ вызове (f(a, a) c f(x Big, mut y Big): ссылка vs копия дают разное
чтение x после записи y). E_REF_ALIAS_OVERLAP расширяется с пар mut×mut на пары
mut×(любой параметр того же root-пути) — тот же синтаксический критерий R9 (стирание
индексов, prefix-overlap). Реализовано (заход-5): check_ref_arg_modes собирает места
НЕ-mut value-параметров и проверяет пересечение с mut-местами; f(a,a) при
f(x Big, mut y Big) → E_REF_ALIAS_OVERLAP; ro×ro того же места и distinct — легальны.
(Р13) Перегрузка по режиму параметра — ЛЕГАЛЬНА, по прецеденту ресивера. Ось
«изменяемость ресивера» уже существует (Plan 135, D84 —
fn T @m() vs fn T mut @m(): разные символы, диспатч по изменяемости биндинга). Для
параметров — то же правило: f(x T) vs f(mut x T) различаются изменяемостью
аргумента-биндинга; mut-аргумент предпочитает mut-перегрузку, иначе ro (приоритет как у
ресивера).
(Р14) @ — форма параметра; единая тройная ось режима. Ресивер = нулевой параметр. Ось
диспатча {ro, mut, consume} едина для ресивера и параметров: различимы
@func / mut @func / consume @func И f(x T) / f(mut x T) / f(consume x T). Правило
выбора: ro-аргумент → только ro-версия; mut-биндинг → mut-версия приоритетнее ro (прецедент
ресивера, Plan 135); consume-версия участвует в резолве ТОЛЬКО когда аргумент в последней
точке использования (consume-чекер это уже вычисляет) — тогда она специфичнее mut; иначе
исключена. Детерминизм без молчаливого потребления живого биндинга. См. амендмент
D84 (тройная ось режима параметров).
✅ СТАТУС Р13/Р14 (заход-6, 2026-07-07): РЕАЛИЗОВАНО.
f(x T)/f(mut x T)/f(consume x T)— легальные перегрузки (ось режима параметра входит в overload-сигнатурный ключ:types/mod.rsdup-детект сравниваетis_mut+consumeкаждого параметра наряду с типом/арностью/возвратом/receiver-mut). Раздельные C-символы —MethodSig.param_modes+ мангл-суффикс режима, добавляемый ТОЛЬКО при коллизии одинаковыхparam_c_types(кучевой случай Р6; существующие символы byte-identical). Диспатч —emit_c.rs::narrow_by_param_mode: ro-аргумент → ro-версия; mut-биндинг → mut-версия (приоритетнее ro, прецедент Plan 135); owned-временное (последняя точка) → consume-версия. Неоднозначность невозможна по построению (классы аргумента взаимоисключающи). Полные факты и правило выбора — D84-подраздел «Ось режима {ro, mut, consume}» в 10-overloading.md. Ограничение: value/примmut-mode-overload меняет ABI на указатель — scoped follow-up[M-184-value-mut-mode-overload-abi]; кучевой T (канонический in-out) покрыт полностью.
Что ретрактировано ревизией Р-184
- D326-R1 (частично): «
ref— режим, НЕ тип; запрещеныref T-локалы/возвраты» — ретрактировано.ref Tтеперь ограниченный тип (Р1): легален в параметрах-внутренне, возвратах, локалах-алиасах. Запрет сохранён только для полей/коллекций/сумм/Option. - D326-R2/R3 (формы параметра):
mut ref x T/ro ref x T/ref x Tв сигнатуре — удалены; заменены наmut x T/x T(Р10). ДиагностикаE_REF_PARAM_FORM_REMOVED. - D326-R4 (call-site маркер
ref x): удалён полностью (Р4). ДиагностикаE_REF_CALL_MARKER_REMOVED. - D181/R7-R8 эвристики (RETURN-оракул + escape-decay для
-> @): заменены типами Р7.
Точки возобновления (для последующих заходов)
- Ф.2 (чекер): in-out семантика
mut x Tдля стековых типов (типRef(T)в носителе, нормализация Р6, запрет позиций Р1, эскейп-правила Р8, расширение эксклюзивности Р12, перегрузка Р13/Р14). До Ф.2 парсер принимаетmut x T, но чекер оставляет старую семантику (приватная копия) — зелёное дерево не ломается. - Локалы-ссылки
ro y ref T = expr(Р1): парсер/чекер — если не влезло в Ф.1,refв тип-позиции пока остаётсяE_REF_NOT_A_TYPE.
Что
ref — режим передачи параметра (borrow), НЕ тип. Даёт безопасную in-place мутацию caller-значения (mut ref) и zero-copy чтение больших стек-значений (ro ref, авто) без сырых указателей и без лайфтаймов. Модель = Swift inout / C# in+ref. @-ресивер — частный случай: mut @ ≡ mut ref @, ro @ ≡ ro ref @, -> @ ≡ ref @.
Правило
(R1) ref — режим параметра, НЕ тип. ⛔ РЕТРАКТИРОВАНО Р-184 (см. ревизию выше: ref T теперь ограниченный тип; запрет сохранён только для полей/коллекций/сумм/Option). Запрещены: ref T-локалы/биндинги, ref-поля, ref в Vec/коллекции/sum/Option, ref-возвраты. Единственное исключение — ресивер @ и его -> @. Лайфтаймов нет. Это НЕ реинтродукция отвергнутого Q29 ref-ТИПА — это param-mode, консистентный с обоснованием самого Q29.
(R2) Две формы. ro ref a T — read-only borrow (авто). mut ref a T — mutable in-out borrow (единственный явный user-facing; callee пишет в caller-сторадж, видно после синхронного вызова).
(R3) ro ref — авто/невидимо (это Plan 172.4 / Q-value-abi-auto-placement, не дублируем). Компилятор передаёт value-параметр скрытым ro-указателем вместо копии, когда sizeof > ~2*sizeof(ptr) (≈16B) и копия ненаблюдаема; семантически тождественно by-value для ro. Маркеров нет.
(R4) call-site маркер ref. ⛔ РЕТРАКТИРОВАНО Р-184 (маркер вызова удалён; вызов везде f(x); f(ref x) → E_REF_CALL_MARKER_REMOVED). mut ref-аргумент на месте вызова помечается: inc(ref x) (короткий ref, не mut ref). Цель — читатель видит возможную мутацию x, не открывая сигнатуру (как C# ref, Swift &, Rust &mut; & занят addr_of, Plan 118.1). ro ref (авто) маркера НЕ имеет.
(R5) ABI ресивера. mut @ ≡ mut ref @ — всегда by-pointer (любой размер; нужно для видимости мутации; даже 1-байтный type Flag value {b bool}). ro @ ≡ ro ref @ — size-discretionary (мелкий by-value, большой hidden-ptr; невидимо — ro content-immutable). Это две РАЗНЫЕ ref-нормы (auto-size для params vs always-by-ptr для mut-ресивера), не сливать.
(R6) -> @ — режим = режим ресивера (D181 дословно, не расширяется). fn T @m() -> @ → ro ref @; fn T mut @m() -> @ → mut ref @; fn T consume @m() -> @ → parse-error E_CONSUME_RECEIVER_RETURNS_AT. В цепочке x.a().b() режим @ от a() гейтит вызываемость b(): ro ref @ → только ro @-методы; mut @ на нём → E_RECEIVER_BINDING_NOT_MUT (c.peek().bump() = ошибка).
(R7) -> @ при биндинге — НЕ новое decay-правило, а D246 RETURN-оракул для типа ресивера:
- heap-record / consume / builder-ресивер → биндинг (
ro y =Иmut y =, вкл. mid-chainmut b = sb.append(); b.append()) — ВСЕГДА АЛИАС, не копия (гарантия D131/D132, load-bearing для consume-чекера; копия раздвоила бы два хэндла на один буфер → потеря use-after-consume). - value-record-ресивер → биндинг = D246
-> Value-оракул = копия (те же ro/mut-знаки коэрсии, ORACLE D).
(R8) value-record @ НЕ эскейпит указателем. В пределах одного полного выражения (пока сторадж ресивера жив) -> @ может оставаться ref; при биндинге/возврате/хранении — decay by-value (D246-оракул). Для rvalue-ресивера (P{x:0}.inc()) хвостовой @ decay’ится в слот биндинга/возврата, НЕ указателем в __tmp_recv (умирает в конце выражения). D228-escape-walker @ не покрывает (срабатывает на явный &v) → этот decay — замещающее escape-правило. E_AT_RETURN_OUTSIDE_METHOD (free-fn -> @) сохраняется.
(R9) Эксклюзивность mut ref — УЗКАЯ синтаксическая, НЕ Rust/Swift. E_REF_ALIAS_OVERLAP срабатывает ⟺ два mut ref-аргумента в ОДНОМ вызове — проекции одного root-локала и один путь — префикс другого (после стирания индексов, не доказуемо-различных int-литералов). f(mut ref x, mut ref x) reject; f(mut ref x.a, mut ref x.b) OK; f(mut ref x.a, mut ref x) reject; arr[1] vs arr[2] OK; любой неконстантный arr[i] vs arr[j] → консервативно overlap (нет SMT/i≠j-прувера в V1, over-reject sound). Явно оговорить в спеке: это анти-footgun, НЕ гарантия — aliased-мутация остаётся sound-под-GC везде ещё (D157, D246-P10 «нет эксклюзивности (GC)»). Через указатели/heap-хэндлы/два []T-слайса над одним буфером эксклюзивность НЕ заявляется (undecidable).
(R10) Эскейп ref запрещён (кроме @, который сам decay’ится R8): не хранить в поле/heap, не захватывать closure/spawn/parallel/supervised/detach, не возвращать. Только синхронный lifetime вызова — это и делает no-lifetimes-модель звучной.
(R11) Слоинг / FFI. ref = safe non-null non-escaping pointer (повседневный инструмент); *T/*mut T = сырые (FFI/unsafe, D246 L3). mut ref ≈ pointee-mut от *mut T но safe+non-escape; ro ref ≈ *T. На extern-границе — только сырой */*mut, никогда ref (у ref нет стабильного ABI — это lowering-выбор).
(R12) Дженерики / consume. fn f[T](mut ref x T) — ок (mode ортогонален type-param, не в type-arg-позиции → mono не затронут). [T consume]-bound (D156) несовместим с ref на том же месте (borrow ≠ move). mut ref НЕ консьюмит аргумент (D131/D133 без изменений). Overlap-отношение — НОВОЕ (per-pair prefix), строится РЯДОМ с consume place-анализом (делит только парсинг места Ident/Member/Index, не решётку MOVE/CONSUME) — НЕ «субсумировано D131».
Почему
mut @ + lvalue (как считал Q29 2026-06-21) НЕ покрывает: out-параметры, мутацию НЕ-ресивера, несколько mut-аргументов (swap(mut ref a, mut ref b)). Swift (inout) и C# (in/ref) оба имеют user-facing in-out РЯДОМ с авто-оптимизацией — не полагаются только на методы. Узкий mut ref закрывает дыру минимальной ценой (param-only, без лайфтаймов, локальная call-site-проверка). Формализация @/-> @ делает reference явной сущностью лоуэринга. Для одиночной мутации канон — mut @, НЕ mut ref (mut ref — узкий инструмент для мульти-mut / control+мутация, не «способ менять любой параметр»).
Что отвергнуто
refкак ТИП (ref T, ref-локалы/поля/возвраты, ref в коллекциях) — Q29 (остаётся отвергнут); вернуло бы лайфтаймы.- Rust/Swift-уровень exclusive-borrow soundness — Nova сознательно разрешает aliased-mut под GC (D157/D246-P10); берём лишь узкий анти-footgun.
- codegen-субсумция
[M-177-ifexpr-value-materialize-codegen]— снята: тот баг =infer_If/emit_if_exprdesync (R3-repair), закрыт отдельно (836befcb, 2026-06-26); ссылки его НЕ чинят. Его fixture (fluent-> @-хвост в if-цепочке) → лишь acceptance-гейт 172.5 (должна компилиться после ref-формализации).
Связь
D181/D184 (режим @), D246 (L3 / RETURN-оракул / P10 no-exclusivity), D131/D132/D133/D180 (consume / alias-гарантия), D228 (escape), D315 (ABI выводится), D156 (consume-bound), D157 (multi-mut sound под GC), Q29 (amend), Plan 172.4 (авто-ro ref/@/heap↔stack — реализует часть), Plan 174.5/174.6 (raw pointers / FFI). Новый код ошибки ровно один: E_REF_ALIAS_OVERLAP; остальное переиспользует existing (E_RECEIVER_BINDING_NOT_MUT, E_CONSUME_RECEIVER_RETURNS_AT, E_AT_RETURN_OUTSIDE_METHOD).
Амендменты по факту реализации (Plan 172.5 Ф.1-Ф.5, 2026-07-06)
mut ref= единственная user-facing реализованная форма.ro refсинтаксис принят (R2), но его zero-copy lowering НЕ дублируется — это size-driven авто-механизм Plan 172.4 (R3). Explicitro ref— семантическая аннотация, которая иначе передаётся как обычный value-параметр; call-site маркерrefнаro ref-параметре — ошибка (E_REF_MARKER_NOT_ALLOWED, R4: маркер сигнализирует ВОЗМОЖНУЮ мутацию, только дляmut ref).- Lowering
mut ref(Ф.4): параметр → C-указательT*(вparams_c, единый для forward-decl и definition); body-использования имени авто-разыменовываются (name→(*name), наборref_paramsв эмиттере); call-siteref x→&x(узелExprKind::RefArg). Форвардингref-параметра в другойmut ref-вызов (&(*v) ≡ v) работает. Скаляр + record проверены (pos-фикстуры зелёные). - AST:
ref— глобальное keyword (TokenKind::KwRef;refне используется идентификатором нигде в std/examples/tests → non-breaking).Param.ref_mode: ParamRefMode{None,RoRef,MutRef}; call-site —ExprKind::RefArg(place)(не тип-узел, не UnOp — производится парсером ТОЛЬКО в arg-позиции). - Коды ошибок (checker/parser). Новый headline-код —
E_REF_ALIAS_OVERLAP(R9, per-pair prefix-overlap; поддержаны литерал-дизъюнктные индексы, dynamic → консервативный overlap). Механические диагностики (не заявленные в исходном дизайне как existing, объявлены как новые по факту):E_REF_NOT_A_TYPE(R1,refв тип-позиции — parse),E_REF_MODE_REQUIRES_RO_OR_MUT(голыйrefбезro/mut— parse),E_REF_MARKER_REQUIRED(пропущенrefнаmut ref, R4),E_REF_MARKER_NOT_ALLOWED(refна не-mut ref, R4),E_REF_ARG_NOT_ADDRESSABLE(не-lvalue / index-в-цепочке, R4),E_REF_ARG_NOT_MUT(borrowro-места, R2),E_REF_ESCAPE_CAPTURE(захватmut ref-параметра closure/spawn, R10).E_CONSUME_RECEIVER_RETURNS_ATреюзнут на существующей parse-проверкеconsume @ -> @(R6). - R6 mid-chain gating (
E_RECEIVER_BINDING_NOT_MUT) — ОТЛОЖЕНО (followup[M-172.5-chain-gating-ro-at]). parse-часть R6 (consume @ -> @) сделана; но гейтинг «mut-метод наro -> @-хвосте» (c.peek().bump()) требует моделирования режима@-возврата сквозь method-chain — глубокое взаимодействие с fluent-машинерией 172.4, вне soundness in-outmut ref. Сейчас такой вызов компилируется (value-record-> @= копия по R7b, мутация копии безвредна); диагностика — отдельная задача. - Generic
fn f[T](mut ref x T)(R12) — codegen отложен ([M-172.5-generic- mut-ref-codegen]):params_c/ref_paramsпокрывают неген. путь (concrete); erased/mono-путиmut refне лоуэрят указатель. Checker-часть R12 (mode ортогонален type-param) не блокирует.
D327 — Codepoint = u32 (а не int): тип кодпоинта в std.unicode (Plan 172.2, 2026-06-26)
Source: Plan 172.2 (scalar narrowing через method-arg), 2026-06-26. Миграция std.unicode под narrowing-чек (D54) вскрыла int↔u32-импеданс: кодпоинты типизированы int, но хранятся Vec[u32] — каждый push был неявным сужением. Owner предложил хранить кодпоинт как u32; обсуждение выявило, что D226 «Signed indexing convention» к кодпоинтам неприменим.
Status: ✅ ADOPTED (sign-off владельца 2026-06-26).
Amends: nv-coding-style.md §числовые-ширины (новый пункт «Codepoint = u32»). Снимает аномалию is_alphabetic(cp int) (был int-кодпоинт по инерции signed-правила).
Cross-ref: D128 (char = nova_char = uint32_t), D226 (signed indexing — ОТДЕЛЬНАЯ категория), D54 (implicit narrowing — мотивация), D77 (fallible → Option).
Что
Unicode codepoint (scalar value, 0..0x10FFFF) — u32, НЕ int. Кодпоинт — character-data интринсик-ширины 32 бит, как UTF-16 code unit — u16 (nv-coding-style: «u32 — когда значение само по себе этой ширины»). Это категория значение-идентификатор, ОТЛИЧНАЯ от D226-категории index/len/offset/счётчик (мера, где underflow/-1-сентинел/mixed-arith мотивируют signed int). D226 к кодпоинтам не относится — кодпоинт не индексируют и не вычитают как длину.
Правило
- Хранилище последовательностей кодпоинтов —
Vec[u32](4 байта; ср. RustVec<char>, Go[]rune=[]int32). НЕVec[int](вдвое память/кэш на горячих путях коллации/нормализации — §2 perf). - Поток и арифметика внутри unicode-движков —
u32. Это убирает int↔u32-границу by-construction (нет неявного narrowing, нет россыпиas u32). char(=u32, D128) — семантический тип кодпоинта на границеstr:s.as_chars()→char,char.try_from(u32)→ валидный скаляр, char-методы ('a'.is_alphabetic()).- Публичные cp-функции принимают
u32(general_category(cp u32),is_alphabetic(cp u32)). Целочисленные литералы адаптируются к u32-контексту →general_category(0x41)остаётся валидным (контракт Plan 159 / plan152_3 сохранён). char-методы делегируют через@ as u32. - Fallible-функции, выдающие кодпоинт →
Option[u32](D77), НЕ-1-сентинел. (-1вint-функции — идиоматичен для find/мер D226, но кодпоинт — не мера.) - Bit-packing нескольких кодпоинтов в один ключ (
(a<<21)|b, > 32 бит) → ключint, явныйas intна упаковке (packed key — не кодпоинт).
Почему не оставить int + Vec[u32]-хранилище
Хранилище обязано быть 4-байтным (perf/идиома; 8-байтный Vec[int] кодпоинтов неидиоматичен). При int-потоке + u32-хранилище int↔u32-граница на каждом push неустранима — это и есть источник narrowing-боли 172.2. u32-поток её снимает.
Звучность / footgun
u32-арифметика cp - lo молча заворачивается при cp < lo (D226 §«нет underflow-trap» — аргумент за signed). В unicode эти вычитания (hangul cp - SBASE) уже под range-guard’ами (cp >= lo && cp <= hi), underflow недостижим, footgun локализован и под охраной. Приемлемо для домена.
D310. Type-set bounds (Plan 172.3)
Статус: дизайн закреплён 2026-06-28 (owner sign-off; Plan 172.3 Ф.0). Amends D72 + D145: «bound = только protocol» → «protocol ИЛИ type-set».
Что
Новая kind-форма объявления типа — type-set — задаёт именованное множество конкретных типов, используемое как generic-bound (fn[T IntSet] …). Это Go-style type-constraint: код, общий для семейства примитивов (int.parse/u32.parse/…), выражается одним generic вместо per-type обёрток. Частично закрывает Q-representation-bound — только explicit-member-set; ~underlying/repr/structural — по-прежнему Plan 102.
// inline
type SignedInt set i8 | i16 | i32 | i64 | int
type UnsignedInt set u8 | u16 | u32 | u64 | uint
// многострочный — | обязателен у каждого члена включая первый
type AnyNumber set
| i8 | i16 | i32 | i64 | int
| u8 | u16 | u32 | u64 | uint
fn[T UnsignedInt] T.parse(s str, radix int) -> Result[T, ParseUIntError] => ...
Амендмент (R3, 2026-07-07, я): примеры этого блока были
T.try_parse(...)(Option-контракт, до-R3 текст).try_parseбез infallible-сиблинга нарушает R3 (D325); я ретрактировал компиляторныйf64.try_parsebuiltin и заменил наf64.parse(s) -> Result[f64, ParseFloatError]([M-f64-try-parse-to-parse-f64]). Примеры выше переписаны наT.parse— future generalization (Plan 174.1) читай какT.parse, неT.try_parse.
Правило
- Синтаксис. Очередная kind-форма под
type(D52/D53/D406):type Name set Member1 | Member2 | …. Диспетчеризация по первому токену после имени — контекстный kind-токенsetоднозначно отличает type-set от sum-type (type X enum A | B, D406) и остальных форм. Backtracking нет (один токен lookahead).set— контекстное слово (только в позиции послеtype Name), НЕ глобально-зарезервированное. Члены — TypeRef через|. Многострочная форма: если первый член на новой строке —|обязателен у каждого члена включая первый (аналогично D406enumи D310set); несколько членов в одной строке допускаются. - Члены — по ИДЕНТИЧНОСТИ. Примитивы и любые объявленные конкретные типы (newtype / named-tuple / record), каждый перечислен ЯВНО. Newtype
type MyI8 i8не член set’а{i8}— нужен явный листинг.~underlyingНЕТ (в Nova нет implicit-coercion; D52/D215). - Bound = membership-предикат. В
[...]-позиции type-set ведёт себя как protocol-bound (D72):[T SignedInt]. Композиция с протоколами через+(D145, conjunction):[T SignedInt + Hash]⇒ T ∈ set И реализует Hash; проверки независимы, per-member. Не более одного type-set в bound-листе (E_MULTIPLE_TYPE_SETS); протоколов — сколько угодно. - Семантика тела. Мономорфизация per член (как обычный
fn[T], Plan 48 worklist).T.MAX/T.MIN/T.new/литералы резолвятся per-instance черезnumeric_type_constant_mappingпо Nova-имени подставленного члена (нужен Nova-name subst-канал T→“i8” ПЕРЕД lookup, отдельный от C-name subst T→“int8_t”). Операторы в теле — пересечение легальных для ВСЕХ членов; чекер материализует resolved-тип каждого T-выражения в per-ExprId канал (codegen лоуэрит, не ре-резолвит). Безnova_int-fallback (§1): неразрешённый член = диагностика чекера, не угадывание. - Знаковость. Один set НЕ смешивает signed/unsigned целые (
u64.MAX = 2^64−1 ∉ i64→ несовместимые value-domains; единое тело несоундно для обеих групп). Чекер:E_TYPE_SET_MIXED_SIGNEDNESSна объявлении. Stdlib даёт два готовых:SignedInt,UnsignedInt. Без рантайм-ветки поT.MIN==0(§2: не платим рантаймом за статически известное).
Проверки / диагностика (чекер, §1/§6; новые коды в 09-tooling)
E_TYPE_NOT_IN_SET— конкретный T не член set’а (фиксируется на инстанцировании, не на use-site внутри тела; сообщение перечисляет членов + fix).E_TYPE_SET_MEMBER_NOT_CONCRETE— член set’а не конкретный тип (protocol / effect / другой type-set).E_TYPE_SET_MIXED_SIGNEDNESS— set смешивает знаковые/беззнаковые целые.E_MULTIPLE_TYPE_SETS— >1 type-set в одном bound-листе.
Почему
- Reuse через семейства примитивов — один
fn[T SignedInt] T.parseвместо ×10 обёрток (разблокирует Plan 174.1, вариант B). - Zero-ambiguity синтаксис через существующий D52-диспетч (kind-токен, как
alias/protocolпод D53) — без нового top-level keyword, без backtracking, без конфликта с sum-|. - Звучность в чекере, лоуэринг в codegen (§0/§1): membership и легальность операторов — чекер;
T.MAX— лоуэринг подставленного имени, безnova_int-fallback. - Знаковость разрешена на уровне декларации (§2/§5), не рантайм-веткой.
Связь
D52 (формы type, first-token dispatch) · D53 (kind-токен под type) · D72 (bound = тип в […]-позиции, amended) · D145 (+ multi-bound conjunction, amended) · D237 (capitalized naming) · D315 (ResolvedType несёт ширину/знак). Q-representation-bound — частично (explicit-member-set); ~/repr → Plan 102. Потребитель: Plan 174.1.
D405 — Арифметика смешанных целых ширин: требовать явный cast (2026-06-30)
Статус: закреплён 2026-06-30 (owner sign-off).
Что
Выражение u8_val + u16_val (или любые два целых операнда разных ширин/знаковостей) — ошибка компиляции E_MIXED_WIDTH_ARITH. Компилятор не делает implicit widening. Программист обязан явно указать целевой тип через as-cast:
ro a u8 = 200
ro b u16 = 300
// ro result = a + b // E_MIXED_WIDTH_ARITH: u8 + u16 — implicit widening запрещён
ro result = a as u16 + b // ok: a явно расширен до u16
Правило
E_MIXED_WIDTH_ARITH— оба операнда бинарного арифметического оператора (+,-,*,/,%,>>,<<,&,|,^) должны иметь одинаковый конкретный целочисленный тип. Разные ширины (u8+u16) или разные знаки (i8+u8) — compile error.- Исключения:
int-литералы без явной ширины инференсируются к типу другого операнда (D55 literal coercion). Только когда OBA операнда именованные — error. - Widening явен — через
as-cast (D54). Выбор ширины — на программисте. Нет автоматического «бери больший».
Почему Rust/Go/Zig-стиль, не Java/Kotlin
| Язык | Поведение | Проблема |
|---|---|---|
| Java/Kotlin | byte + short → int (implicit, always signed) | Результат signed даже для unsigned операндов; теряется знаковость |
| C | u8 + u16 → uint (integer promotion, C-зависимо) | Платформо-зависимо; signed promotion удивляет |
| Rust/Go/Zig/Swift | compile error → явный cast | Никаких сюрпризов; программист контролирует |
| Nova (D405) | compile error → явный as cast | Согласуется с «no implicit conversions» (D54) |
Nova не имеет implicit numeric coercion нигде (D54 — as для всех width-changes). D405 последовательно расширяет это на бинарные операторы: нет молчаливого widen или truncate.
Связь
D54 (as явный cast) · D55 (literal coercion — исключение для нетипизированных литералов) · D315 (ResolvedType несёт ширину/знак — необходим для этой проверки) · D310 (SignedInt/UnsignedInt type-sets — generic-альтернатива per-width обёрткам).
D358 — HTTP message-model (std/http, Plan 178 Ф.1) {#d358}
Статус: ✅ landed (Ф.1, 2026-07-04) — message-model + URL + валидаторы. Http/HttpServer
effect-контракт (D357) и client/server-политики (D360/D361) — Ф.2+.
Pure value-типы поверх net byte-surface (Ф.0.5). Всё fallible → Result[T, HttpError]
(D325). Формы:
Method—| Get | Head | Post | Put | Delete | Connect | Options | Trace | Patch | Other(str);parseвалидирует RFC 7230 tchar; сравнение case-sensitive;@is_safe/@is_idempotent/@allows_body.StatusCode— value-newtype надu16(100..599);@class -> StatusClass;@reason(RFC 9110); zero-arg-фабрики (ok()/not_found()/…, Q17). 4xx/5xx = валидный Response, НЕ ошибка (Q4).Version—| Http10 | Http11 | Http2; OPEN (forward-compat Http3 — wildcard рекомендуется; языкового#open-атрибута нет, свойство конвенциональное).HeaderName/HeaderValue/HeaderMap— case-insensitive по имени, ordered, multi-value. Имя = ASCII tchar (lowercase-канон); значение =[]u8(latin1 fast-pathfrom_str, fallible@to_strна non-ASCII, Q18). Безопасность (by construction):insert/append/from_*ОТВЕРГАЮТ CR/LF/NUL → response-splitting невозможен;@content_lengthловит CL+TE-конфликт (request-smuggling, RFC 7230 §3.3.3).@insert= replace,@append= add (§13.1).Url(промоут_experimental/encoding/url.nv) —parse -> Result[Url, HttpError](былоfrom/Fail, D325 R2),@to_str. Строгий host/SSRF-валидатор: bracket-IPv6, canonical dotted-quad IPv4, REJECT control/NUL/whitespace/non-ASCII, REJECT decimal/octal/hex IP-обфускации (0x7f.1/0177.0.0.1/2130706433/127.1);@is_private_target(loopback/link-local/RFC1918/ metadata).encode_querypercent-encodit КАЖДЫЙ UTF-8-байт (был баг: один байт для >127);decode_query— self-contained UTF-8-валидация. Байт-корректный парсер (byte-offsets, срезы по ASCII-делимитерам). Детали ошибок —ParseUrlError(tuple-варианты) черезErrSource.UrlParse.Mime/ContentType—type/subtype(lowercase-канон) + параметры (charset/boundary).Cookie/SetCookie/SameSite— RFC 6265bis SEND-инварианты enforce’ятся наparse:Secure-cookie не поhttp://(@is_sendable);__Host-/__Secure--префиксы;SameSite=None ⇒ Secure.Request/Response— несут must-consumeBody(D359) → самиconsume; метаданные — borrow-методы; тело разряжается делегирующими consume-методами (resp.text()).
Амендменты по факту реализации (Ф.1)
SameSite.None→SameSite.Cross(wire-value «None» сохранён): вариантNoneв public-enum коллидирует сOption.Noneв namespace любого импортёра std.http → переименован.ErrSource.Url(ParseUrlError)→ErrSource.UrlParse(...): имя-вариант == имя-типUrlломает codegen (cast вместо wrap).ParseUrlError— tuple-варианты (InvalidScheme(str)…), НЕ record-варианты: auto-eq для record-вариантов внутриOption[sum]mis-lower’ит (_0на named-fields).HttpError— non-valuerecord:value+Option[Url]/Option[ErrSource]-поля → codegen emit’ит Option-typedef ПОСЛЕ struct-а (forward-ref «unknown type»).
Амендмент Ф.2 (auto-decompress landing, 2026-07-06)
ErrSource+Compress(CompressError)(OPEN enum → non-breaking): типизированный source для провалившегося decodeContent-Encoding(gzip/deflate). Разблокирован фиксом D381 (collision-aware module-qualified mangling) —compress.ErrorKindиhttp.ErrorKindтеперь СОСУЩЕСТВУЮТ в одном CU (доказаноnova_tests/http_decompress). Bomb (превышениеmax_decompressed, D334) НЕ несётся черезCompress, а мапится вHttpError{BodyTooLarge}(DoS-guard).br(brotli) закрыт — нет кодека[M-178-autodecompress-br]. Клиент шлётAccept-Encoding: gzip, deflateпо умолчанию (opt-out черезHttpClientBuilder.@no_decompress()); при декоде заголовокContent-Encodingснимается, аContent-Lengthпереписывается на декодированную длину (headers описывают тело, которое видит вызыватель).
D359 — must-consume Body (std/http, Plan 178 Ф.1) {#d359}
Body — линейный must-consume (D133): единственный способ «разрядить» — потребляющий метод
(@bytes/@text/@drain/@into_reader). Незакрытое тело = compile-error — чинит главный
Go-footgun (resp.Body-leak) на compile-time. Response/Request держат Body как consume-поле
(двойной D133-маркер), разряжают делегированием in-place.
Repr = InMemory([]u8) | Stream(BodyReader); BodyReader — чистый Nova-декодер над byte-source
(Q19), НЕ C-handle. @with_limit → BodyTooLarge (DoS-guard). @text = строгий UTF-8 (Ф.1).
Амендменты / гейты по факту (Ф.1)
Body—consume(НЕconsume value): value-копия оставила бы consume-поле владельца (Request/Response) неразряженным (@bodyкопировался, не move’ился).- Конструктор из СЫРЬЯ, не из pre-built
Body: запись с consume-полем строится ТОЛЬКО с полем-значением как СВЕЖИМ inline-выражением (Body.from_bytes(..)внутри конструктора); move consume-переменной/параметра в поле НЕ распознаётся checker’ом → конструкторы принимают[]u8/BodyReader.[M-178-consume-field-ctor-from-var] BodyReader— non-consumeв Ф.1 (in-memory, ресурса нет; transport-backed reader держит socket → станетconsumeв Ф.2).@next_chunk -> Result[[]u8]+@at_eof(план-формаResult[Option[[]u8]](None=EOF) упирается в codegen-ordering-баг eqOption[Option[[]u8]]→[M-178-bodyreader-option-eof-eq-ordering]).- ОТЛОЖЕНО в Ф.2 (гейты, НЕ упрощения):
Http-effect на потребляющих методах (park над транспортом)[M-178-body-http-effect-surface];@copy_to(fs-gate 176) /@json[T](serde-gate 180 Ф.4) /@trailers(Ф.2)[M-178-body-copy-json-trailers]; charset-aware@text(latin1-fallback по Content-Type)[M-178-body-text-charset]; typedexpires Timestampв SetCookie (date→epoch, Plan 175)[M-178-setcookie-expires-timestamp].
D340 — serde data-model + protocols (Plan 180)
Format-agnostic typed serialization. Protocols Serialize / Deserialize (contract on a type) + Serializer / Deserializer (backend), generic-bound notation [S Serializer] (D72/D119, NOT impl Trait — Q12). Lean 12-case data-model (bool int uint float str bytes option unit seq map struct enum, Q2; int=i64-wide widen/range-check).
Serialize= push:@serialize[S Serializer](mut s S) -> Result[(), SerError].Deserialize= static pull:.deserialize[D Deserializer](mut d D) -> Result[Self, DeError](D35 static).- Serializer = single mutable stack-machine (realized form): composites framed by
begin_struct/struct_field/end_struct,begin_seq/end_seq,begin_map/map_key/end_map; scalars terminalserialize_X. The backend owns an internal frame stack (format-agnostic). This REPLACES the aspirational “consume sub-serializer returned per composite” form: in value-semantics Nova a sub-serializer mutating shared parent state has no clean ownership; the stack machine is the sound realization (the synthesizer always emits matched begin/end pairs — same balance guarantee). - Deserializer = single cursor:
enter_field/enter_field_or_null/enter_index/enter_keyreturn a sub-cursor (Self) positioned at the child; sub-deserializers only READ, so no write-back/ownership problem (keyed-access model = SwiftKeyedDecodingContainer). NoVisitorcompanion TYPE is synthesized — the keyed/indexed model makes it unnecessary. SerError/DeError=value-record (D215/D322 pattern), OPEN kind +path(D325 R5). Kinds use DISJOINT variant names (SerErrorKind:NonFiniteFloat/SerDepthLimit/SerCustom/SerOther;DeErrorKind:UnexpectedType/MissingField/UnknownField/OutOfRange/LossyInteger/DepthLimitExceeded/Syntax/Custom/Other) — Nova bare-variant construction does not disambiguate a shared variant name by expected-arg type, so overlaps are avoided by construction.- PURE (no effect) — codec over values/bytes.
D341 — record auto-derive contract (compiler synthesis)
#impl(Serialize + Deserialize) opt-in — the 7th/8th members of the auto-derive family (Equal/Hash/Clone/Compare/Display/Debug); is_builtin_protocol extended. SUM is supported (Plan 180 Ф.2-sum, externally-tagged — see D345); the record-path shapes below apply to record/named-tuple types. Emitted shapes:
@serialize:s.begin_struct(name, N)?; per fields.struct_field("k")?; @field.serialize(s)?;s.end_struct()— UNIFORM memberwise push (like@debug)..deserialize: per fieldmut sub = d.enter_field[_or_null]("k")?then TYPE-DIRECTED read — scalar →sub.deser_X()?(instance); record/Vec/HashMap→<T>.deserialize(sub)?(static);Option[T]→ inlineif sub.is_null()? { None } else { Some(<inner>) }(built-inOptiondoes not dispatch a user static method). ThenOk(Type{ f1, f2, … }).- Field-eligibility: primitive /
Option·Vec·HashMap[str,_](recurse) /#impl(P)/ provides-method — elseE_AUTO_DERIVE_FIELD_LACKS_PROTOCOL(named field; no silent drop).HashMapkey must bestr(Q16). priv fields serialize (structural synth). User method wins (D77). - Injection ordering: serde synth is injected BEFORE type-check (its bodies call other methods whose return types codegen’s annotation-free
infer_expr_c_typecannot always resolve; type-checking annotates them). Non-serde protocols inject AFTER check as before (some bodies, e.g.@display’sw.write_str, are intentionally not type-checkable). Bound satisfaction: a#impl(P)type satisfies[T P]for a built-in auto-derivable P even before the method is materialized.
D342 — data-model ↔ synthesis mapping (Plan 180)
record→struct, Vec→seq, HashMap[str,_]→map, Option→option (None→null default; absent accepted on deser, Q7), scalars→prim (int i64-wide widen / exact-integer range-check, Q2/Q15), []u8→bytes. numeric-fidelity (Q15): on deser, int/uint require an f64 that is an exact integer in [-2^53, 2^53] — else LossyInteger; negative→uint → OutOfRange. Option[Option[T]] ambiguity documented (Some(None)==None on wire). []u8→base64 and Timestamp/Duration mappings are followups ([M-180-bytes-base64], Plan 175 coordination).
D344 — JSON backend over std/encoding/json (Plan 180)
JsonSerializer (stack-machine building JsonValue → @into()) / JsonDeserializer (cursor over JsonValue) layered on the existing JsonValue/Json.parse (Q11, reuse — not a new parser). Public API = free functions json_encode[T] / json_decode[T] / json_encode_pretty / json_to_value / json_from_value / json_decode_bytes / json_decode_with (NOT Json.encode namespace-static: turbofish on a namespace/type-static generic method does not monomorphize — Ф.0-verify empirics; free-fn turbofish does. Followup [M-180-namespace-static-generic-mono]). depth-guard (Q14 default 128 → DepthLimitExceeded). ParseJsonError → DeError{Syntax(msg)} with line/col preserved in the message. Map encode sorts keys (determinism). Contract for Plan 178 record-DTO (json_decode[T] / json_encode).
D345 — sum auto-derive + tagging (Plan 180)
Ф.1 — sum rich data-protocol synth (✅ landed 2026-07-06). The six built-in
protocols (Equal/Hash/Clone/Compare/Display/Debug) now synthesize
match @ { … } with one arm per variant instead of the old placeholders
(equal=identity, hash=0, clone=self, compare=0, display/debug=typename). Per
SumVariantKind: Unit (no payload / bare-ctor reconstruction), Tuple(tys)
(positional binds, V(a0,a1)), Record(fields) (named binds, V { f }).
@equal:V(a..) => match other { V(b..) => a==b && …, _ => false }— same variant + payload-wise==; different variant → false.@hash: variant-index seed (idx+1) combined with each payload’s.hash()via the record-path rotate-XOR; distinct unit variants hash apart.@clone: match-arm reconstruction — primitives shallow-copied, composites.clone()d;Unit→bare ctor,Tuple→V(clone…),Record→V { f: clone }.@compare: extract both variant indices, compare those first; on tie compare payloads lexicographically (ro c = a.compare(b); if c != 0 { return c }).@display/@debug:"V"/"V(x, y)"/"V { f: x, g: y }"; display routes primitives viaw.write_str(str.from(x)), debug uniformlyx.debug(w). These inject AFTER type-check (like the record-path), so the emittedmatch/ variant-patterns / variant-construction are lowered by codegen’s annotation-free inference (scrutinee@+other: Selftypes known). [M-126-sum-*-rich] CLOSED.
Ф.2-sum — serde sum-derive, externally-tagged (✅ landed 2026-07-06).
#impl(Serialize + Deserialize) on a sum synthesizes match-arm-per-variant
bodies over the Ф.1 pattern/ctor infra. Externally-tagged (Q4, default):
- unit variant
V→ bare string"V" - single-payload
V(x)→{"V": <x>} - multi-tuple
V(a, b)→{"V": [<a>, <b>]}(inner array) - record variant
V{f, g}→{"V": {"f": <f>, "g": <g>}}(inner struct)
Serialize emits over the existing Serializer primitives (begin_struct/
struct_field/begin_seq/serialize_str/…) — no new enum-specific serializer
methods. Deserialize reads the tag (d.is_str()? → bare string for unit; else
the single object key via map_keys/enter_key), then an if/else-if chain on
the tag name reconstructs the variant, reading payload from the tagged cursor
(tuple → enter_index, record → enter_field, single → direct). Unknown tag →
DeError{UnknownVariant{name, expected}} (new DeErrorKind variant); malformed
(non-single-key object) → DeError{Syntax}. Payload eligibility mirrors the
record-field check (typed E_AUTO_DERIVE_FIELD_LACKS_PROTOCOL by variant, never
a bad synth). Runtime additions: Deserializer.@is_str() + DeErrorKind
UnknownVariant/NoVariantMatched. NOT on Plan 178’s critical path (record-DTO
suffices). Codegen: a static T.deserialize(sub)? whose return-type inference
degrades (mono-collection order perturbation once a sum ALSO derives Deserialize)
is pinned to Result[T, DeError] at the ?-lowering site (emit_c.rs Try arm),
mirroring the .serialize? pin.
Ф.5 — internal/adjacent tagging (✅ landed 2026-07-06, D382); untagged gated.
Now that the #serde(...) declaration-attribute infra exists (D382), the
non-external tagging modes are synthesized from the type-level attributes:
#serde(tag="k")→ internally-tagged ✅: unitV→{"k":"V"}; recordV{f}→{"k":"V","f":…}(fields inlined beside the tag). Tuple/positional payloads are rejected —E_SERDE_INTERNAL_TAG_NON_STRUCT(no object to inline the discriminator into; serde rule).#serde(tag="t", content="c")→ adjacently-tagged ✅: unit→{"t":"V"}; single→{"t":"V","c":x}; tuple→{"t":"V","c":[…]}; record→{"t":"V","c":{…}}.#serde(untagged)→ untagged 🔴 GATED ([M-180-untagged-codegen-mono]): synthesized correctly (unit→null; single→x; tuple→[…]; record→{…}; deserialize buffers theJsonValue, Q17, and tries each variant in declaration order — value-semantics cursor makes each attempt a non-destructive retry;DeError{NoVariantMatched}if all fail). BUT compiling an untagged-derive body perturbsstd/encoding/jsoncodegen in the same CU (mono-collection ordering →Json.parsemis-tags a number as a bool), so#serde(untagged)is rejected at compile time (E_SERDE_UNTAGGED_GATED) until that codegen-hardening prerequisite lands. A compiler bug, NOT a serde-logic defect. Serialize/deserialize are synthesized over the SAMESerializer/Deserializerprimitives as external (begin_struct/struct_field/enter_field/enter_index/is_null/…) — no new backend methods. The mode is computed from the type’sserde_attrsbyserde_tagging_mode(auto_derive.rs) with static validation (conflict / content-without-tag / on-non-sum / internal-on-tuple / untagged-gate). Externally-tagged (no attribute) is unchanged. [M-180-serde-tagging-modes] CLOSED for internal+adjacent; untagged → [M-180-untagged-codegen-mono].
Codegen soundness note (Plan 180 Ф.6). The synthesized deserialize bodies
exercise json.nv’s Deserializer methods heavily; their match m.get(k) { None => Err(..), Some(v) => Ok(..) } shape exposed a latent match/if result-type bug
— Ok(x) alone infers a stub ERR side (NovaRes_<ok>_nova_str) and Err(e) a
stub OK side (NovaRes_nova_int_<err>), so neither arm yields the full
Result[JsonDeserializer, DeError] and the returned cursor was mis-laid-out
(decode returned spurious UnexpectedType). Fixed by reconciling the concrete OK
(from an Ok(..) arm) with the concrete ERR (from an Err(..) arm) across
emit_match/emit_if_expr + their infer_expr_c_type mirrors — splitting the
already-computed arm/branch Result-types via novares_ok_err (side-effect-free;
an earlier re-inference variant perturbed mono-collection order). Order-
independent; genuine Result[int, E] matches unchanged. Zero-regression verified
(~50 dirs, byte-identical to parent). This fixed internal+adjacent; untagged
needs a further, distinct mono-ordering fix (the json.nv corruption above).
D346 — serde soundness invariants (Plan 180)
Q14 depth-guard (both sides, default 128); Q15 exact-integer-check (no silent-lossy); Q16 str-only map keys; Q18 dup-field reconciled with Json.parse’s strict DuplicateKey.
D382 — declaration attributes #serde(...) (Plan 180 Ф.6) {#d382}
Grammar. A declaration attribute is #name(arg (, arg)*), arg := ident [ = StrLit ] — i.e. bare flags (#serde(untagged)), string-valued keys
(#serde(tag="type")), and comma-separated combinations
(#serde(tag="t", content="c")). Multiple #serde(...) annotations on one
declaration accumulate. The only recognized namespace in V1 is #serde. Parsed
by parse_serde_attr (shared across the three positions), extending the
#visible_to/#impl marker-parsing precedent.
AST. A new serde_attrs: Vec<SerdeArg> field on TypeDecl,
SumVariant, and RecordField (empty Vec = default, backward-compat).
SerdeArg = Tag(String) | Content(String) | Untagged — a structured
list, general enough that field-customization keys (rename/skip/…) drop in
without a grammar change. Type-level #serde is threaded through
parse_type_attrs → parse_type_decl; field-level alongside #visible_to in
the record-field loop; variant-level as a leading marker in
parse_one_sum_variant.
Recognized keys (V1). tag, content, untagged — sum-type enum tagging
(consumed by D345 Ф.5 via serde_tagging_mode; tag/content land, untagged
is parsed+validated but its derive is gated E_SERDE_UNTAGGED_GATED, see D345).
Unknown-attribute policy
(convention, mirrors #impl/#from_fields: unknown marker → hard error, never
silent): any other key inside #serde(...) → E_SERDE_BAD_ATTRIBUTE at
parse time (beats Go/Jackson silent tag-typo). Field-customization attributes
(rename/rename_all/skip/default/flatten/alias/
deny_unknown_fields) are parsed by the SAME grammar but their synthesis-
consumption is a scoped follow-up ([M-180-serde-field-attributes]) — they
currently reject as not-yet-supported rather than silently ignore.
Static validation (serde_tagging_mode, surfaced as compile errors):
E_SERDE_TAGGING_CONFLICT (untagged with tag/content; or tag==content);
E_SERDE_CONTENT_WITHOUT_TAG (content without tag);
E_SERDE_TAGGING_ON_NON_SUM (tagging attr on a record/non-sum);
E_SERDE_INTERNAL_TAG_NON_STRUCT (internal tag on a type with a tuple
variant); E_SERDE_UNTAGGED_GATED (untagged derive gated on a codegen-mono fix,
[M-180-untagged-codegen-mono]). See D345 Ф.5 for the emitted wire per mode.