Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Have for pattern in iter not borrow the iterator #8372

Closed
SimonSapin opened this issue Aug 7, 2013 · 36 comments
Closed

Have for pattern in iter not borrow the iterator #8372

SimonSapin opened this issue Aug 7, 2013 · 36 comments
Labels
P-low Low priority

Comments

@SimonSapin
Copy link
Contributor

Test case:

let mut iter = range(1, 10);
for i in iter {
    printfln!(i);
    // error: cannot borrow `iter` as mutable more than once at a time
    printfln!(iter.next());
}

The above currently fails, but I think it should be valid and equivalent to:

let mut iter = range(1, 10);
loop { match iter.next() { None => break, Some(i) => {
    printfln!(i);
    printfln!(iter.next());
}}

In other words, the for loop should only borrow the iterator long enough to call next(), leaving the loop body free to use it.

I understand that this might not be possible when a more complex expression is used as the iterator. The for loop needs to evaluate the expression once and keep the result in an anonymous variable, thus borrowing. Maybe there could be a special case when the iterator expression is "simple" enough: a variable, struct field access, etc?

@thestinger
Copy link
Contributor

This means implementing it as more than a simple parser feature. It's not possible to check the type of the passed in variable at the moment and decide whether a temporary is needed.

@huonw
Copy link
Member

huonw commented Aug 7, 2013

@thestinger for is likely to need to be a non-parser feature anyway: it is currently duck-typed, and gives horrible error messages. (As I'm sure you know.)

@SimonSapin
Copy link
Contributor Author

@thestinger what you say might still be true, but this is the type of AST node (eg. identifier) for the iterator, not Rust type (eg. std::vec::VecIterator.)

@SimonSapin
Copy link
Contributor Author

Also, this is a "nice to have", but not blocking because I can just use loop with match. I even have a macro for it:

// Work around "error: cannot borrow `iter` as mutable more than once at a time"
// when using a normal for loop.
macro_rules! for_iter(
    ($iter: expr, $pattern: pat, $loop_body: expr) => (
        loop {
            match $iter.next() { None => break, Some($pattern) => $loop_body }
        }
    );
)

@bluss
Copy link
Member

bluss commented Aug 7, 2013

Your macro should probably take an ident, not an expression since it doesn't work well with expressions!

@SimonSapin
Copy link
Contributor Author

Good point, @blake2-ppc.

// Work around "error: cannot borrow `iter` as mutable more than once at a time"
// when using a normal for loop.
macro_rules! for_iter(
    ($iter: ident, $pattern: pat, $loop_body: expr) => (
        loop {
            match $iter.next() { None => break, Some($pattern) => $loop_body }
        }
    );
)

@huonw
Copy link
Member

huonw commented Nov 15, 2013

triage, no progress.

@pnkfelix
Copy link
Member

It would be good to decide what we're going to do about these things. (This and #8649)

But its not clear whether it should block 1.0.

@nikomatsakis
Copy link
Contributor

cc me

@ezyang
Copy link
Contributor

ezyang commented Dec 17, 2013

Maybe there could be a special case when the iterator expression is "simple" enough: a variable, struct field access, etc?

It seems to me that there is precedent for language constructs to act specially when the expression in question is an lvalue, c.f. match; that would be a natural choice here.

@emberian
Copy link
Member

Another painful example:

fn main() {
    let s = "foobar bazbang";
    let mut chars = s.chars();

    loop {
        match chars.next() {
            Some(c) => {
                if c == ' ' {
                    chars.next();
                } else {
                    print!("{}", c);
                }
            },
            None => { break; }
        }
    }
}

is good, but:

fn main() {
    let s = "foobar bazbang";
    let mut chars = s.chars();

    for c in chars {
        if c == ' ' {
            chars.next();
        } else {
            print!("{}", c);
        }
    }
}

is not.

@bluss
Copy link
Member

bluss commented Jun 11, 2014

Is it possible to migrate to a for-by-value by recommending users who need the reference behaviour to use Iterator::by_ref() ?

@huonw
Copy link
Member

huonw commented Jun 11, 2014

@blake2-ppc could you write the expansion you are thinking of? (I'm specifically wondering how that would allow for something like for x in foo.iter() { ... }, while also allowing for x in bar { bar.next() } to do the right thing.)

@pczarn
Copy link
Contributor

pczarn commented Jul 30, 2014

Solved by #15809

@SimonSapin
Copy link
Contributor Author

@pczarn, indeed it is, thanks for the heads up! Updated for language/lib changes, the initial test case now works as expected:

fn main() {
    let mut iter = range(1i, 10);
    for i in iter {
        println!("{}", i);
        println!("{}", iter.next());
    }
}
1
Some(2)
3
Some(4)
5
Some(6)
7
Some(8)
9
None

SimonSapin added a commit to SimonSapin/rust that referenced this issue Jul 30, 2014
alexcrichton pushed a commit to alexcrichton/rust that referenced this issue Jul 31, 2014
alexcrichton added a commit to alexcrichton/rust that referenced this issue Jul 31, 2014
Closes rust-lang#16097 (fix variable name in tutorial)
Closes rust-lang#16100 (More defailbloating)
Closes rust-lang#16104 (Fix deprecation commment on `core::cmp::lexical_ordering`)
Closes rust-lang#16105 (fix formatting in pointer guide table)
Closes rust-lang#16107 (remove serialize::ebml, add librbml)
Closes rust-lang#16108 (Fix heading levels in pointer guide)
Closes rust-lang#16109 (rustrt: Don't conditionally init the at_exit QUEUE)
Closes rust-lang#16111 (hexfloat: Deprecate to move out of the repo)
Closes rust-lang#16113 (Add examples for GenericPath methods.)
Closes rust-lang#16115 (Byte literals!)
Closes rust-lang#16116 (Add a non-regression test for issue rust-lang#8372)
Closes rust-lang#16120 (Deprecate semver)
Closes rust-lang#16124 (Deprecate uuid)
Closes rust-lang#16126 (Deprecate fourcc)
Closes rust-lang#16127 (Remove incorrect example)
Closes rust-lang#16129 (Add note about production deployments.)
Closes rust-lang#16131 (librustc: Don't ICE when trying to subst regions in destructor call.)
Closes rust-lang#16133 (librustc: Don't ICE with struct exprs where the name is not a valid struct.)
Closes rust-lang#16136 (Implement slice::Vector for Option<T> and CVec<T>)
Closes rust-lang#16137 (alloc, arena, test, url, uuid: Elide lifetimes.)
pcwalton added a commit to pcwalton/rust that referenced this issue Sep 8, 2014
itself.

This breaks code like:

    for &x in my_vector.iter() {
        my_vector[2] = "wibble";
        ...
    }

Change this code to not invalidate iterators. For example:

    for i in range(0, my_vector.len()) {
        my_vector[2] = "wibble";
        ...
    }

The `for-loop-does-not-borrow-iterators` test for rust-lang#8372 was incorrect
and has been removed.

Closes rust-lang#16820.

[breaking-change]
@SimonSapin
Copy link
Contributor Author

Re-opening, see #17101.

@huonw
Copy link
Member

huonw commented Sep 13, 2014

The implementation we had was incorrect and memory unsafe.

@mahkoh
Copy link
Contributor

mahkoh commented Sep 13, 2014

That's what it says in #17101.

@pnkfelix
Copy link
Member

@mahkoh I do not think your macro properly captures what people expect from for-loops.

In particular, I ported the example from #16820 over to the macro above, and (even after generalizing it to take a pat and an expr instead of two idents), you can see a clear problem: http://is.gd/ZeRT1O
Namely, the expression for the iterator is re-evaluated on each iteration through the loop. You did not observe this in your version because you had restricted your version to take an ident.

But then if you fix that problem in the "obvious" manner, you are back where you started: http://is.gd/3EQAly

This is the problem that one must solve in some manner, the one that I referred to in my comment here: #17101 (comment)

@mahkoh
Copy link
Contributor

mahkoh commented Sep 13, 2014

@pnkfelix: I chose ident over expr precisely to avoid this re-evaluation problem. This issue is about iterators that are stored in identifiers and not arbitrary rvalue iterators.

I don't know how the for loop works, but if it simply rewrites the AST so that it looks like a loop + match, then it could rewrite it to the version in my macro above if the iterator is an identifier and it could rewrite it to a version with a temporary variable otherwise.

@SimonSapin
Copy link
Contributor Author

I’d expect the for loop to have a special case when the iterator expression can be re-evaluated without side effects (e.g. an ident, a field access, etc.) and behave like the macro. Only when necessary should the loop have a temporary to avoid re-evaluation.

@huonw
Copy link
Member

huonw commented Sep 14, 2014

Yeah definitely, the idea is sound, the implementation just wasn't actually implementing that idea.

(I have to admit, I'm confused why @mahkoh decided to ask "why was this removed?" despite acknowledging that they knew the reason was the implementation was wrong. In any case, the situation we have now is just a temporary revert back to the old behaviour that we had when for was a parser feature, i.e. there was only a narrow window (9 days) in which this was "fixed", and so I wouldn't expect this to really break that much.)

@mahkoh
Copy link
Contributor

mahkoh commented Sep 14, 2014

From the comments in this thread it looks like this worked from July 30th until at least September 5th. I don't see where you get the 9 days from. And I don't understand why the rvalue bug had to be fixed so urgently that this case had to be removed with it. Even simply checking if the "iterator" is a single identifier would have kept the cases discussed in this issue working. I don't know if the old version was also unsafe in that case, but if not, then simply adding an if-block around the code added by @pcwalton should do the trick, no?

@huonw
Copy link
Member

huonw commented Sep 14, 2014

(Oh, whoops, read those dates wrong; even so, only 40 days, and we had lived with this for nearly a year before that.)

Being memory safe and correct is very urgent: as it stood, our for loops allowed for iterator invalidation in safe code. Squashing those problems is significantly more important than relatively minor features like this, which has perfectly good synonyms, as your macro demonstrates and as everyone who met this before July 30th knew. In fact the replacement is just 2 lines: for pat in iter { becomes

loop {
    let pat = match iter.next() { Some(x) => x, None => break };

This doesn't seem particularly burdensome compared to 'have an incorrect compiler'.

The fixing-the-horrible-bug patch is being conservative, but it is has the rather large advantage of being correct (and is clearly so).

@TyOverby
Copy link
Contributor

Am I correct in my assumption that this could be fixed very easily with a syntax extension? I don't see how something so simple that macro_rules could solve it could lead to an "incorrect compiler".

@thestinger
Copy link
Contributor

It's simple to write a syntax extension for lvalues where it doesn't borrow the iterator within the body of the loop. It does have to borrow the iterator temporarily at the top of the loop body to call next. The compiler's borrow checking rules would need to gain more understanding of the for construct since it's all wired into the compiler.

@SimonSapin
Copy link
Contributor Author

@TyOverby Sure, this can be done with macro_rules! (see earlier in this thread). So can every for loop, but that doesn’t mean we should remove for from the language.

It’s unfortunate that we have to switch to an entirely different syntactic construct for something this semantically is a for loop, just because the current implementation of for borrows too conservatively.

@Stebalien
Copy link
Contributor

You can now just use while let:

while let Some(item) = iterator.next() {
    // ...
}

It's not perfect but but better than using a macro (IMHO).

@SimonSapin
Copy link
Contributor Author

Oooh, this is much nicer indeed. In particular since there is only one nesting level of {}. Thanks for the tip!

@smosher
Copy link

smosher commented Dec 19, 2014

As far as blocking 1.0 goes, it won't bother me personally if this doesn't get fixed until after. But it might be confusing and offputting to newcomers who already have a lot to learn about ownership. (I know I've invented a few new things to say after "why the ...?" beacuse of this issue.)

Might it be possible to detect the situation and emit a hint about the workaround?

@nikomatsakis
Copy link
Contributor

Now that while let is an option, I am not sure if this should be
fixed. Particularly since for was defined to invoke IntoIterator
on its argument -- and thus in a sense takes ownership of its argument
(though not quite, is that a backwards compat hazard @aturon?). If you
plan to mess with the iterator itself during iteration, it seems to me
that using a distinct kind of loop is a good indication of this (I
certainly don't expect it when I see a for loop).

On Thu, Dec 18, 2014 at 07:46:54PM -0800, Stephen Mosher wrote:

As far as blocking 1.0 goes, it won't bother me personally if this doesn't get fixed until after. But it might be confusing and offputting to newcomers who already have a lot to learn about ownership. (I know I've invented a few new things to say after "why the ...?" beacuse of this issue.)

Might it be possible to detect the situation and emit a hint about the workaround?


Reply to this email directly or view it on GitHub:
#8372 (comment)

@emberian
Copy link
Member

I used to be annoyed about this a ton, but now that we have while let,
I agree.

On Sat, Dec 20, 2014 at 9:08 PM, Niko Matsakis notifications@github.com
wrote:

Now that while let is an option, I am not sure if this should be
fixed. Particularly since for was defined to invoke IntoIterator
on its argument -- and thus in a sense takes ownership of its argument
(though not quite, is that a backwards compat hazard @aturon?). If you
plan to mess with the iterator itself during iteration, it seems to me
that using a distinct kind of loop is a good indication of this (I
certainly don't expect it when I see a for loop).

On Thu, Dec 18, 2014 at 07:46:54PM -0800, Stephen Mosher wrote:

As far as blocking 1.0 goes, it won't bother me personally if this
doesn't get fixed until after. But it might be confusing and offputting to
newcomers who already have a lot to learn about ownership. (I know I've
invented a few new things to say after "why the ...?" beacuse of this
issue.)

Might it be possible to detect the situation and emit a hint about the
workaround?


Reply to this email directly or view it on GitHub:
#8372 (comment)


Reply to this email directly or view it on GitHub
#8372 (comment).

http://octayn.net/

@SimonSapin
Copy link
Contributor Author

I still kinda wish this was fixed, but I understand @nikomatsakis’s argument and I don’t mind as much with the while let alternative.

@japaric
Copy link
Member

japaric commented Jan 31, 2015

#20790 implemented the new for-loops that take the iterator by value, so this issue can't be fixed (without changing the semantics in a backwards incompatible way). This should be closed.

@SimonSapin
Copy link
Contributor Author

Fair enough. I’m happy with the while let Some(value) = iterator.next() { trick.

fhartwig added a commit to fhartwig/rust-clippy that referenced this issue Oct 22, 2015
fhartwig added a commit to fhartwig/rust-clippy that referenced this issue Oct 22, 2015
fhartwig added a commit to fhartwig/rust-clippy that referenced this issue Oct 26, 2015
nivkner added a commit to nivkner/rust that referenced this issue Mar 17, 2018
flip1995 pushed a commit to flip1995/rust that referenced this issue Feb 10, 2022
make unwrap_used also trigger on .get().unwrap()

fixes rust-lang#8124
changelog: make the [unwrap_used] lint trigger for code of the form such as `.get(i).unwrap()` and `.get_mut(i).unwrap()`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
P-low Low priority
Projects
None yet
Development

No branches or pull requests