Informative translation. The Russian text (русский оригинал) is normative.

Informative translation; the Russian text is normative.

Russian original (normative): conversions.md

Nova — type conversions

A consolidated page of all conversion rules in one place. Full D-decisions: D54 (as), D52 (newtype/alias/sum), D325 (the unified fallible std contract), D410 (the to_str/bytes family), D429 (#coerce — zero-cost implicit), D430 (checked narrowing to_*). From/Into/TryFrom/TryInto as protocols were retracted 2026-07-06 (D73/D77) — details in the “from/try_from naming” section below.


The three mechanisms

MechanismWhenExample
asinfallible numeric/newtype/sum cast, compile-time, no runtime code42 as f64, n as i16
.to_str()universal conversion of a value to a string (bare-T blanket + specializations)42.to_str(), bs.to_str()
T.from(v) / T.try_from(v)a concrete static constructor — a naming convention, NOT a protocol/auto-derive. Legal ONLY when the source is a concept rather than a carrier value: for a value the canon is a method on the source, x.to_*() (nv-coding-style §1а, 2026-07-09; lint W_STATIC_CONVERSION)Complex.from_polar(r, phi)
consume @into_TARGET()consuming ownership transfer (a concrete name on the source)sb.into_str(), wb.into_bytes()
#coercedeclarative implicit zero-cost conversion in a position with a known expected type (view/finalize)w.write(s)str implicitly .bytes()

Important (2026-07-06 retraction, see below): .from(v) / .try_from(v) — this is a PAIR of concrete static methods on a concrete type, not a generic From[T]/TryFrom[T,E] protocol. The compiler does not synthesize the reverse form (.into()/.try_into()) automatically — the programmer writes exactly what they declared. There is no “universal” .into() in the language anymore.


Numeric ↔ numeric

There is no automatic widening in an operation (D405, amended 2026-09-04)

Every widening below happens because you wrote as. Nova does not pick the wider type for you when two numeric types meet in one operation — no “take the larger”, no integer promoted to float, no signed compared against unsigned. Mixed operands are a compile error, and the conversion you meant is written down.

This is the rule C spent decades teaching everyone to fear: u32(4294967295) == i32(-1) is true in C, because the signed operand is converted to unsigned behind your back. In Nova it does not compile.

The 2026-09-04 amendment states the rule by category rather than by listing operators, after three doors were measured outside the original list: relational operands of different signedness, an integer against a float, and negation of an unsigned value. All three answered, and answered wrongly.

Two relaxations, and both are about literals only, where no second type exists to disagree with:

  • a float literal is accepted in a float position without as (ro x f32 = 1.5);
  • an untyped literal adapts to the float operand beside it (b + 1 where b f64), the way an untyped constant does in Go.

A value of type int next to a value of type f64 is still an error, literal or not.

Belonging to one category is not permission to mix inside it. Both f32 and f64 are floats; f32 < f64 is still an error. Both i32 and i64 are signed integers; i64 < i32 is still an error. The category decides which operators exist for a type, not which pairs of types may meet. Every one of these is refused, and the conversion you intend is written with as:

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

Unary minus is defined for signed types only. -x with an unsigned x is a compile error, not a modular wrap (-200u8 does not become 56); what you mean is written -(x as i16).

Widening (no precision loss)

From → ToViaSemantics
i8 → i16/i32/i64/intassign-extend
u8 → u16/u32/u64/intaszero-extend
i8/u8 → f64asexact (any int64 representable as f64)
f32 → f64asexact

Narrowing (potential precision loss)

From → ToViaSemantics
i64 → i32/i16/i8aswraparound (modulo 2^N)
u64 → u32/u16/u8/byteaswraparound
iN → uM, including int → uintasbit-pattern ((-1 as int) as uint == 2^64−1; the D130 Q2 saturation special case was retired 2026-09-04 — one table for the whole family)
int ↔ i64, uint ↔ u64asno-op on the 64-bit bootstrap, but distinct types (D129/D130): the cast is required

All integer → integer rows above are one operation: take the low M bits of the source and read them as the target type. “wraparound” and “bit-pattern” are the same thing seen from two sides, so u64 as u8 and i32 as u16 need no manual & 0xFF / & 0xFFFFas is that mask (0x1234 as u8 == 52, 70000 as u16 == 4464). Unary ~ keeps the operand’s type (~5u8 == 250, ~5i32 == -6) and is defined for integers and integer newtypes only (D46 amendment 2026-07-27). | f64 → f32 | as | IEEE rounding (precision loss) | | f64/f32 → iN/uN | as | saturation + NaN→0 + ±∞→bounds |

Float→int saturation — defined behavior on any input (unlike C/C++ UB). Consistent with 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 between integer widths is always wraparound (silent loss of high bits). If you need a check instead of silent wrap — a bounded blanket @to_<T>() on any type from the Ints set, symmetric for all target widths (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

FROM A FLOAT TOO, UNDER THE SAME NAME (D430 amendment R6, 2026-09-04). A second blanket family, fn[S Floats] S @to_<T>(), covers f32 and f64. The semantics are the integers’ semantics, and that is the point of the amendment: the question is “did it fit”, not “was it exact”. The fractional part truncates toward zero, then the target’s range is checked; NaN and the infinities are 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)

One name, one meaning across the whole family: a method whose promise shifts with the kind of its receiver reads as one thing and behaves as two. Donors: Swift’s Int16(f), C#‘s checked.

RangeError — a unit type (“didn’t fit”, no payload — the fact itself is exhaustive). as remains the fast truncating cast, unchanged — to_* does not replace it, but adds a checked alternative alongside.


Numeric ↔ str

str → numeric (parse, fallible) — a method ON THE SOURCE, not a static on the target

Canon (Plan 174.1, 2026-07-08, owner decision — superseded the early static-constructor design T.parse(s)/T.try_from(s)): converting a string to a number is a method on str (s.to_int()), not a static constructor on the target type. Mirrors the s.to_str() family in reverse.

From → ToViaFailure
str → ints.to_int(radix: int = 10)non-digit / overflow / (custom radix) invalid radix
str → i64/u64s.to_i64() / s.to_u64()no extra range-check (same width as the engine)
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 into the target width
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)

