Skip to content

Conversation

@paldepind
Copy link
Contributor

This PR changes how async blocks are modeled in the CFG. Consider this example:

async fn main() {
    let say_godbye = async {
        println!("godbye");
    };
    let say_hello = async {
        println!("hello");
    };
    say_hello.await;
    say_godbye.await;
}

Currently async blocks are treated as regular blocks in the CFG. The control enters the block at when the block is encountered. But, in Rust futures are based on polling and they do nothing unless they are polled through await. This implies that async blocks aren't executed before it is awaited. Hence, in the above example "hello" is always printed after "godbye" since the order of the .awaits determine when the content of the async blocks are polled.

To model this, this PR changes the CFG s.t.

  • a separate CFG scope is created for each async block.
  • async blocks are stepped over when they are encountered.

In the SSA construction:

  • async blocks are handled like closures as they can capture variables.
  • .await expressions are treated like calls as they jump to the execution of an async block or function.

@github-actions github-actions bot added the Rust Pull requests that update Rust code label Nov 11, 2024
| unreachable.rs:301:13:301:32 | CallExpr | Call should have one enclosing callable but has 0. |
| unreachable.rs:306:5:306:18 | CallExpr | Call should have one enclosing callable but has 0. |
| unreachable.rs:308:5:308:18 | CallExpr | Call should have one enclosing callable but has 0. |
| unreachable.rs:310:5:310:18 | CallExpr | Call should have one enclosing callable but has 0. |
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These added inconsistencies are only because more nodes are now included in the CFG. Before the CFG would end in a loop in the for_ever async block. Now the block gets its own scope and the remaining code are the block is also in the CFG.

@paldepind paldepind force-pushed the rust-async-blocks branch 2 times, most recently from 7e78d64 to ef886de Compare November 11, 2024 08:28
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 work! Mostly some stylistic comments.


final class CfgScope = CfgScopeImpl;

class AsyncBlockScope extends CfgScopeImpl, BlockExpr {
Copy link
Contributor

Choose a reason for hiding this comment

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

If you add instance ExprTrees::AsyncBlockExprTree after the extends clause, then you can remove the charpred, and replace the this.(ExprTrees::AsyncBlockExprTree) terms with super.

async fn async_block_capture() {
let mut i: i64 = 0; // i
let block = async {
i = 1; // $ read_access=i
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this not a write_access?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oops. There was even testFailures in the .expected file that I missed 🙈


final class CfgScope = Scope::CfgScope;

predicate getEnclosingCfgScope = Scope::getEnclosingCfgScope/1;
Copy link
Contributor

Choose a reason for hiding this comment

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

We rarely expose classless predicates like this one; I think it would be better as a member predicate on AstNode, like getEnclosingCallable. We probably also want to cache it like with AsNode.getEnclosingCallable.

}

/** Holds if evaluating `e` jumps to the evaluation of a different CFG scope. */
predicate isControlFlowJump(Expr e) { e instanceof CallExprBase or e instanceof AwaitExpr }
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should just make this a (private) predicate inside SsaImpl.qll.

@paldepind
Copy link
Contributor Author

Thanks for the feedback 😄. The comments should be addressed now.

}

/** Gets the CFG scope that encloses this node, if any. */
CfgScope getEnclosingCfgScope() {
Copy link
Contributor

Choose a reason for hiding this comment

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

I still think we'd want 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.

Done.

Copy link
Contributor

Choose a reason for hiding this comment

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

Great. Let's finally do a DCA run before merging.

Copy link
Contributor

Choose a reason for hiding this comment

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

I have started one.

@hvitved
Copy link
Contributor

hvitved commented Nov 13, 2024

DCA looks good, so let's merge.

@hvitved hvitved merged commit 2bb5603 into github:main Nov 13, 2024
10 checks passed
@paldepind paldepind deleted the rust-async-blocks branch November 13, 2024 14:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Rust Pull requests that update Rust code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants