-
Notifications
You must be signed in to change notification settings - Fork 6.3k
Control flow side effects of user defined functions #12082
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
Conversation
|
This is working now apart from recursive function calls. I'm actually wondering if it would be better to finish implementing |
|
Ok actually I think ControlFlowBuilder cannot be really used, but essentially something that actually builds a graph. |
d908dd9 to
6a6ed4e
Compare
| // not only for builtins. | ||
| if (auto const* funCall = get_if<FunctionCall>(&_exprStmt.expression)) | ||
| if (BuiltinFunction const* builtin = m_dialect.builtin(funCall->functionName.name)) | ||
| if (builtin->controlFlowSideEffects.terminates) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ekpyron the above comment can now be implemented.
6a6ed4e to
d5a397a
Compare
|
There was an error when running Please check that your changes are working as intended. |
d5a397a to
6220621
Compare
6220621 to
a1756ae
Compare
libyul/ControlFlowSideEffects.h
Outdated
| #include <stack> | ||
| #include <optional> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All these includes are unnecessary, aren't they? Even set.
| { | ||
| walkVector(_functionCall.arguments | ranges::views::reverse); | ||
| newConnectedNode(); | ||
| m_currentNode->functionCall = {_functionCall.functionName.name}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| m_currentNode->functionCall = {_functionCall.functionName.name}; | |
| m_currentNode->functionCall = _functionCall.functionName.name; |
The braces are a bit misleading for the optional type.
| ScopedSaveAndRestore breakNode(m_break, nullptr); | ||
| ScopedSaveAndRestore continueNode(m_continue, nullptr); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe not if we assume the hoister, but better safe than sorry.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, okay. We could assert that m_continue and m_break are nullptr if we really want to be extra safe. Same with m_leave and m_break.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should either assert or save and restore. What would you prefer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer the assert.
| struct ControlFlowNode | ||
| { | ||
| std::vector<ControlFlowNode const*> successors; | ||
| std::optional<YulString> functionCall; | ||
| }; | ||
|
|
||
| struct FunctionFlow | ||
| { | ||
| ControlFlowNode const* entry; | ||
| ControlFlowNode const* exit; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could use some documentation.
| /// If true, the function contains at least one reachable branch that reverts. | ||
| bool canRevert = false; | ||
| /// If true, the function has a regular outgoing control-flow. | ||
| bool canContinue = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this can be replaced by willTerminate and that sounds more appropriate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure. These three bools are essentially edges to special nodes in the control-flow graph, and I would prefer not to invert the meaning of one of them.
Also canContinue can be false due to the function being always recursive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also canContinue can be false due to the function being always recursive.
This is not currently done, is it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| while (it != end) | ||
| { | ||
| ControlFlowNode const* node = *it; | ||
| if (!node->functionCall || exitKnownReachable(*node->functionCall)) | ||
| { | ||
| m_pendingNodes[_functionName].erase(it); | ||
| return node; | ||
| } | ||
| ++it; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be replaced by std::find, followed by an erase.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like this? Would you prefer it?
auto it = m_pendingNodes[_functionName].begin();
auto end = m_pendingNodes[_functionName].end();
it = find_if(it, end, [](ControlFlowNode const* node) {
return !node->functionCall || exitKnownReachable(*node->functionCall);
}
if (it == end)
return nullptr;
else
{
ControlFlowNode const* node = *it;
m_pendingNodes[_functionName].erase(it);
return node;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes :) But it did look better when I thought about the change.
179ca43 to
35b5a68
Compare
| ScopedSaveAndRestore breakNode(m_break, nullptr); | ||
| ScopedSaveAndRestore continueNode(m_continue, nullptr); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, okay. We could assert that m_continue and m_break are nullptr if we really want to be extra safe. Same with m_leave and m_break.
| flow.exit = newNode(); | ||
| m_currentNode = newNode(); | ||
| flow.entry = m_currentNode; | ||
| newConnectedNode(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this might be redundant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you are right.
| void ControlFlowBuilder::operator()(Leave const&) | ||
| { | ||
| m_currentNode->successors.emplace_back(m_leave); | ||
| m_currentNode = newNode(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if this is really needed. Effectively, edges coming from here doesn't matter, I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we discussed this offline but if you don't disconnect the node, then function f() { leave revert(0, 0) } will have can revert. With dead code removal this might not matter, but I think it is still good to have.
| while (progress) | ||
| { | ||
| progress = false; | ||
| for (auto const& fun: m_pendingNodes) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This may lead to undefined behaviour. The call procesFunction(...) can update m_pendingNodes in two different ways:
nextProcessableNode(...)can remove the current iterator; wouldn't that lead to undefined behaviour when the current one gets incremented?recordReachabilityAndQueuecan add to the beginning ofm_pendingNodes. Why is it guaranteed that this node will get processed?
Would it be more appropriate to do something like while(!m_pendingNodes.empty()) { node = m_pendingNodes.front(); ...}
Also, otherwise could change to m_pendingNodes | range::views::keys)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I follow, I think this might be a misunderstanding on your part: m_pendingNodes is a map by function name - so there is one queue per function name. As long as we don't add or remove a function, the iterator here is not invalidated. I can split recordReachabilityAndQueue into two functions:
- one that can also add a new function
- one that asserts that the function is already there
and use the first one above in line 171 and the second one in line 232
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the change. Was a misunderstanding.
| if (m_functionCalls.count(_function)) | ||
| for (YulString callee: m_functionCalls.at(_function)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could replace the .count followed by .at by valueOrDefault from CommonData.h
| std::map<YulString, ControlFlowSideEffects> m_functionSideEffects; | ||
| std::map<YulString, std::list<ControlFlowNode const*>> m_pendingNodes; | ||
| std::map<YulString, std::set<ControlFlowNode const*>> m_processedNodes; | ||
| std::map<YulString, std::set<YulString>> m_functionCalls; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could use documentation. Wasn't obvious that this is a set of reachable function calls inside a function.
c2bd1d2 to
dc4e951
Compare
|
Some commits are left for squashing, can be merged afterwards. |
95b68f0 to
2c2269d
Compare
No description provided.