Нормативный текст спецификации. Английская страница — 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()

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


Numeric ↔ numeric

Автоматического расширения в операции нет (D405, амендмент 2026-09-04)

Каждое расширение ниже происходит потому, что вы написали as. Nova не выбирает за вас более широкий тип, когда два числовых типа встречаются в одной операции — никакого «бери больший», никакой промоции целого в 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.

Значение типа 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 (любой int64 representable как f64)
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 }; конечный литерал вне f64TooLarge/TooSmall, не молчаливый inf. Правильное округление десятичной записи в двоичную — тоже на Nova: порт Rust core::num::dec2flt (быстрый путь Клинжера, Eisel-Lemire и медленный путь на большом десятичном числе для входов, которые не решает ни один из двух); C в to_f64() больше не участвует, strtod остаётся только эталоном дифференциальных тестов.

Дверь 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]BelowMincp < 0 · AboveMaxcp > 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 (D410as_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

Суммы правило не касается: SqlValue.I(x) подставляется и для переменной — там компилятор ВЫВОДИТ единственный подходящий вариант, а не придумывает утверждение автора. Нужна прежняя мягкость для своего newtype — объявляется парой #coerce.

Операторы над 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 || cond2cond обязан быть 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() — не пишем это руками

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

  • view — не-consume метод с ro-возвратом (заём, без аллокации);
  • finalizeconsume-метод с владеющим возвратом (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)

Ссылки