Errors — structural enums: type ParseIntError enum Empty | InvalidDigit | AboveMax | BelowMin | InvalidRadix (2026-09-05: Overflow split by direction) and type ParseFloatError enum Empty | Malformed { at int } | TooLarge | TooSmall (interim names — registry #136; target vocabulary Invalid/AboveMax/BelowMin)

Float grammar and conversion are both Nova’s (plan 282 Ф.4, 2026-09-05; plan 283 Ф.4, 2026-09-07). s.to_f64() accepts exactly [+-]?(digits[.digits?]|.digits)([eE][+-]?digits)? — no whitespace, no nan/inf, no hex, no _, no locale separator; the first offending byte is reported in Malformed { at }; a finite literal outside f64 is TooLarge/TooSmall, not a silent inf. The correctly-rounded decimal→binary step is Nova too — a port of Rust’s core::num::dec2flt (a Clinger fast path, Eisel-Lemire, and a big-decimal slow path for the inputs neither one resolves); C plays no part in to_f64() any more, and strtod survives only as the differential-test oracle.

str -> f32 exists too, and it rounds in ONE step (plan 283 Ф.7, 2026-09-07). s.to_f32() takes the same grammar and the same ParseFloatError as to_f64(), and converts the decimal digits straight to f32. It is NOT to_f64() followed by a narrowing cast: for a decimal that sits within less than the resolution of f64 above the midpoint between two neighbouring f32 values, rounding to f64 first loses the excess, the value lands exactly on the midpoint, and round-half-to-even then goes the opposite way from the truth (measured: five of six such strings differ). The retracted f32.try_parse did exactly that and is gone.

Comparing a result. Until registry #136 is fixed, compare a Result whose error has a shared variant name (AboveMax/BelowMin live in RangeError, CharError and ParseIntError) with match or a qualified variant (r == Err(ParseIntError.AboveMax)); a bare r == Err(AboveMax) may silently pick another type’s variant. (std/runtime/string/parse.nv).

str → bool (parse, fallible)

Canon (Plan 232.1 T1, owner decision “add”, 2026-07-26): s.to_bool() — strictly "true"/"false", lowercase-only (the Rust str::parse::<bool> canon; no case-insensitive/"1"/"0"/"yes" aliases).

From → ToViaFailure
str → bools.to_bool()empty → Err(Empty); anything other than exactly "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) — the same two-variant pattern as ParseFloatError.

numeric → str (format, infallible) — a single entry point .to_str()

Canon (Plan 174.2, 2026-07-14): str.from(scalar) was retracted. The only public entry point “value → string” is the bare-T blanket fn[T] T @to_str() -> str => "${@}" (D410 amend), specialized by concrete overloads where a different arity/semantics is needed (e.g. decode for []u8, see below).

From → ToVia
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"

Interpolation ("${n}") lowers into the same path directly (for primitives — into a Display helper at the C level, without re-calling .to_str() — no recursion).


Char / Byte / []byte / str

char → str (UTF-8 encode)

ViaSemantics
c.to_str()infallible UTF-8 encode (1-4 bytes) — a specialization of the to_str() blanket, byte-identical to the former str.from(char)

str → char (single codepoint, fallible)

Canon (Plan 232.1 T1, owner decision “add”, 2026-07-26): s.to_char() parses EXACTLY one Unicode codepoint (not a byte — "é".to_char() succeeds, even though é is 2 UTF-8 bytes). A receiver form on the source, the same principle as str @to_int().

ViaFailure
s.to_char() -> Result[char, ParseCharError]empty → 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) — does NOT reuse CharError (see the “int → char” section below): that domain is a codepoint outside the Unicode scalar value range/surrogates, unreachable for str→char (the bytes of a str are already valid UTF-8, R-UTF8).

