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» ниже.
Три механизма
| Механизм | Когда | Пример |
|---|---|---|
as | infallible 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/int | as | sign-extend |
u8 → u16/u32/u64/int | as | zero-extend |
i8/u8 → f64 | as | exact (любое значение i8/u8 представимо) |
f32 → f64 | as | exact |
Narrowing (potential precision loss)
| From → To | Через | Семантика |
|---|---|---|
i64 → i32/i16/i8 | as | wraparound (modulo 2^N) |
u64 → u32/u16/u8/byte | as | wraparound |
iN → uM, включая int → uint | as | bit-pattern ((-1 as int) as uint == 2^64−1; частный случай насыщения D130 Q2 снят 2026-09-04 — одна таблица на всё семейство) |
int ↔ i64, uint ↔ u64 | as | no-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 → int | s.to_int(radix: int = 10) | non-digit / overflow / (custom radix) invalid radix |
str → i64/u64 | s.to_i64() / s.to_u64() | без доп. range-check (та же ширина, что движок) |
str → i8/i16/i32/u8/u16/u32 | s.to_i8() / s.to_i16() / s.to_i32() / s.to_u8() / s.to_u16() / s.to_u32() | + range-check в целевую ширину |
str → f64 | s.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 → bool | s.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 → str | n.to_str() |
f64/f32 → str | f.to_str() |
bool → str | b.to_str() |
char → str | c.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() | -> str | infallible, невалидные последовательности заменяются 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 → int | as | true=1, false=0 |
bool → byte / bool → f64 | as | то же |
bool → str | b.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 MyNewtype | identity (одинаковое C-представление) |
nt as int | identity |
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(). Решением
владельца эти четыре протокола упразднены целиком:
- В Rust conversion-bounds — костыль отсутствия перегрузок; в Nova
перегрузки есть (D84),
From/Intoкак generic-bound в живом std не использовались НИ РАЗУ. ?не делает auto-From-конверсию ошибки (D325: одинXErrorна домен, конверсия — явный.map_err(...)).- Все реальные вызовы
.into()в дереве играли роль «представление в строку» — это осьto_str(), а не передача владения. - Уходит компиляторная магия синтеза (§3 compiler-conventions):
blanket identity
From, auto-deriveFrom→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 |
|---|---|
| Rust | as semantics, from/try_from naming, char::from_u32 |
| Swift | strict bool, no implicit coerce, Int(throwing:) |
| Kotlin | strict if-cond:bool, .toInt()/.toIntOrNull() |
| Go | _ = strconv.ParseInt(s) ≈ try_from |
| Python | str(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)
Ссылки
- 03-syntax.md → D54 —
asоператор - 03-syntax.md → D44 — числовые литералы
- 03-syntax.md → D410 —
to_str/bytes/into_*-семейство имён - 02-types.md → D52 — newtype/alias/sum-декларации
- 02-types.md → D406 —
enum-маркер sum-типа - 02-types.md → D429 —
#coerce(zero-cost implicit view/finalize) - 04-effects.md → D430 — checked narrowing
to_* - 04-effects.md → D325 — единый fallible-контракт std (Result-everywhere)
- 08-runtime.md → D73 —
From/Into(⛔ протокол ретрактирован 2026-07-06) - 08-runtime.md → D77 —
TryFrom/TryInto(⛔ протокол ретрактирован 2026-07-06)