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

Rust: SSA additions #17786

Merged
merged 10 commits into from
Oct 29, 2024
Merged

Rust: SSA additions #17786

merged 10 commits into from
Oct 29, 2024

Conversation

paldepind
Copy link
Contributor

This PR makes two small improvements to the SSA:

  • Mutable variables that are captured are now supported. Immutable variables where already supported. I don't think allowing mutable variables as well pose much of a problem, as they don't introduce the same issues that mutable borrows do.

    The only change I made to support this was to insert pseudo reads upon exit from a closure where captured variables are written to, which I observed was done in the Ruby SSA implementation. There might be other necessary changes that I've missed, so please double check 🙏

  • Mutable variables are now only excluded from SSA when they are mutably borrowed (&mut) whereas before merely immutably borrowing them (&) excluded them. I don't think immutable borrows pose any problems for the SSA construction.

@paldepind paldepind requested a review from hvitved October 16, 2024 13:16
@github-actions github-actions bot added the Rust Pull requests that update Rust code label Oct 16, 2024
Copy link
Contributor

@hvitved hvitved left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great to have the SSA analysis apply to more cases!

We need a bit more work, though, to make the analysis sound for captured mutable variables.

| variables.rs:412:9:412:16 | closure1 | variables.rs:412:9:412:16 | closure1 | variables.rs:415:5:415:12 | closure1 |
| variables.rs:412:20:414:5 | <captured entry> x | variables.rs:410:13:410:13 | x | variables.rs:413:19:413:19 | x |
| variables.rs:418:9:418:13 | y | variables.rs:418:13:418:13 | y | variables.rs:424:15:424:15 | y |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line illustrates the problem with allowing for captured mutable variables: The test snippet is

    let mut y = 2; // y [line 418]
    // Captures mutable value by mutable reference
    let mut closure2 = || {
        y = 3; // $ write_access=y
    };
    closure2(); // $ read_access=closure2
    print_i64(y); // $ read_access=y [line 424]

It is not true that the value written at line 418 is the one read at line 424, because the value is updated in the call to closure2.

One way that we can fix it is to say that mutable captured variables may be read/written via any call, by adding uncertain pseudo reads and writes at all call nodes. In Ruby this is done in the predicates
capturedCallRead and capturedCallWrite, respectively.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense and I did not think of that. I'll look at the Ruby approach and try an adapt it.

@@ -207,14 +207,32 @@ private predicate lastRefSkipUncertainReadsExt(DefinitionExt def, BasicBlock bb,
)
}

/** Holds if `bb` contains a captured access to variable `v`. */
private VariableAccess getCapturedVariableAccess(BasicBlock bb, Variable v) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: getACapturedVariableAccess.

this.isMutable()
implies
not exists(VariableAccess va | va = this.getAnAccess() |
exists(RefExpr re | va = re.getExpr() and re.isMut())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I prefer va = any(RefExpr re | re.isMut()).getExpr()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. That is definitely nicer.

@paldepind
Copy link
Contributor Author

paldepind commented Oct 28, 2024

I've updated the PR to address comment and to handle captured variables in the same way as in Ruby. This fixes the error that was previously pointed out.

I think the way Ruby detects when a call can write/read might have a small soundness problem. Consider this example:

fn mutable_capture_and_write_in_a_sibling_closure() {
    let mut y = 2; // y
    let mut closure1 = || { y = 3; };
    let mut closure2 = || {
        y = 2; // (1): SSA definition for `y`
        closure1(); // no SSA definition for `y` here
        print_i64(y); // (2): reads from (1) which is not correct
    };
    closure2();
    print_i64(y);
}

Here the read at (2) reads what was written by closure1 on the line above, but the SSA analysis doesn't see that closure1 can write to y. This is because hasVariableReadWithCapturedWrite only holds for calls when there is a future read and a captured write in a nested CFG scope of the read's CFG scope. In the example the read is at (2) but the write is in closure1 which not nested inside (2)s CFG scope.

The above example doesn't actually compile due to Rust's borrow checker (which I didn't realize at first). When one closure (closure1 in this case) has mutably borrowed a variable it's not possible to use the variable in any other closure. I haven't been able to reproduce the issue in code that actually compiles. But I think it should be possible to port the example to Ruby and see the issue there.

To fix the above and improve precision, we could say that a call might write to a variable v if

  1. there exists a closure cl that captures and writes to v
  2. there is path from cl to the call through the CFG and through nested CFG scopes.

This should fix the above problem and also make the check more control flow sensitive in examples such as the following:

fn mutable_capture_further_down_in_the_cfg() {
    let mut y = 2;
    print_i64(y); // (1): This call can not write to `y` as the capturing closure is not yet in scope
    let mut closure = || { y = 3; // $ write_access=y };
    closure();
    print_i64(y);
}

In this example the current analysis will conclude that the call at (1) can write to y even though closure is not yet declared. I'm not sure how much value there is in doing that, but I don't think it would be very hard. I could try it out in a follow up PR.

@paldepind paldepind added the no-change-note-required This PR does not need a change note label Oct 28, 2024
Copy link
Contributor

@hvitved hvitved left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few minor comments, otherwise LGTM. We should also do a DCA run before merging.

*/
pragma[noinline]
private predicate hasCapturedWrite(Variable v, Cfg::CfgScope scope) {
any(VariableWriteAccess write | write.getVariable() = v and scope = write.getEnclosingCallable*())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be getEnclosingCallable+.

*/
pragma[noinline]
private predicate hasCapturedRead(Variable v, Cfg::CfgScope scope) {
any(VariableReadAccess read | read.getVariable() = v and scope = read.getEnclosingCallable*())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same

* that writes captured variable `v`.
*/
cached
predicate capturedCallWrite(CallExprBase call, BasicBlock bb, int i, Variable v) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be no need to cache this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I just remove the cached annotation or should I also move it out of the Cached module?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Ruby equivalent is also cached. Is there something different about that one that causes it to need caching?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It needs to be moved out, otherwise the compiler will complain (all public declarations in a cached module must be marked as cached).

The reason why it is cached in Ruby is that it is referenced in a user-visible class.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, thanks. I've addressed the comments :)

Copy link
Contributor

@hvitved hvitved left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Let's wait for DCA (I have started a run) before merging.

@paldepind
Copy link
Contributor Author

DCA looks fine to me.

@hvitved hvitved merged commit 7ddc8f0 into github:main Oct 29, 2024
14 checks passed
@paldepind paldepind deleted the rust-saa-additions branch October 29, 2024 09:58
@paldepind
Copy link
Contributor Author

It's not very important right now, but I wonder what you think about #17786 (comment) @hvitved?

@hvitved
Copy link
Contributor

hvitved commented Nov 1, 2024

It's not very important right now, but I wonder what you think about #17786 (comment) @hvitved?

I think you are right that it may be unsound, but it is probably not something that is likely to happen often in practice.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
no-change-note-required This PR does not need a change note Rust Pull requests that update Rust code
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants