RFC: New range types for Edition 2024

Change the range operators a..b, a.., and a..=b to resolve to new types ops::range::Range, ops::range::RangeFrom, and ops::range::RangeInclusive in Edition 2024. These new types will not implement Iterator, instead implementing Copy and IntoIterator.
Closes #2848
Pre-RFC discussion on IRLO
🖥 Rendered

I personally wouldn't call the old range types "legacy" ranges because, if the new ranges impl IntoIterator, then it makes sense for their IntoIter to be the old range types.

@vitww what do #rit and #rii stand for?

I personally wouldn't call the old range types "legacy" ranges because, if the new ranges impl IntoIterator, then it makes sense for their IntoIter to be the old range types.

This is addressed in the RFC under Use legacy range types as the iterators for the new range types.

I like this. I see one problem, however.  Any crate which provides a container that can be sliced which is not updated will require &container[(a..b).into()].
This could be solved with impl<T: Index<LegacyRange>> Index for T, or by adding an implicit conversion.  I think such is necessary in this case because, even if the user knows what's going on, .into() on indices is really ugly.
Maybe it's also worth a mention that--without some sort of blanket impl--all of stdlib is going to need to implement Index/IndexMut for both.

I like this. I see one problem, however. Any crate which provides a container that can be sliced which is not updated will require &container[(a..b).into()].
This could be solved with impl<T: Index<LegacyRange>> Index for T, or by adding an implicit conversion.

I think it would have to be an implicit conversion since impl<T: ?Sized + Index<LegacyRange>> Index<Range> for T overlaps with existing impl<T: Index<I>, I> Index<I> for Wrapper<T>

What is Wrapper<T> in this context?  Is that a reference to all containers, a specific internal type, or a public type I somehow don't know about?

What is Wrapper<T> in this context? Is that a reference to all containers, a specific internal type, or a public type I somehow don't know about?

just some user-defined type with a type parameter T, e.g.:
pub struct Wrapper<T>(T);

impl<T: Index<I>, I> Index<I> for Wrapper<T> {
    type Output = T::Output;
    fn index(&self, idx: I) -> &Self::Output {
        self.0.index(idx)
    }
}

@programmerjake
Lol yeah.  I think the best part of this discussion is that I wrote one of those this week.  You're right though.
Could the compiler use the fact that it is the compiler to use specialization here?  I dunno what the state of that is these days but it might be better than an implicit conversion.  I hate blanket impls in stdlib, but it's a close race with how much I hate implicit conversions.

@ahicks92 thanks for bringing this up, I hadn't considered the indexing case.
I think many common cases will be covered by SliceIndex, but there will be a lot of collections that will need updates to support the new range types.
I will update the migration and drawbacks sections to cover this.
I would not support adding a blanket impl, specialization, or implicit conversion even if they were possible. I think we should take a different approach:

stabilize the new types as soon as possible
allowing crates to add Index impls before the syntax switch

Well, I do a lot of mathematical code as it is, and even as Rust is today Rust is very verbose a lot of the time for such cases.  That's not really a complaint about Rust as such,  But I don't want to deal with:
c[(a..b).into()].copy_from_slice(c2[(a..b).into()])

Where c is some collection.  I can't find a crate right now but ironically the example that came to mind is ndarray (which sadly uses a different type for slicing and thus isn't applicable) which made me realize that this is actually more general for the case wherein one might be trying to perform operations with ranges where the thing going on is a method call.
The worst practical case of slicing with this proposal would be &c[(a as usize..b as usize).into()], because often one must cast the endpoints of the range.
crates.io is pretty big and pretty full of unmaintained stuff.  I think "let crates start migrating ASAP" is a good point but it doesn't answer what to do about anything older that's not maintained or anything that doesn't notice this is coming in time.  I don't know how much of a problem that would be, and I'm not sure how one would quantify it either.  But that might be a good thing to try to do if it can be done.

I will just mention that (a..b).into() probably won't work unless the only index type is Range - otherwise it will probably fail with a type inference error. We may want to consider adding an inherent method (to_legacy or something) to avoid that issue.

I can't find a crate right now but ironically the example that came to mind is ndarray (which sadly uses a different type for slicing and thus isn't applicable)

In std, the only affected impls would be:

impl ops::Index<ops::Range<usize>> for String (and the respective IndexMut)
impl ops::Index<ops::RangeFrom<usize>> for String (and the respective IndexMut)
impl ops::Index<ops::RangeInclusive<usize>> for String (and the respective IndexMut)
impl ops::Index<ops::RangeFrom<usize>> for CStr

The Index impls for String should probably transition to using SliceIndex like str does. BTreeMap, HashMap, and VecDeque don't support indexing by range at all, and CString and OsString only support RangeFull. BTreeMap::range uses RangeBounds, so no issue there.
So I'd say the only serious one there is CStr, which doesn't seem too bad. I am curious to see how many 3rd-party collections will be affected. I'm not sure how to go about collecting that information.

I am thinking about it like this:

This RFC wants to replace range types in large part because the new ones can be Copy, thus avoiding .clone().
But to do so, it instead exchanges .clone() for .into().

I understand that the other motivation is about having ranges inside other types, but leaving that aside the unstated premise here seems to me to be that we are claiming that working around lack of Copy today is more ubiquitous than needing to convert to legacy types in Rust 2024.  I could believe that is true, my use cases aren't even close to all possible use cases, but not without proof.
I don't consider stdlib to be interesting.  From a language perspective solutions up to and including hacking the compiler for stdlib backward compatibility are feasible.  Solving it is important and interesting but the solution space is large enough there that there is certainly a solution and the question isn't "if", it's "which".

My thoughts:

We need actual data about this. Right now we don't know if this will be an actual issue in practice. The evaluation of std was only an example.
If the issue is widespread, then I agree we should consider doing something more involved.
On the other hand, updating to the new edition is entirely optional. You and others can choose to stay on 2021 if you feel conversion is overly burdensome.

I don't think we can.  One either picks 2024 and converts for 2021 crates, or picks 2021 and converts for 2024 crates.  Right?  So if this is an issue you lose out either way.  Also stay on the old edition doesn't work because eventually the new edition gets something you need.
But as you've already stated data is a good idea.  I'm willing to argue contingent on this being widespread all day long but the entire thing depends on if it's widespread or not.
If we don't have one someone should really figure out how to run a query over all of crates.io, probably via docs.rs, to be like select * where a_range_type in signature.

I don't think we can. One either picks 2024 and converts for 2021 crates, or picks 2021 and converts for 2024 crates. Right?

Not really. For the indexing issue, they already support legacy range types and they can just add impls for the new types. Functions that take a specific range type can use impl Into<Range<_>> to support all editions, with no need for conversion on usage.
The only case where conversion is unavoidable is if you're using the new range types on code expecting old range types. All new code can and should be written to accept both.

I doubt anyone is going to cover both.  That's not a big ask but you then have to teach newbies to the language that due to legacy reasons they'd better.
As far as I know, every other edition has not required supporting the older edition in order to depend on crates supporting the newer one.  Perhaps it has and I've missed it.  But if not, this is establishing the precedent.

I doubt anyone is going to cover both. That's not a big ask but you then have to teach newbies to the language that due to legacy reasons they'd better.

We're not talking about newbies here but library authors who have decided to upgrade their crate to a new edition. Why wouldn't they choose to support both?
Either these are (1) existing crates being updated to support edition 2024 or (2) new crates written after edition 2024. This is less likely for (1) as they don't need to remove existing impls for the legacy types, only add new impls for the new types. (2) are more likely to lack support for the older types, just by accident.
For (1) you can always choose not to update, but for (2) you have fewer options. But in both cases, it's highly likely that they will accept contributions or issues updates on request to support the legacy types, given they are being actively developed / maintained.

As far as I know, every other edition has not required supporting the older edition in order to depend on crates supporting the newer one.

This doesn't change that. We're not talking about these crates being unable to interact. We're just talking about some explicit conversions being necessary, or preferably that the library explicitly supports both legacy and new ranges.
All that said, it would be a good idea to add a clippy lint to warn when public APIs are restricted to only new or only legacy range types.

Awfully disruptive change here.  We do indexing & iterating all the time, so even mild hiccups create chaos.  It's conversely relatively rare range types appear inside a Copy type.
We'll have similar "Copy vs Iterator" problems elsewhere outside std.  We'd avoid some chaos if we address the two roles of Copy directly, for which seferal options exist:
We could change Copy itself to make implicit copies optional, while still promissing bitwize copies.
pub trait Copy {
   const IMPLICIT_COPY: bool = true;
}

#[derive(Clone,Copy)]
pub struct Cpy<T: Copy>(pub T);

At this point all range types become Copy<IMPLICIT_COPY=false>, so no implicit copies, but Cpy(range_expr) provides implicit copies.
We could provide another unsafe impl Copy mechanism to promise soundness, with which we provide some wrapper type that permits copies.
pub unsafe trait CopySafe: Clone {}
// unsafe impl CopySafe for each range type

#[derive(Clone,...)]
pub struct Cpy<T: CopySafe>(pub T);

unsafe impl<T: CopySafe> Copy for Cpy<T> {}

Again Cpy(range_expr) provides implicit copies, but you'd now need it in structs, not required by the associated bool.

The "add iterator methods to range" thing here seems like it solves the iterating churn. In my experience you either iterate a range in a for loop (handled by IntoIterator) or use the iterator methods (handled by making them inherent).  This RFC seems like it mostly leaves that sort of code alone.

fn foo<I: Iterator<Item=usize>>(i: &mut I)

fn bar<I>(i: &mut I)
where for<'a> &'a mut I: Iterator<Item=usize>

It'll require a major version change for rand, so then incompatible traits mess.  etc

IntoIterator is often either not used or unusable.
fn foo<I: Iterator<Item=usize>>(i: &mut I)

fn bar<I>(i: &mut I)
where for<'a> &'a mut I: Iterator<Item=usize>

For functions like this, you can just call .into_iter() at the usage site:
foo(&mut (0..5).into_iter());
bar(&mut (0..5).into_iter());

It should be possible to implement this transformation as part of cargo fix --edition.

It'll require a major version change for rand, so then incompatible traits mess. etc.

Can you specify exactly where in rand this will cause a breaking change?

