Нормативный текст спецификации. Английская страница — informative translation.

Nova — конверсии типов

Сводная страница всех правил конверсии в одном месте. Полные D-decisions: D54 (as), D52 (newtype/alias/sum), D325 (единый fallible-контракт std), D410 (to_str/bytes-семейство), D429 (#coerce — zero-cost implicit), D430 (checked narrowing to_*). From/Into/TryFrom/TryInto как протоколы ретрактированы 2026-07-06 (D73/D77) — подробности в разделе «Именование from/try_from» ниже.


Три механизма

МеханизмКогдаПример
asinfallible numeric/newtype/sum cast, compile-time, без runtime-кода42 as f64, n as i16
.to_str()универсальная конверсия значения в строку (bare-T blanket + специализации)42.to_str(), bs.to_str()
T.from(v) / T.try_from(v)конкретный статик-конструктор — имя-конвенция, НЕ протокол/auto-derive. Законен ТОЛЬКО когда источник — концепт, а не значение-носитель: у значения канон — метод на источнике x.to_*() (§1а nv-coding-style, 2026-07-09; линт W_STATIC_CONVERSION)Complex.from_polar(r, phi)
consume @into_ЦЕЛЬ()потребляющая передача владения (конкретное имя на источнике)sb.into_str(), wb.into_bytes()
#coerceдекларативная неявная zero-cost конверсия в позиции с известным типом (view/finalize)w.write(s) — str неявно .bytes()

Неявная коэрсия — свойство ПОЗИЦИИ, а не значения (D429 R9/R10, владелец 2026-09-10). В позиции с известным ожидаемым типом — типизированное связывание, аргумент, возврат — канон голое значение, пара вставляется компилятором. Внутри выражения ожидаемого типа нет, и та же операция всегда записывается своим именем: s.bytes().len(), sb.into_str().trim(). as никогда не вызывает #coerce-пару (R10): as — закрытая таблица кастов из спеки, #coerce — пользовательский код. Третьей записи нет намеренно.

Важно (2026-07-06 ретракция, см. ниже): .from(v) / .try_from(v) — это ПАРА конкретных статик-методов на конкретном типе, не generic-протокол From[T]/TryFrom[T,E]. Компилятор не синтезирует обратную форму (.into()/.try_into()) автоматически — программист пишет ровно то, что объявил. «Универсального» .into() в языке больше нет.


Numeric ↔ numeric

Значение не меняет свой числовой тип само (D491, D405)

Каждое расширение ниже происходит потому, что вы написали as. Nova не выбирает за вас более широкий тип, когда два числовых типа встречаются в одной операции, и значение с типом не расширяется в позицию другого типа (биндинг, аргумент, возврат, поле, рукав match) — D491, владелец 2026-10-02 — никакого «бери больший», никакой промоции целого в float, никакого сравнения знакового с беззнаковым. Смешанные операнды — ошибка компиляции, а задуманная конверсия записана явно.

Это то правило, бояться которого C учил всех десятилетиями: u32(4294967295) == i32(-1) в C равно true, потому что знаковый операнд за вашей спиной приведён к беззнаковому. В Nova это не компилируется.

Амендмент 2026-09-04 формулирует правило категорией, а не списком операторов — после того, как были измерены три двери вне исходного списка: операнды сравнения разной знаковости, целое против float и отрицание беззнакового. Все три отвечали, и отвечали неверно.

Два послабления, оба — только про литералы, у которых нет второго типа, чтобы спорить:

  • float-литерал принимается в float-позиции без as (ro x f32 = 1.5);
  • литерал без типа адаптируется к float-операнду рядом (b + 1 при b f64) — как нетипизированная константа в Go.

Общее правило за обоими (D489): литерал берёт тип позиции, если его точное значение в нём представимо, а род литерала задаёт форма. Целый идёт в любой целый тип в диапазоне и во float, если точен (16777217 в f32 — ошибка E_LIT_INEXACT); дробный — только во float, и 2.0 в int — ошибка; символьный — только в char, в число — через as. Правило действует и в образце match, и во втором операнде (x == 300 при x u8 — ошибка).

Значение типа int рядом со значением типа f64 — по-прежнему ошибка, литерал или нет.

Принадлежность к одной категории — не разрешение смешивать внутри неё. f32 и f64 — оба float; f32 < f64 всё равно ошибка. i32 и i64 — оба знаковые целые; i64 < i32 всё равно ошибка. Категория решает, какие операторы существуют для типа, а не какие пары типов могут встретиться. Каждое из этого отвергается, а задуманная конверсия пишется через as:

u32 == i32     // ошибка — разная знаковость
i64 < i32      // ошибка — разная ширина
f32 < f64      // ошибка — разная ширина, оба float
int < f64      // ошибка — целое против float

Унарный минус определён только для знаковых типов. -x при беззнаковом x — ошибка компиляции, а не заворот по модулю (-200u8 не станет 56); задуманное пишется -(x as i16).

Widening (no precision loss)

From → ToЧерезСемантика
i8 → i16/i32/i64/intassign-extend
u8 → u16/u32/u64/intaszero-extend
i8/u8 → f64asexact (любое значение i8/u8 представимо)
f32 → f64asexact

Narrowing (potential precision loss)

From → ToЧерезСемантика
i64 → i32/i16/i8aswraparound (modulo 2^N)
u64 → u32/u16/u8/byteaswraparound
iN → uM, включая int → uintasbit-pattern ((-1 as int) as uint == 2^64−1; частный случай насыщения D130 Q2 снят 2026-09-04 — одна таблица на всё семейство)
int ↔ i64, uint ↔ u64asno-op на 64-битном bootstrap, но разные типы (D129/D130): каст обязателен

Все строки целое → целое выше — одна операция: взять младшие M бит источника и прочитать их как целевой тип. «wraparound» и «bit-pattern» — одно и то же с двух сторон, поэтому u64 as u8 и i32 as u16 не требуют & 0xFF / & 0xFFFF руками — as и есть эта маска (0x1234 as u8 == 52, 70000 as u16 == 4464). Унарный ~ сохраняет тип операнда (~5u8 == 250, ~5i32 == -6) и определён только для целых и целочисленных newtype (амендмент D46 2026-07-27). | f64 → f32 | as | IEEE rounding (потеря точности) | | f64/f32 → iN/uN | as | saturation + NaN→0 + ±∞→bounds |

Float→int saturation — defined behavior на любом входе (отличие от C/C++ UB). Согласовано с Rust 1.45+.

ro n = 1e20 as int             // saturates to INT64_MAX
ro m = (-1.0) as u32           // saturates to 0
ro nan = (0.0 / 0.0) as i16    // 0

Checked narrowing — to_* (D430, 2026-07-20)

as между целочисленными ширинами всегда wraparound (тихая потеря старших бит). Если нужна проверка, а не тихий wrap — bounded-бланкет @to_<T>() на любом типе из Ints-набора, симметрично для всех целевых ширин (i8/i16/i32/i64/int/u8/u16/u32/u64/uint):

ro ok = (100 as u32).to_u8()       // Ok(100 as u8)
ro err = (300 as u32).to_u8()      // Err(AboveMax) — не влезло
ro neg = (-1 as i32).to_u8()       // Err(BelowMin) — отрицательное → unsigned

ИЗ FLOAT — ТОЖЕ, С ТОГО ЖЕ ИМЕНИ (D430 амендмент R6, 2026-09-04). Вторая бланкет-семья fn[S Floats] S @to_<T>() покрывает f32/f64. Семантика та же, что у целых, и это главное в амендменте: вопрос — «влезло ли», а не «без потерь ли». Дробная часть усекается к нулю, затем проверяется диапазон цели; NaN и ±∞ — Err.

ro ok  = (2.5).to_i16()            // Ok(2) — усечено к нулю, влезло
ro neg = (-2.5).to_i16()           // Ok(-2) — к нулю, не вниз
ro big = (70000.5).to_i16()        // Err(AboveMax) — не влезло
ro nan = (0.0 / 0.0).to_i16()      // Err(BelowMin)

Одно имя — одно значение по всей семье: метод, чьё обещание меняется вместе с видом приёмника, читается как один, а ведёт себя как два. Доноры: Swift Int16(f), C# checked.

RangeError — unit-тип («не влезло», без payload — сам факт исчерпывающий). as остаётся быстрым обрезающим кастом без изменений — to_* не заменяет его, а добавляет проверяемую альтернативу рядом.


Numeric ↔ str

str → numeric (parse, fallible) — метод НА ИСТОЧНИКЕ, не статик на цели

Канон (Plan 174.1, 2026-07-08, owner decision — superseded ранний static-constructor дизайн T.parse(s)/T.try_from(s)): конверсия строки в число — это метод на str (s.to_int()), а не статик-конструктор на целевом типе. Зеркалит s.to_str()-семью в обратную сторону.

From → ToЧерезFailure
str → ints.to_int(radix: int = 10)non-digit / overflow / (custom radix) invalid radix
str → i64/u64s.to_i64() / s.to_u64()без доп. range-check (та же ширина, что движок)
str → i8/i16/i32/u8/u16/u32s.to_i8() / s.to_i16() / s.to_i32() / s.to_u8() / s.to_u16() / s.to_u32()+ range-check в целевую ширину
str → f64s.to_f64()invalid number format
fn parse_decimal(s str) -> Result[int, ParseIntError] =>
    Ok(s.to_int()?)             // radix 10 по умолчанию, Ok(42)

fn parse_hex(s str) -> Result[u32, ParseIntError] =>
    Ok(s.to_u32(radix: 16)?)    // hex-парсинг

fn parse_decimal_f64(s str) -> Result[f64, ParseFloatError] =>
    Ok(s.to_f64()?)             // Ok(3.14)

Ошибки — структурные enum’ы: type ParseIntError enum Empty | InvalidDigit | AboveMax | BelowMin | InvalidRadix (2026-09-05: Overflow разделён по направлению) и type ParseFloatError enum Empty | Malformed { at int } | TooLarge | TooSmall (временные имена — реестр №136; целевой словарь Invalid/AboveMax/BelowMin)

Грамматика и конверсия float — обе наши (план 282 Ф.4, 2026-09-05; план 283 Ф.4, 2026-09-07). s.to_f64() принимает ровно [+-]?(digits[.digits?]|.digits)([eE][+-]?digits)? — без пробелов, nan/inf, hex, _ и локального разделителя; первый плохой байт — в Malformed { at }; конечный литерал вне f64 — TooLarge/TooSmall, не молчаливый inf. Правильное округление десятичной записи в двоичную — тоже на Nova: порт Rust core::num::dec2flt (быстрый путь Клинжера, Eisel-Lemire и медленный путь на большом десятичном числе для входов, которые не решает ни один из двух); C в to_f64() больше не участвует, strtod остаётся только эталоном дифференциальных тестов.

И ОБРАТНАЯ СТОРОНА ТОЖЕ НАША (план 285, D475, 2026-09-13). Кратчайшая round-trip-запись f64 и f32 производится кодом на Nova (порт Ryu), а точная десятичная арифметика живёт в runtime.decimal и обслуживает ОБЕ стороны: предел у них один и тот же — binary64 требует до 767 десятичных знаков в любую сторону. snprintf остаётся у Fixed/Sci до завершения Ф.4, а print(x) для float до закрытия №1072 идёт ВТОРОЙ дверью, мимо этого кода.

Дверь str → f32 тоже есть, и округляет она за ОДИН шаг (план 283 Ф.7, 2026-09-07). s.to_f32() берёт ту же грамматику и тот же ParseFloatError, что to_f64(), и переводит десятичные цифры прямо в f32. Это НЕ to_f64() с последующим приведением: если десятичное число лежит выше середины между двумя соседними f32 меньше, чем на разрешение f64, то при округлении в f64 добавка теряется, значение садится ровно на середину, и округление «к чётному» уходит в сторону, противоположную верной (замер: пять таких строк из шести дают другой бит). Снятый f32.try_parse делал именно так.

Сравнение результата. До починки строки реестра №136 результат с общим именем варианта ошибки (AboveMax/BelowMin есть у RangeError, CharError и ParseIntError) сравнивать через match или квалифицированно (r == Err(ParseIntError.AboveMax)); голое r == Err(AboveMax) может молча взять вариант другого типа. (std/runtime/string/parse.nv).

str → bool (parse, fallible)

Канон (Plan 232.1 Т1, owner decision «добавить», 2026-07-26): s.to_bool() — строго "true"/"false", lowercase-only (Rust str::parse::<bool>-канон; нет case-insensitive/"1"/"0"/"yes"-алиасов).

From → ToЧерезFailure
str → bools.to_bool()пусто → Err(Empty); что угодно кроме точно "true"/"false" → Err(Invalid)
fn parse_flag(s str) -> Result[bool, ParseBoolError] => s.to_bool()

assert("true".to_bool() == Ok(true))
assert("TRUE".to_bool().is_err())      // регистр не lowercase → Err(Invalid)

type ParseBoolError enum Empty | Invalid (std/runtime/string/parse.nv) — тот же двухвариантный паттерн, что ParseFloatError.

numeric → str (format, infallible) — единый вход .to_str()

Канон (Plan 174.2, 2026-07-14): str.from(scalar) ретрактирован. Единственный публичный вход «значение → строка» — bare-T blanket fn[T] T @to_str() -> str => "${@}" (D410 amend), специализируемый конкретными перегрузками там, где нужна другая arity/семантика (например decode для []u8, см. ниже).

From → ToЧерез
int/iN/uN → strn.to_str()
f64/f32 → strf.to_str()
bool → strb.to_str()
char → strc.to_str()
ro s = 42.to_str()             // "42"
ro f = 3.14.to_str()           // "3.14"

Интерполяция ("${n}") лоуэрится в тот же путь напрямую (для примитивов — в Display-хелпер C-уровня, без повторного вызова .to_str() — рекурсии нет).


Char / Byte / []byte / str

char → str (UTF-8 encode)

ЧерезСемантика
c.to_str()infallible UTF-8 encode (1-4 байта) — специализация to_str()-blanket’а, byte-identical бывшему str.from(char)

str → char (single codepoint, fallible)

Канон (Plan 232.1 Т1, owner decision «добавить», 2026-07-26): s.to_char() парсит РОВНО один Unicode codepoint (не байт — "é".to_char() успешен, хотя é — 2 UTF-8 байта). Ресивер-форма на источнике, тот же принцип, что str @to_int().

ЧерезFailure
s.to_char() -> Result[char, ParseCharError]пусто → Err(Empty); >1 codepoint → Err(TooManyChars)
assert("a".to_char() == Ok('a'))
assert("ab".to_char() == Err(TooManyChars))    // строгий отказ, не first-char silently

type ParseCharError enum Empty | TooManyChars (std/runtime/string/parse.nv) — НЕ переиспользует CharError (см. раздел «int → char» ниже): тот домен — codepoint вне диапазона Unicode scalar value/surrogate, недостижим для str→char (байты str уже валидный UTF-8, R-UTF8).

int → char (codepoint range-check, fallible)

Канон (владелец, 2026-07-09): ресивер-форма на источнике ((cp int).to_char()), не статик char.try_from(n) — тот же принцип цепочечности, что у str @to_int(): (32 + off).to_char()?.

ЧерезFailure
(cp int).to_char() -> Result[char, CharError]BelowMin — cp < 0 · AboveMax — cp > 0x10FFFF · Invalid — зарезервированный блок [0xD800, 0xDFFF] (2026-09-05: одна дверь, одна ошибка; CharError/InvalidCodepoint/str.try_from_codepoint сняты)
fn describe(cp int) -> str =>
    match cp.to_char() {
        Ok(c)              => "codepoint ${cp} = '${c}'"
        Err(e) => "${cp} is not a char: ${e}"
    }

char → byte (only if codepoint < 256, fallible)

Эта пара осталась статик-формой (не мигрировала на ресивер) — единственный случай, где try_ остался на целевом типе:

ЧерезFailure
c.to_u8() -> Result[u8, RangeError]codepoint > 0xFF (не Latin-1) — форма на источнике с 2026-09-05; u8.try_from(c char) снят (амендмент D54: конверсия между конкретными типами — метод на источнике; статика на цели — только для конструкторов Self в протоколах и try_from рядом с from)

Исключение: 'A' as byte, 'A' as int, 'A' as u8 — разрешены для char-литералов (compile-time-known codepoint), см. D54.

[]byte ↔ str — единая to_str-семья (D325/174.1)

Канон: []u8-decode тоже идёт через to_str() — конкретная перегрузка (arity/семантика decode, не format) побеждает bare-T blanket по правилу «конкретное побеждает generic» (D84). str.try_from([]u8) / отдельный str.from_bytes(...) — исторические имена, отозваны, актуальны только формы ниже:

ФормаТипСемантика
bs.to_str()-> Result[str, Utf8Error]checked decode; Utf8Error{byte_offset} указывает первый невалидный байт
bs.to_str_lossy()-> strinfallible, невалидные последовательности заменяются replacement-символом
unsafe { bs.to_str_unchecked() }-> strбез проверки, вызывающий гарантирует валидный UTF-8
unsafe { bs.consume.into_str_unchecked() }-> strкак выше, но потребляющий zero-copy move буфера
fn decode(bytes []u8) -> str =>
    match bytes.to_str() {
        Ok(s)                        => s
        Err(Utf8Error{byte_offset})  => "invalid UTF-8 at ${byte_offset}"
    }

str → []byte (view, infallible, zero-copy) — голый вид, не трансформация: s.bytes() -> ro []u8 (D410 — as_bytes переименован в bytes; это же имя — первая объявленная #coerce-пара, см. раздел «Zero-cost неявные конверсии» ниже).


Bool ↔ всё

From → ToЧерезСемантика
bool → intastrue=1, false=0
bool → byte / bool → f64asто же
bool → strb.to_str()"true" / "false"
int/byte/f64/etc → boolзапрещеноuse n != 0
ro s = true.to_str()           // "true"
ro n = 5
ro ok = if n != 0 { true } else { false }   // explicit != 0, не truthy-int

str → bool — см. TODO выше (не найдено в std на момент этой ревизии).


Newtype ↔ underlying

Newtype (type X Y, без alias, D52) — отдельный от источника тип; конверсия — явный as (identity, тот же C-repr). Это отличается от alias (type X alias Y) — там X и Y взаимозаменяемы без всякого cast’а (не отдельный тип).

ЧерезСемантика
n as MyNewtypeidentity (одинаковое C-представление)
nt as intidentity
type UserId int
ro u UserId = 42 as UserId
ro n int = u as int            // 42

Неявная половина и её граница (D55 amend 2026-08-21). В позиции с явным ожидаемым типом newtype оборачивается сам — но ТОЛЬКО для нетипизированной константы. Типизированная переменная требует явной формы; граница та же, что в Go, на который ссылается D52.

type Row int
ro a Row = 100                 // ok -- a constant
ro b Row = 40 + 60             // ok -- constant arithmetic
ro n = 100
ro c Row = n                   // ERROR E7301 -- a typed variable
ro d Row = Row(n)              // ok
ro e Row = n as Row            // ok
ro g Row = Row(d)              // ERROR E_NEWTYPE_CTOR_SELF -- d is already Row

Суммы правило не касается: вариант ТОЧНОГО типа значения (SqlValue.Int(x) для int, I32(x) для i32; литерал — по умолчанию D44, реестр 221.1 №1637) подставляется для ЛЮБОГО выражения, которое принимает чекер, — переменной, вызова, чтения поля, не только литерала (D55, уточнение 2026-10-01): там компилятор ВЫВОДИТ единственный подходящий вариант, а не придумывает утверждение автора. Нужна прежняя мягкость для своего newtype — объявляется парой #coerce.

Конструктор варианта судится по ожидаемому инстансу (D55, амендмент 2026-10-01). Payload сверяется с полем варианта того ИНСТАНСА, который ждёт позиция, число значений — с объявлением, а конструктор вовсе без источника типа отвергается, а не получает тип по умолчанию:

fn bad() -> Option[str] => Some(1)   // ERROR E7301 -- Option[int] is not Option[str]
ro p = Some(1, 2)                    // ERROR E_VARIANT_CTOR_ARITY
ro q = None                          // ERROR E_VARIANT_CTOR_UNTYPED -- annotate it
ro r Option[str] = None              // ok
ro s = Some(1)                       // ok -- Option[int], from the payload

Операторы над newtype не выходят за его пределы (D52, амендмент 2026-09-04). Арифметика и сравнение определены между двумя значениями одного newtype (плюс литерал, который адаптируется); результат арифметики — тот же newtype, сравнения — bool. Два разных newtype над одним представлением или newtype против значения представления в операторе не встречаются — конверсия пишется через as. Это правило Go для type FnRow int, и именно оно делает типизированные индексы осмысленными: row_id == ty_id не должно компилироваться.

type FnRow int
type TyId int
ro a = 1 as FnRow
ro t = 1 as TyId
ro n = 1
a + 1              // ok — литерал адаптируется, результат FnRow
a == t             // ошибка — разные newtype
a + n              // ошибка — newtype против типизированного int; пишется (a as int) + n

Sum-variant ↔ int (discriminant)

Sum-тип требует маркер enum после имени (D406, 2026-07-01 — старый синтаксис с ведущим | без enum отменён):

type ErrorCode enum NotFound = 404 | InternalError = 500
ro code = NotFound as int      // 404

int → Sum через as запрещён (число может не попасть в варианты). Используй pattern match.


Strict if cond:bool / while cond:bool

if cond, while cond, cond1 && cond2, cond1 || cond2 — cond обязан быть bool. Truthy-int (if a где a: int) запрещён.

ro n int = 5
if n { ... }                    // ❌ compile error
if n != 0 { ... }               // ✅

Прецеденты: Rust, Swift, Kotlin — все требуют bool. Python/C/JS — truthy, известный bug-class.


Zero-cost неявные конверсии — #coerce (D429, Plan 214/214.1)

Отдельно от явных механизмов выше — декларативный атрибут #coerce на унарной функции объявляет неявную конверсию I → O, вставляемую компилятором в позиции с известным ожидаемым типом (call-arg, ro/mut с аннотацией, return, элемент коллекции) — БЕЗ явного вызова на месте:

Форма показана на свежем примере (str @bytes()/StringBuilder @into_str() — уже объявленные в std пары, показывать их повторно здесь означало бы конфликт деклараций):

type Meters { ro raw f64 }
type Boxed consume { ro payload int }

#coerce
fn Meters @value() -> ro f64 => @raw            // view — Meters → ro f64

#coerce
fn Boxed consume @unbox() -> int => @payload    // finalize — потребляющий move

Канон call-сайта — голое значение, не явный вызов. Реальная std-пара str @bytes() -> ro []u8 включается автоматически там, где позиция ждёт []u8, а на руках str:

import std.runtime.write_buffer.{WriteBuffer}

fn write_greeting(mut wb WriteBuffer, s str) -> () =>
    wb.write_bytes(s)   // s неявно .bytes() — не пишем это руками

Так проходит любое выражение типа str — литерал, переменная, результат вызова (решение владельца 2026-10-01, амендмент D55): пара даёт zero-copy вид ro []u8, поэтому там, где нужен изменяемый mut []u8, она не применяется.

Амендмент 2026-10-01 (решение владельца, №1480, вариант (а)): только позиции только-для-чтения. Вид не применяется и там, где значение СОБСТВЕННОЕ и потом может стать изменяемым, — возврат -> []u8, элемент [][]u8, поле записи f []u8: mut-связь такого значения писала бы в байты самой строки. Там нужен явный s.bytes().clone(), иначе E_READONLY_COERCE. Позиции коэрсии — параметр по умолчанию (ro), ro-связь с аннотацией []u8 и любая позиция типа ro []u8 (-> ro []u8, []ro []u8, f ro []u8). Правило общее для всех view-пар D429, не только для str.

Две «полосы», обе гарантированно zero-cost:

  • view — не-consume метод с ro-возвратом (заём, без аллокации);
  • finalize — consume-метод с владеющим возвратом (move, ресивер разряжается в точке вставки; use-after — обычная compile-error линейности).

Правила (см. D429 полностью): ровно одна декларация на пару (I, O); один уровень (цепочки НЕ разворачиваются, коэрсии не компонуются друг с другом и с single-wrapper — конфликт = ошибка, не тихий выбор); exact-match всегда побеждает коэрсию; #coerce-функция обязана быть без эффектов. Первые декларации в std: str @bytes() -> ro []u8, StringBuilder consume @into_str() -> str, WriteBuffer consume @into_bytes() -> []u8. Механизм работает и для generic-образцов (Json[T] @data() -> T, снятие ограничения Plan 214.1, 2026-07-24).

as не задействует #coerce (D429 R10) — as остаётся закрытым, задокументированным в спеке множеством конверсий; #coerce — открытый пользовательский реестр, смешение двух дало бы третью дверь к одной паре.

Амендмент (№520, 2026-08-09): finalize-полоса — потребление ВЕЗДЕ, не только в явном вызове. ro s str = sb (аннотированный let), Rec { s: sb } (поле record-литерала), h.accept(sb) (аргумент метода) и [sb] под annotated let гасят обязательство sb ровно как явный sb.into_str() — использование sb после ЛЮБОЙ из этих форм ловится тем же use-after-consume (D131), а тип с @cleanup не получает повторный авто-вызов на выходе из scope. Возврат и call-arg свободной функции работали так уже до амендмента; деталь — D429 амендмент.


Именование from/try_from — конвенция, не протокол (⛔ ретракция 2026-07-06)

До 2026-07-06 From[T]/Into[U]/TryFrom[T,E]/TryInto[U,E] были generic-протоколами с авто-выводом обратной формы («4-way auto-derive»): написал T.from(v) — компилятор сам синтезировал v.into(). Решением владельца эти четыре протокола упразднены целиком:

  1. В Rust conversion-bounds — костыль отсутствия перегрузок; в Nova перегрузки есть (D84), From/Into как generic-bound в живом std не использовались НИ РАЗУ.
  2. ? не делает auto-From-конверсию ошибки (D325: один XError на домен, конверсия — явный .map_err(...)).
  3. Все реальные вызовы .into() в дереве играли роль «представление в строку» — это ось to_str(), а не передача владения.
  4. Уходит компиляторная магия синтеза (§3 compiler-conventions): blanket identity From, auto-derive From→Into, 4-шаговый resolution.

Что остаётся (три независимых конвенции ИМЁН, каждая — обычная Nova-функция без протокола за спиной):

  • (а) .from(x) / .try_from(x) — конкретные статик-методы, конструктор-конверсия по конвенции имени (не generic-bound-able). Только для источника-КОНЦЕПТА (from_polar, embed): если источник — значение-носитель, дверь запрещена, канон — x.to_*() на источнике (§1а, 2026-07-09; линт W_STATIC_CONVERSION). try_ — только когда есть infallible-сиблинг с тем же именем без префикса (R3, D325); одиночная фаллибельная операция без сиблинга — bare-имя без try_ (пример — s.to_int(), не s.try_int()).
  • (б) consume @into_ЦЕЛЬ() — конкретное имя для потребляющей передачи владения (into_str, into_raw, into_bytes, into_str_unchecked). Не общая операция .into() — генерик-версии больше нет, каждое имя объявляется на своём типе явно.
  • (в) .to_str() / семейство to_* — представление и трансформация (см. D410).

Компилятор НИЧЕГО не синтезирует между этими тремя — ни обратную форму, ни цепочку. Если тип хочет оба направления — программист пишет оба явно, разными именами.

type Celsius f64
type Fahrenheit f64

// Источник у обеих сторон — ЗНАЧЕНИЕ, поэтому статик `Fahrenheit.from(c)` тут
// запрещён (§1а): конверсия живёт методом на источнике.
fn Celsius @to_fahrenheit() -> Fahrenheit =>
    Fahrenheit((@ as f64) * 9.0 / 5.0 + 32.0)

// Компилятор НЕ синтезирует обратную форму — ни `.into()`, ни парную. Нужна
// обратная — пишем её явно и тем же правилом, на своём источнике:
fn Fahrenheit @to_celsius() -> Celsius =>
    Celsius(((@ as f64) - 32.0) * 5.0 / 9.0)

Фаллибельный случай — та же дверь, просто с проверкой: конверсия с валидацией из ro-источника — это x.to_*(), возвращающий Result (§1а, ВТОРАЯ строка). Источник здесь один и он носитель значения, поэтому статик запрещён так же, как выше. Имя без try_, потому что инфаллибельного сиблинга нет (R3, D325); точный близнец в std — int @to_char() -> Result[char, CharError]:

type Port u16

fn u16 @to_port() -> Result[Port, str] =>
    if @ == 0 { Err("port 0 reserved") } else { Ok(Port(@)) }

ro p = (8080 as u16).to_port()?

Type.new(...) — четвёртая строка §1а — про ДРУГОЕ: конструктор БЕЗ источника-носителя, то есть композитный (Date.new(y, m, d)) или оборачивающий значение в машину (Parser.new(input)). Одно значение, ставшее другим типом, туда не относится.


Прецеденты по языкам

ЯзыкГде близок к Nova
Rustas semantics, from/try_from naming, char::from_u32
Swiftstrict bool, no implicit coerce, Int(throwing:)
Kotlinstrict if-cond:bool, .toInt()/.toIntOrNull()
Go_ = strconv.ParseInt(s) ≈ try_from
Pythonstr(x)/int(s) ≈ from/try_from но не type-safe
C/C++(int)x без проверок — UB-class, Nova не повторяет

Текущий статус (актуализировано после ревизии 2026-07-26)

Реализовано и стабильно:

  • ✅ as-cast (numeric/newtype/sum), narrowing wraparound, float→int saturation
  • ✅ str @to_* parse-семья (to_int/to_i64/to_u64/to_i8/to_i16/to_i32/ to_u8/to_u16/to_u32/to_f64) — Plan 174.1, полный SignedInts/UnsignedInts-набор
  • ✅ str @to_bool()/str @to_char() — Plan 232.1 Т1 (2026-07-26)
  • ✅ bare-T @to_str() blanket + специализации (char, []u8) — Plan 174.2
  • ✅ []u8 @to_str()/@to_str_lossy()/@to_str_unchecked()/@into_str_unchecked() — D325
  • ✅ (cp int).to_char(), c.to_u8() — канон D54 (метод на источнике; u8.try_from снят 2026-09-05)
  • ✅ Checked narrowing @to_i8()..@to_uint() — D430 (2026-07-20)
  • ✅ #coerce (view/finalize) — D429/214.1, три std-пары + generic-образцы

Ретрактировано (не воскрешать без нового sign-off):

  • ⛔ Протоколы From/Into/TryFrom/TryInto и их auto-derive синтез — 2026-07-06
  • ⛔ str.from(scalar) static-конструктор — 2026-07-14 (заменён .to_str())
  • ⛔ str.try_from([]u8) / str.from_bytes(...) — заменены []u8 @to_str()-семьёй
  • ⛔ Методы .unwrap()/.unwrap_or()/.unwrap_or_else() на Option/Result — 2026-07-07
  • ⛔ Старый sum-синтаксис без enum-маркера — D406 (2026-07-01)

Ссылки