This is a slightly more formal version of idea sketched here. cc @scottmcm
Summary
This RFC introduces two new “types”, i__
and f__
, the names of which capture the idea of “int/float, but with unspecified width.” (Mind, I don’t like these names, but ilit
and flit
are worse, imo. Please bikeshed better names.) These types represent an untyped integer/float literal, such as 0
, -42
, and 1.3e-4
. Currently, these expressions do not have a nameable type, which is expressed in error messages as {integer}
and {float}
(mind, I don’t know enough about compiler internals to know if this is the case, but this seems like the most reasonable implementaton). For example:
error[E0308]: mismatched types
--> src/main.rs:3:17
|
3 | let _: () = 0;
| ^ expected (), found integral variable
|
= note: expected type `()`
found type `{integer}`
This RFC proposes to give these types a similar status to !
in stable: they can only be used in the type ascription of a const
binding:
const MASK: i__ = 0b0101_0101;
const PI: f__ = 3.14159265358979323846264338327950288419716939937510;
// forbidden:
const NONE: Option<i__> = None;
type IntLit = i__;
impl Sync for i__ {}
This type has no methods defined and implements no traits for now; instead, it coerces to sized integers and floats, exactly like a literal does, at usage sites. Imagine the following behavior:
use std::mem::size_of_val;
const MASK: ilit = 0b0101_1010;
let foo = MASK; // type is infered as i32
assert_eq!(size_of_val(&foo), 4);
// desugaring
macro_rules! MASK { () => {0b0101_1010} }
let foo = MASK!(); // type is infered as i32
assert_eq!(size_of_val(&foo), 4);
Motivation
Numeric compile-time constants in other some low-level languages are always untyped:
#define MASK 0b01010101 /* C */
const kMask = 0b01010101 // Go
In fact, such behavior is currently achievable in Rust with macros, as described in the above desugaring. However, this is an ugly and unergonomic solution. This proposal provides a way to opt into this behavior.
In general untyped numeric literals are a bad idea, since it can confuse the typechecker, causing it to infer exciting, unexpected things, like calling transmute
without a turbofish. However, it does make some code less painful to write, and is the natural type for bit masks, which casting is line noise more than anything else (this neatly solves some of the problems my above post describes).
If, in the future, we support using this type as a parameter in a const fn
, it would be the natural type for implementing custom literals, like our friend operator "" foo
from C++. While this is beyond the scope of this RFC, I like to imagine traits like
// mod core::ops
trait FromInt {
const fn from_int(literal: i__) -> Self;
}
trait FromFloat {
const fn from_float(literal: f__) -> Self;
}
I could also imagine allowing struct Foo<const N: i__> { .. }
as a natural extension of the current syntax, with the same coercion behavior.
Drawbacks
I don’t hack on rustc
, so I’m not sure how much messing about with the typeck will need to be done to move the literal coersion rules to a nameable type. This also opens us up to the exciting bug that arise in C and Go from having typeless constants, though they would be opt-in, and users would be encouraged to use them sparingly. Custom literals are a questionable feature which, to my knowledge, has never been discussed.
Prior Art
Scala’s dotty compiler has explicit literal types: the type of 1
is 1.type
, which is a subtype of Int
(corresponding to the JVM int
type). In addition, String
literals also have types: "foo".type
, but this is beyond the scope of this proposal. These types are mostly intended to be used in generics. I don’t know of any language that uses a single type for all int/float literals, but I haven’t done any research.
As pointed out, many languages have untyped constants, but this is often opt-out, of opt-out-able at all. I think my proposed opt-in mechanism for untyped constants is not the enormous footgun typeless-by-default is.
Some languages have custom literals, but custom literals are beyond the scope of this proposal outside of future extension.
Unresolved Questions
- Should we decide on a representation for these types, or defer it for later? Right now I imagine the compiler should internally represent them as arbitrary-size numbers in your favorite scheme.
- Should we consider a more granular approach, like Scala’s?
- What should such constants look like in FFI? How should they appear in compiled traits?
Note: this is my first RFC. Let me know if I’ve done anything I should improve! I’ve omitted some sections that I think should be filled out by whatever discussion happens here.