int → char (codepoint range-check, fallible)

Canon (owner, 2026-07-09): a receiver form on the source ((cp int).to_char()), not a static char.try_from(n) — the same chaining principle as str @to_int(): (32 + off).to_char()?.

ViaFailure
(cp int).to_char() -> Result[char, CharError]BelowMincp < 0 · AboveMaxcp > 0x10FFFF · Invalid — reserved block [0xD800, 0xDFFF] (2026-09-05: one door, one error; CharError/InvalidCodepoint/str.try_from_codepoint retired)
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)

This pair stayed a static form (did not migrate to a receiver) — the only case where try_ remained on the target type:

ViaFailure
c.to_u8() -> Result[u8, RangeError]codepoint > 0xFF (not Latin-1) — receiver form since 2026-09-05; u8.try_from(c char) retired (D54 amendment: a conversion between concrete types is a method on the source; a static on the target is for Self-constructors in protocols and try_from beside from only)

Exception: 'A' as byte, 'A' as int, 'A' as u8 — allowed for char literals (compile-time-known codepoint), see D54.

[]byte ↔ str — the unified to_str family (D325/174.1)

Canon: []u8 decode also goes through to_str() — a concrete overload (arity/semantics of decode, not format) beats the bare-T blanket by the “concrete beats generic” rule (D84). str.try_from([]u8) / the separate str.from_bytes(...) — historical names, withdrawn, only the forms below are current:

