Repository navigation
put guards' fake reads in the failure block and only put necessary fake reads in the success block - #163948
put guards' fake reads in the failure block and only put necessary fake reads in the success block#163948dianne wants to merge 2 commits into
Conversation
|
dealing with deref patterns is a tiny bit messy, but hopefully it's not too bad? this isn't as efficient as it maybe could be, but I expect it's not a bottleneck. (edit: it indeed seems like the perf impact is negligible) I'll also lang-nominate this with all the others. |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
put guards' fake reads in the faillure block and only put necessary fake reads in the success block
This comment has been minimized.
This comment has been minimized.
|
Finished benchmarking commit (e4cc928): comparison URL. Overall result: ✅ improvements - no action neededBenchmarking means the PR may be perf-sensitive. Consider adding rollup=never if this change is not fit for rolling up. @rustbot label: -S-waiting-on-perf -perf-regression Instruction countOur most reliable metric. Used to determine the overall result above. However, even this metric can be noisy.
Max RSS (memory usage)Results (secondary 0.8%)A less reliable metric. May be of interest, but not used to determine the overall result above.
CyclesResults (primary -2.5%, secondary 5.0%)A less reliable metric. May be of interest, but not used to determine the overall result above.
Binary sizeThis perf run didn't have relevant results for this metric. Bootstrap: 489.893s -> 491.696s (0.37%) |
|
To summarize: for soundness, we add fake shallow borrows and fake reads in Obviously we need to add the fake reads after guard failure. But this problem raises the question of what we're trying to express, fundamentally. We asked @dianne to explore whether we should forbid mutation of the relevant places anywhere in guards, as a possible way to simplify the mental model. @dianne tried that in #162242. But this turns out to not be so simple. The fake reads necessary for this also prevent moving values during early exits. This breaks reasonable-looking code, found by crater. That's enough to convince me we shouldn't do that, as it would be in tension with the intuitions that people pick up, due to NLL, about how borrows work in Rust otherwise. Niko was also persuaded. This suggests another model, which @dianne set out in #162242 (comment): adopt precise semantics that place the borrows and the reads so as to achieve articulable language properties. What are those properties?
Upholding those properties suggests that we (fake) borrow each place the That's what this PR does. This is both a breaking change for the soundness fix (we're not expecting ecosystem breakage) and a change that accepts new code (due to being more precise about the reads after guard success). Let's (propose to) do it. @rfcbot fcp merge lang (Cc @rust-lang/types, @rust-lang/opsem. If you want in on this FCP (#161581 (comment)), no worries, say so and I'll restart.) |
This comment was marked as resolved.
This comment was marked as resolved.
|
r? @matthewjasper or @Nadrieril or anyone familiar with fake borrows and deref patterns |
|
Some changes occurred in match lowering cc @Nadrieril |
same reasoning as in #161857 (comment) 😊 |
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
|
@rfcbot fcp merge lang,types |
|
@traviscross has proposed to merge this. The next step is review by the rest of the tagged team members:
Concerns:
Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up! cc @rust-lang/lang-advisors: FCP proposed for lang, please feel free to register concerns. |
|
On the mental model, I want to double check a property I expect to be true: The success or failure of each arm's pattern should be independent of the other arms; it should never be allowed to change based on code in an earlier arm's guard. This might be implied by TC's point 3 about "how the compiler orders tests", but I'm not sure whether that means within an arm or globally within the match. |
|
That's definitely not true when considering interior mutability. |
Yes, that's the property we'd like, and in fact it's necessary for soundness: if a guard can change the outcomes of further tests within the match, then exhaustiveness checking is no longer enough to ensure at least one arm of the match will succeed.
Even with just raw pointers you can invalidate the property above, but not in safe code: there is currently no way for a pattern to inspect data that's inside an interior mutable container. |
Yeah but two guards can still influence each other that way. Re-reading what Tyler wrote I guess I am not sure whether "The success or failure of each arm's pattern" includes the guard; I thought it would. |
Oh, I understood "pattern" to not include the guard. If we include the guards, then you don't even need interior mutability (play): fn main() {
let mut x = 0;
match true {
true if { x += 1; false} => unreachable!(),
true if x == 1 => {}
_ => unreachable!(),
}
} |
|
Oh right I forgot guards can mutate outside variables.
That's only true outside the module that defines |
|
Yes, I was excluding guards in my statement above. It seems like we are in agreement about the model here, so @rfcbot reviewed |
|
@rfcbot reviewed |
|
Thanks for exploring so many directions here, dianne! @rfcbot reviewed |
There was a problem hiding this comment.
I'm trying to compare with #161581, but tests are largely different.
Can we land tests separately (at least as a separate commit, but ideally as a separate PR that each of these candidate changes are based on)? That way we can see how each PR modifies them.
| match x { | ||
| // By-ref bindings are created before evaluating the guard, so we don't need fake borrows | ||
| // to catch when they would be invalidated. As long as we don't use `b` after, it's ok. | ||
| (Some(ref b), c) if { x.0 = None; false } || return => {} |
There was a problem hiding this comment.
I am missing an example like
let mut x: Option<&u64> = Some(&7);
match x {
Some(ref b) if { x = None; false } || return => {}
_ => {}
}Does this pass?
| //~^ ERROR: cannot assign `x` in match guard | ||
| Some(_) if false || ({ x = None; false } && return) => {} | ||
| //~^ ERROR: cannot assign `x` in match guard | ||
|
|
There was a problem hiding this comment.
Some(_) if { x = None; false } || return => {} and Some(_) if false && ({ x = None; false } || return) => {} are missing, which is in #161581.
|
@rustbot concern details-and-motivation I am reading over the PR description, the changes, and the tests - and I am still not following on exactly the principles trying to be upheld here in terms how and when guard blocks invalidate loans from the match arm patterns. TC's comment in #163948 (comment) sort of does this:
But this very much lacks the technical details of how we do that. As I said in my review comments, tests here don't line up with tests in #161581 and #162242, which makes it harder to evaluate these changes against each other. I would also expect a test that covers the exact crater regression we saw in #162242. @rust concern complexity I understand the desire for extra preciseness of this PR compared to #161581. As said above, I'm not even really clear what exactly the extra preciseness in this PR even gives us. Nonetheless, I'm not convinced that the extra preciseness here is warranted compared to the added complexity. I would much rather land #161581 to fix the unsoundness, then maybe later add extra preciseness here. I will also say: I am very frustrated with the process both here and in #161857. This is very clearly in T-types territory - both the implementation and the logic here is extremely subtle. I don't necessarily agree that T-lang should be involved here, but also don't particularly care from a T-types perspective if the team wants to be (as a lang advisor, I think the team should be leaning into its delegation to T-types, not away from it). HOWEVER, like I said this is clearly something that T-types must be involved in, and the fact that both FCPs here and there started without T-types at first is imo pretty demeaning and distasteful. |
|
Error: Cannot set Please file an issue on GitHub at triagebot if there's a problem with this bot, or reach out on #triagebot on Zulip. |
View all comments
This is #161581 (rebased onto a fresher main branch) but with an additional commit relaxing fake reads of fake borrows on guards' success branches: on guard success, we only keep alive the fake borrows required to ensure we can create that arm's by-value bindings. This is unlikely to come up in practice, but it permits a more compelling explanation of why we prevent mutation of matched-on places in guards.
This is now allowed; none of the guards can fail after assigning to
x.0:Since this includes the change made by #161581, it fixes #161578