From 6ba5f7e0ded814e92c43f80bfc58d1fea08b654a Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 6 Mar 2025 15:26:08 +0200 Subject: [PATCH 01/12] First draft of const-nonzero-ergonomics --- text/0000-const-nonzero-ergonomics.md | 374 ++++++++++++++++++++++++++ 1 file changed, 374 insertions(+) create mode 100644 text/0000-const-nonzero-ergonomics.md diff --git a/text/0000-const-nonzero-ergonomics.md b/text/0000-const-nonzero-ergonomics.md new file mode 100644 index 00000000000..f3520367751 --- /dev/null +++ b/text/0000-const-nonzero-ergonomics.md @@ -0,0 +1,374 @@ +- Feature Name: `const-nonzero-ergonomics` +- Start Date: 2025-03-07 +- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000) +- Rust Issue: none + +# Summary +[summary]: #summary + +The `std::num::NonZero` type allows non-zero integer semantics to be clearly expressed. Yet this +type is only seamlessly usable if all APIs with non-zero semantics use this type, due to required +to/from conversion at any API boundary that differs in its use of `NonZero`. + +The burden of these conversions is especially heavy in tests and examples. This RFC proposes new +coercions to facilitate implicit conversion to `NonZero` from integral constants, simplifying usage +in tests and examples, where succinctness and readability are paramount. + +# Motivation +[motivation]: #motivation + +Using `NonZero` to express non-zero semantics is valuable because it leads to clarity of APIs, +removes the need for some runtime zero-checks and creates potential for niche optimizations. + +In typical logic code, `NonZero` values are validated early in the process, such as when parsing +user input. We start with a value of integral type `T` which may or may not be zero, and we parse +this into a `NonZero` - if this succeeds, we know it is not zero and can skip zero-checks in any +further calls, as long as the API surface uses `NonZero`. + +**However, there is one area where `NonZero` has particularly poor ergonomics: tests and examples!** +In test code, numeric constants are common. By switching a function parameter from `u32` to +`NonZero` we add needless complexity to the test code. + +Without `NonZero`: + +```rust +fn item_fits_exactly_in_packaging(height: u32) -> bool { + assert_ne!(0, height, "cannot package a product with a height of zero"); + 1000 % height == 0 +} + +#[test] +fn item_fits_exactly_in_packaging_if_divides_1000() { + // The packaging has a height of 1000, so any integer that divides it evenly will fit. + assert!(item_fits_exactly_in_packaging(1)); + assert!(!item_fits_exactly_in_packaging(3)); + assert!(item_fits_exactly_in_packaging(25)); + assert!(!item_fits_exactly_in_packaging(999)); + assert!(item_fits_exactly_in_packaging(1000)); +} +``` + +With `NonZero`: + +```rust +use std::num::NonZero; + +fn item_fits_exactly_in_packaging(height: NonZero) -> bool { + // No need to worry about division by zero because we accept NonZero input. + // This means we can avoid checking the denominator in every call to this function. + 1000 % height.get() == 0 +} + +#[test] +fn item_fits_exactly_in_packaging_if_divides_1000() { + // The packaging has a height of 1000, so any integer that divides it evenly will fit. + assert!(item_fits_exactly_in_packaging(NonZero::new(1).unwrap())); + assert!(!item_fits_exactly_in_packaging(NonZero::new(3).unwrap())); + assert!(item_fits_exactly_in_packaging(NonZero::new(25).unwrap())); + assert!(!item_fits_exactly_in_packaging(NonZero::new(999).unwrap())); + assert!(item_fits_exactly_in_packaging(NonZero::new(1000).unwrap())); +} +``` + +Having to manually construct the `NonZero` wrapper in test code can become very noisy and is +especially problematic in example code and doctests, where crate authors want to put their best +foot forward, to show off how easy it is to use the crate. This is because the nature of `NonZero` +creates a difference between the two categories of usage: + +* In real use, the `NonZero` is typically constructed when parsing the input, not when calling some + API that expresses its parameters via `NonZero`. +* In test and example use, there often is no parsing stage, so hardcoded test input must be manually wrapped at + call sites. + +**This means that tests and examples are much more noisy than real-world usage for an API that uses +`NonZero`, giving a false impression of API complexity and discouraging API authors from using +`NonZero` despite its advantages.** + +In test and example code this commonly occurs with integral constants and literals. The values are +known and can be validated at compile time to be non-zero and within expected bounds. + +This RFC proposes that we omit the ceremony with constants, allowing implicit coercion of non-zero +integral constants and literals to `NonZero`, thereby encouraging Rust crate authors to +make more extensive use of `NonZero`, which they may today choose to avoid due to the extra cruft +it adds to tests and examples. + +With this RFC implemented, this would be valid code: + +```rust +use std::num::NonZero; + +fn item_fits_exactly_in_packaging(height: NonZero) -> bool { + // No need to worry about division by zero because we accept NonZero input. + // This means we can avoid checking the denominator in every call to this function. + 1000 % height.get() == 0 +} + +#[test] +fn item_fits_exactly_in_packaging_if_divides_1000() { + // The packaging has a height of 1000, so any integer that divides it evenly will fit. + assert!(item_fits_exactly_in_packaging(1)); + assert!(!item_fits_exactly_in_packaging(3)); + assert!(item_fits_exactly_in_packaging(25)); + assert!(!item_fits_exactly_in_packaging(999)); + assert!(item_fits_exactly_in_packaging(1000)); +} +``` + +# Guide-level explanation +[guide-level-explanation]: #guide-level-explanation + +The `std::num::NonZero` type can be used to express that an integer of type `T` has a non-zero +value. This helps clearly express the intent of the API and encourages "thinking with types", +whereby function calls are maximally validated at compile time, reaching a state where if the code +compiles, it has a high probability of being correct. + +In typical usage, you convert from a `T` such as `u32` into a `NonZero` when parsing the input +and thereafter keep the value in the `NonZero` wrapper: + +```rust ignore +use std::{env, num::NonZero}; + +fn main() { + let Some(product_height) = env::args().nth(2) else { + eprintln!("provide an integer as the first argument ('product_height') to this sample app"); + return; + }; + + let product_height = product_height + .parse::() + .expect("first argument ('product_height') must be an integer"); + + // We validate that integers are non-zero as soon as possible and keep them in the + // NonZero wrapper type after that, to avoid further unnecessary zero-checks. + let Some(product_height) = NonZero::new(product_height) else { + eprintln!("first argument ('product_height') must be non-zero"); + return; + }; + + // .. +} +``` + +You can pass this value as-is to functions that take `NonZero`: + +```rust ignore +fn main() { + // .. + + if !item_fits_exactly_in_packaging(product_height) { + eprintln!("product does not fit in packaging"); + return; + } + + println!("product fits in packaging"); +} + +fn item_fits_exactly_in_packaging(height: NonZero) -> bool { + // No need to worry about division by zero because we accept NonZero input. + // This means we can avoid checking the denominator in every call to this function. + 1000 % height.get() == 0 +} +``` + +When writing test or example code and using hardcoded constants, you can omit the conversion into +`NonZero` - it is done implicitly at compile time. This only works with constant values (either +`const` variables or literals like `1234`). + +```rust ignore +#[test] +fn item_fits_exactly_in_packaging_if_divides_1000() { + // The packaging has a height of 1000, so any integer that divides it evenly will fit. + assert!(item_fits_exactly_in_packaging(1)); + assert!(!item_fits_exactly_in_packaging(3)); + assert!(item_fits_exactly_in_packaging(25)); + assert!(!item_fits_exactly_in_packaging(999)); + assert!(item_fits_exactly_in_packaging(1000)); +} +``` + +This is similar to how in the statement `let foo: u8 = 123;` +the literal `123` is inferred to be `u8`. + +Being able to skip the `NonZero::new` helps avoid unnecessary complexity in tests, encouraging high +test coverage and making test code easier to read. It also helps focus examples and doctests on the +usage of the `NonZero` capable API that you are publishing, without unnecessary noise from parsing +of constant values that would typically happen elsewhere in real code. + +# Reference-level explanation +[reference-level-explanation]: #reference-level-explanation + +This can likely be implemented as a new coercion, consisting approximately of +the following processing steps: + +1. During type checking (`rustc_hir_typeck/src/coercion.rs:coerce()`), we detect the coercion + opportunity from an integral constant (literal or variable, explicitly typed or inferred) to + a `NonZero` with a potentially compatible `T`. At this stage, we only have types and do not + yet have values (which might be zero or out of bounds). +1. If the types are potentially compatible, we emit this as a new `Adjustment::NonZero` variant. +1. During MIR building, when the constant values are available, we apply the adjustment and perform + the "is it zero" and "is it in bounds of T" check, before emitting MIR for something like + `NonZero::new_unchecked(123)`. +1. If at this stage we detect an invalid value (zero or out of bounds), we emit a compiler + diagnostic and give up on this coercion. + +The situation before this change would be that the compilation fails at the type check stage +because `u32 != NonZero`, so this should not be breaking any existing code, only enabling +new capabilities. + +# Drawbacks +[drawbacks]: #drawbacks + +Any implicit behavior in the language risks becoming "magic" that is hard to understand and reason +about. For this reason such behaviors are very uncommon in Rust. The greatest drawback is that we +may open a box that also contains some demons in addition to this simple enhancement - what other +implicit behaviors might get proposed using this as an example? The fact that this RFC is scoped +to constants and not variables is the mitigating factor here. After all in `let foo: u8 = 123` the +literals becomes a `u8` and is not too different in nature from `let foo: NonZero = 123`, though +the mechanics are of course different. + +# Rationale and alternatives +[rationale-and-alternatives]: #rationale-and-alternatives` + +An alternative with less magic would be to require an explicit "nonzero" suffix for constant +literals to activate this coercion: + +```rust +fn foo(value: NonZero) {} + +foo(1234); // Error +foo(1234_nz); // OK +foo(1234_u32_nz); // OK +``` + +This would still eliminate most of the cruft from tests and examples while still requiring an +explicit action by the user to invoke the coercion. + +Given that the potential for "confusion by too much magic" seems minimal as the concept of +"non-zero" is trivial to understand and the proposal does not involve any size-coercion, +the value of avoiding fully implicit coercion here seems low - there is not an obvious benefit +from only taking half a step toward an improvement. + +If we consider a library approach rather than a language approach, one viable alternative is to +create a macro to easily construct non-zero values and validate that they really are non-zero. +For example: + +```rust +use std::num::nonzero; + +fn foo(value: NonZero) {} + +foo(1234); // Error +foo(nonzero!(1234)); // OK + +macro_rules! nonzero { + ($x:literal) => { + const { ::std::num::NonZero::new($x).expect("literal must have non-zero value") } + }; +} +``` + +This macro will attempt to construct the value in a const context and emit a meaningful error if +that fails. This may offer a satisfactory user experience if such a macro were included in `std`, +though still comes at a cost of an unusual level of cruft for merely passing integer constants to +code, and remains something the user of the code has to think and know about. + +Given that passing integer constants to code is an everyday task for a programming language, that +the `NonZero` wrapper type exists already in `std` and that there are so far no known corner cases +that could not be easily verified correct at compile time, having it "just work" seems to offer the +best tradeoff of factors to encourage wider usage of non-zero semantics, yielding more correct and +potentially more efficient code in APIs where `NonZero` is applicable. + +That said, all these alternatives are better than what we have today - manually having to construct +an instance of `NonZero`. + +[Previous discussions](https://internals.rust-lang.org/t/elevate-nonzero-types-to-true-primitive-integer-types/18040/20) +have suggested that other ergonomic challenges exist with `NonZero` (e.g. because it lacks many of +the methods that you would expect to find on primitive integer types it wraps) and that improving +its ergonomics across the board would likely be challenging, if not due to the mechanics then at +least due to the scope of functionality associated with numeric types. Nevertheless, this does not +appear to be a meaningful argument against making `NonZero` ergonomics better for the scenarios +where it is useful to Rust users today, even if those cases are more limited than raw numerics. +In practice, `NonZero` types do appear to be gaining new members, so +there does not appear to be a consensus against improving them (e.g. 1.84 stabilized +[`NonZero::isqrt`](https://doc.rust-lang.org/std/num/struct.NonZero.html#method.isqrt)) + +# Prior art +[prior-art]: #prior-art + +Exploration of other languages suggests that while refinement types like `NonZero` are common, they +generally require explicit conversion as they are not specific to integers as a general language +feature. In contrast, this RFC deals with integer refinement in particular, as the `NonZero` types +are focused specifically on this mode of refinement. + +Similar functionality appears to be present in Ada if we use its subtyping feature to +define a `NonZero_Int` type: + +```ada +with Ada.Text_IO; use Ada.Text_IO; +with Ada.Integer_Text_IO; use Ada.Integer_Text_IO; + +procedure Item_Fits_Test is + subtype NonZero_Int is Integer range 1 .. Integer'Last; + + function Item_Fits_Exactly_In_Packaging(Height : NonZero_Int) return Boolean is + begin + -- No need to check for zero; the subtype guarantees it's non-zero + return 1000 mod Height = 0; + end Item_Fits_Exactly_In_Packaging; + + procedure Test_Item_Fits_Exactly_In_Packaging_If_Divides_1000 is + Result : Boolean; + begin + Result := Item_Fits_Exactly_In_Packaging(1); + pragma Assert(Result, "1 should divide 1000 evenly"); + Put_Line("Test 1 passed"); + + Result := Item_Fits_Exactly_In_Packaging(3); + pragma Assert(not Result, "3 should not divide 1000 evenly"); + Put_Line("Test 3 passed"); + + Result := Item_Fits_Exactly_In_Packaging(25); + pragma Assert(Result, "25 should divide 1000 evenly"); + Put_Line("Test 25 passed"); + + Result := Item_Fits_Exactly_In_Packaging(999); + pragma Assert(not Result, "999 should not divide 1000 evenly"); + Put_Line("Test 999 passed"); + + Result := Item_Fits_Exactly_In_Packaging(1000); + pragma Assert(Result, "1000 should divide 1000 evenly"); + Put_Line("Test 1000 passed"); + + -- This would cause a compile-time error if uncommented: + -- Result := Item_Fits_Exactly_In_Packaging(0); + -- Error: constraint error, 0 not in range 1 .. Integer'Last + end Test_Item_Fits_Exactly_In_Packaging_If_Divides_1000; + +begin + Test_Item_Fits_Exactly_In_Packaging_If_Divides_1000; + Put_Line("All tests completed successfully!"); +end Item_Fits_Test; +``` + +# Unresolved questions +[unresolved-questions]: #unresolved-questions + +The details of how the coercions would be implemented are only vaguely understood by the RFC author +and compiler experts may have different perspectives on how the details would best work out. Take +the draft implementation thoughts above with a grain of salt. + +The alternatives presented above appear inferior but only slightly so - we should carefully consider +which strategy to apply here, especially if there appear corner cases not yet explored, where the +different approaches may show off their respective strengths. + +# Future possibilities +[future-possibilities]: #future-possibilities + +If we accept that `NonZero` deserves to be implicitly coerced from non-zero values of `T`, the +experience from implementing and stabilizing this may offer valuable insights for how deeply and +how explicitly/implicitly to integrate other bounded/restricted types such as +`Bounded` with custom minimum and maximum values or other types of refinement. +This is out of scope of this RFC. Related discussions: +* [Is there a type like `std::num::NonZero*` but for other values than zero?](https://www.reddit.com/r/rust/comments/x0lwxt/is_there_a_type_like_stdnumnonzero_but_for_other/) +* [Range types for integers (or refinement types?)](https://github.com/rust-lang/rfcs/issues/671) +* [Pattern types MVP](https://github.com/rust-lang/rust/pull/107606) \ No newline at end of file From 75f32cf2494111ab0acc98cdae04fc38ebbdfac3 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 7 Mar 2025 11:22:38 +0200 Subject: [PATCH 02/12] 0000 -> 3786 --- ...t-nonzero-ergonomics.md => 3786-const-nonzero-ergonomics.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename text/{0000-const-nonzero-ergonomics.md => 3786-const-nonzero-ergonomics.md} (99%) diff --git a/text/0000-const-nonzero-ergonomics.md b/text/3786-const-nonzero-ergonomics.md similarity index 99% rename from text/0000-const-nonzero-ergonomics.md rename to text/3786-const-nonzero-ergonomics.md index f3520367751..8a1ca4b83a4 100644 --- a/text/0000-const-nonzero-ergonomics.md +++ b/text/3786-const-nonzero-ergonomics.md @@ -1,6 +1,6 @@ - Feature Name: `const-nonzero-ergonomics` - Start Date: 2025-03-07 -- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000) +- RFC PR: [rust-lang/rfcs#3786](https://github.com/rust-lang/rfcs/pull/3786) - Rust Issue: none # Summary From b001b8e7727239f2eead721fcd497477f59733dd Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 7 Mar 2025 11:51:50 +0200 Subject: [PATCH 03/12] Add reference to pattern types as prior art. --- text/3786-const-nonzero-ergonomics.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/text/3786-const-nonzero-ergonomics.md b/text/3786-const-nonzero-ergonomics.md index 8a1ca4b83a4..f7d48b6242c 100644 --- a/text/3786-const-nonzero-ergonomics.md +++ b/text/3786-const-nonzero-ergonomics.md @@ -350,6 +350,10 @@ begin end Item_Fits_Test; ``` +Pattern types [rust#123646](https://github.com/rust-lang/rust/issues/123646) are a generalized form +of the concept underpinning `NonZero` and may offer a generalized solution to the problems described +in this RFC, if they are adopted into Rust. + # Unresolved questions [unresolved-questions]: #unresolved-questions From 2f3ff056b3b92da346f6c51ebd03798f883ff994 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 10 Mar 2025 09:22:19 +0200 Subject: [PATCH 04/12] Constrain behavior to literals only; constants out of scope --- text/3786-const-nonzero-ergonomics.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/text/3786-const-nonzero-ergonomics.md b/text/3786-const-nonzero-ergonomics.md index f7d48b6242c..2d626c38736 100644 --- a/text/3786-const-nonzero-ergonomics.md +++ b/text/3786-const-nonzero-ergonomics.md @@ -11,7 +11,7 @@ type is only seamlessly usable if all APIs with non-zero semantics use this type to/from conversion at any API boundary that differs in its use of `NonZero`. The burden of these conversions is especially heavy in tests and examples. This RFC proposes new -coercions to facilitate implicit conversion to `NonZero` from integral constants, simplifying usage +coercions to facilitate implicit conversion to `NonZero` from integer literals, simplifying usage in tests and examples, where succinctness and readability are paramount. # Motivation @@ -21,7 +21,7 @@ Using `NonZero` to express non-zero semantics is valuable because it leads to cl removes the need for some runtime zero-checks and creates potential for niche optimizations. In typical logic code, `NonZero` values are validated early in the process, such as when parsing -user input. We start with a value of integral type `T` which may or may not be zero, and we parse +user input. We start with a value of integer type `T` which may or may not be zero, and we parse this into a `NonZero` - if this succeeds, we know it is not zero and can skip zero-checks in any further calls, as long as the API surface uses `NonZero`. @@ -84,11 +84,11 @@ creates a difference between the two categories of usage: `NonZero`, giving a false impression of API complexity and discouraging API authors from using `NonZero` despite its advantages.** -In test and example code this commonly occurs with integral constants and literals. The values are +In test and example code this commonly occurs with integer literals. The values are known and can be validated at compile time to be non-zero and within expected bounds. -This RFC proposes that we omit the ceremony with constants, allowing implicit coercion of non-zero -integral constants and literals to `NonZero`, thereby encouraging Rust crate authors to +This RFC proposes that we omit the ceremony with literals, allowing implicit coercion of non-zero +integer literals to `NonZero`, thereby encouraging Rust crate authors to make more extensive use of `NonZero`, which they may today choose to avoid due to the extra cruft it adds to tests and examples. @@ -171,8 +171,8 @@ fn item_fits_exactly_in_packaging(height: NonZero) -> bool { ``` When writing test or example code and using hardcoded constants, you can omit the conversion into -`NonZero` - it is done implicitly at compile time. This only works with constant values (either -`const` variables or literals like `1234`). +`NonZero` - it is done implicitly at compile time. This only works with integer literals +like `1234`. ```rust ignore #[test] @@ -201,7 +201,7 @@ This can likely be implemented as a new coercion, consisting approximately of the following processing steps: 1. During type checking (`rustc_hir_typeck/src/coercion.rs:coerce()`), we detect the coercion - opportunity from an integral constant (literal or variable, explicitly typed or inferred) to + opportunity from an integer literal (either explicitly typed or inferred) to a `NonZero` with a potentially compatible `T`. At this stage, we only have types and do not yet have values (which might be zero or out of bounds). 1. If the types are potentially compatible, we emit this as a new `Adjustment::NonZero` variant. From 19dacd14c3fdba668c2e6ba0aa0b05cc2e064687 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 10 Mar 2025 09:33:27 +0200 Subject: [PATCH 05/12] Replace reference-level explanation with more appropriate text instead of talking about implementation --- text/3786-const-nonzero-ergonomics.md | 30 ++++++++------------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/text/3786-const-nonzero-ergonomics.md b/text/3786-const-nonzero-ergonomics.md index 2d626c38736..209b5e8862e 100644 --- a/text/3786-const-nonzero-ergonomics.md +++ b/text/3786-const-nonzero-ergonomics.md @@ -197,23 +197,15 @@ of constant values that would typically happen elsewhere in real code. # Reference-level explanation [reference-level-explanation]: #reference-level-explanation -This can likely be implemented as a new coercion, consisting approximately of -the following processing steps: - -1. During type checking (`rustc_hir_typeck/src/coercion.rs:coerce()`), we detect the coercion - opportunity from an integer literal (either explicitly typed or inferred) to - a `NonZero` with a potentially compatible `T`. At this stage, we only have types and do not - yet have values (which might be zero or out of bounds). -1. If the types are potentially compatible, we emit this as a new `Adjustment::NonZero` variant. -1. During MIR building, when the constant values are available, we apply the adjustment and perform - the "is it zero" and "is it in bounds of T" check, before emitting MIR for something like - `NonZero::new_unchecked(123)`. -1. If at this stage we detect an invalid value (zero or out of bounds), we emit a compiler - diagnostic and give up on this coercion. - -The situation before this change would be that the compilation fails at the type check stage -because `u32 != NonZero`, so this should not be breaking any existing code, only enabling -new capabilities. +Integer literals (e.g., `123`, `0xFF`, `0_u32`) are implicitly coerced to `std::num::NonZero` +(`T` being `u8`, `u32`, etc.) if all of the following are true: + +* The value of the literal is not 0. +* The value of the literal fits within `T`’s range (e.g., `300` fails for `NonZero`). +* The target type is explicitly `NonZero` or inferred as such. + +The coercion happens at compile time, with the emitted code being the equivalent of +`const { NonZero::new(literal).unwrap() }` for valid cases, with no runtime checks. # Drawbacks [drawbacks]: #drawbacks @@ -357,10 +349,6 @@ in this RFC, if they are adopted into Rust. # Unresolved questions [unresolved-questions]: #unresolved-questions -The details of how the coercions would be implemented are only vaguely understood by the RFC author -and compiler experts may have different perspectives on how the details would best work out. Take -the draft implementation thoughts above with a grain of salt. - The alternatives presented above appear inferior but only slightly so - we should carefully consider which strategy to apply here, especially if there appear corner cases not yet explored, where the different approaches may show off their respective strengths. From 6e48c32e4537643ebf7dcc1ea9d9370c1dd6d723 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 10 Mar 2025 10:10:58 +0200 Subject: [PATCH 06/12] Clarify proposed behavior regarding typed integers (`0u32`) and const folding --- text/3786-const-nonzero-ergonomics.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/text/3786-const-nonzero-ergonomics.md b/text/3786-const-nonzero-ergonomics.md index 209b5e8862e..497fa7e1ae9 100644 --- a/text/3786-const-nonzero-ergonomics.md +++ b/text/3786-const-nonzero-ergonomics.md @@ -197,16 +197,37 @@ of constant values that would typically happen elsewhere in real code. # Reference-level explanation [reference-level-explanation]: #reference-level-explanation -Integer literals (e.g., `123`, `0xFF`, `0_u32`) are implicitly coerced to `std::num::NonZero` +Integer literals (e.g., `123`, `0xFF`) are implicitly coerced to `std::num::NonZero` (`T` being `u8`, `u32`, etc.) if all of the following are true: * The value of the literal is not 0. * The value of the literal fits within `T`’s range (e.g., `300` fails for `NonZero`). * The target type is explicitly `NonZero` or inferred as such. +* A single integer type can be identified for the coercion. + * If the literal is untyped (e.g. `123`), the `T` must be unambiguously resolved from the target `NonZero` type. + * If the literal is typed (e.g. `123u32`), the `T` in the `NonZero` must either be inferred or match the literal's type. The coercion happens at compile time, with the emitted code being the equivalent of `const { NonZero::new(literal).unwrap() }` for valid cases, with no runtime checks. +The coercion only happens when the source type is identified as an integer literal at the type check +stage. This implies that the coercion does not apply to expressions, even those that combine literals. + +```rust ignore +fn foo(count: NonZero) { } + +foo(123); // OK +foo(0x11); // OK +foo(123_u8); // OK + +foo(0); // Error - value cannot be zero. +foo(300); // Error - out of bounds of u8. +foo(123_usize); // Error - literal has non-matching type. +foo(123 - 1); // Error - coercion not applied for expressions. + +const MAGIC_VALUE: NonZero = 123; // OK - coercion logic is same as when calling a fn. +``` + # Drawbacks [drawbacks]: #drawbacks From ab5a452a3a741cfc2875c28f6ebbbb812e6a8411 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 10 Mar 2025 10:24:25 +0200 Subject: [PATCH 07/12] Improve wording --- text/3786-const-nonzero-ergonomics.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/3786-const-nonzero-ergonomics.md b/text/3786-const-nonzero-ergonomics.md index 497fa7e1ae9..b29ed7c9909 100644 --- a/text/3786-const-nonzero-ergonomics.md +++ b/text/3786-const-nonzero-ergonomics.md @@ -205,7 +205,7 @@ Integer literals (e.g., `123`, `0xFF`) are implicitly coerced to `std::num::NonZ * The target type is explicitly `NonZero` or inferred as such. * A single integer type can be identified for the coercion. * If the literal is untyped (e.g. `123`), the `T` must be unambiguously resolved from the target `NonZero` type. - * If the literal is typed (e.g. `123u32`), the `T` in the `NonZero` must either be inferred or match the literal's type. + * If the literal is typed (e.g. `123u32`), the `T` in the `NonZero` must either explicitly or via inference match the literal's type. The coercion happens at compile time, with the emitted code being the equivalent of `const { NonZero::new(literal).unwrap() }` for valid cases, with no runtime checks. From c335cb608a2f346e0b3c07451d2fac723d6ad512 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 24 Mar 2025 09:06:17 +0200 Subject: [PATCH 08/12] Tighten suffix handling and clarify negation handling --- text/3786-const-nonzero-ergonomics.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/text/3786-const-nonzero-ergonomics.md b/text/3786-const-nonzero-ergonomics.md index b29ed7c9909..cecc6fcb728 100644 --- a/text/3786-const-nonzero-ergonomics.md +++ b/text/3786-const-nonzero-ergonomics.md @@ -197,35 +197,35 @@ of constant values that would typically happen elsewhere in real code. # Reference-level explanation [reference-level-explanation]: #reference-level-explanation -Integer literals (e.g., `123`, `0xFF`) are implicitly coerced to `std::num::NonZero` -(`T` being `u8`, `u32`, etc.) if all of the following are true: +Untyped integer literals (e.g., `123`, `0xFF` but not `123_u32`) are implicitly coerced +to `std::num::NonZero` (T` being `u8`, `u32`, etc.) if all of the following are true: * The value of the literal is not 0. * The value of the literal fits within `T`’s range (e.g., `300` fails for `NonZero`). * The target type is explicitly `NonZero` or inferred as such. -* A single integer type can be identified for the coercion. - * If the literal is untyped (e.g. `123`), the `T` must be unambiguously resolved from the target `NonZero` type. - * If the literal is typed (e.g. `123u32`), the `T` in the `NonZero` must either explicitly or via inference match the literal's type. +* The type `T` is unambiguously resolved from the target `NonZero` type. The coercion happens at compile time, with the emitted code being the equivalent of `const { NonZero::new(literal).unwrap() }` for valid cases, with no runtime checks. -The coercion only happens when the source type is identified as an integer literal at the type check -stage. This implies that the coercion does not apply to expressions, even those that combine literals. +The coercion is allowed when the source is an integer literal (`123`) or an integer literal +negation expression (`-123`). The coercion does not apply to other expressions besides unary +negation, even if the expressions combine literals (e.g. `123 + 456` does not qualify). ```rust ignore -fn foo(count: NonZero) { } +fn foo(count: NonZero) { } foo(123); // OK +foo(-123); // OK foo(0x11); // OK -foo(123_u8); // OK foo(0); // Error - value cannot be zero. -foo(300); // Error - out of bounds of u8. +foo(300); // Error - out of bounds of i8. +foo(123_i8); // Error - only untyped literals accepted. foo(123_usize); // Error - literal has non-matching type. foo(123 - 1); // Error - coercion not applied for expressions. -const MAGIC_VALUE: NonZero = 123; // OK - coercion logic is same as when calling a fn. +const MAGIC_VALUE: NonZero = 123; // OK - coercion logic is same as when calling a fn. ``` # Drawbacks From bb973f45739eb76031e9c1e00f9aa143ce8566c0 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 31 Mar 2025 10:19:25 +0300 Subject: [PATCH 09/12] Update text/3786-const-nonzero-ergonomics.md Co-authored-by: teor --- text/3786-const-nonzero-ergonomics.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/3786-const-nonzero-ergonomics.md b/text/3786-const-nonzero-ergonomics.md index cecc6fcb728..8d04e2ebdac 100644 --- a/text/3786-const-nonzero-ergonomics.md +++ b/text/3786-const-nonzero-ergonomics.md @@ -198,7 +198,7 @@ of constant values that would typically happen elsewhere in real code. [reference-level-explanation]: #reference-level-explanation Untyped integer literals (e.g., `123`, `0xFF` but not `123_u32`) are implicitly coerced -to `std::num::NonZero` (T` being `u8`, `u32`, etc.) if all of the following are true: +to `std::num::NonZero` (`T` being `u8`, `u32`, etc.) if all of the following are true: * The value of the literal is not 0. * The value of the literal fits within `T`’s range (e.g., `300` fails for `NonZero`). From 3e7278780a6721d8b6e6e5e3fb98ec32ef49d255 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 11 Apr 2025 11:21:56 +0300 Subject: [PATCH 10/12] Add counterexample for values being used instead of literals --- text/3786-const-nonzero-ergonomics.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/text/3786-const-nonzero-ergonomics.md b/text/3786-const-nonzero-ergonomics.md index 8d04e2ebdac..0091d8fc0a9 100644 --- a/text/3786-const-nonzero-ergonomics.md +++ b/text/3786-const-nonzero-ergonomics.md @@ -226,6 +226,9 @@ foo(123_usize); // Error - literal has non-matching type. foo(123 - 1); // Error - coercion not applied for expressions. const MAGIC_VALUE: NonZero = 123; // OK - coercion logic is same as when calling a fn. + +let i: i8 = 123; +foo(i); // Error - the coercion only applies to literals, not for values in general. ``` # Drawbacks From bec3ca4ef9ba56f1d78cbe57af05e936a9f61c88 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 11 Apr 2025 11:25:40 +0300 Subject: [PATCH 11/12] More specific wording --- text/3786-const-nonzero-ergonomics.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/3786-const-nonzero-ergonomics.md b/text/3786-const-nonzero-ergonomics.md index 0091d8fc0a9..33391c5c03d 100644 --- a/text/3786-const-nonzero-ergonomics.md +++ b/text/3786-const-nonzero-ergonomics.md @@ -228,7 +228,7 @@ foo(123 - 1); // Error - coercion not applied for expressions. const MAGIC_VALUE: NonZero = 123; // OK - coercion logic is same as when calling a fn. let i: i8 = 123; -foo(i); // Error - the coercion only applies to literals, not for values in general. +foo(i); // Error - the coercion only applies to literals and `i` is not a literal. ``` # Drawbacks From 7da7c135cfe84ec6cae4c01faa4e3b88c873db6e Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 11 Apr 2025 11:26:15 +0300 Subject: [PATCH 12/12] Simplify counterexample --- text/3786-const-nonzero-ergonomics.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/3786-const-nonzero-ergonomics.md b/text/3786-const-nonzero-ergonomics.md index 33391c5c03d..ec7093dc81e 100644 --- a/text/3786-const-nonzero-ergonomics.md +++ b/text/3786-const-nonzero-ergonomics.md @@ -227,7 +227,7 @@ foo(123 - 1); // Error - coercion not applied for expressions. const MAGIC_VALUE: NonZero = 123; // OK - coercion logic is same as when calling a fn. -let i: i8 = 123; +let i = 123; foo(i); // Error - the coercion only applies to literals and `i` is not a literal. ```