FormTypeSemantics
bs.to_str()-> Result[str, Utf8Error]checked decode; Utf8Error{byte_offset} points at the first invalid byte
bs.to_str_lossy()-> strinfallible, invalid sequences are replaced with a replacement character
unsafe { bs.to_str_unchecked() }-> strunchecked, the caller guarantees valid UTF-8
unsafe { bs.consume.into_str_unchecked() }-> stras above, but a consuming zero-copy move of the buffer
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) — a bare view, not a transformation: s.bytes() -> ro []u8 (D410as_bytes was renamed to bytes; this same name is the first declared #coerce pair, see the “Zero-cost implicit conversions” section below).


Bool ↔ everything

From → ToViaSemantics
bool → intastrue=1, false=0
bool → byte / bool → f64asthe same
bool → strb.to_str()"true" / "false"
int/byte/f64/etc → boolforbiddenuse 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 — see the TODO above (not found in std as of this revision).


Newtype ↔ underlying

A newtype (type X Y, without alias, D52) — a type separate from the source; conversion is an explicit as (identity, same C-repr). This differs from alias (type X alias Y) — there X and Y are interchangeable without any cast (not a separate type).

ViaSemantics
n as MyNewtypeidentity (same C representation)
nt as intidentity
type UserId int
ro u UserId = 42 as UserId
ro n int = u as int            // 42

The implicit half and its boundary (D55 amend, 2026-08-21). At a position with an explicit expected type a newtype wraps itself — but for an UNTYPED CONSTANT only. A typed variable needs the explicit form; the boundary is Go’s, which D52 cites when recommending the form.

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

Sums are untouched: SqlValue.I(x) is still inserted for a variable — there the compiler DERIVES the only matching variant instead of inventing the author’s claim. For the old softness on your own newtype, declare it as a #coerce pair.

Operators on a newtype stay inside the newtype (D52 amend, 2026-09-04). Arithmetic and comparison are defined between two values of the same newtype (plus a literal, which adapts); the arithmetic result is that newtype, a comparison is bool. Two different newtypes over the same representation, or a newtype against a value of the representation, do not meet in an operator — the conversion is written with as. This is Go’s rule for type FnRow int, and it is what makes typed indices worth having: row_id == ty_id must not compile.

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)

A sum type requires the enum marker after the name (D406, 2026-07-01 — the old syntax with a leading | without enum is revoked):

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

int → Sum via as is forbidden (a number may not hit any variant). Use pattern matching.


Strict if cond:bool / while cond:bool

if cond, while cond, cond1 && cond2, cond1 || cond2cond must be bool. Truthy-int (if a where a: int) is forbidden.

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

Precedents: Rust, Swift, Kotlin — all require bool. Python/C/JS — truthy, a known bug-class.


Zero-cost implicit conversions — #coerce (D429, Plan 214/214.1)

Separately from the explicit mechanisms above — the declarative #coerce attribute on a unary function declares an implicit conversion I → O, inserted by the compiler in positions with a known expected type (call-arg, ro/mut with an annotation, return, collection element) — WITHOUT an explicit call on site:

The form is shown on a fresh example (str @bytes()/StringBuilder @into_str() — pairs already declared in std; showing them again here would mean a declaration conflict):

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

The call-site canon is a bare value, not an explicit call. The real std pair str @bytes() -> ro []u8 kicks in automatically where the position expects []u8 and a str is on hand:

import std.runtime.write_buffer.{WriteBuffer}

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

Two “lanes”, both guaranteed zero-cost:

  • view — a non-consume method with a ro return (a borrow, no allocation);
  • finalize — a consume method with an owning return (a move; the receiver is discharged at the insertion point; use-after — an ordinary linearity compile error).

Rules (see D429 in full): exactly one declaration per pair (I, O); one level (chains are NOT unfolded, coercions do not compose with each other or with a single-wrapper — a conflict is an error, not a silent choice); exact-match always beats coercion; a #coerce function must be effect-free. The first declarations in std: str @bytes() -> ro []u8, StringBuilder consume @into_str() -> str, WriteBuffer consume @into_bytes() -> []u8. The mechanism also works for generic patterns (Json[T] @data() -> T, bound removal in Plan 214.1, 2026-07-24).

as does not engage #coerce (D429 R10) — as remains a closed, documented-in-spec set of conversions; #coerce is an open user registry; mixing the two would give a third door to one pair.