We need actual data about this. Right now we don't know if this will be an actual issue in practice. The evaluation of std was only an example.

Github search for Index<Range<_>> and friends yields 688 files, almost all of which appear to be true matches (at least on the first 5 pages). No idea how many of these are published library crates.

isn't it quite uncommon to have functions that take &mut Iter though? Typically you'd call .by_ref() at call site instead.

But to do so, it instead exchanges .clone() for .into().

A big difference to me is that ranges not implementing Copy also affects types that contain a range, making it viral.  For a good number of libraries, ranges are a fundamental type and the current design pushes them to implement their own versions.

It occurs to me that a "this can secretly be copy" marker type which wraps an inner type to make it Copy could be done today without further help from the language.  I believe UnsafeCell would allow for it, but if not then there are other tricks like aligned u8 arrays and pointer casts.  If ranges guaranteed that they can be Copy, but aren't for legacy reasons, this could be even delegated to a library.  By not implementing Iterator on such a wrapper, and instead implementing IntoIterator, wouldn't that solve this without much help from the language other than a trivial layout guarantee that's upheld anyway?
I guess "use a library" isn't ideal but at the same time neither is this, and implementing Deref and DerefMut would handle a lot of the code churn for those wishing to adopt.  If nothing else the churn is limited to internals of crates, not the public API boundary.

Copy is a good motivation. Fixing for i in x..=y performance is I would argue an even better one.
The performance of the current RangeInclusive when used as an external iterator is bonkers. This leads to inquiries such as:

A Call for Proposals for 2024
Why is iterator so much faster?
Why isn't the for loop optimized better (in this one example)?
Why does iteration over an inclusive range generate longer assembly in Rust than in C++?

Where the only response so far is: "Oh yeah, sorry, RangeInclusive doesn't optimize well, better switch to Range" which is really NOT a response I like to give in Rust.
And a bunch of issues that led to nowhere, such as:

https://github.com/rust-lang/rust/issues/45222
https://github.com/rust-lang/rust/issues/75035

A lot of people tried their hands at fixing this performance issue and by now I think the only reasonable conclusion is that it's very unlikely that after years of effort we'll finally figure out how to tame it. Other than by separating RangeInclusive and its iterator type, of course.

The cause of the slow-down is the mutation of the flag within the loop: a well-known optimization issue with chain-like iterators. The iterator of RangeInclusive should attempt to avoid such a mutation as much as possible, which can be done at least for all non-full ranges.
It's notable that as long as end can be incremented, or start can be decremented, it's relatively trivial to switch to a semi-exclusive range, instead of an inclusive range, when it comes to iterating:
enum RangeInclusiveIter<T> {
    //  Contains a..(b+1) from a..=b.
    Exclusive { start: T, end: T },
    //  Contains (a-1)..b from a..=b, requires +1 at each step.
    Shifted { start: T, end: T },
    //  Contains (a, b) from a..=b.
    Full { start: T, end: T, exhausted: bool },
}

Of course, code-generation tests should be performed -- possibly reusing the examples provided in the above links -- to see that code generation actually benefits from those shenanigans.

@matthieu-m I mention that enum-based approach in Future possibilities, and even provide a playground demo.
Another option for fixing the performance is to use the next larger integer type (on just one of the fields so same size). But that would require adding an associated type to Step (or introducing a new trait for that) so that each Implementor can specify a separate iterator type.

There isn't always a next (native) integer type.  While u128 compiles on all platforms, it does not necessarily compile efficiently, and that still would leave open the question of what one does if the user wants to iterate over u128 ranges.  In order to support that, it'd be necessary to carve out at least one implementation from an otherwise generic scenario.  RPITIT may solve this to at least some extent but I think that requires named existential types.
Enum-based approaches likely do not optimize well in many cases because they rely on having at least tons of inlining to see through the branches.  I would presume other methods such as map and such could be optimized today without major type changes.
A "fix" here would be simply adding a method to RangeInclusive which tries to convert it to a normal boring Range, e.g. fn to_exclusive(&self)->Option<Range<T>> etc.  Again, I don't think such a solution would need a new type.

There isn't always a next (native) integer type.

Yes, I know. That why I said

each Implementor can specify a separate iterator type

I meant that it would look like this:
trait StepInclusive: Step {
    type StartEnd;
}

impl StepInclusive for u8 {
    type StartEnd = (u8, i16);
}
// ...
impl StepInclusive for i16 {
    type StartEnd = (i16, i32);
}
// ...
impl StepInclusive for u128 {
    type StartEnd = (u128, u128, bool);
}
// ...

That said, my comment on size was incorrect. Due to alignment it would actually be double the size of the range type, rather than just 1.5x the size.

I don't think performance of RangeInclusive iteration is the strongest argument for this change.
In my opinion, the strongest argument is consistency. With the changes in this proposal:

all range types will implement Copy
all range types will have only public fields
both double-ended ranges will have the same size

You need &mut Iterator so callees handle lifetimes correctly sometimes.  It helps manage the Item lifetimes too.
fn run(self, r: Range<..>) is broken by this.  Yes, all this code could be upgraded, but we've breakage during the upgrade.
It's considerable pain for a change that's orthogonal to the real problem:  Seperating the two roles of Copy.

It occurs to me that a "this can secretly be copy" marker type which wraps an inner type to make it Copy could be done today without further help from the language.