Amendment (#520, 2026-08-09): the finalize lane — consumption EVERYWHERE, not only in an explicit call. ro s str = sb (an annotated let), Rec { s: sb } (a record-literal field), h.accept(sb) (a method argument) and [sb] under an annotated let discharge sb’s obligation exactly like an explicit sb.into_str() — using sb after ANY of these forms is caught by the same use-after-consume (D131), and a type with @cleanup gets no second automatic call on scope exit. Return and a free-function call argument already worked this way before the amendment; detail — D429 amendment.


from/try_from naming — a convention, not a protocol (⛔ retraction 2026-07-06)

Until 2026-07-06 From[T]/Into[U]/TryFrom[T,E]/TryInto[U,E] were generic protocols with auto-derivation of the reverse form (“4-way auto-derive”): you wrote T.from(v) — the compiler synthesized v.into() itself. By the owner’s decision all four protocols are abolished entirely:

  1. In Rust, conversion bounds are a crutch for the lack of overloading; in Nova overloading exists (D84), and From/Into as a generic bound was never used in live std, NOT ONCE.
  2. ? does not do auto-From error conversion (D325: one XError per domain, conversion is an explicit .map_err(...)).
  3. All real .into() calls in the tree played the role of “value to string” — that is the to_str() axis, not ownership transfer.
  4. The compiler magic of synthesis goes away (§3 compiler-conventions): the blanket identity From, auto-derive From→Into, 4-step resolution.

What remains (three independent naming conventions, each an ordinary Nova function with no protocol behind it):

  • (a) .from(x) / .try_from(x) — concrete static methods, constructor-conversion by naming convention (not generic-bound-able). For a CONCEPT source only (from_polar, embed): if the source is a carrier value, this door is forbidden and the canon is x.to_*() on the source (nv-coding-style §1а, 2026-07-09; lint W_STATIC_CONVERSION). try_only when there is an infallible sibling with the same name without the prefix (R3, D325); a lone fallible operation without a sibling — a bare name without try_ (example — s.to_int(), not s.try_int()).
  • (b) consume @into_TARGET() — a concrete name for a consuming ownership transfer (into_str, into_raw, into_bytes, into_str_unchecked). Not the general .into() operation — a generic version no longer exists; each name is declared on its own type explicitly.
  • (c) .to_str() / the to_* family — representation and transformation (see D410).

The compiler synthesizes NOTHING between these three — neither the reverse form nor a chain. If a type wants both directions — the programmer writes both explicitly, under different names.

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)

The fallible case is the same door with a check: a conversion with validation from a ro-source is x.to_*() returning a Result (nv-coding-style §1а, SECOND row). The source here is a single carrier value, so a static is forbidden for the same reason as above. The name carries no try_ because there is no infallible sibling (R3, D325); the exact twin in std is 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(...) — the fourth row of §1а — is about something else: a constructor with NO carrier source, i.e. composite (Date.new(y, m, d)) or wrapping a value in a machine (Parser.new(input)). One value becoming another type does not belong there.


Precedents by language

LanguageWhere close to 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 but not type-safe
C/C++(int)x without checks — UB-class, Nova does not repeat it

Current status (updated after the 2026-07-26 revision)

Implemented and stable:

  • as-cast (numeric/newtype/sum), narrowing wraparound, float→int saturation
  • str @to_* parse family (to_int/to_i64/to_u64/to_i8/to_i16/to_i32/ to_u8/to_u16/to_u32/to_f64) — Plan 174.1, the full SignedInts/UnsignedInts set
  • str @to_bool()/str @to_char() — Plan 232.1 T1 (2026-07-26)
  • ✅ bare-T @to_str() blanket + specializations (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 canon (source-method; u8.try_from retired 2026-09-05)
  • ✅ Checked narrowing @to_i8()..@to_uint()D430 (2026-07-20)
  • #coerce (view/finalize) — D429/214.1, three std pairs + generic patterns

Retracted (do not resurrect without a new sign-off):

  • ⛔ The From/Into/TryFrom/TryInto protocols and their auto-derive synthesis — 2026-07-06
  • ⛔ The str.from(scalar) static constructor — 2026-07-14 (replaced by .to_str())
  • str.try_from([]u8) / str.from_bytes(...) — replaced by the []u8 @to_str() family
  • ⛔ The .unwrap()/.unwrap_or()/.unwrap_or_else() methods on Option/Result — 2026-07-07
  • ⛔ The old sum syntax without the enum marker — D406 (2026-07-01)

References

Last updated July 26, 2026