How?  An aligned [u8; size_of::()]` works maybe, maybe even some union, but that's pretty annoying.
We've issues like https://github.com/rust-lang/rust/issues/25053 and https://github.com/rust-lang/rust/issues/62835 in which everyone thinks the dangerous inner type requires Copy.  I doubt they'd say this if you could make the outer type Copy easily.
Yes, we should've a crate that provides the copy wrapper now, but I think rustc should provide something like unsafe impl Copy eventually.

A lot of people tried their hands at fixing this performance issue

You've avoided specialization via the Idx: Step bound on RangeInclusive: IntoIterator.  Your code should optimize because it'll write the loop twice, once efficently and once inefficently, but the inefficent one never runs.  Assuming this, RangeInclusive: IntoIterator could do that conversion at runtime too:
enum RangeInclusiveInner<T> {
    //  Contains a..b from a..=b before knowing T: Step
    Initial { start: T, end: T },
    //  Contains a..(b+1) from a..=b.
    Exclusive { start: T, end: T },
    //  Contains (a-1)..b from a..=b, requires +1 at each step.
    Shifted { start: T, end: T },
    //  Contains (a, b) from a..=b.
    Full { start: T, end: T, exhausted: bool },
}
pub struct RangeInclusive<T>(RangeInclusiveInner<T>);

Also RangeInclusiveIter cannot shed current functionality:  You'd need RangeInclusiveIter<T>: PartialEq+Hash. Your RangeInclusiveIter::{start,end} becomes -> Idx too.
It's true seperation of ..= and the iterator permit (a..=b).{start,end} to be values not fns, which avoids requiring T: Clone everywhere.  Yeah that's something.

adding a method to RangeInclusive which tries to convert it to a normal boring Range

Yeah it's manual but gives optimal code.
fn to_exclusive(&self)->Option<Range<T>>  // fails if end == MAX
fn to_shifted(&self)->Option<Range<T>>  // fails if start == MIN
fn to_larger<U>(&self)-> Range<U>  // to_larger::<usize> being the standard usage
where U: From<T>+Step+TryInto<T>

Anyways..
This all looks half baked without really assessing the breakage, including how ecosystem trait changes propogate.  unsafe impl Copy or similar looks very natural, and solves the largest demand here.  RangeInclusive performance looks important, but orthogonal.

fn run(self, r: Range<..>) is broken by this.

Broken is a strong word, but I get what you mean. It will require either a conversion at the usage site or changing the library to have fn run(self, r: impl Into<Range<..>>) (which is fully backwards compatible BTW).
I will also point out that APIs which take a raw range like this are pretty rare and should probably take impl RangeBounds instead.

Yes, all this code could be upgraded, but we'll have trait breakage during the upgrade.

What do you mean by "during the upgrade"? If you follow the suggested migrations before upgrading edition, you'll experience no breakage because all of the suggested migrations are backwards compatible.

@burdges do you have an actual example? The lifetime of iterator items is completely independent of the lifetime of the &mut self in Iterator::next(). This is the main reason why IterMut is sometimes difficult to implement in safe Rust.

a@burdges
One can get a Copy type via carefully ensuring that alignments match, then using a u8 array and pointer casting.  The only problem is that I don't think the size and alignment can be determined at runtime right now.  Thinking about this more it perhaps falls down due to the requirement of being generic, since we don't yet have const expressions, but for one concrete type it's not a problem disregarding that it's sort of UB-adjacent without the extra guarantee.

@burdges
You never answered this question

It'll require a major version change for rand, so then incompatible traits mess. etc.

Can you specify exactly where in rand you're referring to?

@RalfJung
To me the important point is it can be done, not how to do it, so I won't derail further with concrete proposals.  Still, this is why I hope we get a simple to understand official model of unsafe soon.  I'm not going to argue against people involved in that effort, though I will say that I find this surprising; I would have thought MaybeUninit<u8> should never be needed because one can start with a zeroed value and all bit patterns for u8 are valid, and by the time the pointer/reference is materialized it can be guaranteed to point at initialized bytes valid for the type.

You're using a naive model of how memory works in languages like Rust or C, leading you to wrong conclusions. It's off-topic here to further go into details, so I will leave you with links to some blog posts:

about uninit memory
about pointers

For further discussion, please see the t-opsem zulip and our issue tracker.

@RalfJung
I'll put some stuff together perhaps and reach out via a different avenue if I feel it's necessary. Not sure I can do so via Zulip because I am blind and last I checked their accessibility was...interesting.  I know that what I am suggesting is wrong in the C case and believe I can explain why.  There is insufficient info in the language reference to provide a workable model for Rust.  Feel free to reach out if you think I'm an interesting case study of how unsafe understanding in Rust goes wrong; I have a decade of C/C++ experience.

@RalfJung I suppose unsafe impl Copy makes sense though.  It's seemingly equivelent to Copy<IMPLICIT_COPY=false> on intermediate types.  Above: https://github.com/rust-lang/rfcs/pull/3550#issuecomment-1873330594

Can you specify exactly where in rand you're referring to?

I'm wrong that rand::Rng::get_range breaks per se, because they can simply impl SampleRange in rand, but..

If you follow the suggested migrations before upgrading edition, you'll experience no breakage because all of the suggested migrations are backwards compatible.

Yes & no.  There are many reasons critical code does not upgrade specific dependencies for long periods of time, so when you cause breakage anytime you make people upgrade traits used across a diverse set of crates.
In particular, older rand::Rngs still break, so this forces rand upgrades, which many resist due to rand's history and complexity.  We'll thus have incompatible Rng traits in larger projects for a while.  We already have the multiple trait issue, including Rng, so editions being more trait breakage maybe just expected, but..
It's not a free, or even cheap, change for the ecosystem, so whatever happens should be really the right thing and worth the effort.  It's otoh basically free to address Copy's dual role directly though, assuming it cannot sneak in unsoundness somewhere.

I will also point out that APIs which take a raw range like this are pretty rare and should probably take impl RangeBounds instead.

Yes fair enough, or provide their own like rand, but already Index does not do so, due to it being a type paramater etc.

unsafe impl Copy makes perfect sense and I sometimes wished it existed.

Addendum: this is modulo the issues brought up in some prior discussions around UnsafeCell: Copy, that there might be code out there that assumes that Copy implies Freeze. I hate the fact that such code exists but I can't deny it.

@RalfJung
GitHub works; Zulip might work well enough but checking takes time, new chat/collaboration platforms are nontrivial.
There's no point continuing down this avenue until I have time to think about what I might have to say that hasn't been said already, though.  This is the second time I've managed to end up in a derailed unsafe-related discussion on an RFC thread, and the rest of you aren't the only ones frustrated by it.  It is now time for me to find out if my criticisms have been voiced or not.

I feel there is a clash of priorities, here, between backwards-compatibility and clean design.
There are 3 concerns for backward/forward-compatibility:

The ability to upgrade to edition 2024 painlessly.
The ability to use crates of prior edition from edition 2024 painlessly.
The ability to use crates of edition 2024 from prior editions painlessly.

I am not necessarily concerned by (1), so long as cargo can fix the code during the upgrade, but (2) and (3) are definitely critical to avoiding an ecosystem lock-up. Unless perfect backward/forward-compatibility can be ensured, we would likely need a quantification of the number of crates exposing Range (& co) in their API, and ideally how long ago they were updated -- as a potential hint to their author's responsiveness should their code need be adapted.
Note: proposals of compiler magic to ensure perfect backward/forward-compatibility are quite likely dead on arrival, the compiler team is generally looking at reducing the amount of magic, not increasing it.
On the other hand, it's also critical to consider what a clean design should be. If we're about to have a disruption -- even mild -- we really should endeavor to make it worth it... and aim to not have a need for further disruption in the future.
As far as I am concerned, a clean design for Ranges -- with no concern for compatibility -- entails at least:

Copy.
Separating the Range and its Iterator type; it would of course implement IntoIterator.
Enabling functional "pipelines" on Ranges, such as Range::map yielding a Range, etc...

Note: interestingly, with Range implementing IntoIterator and Range::map yielding a Range a for i in (0..3).map(...) may still compile, so long as the mapped type implements Step, though it may produce a different result. For this reason, it may be better to postpone all such functions for 1 year, to ensure as smooth a transition as possible.

With regard to compatibility, I would note that the best time for any slight breaking change is now. The more we wait, the greater the impact in number of codebases and users affected. With that said, if we can minimize or even eliminate breakage, it's all the better of course.

Regarding map, I posted the following on IRLO.

That worries me, in some cases. If Range is not an iterator, I might expect Range::map to be more like (10..20).map(|x| x * 2) → 20..40, like Option::map.

I don't think confusion is likely given how common this already is. Plus, it's pretty easy to tell based on the function signature what is going on and this is easy to document.

(And, practically, I'd like a rule that limits the copying as much as possible. There are just so many Iterator methods! I actually rather like rev: Range<T> → RevRange<T>, though, where RevRange<T> could again just be IntoIterator, not Iterator.

I agree that we should limit it to the most useful / common cases.
I don't really see any benefit to returning an IntoIterator type instead of Iterator. Feels kinda like if drain returned an IntoIterator
What else would that type be able to do? I guess it could implement RangeBounds but reverse bounds don't really fit and mapped bounds can't be used the same.
Making it return an iterator has a few benefits:

easier to implement
matches existing behavior
less code churn
can chain iterator methods right off it

The last one is really important to avoid needing into_iter in the middle of a chain:
(3..11).map(|x| x*2).rev()
// vs
(3..11).map(|x| x*2).into_iter().rev()

(0..LEN).map(|_| Default::default()).collect()
// vs
(0..LEN).map(|_| Default::default()).into_iter().collect()

(0..max_width_index_in_input)
    .rev()
    .skip_while(.....)
// vs
(0..max_width_index_in_input)
    .rev()
    .into_iter()
    .skip_while(.....)

I think changing the meaning of (1..11).map(...) is a pretty big hazard. There is a lot of existing code, documentation, etc that uses it in the Iterator sense. It seems like it would be pretty confusing especially to a newcomer to have it so something totally different between editions. Especially since in some cases it could silently change meaning:
for n in (1..11).map(|n| n*2) {
    // n = 2, 4, 6, ...., 16, 18, 20
}
// vs
for n in (1..11).map(|n| n*2) {
    // n = 2, 3, 4, 5, 6, 7, ...., 15, 16, 17, 18, 19, 20, 21
}

I'm not opposed to something like map_bounds though. Could even live on RangeBounds

I do not think one should implement map on ranges as a combinator which maps the endpoints.  Every other occurrence of that works like an iterator.  While one might write (a..b).map(|x| x*2) and while the argument can be made that this should touch the endpoints only, that's surprising if the user has side effects in there at least.  Probably better to have map_endpoints or something, if we wanted to go there.  I have definitely done .map(...).collect() on ranges, in particular to get vecs meeting various patterns for testing, e.g. .map(|x| (x as f64 * delta).sin()) type things
A more interesting but possibly equally confusing proposal would be to just implement the ops traits, then one could write (a..b) * 2.  If all operators touch the endpoints then that's well-defined and sensible: * and / magnify and shrink ranges, + and - shift them up and down.  I suspect that covers many cases of endpoint manipulation, possibly most (and of course these could be methods which would fix the confusing part, though I do find the operators elegant).

I don't really see any benefit to returning an IntoIterator type instead of Iterator.

It could maybe be used as an Index type to indicate you want the response in reverse order.  Having a range type with a step would have similar utility.
As for map, I don't think it makes any more sense to have that, at least in the form that returns an iterator, than to have a similar convenience method on slice or Vec, but we don't have that. At least considered from a vacuum. It would simplify migration, although cargo fix could just add a call to iter() first.

Ugh sorry to double-comment but 30 seconds later it occurred to me that changing the meaning of these methods literally makes it such that one must know the edition of the file they're reading to know what they do...

I think changing the meaning of (1..11).map(...) is a pretty big hazard.

Ugh sorry to double-comment but 30 seconds later it occurred to me that changing the meaning of these methods literally makes it such that one must know the edition of the file they're reading to know what they do...

I absolutely agree. The reason why Option::map works is because it has the same behaviour as Iterator::map - it maps the single element that the Iterator would have returned. For ranges, it just feels right that map maps over the elements in the range, not its endpoints (even though the two can overlap). Having explicit endpoint mapping methods would be beneficial to have in any case and in all editions and so it would make sense to give them an explicit name.

I do think there is utility in being able to map the endpoints,  (possibly with try_map) or the purpose of converting from one type. Although, in the cases where I have wanted that, impl<T, U> From<Range<T>> for Range<U> where T: Into<U> and similarly for TryFrom would have been even better.

could n..m and family be treated as a sort of literal, and similar to numeric literals, the final type of the expression depends on the context.  If it is used in a context that expects the old range type (for example as an argument to a function that expects, or assigned to a variable explicitly typed as such), it will create the old range type. Otherwise it uses the new range type. Similar how the literal "8" is usable anywhere any integer type is expected.  And that would only happen for direct usage of .., so there isn't implicit conversion from one type to another.

@tmccombs Agree. As we have different types of numbers 5, 5i32, 5u32, 5i64, 5usize we also could have different "types" of Ranges (Ranges with Iterators, Ranges with Into  Iterators), like 0..5, 0..#rit 5, 0..#rii 5 (where #rit/#rii are suffixes).
This does not break compatibility and same time allows to use "alternative" Ranges

could n..m and family be treated as a sort of literal, and similar to numeric literals, the final type of the expression depends on the context. If it is used in a context that expects the old range type (for example as an argument to a function that expects, or assigned to a variable explicitly typed as such), it will create the old range type. Otherwise it uses the new range type. Similar how the literal "8" is usable anywhere any integer type is expected. And that would only happen for direct usage of .., so there isn't implicit conversion from one type to another.

Theoretically, it could. Practically, the compiler team may not be keen on introducing magic.
I would also note that any function taking a Range (or impl RangeBounds) as a parameter, or any Range taken from a struct would not be a literal, and those would require fixing regardless.
The fact that extracting the loop into a function would require modifying the loop invocation is not great, to say the least. It somewhat violates the Principle of Least Surprise.

I think changing the meaning of (1..11).map(...) is a pretty big hazard. There is a lot of existing code, documentation, etc that uses it in the Iterator sense. It seems like it would be pretty confusing especially to a newcomer to have it do something totally different between editions.

I (reluctantly) must agree. I had not considered the fact of working across editions.
In this case, I would then argue against having a map on Range & co, at least in the short-term: it's always easier to add rather than remove, so I would move the proposal of having methods such as map/rev/etc... on Range & co to a Future Possibility section, so that each addition (if any) is properly motivated.
In the meantime, possibly adding .into_iter() is a relatively minor drain.

I'm not opposed to something like map_bounds though. Could even live on RangeBounds.

I propose to just table that discussion -- it would need its own RFC anyway, so no point in derailing this one.

The problem with suffixes is simpler than that.  (expr)..(expr)suffix is almost certainly not something anyone wants to parse.  Or more to the pointa..b + c (where does the suffix go?).

@ahicks92  The problem with suffixes is simpler than that. (expr)..(expr)suffix is almost certainly not something anyone wants to parse. Or more to the pointa..b + c (where does the suffix go?).

That's why I suggest to have suffix right after dot-dot, like (expr)..(suffix )?(expr) and maybe suffix must start with special letter, like #.
In this case a..b + c would be equivalent a..#r2 b + c

At some point the syntax of a suffix is as bad as just using type declarations or .into() or letting the edition of the file determine it.  I'm pretty sure that almost no one is going to be behind that--I certainly am not.  If we want to go there give the types a new method and just Range::new(a, b) or so on.  The integer suffixes both have prior art in other languages and are part of the literal, but "range literal" isn't even a thing to start with so this is effectively proposing new operators in a way, not a suffix.

Having a suffix on a range literal is a reasonable request if there is an intuitive syntax that looks good. The problem is there isn't (at least we haven't come up with one).

If # looks ugly, we could put some other limitation to suffix of range literals, like optional suffix must starts with rng:
(expr)..=?(suffix_rng )?(expr)
let r1 = 0..rng1 5;
let r2 = 1..=7;

This is quite pretty

Around magic, would this work?  And provide enough backwards compatibility?
impl<T: Step> CoerceUnsized<RangeIter<T>> for Range<T>  { }

Yes, but what is a "range literal"?  .. isn't a literal, it's an operator like + or -.  If you call 1..2 a "range literal", then by extension 1 + 2 is a "integer literal".  It's not meaningful without more definition, even if we don't bring non-const expressions into the discussion of it.
If we say that a "range literal" is specifically a..b where a and b are integer literals, or something like that, then that's not going to hit most of the cases.  It is rare to know the endpoints of the range at compile time.
What is actually being proposed with such proposals is a new set of operators, the "legacy range operators" and the "new range operators", then you have the "range operators" which presumably do something odd around type inference.  That's 3 sets each with 2 operators, and some of the forms such as a.. are already adding to the combinatorial explosion there because they're effectively different even though they aren't different in terms of the grammar (a.. produces RangeFrom, ..b produces RangeTo).  There are 6 range types in std::ops each corresponding to a way to write a range, and with these suffix proposals 3 ways to write the operator, giving us 18 total ways to write ranges, each counting as a different operator.
That's more than the entire set of arithmetic operators by itself, also more than the entire set of logical operators.  I find that hugely excessive compared to what we're getting out of the new range types.  IMO if fixing compatibility boils down to a bunch of new operators maybe we should explore things that would allow unsafe Copy impls instead by allowing one to be generic over things that don't have interior mutability, and maybe also something like negative Drop impls.  that would at least solve a bigger problem.  They're significantly harder but not messy.

@ahicks92 Strictly speaking, range expressions never counts in Rust as operators. (But you are right that all operators in Rust are a bit magical)

There are 6 range types in std::ops each corresponding to a way to write a range, and with these suffix proposals 3 ways to write the operator, giving us 18 total ways to write ranges, each counting as a different operator.

I just propose to add suffixes to Ranges.
But yes, with suffixes we could support not all, but just 1 construction (or unify all ranges into 1 and deprecate the rest)
expr1 ..(suffix )? expr2, for example:

rf means RangeFull, it ignores both expr1 and expr2
r1 means simple Range
rfrm means RangeFrom, it ignores expr2
rto means RangeTo, it ignores expr1
rcls means RangeInclusive
rito means RangeToInclusive, it ignores expr1
r2 means new IntoRange
...

@VitWW
The range operators are indeed considered operators, and have precedence.  Perhaps that's not how the compiler chooses to handle them, but they are listed in the operator table and have precedence: https://doc.rust-lang.org/reference/expressions.html#expression-precedence
The suffix discussion would need to solve at least the following:

The old operators aren't going away.
The old operators already cover the from/to case with a generally informative syntax that doesn't require understanding abbreviations.

In particular ..= uses = to mean "include upper".
With most well-written code and operations such as slicing, the notations we have today "do the right thing" as it is; making users care that &x[..b] is a specific type isn't useful.

When teaching this to someone new to Rust, it goes like this:

You can make ranges with .. and ..=.
Except also there is this second syntax with suffixes, the old one is deprecated but you'll see it in the huge body of Rust code that's already out there but really don't use it.
O and by the way because we made a mistake there are two range types for everything.
Right also just fyi if you're making a library consider if you need to handle both.

This would make ranges--already somewhat weak--C++-level bad in terms of how many cases there are and factors to think about.
Type unification of ranges is not particularly possible in the sense that you can't collapse RangeFrom, RangeTo, etc. into one thing.  For one example, implementations of IntoIterator would likely have to panic on fully open ranges, because it is not necessarily the case that any given domain is fixed in size, and so one cannot iterate without a lower endpoint (consider for example a big integer library; you can't default to u64::MIN). It also isn't possible to represent open versus closed ranges efficiently as one type.  You can currently represent 1..2u64 as a struct containing 2 u64s and in a way which compiles well, at least under this proposal.  If you try to represent all permutations with one type, it's actually going to need Option on both endpoints plus a flag that indicates whether it's open or close at the upper end, which is effectively 2 u64s plus 3 bools and additionally some rather complicated branchy code in the iterator since there's only one type on which one could implement IntoIterator.  That may seem acceptable in other languages, but Rust is low level and something as fundamental as slicing would then rely on inlining and the optimizer to generate something efficient.  For slicing specifically, I would expect an order of magnitude performance decrease with such a proposal: it's likely a good way to make the autovectorizer not trigger.

@ahicks92 The suffix discussion would need to solve at least the following

Thanks! Agree.
This RFC says that special syntax for creating Ranges (..,  ..=) is handy instead of using universal functional way (like Range::new()). And this RFC says lets use special syntax for new types of Ranges instead of old ones.
My alternative is to add suffixes for ranges at least  for new types.
And I mention, that my alternative is scalable for future proposals (for example adding suffixes to old types), but this is not a mandatory.

@VitWW
This RFC says that the existing syntax would be used for the new range types, and doesn't currently propose adding new syntax at all.  It's not particularly relevant whether the already existing syntax is better.  What is relevant is that it's out there in the wild, in all the teaching materials, in everyone's crates, so on.
If we say that this can be solved with custom syntax, then a quick crate that provides a r macro of the form r!(a..b), r!(..b) etc. where it works out the type based on the form to build some Copy-capable range types is as good as this RFC, thus removing the need for this RFC.  Like:
for i in r!(1..5) { ... }

Can be made to work today.  I don't think it'd even have to be a proc macro.

The revision I just pushed should address most of the concerns brought up over the last couple weeks.
I will just address this from @matthieu-m

In this case, I would then argue against having a map on Range & co, at least in the short-term: it's always easier to add rather than remove, so I would move the proposal of having methods such as map/rev/etc... on Range & co to a Future Possibility section, so that each addition (if any) is properly motivated.

This defeats one of the primary reasons for having those methods in the first place: less code churn. Sure, having the most common methods there is convenient when writing code, but arguably the more important reason for having them there is that it allows us to switch to Edition 2024 with zero code changes in most cases.

If I understand, this still breaks compatibility between traits in different editions, no?  Aka using rand 0.7 from the future edition, especially since many crates would not upgrade rand quickly.

@burdges cases where a trait is implemented for std::ops::Range (legacy type) but not std::ops::range::Range (new type) would require some form of explicit conversion.

Aka using rand 0.7 from the future edition

Yes, unless a patch version is released to add SampleRange impls for the new types. Adding that impl would not require a major version.

many crates would not upgrade rand quickly

If they aren't updating their dependencies, why would they be upgrading to the newest edition?

Yeah, patch versions would presumably be the overall solution there, but they can cause some chaos too, especially when someone alraedy has a patch version which does something unusual.

If they aren't updating their dependencies, why would they be upgrading to the latest edition?

Another dependency requires upgrades or whatever.  As I said above, we already have problems caused by incompatible traits, espeically when people fear upgrading one dependency.
Anyways I'm pointing out that this edition change would be breaking in way previous editions avoided, and require more delicate upgrades.

Yeah, patch versions would presumably be the overall solution there, but they can cause some chaos too, especially when someone alraedy has a patch version which does something unusual.

Crate authors breaking semver isn't our problem.

Another dependency requires upgrades or whatever.

There is no reason that a dependency update forces an edition upgrade.

As I said above, we already have problems caused by incompatible traits, espeically when people fear upgrading one dependency.

The incompatibility is only in one direction. Only crates which don't support

Anyways I'm pointing out that this edition change would be breaking in way previous editions avoided, and require more delicate upgrades.

I understand your concern.  We must balance the trade-offs, and in my opinion the best balance is what currently stands in the RFC.

Not the issue. It's when they've already attempted to provide some bridge of their own then sometimes it breaks other bridges.

I don't understand. Can you give an example?

This is clearly false in practice.

How? When? Why? Editions are literally designed to support this. You're going to have to provide some evidence for your assertion.

How much does the change proposed here matter?

RangeInclusive is 1.5x the size it should be
ExactSizeIterator impls are incorrect
People have been asking for Copy ranges since 2015
Consistency

If I underrstand, this also proposes that new iterator forms become opaque.

Yes.

Ideally, one should assess how much code could even be converted, aka does not use the structure of Range dxuring iteration, without using the legacy form.

This is pretty rare, but I agree it's important to support.
I leave the exact API up to T-libs-api but I suggest a couple options for these cases in the RFC:

remainder method on the iterator for getting the remaining range
by_ref method that returns a different iterator type that mutates the range in-place

People avoid updating crate dependencies but will still upgrade their edition if:

The new version of the crate has a bug.
The new version drops functionality.
The new version significantly changes the API.
The environment they're in requires approval of dependencies (e.g. big corp).

I could probably think of more.

@ahicks92 is that supposed to address this?

There is no reason that a dependency update forces an edition upgrade.

This is clearly false in practice.

How? When? Why? Editions are literally designed to support this. You're going to have to provide some evidence for your assertion.

Because what you said seems to be focused on a different question. You're answering "when would a crate upgrade edition without updating dependencies" rather than "when would a dependency update force an edition upgrade".

@pitaj
No. Above, you said "If they aren't updating their dependencies, why would they be upgrading to the latest edition?".  I will attempt to quote in future, but the question was part of the discussion.  On a re-read more closely you're referring to crates on crates.io, however all but my last point still apply to that situation.  As one example Diesel was forcing an old version of rusqlite for the longest time.
(if that was part of a thread and I should have hit a reply button GitHub isn't indicating that to screen readers anymore)

Understood. Thanks for clarifying.
Still, your answers mostly address the conversation of the question. I asked "why would a crate upgrade the edition when they aren't updating their dependencies" but you answered "why would a crate not update their dependencies when upgrading the edition".
I think it's an important distinction because one of my primary arguments is that crates always have the option of not upgrading to the newest edition. If they have direct dependencies that (for whatever reason) are missing impls for the new range types, they can just not upgrade to edition 2024 (temporarily or permanently).

I do view that as an answer to why a crate or other project would upgrade editions without upgrading dependencies.  Since editions may vary per crate, there is no reason not to go ahead and do it to get new stuff.  It's usually one line in Cargo.toml, then maybe a cargo fix, then profit.  As things are today there is no blocker to doing that even if you can't upgrade anything else, not that I know of--it's just pure win, and takes about 30 seconds to 5 minutes tops, even on large codebases, even if you're fixing the warnings on the way.
So why aren't they the same question exactly?  it seems like if they aren't the same question, then the underlying assumption is that edition upgrades are hard and no one does them by default, and that's certainly contrary to my experience.
People do things incrementally when they can.  If the edition upgrade is low-hanging fruit and dependency updates aren't, then of course I'll want to do that.  Today, it is low-hanging fruit.  With this RFC it may still be but is less so.
here's a genuine question.  Above, I wrote that you could make a macro of the form r!(a..b) which could parse to a range type in some crate which is Copy etc.  I considered it mostly to be making a point around suffixes, but maybe it's something that should be legitimately examined as a counter to this RFC.  If we're going to put friction on someone, why not just build that crate, say if you're in the minority of users who really cares about it then here's a crate that requires typing 4 extra symbols, and then call it a day?  The assumption here seems to be that it's worth making everyone who doesn't care deal with it, and while the people asking for it are loud I have little evidence that they are numerous at least at this time, so why should it be on everyone?

You must upgrade one for whatever reason, ala security fix, etc, which then upgrades one of its dependencies (or an edition).  Another of your dependency refuses to upgrade one of its dependencies, for whatever reason, like no current audit, dislike some change, etc (so no edition upgrade here).  We then have dependencies who disagree about trait versions for rand, serde, rustls, rustcrypto, dalek, or whatever.  At this point, rustc gives unhelpful error messages because you've several versions of a trait in play.
Importantly, there are no choices being made by whoever encountered the problem:  They must upgrade one dependency, but they cannot upgrade another nested one (without forking some dependencies).  They've no idea what changed in the dependencies of the upgraded dependency, but now their dependencies cannot communicate anymore.  It's even likely they already had two+ versions of the same traits in play, possible bridged, but added one more which breaks things.
There are tricks which help like reexporting dependencies and trait bridges, either by patch versions or by wrappers, but they're not always possible, some ocasionally make things worse, or at least make finding errors harder.  All this happens without editions, but afaik an edition change cannot smooth this part.

We've no reason for macros here:  As syntax goes, methods like .iter() with this RFC, or say .copyable() otherwise make more sense than macros.

@burdges
I'm using the macro point as a devil's advocate argument: you could write that as a library today where the macro expands to a range type in the library instead of std.  I am definitely not proposing it as the solution.  I'm saying: if we want to have ecosystem churn why not publish such a crate and then let the people who care churn their ecosystem?  I don't think that it will or should happen that way, but it does I think serve as a good question by establishing a bar one must be above before making everyone who doesn't care deal with it.  That could go a few different ways: data that says everyone works around this, some plan to not cause the churn, etc.

I'll add a real dependency upgrading story the conclusion of which is "we probably never can".  Why I know that Diesel was on an old version of Rusqlite for a long time is that the links field in Cargo.toml can't conflict.  I forget the exact situation because it's been a year or so but it was something like Diesel wanted rusqlite <= x, something else wanted rusqlite >= y, x < y, both try to link sqlite, and then explosion because Cargo can't satisfy both.  The other crate was one of the sqlite migration solutions, I just can't remember which one.  We have a bunch of code that doesn't use Diesel because we decided to avoid it for future projects after using it once, we have a chunk of code that's business critical using Diesel and that code would be hard and risky to rewrite, and writing our own sqlite migration solution took one dev-day instead of weeks.  Q.E.D. we will probably be on old rusqlite until the end of time.
I would like to see such problems given more weight here, kinda.  The easy answer is "well everyone upgrades dependencies" but in the real world man does that fall down so hard sometimes.

@ahicks92

I do view that as an answer to why a crate or other project would upgrade editions without upgrading dependencies. Since editions may vary per crate, there is no reason not to go ahead and do it to get new stuff. It's usually one line in Cargo.toml, then maybe a cargo fix, then profit. As things are today there is no blocker to doing that even if you can't upgrade anything else, not that I know of--it's just pure win, and takes about 30 seconds to 5 minutes tops, even on large codebases, even if you're fixing the warnings on the way.

None of that will change with the RFC as written. cargo fix --edition 2024 will make all migrations automatically (adding explicit conversions where necessary).
Again, users can choose whether they want to upgrade editions and accept the extra conversions, or whether they want to wait for dependencies to support the new ranges directly.

So why aren't they the same question exactly? it seems like if they aren't the same question, then the underlying assumption is that edition upgrades are hard and no one does them by default, and that's certainly contrary to my experience.

My experience is that crates generally fall into two modes:

actively maintained / developed, updated at high frequency, minor dep updates likely automated
maintenance mode, updated rarely and in batches

(1) probably already have compatible dependencies when they upgrade to Edition 2024, but have no qualms with updating their dependencies. (2) will just roll the dependency updates and edition upgrade into the same maintenance batch.

Above, I wrote that you could make a macro of the form r!(a..b) which could parse to a range type in some crate which is Copy etc. ... why not just build that crate, say if you're in the minority of users who really cares about it then here's a crate that requires typing 4 extra symbols

These foundational (and extremely common) types from the standard library have the wrong design. It is widely considered now that ranges should have been IntoIterator etc from the beginning. This RFC offers an opportunity to correct that mistake for all future users of Rust, at the mild inconvenience of some users upgrading to Edition 2024.

The assumption here seems to be that it's worth making everyone who doesn't care deal with it, and while the people asking for it are loud I have little evidence that they are numerous at least at this time, so why should it be on everyone?

First of all, the cases where explicit conversions are necessary will be relatively rare. The most common use of ranges are slicing and iteration. SliceIndex impls in core and the IntoIterator impl cover those.

As for popularity, here's where a few quick searches got me
I implore you to check the reactions / upvotes / etc on various posts and such talking about how to fix the range types. You will see that these are routinely popular.
IRLO

pre-RFC for this
https://internals.rust-lang.org/t/should-std-ops-range-be-copy/4388
https://internals.rust-lang.org/t/a-bad-solution-to-range-copy/17216
https://internals.rust-lang.org/t/enhancing-rust-range/19197
https://internals.rust-lang.org/t/how-do-we-actually-want-rangefrom-to-behave/11823/9

URLO

https://users.rust-lang.org/t/why-doesnt-range-t-copy-implement-copy/51224/2
https://users.rust-lang.org/t/implement-copy-when-having-fields-with-foreign-types/38207

Github

https://github.com/rust-lang/rfcs/issues/2848
https://github.com/rust-lang/rust/pull/27186
https://github.com/rust-lang/rust/pull/21846
https://github.com/rust-lang/rust/issues/90190
https://github.com/rust-lang/rust/issues/48649

SO

https://stackoverflow.com/questions/31045637/re-using-a-range-for-iteration
https://stackoverflow.com/questions/43416914/why-doesnt-opsranget-implement-copy-even-if-t-is-copy

Blogs

https://kaylynn.gay/blog/post/rust_ranges_and_suffering
https://ridiculousfish.com/blog/posts/least-favorite-rust-type.html

Reddit

https://www.reddit.com/r/rust/comments/47uqgj/why_does_stdopsrange_not_derive_copy/
https://www.reddit.com/r/rust/comments/rrgxr0/a_critique_of_rusts_range_types/
https://www.reddit.com/r/rust/comments/ix751t/my_least_favorite_rust_type/

@rustbot labels +I-lang-nominated
Since this is relevant for Rust 2024, we should discuss this soon.
For the author, @pitaj, or others in the thread, if you could offer a concise summary of what the lang team may want to take into account when determining how to prioritize this for the edition, including e.g. any developments that have occurred in this thread, that would be helpful.

@traviscross
Key Lang Concerns
Switching range syntax lowering across editions
RFC proposes to switch range syntax (s.., s..e, s..=e) from existing range types (core::ops::{RangeFrom, Range, RangeInclusive}) to a set of new range types (under a new range module) in Edition 2024.
That is the full extent of lang changes in the RFC at this time.
Implicitness for Convenience
RFC does not recommend any form of implicitness (coercions, inference, etc). Such "magic" could improve the experience with the new edition:

fewer explicit conversions would be added by cargo fix --edition (possibly zero)
new code would not hit issues when interfacing with APIs expecting only the legacy range types, such as

takes_range(0..5) with fn takes_range(range: std::ops::Range<usize>)
custom_type[0..5] where custom_type: CustomType and CustomType: Index<std::ops::Range<usize>>
takes_iter(0..5) with fn takes_iter(iter: impl Iterator<usize>)

The RFC recommends only a mitigation path where the new types are stabilized well before the new edition is released, allowing third-party libraries to publish updates supporting the new types in time for the new edition.
The best solution I've seen along these lines is to make range syntax similar to integer literals, where the concrete range type is inferred based on context. This would cover the most prominent ergonomic drawbacks.
The lang team should at least consider this. If T-lang decides that something along these lines is warranted, I am fully willing to modify the RFC to include it.

We discussed this in today's @rust-lang/lang meeting. We're in favor of the lang side of this, as long as @rust-lang/libs-api is in favor of the libs-api side. The lang portion of this is whether we're willing to desugar range syntax differently; we consider it a libs-api matter to decide what iterator types to use for IntoIter and similar.
@rfcbot merge

A suggestion with my @rust-lang/libs-api hat on: I think we should impl From from the old types to the new and vice versa, so that if someone has a new range and needs to pass it to a function taking the old type (or vice versa), they can write .into().

@joshtriplett this is already proposed by the RFC:

There exist From impls for converting from the new range types to the legacy range types.

(although ig it could also mention the inverse impl)

It's also under Changed Structure and API:

The following conversions between the new and legacy types will be implemented:

impl<Idx> From<range::Range<Idx>> for range::legacy::Range<Idx>
impl<Idx> From<range::RangeFrom<Idx>> for range::legacy::RangeFrom<Idx>
impl<Idx> From<range::RangeInclusive<Idx>> for range::legacy::RangeInclusive<Idx>

impl<Idx> From<range::legacy::Range<Idx>> for range::Range<Idx>
impl<Idx> From<range::legacy::RangeFrom<Idx>> for range::RangeFrom<Idx>
// Fallible because legacy RangeInclusive can be exhausted
impl<Idx> TryFrom<range::legacy::RangeInclusive<Idx>> for range::RangeInclusive<Idx>

I want to add to what @joshtriplett said: we discussed the concerns asked in https://github.com/rust-lang/rfcs/pull/3550#issuecomment-1897809749 and the consensus was that we were comfortable switching the range syntax lowering, but that the suggestions under "implicitness for convenience" would require pretty strong evidence of need to be considered.
For reference, here are the notes from the lang meeting.

@rustbot labels -I-lang-nominated
As tmandry mentioned, we discussed this today in T-lang triage, and our consensus was positive overall on doing this.  We're proposing this for FCP merge jointly with T-libs-api.
We'll remove the T-lang nomination, but please renominate if questions arise that should be discussed or addressed by the lang team.

if you could offer a concise summary of what the key lang questions are here and what the lang team may want to take into account when determining how to prioritize this for the edition, including e.g. any developments that have occurred in this thread, that would be helpful.

I've no idea if the Index concerns were resolved.
There is an ergonomics hiccup if (0..n).iter().rev() or whatever becomes commonplace.  Inherent methods fix some like rev, but map looks messy.  We'd need multiple editions in which (0..n).map(..) is a hard error before map could map the range itself.  We could've map_range instead, so then inherent map could return the iterator type.
There is a concern that an opaque iterator type for Range itself loses some functionality.  I've no idea how many crates edit a range while iterating though anyways.
There is a concern that complex dependency hierarchies break when they need ecosystem traits to cross the edition boundary.  Crate x depends upon rand edition 2024, crate y depends upon rand from an earlier edition and will not update for many years, crate z depends upon the traits in crate x and y being compatible, which shall not happen again anytime soon.  This is significantly worse than the RFC acknowledges, but it's not worse than what often happens anyways when you hit version miss-matches in ecosystem traits.
As a rule, crate z resolves this by not upgrading edition or crate x, until crate y updates.  This should work so long as crate x still provide security fixes to their older edition crates.  It's risky if crate x updates edition in a patch version.
We discussed imporant changes like RangeInclusive performance and ExactSizeIterator correctness, both of which require some breakage anyways, but the relevance of this RFC seems overstated.  Alone there is no concern here per se, but it's relevant to the traits issue from two directions:  Another solution might cause less breakage, but some breakage is necessary, so maybe less breakage worse if the result is "more hacky".

I don't think it's an edge since I see people in discussion groups complain about it from time to time. Anyway, can you elaborate on the design of your wrapper type?

I don't think it's an edge case since I see people in discussion groups complain about it from time to time. Anyway, can you elaborate on the design of your wrapper type?

Not my design - but a workaround was described in the initial issue:
https://github.com/rust-lang/rfcs/issues/2848

It's not a "wrapper" (since you cannot make it Copy by just wrap it by a wrapper), but a new range type - much like the current proposal in the RFC, only without the resolution change.

It's not a "wrapper" (since you cannot make it Copy by just wrap it by a wrapper), but also a new Range type - much like the current proposal. It doesn't require the resolution change but seems less ergonomic since we will need to convert between the two types back and forth.

It's not a "wrapper" (since you cannot make it Copy by just wrap it by a wrapper), but also a new Range type - much like the current proposal. It doesn't require the resolution change but seems less ergonomic since we will need to convert between the two types back and forth.

Apologies - I should not have used the term "wrapper" - I had used that term because that's how it was described in the issue.
The point that I'm trying to make is that the proposed solution also impacts ergonomics in some cases. It's trading ergonomics in one area for ergonomics in another area (and causing breakage in some cases). Is this issue so severe that the breakage and decreased ergonomics in other areas are worth the change?

@m-ou-se

So we end up in a world where library authors have to manually make their public API compatible with both the old and new edition, which very much goes against the idea of editions being a package/crate local decision.
Basically, the edition change only works effortlessly if all libraries that take ranges in their API will update their API before the new edition. This is very unlike other edition changes.

I agree that we should seriously consider this concern.

If this is the only way forward with the range types, this might be an acceptable outcome of the trade-off, but I think we can only go forward if we know how big the impact will be on the ecosystem. Do we know how many libraries have ranges in their public API? And how many would not need any changes?

I would also love to see data for this, and I'm willing to put in the work to gather it. However, I'm not aware of an existing method for capturing this kind of information.
How do we go about surveying library crates for this?

Even if libraries do upgrade to accept both the old and new range types, running cargo fix --edition would still result in .to_legacy() calls being inserted if the dependency is updated later. Afterwards, nothing would automatically remove to_legacy() because it'd work with both the new and old types, resulting in unnecessary clutter.

That would be non-ideal, but can be remedied with a lint.

Most importantly, I would like to avoid a world where library maintainers will have to deal with bug reports or feature requests like "please support 2024 ranges" or "please support legacy ranges". In the past, edition changes that resulted in extra work for some library maintainers were not received well, and I don't think we should repeat that mistake on even larger scale.

To avoid it entirely, the only options are (1) not going through with the change and (2) add compiler support for implicit handling of the different range types. All the more reason to gather that data so we can make an informed decision knowing the trade-offs.
Do you mind referencing the edition changes you're referring to here?

Secondly, there is no way to consistently project the Option::map(...) -> Option or [T; N]::map(...) -> [U; N] pattern onto ranges, because ranges aren't collections

Sure there is. A range is sort of a collection of two elements (or 1 for the unbounded ranges), and it isn't hard to imagine an operation to map over those values. For example:

You have a Range<u16> but you want a Range<u32>: range.map(|i| i as u32)
You want to shift a range up by 2: range.map(|i| i + 2).

Granted, I think the first one is more useful than the second, and the first could probably be better accomplished by something like:
impl<T, U> From<T> for Range<U> where T: Into<U>  and impl<T, U> TryFrom<T>  for Range<U> where T:  TryInto<U>.

Sure there is. A range is sort of a collection of two elements (or 1 for the unbounded ranges), and it isn't hard to imagine an operation to map over those values.

I would consider that inconsistent with Option::map etc, because it would break correspondence with .into_iter().map(...).collect()

@rustbot labels +I-lang-nominated
Nominating for two reasons.  One is that we did not discuss this bit that @pitaj proposed:

The best solution I've seen along these lines is to make range syntax similar to integer literals, where the concrete range type is inferred based on context. This would cover the most prominent ergonomic drawbacks.
The lang team should at least consider this. If T-lang decides that something along these lines is warranted, I am fully willing to modify the RFC to include it.

We probably should at least see how people might feel about this.
Secondly, we should discuss the automatic migration concerns raised by @m-ou-se and whether there's anything we might be able to do about that on the lang side.

@rustbot labels -I-lang-nominated
We discussed this yesterday, 2024-02-07.  On the first point, where @pitaj wanted to get a sense of how people might feel about using inference, the feeling was generally positive assuming that it's possible.  It was mentioned that we should ask the types team about this.
cc @rust-lang/types
The concern centered on the idea that, e.g., for x in a..b might itself be ambiguous and that we'd need some rule to disambiguate.  It was mentioned that type inference defaults have some relevance here, and that it would be great if we could make progress on a subset of that.
@nikomatsakis proposed the following:

It occurs to me there's another route here.  Type inference is a hybrid system.  We take into account the context.  There's a rule here we could adopt here that would sidestep the fallback problems.  E.g.:
let i = 0..5; // No expected type, uses 'new' range.
let i: LegacyRange = 0..5; // Expected type is legacy range, uses 'old' range.
takes_range_1(0..5); // Expected type comes from fn below, uses "legacy" range.
takes_range_2(0..5); // No expected type, chooes "new" range.

// Importantly...

fn takes_range_1(range: std::range::legacy::Range<usize>) {}

fn takes_range_2<R>(range: std::range::legacy::Range<usize>)
where
    R: Something,
{}

@pitaj: Given the apparent interest, it may be worth writing out the mechanics and implications of this idea in the RFC and talking with the types team about this so that the lang team can better evaluate the feasibility here.

On the second subject, the concern @m-ou-se had raised, we discussed that traditionally we have separated migration lints from idiom lints.  The former are semantics-preserving, but the latter are not, necessarily, and that a full idiomatic migration to using these new range types may be a combination of both.
On the question of how to weigh pressure that crate maintainers might face, one thing we discussed was the principle that editions are meant to be adopted, and that we're leaning heavily here on that principle.
In terms of the next steps, we had three asks:
One, we wanted the proposed "ask" of crate maintainers to be articulated.  What would our guide say exactly about how library authors should handle this change?
Two, we wanted some data to be collected about the impact here.  Specifically:

How often is each range type used concretely by name in crates and in which positions (i.e., in fn arguments, struct fields, Index impls, other impls)?

In particular, this may be relevant how often Range vs RangeInclusive is used concretely; we expect the latter may be more rare.

How often are the range traits used and in which positions?
How often do people use .. vs ..= syntax?

Three, we wanted to know whether there is any proposed solution that would not require library authors who are staying on an older edition to do any work to best support users on a newer edition.  We acknowledged that such a solution, if it exists, may be undesirable in other ways, but we at least wanted to see what the tradeoffs might be.

On a meta note, we know we're asking for a lot of work here, which is unfortunate.  We appreciate @pitaj for driving forward on this.
Please renominate when any of (even if not all of) the answers above are available.

How often is each range type used concretely by name in crates and in which positions .. ?

Alone concrete references do not suffice.  Anytime crate maintainers provide their own traits which abstract over range types, because those cannot necessarily be upgraded cleanly.
In particular rand::Rng::gen_range uses their own trait rand::distributions::uniform::SampleRange.  Also, rand already pushed patch version bumps that bridge between minor version trait changes, but anytime this created problems people exclude those patch versions.  It's therefore impossible for rand to provide patch bumps for all past minor versions.

@traviscross :  @nikomatsakis proposed the following:

It occurs to me there's another route here.  Type inference is a hybrid system.  We take into account the context.  There's a rule here we could adopt here that would sidestep the fallback problems.  E.g.:
let i = 0..5; // No expected type, uses 'new' range.
let i: LegacyRange = 0..5; // Expected type is legacy range, uses 'old' range.
takes_range_1(0..5); // Expected type comes from fn below, uses "legacy" range.
takes_range_2(0..5); // No expected type, chooses "new" range.

It's almost my alternative of using .. and ..= like integer-literals. But this is mostly "implicit rules" of default using.
I just propose to ADD specific suffixes for explicit show which type of Range we wish use.
let i1 = 0..5; // IMPLICIT: No expected type, uses 'new' range.
let i2: LegacyRange = 0..5; // INFERRES Expected type is legacy range, uses 'old' range.
let i3: NewRange = 0..5; // INFERRES Expected type is new range, uses 'new' range.
let i4 = 0..rnew 5; // EXPLICIT: Expected type "rnew" (by suffix) uses 'new' range.
let i5 = 0..rold 5; // EXPLICIT: Expected type "rold" (by suffix) uses 'old' range.

@traviscross
Wanted to provide a quick update on these points. I've been working on implementing a lint to gather the data as discussed.
I've made quite a bit of progress. All of the cases I'm focused on are being detected, I just need to improve the diagnostics in order to improve the precision.

One thing I didn't see mentioned in the RFC and discussion so far, is the ability to abstract over ranges. In past one of the pain points when using the existing range types was the difficulty to abstract over them. Function that want to take all kinds of ranges are quite cumbersome to write. A trait that all of them implement seems like a good fit. A rough sketch:
trait Range {
    fn start(&self) -> Option<usize>;
    fn end(&self) -> Option<usize>;
}

@Voultapher  RangeBounds

@rustbot labels +I-lang-nominated
After we last discussed this on 2024-02-07, we made a number of requests for further analysis and data collection.  @pitaj has now delivered on that.
The design document with this data and analysis is here:
https://hackmd.io/@uhs6rVdLTSS0gnie4q0fqA/ryjYJW2pa
And the raw data he extracted from crater is here:
https://github.com/rust-lang/lang-team/files/14659048/data.zip
Let's nominate so that we discuss.  Since there is a design document ready and  this may call for a design meeting, we've also filed an issue for that here:
https://github.com/rust-lang/lang-team/issues/259
Huge thanks to @pitaj for putting together a comprehensive design document for us.

I don't think "excluding trait impls" gives the right information here, since the trait impls cannot necessarily be cleanly upgraded, due to conflicts with other upgrades.  The trait impls should be counted seperately of course.

@burdges the trait impls are counted separately, under "Range Type explicit usage in public trait impls".

The strongest argument for new iterator types is that the current range types have incorrect ExactSizeIterator impls.

Can you elaborate what you mean by this? Do you mean that Range<u32> implements ExactSizeIterator which it cannot on 16-bit platforms? If so, I'd consider this a very weak point. None of Rust's major platforms has a 16-bit pointer width.

The strongest argument for new iterator types is that the current range types have incorrect ExactSizeIterator impls.

Can you elaborate what you mean by this? Do you mean that Range<u32> implements ExactSizeIterator which it cannot on 16-bit platforms? If so, I'd consider this a very weak point. None of Rust's major platforms has a 16-bit pointer width.

It's explained in detail in the Iterator types section. Yes, only 16-bit targets are affected - but I disagree that these platforms should be considered second class citizens. Sure, they aren't tier 1 targets, but they don't deserve to silently get incorrect behavior.

That width function could even be generalized and implemented for any Range<T:  Sub>

there is no valid width for -128i8 .. 127i8 if you require T: Sub.

Hmm, I think having a way to get the size of the range in generic code, and ideally in a way that generalizes to more than just unsigned integers would be very useful, but dealing with a size that isn't representanle in the Idx type is a tricky problem.
I can think of a few ways to do it:

just do whatever Sub does in that case. In many cases that is probably fine, because cases that would overflow would be rare. And in the happy path this is probably the easiest to use. But in some cases panicing would be undesirable, and the behavior of a saturating or wrapping type is probably even worse, since it gives you an incorrect answer.
have a way (a new trait?) to specify a widened type  for the result, the "width" of a Range is actually a u8, or maybe an i16 (if we wanted to allow reversed ranges). But this would add complexity and have more surface area for the API
have the function always return an Option, but that is a little inconvenient for cases where it will never fail.

None of these are really a clear winner.

@pitaj as other have pointed out, RangeBounds is useless in many situations.
For example
/// Returns a new `Vec` with `len` elements, where the elements are uniformly random in the `range`.
fn random_uniform<R>(len: usize, range: impl RangeBounds) -> Vec<i32>

I don't know how to implement that function signature, without brute-force contains sampling. If we are doing new range types, let's please figure out how to provide a width function.

One idea would be to have a trait that is implemented for all types that implement Sub and provide width that way, similar to DoubleEndedIterator.

@pitaj as others have pointed out, RangeBounds is useless in many situations.
For example
/// Returns a new `Vec` with `len` elements, where the elements are uniformly random in the `range`.
fn random_uniform<R: RangeBounds<i32>>(len: usize, range: R) -> Vec<i32>

I don't know how to implement that function signature, without brute-force contains sampling. If we are doing new range types, let's please figure out how to provide a width function.

Am I missing something? You can get the .start_bound() and .end_bound() directly and do whatever you want with those.

Indeed, you'd get &i32s which one can sub, I'm not sure what made me stumble in the past. But I did struggle with that signature and had to go with a workaround. It's been 2 years since I ran into the problem, so I don't remember the specifics, maybe I just didn't realize how to use them, or didn't find the trait? But I swear, I tried to abstract over the range and ran into an issue. Hmm. Consider my point moot.

@tmccombs somehow these topics got mixed together. I brought up RangeBounds to answer @Voultapher's original question. I never suggested that RangeBounds would solve the width issue.

Functions that want to take all kinds of ranges are quite cumbersome to write. A trait that all of them implement seems like a good fit. A rough sketch:
trait Range {
    fn start(&self) -> Option<usize>;
    fn end(&self) -> Option<usize>;
}

What they suggest is practically identical to the existing RangeBounds trait and suffers the same issues you brought up, especially #1.
To address that, I do think there's room for improvement in the range-related APIs. For instance, adding a ClosedRange trait would be nice:
trait ClosedRange<Idx>: RangeBounds<Idx> {
    fn start(&self) -> Idx;
    fn end(&self) -> Result<Idx, Overflow<Idx>>;
}

For #2, I think we could solve the "inclusive might overflow" issue with an associated type:
trait RangeWidth {
    type Output: TryInto<u128> + TryInto<usize>;
    fn width(&self) -> Self::Output;
}

Most integers would just return the next larger unsigned type:
impl RangeWidth for RangeInclusive<u8> {
    type Output = u16;
    // ...
}
impl RangeWidth for RangeInclusive<i16> {
    type Output = u32;
    // ...
}

But some would need to use a special type to signal a possible overflow:
impl RangeWidth for RangeInclusive<u128> {
    type Output = MaybeOverflow<u128>;
    // ...
}
impl RangeWidth for RangeInclusive<usize> {
    type Output = MaybeOverflow<usize>;
    // ...
}

Though this API is kinda ugly and it might be better to wait for something like generic-sized integers:
impl<const BITS: usize> RangeWidth for RangeInclusive<Integer<BITS>> {
    type Output = Unsigned<{ BITS + 1 }>;
    // ...
}
// Same for Unsigned

Thanks for clarifying, as I understand it, the plan is for the new range types to implement RangeBounds for compatibility reasons. But given we are breaking compatibility anyway, wouldn't this RFC be precisely the place to decide to implement new traits?

I'm not sure what you mean. We can always add new traits without breaking compatibility.

But given we are breaking compatibility anyway, wouldn't this RFC be precisely the place to decide to implement new traits?

Not necessarily. The more traits and the more changes we add to something like this, the less likely it is that it will land in time for the 2024 edition. New traits requires a ton of design work. Therefore, the only way it makes sense to bundle new traits into this proposal is if there is no other choice, i.e., they for some reason cannot be added later.
I think it would be better if y'all took this design to IRLO so that we don't derail this RFC discussion.

Let's discuss the design space here https://rust-lang.zulipchat.com/#narrow/stream/219381-t-libs/topic/Abstracting.20over.20ranges.2C.20.60RangeBounds.60.20alternatives

As stated in https://github.com/rust-lang/rfcs/pull/3550#issuecomment-2021569980, @pitaj has analyzed of the usage of the old APIs in public crates and provided a recommendation in this doc. I think @rust-lang/libs-api would be best positioned to evaluate the ecosystem impact of the change, so nominating for their discussion.
In particular, the ask is to stabilize the new range types ASAP (ahead of the edition). This will smooth the transition out more and give library authors time to prepare updates. Of course, we need to know we have a plan to use them before stabilizing them.
To boil it down to a question:

Without language-level mitigations,
but with all supposed compiler mitigations (diagnostics, migrations, and linting) presented in the doc,
and possibly the help of a migration working group

Does the libs-api team think the level of ecosystem disruption – and the overhead of having two sets of range types – would be acceptable or unacceptable?
The answer may be that we need to have a joint discussion between @rust-lang/libs-api and @rust-lang/lang on what we consider acceptable, but I'd like the libs-api folks to make a first pass.
@rustbot label I-libs-api-nominated

@pitaj Thanks for the analysis, great job, it answered (almost) all of my questions just as I was thinking to ask them! I'm going to post a comment after this with a few minor confirmations and follow-ups.
What @m-ou-se requested -- and what I'm not sure we have spelled out -- is what kind of advice we expect to be giving library authors in the future. This is an interesting question which intersects a lot of live debates (e.g., it seems related to MSRV policies and LTS). To me, though, the right answer is to return to the edition tenets established in RFC 3085, specifically Editions are meant to be adopted:

Our goal is to see all Rust users adopt the new edition, just as we would generally prefer for people to move to the “latest and greatest” ways of using Rust once they are available. Pursuant to the previously stated principles, we don’t force the edition on our users, but we do feel free to encourage adoption of the edition through other means. For example, the default edition for new projects will always be the newest one. When designing features, we are generally willing to require edition adoption to gain access to the full feature or the most ergonomic forms of the feature.

To this end, I would say best practice is to advise library authors to just use the new range types, and to encourage users from old Editions to upgrade so they don't have to write .into(). Of course I wouldn't stop you from adding more impls if you feel the need, but I don't recommend it.
Existing functions that take the old range types should presumably be converted to use range bounds and/or deprecated (I know that changing from a concrete type to a impl RangeBounds is not 100% semver compatible).

@pitaj, can you answer a question and confirm some of my takeaways (i.e., did I read the document right?):

Question: I wanted to see the "places where Range is used" just for the crates with >100 dependents (taking those as a proxy for "widely used and well designed" or something like that). Is that may be what the "affected crates" vs "Total occurrences" table was trying to summarize? (I wasn't sure what "affected crates" meant)

Confirmations to be sure I was following:

Most widely used dependencies are also regularly updated -- therefore, if we stabilize the range types quickly and provide guidance, we can expect that if we add new range types?
There are places where impl RangeBounds cannot be used, e.g., impls, and those are fairly common (as you would expect).
Most folks wind up using concrete Range types vs the impl -- not too surprising, since it's much more obvious how to manage it. (Is there a clippy lint here?)

@pitaj in the Zulip conversation you showed a draft that was very promising, and if adopted would require a new RangeBounds trait. My understanding of the ways forward are:
A) The width use-cases are not deemed important so no change is made
B) The width use-cases are served by a new trait sometime in the future at the price of discoverability and ergonomics
C) The width use-cases are served by a new RangeBounds trait similar to what you proposed in the Zulip chat
What is your position on the topic? I'm asking this here and not in the Zulip thread, because it affects the path forward for this RFC. I think C has the best result in terms of final product but bears a risk that this RFC won't make it for 2024.

@nikomatsakis

What @m-ou-se requested -- and what I'm not sure we have spelled out -- is what kind of advice we expect to be giving library authors in the future.

My answer to this is the Libraries section in the RFC. The wording can be strengthened to make explicit that those changes are strongly encouraged.

To this end, I would say best practice is to advise library authors to just use the new range types, and to encourage users from old Editions to upgrade so they don't have to write .into(). Of course I wouldn't stop you from adding more impls if you feel the need, but I don't recommend it.

I agree this is the ideal. However, it would require the affected packages to make semver-major releases. And it's not really necessary: as explained in the RFC, both old and new types can be supported in a backwards-compatible way without much extra effort.

Question: I wanted to see the "places where Range is used" just for the crates with >100 dependents (taking those as a proxy for "widely used and well designed" or something like that). Is that may be what the "affected crates" vs "Total occurrences" table was trying to summarize? (I wasn't sure what "affected crates" meant)

In that table, the "total occurrences" row is a count of the total number of times the given condition was found across all of the crates with >=100 dependents. The "affected crates" row has the count of those crates that have the given condition.  So for the first column: there were 22 "derive" cases across 6 of the popular crates.

Confirmations to be sure I was following:

Most widely used dependencies are also regularly updated -- therefore, if we stabilize the range types quickly and provide guidance, we can expect that if we add new range types?

That's my conclusion, yes.

There are places where impl RangeBounds cannot be used, e.g., impls, and those are fairly common (as you would expect).

Indeed. (Maybe a language feature like sealed traits can help prevent this kind of issue down the line)

Most folks wind up using concrete Range types vs the impl -- not too surprising, since it's much more obvious how to manage it. (Is there a clippy lint here?)

Yeah as pointed out by others here, there's room for improvement to make these APIs more usable for common cases. A clippy lint isn't feasible right now because RangeBounds isn't expressive or ergonomic enough (for instance you can't restrict it to only two-sided ranges). That API work can happen in parallel and is mostly orthogonal to this RFC (would help regardless).

@pitaj in the Zulip conversation you showed a draft that was very promising, and if adopted would require a new RangeBounds trait. My understanding of the ways forward are:

It wouldn't require a new trait. Adding default methods is not a breaking change.

What is your position on the topic? I'm asking this here and not in the Zulip thread, because it affects the path forward for this RFC. I think C has the best result in terms of final product but bears a risk that this RFC won't make it for 2024.

Range API improvements can be discussed and implemented independent of this RFC.

@pitaj

Sure, they aren't tier 1 targets, but they don't deserve to silently get incorrect behavior.

That can be resolved by removing this implementation on 16-bit targets. New range types are certainly not needed to fix this particular issue.

Is there a plan around the timing of these changes in the ecosystem? Will we stabilize the new range types together with the new edition? Or do we want to stabilize the new range types sooner, such that popular libraries can all be updated before the new edition is released? If so, how much time do we expect that to take? And how about libraries that wish to maintain some MSRV?

What will be expected of authors of new libraries in the future? (After the new edition is stable.) Will some of their users still expect them to add support for the legacy range types? (That would mean that (new) Rust 2024 users will need to learn about older editions because of downstream users of their code.)

Is there a plan around the timing of these changes in the ecosystem? Will we stabilize the new range types together with the new edition? Or do we want to stabilize the new range types sooner, such that popular libraries can all be updated before the new edition is released?

That's discussed to a degree under Mitigation:

To mitigate these drawbacks, we recommend introducing and stabilizing an MVP of the new types as soon as possible, well before Edition 2024 releases (even before the implementation of the syntax feature is complete). This will give libraries time to issue updates supporting the new range types.

If so, how much time do we expect that to take?

I wouldn't expect the changes to take more than a couple hours, even for a Rust novice. I would imagine one person could probably take care of the 39 popular crates within a month given an hour of work a day.
A group of ten community members could probably handle the ~300 crates with 5 or more dependents within a month.

And how about libraries that wish to maintain some MSRV?

I'll note this also for my next revision of the RFC. The only way I can think of making this work is for those libraries to gate code using the new range types behind a feature (maybe named after the rust version). Depending on the crate's stability policy, such a feature could even be enabled by default.

The only way I can think of making this work is for those libraries to gate code using the new range types behind a feature (maybe named after the rust version).

It sure would be nice if we could have conditional compilation based on whether a feature exists and is stable (or on the rust version).

It sure would be nice if we could have conditional compilation based on whether a feature exists and is stable (or on the rust version).

there is #2523 (= rust-lang/rust#64797).

It sure would be nice if we could have conditional compilation based on whether a feature exists and is stable (or on the rust version).

there is https://github.com/rust-lang/rfcs/pull/2523 (= https://github.com/rust-lang/rust/issues/64797).

And in the meantime, there is https://crates.io/crates/rustversion

If so, how much time do we expect that to take?

I wouldn't expect the changes to take more than a couple hours, even for a Rust novice. I would imagine one person could probably take care of the 39 popular crates within a month given an hour of work a day.

I'm not asking how many hours of work that requires, which is probably low indeed. I'm asking how much time do we think the ecosystem will need to adjust. This includes not only writing the patches, but also spreading awareness, having patches reviewed and merged, waiting for new (semver compatible) versions of libraries to be released, etc.
This can be especially tricky if, for example, many popular libraries with Range types in their API like to maintain a certain MSRV, or if some of those libraries are not regularly maintained. I don't know if that's the case, but those are important factors in whether adoption will take weeks vs months vs years.
I think this RFC can work, but I want to make sure there is a realistic plan for avoiding the situation where many Rust users that upgrade to the new edition will get noisy code because their dependencies haven't updated yet.
Related: this probably also means that cargo fix --edition should suggest/imply cargo update.

The Migrating proposes to use impl Into<...> as a fix for a function that takes Ranges by ownership. But I'm not seeing anything that deals with methods that take or return &mut Range.

The Migrating proposes to use impl Into<...> as a fix for a function that takes Ranges by ownership. But I'm not seeing anything that deals with methods that take or return &mut Range.

for RangeFrom and Range, but not RangeInclusive, we can use repr shenanigans to guarantee the old and new ranges have the same layout so you can convert &OldRange to &NewRange and vis versa losslessly.

I think a way forward could be to add the new range types as unstable to the standard library, so we can start on the detailed design of those types and their API. I don't think those types should be stabilized before we know what will happen with the related lang changes, but adding an unstable std::range module for now to work on something more concrete seems like it would be useful at this point.
(And perhaps there are good arguments for adding these types regardless of what happens with .. syntax. If there are, that might be useful to put in a libs-api ACP.)
For this RFC, I'd be happy to resolve my rfcbot concern if an unresolved question is added on whether the proposed migration is not too disruptive, and how to properly document and execute the ecosystem migration, so we make sure that is taken into consideration before stabilization.

We briefly discussed this in the libs-api meeting. We're happy to see a PR for adding std::range with the new range types as unstable. Feel free to open a libs-api tracking issue for the new range types.

implement IntoIterator directly (when Idx: Step)

If ranges are overhauled, should Step also be stabilized? Currently Ranges using a custom type is limited, given that Step is unstable. So Range<Foo> always require implementing a separate Iterator, e.g. struct FooRangeIter(Range<Foo>) with impl Iterator for FooRangeIter.
For instance chrono's iter_days() / NaiveDateDaysIterator could effectively simply be impl Step for NaiveDate.
Related: https://github.com/rust-lang/rust/issues/42168

Status update:
I was hoping to get back to this over the weekend but unfortunately didn't have time. I will try to get to it this week.

@rfcbot resolve No clean migration path.

The teams have accepted this RFC and it has now been merged.
Thanks to @pitaj for pushing this work forward, and thanks to all those who reviewed and contributed helpful feedback.
For further updates, follow the tracking issue:

https://github.com/rust-lang/rust/issues/123741